Skip to content

Commit b259854

Browse files
author
DavidQ
committed
Add basic 3D movement and collision primitives<BUILD_PR_LEVEL_17_4_MOVEMENT_COLLISION_BASE>
1 parent 7104065 commit b259854

12 files changed

Lines changed: 438 additions & 9 deletions

docs/dev/CODEX_COMMANDS.md

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

44
COMMAND:
5-
Implement minimal 3D camera:
6-
- create camera state (position, rotation)
7-
- integrate into scene safely
8-
- ensure no impact to 2D camera
9-
- validate all samples still pass
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

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Introduce minimal 3D camera foundation<BUILD_PR_LEVEL_17_3_CAMERA_FOUNDATION>
1+
Add basic 3D movement and collision primitives<BUILD_PR_LEVEL_17_4_MOVEMENT_COLLISION_BASE>

docs/dev/reports/launch_smoke_report.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Launch Smoke Report
22

3-
Generated: 2026-04-15T19:11:35.746Z
3+
Generated: 2026-04-15T19:21:48.819Z
44

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Post-run: verify camera init + no regression
1+
Post-run: verify movement + collision + no regression
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# BUILD PR: 17.4 Movement + Collision Base (3D)
2+
3+
## Purpose
4+
Introduce minimal 3D movement and collision primitives.
5+
6+
## Scope
7+
- Basic position update (velocity integration)
8+
- Simple collision bounds (AABB or sphere)
9+
- No physics engine yet (foundation only)
10+
11+
## Testability
12+
- 2D systems remain unaffected
13+
- 3D entities can move and collide in isolation
14+
15+
## Acceptance
16+
- [ ] Movement updates correctly
17+
- [ ] Collision detection triggers
18+
- [ ] No regression in 2D or networking

src/engine/core/Engine.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,16 @@ export default class Engine {
196196

197197
const updateStart = performance.now();
198198
const tickerResult = this.fixedTicker.advance(deltaMs, (stepSeconds) => {
199+
if (this.scene && typeof this.scene.step3DPhysics === 'function') {
200+
try {
201+
this.scene.step3DPhysics(stepSeconds, this);
202+
} catch (error) {
203+
this.logger?.warn?.('Engine scene step3DPhysics hook failed.', {
204+
error: error?.message || String(error),
205+
});
206+
}
207+
}
208+
199209
if (this.scene && typeof this.scene.update === 'function') {
200210
this.scene.update(stepSeconds, this);
201211
}

src/engine/physics/collision3d.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/15/2026
5+
collision3d.js
6+
*/
7+
function toFinite(value, fallback = 0) {
8+
return Number.isFinite(value) ? value : fallback;
9+
}
10+
11+
function getAabb3D(bounds) {
12+
return {
13+
x: toFinite(bounds?.x, 0),
14+
y: toFinite(bounds?.y, 0),
15+
z: toFinite(bounds?.z, 0),
16+
width: Math.max(0, toFinite(bounds?.width, 0)),
17+
height: Math.max(0, toFinite(bounds?.height, 0)),
18+
depth: Math.max(0, toFinite(bounds?.depth, 0)),
19+
};
20+
}
21+
22+
export function isAabbColliding3D(a, b) {
23+
const left = getAabb3D(a);
24+
const right = getAabb3D(b);
25+
26+
return (
27+
left.x < right.x + right.width &&
28+
left.x + left.width > right.x &&
29+
left.y < right.y + right.height &&
30+
left.y + left.height > right.y &&
31+
left.z < right.z + right.depth &&
32+
left.z + left.depth > right.z
33+
);
34+
}
35+
36+
export function resolveAabbCollision3D(body, obstacle) {
37+
if (!body || !obstacle || body === obstacle) {
38+
return {
39+
collided: false,
40+
axis: null,
41+
overlap: 0,
42+
};
43+
}
44+
45+
const moving = getAabb3D(body);
46+
const fixed = getAabb3D(obstacle);
47+
48+
if (!isAabbColliding3D(moving, fixed)) {
49+
return {
50+
collided: false,
51+
axis: null,
52+
overlap: 0,
53+
};
54+
}
55+
56+
const movingCenterX = moving.x + moving.width / 2;
57+
const movingCenterY = moving.y + moving.height / 2;
58+
const movingCenterZ = moving.z + moving.depth / 2;
59+
const fixedCenterX = fixed.x + fixed.width / 2;
60+
const fixedCenterY = fixed.y + fixed.height / 2;
61+
const fixedCenterZ = fixed.z + fixed.depth / 2;
62+
63+
const deltaX = movingCenterX - fixedCenterX;
64+
const deltaY = movingCenterY - fixedCenterY;
65+
const deltaZ = movingCenterZ - fixedCenterZ;
66+
const overlapX = (moving.width + fixed.width) / 2 - Math.abs(deltaX);
67+
const overlapY = (moving.height + fixed.height) / 2 - Math.abs(deltaY);
68+
const overlapZ = (moving.depth + fixed.depth) / 2 - Math.abs(deltaZ);
69+
70+
let axis = 'x';
71+
let overlap = overlapX;
72+
73+
if (overlapY < overlap) {
74+
axis = 'y';
75+
overlap = overlapY;
76+
}
77+
78+
if (overlapZ < overlap) {
79+
axis = 'z';
80+
overlap = overlapZ;
81+
}
82+
83+
if (axis === 'x') {
84+
body.x += deltaX < 0 ? -overlap : overlap;
85+
body.velocityX = 0;
86+
} else if (axis === 'y') {
87+
body.y += deltaY < 0 ? -overlap : overlap;
88+
body.velocityY = 0;
89+
} else {
90+
body.z += deltaZ < 0 ? -overlap : overlap;
91+
body.velocityZ = 0;
92+
}
93+
94+
return {
95+
collided: true,
96+
axis,
97+
overlap,
98+
};
99+
}
100+
101+
export function resolveAabbCollisions3D(body, obstacles = []) {
102+
if (!Array.isArray(obstacles) || obstacles.length === 0) {
103+
return {
104+
collided: false,
105+
collisionCount: 0,
106+
axes: [],
107+
};
108+
}
109+
110+
const axes = [];
111+
for (const obstacle of obstacles) {
112+
const result = resolveAabbCollision3D(body, obstacle);
113+
if (result.collided) {
114+
axes.push(result.axis);
115+
}
116+
}
117+
118+
return {
119+
collided: axes.length > 0,
120+
collisionCount: axes.length,
121+
axes,
122+
};
123+
}

src/engine/physics/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ index.js
77
export { applyDrag } from './drag.js';
88
export { stepArcadeBody } from './arcadeBody.js';
99
export { integrateVelocity2D } from './integration.js';
10-
10+
export { integrateVelocity3D } from './integration3d.js';
11+
export { isAabbColliding3D, resolveAabbCollision3D, resolveAabbCollisions3D } from './collision3d.js';
12+
export { stepSceneBodies3D } from './scene3d.js';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/15/2026
5+
integration3d.js
6+
*/
7+
function toFinite(value, fallback = 0) {
8+
return Number.isFinite(value) ? value : fallback;
9+
}
10+
11+
export function integrateVelocity3D(body, dtSeconds) {
12+
if (!body || typeof body !== 'object') {
13+
return body;
14+
}
15+
16+
const dt = toFinite(dtSeconds, 0);
17+
body.x = toFinite(body.x, 0) + toFinite(body.velocityX, 0) * dt;
18+
body.y = toFinite(body.y, 0) + toFinite(body.velocityY, 0) * dt;
19+
body.z = toFinite(body.z, 0) + toFinite(body.velocityZ, 0) * dt;
20+
return body;
21+
}

src/engine/physics/scene3d.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/15/2026
5+
scene3d.js
6+
*/
7+
import { integrateVelocity3D } from './integration3d.js';
8+
import { resolveAabbCollisions3D } from './collision3d.js';
9+
10+
export function stepSceneBodies3D(
11+
scene,
12+
dtSeconds,
13+
{
14+
bodiesKey = 'bodies3D',
15+
collidersKey = 'staticColliders3D',
16+
} = {},
17+
) {
18+
if (!scene || typeof scene !== 'object') {
19+
return {
20+
movedBodies: 0,
21+
resolvedCollisions: 0,
22+
};
23+
}
24+
25+
const bodies = Array.isArray(scene[bodiesKey]) ? scene[bodiesKey] : [];
26+
const staticColliders = Array.isArray(scene[collidersKey]) ? scene[collidersKey] : [];
27+
let movedBodies = 0;
28+
let resolvedCollisions = 0;
29+
30+
for (const body of bodies) {
31+
integrateVelocity3D(body, dtSeconds);
32+
movedBodies += 1;
33+
34+
const colliders = Array.isArray(body?.colliders3D) ? body.colliders3D : staticColliders;
35+
const collisionResult = resolveAabbCollisions3D(body, colliders);
36+
resolvedCollisions += collisionResult.collisionCount;
37+
}
38+
39+
return {
40+
movedBodies,
41+
resolvedCollisions,
42+
};
43+
}

0 commit comments

Comments
 (0)