Skip to content

Commit 3a7608f

Browse files
author
DavidQ
committed
Add core dungeon crawler loop to Sample 1608<BUILD_PR_LEVEL_17_14_SAMPLE_1608_DUNGEON_CRAWLER_CORE_LOOP>
1 parent 925a866 commit 3a7608f

7 files changed

Lines changed: 144 additions & 38 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11

22
MODEL: GPT-5.3-codex
33
REASONING: high
4-
Implement 1607 loop.
4+
5+
COMMAND:
6+
Implement Sample 1608 dungeon crawler core loop.
7+
8+
Rules:
9+
- smallest scoped change
10+
- no zip output
11+
- sample only
12+
13+
Validate:
14+
- movement works
15+
- collision blocks
16+
- interaction visible
17+
- sanity + smoke pass

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1607 core loop
1+
Add core dungeon crawler loop to Sample 1608<BUILD_PR_LEVEL_17_14_SAMPLE_1608_DUNGEON_CRAWLER_CORE_LOOP>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1607 adds playable loop
1+
1608 now has a readable dungeon loop with movement, blocking collision, and interaction.
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Launch Smoke Report
22

3-
Generated: 2026-04-15T21:41:25.257Z
3+
Generated: 2026-04-15T21:47:58.859Z
44

5-
Filters: games=false, samples=true, tools=false, sampleRange=1607-1607
5+
Filters: games=false, samples=true, tools=false, sampleRange=1608-1608
66

77
| Status | Type | Label | Path | Notes | Steps |
88
| --- | --- | --- | --- | --- | --- |
9-
| PASS | sample | 1607 | samples\phase-16\1607\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
9+
| PASS | sample | 1608 | samples\phase-16\1608\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11

2-
[ ] movement
3-
[ ] shooting
4-
[ ] hit
2+
[ ] movement works
3+
[ ] walls block
4+
[ ] interaction visible
5+
[ ] loop understandable
6+
[ ] sanity pass
7+
[ ] smoke pass
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
# BUILD PR: 17.14 Sample 1608 Dungeon Crawler Core Loop
3+
4+
## Purpose
5+
Create a clear dungeon crawler loop: move → explore → collide → react.
6+
7+
## Scope
8+
- player movement (grid or free)
9+
- wall collision (blocking)
10+
- simple enemy or obstacle interaction
11+
- readable environment
12+
13+
## Constraints
14+
- sample only
15+
- no engine changes
16+
- no networking / 2D impact
17+
18+
## Acceptance
19+
- movement works
20+
- walls block correctly
21+
- interaction visible
22+
- loop understandable
23+
- sanity + smoke pass

samples/phase-16/1608/DungeonCrawler3DScene.js

Lines changed: 96 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default class DungeonCrawler3DScene extends Scene {
1919
super();
2020
this.world = new World();
2121
this.moveSpeed = 6.2;
22+
this.playerSpawn = { x: -8.2, y: 0, z: 4.2 };
2223
this.viewport = {
2324
x: 40,
2425
y: 170,
@@ -52,16 +53,23 @@ export default class DungeonCrawler3DScene extends Scene {
5253
};
5354
this.relicCollected = false;
5455
this.escaped = false;
56+
this.runState = 'seek-relic';
57+
this.runElapsedSeconds = 0;
58+
this.lastCompletionSeconds = 0;
59+
this.runCount = 1;
60+
this.lastResetReason = 'spawn';
61+
this.interactionFlashSeconds = 0;
62+
this.resetLatch = false;
5563
this.lastPhysicsSummary = { movedEntities: 0, collisionCount: 0 };
5664

5765
this.playerId = this.world.createEntity();
5866
this.world.addComponent(this.playerId, 'transform3D', {
59-
x: -8.2,
60-
y: 0,
61-
z: 4.2,
62-
previousX: -8.2,
63-
previousY: 0,
64-
previousZ: 4.2,
67+
x: this.playerSpawn.x,
68+
y: this.playerSpawn.y,
69+
z: this.playerSpawn.z,
70+
previousX: this.playerSpawn.x,
71+
previousY: this.playerSpawn.y,
72+
previousZ: this.playerSpawn.z,
6573
});
6674
this.world.addComponent(this.playerId, 'size3D', { width: 1.0, height: 1.4, depth: 1.0 });
6775
this.world.addComponent(this.playerId, 'velocity3D', { x: 0, y: 0, z: 0 });
@@ -133,20 +141,63 @@ export default class DungeonCrawler3DScene extends Scene {
133141
});
134142
}
135143

144+
resetRun(reason = 'manual-reset') {
145+
const player = this.world.requireComponent(this.playerId, 'transform3D');
146+
const velocity = this.world.requireComponent(this.playerId, 'velocity3D');
147+
player.x = this.playerSpawn.x;
148+
player.y = this.playerSpawn.y;
149+
player.z = this.playerSpawn.z;
150+
player.previousX = this.playerSpawn.x;
151+
player.previousY = this.playerSpawn.y;
152+
player.previousZ = this.playerSpawn.z;
153+
velocity.x = 0;
154+
velocity.y = 0;
155+
velocity.z = 0;
156+
157+
this.relicCollected = false;
158+
this.escaped = false;
159+
this.runState = 'seek-relic';
160+
this.runElapsedSeconds = 0;
161+
this.interactionFlashSeconds = 0;
162+
this.lastResetReason = reason;
163+
this.runCount += 1;
164+
165+
const gateSolid = this.world.requireComponent(this.gateId, 'solid3D');
166+
gateSolid.enabled = true;
167+
const gateRenderable = this.world.requireComponent(this.gateId, 'renderable3D');
168+
gateRenderable.color = '#f87171';
169+
}
170+
136171
step3DPhysics(dt, engine) {
137172
const input = engine.input;
173+
const resetPressed = input?.isDown('KeyR') === true;
174+
if (resetPressed && !this.resetLatch) {
175+
this.resetRun('manual-reset');
176+
}
177+
this.resetLatch = resetPressed;
178+
179+
this.interactionFlashSeconds = Math.max(0, this.interactionFlashSeconds - dt);
180+
138181
const velocity = this.world.requireComponent(this.playerId, 'velocity3D');
139-
const axisX = (input?.isDown('KeyD') ? 1 : 0) - (input?.isDown('KeyA') ? 1 : 0);
140-
const axisZ = (input?.isDown('KeyW') ? 1 : 0) - (input?.isDown('KeyS') ? 1 : 0);
141-
const length = Math.hypot(axisX, axisZ) || 1;
182+
if (!this.escaped) {
183+
const axisX = (input?.isDown('KeyD') ? 1 : 0) - (input?.isDown('KeyA') ? 1 : 0);
184+
const axisZ = (input?.isDown('KeyW') ? 1 : 0) - (input?.isDown('KeyS') ? 1 : 0);
185+
const length = Math.hypot(axisX, axisZ) || 1;
142186

143-
velocity.x = (axisX / length) * this.moveSpeed;
144-
velocity.z = (axisZ / length) * this.moveSpeed;
145-
velocity.y = 0;
187+
velocity.x = (axisX / length) * this.moveSpeed;
188+
velocity.z = (axisZ / length) * this.moveSpeed;
189+
velocity.y = 0;
146190

147-
this.lastPhysicsSummary = stepWorldPhysics3D(this.world, dt, {
148-
worldBounds: this.worldBounds,
149-
});
191+
this.lastPhysicsSummary = stepWorldPhysics3D(this.world, dt, {
192+
worldBounds: this.worldBounds,
193+
});
194+
this.runElapsedSeconds += dt;
195+
} else {
196+
velocity.x = 0;
197+
velocity.y = 0;
198+
velocity.z = 0;
199+
this.lastPhysicsSummary = { movedEntities: 0, collisionCount: 0 };
200+
}
150201

151202
const player = this.world.requireComponent(this.playerId, 'transform3D');
152203
const playerSize = this.world.requireComponent(this.playerId, 'size3D');
@@ -161,6 +212,8 @@ export default class DungeonCrawler3DScene extends Scene {
161212

162213
if (!this.relicCollected && isAabbColliding3D(playerAabb, this.relic)) {
163214
this.relicCollected = true;
215+
this.runState = 'escape';
216+
this.interactionFlashSeconds = 1.1;
164217
const gateSolid = this.world.requireComponent(this.gateId, 'solid3D');
165218
gateSolid.enabled = false;
166219
const gateRenderable = this.world.requireComponent(this.gateId, 'renderable3D');
@@ -169,17 +222,27 @@ export default class DungeonCrawler3DScene extends Scene {
169222

170223
if (this.relicCollected && isAabbColliding3D(playerAabb, this.exitGoal)) {
171224
this.escaped = true;
225+
this.runState = 'complete';
226+
this.lastCompletionSeconds = this.runElapsedSeconds;
227+
this.interactionFlashSeconds = 1.6;
172228
}
173229

174230
this.syncCamera();
175231
}
176232

177233
render(renderer) {
234+
const objectiveLine =
235+
this.runState === 'seek-relic'
236+
? 'Objective: collect the relic to unlock the gate.'
237+
: this.runState === 'escape'
238+
? 'Objective: pass the unlocked gate and reach the exit.'
239+
: `Run complete in ${this.lastCompletionSeconds.toFixed(1)} s. Press R to restart.`;
240+
178241
drawFrame(renderer, theme, [
179242
'Sample 1608 - 3D Dungeon Crawler',
180-
'Explore rooms, collect the relic, and unlock the gate to escape.',
181-
'Move: W A S D',
182-
this.escaped ? 'Escape complete: dungeon cleared.' : 'Collect relic first, then reach the green exit marker.',
243+
'Explore rooms, collect the relic, then escape through the unlocked route.',
244+
'Move: W A S D | Restart run: R',
245+
objectiveLine,
183246
]);
184247

185248
renderer.strokeRect(this.viewport.x, this.viewport.y, this.viewport.width, this.viewport.height, '#d8d5ff', 2);
@@ -226,24 +289,28 @@ export default class DungeonCrawler3DScene extends Scene {
226289
);
227290
}
228291

229-
drawWireBox(
230-
renderer,
231-
this.exitGoal,
232-
{ width: this.exitGoal.width, height: this.exitGoal.height, depth: this.exitGoal.depth },
233-
cameraState,
234-
projectionViewport,
235-
'#4ade80',
236-
2,
237-
);
292+
const exitColor = this.relicCollected ? '#4ade80' : '#475569';
293+
drawWireBox(renderer, this.exitGoal, { width: this.exitGoal.width, height: this.exitGoal.height, depth: this.exitGoal.depth }, cameraState, projectionViewport, exitColor, 2);
238294

239295
const player = this.world.requireComponent(this.playerId, 'transform3D');
240-
drawPanel(renderer, 620, 34, 300, 126, 'Dungeon Runtime', [
296+
const statusLine =
297+
this.runState === 'seek-relic'
298+
? 'Seek relic'
299+
: this.runState === 'escape'
300+
? 'Exit route open'
301+
: 'Escaped';
302+
const flashLine = this.interactionFlashSeconds > 0 ? 'Interaction: event pulse active' : 'Interaction: idle';
303+
304+
drawPanel(renderer, 620, 34, 300, 236, 'Dungeon Runtime', [
241305
`Explorer: x=${player.x.toFixed(2)} y=${player.y.toFixed(2)} z=${player.z.toFixed(2)}`,
306+
`Run: ${this.runCount} | State: ${statusLine}`,
242307
`Relic: ${this.relicCollected ? 'collected' : 'missing'}`,
243308
`Gate: ${this.relicCollected ? 'unlocked' : 'locked'}`,
244309
`Escaped: ${this.escaped ? 'yes' : 'no'}`,
245310
`Resolved collisions: ${this.lastPhysicsSummary.collisionCount}`,
311+
`Run time: ${this.runElapsedSeconds.toFixed(1)} s | Last clear: ${this.lastCompletionSeconds.toFixed(1)} s`,
312+
flashLine,
313+
`Last reset: ${this.lastResetReason}`,
246314
]);
247315
}
248316
}
249-

0 commit comments

Comments
 (0)