incanto 0.12.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 +194 -71
- package/dist/3d.js +74 -4
- package/dist/{behavior-Bod1AyJS.d.ts → behavior-CPibUfnH.d.ts} +4 -2
- package/dist/{create-game-C_xf4gm9.js → create-game-Ct86zczq.js} +5 -5
- package/dist/{create-game-B2fewJUy.js → create-game-J6g4QInv.js} +5 -5
- 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 -3
- 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/{pathfinding-BSiXjd6W.d.ts → pathfinding-RWYkNKx9.d.ts} +1 -1
- package/dist/{physics-2d-B6D864oZ.js → physics-2d-BiIdl51r.js} +2 -2
- package/dist/{physics-3d-RLNaxRyw.js → physics-3d-DhaAaKJo.js} +2 -2
- 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-D_4QY8Aq.js → agent8-BF8Jfbkl.js} +1 -1
- package/editor/assets/{index-s0DZuroe.js → index-DSq0gdhE.js} +74 -74
- package/editor/index.html +1 -1
- package/package.json +4 -2
- package/schemas/scene.schema.json +263 -0
- package/skills/incanto-3d-character.md +35 -0
- package/skills/incanto-building-3d-games.md +48 -0
- 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,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanguard — a 3D THIRD-PERSON shooter built on Incanto. Move with WASD, look
|
|
3
|
+
* with the mouse (click locks the pointer), Shift to sprint, Space to jump,
|
|
4
|
+
* left-click / F to fire your hitscan rifle, R to reload. The camera rides over
|
|
5
|
+
* your soldier's shoulder (CharacterController3D `view: 'free'`); the crosshair
|
|
6
|
+
* is where you shoot. Clear three waves of chasing enemies to SECURE THE AREA;
|
|
7
|
+
* let them drain your HP to 0 and the MISSION FAILS.
|
|
8
|
+
*
|
|
9
|
+
* The whole loop is composed from built-in gameplay behaviors wired in
|
|
10
|
+
* `game.scene.json` (third-person movement+camera, enemy AI via Chase, contact
|
|
11
|
+
* damage, WaveSpawner, Health, ScoreKeeper). The only custom TypeScript is
|
|
12
|
+
* `Shoot` (the click-to-fire hitscan + face-aim) and `HudUpdater` (the HUD +
|
|
13
|
+
* win/lose banner).
|
|
14
|
+
*/
|
|
15
|
+
import { assetUrls, preloadUrls } from 'incanto';
|
|
16
|
+
import { createGame3D } from 'incanto/3d';
|
|
17
|
+
// A bundled clip looped as low ambience (no large music files ship in the
|
|
18
|
+
// package — see incanto-audio.md). vite resolves + hashes the real file.
|
|
19
|
+
import musicUrl from 'incanto/assets/audio/spells_cast.mp3';
|
|
20
|
+
import { HudUpdater, Shoot } from './behaviors';
|
|
21
|
+
import sceneJson from './game.scene.json';
|
|
22
|
+
|
|
23
|
+
// The soldier is a real GLB character (base-model.glb) animated by mixamo clips
|
|
24
|
+
// (idle/walk/run/jump) — declared in the scene `assets`. Warm the HTTP cache with
|
|
25
|
+
// a progress bar before booting; the arena, enemies, SFX + music stay zero-fetch.
|
|
26
|
+
const fill = document.querySelector('#progress') as HTMLElement | null;
|
|
27
|
+
const text = document.querySelector('#progress-text') as HTMLElement | null;
|
|
28
|
+
await preloadUrls(assetUrls(sceneJson.assets), (done, total) => {
|
|
29
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 100;
|
|
30
|
+
if (fill) fill.style.width = `${pct}%`;
|
|
31
|
+
if (text) text.textContent = `${pct}%`;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
|
|
35
|
+
// createGame3D boots everything: register (gameplay behaviors auto-registered) →
|
|
36
|
+
// load → physics (auto; the scene has bodies) → keyboard + pointer-lock look →
|
|
37
|
+
// renderer → start. We pass the two custom behaviors.
|
|
38
|
+
const game = await createGame3D({
|
|
39
|
+
debug: import.meta.env.VITE_INCANTO_DEBUG === '1',
|
|
40
|
+
canvas,
|
|
41
|
+
scene: sceneJson,
|
|
42
|
+
behaviors: { Shoot, HudUpdater },
|
|
43
|
+
pointer: true, // pointer-lock mouse look (lockOnClick) — drives the free-view yaw/pitch
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
document.querySelector('#loading')?.remove();
|
|
47
|
+
|
|
48
|
+
// Looping background music on the music bus (a calm tense pad). The engine
|
|
49
|
+
// auto-resumes a gesture-blocked track on the first click/keypress.
|
|
50
|
+
game.engine.music.play(musicUrl, { loop: true, fadeIn: 1.5 });
|
|
51
|
+
game.engine.audio.music = 0.3;
|
|
52
|
+
|
|
53
|
+
// On-screen FIRE / JUMP buttons (mobile + desktop parity).
|
|
54
|
+
for (const [id, code] of [
|
|
55
|
+
['btn-fire', 'KeyF'],
|
|
56
|
+
['btn-jump', 'Space'],
|
|
57
|
+
] as const) {
|
|
58
|
+
const el = document.querySelector(`#${id}`);
|
|
59
|
+
el?.addEventListener('pointerdown', () => game.engine.input.handleKey(code, true));
|
|
60
|
+
el?.addEventListener('pointerup', () => game.engine.input.handleKey(code, false));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Console playground for vibe-coding: game.scene.root.getNode('Player') …
|
|
64
|
+
Object.assign(window, { game });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"types": ["vite/client", "node"]
|
|
11
|
+
},
|
|
12
|
+
"include": ["src", "verify.ts"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless proof that Vanguard actually PLAYS — the browserless half of the
|
|
3
|
+
* agent loop (author → VERIFY → fix). Runs the real `game.scene.json` through
|
|
4
|
+
* `incanto/test`'s `runScript` at a fixed, seeded timestep and asserts the whole
|
|
5
|
+
* built-in + custom composition end to end:
|
|
6
|
+
*
|
|
7
|
+
* MOVE: holding 'move' walks the player (CharacterController3D `view: 'free'`).
|
|
8
|
+
* WIN : WaveSpawner drips enemies that Chase the player; an autopilot aims the
|
|
9
|
+
* third-person camera at the nearest enemy and fires the hitscan `Shoot`
|
|
10
|
+
* rifle — each kill frees the enemy (Health.freeOnDeath) and scores (+1).
|
|
11
|
+
* Clearing all three waves emits Spawner.allCleared → HudUpdater
|
|
12
|
+
* .onAllCleared → AREA SECURED.
|
|
13
|
+
* LOSE: an enemy hitbox (DamageOnContact targetGroup 'player') drains the
|
|
14
|
+
* player's Health to 0 → `died` → ScoreKeeper.loseLife → `lost`.
|
|
15
|
+
*
|
|
16
|
+
* Run with `bun verify.ts` (or `bun run verify`). Audio + rendering are no-ops
|
|
17
|
+
* headlessly, so this isolates the gameplay logic.
|
|
18
|
+
*/
|
|
19
|
+
import type { Node } from 'incanto';
|
|
20
|
+
import { runScript } from 'incanto/test';
|
|
21
|
+
import { HudUpdater, Shoot } from './src/behaviors';
|
|
22
|
+
import sceneJson from './src/game.scene.json';
|
|
23
|
+
|
|
24
|
+
const behaviors = { Shoot, HudUpdater };
|
|
25
|
+
|
|
26
|
+
interface Vec {
|
|
27
|
+
position: number[];
|
|
28
|
+
}
|
|
29
|
+
interface Ctl {
|
|
30
|
+
yaw: number;
|
|
31
|
+
pitch: number;
|
|
32
|
+
eyeHeight: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function liveEnemies(ctx: { scene: { tree: { getNodesInGroup(g: string): Node[] } } }): Node[] {
|
|
36
|
+
return ctx.scene.tree.getNodesInGroup('enemy');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ok = (label: string, cond: boolean): void => {
|
|
40
|
+
if (!cond) {
|
|
41
|
+
console.error(`FAIL: ${label}`);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`pass: ${label}`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ---- MOVE + WIN path ----------------------------------------------------------
|
|
49
|
+
{
|
|
50
|
+
let won = 0;
|
|
51
|
+
let moved = false;
|
|
52
|
+
let sawEnemy = false;
|
|
53
|
+
let chaseClosedIn = false;
|
|
54
|
+
let maxScore = 0;
|
|
55
|
+
|
|
56
|
+
const result = await runScript(sceneJson, {
|
|
57
|
+
durationMs: 60000,
|
|
58
|
+
seed: 1,
|
|
59
|
+
behaviors,
|
|
60
|
+
steps: [
|
|
61
|
+
{
|
|
62
|
+
atMs: 100,
|
|
63
|
+
label: 'subscribe to win + prove movement',
|
|
64
|
+
do: (ctx) => {
|
|
65
|
+
ctx.getNode('/root/Spawner').on('allCleared', () => won++);
|
|
66
|
+
const player = ctx.getNode('/root/Player') as unknown as Vec;
|
|
67
|
+
const x0 = player.position[0] ?? 0;
|
|
68
|
+
ctx.engine.input.setActionVector('move', 1, 0);
|
|
69
|
+
for (let i = 0; i < 30; i++) ctx.engine.step();
|
|
70
|
+
moved = Math.abs((player.position[0] ?? 0) - x0) > 0.2;
|
|
71
|
+
ctx.engine.input.setActionVector('move', 0, 0);
|
|
72
|
+
player.position = [0, 1.4, 16];
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
atMs: 2500,
|
|
77
|
+
label: 'enemies spawn, chase, and the hitscan rifle clears every wave → WIN',
|
|
78
|
+
do: (ctx) => {
|
|
79
|
+
const player = ctx.getNode('/root/Player') as unknown as Vec;
|
|
80
|
+
const ctl = ctx.getNode('/root/Player/Controller') as unknown as Ctl;
|
|
81
|
+
|
|
82
|
+
const first = liveEnemies(ctx)[0] as unknown as Vec | undefined;
|
|
83
|
+
if (first) {
|
|
84
|
+
sawEnemy = true;
|
|
85
|
+
const d0 = Math.hypot((first.position[0] ?? 0) - 0, (first.position[2] ?? 0) - 16);
|
|
86
|
+
for (let i = 0; i < 20; i++) ctx.engine.step();
|
|
87
|
+
const d1 = Math.hypot((first.position[0] ?? 0) - 0, (first.position[2] ?? 0) - 16);
|
|
88
|
+
chaseClosedIn = d1 < d0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const shoot = ctx.getNode('/root/Player/Controller').behavior as unknown as {
|
|
92
|
+
ammo: number;
|
|
93
|
+
reloading: boolean;
|
|
94
|
+
};
|
|
95
|
+
for (let i = 0; i < 5000 && won === 0; i++) {
|
|
96
|
+
player.position = [0, 1.4, 16];
|
|
97
|
+
const enemies = liveEnemies(ctx);
|
|
98
|
+
if (enemies.length) {
|
|
99
|
+
const ex = (enemies[0] as unknown as Vec).position;
|
|
100
|
+
const dx = (ex[0] ?? 0) - 0;
|
|
101
|
+
const dy = (ex[1] ?? 0) - (1.4 + ctl.eyeHeight);
|
|
102
|
+
const dz = (ex[2] ?? 0) - 16;
|
|
103
|
+
// Invert the forward() mapping to point the camera at the target.
|
|
104
|
+
ctl.yaw = Math.atan2(-dx, -dz);
|
|
105
|
+
ctl.pitch = Math.atan2(-dy, Math.hypot(dx, dz));
|
|
106
|
+
}
|
|
107
|
+
if (shoot.ammo <= 0 && !shoot.reloading) {
|
|
108
|
+
ctx.engine.input.pressAction('reload');
|
|
109
|
+
ctx.engine.step();
|
|
110
|
+
ctx.engine.input.releaseAction('reload');
|
|
111
|
+
} else {
|
|
112
|
+
ctx.engine.input.pressAction('fire');
|
|
113
|
+
ctx.engine.step();
|
|
114
|
+
ctx.engine.input.releaseAction('fire');
|
|
115
|
+
}
|
|
116
|
+
ctx.engine.step();
|
|
117
|
+
const score = (ctx.getNode('/root').behavior as unknown as { score: number }).score;
|
|
118
|
+
if (score > maxScore) maxScore = score;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
ok('holding move walked the player', moved);
|
|
126
|
+
ok('a wave produced enemies', sawEnemy);
|
|
127
|
+
ok('an enemy closed in on the player (Chase.moveParent)', chaseClosedIn);
|
|
128
|
+
ok('hitscan kills scored points', maxScore >= 5);
|
|
129
|
+
ok('clearing all waves emitted allCleared (WIN)', won === 1);
|
|
130
|
+
ok('runScript reported no failures (WIN path)', result.ok);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- LOSE path ----------------------------------------------------------------
|
|
134
|
+
{
|
|
135
|
+
let lost = 0;
|
|
136
|
+
let damaged = 0;
|
|
137
|
+
|
|
138
|
+
const result = await runScript(sceneJson, {
|
|
139
|
+
durationMs: 8000,
|
|
140
|
+
seed: 2,
|
|
141
|
+
behaviors,
|
|
142
|
+
steps: [
|
|
143
|
+
{
|
|
144
|
+
atMs: 3000,
|
|
145
|
+
label: 'enemy contact drains the player to 0 HP → LOSE',
|
|
146
|
+
do: (ctx) => {
|
|
147
|
+
ctx.getNode('/root').on('lost', () => lost++);
|
|
148
|
+
ctx.getNode('/root/Player').on('damaged', () => damaged++);
|
|
149
|
+
const enemy = liveEnemies(ctx)[0];
|
|
150
|
+
if (!enemy) return;
|
|
151
|
+
const hitbox = enemy.findChild('Hit') as Node;
|
|
152
|
+
const player = ctx.getNode('/root/Player');
|
|
153
|
+
for (let i = 0; i < 60 && lost === 0; i++) {
|
|
154
|
+
hitbox.emit('triggerEnter', player);
|
|
155
|
+
for (let f = 0; f < 40; f++) ctx.engine.step();
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
ok('player took repeated damage', damaged >= 3);
|
|
163
|
+
ok('player death emitted `lost` once (ScoreKeeper.loseLife)', lost === 1);
|
|
164
|
+
ok('runScript reported no failures (LOSE path)', result.ok);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(process.exitCode ? '\nVERIFY FAILED' : '\nVERIFY OK — Vanguard plays end to end');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Emberwood — Context
|
|
2
|
+
|
|
3
|
+
A 3D quest vignette and the **0.8 feature showcase**: every headline capability
|
|
4
|
+
that shipped in incanto 0.8 is load-bearing here, in one small, readable game.
|
|
5
|
+
|
|
6
|
+
## What it plays like
|
|
7
|
+
|
|
8
|
+
Third-person hero in a sunset village. The Elder (walk up, press **E**) runs a
|
|
9
|
+
typewriter dialogue with a real choice; accepting opens the north gate. Through
|
|
10
|
+
the gate (fade transition) lies a hedge-maze grove at dusk where three
|
|
11
|
+
red-eyed wolves walk patrol routes. Sword-strike them (**click/F** — a glowing
|
|
12
|
+
arc), watch them poof, take the portal home, report back: QUEST COMPLETE, play
|
|
13
|
+
again button.
|
|
14
|
+
|
|
15
|
+
## Why it exists (dogfooding map)
|
|
16
|
+
|
|
17
|
+
| 0.8 feature | Where it carries weight |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `UiDialogue` + choices | the Elder conversation IS the quest state machine |
|
|
20
|
+
| `UiButton` | the play-again button |
|
|
21
|
+
| `findPath` + `PathFollow` | wolf patrol rings through the hedge maze |
|
|
22
|
+
| `Trail3D` | sword arc + sprint dash trail |
|
|
23
|
+
| `engine.sfx.startVoice('wind')` | live ambience that rises as you run |
|
|
24
|
+
| `goToScene` | village ⇄ grove, with the black fade |
|
|
25
|
+
| controller `animations` map | idle/walk/run/sprint/jump — zero animation code |
|
|
26
|
+
| `startRecording`/`replay` | verify.ts proves bit-identical determinism |
|
|
27
|
+
| `auditScene` | both scenes assert zero semantic warnings |
|
|
28
|
+
| `parseCells`+`mergeSolidRects` | ONE grid string builds hedge colliders AND routes the wolves |
|
|
29
|
+
|
|
30
|
+
## Design notes
|
|
31
|
+
|
|
32
|
+
- The grove maze exists exactly once, as `GROVE_GRID` in behaviors.ts. The
|
|
33
|
+
hedge bodies, the wolves' A* pathfinding and the spawn cells all read that
|
|
34
|
+
one string — change the maze by editing text.
|
|
35
|
+
- Cross-scene quest state lives in `src/quest.ts` (a module singleton survives
|
|
36
|
+
`goToScene`; the node tree does not).
|
|
37
|
+
- Custom code is three behaviors: VillageDirector, GroveDirector, SwordStrike.
|
|
38
|
+
Movement, camera, animation, NPC proximity, patrol walking, dialogue, HUD —
|
|
39
|
+
all engine built-ins wired in scene JSON.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Emberwood — Requirements
|
|
2
|
+
|
|
3
|
+
## Functional
|
|
4
|
+
|
|
5
|
+
1. **Quest arc**: talk → accept (choice) → gate opens → clear 3 wolves →
|
|
6
|
+
return → report → QUEST COMPLETE + play-again.
|
|
7
|
+
2. **Dialogue**: typewriter lines, E or click advances, choices render as
|
|
8
|
+
buttons, choosing "Not yet" leaves the quest un-accepted.
|
|
9
|
+
3. **Gate rules**: entering the gate before accepting shows a hint line and
|
|
10
|
+
does NOT change scenes.
|
|
11
|
+
4. **Wolves**: walk continuous patrol loops through the maze (never through
|
|
12
|
+
hedges), face their travel direction, die to one sword strike in reach
|
|
13
|
+
(2.6 m), each death poofs + counts down the HUD.
|
|
14
|
+
5. **Sword**: click/F sweeps a visible arc (Trail3D); misses are allowed.
|
|
15
|
+
6. **Return portal**: active any time; quest completes only when all wolves
|
|
16
|
+
are down (quest.cleared).
|
|
17
|
+
7. **Restart**: play-again resets the quest and returns to a fresh village.
|
|
18
|
+
8. **Wolves fight back**: within 6 m they abandon patrol and re-path to the
|
|
19
|
+
player THROUGH the maze (never through hedges); their fangs bite for 25
|
|
20
|
+
every ~1.1 s of contact; the HP bar tracks it.
|
|
21
|
+
9. **Losing**: 0 HP → "YOU FELL" banner → automatic fresh-grove respawn
|
|
22
|
+
(full HP, 3 wolves, quest progress kept).
|
|
23
|
+
10. **Sword**: the blade rides the right-hand bone; a strike plays the melee
|
|
24
|
+
clip and the trail follows the real swing.
|
|
25
|
+
|
|
26
|
+
## Feel
|
|
27
|
+
|
|
28
|
+
- Wind ambience always present, louder + higher while sprinting.
|
|
29
|
+
- Dash trail only while sprinting.
|
|
30
|
+
- Village = warm sunset; grove = cold dusk, denser fog, moonlight.
|
|
31
|
+
|
|
32
|
+
## Verification (must stay green)
|
|
33
|
+
|
|
34
|
+
- `bun run verify` — auditScene(×2)=0 warnings, full quest E2E headless,
|
|
35
|
+
record/replay bit-identical.
|
|
36
|
+
- `bun run typecheck`, `bun run check` (incanto-check on src/).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Emberwood — Status
|
|
2
|
+
|
|
3
|
+
## Done (2026-07-10, round 2)
|
|
4
|
+
|
|
5
|
+
- Full quest arc playable + LOSABLE: wolves hunt the player through the maze
|
|
6
|
+
(findPath re-pathing), bites drain HP (repeatEvery fangs), 0 HP → YOU FELL →
|
|
7
|
+
fresh grove respawn.
|
|
8
|
+
- Sword rides the hand bone (BoneAttachment3D) and strikes play the real
|
|
9
|
+
melee-attack clip — the Trail3D arc follows the actual arm swing.
|
|
10
|
+
- Elder is a GLB villager (base-model, purple tint, idle clip).
|
|
11
|
+
- Headless verify green: audits, quest E2E, hunt/bite/respawn, replay
|
|
12
|
+
bit-identical. Visual pass in a real browser (screenshots reviewed).
|
|
13
|
+
|
|
14
|
+
## Round 3 (2026-07-11)
|
|
15
|
+
|
|
16
|
+
- Grove is a real DUSK now: low warm sun, cool ambient, `environment.post`
|
|
17
|
+
vignette + desaturation grade, moonlight fill, gentle bloom. Village gets a
|
|
18
|
+
subtle warm grade.
|
|
19
|
+
- Wolves take TWO strikes: the first flinches them (knockback along the
|
|
20
|
+
strike + red hurt-flash), the second fells them.
|
|
21
|
+
- (previous round) camera sphere-probe landed engine-side; hedges raised
|
|
22
|
+
above the eye line.
|
|
23
|
+
|
|
24
|
+
## Known limitations / ideas
|
|
25
|
+
|
|
26
|
+
- A hurt/attack clip for the wolves once one lands in the animation catalog.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Emberwood — Structure
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
index.html canvas + loading bar
|
|
5
|
+
src/main.ts preload GLB/anim assets → createGame3D (village)
|
|
6
|
+
src/village.scene.json sunset village: Elder (Interactable), gate Area3D,
|
|
7
|
+
player rig (controller `animations` map, Trail3D ×2),
|
|
8
|
+
HUD (UiText/UiBanner/UiDialogue/UiButton)
|
|
9
|
+
src/grove.scene.json dusk grove: 3 wolves (PathFollow), return portal,
|
|
10
|
+
empty `Maze` holder (hedges are built at runtime)
|
|
11
|
+
src/behaviors.ts GROVE_GRID + VillageDirector / GroveDirector / SwordStrike
|
|
12
|
+
src/quest.ts cross-scene quest flags (module singleton)
|
|
13
|
+
verify.ts audit + headless quest E2E + replay determinism
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Scene relationship
|
|
17
|
+
|
|
18
|
+
`main.ts` boots the **village**. `goToScene(grove)` happens in
|
|
19
|
+
VillageDirector.onGateEnter; `goToScene(village)` in GroveDirector.onPortalEnter
|
|
20
|
+
and the play-again button. Both scenes redeclare the same input map and player
|
|
21
|
+
rig — scenes are self-contained by design (a scene JSON is the whole truth).
|
|
22
|
+
|
|
23
|
+
## The one-grid trick
|
|
24
|
+
|
|
25
|
+
`GROVE_GRID` (rows of `#`/`.`) is the single source for:
|
|
26
|
+
1. hedge collider boxes — `parseCells` + `mergeSolidRects` (from `incanto/2d`,
|
|
27
|
+
the pure helpers are dimension-agnostic) → few merged StaticBody3D boxes;
|
|
28
|
+
2. wolf routes — `gridFromRows` + `findPath` (A*, no diagonals) → PathFollow
|
|
29
|
+
waypoint rings;
|
|
30
|
+
3. spawn/patrol cells in `PATROLS`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<userRequest>
|
|
2
|
+
YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template:
|
|
3
|
+
|
|
4
|
+
1. 📖 READ THE SKILLS FIRST: `node_modules/incanto/skills/` is the engine
|
|
5
|
+
manual, version-pinned to what is installed. Start with
|
|
6
|
+
`incanto-building-3d-games.md`, `incanto-3d-character.md`,
|
|
7
|
+
`incanto-gameplay-behaviors.md`, `incanto-environment.md` and
|
|
8
|
+
`incanto-audio.md` — this template composes all five.
|
|
9
|
+
2. 🧱 STRUCTURE IS JSON: scenes, nodes, props, assets, input maps and
|
|
10
|
+
replication all live in `src/game.scene.json`. Add nodes there (or with
|
|
11
|
+
`npx incanto-editor`), NOT by constructing them ad-hoc in code.
|
|
12
|
+
3. 🧠 LOGIC IS BEHAVIORS: gameplay code = small TypeScript Behavior classes,
|
|
13
|
+
registered with `registerBehavior('Name', Class)` and attached in JSON via
|
|
14
|
+
`"script": { "name": "Name" }`. Reach for a BUILT-IN gameplay behavior
|
|
15
|
+
(Health, Chase, WaveSpawner, DamageOnContact, ScoreKeeper…) before writing
|
|
16
|
+
your own — this game's only custom behaviors are `Shoot` and `HudUpdater`.
|
|
17
|
+
4. 🔑 UIDS ARE GENERATED: every node uid comes from
|
|
18
|
+
`import { newUid } from 'incanto'`. NEVER invent readable uid strings.
|
|
19
|
+
5. 📦 ASSETS ARE DECLARED: `assets` entries need `type` + `url`; reference
|
|
20
|
+
them as `"$key"`. Free extra key-values are preserved — use them to leave
|
|
21
|
+
notes for yourself (license, palette, source prompt).
|
|
22
|
+
6. ⬆️ Y IS UP: 3D space is y-UP meters, +Z is "south", rotations are degrees
|
|
23
|
+
(Euler XYZ). Negative gravity falls.
|
|
24
|
+
7. 🧲 COLLIDERS ARE PROPS: `"collider": { "shape": "box"|"sphere"|"capsule", … }`
|
|
25
|
+
on a body node — never child shape nodes. Wrong shapes hard-fail at load.
|
|
26
|
+
8. 🖥️ THE HUD IS DOM, NOT NODES: 3D scenes have no 2D `UILayer`/`Label` nodes
|
|
27
|
+
(those are `incanto/2d`). The HUD + crosshair + banner are plain HTML in
|
|
28
|
+
`index.html`; `HudUpdater` writes the live numbers into them (guarded so
|
|
29
|
+
headless runs are a no-op).
|
|
30
|
+
9. 🐛 DEBUG WHEN UNSURE: `physics.debugDraw = true` (the object
|
|
31
|
+
`enablePhysics3D` returns) draws every collider in the running game; or boot
|
|
32
|
+
with `?incanto_debug=1` for the live Explorer/Inspector/Stats overlay.
|
|
33
|
+
10. 🎬 DELTA DISCIPLINE: only write props that differ from defaults — the
|
|
34
|
+
loader treats unknown/garbage props as hard errors, which is your friend.
|
|
35
|
+
11. ✅ VERIFY LIKE A USER: `bun run check` validates the scene, `bun run verify`
|
|
36
|
+
proves the loop headlessly, then `bun run dev` and actually PLAY what you
|
|
37
|
+
changed (move, shoot, clear a wave) before declaring it done.
|
|
38
|
+
</userRequest>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2220%22 fill=%22%234a6b3a%22/><text x=%2250%22 y=%2268%22 font-size=%2255%22 text-anchor=%22middle%22 font-family=%22Arial%22 font-weight=%22bold%22 fill=%22%23ffd166%22>eb</text></svg>" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Emberwood — Incanto quest</title>
|
|
8
|
+
<style>
|
|
9
|
+
html,
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
height: 100%;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
font-family: Arial, sans-serif;
|
|
16
|
+
background: #10130d;
|
|
17
|
+
}
|
|
18
|
+
canvas {
|
|
19
|
+
display: block;
|
|
20
|
+
width: 100vw;
|
|
21
|
+
height: 100vh;
|
|
22
|
+
}
|
|
23
|
+
.loading-container {
|
|
24
|
+
position: fixed;
|
|
25
|
+
inset: 0;
|
|
26
|
+
z-index: 50;
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
background: black;
|
|
32
|
+
color: white;
|
|
33
|
+
}
|
|
34
|
+
.loading-title {
|
|
35
|
+
font-size: 1.5rem;
|
|
36
|
+
font-weight: bold;
|
|
37
|
+
margin-bottom: 1rem;
|
|
38
|
+
}
|
|
39
|
+
.progress-track {
|
|
40
|
+
width: 240px;
|
|
41
|
+
height: 8px;
|
|
42
|
+
border-radius: 4px;
|
|
43
|
+
background: #2a2f26;
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
}
|
|
46
|
+
#progress {
|
|
47
|
+
width: 0%;
|
|
48
|
+
height: 100%;
|
|
49
|
+
background: #ffd166;
|
|
50
|
+
transition: width 0.15s;
|
|
51
|
+
}
|
|
52
|
+
#progress-text {
|
|
53
|
+
margin-top: 8px;
|
|
54
|
+
font-size: 0.85rem;
|
|
55
|
+
color: #aaa;
|
|
56
|
+
}
|
|
57
|
+
</style>
|
|
58
|
+
</head>
|
|
59
|
+
<body>
|
|
60
|
+
<canvas></canvas>
|
|
61
|
+
<div id="loading" class="loading-container">
|
|
62
|
+
<div class="loading-title">Emberwood</div>
|
|
63
|
+
<div class="progress-track"><div id="progress"></div></div>
|
|
64
|
+
<div id="progress-text">0%</div>
|
|
65
|
+
</div>
|
|
66
|
+
<script type="module" src="/src/main.ts"></script>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "village-quest-3d",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"check": "incanto-check src",
|
|
10
|
+
"verify": "bun verify.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@dimforge/rapier2d-compat": "0.19.3",
|
|
14
|
+
"@dimforge/rapier3d-compat": "0.19.3",
|
|
15
|
+
"@pixiv/three-vrm": "^3.5.3",
|
|
16
|
+
"incanto": "^0.13.0",
|
|
17
|
+
"three": "^0.184.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^24.0.0",
|
|
21
|
+
"@types/three": "^0.184.1",
|
|
22
|
+
"typescript": "^6.0.3",
|
|
23
|
+
"vite": "^8.0.16"
|
|
24
|
+
}
|
|
25
|
+
}
|