incanto 0.36.1 → 0.37.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.
Files changed (56) hide show
  1. package/bin/incanto-feel.mjs +93 -0
  2. package/bin/incanto-playtest.mjs +136 -0
  3. package/dist/2d.d.ts +2 -2
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +14 -3
  6. package/dist/3d.js +4 -4
  7. package/dist/{behavior-DibCwrW7.d.ts → behavior-5o1EkbLD.d.ts} +11 -0
  8. package/dist/{create-game-D-StzrTj.js → create-game-DmGc-Mrn.js} +5 -5
  9. package/dist/{create-game-DnOu2aAa.js → create-game-QqrrK3yr.js} +5 -5
  10. package/dist/debug.d.ts +1 -1
  11. package/dist/{duplicate-Cvb1BSca.js → duplicate-CRtihGmC.js} +1 -1
  12. package/dist/editor.js +1 -1
  13. package/dist/{environment-presets-fK9oyrrR.js → environment-presets-DWHxLHM5.js} +75 -9
  14. package/dist/{gameplay-C0MUMSak.js → gameplay-CsJDUQh_.js} +2 -2
  15. package/dist/gameplay.d.ts +1 -1
  16. package/dist/gameplay.js +1 -1
  17. package/dist/index.d.ts +4 -50
  18. package/dist/index.js +5 -75
  19. package/dist/{loader-BqigZsfF.d.ts → loader-iGq2PT_k.d.ts} +1 -1
  20. package/dist/{loader-DAZlgqeC.js → loader-r49nDwB4.js} +15 -0
  21. package/dist/net.d.ts +1 -1
  22. package/dist/net.js +3 -3
  23. package/dist/{pathfinding-B3QtdhvZ.d.ts → pathfinding-pPrqtlWS.d.ts} +1 -1
  24. package/dist/{physics-2d-B4v39hca.js → physics-2d-C8Hi40qb.js} +2 -2
  25. package/dist/{physics-3d-DFJVMYET.js → physics-3d-C0mWoWXC.js} +3 -3
  26. package/dist/react.d.ts +1 -1
  27. package/dist/react.js +1 -1
  28. package/dist/{register-COGCNitr.js → register-DFSW2Y30.js} +2 -2
  29. package/dist/{register-BHAwM4bK.js → register-DOWGnxe1.js} +1 -1
  30. package/dist/{register-DXwtIVqP.js → register-t15rydSm.js} +2 -2
  31. package/dist/{audit-D7F3n3Nt.js → replay-BuOTl5rM.js} +72 -2
  32. package/dist/replay-DGzhZWrM.d.ts +86 -0
  33. package/dist/test-BiIO9ULW.js +1404 -0
  34. package/dist/test.d.ts +113 -4
  35. package/dist/test.js +3 -3
  36. package/dist/vite.js +1 -1
  37. package/editor/assets/{agent8-tvTJH3IX.js → agent8-CeeI7P_0.js} +1 -1
  38. package/editor/assets/{debug-CPbraUYm.js → debug-DN9X9G6z.js} +1 -1
  39. package/editor/assets/{index-DPJkbebq.js → index-C833FBzB.js} +42 -42
  40. package/editor/index.html +1 -1
  41. package/package.json +3 -1
  42. package/schemas/scene.schema.json +24 -0
  43. package/skills/incanto-3d-character.md +35 -0
  44. package/skills/incanto-building-2d-games.md +6 -0
  45. package/skills/incanto-building-3d-games.md +6 -0
  46. package/skills/incanto-game-feel.md +76 -0
  47. package/skills/incanto-node-reference.md +6 -0
  48. package/skills/incanto-playtesting.md +143 -0
  49. package/skills/incanto-verifying-your-game.md +41 -0
  50. package/templates-app/beacon-isle-3d/package.json +1 -1
  51. package/templates-app/tps-3d/package.json +1 -1
  52. package/templates-app/village-quest-3d/package.json +1 -1
  53. package/templates-app/village-quest-3d/src/village.scene.json +3 -1
  54. package/templates-app/village-quest-3d/verify.ts +55 -5
  55. package/dist/errors-DGRtWlSx.d.ts +0 -39
  56. package/dist/test-X_xE6Yay.js +0 -642
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ /**
5
+ * incanto-feel — what your controls actually FEEL like, as numbers.
6
+ *
7
+ * bunx incanto-feel src/game.scene.json
8
+ *
9
+ * `capture()` and `framing()` are snapshots. Game feel lives on the time axis:
10
+ * how high the jump really goes, how many frames a turn costs, how wide the
11
+ * coyote window really is. This drives the player through scripted probes and
12
+ * reports the measurements.
13
+ *
14
+ * The windows are MEASURED by probing, not read off the schema — so a
15
+ * `coyoteSeconds` the controller never consults reports 0 ms. That was a real
16
+ * bug in this engine's own 3D controller, and the JSON looked correct
17
+ * throughout.
18
+ *
19
+ * It reports numbers and does not grade them: "good" jump feel is a genre
20
+ * decision, and a PASS/FAIL table against invented bands would be confidently
21
+ * wrong for most games.
22
+ */
23
+ import { fileURLToPath, pathToFileURL } from 'node:url';
24
+
25
+ const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
26
+
27
+ function parseArgs(argv) {
28
+ const args = {};
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const a = argv[i];
31
+ if (a === '--behaviors') args.behaviors = argv[++i];
32
+ else if (a === '--move') args.move = argv[++i];
33
+ else if (a === '--jump') args.jump = argv[++i];
34
+ else if (a === '--json') args.json = true;
35
+ else if (a === '--help' || a === '-h') args.help = true;
36
+ else if (a.startsWith('--')) {
37
+ console.error(`unknown flag: ${a}`);
38
+ args.help = true;
39
+ } else if (!args.scene) args.scene = a;
40
+ }
41
+ return args;
42
+ }
43
+
44
+ const args = parseArgs(process.argv.slice(2));
45
+ if (args.help || !args.scene) {
46
+ console.error(`Usage: incanto-feel <scene.json> [options]
47
+
48
+ --behaviors FILE your Behavior subclasses (.ts works on node >= 23.6 / bun)
49
+ --move ACTION the vector action to move with (default: the first vector2)
50
+ --jump ACTION the button to jump with (default: "jump")
51
+ --json the report as JSON instead of prose`);
52
+ process.exit(args.help && args.scene !== undefined ? 0 : 1);
53
+ }
54
+
55
+ const { feelReport, feelText } = await import(pathToFileURL(join(PKG, 'dist', 'test.js')).href);
56
+ const incanto = await import(pathToFileURL(join(PKG, 'dist', 'index.js')).href);
57
+
58
+ const behaviors = {};
59
+ if (args.behaviors) {
60
+ let mod;
61
+ try {
62
+ mod = await import(pathToFileURL(resolve(args.behaviors)).href);
63
+ } catch (e) {
64
+ console.error(`could not load --behaviors '${args.behaviors}' (${e?.message ?? e}).`);
65
+ process.exit(1);
66
+ }
67
+ for (const [name, value] of Object.entries(mod)) {
68
+ if (typeof value === 'function' && value.prototype instanceof incanto.Behavior) {
69
+ behaviors[name] = value;
70
+ }
71
+ }
72
+ }
73
+
74
+ const scenePath = resolve(args.scene);
75
+ const sceneDir = dirname(scenePath);
76
+ const sceneJson = JSON.parse(readFileSync(scenePath, 'utf-8'));
77
+
78
+ let report;
79
+ try {
80
+ report = await feelReport(sceneJson, {
81
+ behaviors,
82
+ stubMissingBehaviors: !args.behaviors,
83
+ ...(args.move ? { moveAction: args.move } : {}),
84
+ ...(args.jump ? { jumpAction: args.jump } : {}),
85
+ resolveScene: (p) => JSON.parse(readFileSync(resolve(sceneDir, p), 'utf-8')),
86
+ });
87
+ } catch (e) {
88
+ console.error(`feel failed: ${e?.message ?? e}`);
89
+ process.exit(1);
90
+ }
91
+
92
+ process.stdout.write(args.json ? `${JSON.stringify(report, null, 2)}\n` : `${feelText(report)}\n`);
93
+ process.exit(report.player ? 0 : 1);
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ /**
5
+ * incanto-playtest — the engine plays your game and tells you whether it works.
6
+ *
7
+ * bunx incanto-playtest src/game.scene.json --runs 20 --seconds 60
8
+ *
9
+ * `incanto-check` answers "will it load". This answers "is it a game": can it
10
+ * be finished, which destinations are unreachable, which wires never fire, and
11
+ * whether anything in it can hurt the player.
12
+ *
13
+ * Runs are seeded and headless — no renderer, no browser — so twenty minutes of
14
+ * simulated play costs a second or two. A run that did NOT win is written out
15
+ * as a replay you can watch:
16
+ *
17
+ * bunx incanto-play src/game.scene.json --commands .incanto/playtest/fell-seed7.txt
18
+ *
19
+ * Without --behaviors, unregistered scripts are stubbed: the structure plays,
20
+ * your game logic does not. Pass the file that exports your Behavior subclasses
21
+ * to test the real thing.
22
+ *
23
+ * Exit code 1 when no run reached a win, so CI can gate on it.
24
+ */
25
+ import { fileURLToPath, pathToFileURL } from 'node:url';
26
+
27
+ const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
28
+
29
+ function parseArgs(argv) {
30
+ const args = { runs: 20, seconds: 60, seed: 1, out: '.incanto/playtest' };
31
+ for (let i = 0; i < argv.length; i++) {
32
+ const a = argv[i];
33
+ if (a === '--behaviors') args.behaviors = argv[++i];
34
+ else if (a === '--runs') args.runs = Number(argv[++i]);
35
+ else if (a === '--seconds') args.seconds = Number(argv[++i]);
36
+ else if (a === '--seed') args.seed = Number(argv[++i]);
37
+ else if (a === '--out') args.out = argv[++i];
38
+ else if (a === '--no-replays') args.noReplays = true;
39
+ else if (a === '--json') args.json = true;
40
+ else if (a === '--help' || a === '-h') args.help = true;
41
+ else if (a.startsWith('--')) {
42
+ console.error(`unknown flag: ${a}`);
43
+ args.help = true;
44
+ } else if (!args.scene) args.scene = a;
45
+ }
46
+ return args;
47
+ }
48
+
49
+ const args = parseArgs(process.argv.slice(2));
50
+ if (args.help || !args.scene) {
51
+ console.error(`Usage: incanto-playtest <scene.json> [options]
52
+
53
+ --runs N independent seeded runs (default 20)
54
+ --seconds N simulated seconds per run before calling it stuck (default 60)
55
+ --seed N first seed; run i uses seed+i (default 1)
56
+ --behaviors FILE your Behavior subclasses (.ts works on node >= 23.6 / bun)
57
+ --out DIR where failing replays go (default .incanto/playtest)
58
+ --no-replays report only, write nothing
59
+ --json the full report as JSON instead of prose
60
+
61
+ Exits 1 when no run reached a win.`);
62
+ process.exit(args.help && args.scene !== undefined ? 0 : 1);
63
+ }
64
+
65
+ const { playtest, playtestText, failingReplays } = await import(
66
+ pathToFileURL(join(PKG, 'dist', 'test.js')).href
67
+ );
68
+ const incanto = await import(pathToFileURL(join(PKG, 'dist', 'index.js')).href);
69
+
70
+ const behaviors = {};
71
+ if (args.behaviors) {
72
+ let mod;
73
+ try {
74
+ mod = await import(pathToFileURL(resolve(args.behaviors)).href);
75
+ } catch (e) {
76
+ console.error(
77
+ `could not load --behaviors '${args.behaviors}' (${e?.message ?? e}). ` +
78
+ 'Pass the file that exports your Behavior subclasses, e.g. src/behaviors.ts.',
79
+ );
80
+ process.exit(1);
81
+ }
82
+ for (const [name, value] of Object.entries(mod)) {
83
+ if (typeof value === 'function' && value.prototype instanceof incanto.Behavior) {
84
+ behaviors[name] = value;
85
+ }
86
+ }
87
+ if (Object.keys(behaviors).length === 0) {
88
+ console.error(`warning: no Behavior subclasses exported from ${args.behaviors}`);
89
+ }
90
+ }
91
+
92
+ const scenePath = resolve(args.scene);
93
+ const sceneJson = JSON.parse(readFileSync(scenePath, 'utf-8'));
94
+ const sceneDir = dirname(scenePath);
95
+
96
+ let report;
97
+ try {
98
+ report = await playtest(sceneJson, {
99
+ runs: args.runs,
100
+ seconds: args.seconds,
101
+ seed: args.seed,
102
+ behaviors,
103
+ stubMissingBehaviors: !args.behaviors,
104
+ // sub-scenes resolve relative to the scene file, as the loader does
105
+ resolveScene: (p) => JSON.parse(readFileSync(resolve(sceneDir, p), 'utf-8')),
106
+ });
107
+ } catch (e) {
108
+ console.error(`playtest failed: ${e?.message ?? e}`);
109
+ process.exit(1);
110
+ }
111
+
112
+ if (args.json) {
113
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
114
+ } else {
115
+ process.stdout.write(`${playtestText(report)}\n`);
116
+ }
117
+
118
+ // A failing run is only useful if you can watch it. One file per outcome kind —
119
+ // twenty identical "stuck" replays teach nothing that the first one does not.
120
+ if (!args.noReplays && !args.json) {
121
+ const seen = new Set();
122
+ const written = [];
123
+ for (const { seed, outcome, replay } of failingReplays(report)) {
124
+ if (seen.has(outcome)) continue;
125
+ seen.add(outcome);
126
+ const file = join(resolve(args.out), `${outcome}-seed${seed}.json`);
127
+ mkdirSync(dirname(file), { recursive: true });
128
+ writeFileSync(file, JSON.stringify(replay, null, 2));
129
+ written.push(file);
130
+ }
131
+ if (written.length > 0) {
132
+ process.stdout.write(`\n replays: ${written.join('\n ')}\n`);
133
+ }
134
+ }
135
+
136
+ process.exit(report.runs.some((r) => r.outcome === 'won') ? 0 : 1);
package/dist/2d.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { N as Scene$1, S as Scheduler, T as RendererStats, Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-DibCwrW7.js";
1
+ import { N as Scene$1, S as Scheduler, T as RendererStats, Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-5o1EkbLD.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DVwIGZdK.js";
3
3
  import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
4
- import { t as LoadSceneOptions } from "./loader-BqigZsfF.js";
4
+ import { t as LoadSceneOptions } from "./loader-iGq2PT_k.js";
5
5
  import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
6
6
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
7
7
  import * as RapierNs from "@dimforge/rapier2d-compat";
package/dist/2d.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-D-StzrTj.js";
3
- import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-COGCNitr.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-B4v39hca.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-DmGc-Mrn.js";
3
+ import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-DFSW2Y30.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-C8Hi40qb.js";
5
5
  //#region src/2d/library-sprite.ts
6
6
  /**
7
7
  * Turn a library sprite-animation JSON into a scene-ready spritesheet asset
package/dist/3d.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { N as Scene$1, S as Scheduler, T as RendererStats, Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-DibCwrW7.js";
1
+ import { N as Scene$1, S as Scheduler, T as RendererStats, Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-5o1EkbLD.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DVwIGZdK.js";
3
3
  import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
4
- import { t as LoadSceneOptions } from "./loader-BqigZsfF.js";
4
+ import { t as LoadSceneOptions } from "./loader-iGq2PT_k.js";
5
5
  import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
6
- import { n as PathGrid, s as SpatialPose } from "./pathfinding-B3QtdhvZ.js";
6
+ import { n as PathGrid, s as SpatialPose } from "./pathfinding-pPrqtlWS.js";
7
7
  import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
8
8
  import { VRM } from "@pixiv/three-vrm";
9
9
  import { Sky } from "three/examples/jsm/objects/Sky.js";
@@ -1058,6 +1058,12 @@ declare class CharacterController3D extends Node3D {
1058
1058
  sprintMultiplier: number;
1059
1059
  jumpVelocity: number;
1060
1060
  sprintJumpMultiplier: number;
1061
+ coyoteSeconds: number;
1062
+ jumpBufferSeconds: number;
1063
+ jumpCutMultiplier: number;
1064
+ maxJumps: number;
1065
+ airControl: number;
1066
+ fallGravity: number;
1061
1067
  floatHeight: number;
1062
1068
  mouseLook: boolean;
1063
1069
  zoomMin: number;
@@ -1093,6 +1099,11 @@ declare class CharacterController3D extends Node3D {
1093
1099
  /** idle | walk | run | fastRun | airborne — drive animations from this. */
1094
1100
  state: "idle" | "walk" | "run" | "fastRun" | "airborne";
1095
1101
  grounded: boolean;
1102
+ /** @internal Feel timers — see the jump block for what each one buys. */
1103
+ private coyoteLeft;
1104
+ private bufferLeft;
1105
+ private jumpsUsed;
1106
+ private rising;
1096
1107
  private get body();
1097
1108
  override onEnterTree(): void;
1098
1109
  override fixedUpdate(dt: number): void;
package/dist/3d.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-C0MUMSak.js";
3
- import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-fK9oyrrR.js";
4
- import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-DnOu2aAa.js";
2
+ import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-CsJDUQh_.js";
3
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-DWHxLHM5.js";
4
+ import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-QqrrK3yr.js";
5
5
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
6
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-DFJVMYET.js";
6
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-C0mWoWXC.js";
7
7
  //#region src/3d/environment-runtime.ts
8
8
  /**
9
9
  * Live environment editing — the renderer re-applies `scene.environment`
@@ -670,6 +670,17 @@ declare class InputMap {
670
670
  * scripted gameplay tests and touch buttons drive the game by intent
671
671
  * (`press('jump')`) instead of reverse-engineering keybinds.
672
672
  */
673
+ /**
674
+ * Every action this scene declared, with its kind.
675
+ *
676
+ * A scene states its own control vocabulary in `input{}`, which means a tool
677
+ * can drive a game it has never seen — the automated playtester's entire
678
+ * premise. Nothing else could ask: `actions` was private.
679
+ */
680
+ declaredActions(): Array<{
681
+ name: string;
682
+ type: "button" | "vector2";
683
+ }>;
673
684
  pressAction(action: string): void;
674
685
  /** Release an injected button action (yields one justReleased frame). */
675
686
  releaseAction(action: string): void;
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { n as loadScene, o as computeViewport, s as resolveViewport, v as diagnose, w as registerBehavior } from "./loader-DAZlgqeC.js";
3
- import { h as Engine, m as AudioPlayer } from "./register-BHAwM4bK.js";
2
+ import { n as loadScene, o as computeViewport, s as resolveViewport, v as diagnose, w as registerBehavior } from "./loader-r49nDwB4.js";
3
+ import { h as Engine, m as AudioPlayer } from "./register-DOWGnxe1.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
6
6
  import { i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-CCtAMDLB.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-C0MUMSak.js";
8
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-COGCNitr.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-CsJDUQh_.js";
8
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-DFSW2Y30.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { n as enablePhysics2D } from "./physics-2d-B4v39hca.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-C8Hi40qb.js";
11
11
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
12
12
  //#region src/2d/assets.ts
13
13
  /**
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { n as loadScene, v as diagnose, w as registerBehavior } from "./loader-DAZlgqeC.js";
3
- import { h as Engine, m as AudioPlayer } from "./register-BHAwM4bK.js";
2
+ import { n as loadScene, v as diagnose, w as registerBehavior } from "./loader-r49nDwB4.js";
3
+ import { h as Engine, m as AudioPlayer } from "./register-DOWGnxe1.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
6
6
  import { a as poseFromRenderer, i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-CCtAMDLB.js";
7
- import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-C0MUMSak.js";
7
+ import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-CsJDUQh_.js";
8
8
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
9
- import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-fK9oyrrR.js";
10
- import { n as enablePhysics3D } from "./physics-3d-DFJVMYET.js";
9
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-DWHxLHM5.js";
10
+ import { n as enablePhysics3D } from "./physics-3d-C0mWoWXC.js";
11
11
  import { ACESFilmicToneMapping, AmbientLight, Box3, 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";
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { T as RendererStats, b as Engine, kt as LogLevel } from "./behavior-DibCwrW7.js";
1
+ import { T as RendererStats, b as Engine, kt as LogLevel } from "./behavior-5o1EkbLD.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
@@ -1,4 +1,4 @@
1
- import { i as serializeNode, t as buildNodeJson } from "./loader-DAZlgqeC.js";
1
+ import { i as serializeNode, t as buildNodeJson } from "./loader-r49nDwB4.js";
2
2
  //#region src/core/scene/duplicate.ts
3
3
  function stripUids(json) {
4
4
  const { uid: _uid, ...rest } = json;
package/dist/editor.js CHANGED
@@ -4057,7 +4057,7 @@ var Ur = class {
4057
4057
  }) === !0;
4058
4058
  }
4059
4059
  reset(e) {
4060
- this.working = e, this.original = JSON.stringify(e), this.working.root && Wr(this.working.root), this.undoStack = [], this.selection = [], this.emit();
4060
+ this.working = e, this.working.root && Wr(this.working.root), this.original = JSON.stringify(this.working), this.undoStack = [], this.selection = [], this.emit();
4061
4061
  }
4062
4062
  markSaved() {
4063
4063
  this.original = JSON.stringify(this.working), this.emit();
@@ -1,10 +1,10 @@
1
- import { t as registerCoreNodes } from "./register-BHAwM4bK.js";
1
+ import { t as registerCoreNodes } from "./register-DOWGnxe1.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as Rng } from "./rng-DP-SR7eg.js";
4
4
  import { i as getNodeSchema, l as registerNode } from "./registry-IyWCGe4q.js";
5
5
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
6
6
  import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
7
- import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-C0MUMSak.js";
7
+ import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-CsJDUQh_.js";
8
8
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
9
9
  import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
10
10
  import { clone } from "three/addons/utils/SkeletonUtils.js";
@@ -2488,11 +2488,9 @@ var Camera3D = class extends Node3D {
2488
2488
  //#endregion
2489
2489
  //#region src/3d/nodes/character-controller-3d.ts
2490
2490
  const ACC_DELTA_TIME = 8;
2491
- const AIR_DRAG = .2;
2492
2491
  const DRAG_DAMPING = .15;
2493
2492
  const SPRING_K = 1.2;
2494
2493
  const SPRING_DAMPING = .08;
2495
- const FALLING_GRAVITY_SCALE = 2.5;
2496
2494
  const FALLING_MAX_VEL = -20;
2497
2495
  const LOOK_SPEED = .002;
2498
2496
  const RAD2DEG = 180 / Math.PI;
@@ -2539,6 +2537,18 @@ var CharacterController3D = class extends Node3D {
2539
2537
  sprintMultiplier: { default: 2 },
2540
2538
  jumpVelocity: { default: 4 },
2541
2539
  sprintJumpMultiplier: { default: 1.2 },
2540
+ /** Jump this long AFTER walking off a ledge. The single biggest one. */
2541
+ coyoteSeconds: { default: 0 },
2542
+ /** Press jump this long BEFORE landing and still get it. */
2543
+ jumpBufferSeconds: { default: 0 },
2544
+ /** Releasing jump early cuts the rise to this fraction (1 = fixed height). */
2545
+ jumpCutMultiplier: { default: 1 },
2546
+ /** 2 = double jump. Extra jumps work in mid-air. */
2547
+ maxJumps: { default: 1 },
2548
+ /** How much of your ground control you keep in the air (0 = none, 1 = full). */
2549
+ airControl: { default: .2 },
2550
+ /** Gravity multiplier while falling — >1 is the "snappy" arc. */
2551
+ fallGravity: { default: 2.5 },
2542
2552
  floatHeight: { default: .01 },
2543
2553
  mouseLook: { default: true },
2544
2554
  zoomMin: { default: 0 },
@@ -2565,6 +2575,12 @@ var CharacterController3D = class extends Node3D {
2565
2575
  sprintMultiplier = 2;
2566
2576
  jumpVelocity = 4;
2567
2577
  sprintJumpMultiplier = 1.2;
2578
+ coyoteSeconds = 0;
2579
+ jumpBufferSeconds = 0;
2580
+ jumpCutMultiplier = 1;
2581
+ maxJumps = 1;
2582
+ airControl = .2;
2583
+ fallGravity = 2.5;
2568
2584
  floatHeight = .01;
2569
2585
  mouseLook = true;
2570
2586
  zoomMin = 0;
@@ -2600,6 +2616,11 @@ var CharacterController3D = class extends Node3D {
2600
2616
  /** idle | walk | run | fastRun | airborne — drive animations from this. */
2601
2617
  state = "idle";
2602
2618
  grounded = false;
2619
+ /** @internal Feel timers — see the jump block for what each one buys. */
2620
+ coyoteLeft = 0;
2621
+ bufferLeft = 0;
2622
+ jumpsUsed = 0;
2623
+ rising = false;
2603
2624
  get body() {
2604
2625
  const parent = this.parent;
2605
2626
  if (!(parent instanceof RigidBody3D)) throw new IncantoError("TREE_VIOLATION", `CharacterController3D '${this.name}' must be a child of a RigidBody3D (parent is '${parent?.name ?? "none"}').`);
@@ -2653,6 +2674,7 @@ var CharacterController3D = class extends Node3D {
2653
2674
  }
2654
2675
  }
2655
2676
  this.grounded = toi !== null && toi < floatingDis * 2;
2677
+ const footed = toi !== null && toi < floatingDis * 1.3 && (vel[1] ?? 0) <= .5;
2656
2678
  let mx = 0;
2657
2679
  let mz = 0;
2658
2680
  let moving = false;
@@ -2673,7 +2695,7 @@ var CharacterController3D = class extends Node3D {
2673
2695
  const intensity = keyboardIntensity(moving, sprinting);
2674
2696
  const speedMultiplier = 1 + (this.sprintMultiplier - 1) * intensity;
2675
2697
  const targetSpeed = moving ? this.maxSpeed * speedMultiplier : 0;
2676
- const control = this.grounded ? 1 : AIR_DRAG;
2698
+ const control = this.grounded ? 1 : this.airControl;
2677
2699
  const ax = (mx * targetSpeed - vel[0]) / ACC_DELTA_TIME * control;
2678
2700
  const az = (mz * targetSpeed - vel[2]) / ACC_DELTA_TIME * control;
2679
2701
  body.applyImpulse([
@@ -2694,7 +2716,18 @@ var CharacterController3D = class extends Node3D {
2694
2716
  0
2695
2717
  ]);
2696
2718
  }
2697
- if (this.grounded && safePressed(input, this.jumpAction)) {
2719
+ if (footed) {
2720
+ this.coyoteLeft = this.coyoteSeconds;
2721
+ this.jumpsUsed = 0;
2722
+ this.rising = false;
2723
+ } else {
2724
+ this.coyoteLeft = Math.max(0, this.coyoteLeft - dt);
2725
+ if (this.coyoteLeft <= 0 && this.jumpsUsed === 0) this.jumpsUsed = 1;
2726
+ }
2727
+ const pressedJump = safeJustPressed(input, this.jumpAction);
2728
+ if (pressedJump) this.bufferLeft = Math.max(this.jumpBufferSeconds, 0);
2729
+ else this.bufferLeft = Math.max(0, this.bufferLeft - dt);
2730
+ if ((this.bufferLeft > 0 || pressedJump) && (footed || this.coyoteLeft > 0 || this.jumpsUsed < this.maxJumps)) {
2698
2731
  const jumpVel = this.jumpVelocity * (1 + (this.sprintJumpMultiplier - 1) * intensity);
2699
2732
  body.linearVelocity = [
2700
2733
  vel[0],
@@ -2702,16 +2735,34 @@ var CharacterController3D = class extends Node3D {
2702
2735
  vel[2]
2703
2736
  ];
2704
2737
  this.grounded = false;
2738
+ this.jumpsUsed += 1;
2739
+ this.bufferLeft = 0;
2740
+ this.coyoteLeft = 0;
2741
+ this.rising = true;
2742
+ vel[1] = jumpVel;
2743
+ }
2744
+ if (this.rising) {
2745
+ if ((vel[1] ?? 0) <= 0) this.rising = false;
2746
+ else if (this.jumpCutMultiplier < 1 && !safePressed(input, this.jumpAction)) {
2747
+ const cut = (vel[1] ?? 0) * this.jumpCutMultiplier;
2748
+ body.linearVelocity = [
2749
+ vel[0],
2750
+ cut,
2751
+ vel[2]
2752
+ ];
2753
+ vel[1] = cut;
2754
+ this.rising = false;
2755
+ }
2705
2756
  }
2706
- if (vel[1] < FALLING_MAX_VEL) body.gravityScale = 0;
2707
- else if (vel[1] < 0 && !this.grounded) body.gravityScale = FALLING_GRAVITY_SCALE;
2757
+ if ((vel[1] ?? 0) < FALLING_MAX_VEL) body.gravityScale = 0;
2758
+ else if ((vel[1] ?? 0) < 0 && !footed) body.gravityScale = this.fallGravity;
2708
2759
  else body.gravityScale = 1;
2709
2760
  if (this.view === "side" && Math.abs(pos[2]) > .01) body.position = [
2710
2761
  body.position[0] ?? 0,
2711
2762
  body.position[1] ?? 0,
2712
2763
  0
2713
2764
  ];
2714
- const next = movementState(this.grounded, moving ? intensity : 0);
2765
+ const next = movementState(footed, moving ? intensity : 0);
2715
2766
  if (next !== this.state) {
2716
2767
  this.state = next;
2717
2768
  const clip = this.animations[next];
@@ -2855,6 +2906,21 @@ function safePressed(input, action) {
2855
2906
  return false;
2856
2907
  }
2857
2908
  }
2909
+ /**
2910
+ * The EDGE, not the level.
2911
+ *
2912
+ * Jump used to read `isPressed`, so holding the button re-jumped on every frame
2913
+ * the raycast found ground — an auto-hop nobody asked for, and one that makes
2914
+ * coyote time incoherent (a held button would re-fire through the whole
2915
+ * window). Same undeclared-action tolerance as `safePressed`.
2916
+ */
2917
+ function safeJustPressed(input, action) {
2918
+ try {
2919
+ return input.justPressed(action);
2920
+ } catch {
2921
+ return false;
2922
+ }
2923
+ }
2858
2924
  //#endregion
2859
2925
  //#region src/3d/vegetation/grass-placement.ts
2860
2926
  const NOISE_SCALE = .15;
@@ -1,7 +1,7 @@
1
- import { f as Node, h as effectiveOrder, n as loadScene, w as registerBehavior, y as Behavior } from "./loader-DAZlgqeC.js";
1
+ import { f as Node, h as effectiveOrder, n as loadScene, w as registerBehavior, y as Behavior } from "./loader-r49nDwB4.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as jsonClone } from "./json-BLk7H2Qa.js";
4
- import { t as duplicateNode } from "./duplicate-Cvb1BSca.js";
4
+ import { t as duplicateNode } from "./duplicate-CRtihGmC.js";
5
5
  import { Color, CubeCamera, DepthTexture, DoubleSide, Euler, FloatType, Frustum, HalfFloatType, LinearMipmapLinearFilter, MathUtils, Matrix4, Mesh, MeshDepthMaterial, Object3D, PerspectiveCamera, Plane, PlaneGeometry, Quaternion, ShaderMaterial, Sphere, Vector2, Vector3, Vector4, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
6
6
  //#region src/3d/frustum.ts
7
7
  const scratchFrustum = new Frustum();
@@ -1,4 +1,4 @@
1
- import { Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, t as Behavior } from "./behavior-DibCwrW7.js";
1
+ import { Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, t as Behavior } from "./behavior-5o1EkbLD.js";
2
2
  import { c as JsonValue, s as JsonObject } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/gameplay/chase.d.ts
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { C as FollowCamera, D as Health, E as DamageOnContact, O as Collector, S as restartScene, T as phaseOf, _ as hitStop, a as Wander, b as GameFlow, c as Projectile, d as PathFollow, f as Oscillate, g as Cooldown, h as CameraShake, i as WaveSpawner, k as Chase, l as Pickup, m as Lifetime, n as registerGameplayBehaviors, o as Spawner, p as MoveTo, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as screenFlash, w as DayNight, x as goToScene, y as Interactable } from "./gameplay-C0MUMSak.js";
1
+ import { C as FollowCamera, D as Health, E as DamageOnContact, O as Collector, S as restartScene, T as phaseOf, _ as hitStop, a as Wander, b as GameFlow, c as Projectile, d as PathFollow, f as Oscillate, g as Cooldown, h as CameraShake, i as WaveSpawner, k as Chase, l as Pickup, m as Lifetime, n as registerGameplayBehaviors, o as Spawner, p as MoveTo, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as screenFlash, w as DayNight, x as goToScene, y as Interactable } from "./gameplay-CsJDUQh_.js";
2
2
  export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };