Skip to content

Commit 98ab001

Browse files
author
DavidQ
committed
Introduce 3D physics base and first sample 1601 Cube Explorer<BUILD_PR_LEVEL_17_5_PHYSICS_BASE_AND_SAMPLE_1601>
1 parent b259854 commit 98ab001

13 files changed

Lines changed: 567 additions & 13 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ MODEL: GPT-5.3-codex
22
REASONING: high
33

44
COMMAND:
5-
Implement basic 3D movement + collision:
6-
- add velocity-based position updates
7-
- implement simple collision bounds (AABB or sphere)
8-
- isolate from 2D systems
9-
- validate no regression across samples
5+
Implement minimal 3D physics + sample:
6+
- add basic physics update loop
7+
- integrate with movement system
8+
- create sample 1601 (3D cube explorer)
9+
- ensure sample loads via samples index
10+
- validate no regressions across all samples

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Add basic 3D movement and collision primitives<BUILD_PR_LEVEL_17_4_MOVEMENT_COLLISION_BASE>
1+
Introduce 3D physics base and first sample 1601 Cube Explorer<BUILD_PR_LEVEL_17_5_PHYSICS_BASE_AND_SAMPLE_1601>

docs/dev/reports/launch_smoke_report.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Launch Smoke Report
22

3-
Generated: 2026-04-15T19:21:48.819Z
3+
Generated: 2026-04-15T19:36:10.578Z
44

55
Filters: games=false, samples=true, tools=false, sampleRange=all
66

@@ -203,4 +203,5 @@ Filters: games=false, samples=true, tools=false, sampleRange=all
203203
| PASS | sample | 1503 | samples\phase-15\1503\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
204204
| PASS | sample | 1504 | samples\phase-15\1504\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
205205
| PASS | sample | 1505 | samples\phase-15\1505\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
206-
| PASS | sample | 1506 | samples\phase-15\1506\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
206+
| PASS | sample | 1506 | samples\phase-15\1506\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
207+
| PASS | sample | 1601 | samples\phase-16\1601\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Post-run: verify movement + collision + no regression
1+
Post-run: verify physics loop + sample 1601 execution + no regression
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# BUILD PR: 17.5 Physics Base + Sample 1601 (3D Cube Explorer)
2+
3+
## Purpose
4+
Introduce minimal 3D physics foundation AND first executable sample (1601).
5+
6+
## Scope
7+
- Basic physics step (gravity optional, integration loop)
8+
- Hook into movement system
9+
- Create sample 1601: Cube Explorer
10+
11+
## Testability
12+
- Sample 1601 loads and runs
13+
- Cube can move in 3D space
14+
- No regression in 2D, networking, or prior systems
15+
16+
## Acceptance
17+
- [ ] Physics step runs
18+
- [ ] Cube moves in 3D
19+
- [ ] Sample 1601 loads cleanly
20+
- [ ] No regressions across samples

samples/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ <h2>Phase 15 - Advanced Systems</h2>
473473
<h2>Phase 16 - 3D Capability Track</h2>
474474
<p>Playable 3D sample games showcasing camera systems, 3D movement, rendering, physics, and world interaction built on the engine.</p>
475475
<div class="grid">
476-
<a class="planned" href="./index.html#phase-16-sample-1601">Sample 1601 - 3D Cube Explorer</a>
476+
<a class="live" href="./phase-16/1601/index.html" title="Minimal 3D cube movement, camera orbit, and AABB collision on top of the engine loop." data-tags="camera3d, movement3d, physics3d, scene, themetokens" data-primary="cube-explorer">Sample 1601 - 3D Cube Explorer</a>
477477
<a class="planned" href="./index.html#phase-16-sample-1602">Sample 1602 - 3D Maze Runner</a>
478478
<a class="planned" href="./index.html#phase-16-sample-1603">Sample 1603 - First Person Walkthrough</a>
479479
<a class="planned" href="./index.html#phase-16-sample-1604">Sample 1604 - 3D Platformer</a>
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/15/2026
5+
CubeExplorer3DScene.js
6+
*/
7+
import { Scene } from '/src/engine/scene/index.js';
8+
import { Theme, ThemeTokens } from '/src/engine/theme/index.js';
9+
import { drawFrame, drawPanel } from '/src/engine/debug/index.js';
10+
import { World } from '/src/engine/ecs/index.js';
11+
import { stepWorldPhysics3D } from '/src/engine/systems/index.js';
12+
13+
const theme = new Theme(ThemeTokens);
14+
const BOX_EDGES = [
15+
[0, 1], [1, 2], [2, 3], [3, 0],
16+
[4, 5], [5, 6], [6, 7], [7, 4],
17+
[0, 4], [1, 5], [2, 6], [3, 7],
18+
];
19+
20+
function clamp(value, min, max) {
21+
return Math.max(min, Math.min(max, value));
22+
}
23+
24+
function createBoxVertices(transform3D, size3D) {
25+
const x = transform3D.x;
26+
const y = transform3D.y;
27+
const z = transform3D.z;
28+
const w = size3D.width;
29+
const h = size3D.height;
30+
const d = size3D.depth;
31+
return [
32+
{ x, y, z },
33+
{ x: x + w, y, z },
34+
{ x: x + w, y: y + h, z },
35+
{ x, y: y + h, z },
36+
{ x, y, z: z + d },
37+
{ x: x + w, y, z: z + d },
38+
{ x: x + w, y: y + h, z: z + d },
39+
{ x, y: y + h, z: z + d },
40+
];
41+
}
42+
43+
function rotateToCameraSpace(point, cameraState) {
44+
const yaw = -(cameraState.rotation.y ?? 0);
45+
const pitch = -(cameraState.rotation.x ?? 0);
46+
47+
let x = point.x - cameraState.position.x;
48+
let y = point.y - cameraState.position.y;
49+
let z = point.z - cameraState.position.z;
50+
51+
const cosYaw = Math.cos(yaw);
52+
const sinYaw = Math.sin(yaw);
53+
const yawX = x * cosYaw - z * sinYaw;
54+
const yawZ = x * sinYaw + z * cosYaw;
55+
x = yawX;
56+
z = yawZ;
57+
58+
const cosPitch = Math.cos(pitch);
59+
const sinPitch = Math.sin(pitch);
60+
const pitchY = y * cosPitch - z * sinPitch;
61+
const pitchZ = y * sinPitch + z * cosPitch;
62+
63+
return {
64+
x,
65+
y: pitchY,
66+
z: pitchZ,
67+
};
68+
}
69+
70+
function projectPoint(point, cameraState, viewport) {
71+
const localPoint = rotateToCameraSpace(point, cameraState);
72+
if (localPoint.z <= 0.2) {
73+
return null;
74+
}
75+
76+
const scale = viewport.focalLength / localPoint.z;
77+
return {
78+
x: viewport.centerX + localPoint.x * scale,
79+
y: viewport.centerY - localPoint.y * scale,
80+
depth: localPoint.z,
81+
};
82+
}
83+
84+
function drawWireBox(renderer, transform3D, size3D, cameraState, viewport, color) {
85+
const vertices = createBoxVertices(transform3D, size3D);
86+
const projected = vertices.map((vertex) => projectPoint(vertex, cameraState, viewport));
87+
88+
for (const [startIndex, endIndex] of BOX_EDGES) {
89+
const start = projected[startIndex];
90+
const end = projected[endIndex];
91+
if (!start || !end) {
92+
continue;
93+
}
94+
renderer.drawLine(start.x, start.y, end.x, end.y, color, 2);
95+
}
96+
}
97+
98+
export default class CubeExplorer3DScene extends Scene {
99+
constructor() {
100+
super();
101+
this.world = new World();
102+
this.moveSpeed = 7.5;
103+
this.cameraYaw = 0;
104+
this.cameraPitch = -0.25;
105+
this.turnSpeed = 1.8;
106+
this.pitchSpeed = 1.1;
107+
this.viewport = {
108+
x: 40,
109+
y: 170,
110+
width: 860,
111+
height: 320,
112+
focalLength: 460,
113+
};
114+
this.worldBounds = {
115+
x: -8,
116+
y: -2,
117+
z: 2,
118+
width: 16,
119+
height: 8,
120+
depth: 22,
121+
};
122+
this.lastPhysicsSummary = { movedEntities: 0, collisionCount: 0 };
123+
124+
this.playerId = this.world.createEntity();
125+
this.world.addComponent(this.playerId, 'transform3D', {
126+
x: 0,
127+
y: 0,
128+
z: 8,
129+
previousX: 0,
130+
previousY: 0,
131+
previousZ: 8,
132+
});
133+
this.world.addComponent(this.playerId, 'size3D', { width: 1.6, height: 1.6, depth: 1.6 });
134+
this.world.addComponent(this.playerId, 'velocity3D', { x: 0, y: 0, z: 0 });
135+
this.world.addComponent(this.playerId, 'collider3D', { enabled: true, solid: false });
136+
this.world.addComponent(this.playerId, 'renderable3D', { color: '#7dd3fc' });
137+
138+
this.addSolidBox({ x: -3.5, y: -0.2, z: 10.5 }, { width: 2.2, height: 2.2, depth: 2.2 }, '#f87171');
139+
this.addSolidBox({ x: 2.3, y: -0.2, z: 14.0 }, { width: 2.0, height: 3.0, depth: 2.0 }, '#fbbf24');
140+
this.addSolidBox({ x: -1.0, y: -0.2, z: 18.0 }, { width: 3.0, height: 1.8, depth: 2.2 }, '#86efac');
141+
}
142+
143+
setCamera3D(camera3D) {
144+
this.camera3D = camera3D;
145+
this.syncCameraToPlayer();
146+
}
147+
148+
addSolidBox(transform3D, size3D, color = '#fca5a5') {
149+
const id = this.world.createEntity();
150+
this.world.addComponent(id, 'transform3D', {
151+
x: transform3D.x,
152+
y: transform3D.y,
153+
z: transform3D.z,
154+
previousX: transform3D.x,
155+
previousY: transform3D.y,
156+
previousZ: transform3D.z,
157+
});
158+
this.world.addComponent(id, 'size3D', {
159+
width: size3D.width,
160+
height: size3D.height,
161+
depth: size3D.depth,
162+
});
163+
this.world.addComponent(id, 'solid3D', { enabled: true });
164+
this.world.addComponent(id, 'renderable3D', { color });
165+
}
166+
167+
syncCameraToPlayer() {
168+
if (!this.camera3D) {
169+
return;
170+
}
171+
172+
const playerTransform = this.world.requireComponent(this.playerId, 'transform3D');
173+
const orbitDistance = 10;
174+
const cameraX = playerTransform.x + Math.sin(this.cameraYaw) * orbitDistance;
175+
const cameraZ = playerTransform.z - Math.cos(this.cameraYaw) * orbitDistance;
176+
const cameraY = playerTransform.y + 4.8;
177+
178+
this.camera3D.setPosition({
179+
x: cameraX,
180+
y: cameraY,
181+
z: cameraZ,
182+
});
183+
this.camera3D.setRotation({
184+
x: this.cameraPitch,
185+
y: this.cameraYaw,
186+
z: 0,
187+
});
188+
}
189+
190+
step3DPhysics(dt, engine) {
191+
const velocity = this.world.requireComponent(this.playerId, 'velocity3D');
192+
const input = engine.input;
193+
194+
velocity.x = 0;
195+
velocity.y = 0;
196+
velocity.z = 0;
197+
198+
if (input?.isDown('KeyA')) velocity.x -= this.moveSpeed;
199+
if (input?.isDown('KeyD')) velocity.x += this.moveSpeed;
200+
if (input?.isDown('KeyR')) velocity.y += this.moveSpeed;
201+
if (input?.isDown('KeyF')) velocity.y -= this.moveSpeed;
202+
if (input?.isDown('KeyW')) velocity.z += this.moveSpeed;
203+
if (input?.isDown('KeyS')) velocity.z -= this.moveSpeed;
204+
205+
const yawInput = (input?.isDown('ArrowRight') ? 1 : 0) - (input?.isDown('ArrowLeft') ? 1 : 0);
206+
const pitchInput = (input?.isDown('ArrowUp') ? 1 : 0) - (input?.isDown('ArrowDown') ? 1 : 0);
207+
this.cameraYaw += yawInput * this.turnSpeed * dt;
208+
this.cameraPitch = clamp(this.cameraPitch + pitchInput * this.pitchSpeed * dt, -0.85, 0.6);
209+
210+
this.lastPhysicsSummary = stepWorldPhysics3D(this.world, dt, {
211+
worldBounds: this.worldBounds,
212+
});
213+
this.syncCameraToPlayer();
214+
}
215+
216+
render(renderer) {
217+
drawFrame(renderer, theme, [
218+
'Sample 1601 - 3D Cube Explorer',
219+
'Minimal 3D movement + AABB collision using an isolated physics loop',
220+
'Move: W A S D | Vertical: R/F | Camera orbit: Arrow keys',
221+
'Goal: navigate around blocking boxes while remaining inside world bounds',
222+
]);
223+
224+
renderer.strokeRect(
225+
this.viewport.x,
226+
this.viewport.y,
227+
this.viewport.width,
228+
this.viewport.height,
229+
'#d8d5ff',
230+
2,
231+
);
232+
233+
const cameraState = this.camera3D?.getState?.() ?? {
234+
position: { x: 0, y: 3, z: -8 },
235+
rotation: { x: -0.25, y: 0, z: 0 },
236+
};
237+
238+
const projectionViewport = {
239+
centerX: this.viewport.x + this.viewport.width * 0.5,
240+
centerY: this.viewport.y + this.viewport.height * 0.5,
241+
focalLength: this.viewport.focalLength,
242+
};
243+
244+
for (let lineZ = 4; lineZ <= 24; lineZ += 2) {
245+
const start = projectPoint({ x: -8, y: -1, z: lineZ }, cameraState, projectionViewport);
246+
const end = projectPoint({ x: 8, y: -1, z: lineZ }, cameraState, projectionViewport);
247+
if (start && end) {
248+
renderer.drawLine(start.x, start.y, end.x, end.y, '#334155', 1);
249+
}
250+
}
251+
252+
for (let lineX = -8; lineX <= 8; lineX += 2) {
253+
const start = projectPoint({ x: lineX, y: -1, z: 2 }, cameraState, projectionViewport);
254+
const end = projectPoint({ x: lineX, y: -1, z: 24 }, cameraState, projectionViewport);
255+
if (start && end) {
256+
renderer.drawLine(start.x, start.y, end.x, end.y, '#1f334d', 1);
257+
}
258+
}
259+
260+
const entities = this.world.getEntitiesWith('transform3D', 'size3D', 'renderable3D').map((entityId) => ({
261+
entityId,
262+
transform3D: this.world.requireComponent(entityId, 'transform3D'),
263+
size3D: this.world.requireComponent(entityId, 'size3D'),
264+
renderable3D: this.world.requireComponent(entityId, 'renderable3D'),
265+
}));
266+
267+
entities.sort((left, right) => right.transform3D.z - left.transform3D.z);
268+
entities.forEach(({ transform3D, size3D, renderable3D }) => {
269+
drawWireBox(
270+
renderer,
271+
transform3D,
272+
size3D,
273+
cameraState,
274+
projectionViewport,
275+
renderable3D.color,
276+
);
277+
});
278+
279+
const player = this.world.requireComponent(this.playerId, 'transform3D');
280+
drawPanel(renderer, 620, 34, 300, 126, '3D Runtime', [
281+
`Cube: x=${player.x.toFixed(2)} y=${player.y.toFixed(2)} z=${player.z.toFixed(2)}`,
282+
`Camera yaw: ${this.cameraYaw.toFixed(2)} pitch: ${this.cameraPitch.toFixed(2)}`,
283+
`Moved entities: ${this.lastPhysicsSummary.movedEntities}`,
284+
`Resolved collisions: ${this.lastPhysicsSummary.collisionCount}`,
285+
'Physics loop: stepWorldPhysics3D (MovementSystem + AABB)',
286+
]);
287+
}
288+
}

samples/phase-16/1601/index.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!--
2+
Toolbox Aid
3+
David Quesenberry
4+
04/15/2026
5+
index.html
6+
-->
7+
<!DOCTYPE html>
8+
<html lang="en">
9+
<head>
10+
<meta charset="UTF-8" />
11+
<title>Sample 1601 - 3D Cube Explorer</title>
12+
<link rel="stylesheet" href="../../../src/engine/ui/baseLayout.css" />
13+
</head>
14+
<body>
15+
<main>
16+
<h1>Sample 1601 - 3D Cube Explorer</h1>
17+
<p>Minimal 3D movement, camera orbit, and AABB collision on the engine runtime.</p>
18+
<canvas id="game" width="960" height="540"></canvas>
19+
20+
<section>
21+
<h3>Engine Classes Used</h3>
22+
<ul>
23+
<li>Engine</li>
24+
<li>Scene</li>
25+
<li>Camera3D</li>
26+
<li>World</li>
27+
<li>MovementSystem (3D)</li>
28+
<li>PhysicsSystem (3D)</li>
29+
</ul>
30+
</section>
31+
</main>
32+
33+
<script type="module" src="/samples/shared/sampleDetailPageEnhancement.js"></script>
34+
<script type="module" src="./main.js"></script>
35+
</body>
36+
</html>

0 commit comments

Comments
 (0)