Physics stage 1b: six new nodes act on dynamic bodies in play (Apply force over the frame, independent of the frame rate, Apply impulse, Set velocity, Set angular velocity, Get velocity, Set gravity for the rest of the run), a node on an object without a dynamic body and moving a dynamic body directly each log a hint once per object, and Move with collision now pushes dynamic bodies with a mass-dependent push while the mover stays flush; Move keeps its own move-and-slide instead of Rapier's character controller, which measurably snags on seams between colliders (#147)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
78899b1ace
commit
0ff3c8b742
10 files changed
+783
-43
No files matched your search
@@ -11,6 +11,7 @@ import {
|
||||
type GraphLookup,
|
||||
NO_LOOKUP,
|
||||
type NodeContext,
|
||||
type PhysicsControl,
|
||||
resolveDef,
|
||||
type RunResult,
|
||||
type Value,
|
||||
@@ -86,6 +87,26 @@ const emitTarget = fakeEntity('Sparks');
|
||||
const dancer = fakeEntity('Dancer');
|
||||
const painted = fakeHandle({ width: 100, text: '' });
|
||||
|
||||
type PhysicsDouble = PhysicsControl & { calls: unknown[][] };
|
||||
/** Records what the physics nodes ask for; "crate" is the one object with a dynamic body. */
|
||||
const physicsDouble = (): PhysicsDouble => {
|
||||
const calls: unknown[][] = [];
|
||||
const has = (target: { id: string }) => target.id === 'crate';
|
||||
return {
|
||||
calls,
|
||||
velocityOf: (target) => (has(target) ? { x: 12, y: -3, angular: 45 } : null),
|
||||
setVelocity: (target, x, y) => (calls.push(['setVelocity', target.id, x, y]), has(target)),
|
||||
setAngularVelocity: (target, degrees) => (
|
||||
calls.push(['setAngularVelocity', target.id, degrees]),
|
||||
has(target)
|
||||
),
|
||||
applyImpulse: (target, x, y) => (calls.push(['applyImpulse', target.id, x, y]), has(target)),
|
||||
setGravity: (x, y) => void calls.push(['setGravity', x, y]),
|
||||
};
|
||||
};
|
||||
const physicsCalls = (ctx: FakeContext) => (ctx.physics as PhysicsDouble).calls;
|
||||
const crate = fakeEntity('crate');
|
||||
|
||||
const pointer = (overrides: Partial<NodeContext['pointer']>): Partial<NodeContext> => ({
|
||||
pointer: { ...NO_POINTER, ...overrides },
|
||||
});
|
||||
@@ -832,6 +853,82 @@ const CASES: Record<string, Case[]> = {
|
||||
},
|
||||
{ name: 'no object is a no-op', inputs: { object: null, x: 1, y: 1 }, next: 'exec' },
|
||||
],
|
||||
ApplyForce: [
|
||||
{
|
||||
name: 'pushes for the length of the frame: force × dt, handed on as an impulse',
|
||||
inputs: { object: crate, x: 10, y: -4 },
|
||||
context: { physics: physicsDouble() },
|
||||
next: 'exec',
|
||||
after: (ctx) => expect(physicsCalls(ctx)).toEqual([['applyImpulse', 'crate', 5, -2]]),
|
||||
},
|
||||
{
|
||||
name: 'no object asks nothing and continues',
|
||||
inputs: { object: null, x: 10, y: 0 },
|
||||
context: { physics: physicsDouble() },
|
||||
next: 'exec',
|
||||
after: (ctx) => expect(physicsCalls(ctx)).toEqual([]),
|
||||
},
|
||||
{
|
||||
name: 'without physics (the editor run) it continues',
|
||||
inputs: { object: crate, x: 1, y: 1 },
|
||||
next: 'exec',
|
||||
},
|
||||
],
|
||||
ApplyImpulse: [
|
||||
{
|
||||
name: 'kicks the object with the impulse as given',
|
||||
inputs: { object: crate, x: 3, y: -4 },
|
||||
context: { physics: physicsDouble() },
|
||||
next: 'exec',
|
||||
after: (ctx) => expect(physicsCalls(ctx)).toEqual([['applyImpulse', 'crate', 3, -4]]),
|
||||
},
|
||||
{ name: 'without physics it continues', inputs: { object: crate, x: 1, y: 1 }, next: 'exec' },
|
||||
],
|
||||
SetVelocity: [
|
||||
{
|
||||
name: 'sets the velocity as given',
|
||||
inputs: { object: crate, x: 120, y: -30 },
|
||||
context: { physics: physicsDouble() },
|
||||
next: 'exec',
|
||||
after: (ctx) => expect(physicsCalls(ctx)).toEqual([['setVelocity', 'crate', 120, -30]]),
|
||||
},
|
||||
{ name: 'without physics it continues', inputs: { object: crate, x: 1, y: 1 }, next: 'exec' },
|
||||
],
|
||||
SetAngularVelocity: [
|
||||
{
|
||||
name: 'sets the turning speed in degrees per second',
|
||||
inputs: { object: crate, speed: 90 },
|
||||
context: { physics: physicsDouble() },
|
||||
next: 'exec',
|
||||
after: (ctx) => expect(physicsCalls(ctx)).toEqual([['setAngularVelocity', 'crate', 90]]),
|
||||
},
|
||||
{ name: 'without physics it continues', inputs: { object: crate, speed: 90 }, next: 'exec' },
|
||||
],
|
||||
GetVelocity: [
|
||||
{
|
||||
name: 'reads velocity and turning speed',
|
||||
inputs: { object: crate },
|
||||
context: { physics: physicsDouble() },
|
||||
outputs: { x: 12, y: -3, angular: 45 },
|
||||
},
|
||||
{
|
||||
name: 'an object without a dynamic body reads 0',
|
||||
inputs: { object: fakeEntity('wall') },
|
||||
context: { physics: physicsDouble() },
|
||||
outputs: { x: 0, y: 0, angular: 0 },
|
||||
},
|
||||
{ name: 'without physics it reads 0', inputs: { object: crate }, outputs: { x: 0, y: 0, angular: 0 } },
|
||||
],
|
||||
SetGravity: [
|
||||
{
|
||||
name: 'sets the scene gravity',
|
||||
inputs: { x: 0, y: -500 },
|
||||
context: { physics: physicsDouble() },
|
||||
next: 'exec',
|
||||
after: (ctx) => expect(physicsCalls(ctx)).toEqual([['setGravity', 0, -500]]),
|
||||
},
|
||||
{ name: 'without physics it continues', inputs: { x: 0, y: 1 }, next: 'exec' },
|
||||
],
|
||||
Spawn: [
|
||||
{
|
||||
name: 'no asset chosen logs a hint and continues with no object',
|
||||
|
||||
Reference in new issue
Block a user