incanto 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/2d.d.ts +74 -6
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +174 -16
- package/dist/3d.js +4 -4
- package/dist/{audio-player-DqUR3XFs.d.ts → audio-player-DkBqRTs4.d.ts} +1 -1
- package/dist/{behavior-BAQq7HGM.d.ts → behavior-CWhW3oa6.d.ts} +54 -0
- package/dist/{create-game-CZHROKcT.js → create-game-BFESHv1X.js} +37 -25
- package/dist/{create-game-CMS7DiPi.js → create-game-D8UvwXme.js} +231 -8
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-DP2WPYom.js → duplicate-BEvGBtb_.js} +1 -1
- package/dist/{gameplay-Ccruc3Wd.js → gameplay-CDFgSG6z.js} +293 -24
- package/dist/gameplay.d.ts +113 -2
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +139 -5
- package/dist/index.js +59 -6
- package/dist/{loader-CGs_G-r0.js → loader-B4OEXDZ8.js} +60 -0
- package/dist/{loader-Mo0KghCv.d.ts → loader-D2u0fVW2.d.ts} +1 -1
- package/dist/net.d.ts +1 -1
- package/dist/net.js +1 -1
- package/dist/{particle-sim-DYuSUxvK.js → particle-sim-CFkILGwh.js} +84 -3
- package/dist/{particle-sim-CbN4YUuH.d.ts → particle-sim-CwJ5rI_P.d.ts} +3 -0
- package/dist/{physics-2d-KuMWPTf6.js → physics-2d-KXn1N1jF.js} +95 -10
- package/dist/{physics-3d-CXOBOUKG.js → physics-3d-C58oQzTX.js} +121 -43
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-B0gq63VW.js → register-C35HDZpm.js} +2 -2
- package/dist/{register-DPEV9_9t.js → register-D71C4rDC.js} +76 -5
- package/dist/{register-BuUV1_KB.js → register-Dl_ixIJe.js} +275 -2
- package/dist/{register-02aSywx2.js → register-DvPHJdVj.js} +390 -59
- package/dist/test.d.ts +2 -2
- package/dist/test.js +10 -10
- package/editor/assets/{agent8-DUVZGcuO.js → agent8-DruKhR22.js} +1 -1
- package/editor/assets/index-D2qufqUo.js +7417 -0
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +847 -133
- package/skills/incanto-3d-models.md +24 -1
- package/skills/incanto-building-3d-games.md +35 -3
- package/skills/incanto-gameplay-behaviors.md +60 -0
- package/skills/incanto-hud.md +58 -0
- package/skills/incanto-node-reference.md +113 -0
- package/skills/incanto-physics-and-input.md +47 -0
- package/editor/assets/index-DsM2Yp9F.js +0 -7365
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { _ as registerBehavior, n as loadScene } from "./loader-
|
|
3
|
-
import { a as Engine } from "./particle-sim-
|
|
2
|
+
import { _ as registerBehavior, n as loadScene } from "./loader-B4OEXDZ8.js";
|
|
3
|
+
import { a as Engine } from "./particle-sim-CFkILGwh.js";
|
|
4
4
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
5
|
-
import {
|
|
5
|
+
import { s as AudioPlayer } from "./register-Dl_ixIJe.js";
|
|
6
6
|
import { i as resolveRendering, n as attachTouchControls } from "./touch-031PxtCR.js";
|
|
7
|
-
import { n as registerGameplayBehaviors } from "./gameplay-
|
|
7
|
+
import { n as registerGameplayBehaviors } from "./gameplay-CDFgSG6z.js";
|
|
8
8
|
import { t as debugSources } from "./debug-draw-CZmOYjL2.js";
|
|
9
|
-
import {
|
|
10
|
-
import { n as enablePhysics3D } from "./physics-3d-
|
|
11
|
-
import { ACESFilmicToneMapping, AmbientLight, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
|
|
9
|
+
import { O as PhysicsBody3D, c as ModelInstance3D, d as DirectionalLight3D, j as Node3D, t as registerNodes3D, y as Camera3D } from "./register-DvPHJdVj.js";
|
|
10
|
+
import { n as enablePhysics3D } from "./physics-3d-C58oQzTX.js";
|
|
11
|
+
import { ACESFilmicToneMapping, AmbientLight, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
|
|
12
12
|
import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
|
|
13
13
|
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
14
14
|
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
|
|
@@ -127,6 +127,122 @@ var AssetStore3D = class {
|
|
|
127
127
|
}
|
|
128
128
|
};
|
|
129
129
|
//#endregion
|
|
130
|
+
//#region src/3d/bloom/bloom-composite.ts
|
|
131
|
+
/**
|
|
132
|
+
* HDR bloom post pass:
|
|
133
|
+
*
|
|
134
|
+
* scene → offscreen HalfFloat target (raw linear HDR — three applies neither
|
|
135
|
+
* tone mapping nor the sRGB transform when rendering into a target)
|
|
136
|
+
* bright-pass at HALF resolution (keep only luminance above `threshold`;
|
|
137
|
+
* with HDR buffers, emissives and sun glints sit above 1.0)
|
|
138
|
+
* separable 9-tap Gaussian blur (H then V) at half res
|
|
139
|
+
* composite: scene + glow × strength, THEN the standard postprocessing tail
|
|
140
|
+
* (ACES tone mapping + linear→sRGB) — skipping that tail is exactly the
|
|
141
|
+
* dark/oversaturated bug the volumetric-clouds pipeline shipped with.
|
|
142
|
+
*/
|
|
143
|
+
const VERT = `
|
|
144
|
+
varying vec2 vUv;
|
|
145
|
+
void main() {
|
|
146
|
+
vUv = uv;
|
|
147
|
+
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
const BRIGHT_FRAG = `
|
|
151
|
+
precision highp float;
|
|
152
|
+
varying vec2 vUv;
|
|
153
|
+
uniform sampler2D tScene;
|
|
154
|
+
uniform float uThreshold;
|
|
155
|
+
void main() {
|
|
156
|
+
vec3 c = texture2D(tScene, vUv).rgb;
|
|
157
|
+
// HDR luminance knee: the scene buffer is raw linear HDR (tone mapping is
|
|
158
|
+
// applied later, in the composite), so emissives/sun glints sit ABOVE 1.0 —
|
|
159
|
+
// keep only the energy above the threshold, in proportion
|
|
160
|
+
float l = dot(c, vec3(0.2126, 0.7152, 0.0722));
|
|
161
|
+
float k = max(l - uThreshold, 0.0);
|
|
162
|
+
gl_FragColor = vec4(c * (k / max(l, 1e-4)), 1.0);
|
|
163
|
+
}
|
|
164
|
+
`;
|
|
165
|
+
const BLUR_FRAG = `
|
|
166
|
+
precision highp float;
|
|
167
|
+
varying vec2 vUv;
|
|
168
|
+
uniform sampler2D tTex;
|
|
169
|
+
uniform vec2 uDir;
|
|
170
|
+
void main() {
|
|
171
|
+
// 9-tap Gaussian, sigma ~2 texels (same recipe as the cloud blur)
|
|
172
|
+
vec4 sum = texture2D(tTex, vUv) * 0.227027;
|
|
173
|
+
sum += texture2D(tTex, vUv + uDir * 1.3846) * 0.3162162;
|
|
174
|
+
sum += texture2D(tTex, vUv - uDir * 1.3846) * 0.3162162;
|
|
175
|
+
sum += texture2D(tTex, vUv + uDir * 3.2308) * 0.0702703;
|
|
176
|
+
sum += texture2D(tTex, vUv - uDir * 3.2308) * 0.0702703;
|
|
177
|
+
gl_FragColor = sum;
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
180
|
+
const COMPOSITE_FRAG = `
|
|
181
|
+
precision highp float;
|
|
182
|
+
varying vec2 vUv;
|
|
183
|
+
uniform sampler2D tScene;
|
|
184
|
+
uniform sampler2D tBloom;
|
|
185
|
+
uniform float uStrength;
|
|
186
|
+
void main() {
|
|
187
|
+
vec3 base = texture2D(tScene, vUv).rgb;
|
|
188
|
+
vec3 glow = texture2D(tBloom, vUv).rgb;
|
|
189
|
+
gl_FragColor = vec4(base + glow * uStrength, 1.0);
|
|
190
|
+
// three skips BOTH tone mapping and the output colorspace transform when the
|
|
191
|
+
// scene renders into an offscreen target — the buffer holds raw linear HDR.
|
|
192
|
+
// This is the final to-screen pass, so apply the standard postprocessing
|
|
193
|
+
// tail here (ACES then linear→sRGB); without it the frame comes out dark
|
|
194
|
+
// and oversaturated (the volumetric-clouds pipeline bug, root-caused).
|
|
195
|
+
#include <tonemapping_fragment>
|
|
196
|
+
#include <colorspace_fragment>
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
199
|
+
function quad(fragment, uniforms) {
|
|
200
|
+
const material = new ShaderMaterial({
|
|
201
|
+
vertexShader: VERT,
|
|
202
|
+
fragmentShader: fragment,
|
|
203
|
+
depthTest: false,
|
|
204
|
+
depthWrite: false,
|
|
205
|
+
uniforms
|
|
206
|
+
});
|
|
207
|
+
const mesh = new Mesh(new PlaneGeometry(2, 2), material);
|
|
208
|
+
mesh.frustumCulled = false;
|
|
209
|
+
return mesh;
|
|
210
|
+
}
|
|
211
|
+
/** Half-res bright-pass quad — keeps only pixels brighter than the threshold. */
|
|
212
|
+
function createBloomBrightQuad() {
|
|
213
|
+
const mesh = quad(BRIGHT_FRAG, {
|
|
214
|
+
tScene: { value: null },
|
|
215
|
+
uThreshold: { value: 1 }
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
mesh,
|
|
219
|
+
uniforms: mesh.material.uniforms
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/** Separable-blur quad (run twice: uDir = (1/w, 0) then (0, 1/h)). */
|
|
223
|
+
function createBloomBlurQuad() {
|
|
224
|
+
const mesh = quad(BLUR_FRAG, {
|
|
225
|
+
tTex: { value: null },
|
|
226
|
+
uDir: { value: new Vector2() }
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
mesh,
|
|
230
|
+
uniforms: mesh.material.uniforms
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** Full-res composite quad — adds the blurred glow over the scene. */
|
|
234
|
+
function createBloomCompositeQuad() {
|
|
235
|
+
const mesh = quad(COMPOSITE_FRAG, {
|
|
236
|
+
tScene: { value: null },
|
|
237
|
+
tBloom: { value: null },
|
|
238
|
+
uStrength: { value: .85 }
|
|
239
|
+
});
|
|
240
|
+
return {
|
|
241
|
+
mesh,
|
|
242
|
+
uniforms: mesh.material.uniforms
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
130
246
|
//#region src/3d/clouds/clouds-composite.ts
|
|
131
247
|
/**
|
|
132
248
|
* Volumetric clouds — a raymarched cloud DECK the camera flies through, drawn as
|
|
@@ -446,6 +562,7 @@ const CLOUD_KEYS = [
|
|
|
446
562
|
"speed",
|
|
447
563
|
"scale"
|
|
448
564
|
];
|
|
565
|
+
const BLOOM_KEYS = ["threshold", "strength"];
|
|
449
566
|
const SHADOW_KEYS = ["mapSize", "radius"];
|
|
450
567
|
const SHADOW_MAP_SIZES = [1024, 2048];
|
|
451
568
|
const DEFAULT_TURBIDITY = 2;
|
|
@@ -462,6 +579,7 @@ function parseEnvironment3D(env) {
|
|
|
462
579
|
sky: parseSky(env?.sky),
|
|
463
580
|
fog: parseFog(env?.fog, env?.sky !== void 0),
|
|
464
581
|
clouds: parseClouds(env?.clouds),
|
|
582
|
+
bloom: parseBloom(env?.bloom),
|
|
465
583
|
shadows: parseShadows(env?.shadows)
|
|
466
584
|
};
|
|
467
585
|
}
|
|
@@ -616,6 +734,23 @@ function parseClouds(value) {
|
|
|
616
734
|
scale
|
|
617
735
|
};
|
|
618
736
|
}
|
|
737
|
+
function parseBloom(value) {
|
|
738
|
+
if (value === void 0) return null;
|
|
739
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.bloom must be an object ({ threshold?, strength? }), got ${JSON.stringify(value)}.`, { prop: "bloom" });
|
|
740
|
+
const b = value;
|
|
741
|
+
for (const key of Object.keys(b)) if (!BLOOM_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.bloom has unknown key '${key}'. Valid keys: [${BLOOM_KEYS.join(", ")}].`, {
|
|
742
|
+
prop: "bloom",
|
|
743
|
+
validOptions: BLOOM_KEYS
|
|
744
|
+
});
|
|
745
|
+
const threshold = numberOr(b.threshold, 1, "bloom.threshold");
|
|
746
|
+
if (threshold < 0 || threshold > 8) throw new IncantoError("BAD_FORMAT", `environment.bloom.threshold must be in [0, 8] (linear-HDR luminance; 1 = white), got ${threshold}.`, { prop: "bloom" });
|
|
747
|
+
const strength = numberOr(b.strength, .8, "bloom.strength");
|
|
748
|
+
if (strength < 0) throw new IncantoError("BAD_FORMAT", `environment.bloom.strength must be >= 0, got ${strength}.`, { prop: "bloom" });
|
|
749
|
+
return {
|
|
750
|
+
threshold,
|
|
751
|
+
strength
|
|
752
|
+
};
|
|
753
|
+
}
|
|
619
754
|
function parseShadows(value) {
|
|
620
755
|
if (value === void 0) return null;
|
|
621
756
|
if (value === false) return false;
|
|
@@ -730,6 +865,10 @@ var Environment3D = class {
|
|
|
730
865
|
get clouds() {
|
|
731
866
|
return this.config.clouds;
|
|
732
867
|
}
|
|
868
|
+
/** Parsed bloom config, or null when no bloom pass is declared. */
|
|
869
|
+
get bloom() {
|
|
870
|
+
return this.config.bloom;
|
|
871
|
+
}
|
|
733
872
|
/** The live scene fog (color/near/far) the cloud composite fades into, or null. */
|
|
734
873
|
get sceneFog() {
|
|
735
874
|
return this.scene.fog instanceof Fog ? this.scene.fog : null;
|
|
@@ -1228,6 +1367,15 @@ var Renderer3D = class {
|
|
|
1228
1367
|
cloudsBlurUniforms = null;
|
|
1229
1368
|
cloudsCompositeScene = null;
|
|
1230
1369
|
cloudsCompositeUniforms = null;
|
|
1370
|
+
bloomTarget = null;
|
|
1371
|
+
bloomBrightTarget = null;
|
|
1372
|
+
bloomBlurTarget = null;
|
|
1373
|
+
bloomBrightScene = null;
|
|
1374
|
+
bloomBrightUniforms = null;
|
|
1375
|
+
bloomBlurScene = null;
|
|
1376
|
+
bloomBlurUniforms = null;
|
|
1377
|
+
bloomCompositeScene = null;
|
|
1378
|
+
bloomCompositeUniforms = null;
|
|
1231
1379
|
constructor(opts) {
|
|
1232
1380
|
this.canvas = opts.canvas;
|
|
1233
1381
|
this.engine = opts.engine;
|
|
@@ -1334,11 +1482,80 @@ var Renderer3D = class {
|
|
|
1334
1482
|
}
|
|
1335
1483
|
this.environment.applyUnderwater(underwater);
|
|
1336
1484
|
const clouds = this.environment.clouds;
|
|
1485
|
+
const bloom = this.environment.bloom;
|
|
1337
1486
|
if (underwater?.caustics.enabled) this.renderWithCaustics(activeCamera, underwater);
|
|
1338
1487
|
else if (clouds && !underwater) this.renderWithClouds(activeCamera, clouds, threeLight);
|
|
1488
|
+
else if (bloom && !underwater) this.renderWithBloom(activeCamera, bloom);
|
|
1339
1489
|
else this.webgl.render(this.threeScene, activeCamera);
|
|
1340
1490
|
}
|
|
1341
1491
|
/**
|
|
1492
|
+
* Bloom post pass (only when `environment.bloom` is declared). LDR and safe:
|
|
1493
|
+
* 1. scene → full-res offscreen (tone-mapped exactly like direct output)
|
|
1494
|
+
* 2. bright-pass at HALF res (keep pixels above threshold)
|
|
1495
|
+
* 3. separable Gaussian blur (H, V) at half res
|
|
1496
|
+
* 4. composite scene + glow×strength → screen (pure ADD — never re-touches
|
|
1497
|
+
* the scene's tone; the clouds pipeline taught us that lesson)
|
|
1498
|
+
* Caustics/clouds paths take precedence (composite pipelines don't stack yet).
|
|
1499
|
+
*/
|
|
1500
|
+
renderWithBloom(camera, bloom) {
|
|
1501
|
+
const buf = this.webgl.getDrawingBufferSize(sizeScratch);
|
|
1502
|
+
const w = Math.max(1, buf.x);
|
|
1503
|
+
const h = Math.max(1, buf.y);
|
|
1504
|
+
const hw = Math.max(1, Math.ceil(w / 2));
|
|
1505
|
+
const hh = Math.max(1, Math.ceil(h / 2));
|
|
1506
|
+
if (this.bloomTarget && (this.bloomTarget.width !== w || this.bloomTarget.height !== h)) {
|
|
1507
|
+
this.bloomTarget.dispose();
|
|
1508
|
+
this.bloomTarget = null;
|
|
1509
|
+
this.bloomBrightTarget?.dispose();
|
|
1510
|
+
this.bloomBrightTarget = null;
|
|
1511
|
+
this.bloomBlurTarget?.dispose();
|
|
1512
|
+
this.bloomBlurTarget = null;
|
|
1513
|
+
}
|
|
1514
|
+
if (!this.bloomTarget) {
|
|
1515
|
+
this.bloomTarget = new WebGLRenderTarget(w, h, { type: HalfFloatType });
|
|
1516
|
+
this.bloomBrightTarget = new WebGLRenderTarget(hw, hh, { type: HalfFloatType });
|
|
1517
|
+
this.bloomBlurTarget = new WebGLRenderTarget(hw, hh, { type: HalfFloatType });
|
|
1518
|
+
}
|
|
1519
|
+
if (!this.bloomBrightScene) {
|
|
1520
|
+
const bright = createBloomBrightQuad();
|
|
1521
|
+
this.bloomBrightScene = new Scene();
|
|
1522
|
+
this.bloomBrightScene.add(bright.mesh);
|
|
1523
|
+
this.bloomBrightUniforms = bright.uniforms;
|
|
1524
|
+
const blur = createBloomBlurQuad();
|
|
1525
|
+
this.bloomBlurScene = new Scene();
|
|
1526
|
+
this.bloomBlurScene.add(blur.mesh);
|
|
1527
|
+
this.bloomBlurUniforms = blur.uniforms;
|
|
1528
|
+
const composite = createBloomCompositeQuad();
|
|
1529
|
+
this.bloomCompositeScene = new Scene();
|
|
1530
|
+
this.bloomCompositeScene.add(composite.mesh);
|
|
1531
|
+
this.bloomCompositeUniforms = composite.uniforms;
|
|
1532
|
+
}
|
|
1533
|
+
const brightTarget = this.bloomBrightTarget;
|
|
1534
|
+
const blurTarget = this.bloomBlurTarget;
|
|
1535
|
+
this.webgl.setRenderTarget(this.bloomTarget);
|
|
1536
|
+
this.webgl.render(this.threeScene, camera);
|
|
1537
|
+
const bu = this.bloomBrightUniforms;
|
|
1538
|
+
bu.tScene.value = this.bloomTarget.texture;
|
|
1539
|
+
bu.uThreshold.value = bloom.threshold;
|
|
1540
|
+
this.webgl.setRenderTarget(brightTarget);
|
|
1541
|
+
this.webgl.render(this.bloomBrightScene, camera);
|
|
1542
|
+
const blu = this.bloomBlurUniforms;
|
|
1543
|
+
blu.tTex.value = brightTarget.texture;
|
|
1544
|
+
blu.uDir.value.set(1 / hw, 0);
|
|
1545
|
+
this.webgl.setRenderTarget(blurTarget);
|
|
1546
|
+
this.webgl.render(this.bloomBlurScene, camera);
|
|
1547
|
+
blu.tTex.value = blurTarget.texture;
|
|
1548
|
+
blu.uDir.value.set(0, 1 / hh);
|
|
1549
|
+
this.webgl.setRenderTarget(brightTarget);
|
|
1550
|
+
this.webgl.render(this.bloomBlurScene, camera);
|
|
1551
|
+
const cu = this.bloomCompositeUniforms;
|
|
1552
|
+
cu.tScene.value = this.bloomTarget.texture;
|
|
1553
|
+
cu.tBloom.value = brightTarget.texture;
|
|
1554
|
+
cu.uStrength.value = bloom.strength;
|
|
1555
|
+
this.webgl.setRenderTarget(null);
|
|
1556
|
+
this.webgl.render(this.bloomCompositeScene, camera);
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1342
1559
|
* Volumetric cloud pass (only when `environment.clouds` is declared — scenes
|
|
1343
1560
|
* without it render exactly as before, zero cost). Three steps:
|
|
1344
1561
|
* 1. scene → full-res offscreen target (color + depth)
|
|
@@ -1564,10 +1781,16 @@ var Renderer3D = class {
|
|
|
1564
1781
|
this.cloudsTarget?.dispose();
|
|
1565
1782
|
this.cloudsHalfTarget?.dispose();
|
|
1566
1783
|
this.cloudsBlurTarget?.dispose();
|
|
1784
|
+
this.bloomTarget?.dispose();
|
|
1785
|
+
this.bloomBrightTarget?.dispose();
|
|
1786
|
+
this.bloomBlurTarget?.dispose();
|
|
1567
1787
|
for (const s of [
|
|
1568
1788
|
this.cloudsScene,
|
|
1569
1789
|
this.cloudsBlurScene,
|
|
1570
|
-
this.cloudsCompositeScene
|
|
1790
|
+
this.cloudsCompositeScene,
|
|
1791
|
+
this.bloomBrightScene,
|
|
1792
|
+
this.bloomBlurScene,
|
|
1793
|
+
this.bloomCompositeScene
|
|
1571
1794
|
]) {
|
|
1572
1795
|
const m = s?.children[0];
|
|
1573
1796
|
m?.geometry?.dispose();
|
package/dist/debug.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as RendererStats, k as LogLevel, v as Engine } from "./behavior-
|
|
1
|
+
import { C as RendererStats, k as LogLevel, v as Engine } from "./behavior-CWhW3oa6.js";
|
|
2
2
|
|
|
3
3
|
//#region src/debug/panel.d.ts
|
|
4
4
|
/** Minimal document surface the overlay needs (injectable for tests). */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { _ as registerBehavior, m as Behavior } from "./loader-
|
|
1
|
+
import { _ as registerBehavior, m as Behavior, n as loadScene } from "./loader-B4OEXDZ8.js";
|
|
2
2
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
3
|
-
import { t as
|
|
3
|
+
import { t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
4
|
+
import { t as duplicateNode } from "./duplicate-BEvGBtb_.js";
|
|
4
5
|
//#region src/gameplay/spatial.ts
|
|
5
6
|
/** Duck-type test: every spatial node (Node2D/Node3D) exposes `position: number[]`. */
|
|
6
7
|
function hasPosition$1(node) {
|
|
@@ -36,13 +37,6 @@ function add(a, b) {
|
|
|
36
37
|
for (let i = 0; i < n; i++) out[i] = (a[i] ?? 0) + (b[i] ?? 0);
|
|
37
38
|
return out;
|
|
38
39
|
}
|
|
39
|
-
/** Component-wise `a - b`. */
|
|
40
|
-
function sub(a, b) {
|
|
41
|
-
const n = Math.max(a.length, b.length);
|
|
42
|
-
const out = new Array(n);
|
|
43
|
-
for (let i = 0; i < n; i++) out[i] = (a[i] ?? 0) - (b[i] ?? 0);
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
40
|
/** Vector length. */
|
|
47
41
|
function length(v) {
|
|
48
42
|
let sum = 0;
|
|
@@ -60,15 +54,28 @@ function normalize(v) {
|
|
|
60
54
|
* whether the destination was reached this step (clamped — no overshoot).
|
|
61
55
|
*/
|
|
62
56
|
function moveToward(from, to, maxStep) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
57
|
+
const n = Math.max(from.length, to.length);
|
|
58
|
+
let sum = 0;
|
|
59
|
+
for (let i = 0; i < n; i++) {
|
|
60
|
+
const d = (to[i] ?? 0) - (from[i] ?? 0);
|
|
61
|
+
sum += d * d;
|
|
62
|
+
}
|
|
63
|
+
const dist = Math.sqrt(sum);
|
|
64
|
+
const position = new Array(n);
|
|
65
|
+
if (dist <= maxStep || dist === 0) {
|
|
66
|
+
for (let i = 0; i < n; i++) position[i] = to[i] ?? 0;
|
|
67
|
+
return {
|
|
68
|
+
position,
|
|
69
|
+
reached: true
|
|
70
|
+
};
|
|
71
|
+
}
|
|
69
72
|
const f = maxStep / dist;
|
|
73
|
+
for (let i = 0; i < n; i++) {
|
|
74
|
+
const c = from[i] ?? 0;
|
|
75
|
+
position[i] = c + ((to[i] ?? 0) - c) * f;
|
|
76
|
+
}
|
|
70
77
|
return {
|
|
71
|
-
position
|
|
78
|
+
position,
|
|
72
79
|
reached: false
|
|
73
80
|
};
|
|
74
81
|
}
|
|
@@ -427,19 +434,128 @@ var FollowCamera = class extends Behavior {
|
|
|
427
434
|
if (this.deadzone < 0) throw new IncantoError("PROP_TYPE_MISMATCH", `FollowCamera on '${this.node.getPath()}': "deadzone" must be >= 0, got ${this.deadzone}.`, { prop: "deadzone" });
|
|
428
435
|
requirePosition(this);
|
|
429
436
|
}
|
|
437
|
+
shakeMag = 0;
|
|
438
|
+
shakeT = 0;
|
|
439
|
+
shakeFalloff = .28;
|
|
440
|
+
/** Kick the camera (impacts, explosions). Composes with the follow. */
|
|
441
|
+
shake(magnitude, seconds = .28) {
|
|
442
|
+
this.shakeMag = Math.max(this.shakeMag, magnitude);
|
|
443
|
+
this.shakeT = seconds;
|
|
444
|
+
this.shakeFalloff = seconds;
|
|
445
|
+
}
|
|
430
446
|
update(dt) {
|
|
431
447
|
const cam = this.node;
|
|
432
448
|
if (!hasPosition$1(cam)) return;
|
|
433
449
|
const target = cam.getNodeOrNull(this.target);
|
|
434
450
|
if (!target || !hasPosition$1(target)) return;
|
|
435
451
|
const desired = add(target.position, this.offset);
|
|
436
|
-
if (this.deadzone > 0 && distance$1(cam.position, desired) <= this.deadzone)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
cam.position = [
|
|
440
|
-
|
|
452
|
+
if (!(this.deadzone > 0 && distance$1(cam.position, desired) <= this.deadzone)) {
|
|
453
|
+
const f = smoothingFactor(this.smoothing, dt);
|
|
454
|
+
if (f >= 1) cam.position = [...desired];
|
|
455
|
+
else cam.position = cam.position.map((c, i) => c + ((desired[i] ?? c) - c) * f);
|
|
456
|
+
}
|
|
457
|
+
if (this.shakeT > 0) {
|
|
458
|
+
this.shakeT -= dt;
|
|
459
|
+
const k = this.shakeMag * Math.max(0, this.shakeT) / this.shakeFalloff;
|
|
460
|
+
if (this.shakeT <= 0) this.shakeMag = 0;
|
|
461
|
+
const jolted = cam.position.slice();
|
|
462
|
+
for (let i = 0; i < jolted.length; i++) {
|
|
463
|
+
const scale = jolted.length >= 3 && i === 1 ? .5 : 1;
|
|
464
|
+
jolted[i] = (jolted[i] ?? 0) + (this.engine.rng.next() * 2 - 1) * k * scale;
|
|
465
|
+
}
|
|
466
|
+
cam.position = jolted;
|
|
441
467
|
}
|
|
442
|
-
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/gameplay/game-flow.ts
|
|
472
|
+
/**
|
|
473
|
+
* Reload the CURRENT scene from its source JSON — fresh nodes, reset physics,
|
|
474
|
+
* rewired input. The restart primitive every game-over screen wants.
|
|
475
|
+
*/
|
|
476
|
+
function restartScene(engine) {
|
|
477
|
+
const source = engine.scene?.source;
|
|
478
|
+
if (!source) return;
|
|
479
|
+
engine.timeScale = 1;
|
|
480
|
+
engine.setScene(loadScene(jsonClone(source)));
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* The win/lose/restart state machine 6+ examples hand-rolled as `over`/`win`
|
|
484
|
+
* booleans. Attach to any node (the scene root is natural):
|
|
485
|
+
*
|
|
486
|
+
* const flow = root.behavior as GameFlow; // script: { "name": "GameFlow" }
|
|
487
|
+
* flow.gameOver('YOU DIED'); // freezes time, sticky banner
|
|
488
|
+
* flow.win('AREA CLEAR');
|
|
489
|
+
* flow.pause(); flow.resume();
|
|
490
|
+
*
|
|
491
|
+
* While in `gameover`/`won`, pressing `restartAction` (default action name
|
|
492
|
+
* 'restart' — declare it in the scene input map, or leave undeclared and call
|
|
493
|
+
* `flow.restart()` yourself) reloads the scene from source. Banners render
|
|
494
|
+
* through a `%Banner` UiBanner when one exists; otherwise states are silent
|
|
495
|
+
* (drive your own UI off the `flowChanged` signal).
|
|
496
|
+
*/
|
|
497
|
+
var GameFlow = class extends Behavior {
|
|
498
|
+
static signals = ["flowChanged"];
|
|
499
|
+
static props = {
|
|
500
|
+
restartAction: { default: "restart" },
|
|
501
|
+
freezeOnEnd: { default: true },
|
|
502
|
+
bannerPath: { default: "%Banner" }
|
|
503
|
+
};
|
|
504
|
+
/** Input action that restarts from gameover/won (declare it in `input{}`). */
|
|
505
|
+
restartAction = "restart";
|
|
506
|
+
/** Freeze `engine.timeScale` on gameover/won (banner UI keeps rendering). */
|
|
507
|
+
freezeOnEnd = true;
|
|
508
|
+
/** Where the flow looks for a UiBanner ('' = never). */
|
|
509
|
+
bannerPath = "%Banner";
|
|
510
|
+
state = "playing";
|
|
511
|
+
frozeScale = false;
|
|
512
|
+
gameOver(text = "GAME OVER", color = "#ef4444") {
|
|
513
|
+
this.transition("gameover", text, color);
|
|
514
|
+
}
|
|
515
|
+
win(text = "YOU WIN", color = "#4ade80") {
|
|
516
|
+
this.transition("won", text, color);
|
|
517
|
+
}
|
|
518
|
+
pause() {
|
|
519
|
+
if (this.state !== "playing") return;
|
|
520
|
+
this.state = "paused";
|
|
521
|
+
this.freeze();
|
|
522
|
+
this.node.emit("flowChanged", this.state);
|
|
523
|
+
}
|
|
524
|
+
resume() {
|
|
525
|
+
if (this.state !== "paused") return;
|
|
526
|
+
this.state = "playing";
|
|
527
|
+
this.thaw();
|
|
528
|
+
this.node.emit("flowChanged", this.state);
|
|
529
|
+
}
|
|
530
|
+
restart() {
|
|
531
|
+
this.thaw();
|
|
532
|
+
restartScene(this.engine);
|
|
533
|
+
}
|
|
534
|
+
transition(state, text, color) {
|
|
535
|
+
if (this.state === "gameover" || this.state === "won") return;
|
|
536
|
+
this.state = state;
|
|
537
|
+
if (this.freezeOnEnd) this.freeze();
|
|
538
|
+
if (this.bannerPath !== "") this.node.getNodeOrNull(this.bannerPath)?.show(text, {
|
|
539
|
+
color,
|
|
540
|
+
seconds: 0
|
|
541
|
+
});
|
|
542
|
+
this.node.emit("flowChanged", state);
|
|
543
|
+
}
|
|
544
|
+
freeze() {
|
|
545
|
+
if (this.frozeScale) return;
|
|
546
|
+
this.frozeScale = true;
|
|
547
|
+
this.engine.timeScale = 0;
|
|
548
|
+
}
|
|
549
|
+
thaw() {
|
|
550
|
+
if (!this.frozeScale) return;
|
|
551
|
+
this.frozeScale = false;
|
|
552
|
+
this.engine.timeScale = 1;
|
|
553
|
+
}
|
|
554
|
+
update() {
|
|
555
|
+
if (this.state !== "gameover" && this.state !== "won") return;
|
|
556
|
+
try {
|
|
557
|
+
if (this.engine.input.justPressed(this.restartAction)) this.restart();
|
|
558
|
+
} catch {}
|
|
443
559
|
}
|
|
444
560
|
};
|
|
445
561
|
//#endregion
|
|
@@ -508,6 +624,153 @@ function collectGroup(node, group) {
|
|
|
508
624
|
return out;
|
|
509
625
|
}
|
|
510
626
|
//#endregion
|
|
627
|
+
//#region src/gameplay/juice.ts
|
|
628
|
+
/**
|
|
629
|
+
* Game-feel primitives ("juice"): the weapon cooldown, camera shake, screen
|
|
630
|
+
* flash and hit-stop that action examples kept hand-rolling.
|
|
631
|
+
*/
|
|
632
|
+
/**
|
|
633
|
+
* A fire-rate / ability cooldown — replaces the copy-pasted
|
|
634
|
+
* `this.clock += dt * 1000; if (now > nextFire)` pattern:
|
|
635
|
+
*
|
|
636
|
+
* private gun = new Cooldown(0.2);
|
|
637
|
+
* override update(dt: number): void {
|
|
638
|
+
* this.gun.tick(dt);
|
|
639
|
+
* if (this.engine.input.isPressed('fire') && this.gun.tryUse()) this.shoot();
|
|
640
|
+
* }
|
|
641
|
+
*/
|
|
642
|
+
var Cooldown = class {
|
|
643
|
+
seconds;
|
|
644
|
+
remaining = 0;
|
|
645
|
+
constructor(seconds) {
|
|
646
|
+
this.seconds = seconds;
|
|
647
|
+
}
|
|
648
|
+
/** Advance time. Call once per update with the frame dt. */
|
|
649
|
+
tick(dt) {
|
|
650
|
+
if (this.remaining > 0) this.remaining -= dt;
|
|
651
|
+
}
|
|
652
|
+
get ready() {
|
|
653
|
+
return this.remaining <= 0;
|
|
654
|
+
}
|
|
655
|
+
/** 0..1 — how far through the cooldown we are (1 = ready). UiBar-friendly. */
|
|
656
|
+
get progress() {
|
|
657
|
+
return this.seconds <= 0 ? 1 : Math.min(1, Math.max(0, 1 - this.remaining / this.seconds));
|
|
658
|
+
}
|
|
659
|
+
/** Consume if ready. Returns whether the action should fire. */
|
|
660
|
+
tryUse() {
|
|
661
|
+
if (this.remaining > 0) return false;
|
|
662
|
+
this.remaining = this.seconds;
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
/** Force-ready (pickups that reset your reload). */
|
|
666
|
+
reset() {
|
|
667
|
+
this.remaining = 0;
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
const SHAKE_FALLOFF_SECONDS = .28;
|
|
671
|
+
/**
|
|
672
|
+
* Camera shake as a standalone behavior for cameras WITHOUT another script.
|
|
673
|
+
* (FollowCamera has this built in — one script per node.) Composes with any
|
|
674
|
+
* other position writer by applying only the DELTA of its own offset, so the
|
|
675
|
+
* camera returns exactly to where the other writer left it.
|
|
676
|
+
*
|
|
677
|
+
* (camera.behavior as CameraShake).shake(8); // pixels (2D) / meters·100 feel (3D: use ~0.2)
|
|
678
|
+
*/
|
|
679
|
+
var CameraShake = class extends Behavior {
|
|
680
|
+
static props = { falloff: { default: SHAKE_FALLOFF_SECONDS } };
|
|
681
|
+
/** Seconds a shake takes to decay to zero. */
|
|
682
|
+
falloff = SHAKE_FALLOFF_SECONDS;
|
|
683
|
+
magnitude = 0;
|
|
684
|
+
t = 0;
|
|
685
|
+
prev = [];
|
|
686
|
+
shake(magnitude, seconds) {
|
|
687
|
+
this.magnitude = Math.max(this.magnitude, magnitude);
|
|
688
|
+
this.t = seconds ?? this.falloff;
|
|
689
|
+
}
|
|
690
|
+
update(dt) {
|
|
691
|
+
const node = requirePosition(this);
|
|
692
|
+
const pos = node.position;
|
|
693
|
+
for (let i = 0; i < this.prev.length; i++) pos[i] = (pos[i] ?? 0) - (this.prev[i] ?? 0);
|
|
694
|
+
if (this.t <= 0) {
|
|
695
|
+
if (this.prev.length > 0) {
|
|
696
|
+
node.position = pos.slice();
|
|
697
|
+
this.prev.length = 0;
|
|
698
|
+
}
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
this.t -= dt;
|
|
702
|
+
const k = this.magnitude * Math.max(0, this.t) / this.falloff;
|
|
703
|
+
if (this.t <= 0) this.magnitude = 0;
|
|
704
|
+
const next = pos.slice();
|
|
705
|
+
if (this.prev.length !== next.length) this.prev = new Array(next.length).fill(0);
|
|
706
|
+
for (let i = 0; i < next.length; i++) {
|
|
707
|
+
const scale = next.length >= 3 && i === 1 ? .5 : 1;
|
|
708
|
+
this.prev[i] = (this.engine.rng.next() * 2 - 1) * k * scale;
|
|
709
|
+
next[i] = (next[i] ?? 0) + this.prev[i];
|
|
710
|
+
}
|
|
711
|
+
node.position = next;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
/**
|
|
715
|
+
* Full-screen color flash (damage red, pickup white). DOM overlay above
|
|
716
|
+
* everything; headless no-op. Repeated calls restart the fade.
|
|
717
|
+
*/
|
|
718
|
+
function screenFlash(color = "#ffffff", opacity = .35, seconds = .25) {
|
|
719
|
+
if (typeof document === "undefined") return;
|
|
720
|
+
const id = "incanto-screen-flash";
|
|
721
|
+
let el = document.getElementById(id);
|
|
722
|
+
if (!el) {
|
|
723
|
+
el = document.createElement("div");
|
|
724
|
+
el.id = id;
|
|
725
|
+
el.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:9999;opacity:0;transition:opacity .05s;";
|
|
726
|
+
document.body.appendChild(el);
|
|
727
|
+
}
|
|
728
|
+
el.style.background = color;
|
|
729
|
+
el.style.transition = "opacity .05s";
|
|
730
|
+
el.style.opacity = String(opacity);
|
|
731
|
+
const fade = () => {
|
|
732
|
+
el.style.transition = `opacity ${seconds}s`;
|
|
733
|
+
el.style.opacity = "0";
|
|
734
|
+
};
|
|
735
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(fade);
|
|
736
|
+
else fade();
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Hit-stop: freeze game time for `seconds` of REAL time, then restore the
|
|
740
|
+
* previous timeScale. Stacking calls extend the freeze instead of fighting.
|
|
741
|
+
*/
|
|
742
|
+
function hitStop(engine, seconds = .08) {
|
|
743
|
+
const state = hitStops.get(engine);
|
|
744
|
+
if (state) {
|
|
745
|
+
state.until = Math.max(state.until, realNow() + seconds * 1e3);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const restore = engine.timeScale;
|
|
749
|
+
const entry = {
|
|
750
|
+
until: realNow() + seconds * 1e3,
|
|
751
|
+
restore
|
|
752
|
+
};
|
|
753
|
+
hitStops.set(engine, entry);
|
|
754
|
+
engine.timeScale = 0;
|
|
755
|
+
const pump = () => {
|
|
756
|
+
if (realNow() >= entry.until) {
|
|
757
|
+
engine.timeScale = entry.restore;
|
|
758
|
+
hitStops.delete(engine);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
schedule(pump);
|
|
762
|
+
};
|
|
763
|
+
schedule(pump);
|
|
764
|
+
}
|
|
765
|
+
const hitStops = /* @__PURE__ */ new Map();
|
|
766
|
+
function realNow() {
|
|
767
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
768
|
+
}
|
|
769
|
+
function schedule(fn) {
|
|
770
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(fn);
|
|
771
|
+
else setTimeout(fn, 16);
|
|
772
|
+
}
|
|
773
|
+
//#endregion
|
|
511
774
|
//#region src/gameplay/lifetime.ts
|
|
512
775
|
/**
|
|
513
776
|
* Self-destruct after a fixed time — bullets, particles, temporary spawns,
|
|
@@ -876,7 +1139,11 @@ var Projectile = class extends Behavior {
|
|
|
876
1139
|
update(dt) {
|
|
877
1140
|
const node = this.node;
|
|
878
1141
|
if (this.gravity !== 0) this.velocity[1] = (this.velocity[1] ?? 0) + this.gravity * dt;
|
|
879
|
-
|
|
1142
|
+
const pos = node.position;
|
|
1143
|
+
const n = Math.max(pos.length, this.velocity.length);
|
|
1144
|
+
const next = new Array(n);
|
|
1145
|
+
for (let i = 0; i < n; i++) next[i] = (pos[i] ?? 0) + (this.velocity[i] ?? 0) * dt;
|
|
1146
|
+
node.position = next;
|
|
880
1147
|
}
|
|
881
1148
|
/** Resolve `direction` to a unit vector, deriving 'forward' from rotation. */
|
|
882
1149
|
resolveDirection() {
|
|
@@ -1470,6 +1737,8 @@ var ZombieAI = class extends Behavior {
|
|
|
1470
1737
|
*/
|
|
1471
1738
|
/** Every built-in gameplay behavior, keyed by its registration name. */
|
|
1472
1739
|
const GAMEPLAY_BEHAVIORS = {
|
|
1740
|
+
CameraShake,
|
|
1741
|
+
GameFlow,
|
|
1473
1742
|
Health,
|
|
1474
1743
|
Lifetime,
|
|
1475
1744
|
ScoreKeeper,
|
|
@@ -1498,4 +1767,4 @@ function registerGameplayBehaviors(opts) {
|
|
|
1498
1767
|
for (const [name, ctor] of Object.entries(GAMEPLAY_BEHAVIORS)) registerBehavior(name, ctor, { replace });
|
|
1499
1768
|
}
|
|
1500
1769
|
//#endregion
|
|
1501
|
-
export { Health as _, Wander as a, Projectile as c, Oscillate as d, MoveTo as f,
|
|
1770
|
+
export { Health as C, DamageOnContact as S, Chase as T, screenFlash as _, Wander as a, restartScene as b, Projectile as c, Oscillate as d, MoveTo as f, hitStop as g, Cooldown as h, WaveSpawner as i, Pickup as l, CameraShake as m, registerGameplayBehaviors as n, Spawner as o, Lifetime as p, ZombieAI as r, ScoreKeeper as s, GAMEPLAY_BEHAVIORS as t, Patrol as u, Interactable as v, Collector as w, FollowCamera as x, GameFlow as y };
|