pelican-engine 0.3.11 → 0.3.12
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/AGENTS.md +17 -1
- package/distribution.json +12 -12
- package/docs/STATUS.md +2 -1
- package/docs/api/REFERENCE.md +107 -91
- package/docs/api/index.json +107 -92
- package/docs/npm/GAME-AUTHOR.md +17 -1
- package/engine.manifest.json +1 -1
- package/lib/physics/index.js +3 -0
- package/lib/sdk/index.d.ts +16 -8
- package/lib/sdk/index.js +1 -1
- package/lib/simulation/rope.js +34 -5
- package/package.json +1 -1
package/docs/npm/GAME-AUTHOR.md
CHANGED
|
@@ -137,8 +137,24 @@ all earlier automatic ropes from that phase; the host tick may still have other
|
|
|
137
137
|
partial changes and must be recovered from a known-good checkpoint. The source
|
|
138
138
|
checkout includes `examples/rope-particles`.
|
|
139
139
|
|
|
140
|
+
For each latest-tick contact, `reactionImpulse` and `reactionAngularImpulse`
|
|
141
|
+
report the opposite impulse produced by the particle's position projections,
|
|
142
|
+
and its moment about the world origin. These optional fields are absent when
|
|
143
|
+
restoring older contact snapshots. They do not automatically act on a rigid body.
|
|
144
|
+
For an explicitly mapped dynamic body, game code can read its actual world center
|
|
145
|
+
of mass via `ctx.physics.centerOfMass(id)`, apply the linear impulse, and apply
|
|
146
|
+
`reactionAngularImpulse - centerOfMass × reactionImpulse` as torque impulse.
|
|
147
|
+
Apply each contact at most once for its `tick`; repeated `inspect` calls return
|
|
148
|
+
the same contacts. The source checkout has an illustrative `examples/rope-reaction`.
|
|
149
|
+
|
|
150
|
+
This sampled, manually applied reaction is delayed and uses positional correction,
|
|
151
|
+
not a coupled contact solver: it omits body velocity/effective mass, friction and
|
|
152
|
+
continuous collision. Applying the linear and angular impulses uses separate
|
|
153
|
+
operations and is not transactional if a tick fails; recover from a known-good
|
|
154
|
+
checkpoint. It must not be represented as complete two-way rigid-body interaction.
|
|
155
|
+
|
|
140
156
|
This is an unverified development-stage increment, not complete rope physics:
|
|
141
|
-
there is no built-in rope mesh, rigid-body reaction, segment or self collision,
|
|
157
|
+
there is no built-in rope mesh, automatic rigid-body reaction, segment or self collision,
|
|
142
158
|
or tearing. Moving entities to particle positions is not a rope renderer or
|
|
143
159
|
coupled physics. Plane normals must be unit length; `offset` is the signed
|
|
144
160
|
normal distance from the origin. Host intervals must be between 1/1000 and
|
package/engine.manifest.json
CHANGED
package/lib/physics/index.js
CHANGED
package/lib/sdk/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Pelican Engine public API. No renderer or GPU implementation types escape this module. */
|
|
2
|
-
export declare const SDK_VERSION = "0.3.
|
|
2
|
+
export declare const SDK_VERSION = "0.3.12";
|
|
3
3
|
export type Vec2 = readonly [number, number];
|
|
4
4
|
export type Vec3 = readonly [number, number, number];
|
|
5
5
|
export type Vec4 = readonly [number, number, number, number];
|
|
@@ -104,7 +104,7 @@ export interface SystemContext extends EngineFacade {
|
|
|
104
104
|
readonly ropes: RopeService;
|
|
105
105
|
}
|
|
106
106
|
/** CPU particles with sampled sphere/plane collisions. Collision only moves rope particles;
|
|
107
|
-
* no rigid-body reaction, segment/self collision, rendering or complete
|
|
107
|
+
* no automatic rigid-body reaction, segment/self collision, rendering or complete physics. */
|
|
108
108
|
export type RopeCollider = {
|
|
109
109
|
kind: "sphere";
|
|
110
110
|
id: string;
|
|
@@ -135,6 +135,17 @@ export interface RopeDefinition {
|
|
|
135
135
|
position: Vec3;
|
|
136
136
|
}[];
|
|
137
137
|
}
|
|
138
|
+
/** Sampled-collider contact from the most recently completed rope tick. */
|
|
139
|
+
export interface RopeContact {
|
|
140
|
+
tick: number;
|
|
141
|
+
particle: number;
|
|
142
|
+
colliderId: string;
|
|
143
|
+
penetration: number;
|
|
144
|
+
/** Opposite of particle projection impulses, N·s; absent in older snapshots. */
|
|
145
|
+
reactionImpulse?: Vec3;
|
|
146
|
+
/** Angular impulse about the world origin; absent in older snapshots. */
|
|
147
|
+
reactionAngularImpulse?: Vec3;
|
|
148
|
+
}
|
|
138
149
|
export interface RopeState {
|
|
139
150
|
tick: number;
|
|
140
151
|
simulatedSeconds: number;
|
|
@@ -145,12 +156,7 @@ export interface RopeState {
|
|
|
145
156
|
index: number;
|
|
146
157
|
position: Vec3;
|
|
147
158
|
}[];
|
|
148
|
-
contacts:
|
|
149
|
-
tick: number;
|
|
150
|
-
particle: number;
|
|
151
|
-
colliderId: string;
|
|
152
|
-
penetration: number;
|
|
153
|
-
}[];
|
|
159
|
+
contacts: RopeContact[];
|
|
154
160
|
maxDistanceError: number;
|
|
155
161
|
resources: {
|
|
156
162
|
particles: number;
|
|
@@ -396,6 +402,8 @@ export interface PhysicsService {
|
|
|
396
402
|
moveCharacter(entity: string, displacement: Vec3): void;
|
|
397
403
|
grounded(entity: string): boolean;
|
|
398
404
|
velocity(entity: string): Vec3;
|
|
405
|
+
/** World-space rigid-body center of mass, including collider offsets. */
|
|
406
|
+
centerOfMass(entity: string): Vec3;
|
|
399
407
|
setVelocity(entity: string, velocity: Vec3): void;
|
|
400
408
|
impulse(entity: string, impulse: Vec3): void;
|
|
401
409
|
/** Angular velocity in world-space radians per second. */
|
package/lib/sdk/index.js
CHANGED
package/lib/simulation/rope.js
CHANGED
|
@@ -9,6 +9,15 @@ export class RopeError extends Error {
|
|
|
9
9
|
const fail = (code, reason) => { throw new RopeError(code, reason); };
|
|
10
10
|
const vec = (v) => Array.isArray(v) && v.length === 3 && v.every(x => typeof x === "number" && Number.isFinite(x) && Math.abs(x) <= 1e6);
|
|
11
11
|
const copy = (v) => [v[0], v[1], v[2]];
|
|
12
|
+
const finiteVector = (v) => Array.isArray(v) && v.length === 3 && v.every(x => typeof x === "number" && Number.isFinite(x));
|
|
13
|
+
const copyContact = (contact) => {
|
|
14
|
+
const { reactionImpulse, reactionAngularImpulse, ...rest } = contact;
|
|
15
|
+
return {
|
|
16
|
+
...rest,
|
|
17
|
+
...(reactionImpulse === undefined ? {} : { reactionImpulse: copy(reactionImpulse) }),
|
|
18
|
+
...(reactionAngularImpulse === undefined ? {} : { reactionAngularImpulse: copy(reactionAngularImpulse) }),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
12
21
|
const length = (v) => Math.hypot(v[0], v[1], v[2]);
|
|
13
22
|
const validVec = (v, name) => vec(v) ? copy(v) : fail("ROPE_INPUT", `${name} must be a finite 3D vector (|component| <= 1e6)`);
|
|
14
23
|
const positive = (v, name) => typeof v === "number" && Number.isFinite(v) && v > 0 && v <= 1e6 ? v : fail("ROPE_INPUT", `${name} must be positive and <= 1e6`);
|
|
@@ -220,12 +229,30 @@ export class RopeSolver {
|
|
|
220
229
|
continue;
|
|
221
230
|
for (let k = 0; k < 3; k++)
|
|
222
231
|
p[k] += normal[k] * depth;
|
|
232
|
+
// Each positional projection changes the particle's velocity by depth / h.
|
|
233
|
+
// Report its equal-and-opposite impulse and moment on the sampled surface;
|
|
234
|
+
// the collider itself remains an immutable input and is not moved here.
|
|
235
|
+
const magnitude = -mass * depth / h;
|
|
236
|
+
const reaction = [normal[0] * magnitude, normal[1] * magnitude, normal[2] * magnitude];
|
|
237
|
+
const point = c.kind === "sphere"
|
|
238
|
+
? [c.center[0] + normal[0] * c.radius, c.center[1] + normal[1] * c.radius, c.center[2] + normal[2] * c.radius]
|
|
239
|
+
: [p[0] - normal[0] * radius, p[1] - normal[1] * radius, p[2] - normal[2] * radius];
|
|
240
|
+
const moment = [point[1] * reaction[2] - point[2] * reaction[1], point[2] * reaction[0] - point[0] * reaction[2], point[0] * reaction[1] - point[1] * reaction[0]];
|
|
241
|
+
if (!finiteVector(reaction) || !finiteVector(moment))
|
|
242
|
+
fail("ROPE_NUMERIC", "contact reaction exceeded finite numeric bounds");
|
|
223
243
|
const key = `${i}:${c.id}`;
|
|
224
244
|
const existing = contacts.get(key);
|
|
225
|
-
if (existing)
|
|
245
|
+
if (existing) {
|
|
226
246
|
existing.penetration = Math.max(existing.penetration, depth);
|
|
247
|
+
for (let k = 0; k < 3; k++) {
|
|
248
|
+
existing.reactionImpulse[k] += reaction[k];
|
|
249
|
+
existing.reactionAngularImpulse[k] += moment[k];
|
|
250
|
+
}
|
|
251
|
+
if (!finiteVector(existing.reactionImpulse) || !finiteVector(existing.reactionAngularImpulse))
|
|
252
|
+
fail("ROPE_NUMERIC", "contact reaction accumulation exceeded finite numeric bounds");
|
|
253
|
+
}
|
|
227
254
|
else
|
|
228
|
-
contacts.set(key, { tick: this.tickValue + 1, particle: i, colliderId: c.id, penetration: depth });
|
|
255
|
+
contacts.set(key, { tick: this.tickValue + 1, particle: i, colliderId: c.id, penetration: depth, reactionImpulse: reaction, reactionAngularImpulse: moment });
|
|
229
256
|
}
|
|
230
257
|
}
|
|
231
258
|
}
|
|
@@ -255,7 +282,7 @@ export class RopeSolver {
|
|
|
255
282
|
tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: false,
|
|
256
283
|
positions: this.positions.map(copy), velocities: this.velocities.map(copy),
|
|
257
284
|
anchors: [...this.anchors].map(([index, position]) => ({ index, position: copy(position) })).sort((a, b) => a.index - b.index),
|
|
258
|
-
contacts: this.contacts.map(
|
|
285
|
+
contacts: this.contacts.map(copyContact), maxDistanceError,
|
|
259
286
|
resources: { particles: this.positions.length, constraints: this.config.restLengths.length, anchors: this.anchors.size, colliders: this.colliders.length },
|
|
260
287
|
};
|
|
261
288
|
}
|
|
@@ -294,14 +321,16 @@ export class RopeSolver {
|
|
|
294
321
|
input.contacts.some(contact => !contact || contact.tick !== input.tick ||
|
|
295
322
|
!Number.isSafeInteger(contact.particle) || contact.particle < 0 || contact.particle >= count ||
|
|
296
323
|
!colliders.some(collider => collider.id === contact.colliderId) ||
|
|
297
|
-
typeof contact.penetration !== "number" || !Number.isFinite(contact.penetration) || contact.penetration <= 0
|
|
324
|
+
typeof contact.penetration !== "number" || !Number.isFinite(contact.penetration) || contact.penetration <= 0 ||
|
|
325
|
+
(contact.reactionImpulse === undefined) !== (contact.reactionAngularImpulse === undefined) ||
|
|
326
|
+
(contact.reactionImpulse !== undefined && (!finiteVector(contact.reactionImpulse) || !finiteVector(contact.reactionAngularImpulse)))))
|
|
298
327
|
fail("ROPE_SNAPSHOT", "invalid contact state");
|
|
299
328
|
this.positions = input.positions.map(copy);
|
|
300
329
|
this.velocities = input.velocities.map(copy);
|
|
301
330
|
this.forces = input.forces.map(copy);
|
|
302
331
|
this.anchors = anchors;
|
|
303
332
|
this.colliders = colliders;
|
|
304
|
-
this.contacts = input.contacts.map(
|
|
333
|
+
this.contacts = input.contacts.map(copyContact);
|
|
305
334
|
this.tickValue = input.tick;
|
|
306
335
|
}
|
|
307
336
|
dispose() {
|