incanto 0.11.0 → 0.13.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/README.md +22 -0
- package/bin/incanto-new.mjs +87 -0
- package/dist/2d.d.ts +2 -2
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +246 -71
- package/dist/3d.js +143 -4
- package/dist/{behavior-Bod1AyJS.d.ts → behavior-CPibUfnH.d.ts} +4 -2
- package/dist/{create-game-CdWhP6ve.js → create-game-Ct86zczq.js} +5 -5
- package/dist/{create-game-DZ_4Nfq8.js → create-game-J6g4QInv.js} +75 -24
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-BEvGBtb_.js → duplicate-BINwP0WG.js} +1 -1
- package/dist/{gameplay-Dn3yidJw.js → gameplay-sSqrUcnz.js} +75 -3
- package/dist/gameplay.d.ts +37 -2
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +3 -30
- package/dist/index.js +4 -4
- package/dist/{loader-B4OEXDZ8.js → loader-BZqOKfI2.js} +3 -1
- package/dist/{loader-BcUDfNHn.d.ts → loader-DILt9PGC.d.ts} +1 -1
- package/dist/net.d.ts +1 -1
- package/dist/net.js +3 -3
- package/dist/{audio-player-Dyjg5k92.d.ts → pathfinding-RWYkNKx9.d.ts} +29 -2
- package/dist/{physics-2d-DweTURFt.js → physics-2d-BiIdl51r.js} +8 -4
- package/dist/{physics-3d-Br97Ans2.js → physics-3d-DhaAaKJo.js} +8 -4
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-Cwc-yCYx.js → register-BFFE1Mh1.js} +2 -2
- package/dist/{register-ysLR3WQS.js → register-BFw4c83z.js} +508 -123
- package/dist/{register-BD6zgrv_.js → register-Djwckzgx.js} +2 -2
- package/dist/{register-DJZXxwAn.js → register-nObreUQR.js} +1 -1
- package/dist/test.d.ts +2 -2
- package/dist/test.js +10 -10
- package/editor/assets/{agent8-BxNvAcLA.js → agent8-BF8Jfbkl.js} +1 -1
- package/editor/assets/{index-DMCQXowF.js → index-DSq0gdhE.js} +53 -41
- package/editor/index.html +1 -1
- package/package.json +4 -2
- package/schemas/scene.schema.json +263 -0
- package/skills/incanto-3d-character.md +37 -1
- package/skills/incanto-building-3d-games.md +83 -0
- package/skills/incanto-editor.md +3 -3
- package/skills/incanto-gameplay-behaviors.md +23 -1
- package/skills/incanto-node-reference.md +51 -0
- package/templates-app/beacon-isle-3d/PROJECT/Context.md +44 -0
- package/templates-app/beacon-isle-3d/PROJECT/Requirements.md +16 -0
- package/templates-app/beacon-isle-3d/PROJECT/Status.md +38 -0
- package/templates-app/beacon-isle-3d/PROJECT/Structure.md +24 -0
- package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +38 -0
- package/templates-app/beacon-isle-3d/generate-world.ts +814 -0
- package/templates-app/beacon-isle-3d/index.html +68 -0
- package/templates-app/beacon-isle-3d/package.json +26 -0
- package/templates-app/beacon-isle-3d/src/behaviors.ts +570 -0
- package/templates-app/beacon-isle-3d/src/game.scene.json +2992 -0
- package/templates-app/beacon-isle-3d/src/main.ts +33 -0
- package/templates-app/beacon-isle-3d/tsconfig.json +13 -0
- package/templates-app/beacon-isle-3d/verify.ts +285 -0
- package/templates-app/beacon-isle-3d/vite.config.ts +5 -0
- package/templates-app/tps-3d/PROJECT/Context.md +49 -0
- package/templates-app/tps-3d/PROJECT/Requirements.md +38 -0
- package/templates-app/tps-3d/PROJECT/Status.md +41 -0
- package/templates-app/tps-3d/PROJECT/Structure.md +44 -0
- package/templates-app/tps-3d/docs/project-3d-rules.md +38 -0
- package/templates-app/tps-3d/index.html +244 -0
- package/templates-app/tps-3d/package.json +25 -0
- package/templates-app/tps-3d/src/behaviors.ts +405 -0
- package/templates-app/tps-3d/src/game.scene.json +677 -0
- package/templates-app/tps-3d/src/main.ts +64 -0
- package/templates-app/tps-3d/tsconfig.json +13 -0
- package/templates-app/tps-3d/verify.ts +167 -0
- package/templates-app/tps-3d/vite.config.ts +5 -0
- package/templates-app/village-quest-3d/PROJECT/Context.md +39 -0
- package/templates-app/village-quest-3d/PROJECT/Requirements.md +36 -0
- package/templates-app/village-quest-3d/PROJECT/Status.md +26 -0
- package/templates-app/village-quest-3d/PROJECT/Structure.md +30 -0
- package/templates-app/village-quest-3d/docs/project-3d-rules.md +38 -0
- package/templates-app/village-quest-3d/index.html +68 -0
- package/templates-app/village-quest-3d/package.json +25 -0
- package/templates-app/village-quest-3d/src/behaviors.ts +529 -0
- package/templates-app/village-quest-3d/src/grove.scene.json +826 -0
- package/templates-app/village-quest-3d/src/main.ts +35 -0
- package/templates-app/village-quest-3d/src/quest.ts +18 -0
- package/templates-app/village-quest-3d/src/village.scene.json +680 -0
- package/templates-app/village-quest-3d/tsconfig.json +13 -0
- package/templates-app/village-quest-3d/verify.ts +254 -0
- package/templates-app/village-quest-3d/vite.config.ts +5 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless proof that Emberwood actually PLAYS — author → VERIFY → fix,
|
|
3
|
+
* no browser. Three passes:
|
|
4
|
+
*
|
|
5
|
+
* AUDIT : auditScene finds nothing to warn about in either scene.
|
|
6
|
+
* QUEST : the full loop — talk to the Elder (UiDialogue), accept via
|
|
7
|
+
* choose(0), walk the north gate (goToScene → grove), wolves are
|
|
8
|
+
* walking their A* patrol rings, three sword strikes clear them,
|
|
9
|
+
* the portal returns home, reporting back completes the quest.
|
|
10
|
+
* REPLAY: the same recorded inputs replayed into a fresh engine land the
|
|
11
|
+
* player on the BIT-IDENTICAL position — the determinism contract
|
|
12
|
+
* (seeded rng + fixed clock) that makes recordings regressions.
|
|
13
|
+
*
|
|
14
|
+
* Run with `bun verify.ts` (or `bun run verify`).
|
|
15
|
+
*/
|
|
16
|
+
import type { UiBanner, UiDialogue, UiText } from 'incanto';
|
|
17
|
+
import { Engine, loadScene, registerBehavior, replay, startRecording } from 'incanto';
|
|
18
|
+
import { enablePhysics3D } from 'incanto/3d';
|
|
19
|
+
import { auditScene, registerAllNodes, runScript } from 'incanto/test';
|
|
20
|
+
import { BEHAVIORS, cellToWorld } from './src/behaviors';
|
|
21
|
+
import groveJson from './src/grove.scene.json';
|
|
22
|
+
import { quest, resetQuest } from './src/quest';
|
|
23
|
+
import villageJson from './src/village.scene.json';
|
|
24
|
+
|
|
25
|
+
interface Vec {
|
|
26
|
+
position: number[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ok = (label: string, cond: boolean): void => {
|
|
30
|
+
if (!cond) {
|
|
31
|
+
console.error(`FAIL: ${label}`);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
} else {
|
|
34
|
+
console.log(`pass: ${label}`);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ---- AUDIT ------------------------------------------------------------------------
|
|
39
|
+
{
|
|
40
|
+
registerAllNodes();
|
|
41
|
+
for (const [name, ctor] of Object.entries(BEHAVIORS)) {
|
|
42
|
+
registerBehavior(name, ctor, { replace: true });
|
|
43
|
+
}
|
|
44
|
+
const w1 = auditScene(villageJson as unknown as Parameters<typeof auditScene>[0]);
|
|
45
|
+
const w2 = auditScene(groveJson as unknown as Parameters<typeof auditScene>[0]);
|
|
46
|
+
ok(`auditScene(village) is clean (${w1.length} warnings)`, w1.length === 0);
|
|
47
|
+
ok(`auditScene(grove) is clean (${w2.length} warnings)`, w2.length === 0);
|
|
48
|
+
for (const w of [...w1, ...w2]) console.error(` warn: ${w}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- QUEST — the whole loop through both scenes ------------------------------------
|
|
52
|
+
{
|
|
53
|
+
resetQuest();
|
|
54
|
+
let sceneName = (): string => 'unset';
|
|
55
|
+
let wolvesWalked = false;
|
|
56
|
+
|
|
57
|
+
const result = await runScript(villageJson, {
|
|
58
|
+
durationMs: 60000,
|
|
59
|
+
seed: 7,
|
|
60
|
+
behaviors: BEHAVIORS,
|
|
61
|
+
steps: [
|
|
62
|
+
{
|
|
63
|
+
atMs: 100,
|
|
64
|
+
label: 'talk to the Elder and accept the quest',
|
|
65
|
+
do: (ctx) => {
|
|
66
|
+
sceneName = () => ctx.engine.scene?.source?.name as string;
|
|
67
|
+
const player = ctx.getNode('/root/Player') as unknown as Vec;
|
|
68
|
+
player.position = [3, 1.4, 0.2]; // step up to the Elder
|
|
69
|
+
for (let i = 0; i < 5; i++) ctx.engine.step();
|
|
70
|
+
ctx.engine.input.pressAction('interact');
|
|
71
|
+
ctx.engine.step();
|
|
72
|
+
ctx.engine.input.releaseAction('interact');
|
|
73
|
+
const talk = ctx.getNode('/root/HUD/Dialogue') as UiDialogue;
|
|
74
|
+
if (!talk.active) throw new Error('dialogue did not open');
|
|
75
|
+
// let line 1 type out, advance, let the choice line type out
|
|
76
|
+
for (let i = 0; i < 150; i++) ctx.engine.step();
|
|
77
|
+
talk.advance();
|
|
78
|
+
for (let i = 0; i < 150; i++) ctx.engine.step();
|
|
79
|
+
talk.choose(0); // "I will go"
|
|
80
|
+
ctx.engine.step();
|
|
81
|
+
if (!quest.accepted) throw new Error('choice did not accept the quest');
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
atMs: 6000,
|
|
86
|
+
label: 'north gate swaps to the grove (goToScene)',
|
|
87
|
+
do: (ctx) => {
|
|
88
|
+
const player = ctx.getNode('/root/Player') as unknown as Vec;
|
|
89
|
+
player.position = [0, 1.5, -26.4]; // into the gate trigger
|
|
90
|
+
for (let i = 0; i < 30 && sceneName() !== 'WolfGrove'; i++) ctx.engine.step();
|
|
91
|
+
if (sceneName() !== 'WolfGrove') throw new Error(`still in ${sceneName()}`);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
atMs: 8000,
|
|
96
|
+
label: 'wolves walk their A* patrol rings',
|
|
97
|
+
do: (ctx) => {
|
|
98
|
+
const root = ctx.engine.scene?.tree.root;
|
|
99
|
+
if (!root) throw new Error('no scene');
|
|
100
|
+
const wolf = root.getNode('Wolves/Wolf1') as unknown as Vec;
|
|
101
|
+
const before = [...wolf.position];
|
|
102
|
+
for (let i = 0; i < 90; i++) ctx.engine.step();
|
|
103
|
+
const dx = (wolf.position[0] ?? 0) - (before[0] ?? 0);
|
|
104
|
+
const dz = (wolf.position[2] ?? 0) - (before[2] ?? 0);
|
|
105
|
+
wolvesWalked = Math.hypot(dx, dz) > 0.5;
|
|
106
|
+
if (!wolvesWalked) throw new Error('Wolf1 never moved');
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
atMs: 9000,
|
|
111
|
+
label: 'wolves HUNT: player inside aggro range pulls a wolf off patrol',
|
|
112
|
+
do: (ctx) => {
|
|
113
|
+
const engine = ctx.engine;
|
|
114
|
+
const root = engine.scene?.tree.root;
|
|
115
|
+
if (!root) throw new Error('no scene');
|
|
116
|
+
const wolf = root.getNode('Wolves/Wolf2') as unknown as Vec;
|
|
117
|
+
const player = root.getNode('Player') as unknown as Vec;
|
|
118
|
+
// stand on an OPEN cell on Wolf2's patrol row and wait for it to
|
|
119
|
+
// come into aggro range — it should re-path and close to bite range
|
|
120
|
+
const [ox, , oz] = cellToWorld(9, 1, 0);
|
|
121
|
+
const gap = () =>
|
|
122
|
+
Math.hypot((wolf.position[0] ?? 0) - (ox ?? 0), (wolf.position[2] ?? 0) - (oz ?? 0));
|
|
123
|
+
let reached = false;
|
|
124
|
+
for (let i = 0; i < 900 && !reached; i++) {
|
|
125
|
+
player.position = [ox ?? 0, 1.4, oz ?? 0];
|
|
126
|
+
engine.step();
|
|
127
|
+
reached = gap() < 1.2;
|
|
128
|
+
}
|
|
129
|
+
if (!reached) throw new Error(`wolf never closed in (gap ${gap().toFixed(2)})`);
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
atMs: 11000,
|
|
134
|
+
label: 'a bite drains HP; 0 HP → YOU FELL → fresh grove respawn',
|
|
135
|
+
do: (ctx) => {
|
|
136
|
+
const engine = ctx.engine;
|
|
137
|
+
const root = () => engine.scene?.tree.root;
|
|
138
|
+
const health = () =>
|
|
139
|
+
root()?.getNode('Player')?.behavior as unknown as { current: number };
|
|
140
|
+
const wolves = () => engine.scene?.tree.getNodesInGroup('wolf') ?? [];
|
|
141
|
+
// stand ON a wolf until dead (invulnerability windows between bites)
|
|
142
|
+
for (let i = 0; i < 3000 && (health()?.current ?? 0) > 0; i++) {
|
|
143
|
+
const w = wolves()[0] as unknown as Vec | undefined;
|
|
144
|
+
if (!w) throw new Error('no wolves left to bite');
|
|
145
|
+
const p = root()?.getNode('Player') as unknown as Vec;
|
|
146
|
+
p.position = [w.position[0] ?? 0, 1.4, w.position[2] ?? 0];
|
|
147
|
+
engine.step();
|
|
148
|
+
}
|
|
149
|
+
if ((health()?.current ?? 1) > 0) throw new Error('wolves never killed the player');
|
|
150
|
+
// respawn countdown (1.6s) → fresh grove with full wolves + full HP
|
|
151
|
+
for (let i = 0; i < 130; i++) engine.step();
|
|
152
|
+
if (wolves().length !== 3) throw new Error(`respawn left ${wolves().length} wolves`);
|
|
153
|
+
if ((health()?.current ?? 0) < 100) throw new Error('respawn did not reset HP');
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
atMs: 14000,
|
|
158
|
+
label: 'three strikes clear the grove',
|
|
159
|
+
do: (ctx) => {
|
|
160
|
+
const engine = ctx.engine;
|
|
161
|
+
const root = engine.scene?.tree.root;
|
|
162
|
+
if (!root) throw new Error('no scene');
|
|
163
|
+
const player = root.getNode('Player') as unknown as Vec;
|
|
164
|
+
// wolves take TWO strikes now (first = flinch knockback)
|
|
165
|
+
for (let round = 0; round < 6; round++) {
|
|
166
|
+
const wolves = engine.scene?.tree.getNodesInGroup('wolf') ?? [];
|
|
167
|
+
const target = wolves[0] as unknown as Vec | undefined;
|
|
168
|
+
if (!target) break;
|
|
169
|
+
player.position = [
|
|
170
|
+
(target.position[0] ?? 0) + 0.8,
|
|
171
|
+
1.4,
|
|
172
|
+
(target.position[2] ?? 0) + 0.8,
|
|
173
|
+
];
|
|
174
|
+
engine.step();
|
|
175
|
+
engine.input.pressAction('attack');
|
|
176
|
+
engine.step();
|
|
177
|
+
engine.input.releaseAction('attack');
|
|
178
|
+
for (let i = 0; i < 40; i++) engine.step();
|
|
179
|
+
}
|
|
180
|
+
const left = engine.scene?.tree.getNodesInGroup('wolf').length ?? -1;
|
|
181
|
+
if (left !== 0) throw new Error(`${left} wolves survived`);
|
|
182
|
+
for (let i = 0; i < 5; i++) engine.step(); // director notices → cleared
|
|
183
|
+
if (!quest.cleared) throw new Error('quest.cleared never set');
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
atMs: 20000,
|
|
188
|
+
label: 'portal home + report back = QUEST COMPLETE',
|
|
189
|
+
do: (ctx) => {
|
|
190
|
+
const engine = ctx.engine;
|
|
191
|
+
const root = () => engine.scene?.tree.root;
|
|
192
|
+
const player = () => root()?.getNode('Player') as unknown as Vec;
|
|
193
|
+
player().position = [3.5, 1.4, 13.5]; // into the return portal
|
|
194
|
+
for (let i = 0; i < 30 && sceneName() !== 'EmberwoodVillage'; i++) engine.step();
|
|
195
|
+
if (sceneName() !== 'EmberwoodVillage') throw new Error(`still in ${sceneName()}`);
|
|
196
|
+
player().position = [3, 1.4, 0.2];
|
|
197
|
+
for (let i = 0; i < 5; i++) engine.step();
|
|
198
|
+
engine.input.pressAction('interact');
|
|
199
|
+
engine.step();
|
|
200
|
+
engine.input.releaseAction('interact');
|
|
201
|
+
const talk = root()?.getNode('HUD/Dialogue') as UiDialogue;
|
|
202
|
+
for (let i = 0; i < 400; i++) engine.step();
|
|
203
|
+
talk.advance();
|
|
204
|
+
for (let i = 0; i < 400; i++) engine.step();
|
|
205
|
+
talk.advance();
|
|
206
|
+
for (let i = 0; i < 10; i++) engine.step();
|
|
207
|
+
if (!quest.done) throw new Error('quest never completed');
|
|
208
|
+
const bannerText = (root()?.getNode('HUD/Banner') as UiBanner & { text?: string }) ?? {};
|
|
209
|
+
const questLine = (root()?.getNode('HUD/Quest') as UiText).text;
|
|
210
|
+
if (!questLine.includes('safe')) throw new Error(`quest line: ${questLine}`);
|
|
211
|
+
void bannerText;
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
ok('QUEST: full loop (dialogue → grove → wolves → report back)', result.ok);
|
|
218
|
+
for (const f of result.failures) console.error(` ${f.label}: ${f.message}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---- REPLAY — recorded inputs are a regression test ---------------------------------
|
|
222
|
+
{
|
|
223
|
+
resetQuest();
|
|
224
|
+
const boot = async (): Promise<Engine> => {
|
|
225
|
+
const engine = new Engine({ seed: 42, scheduler: () => () => {} });
|
|
226
|
+
engine.setScene(loadScene(structuredClone(villageJson) as Parameters<typeof loadScene>[0]));
|
|
227
|
+
await enablePhysics3D(engine);
|
|
228
|
+
return engine;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const a = await boot();
|
|
232
|
+
const rec = startRecording(a);
|
|
233
|
+
for (let frame = 0; frame <= 300; frame++) {
|
|
234
|
+
if (frame === 20) a.input.handleKey('KeyW', true);
|
|
235
|
+
if (frame === 160) a.input.handleKey('KeyD', true);
|
|
236
|
+
if (frame === 240) a.input.handleKey('KeyW', false);
|
|
237
|
+
a.tick(frame * (1000 / 60));
|
|
238
|
+
}
|
|
239
|
+
const recording = rec.stop();
|
|
240
|
+
const posA = (a.scene?.tree.root?.getNode('Player') as unknown as Vec).position;
|
|
241
|
+
|
|
242
|
+
resetQuest();
|
|
243
|
+
const b = await boot();
|
|
244
|
+
replay(b, recording);
|
|
245
|
+
const posB = (b.scene?.tree.root?.getNode('Player') as unknown as Vec).position;
|
|
246
|
+
|
|
247
|
+
ok(
|
|
248
|
+
`REPLAY: bit-identical player position after 300 replayed frames (${posA.join(', ')})`,
|
|
249
|
+
posA.length === 3 && posA.every((v, i) => v === posB[i]),
|
|
250
|
+
);
|
|
251
|
+
ok('REPLAY: the player actually moved', Math.hypot(posA[0] ?? 0, (posA[2] ?? 0) - 10) > 1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(process.exitCode ? '\nVERIFY FAILED' : '\nVERIFY OK');
|