@gg-web-engine/matter 0.0.72 → 0.0.74

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.
@@ -0,0 +1,191 @@
1
+ import { CharacterController2dOptions, CollisionGroup, DebugBody2DSettings, ICharacterController2dComponent, IEntity, Point2 } from '@gg-web-engine/core';
2
+ import { Body } from 'matter-js';
3
+ import { MatterRigidBodyComponent } from './matter-rigid-body.component';
4
+ import { MatterWorldComponent } from './matter-world.component';
5
+ import { MatterGgWorld, MatterPhysicsTypeDocRepo } from '../types';
6
+ /**
7
+ * A capsule-shaped kinematic character controller implemented as a direct discrete-query
8
+ * sweep-and-slide mover, driven entirely by this class's own `move()` - the 2D counterpart of a
9
+ * from-scratch native-engine character mover (see `gg-engine-physics-adapter`'s own section on this
10
+ * general pattern).
11
+ *
12
+ * **Why this isn't built on `createRigidBody`/a `kinematic_pos` body**: matter-js has no native
13
+ * kinematic body concept at all (see `MatterFactory.transformOptions`'s doc) - a `kinematic_pos`/
14
+ * `kinematic_vel` request there falls back to a plain `isStatic: true` body, which never moves and
15
+ * never pushes/wakes anything. This component instead constructs its own `Matter.Body` directly (a
16
+ * capsule via `Bodies.rectangle` with a `chamfer`, matching `MatterFactory.createRigidBody`'s own
17
+ * `CAPSULE` case), marks it `isStatic: true` so matter's own `Engine.update` never touches it, and
18
+ * **never adds it to `Composite`/`engine.world` at all** - there is no need to, since every query this
19
+ * class issues (`Matter.Query.collides`) is run directly against this body and an explicit list of
20
+ * other bodies, not through matter's own broadphase/`Engine.update` pipeline. This also means
21
+ * matter-js's own per-engine `collisionFilter`-aware broadphase (`Detector.canCollide`) never runs for
22
+ * this body either - `collectObstacles()` below replicates that exact category/mask check by hand
23
+ * before ever calling `Query.collides`, since that function tests raw geometry with no collision-group
24
+ * awareness of its own.
25
+ *
26
+ * **Why movement is substep-marched rather than a single discrete overlap test at the final
27
+ * position**: matter-js has no continuous collision detection at all (see `MatterFactory
28
+ * .transformOptions`'s own `ccd` note) and `Matter.Query.collides`/`Collision.collides` are purely
29
+ * discrete overlap tests at whatever transform a body currently has - matter-js exposes no
30
+ * swept/time-of-impact query to call instead. A
31
+ * single test-then-clamp at the fully-displaced candidate position would tunnel clean through any
32
+ * obstacle thinner than the requested displacement (a large single-tick `move()` call, or a thin wall,
33
+ * would simply never register contact at all). `marchMove` compensates by subdividing the requested
34
+ * delta into substeps no longer than `min(radius, 0.1)` and re-querying after each one, stopping at the
35
+ * first substep that would overlap something - a standard workaround for discrete-only collision
36
+ * detection, and the direct 2D analog of what a sweep primitive gives other backends for free.
37
+ *
38
+ * **Collision normal convention**: `Matter.Collision`'s own `collision.bodyA`/`collision.bodyB` (and
39
+ * `parentA`/`parentB`) are reassigned by ascending `Body.id`, not by the order two bodies were passed
40
+ * into `Collision.collides`/`Query.collides` - and the final `collision.normal` is oriented so that
41
+ * `dot(normal, bodyB.position - bodyA.position) <= 0` always holds (verified empirically against
42
+ * `Collision.js`'s own flip check; its inline comment claims the opposite, "facing away from bodyA",
43
+ * which does not match the code - see `gg-engine-physics-adapter-matter`'s own note on this same
44
+ * gotcha for the engine-wide `collisionStart`/`collisionEnd` event wiring). Concretely this means the
45
+ * final normal always points *towards* `collision.bodyA`/`parentA`, away from `bodyB`/`parentB`,
46
+ * regardless of which side of the original `Query.collides(body, bodies)` call each one came from -
47
+ * `normalTowardCharacter` below re-derives a consistent "points away from the obstacle, towards this
48
+ * character" direction from that by comparing `parentA`/`parentB` against this character's own native
49
+ * body, not by assuming a fixed argument order.
50
+ *
51
+ * **Ground overlap for `Trigger2dEntity`**: since this character's phantom body is deliberately never
52
+ * added to matter's own `Composite`, matter's native `collisionStart`/`collisionEnd` engine events
53
+ * (what `MatterTriggerComponent`'s own enter/exit detection is normally driven by) can never fire for
54
+ * it - there is no pair for the engine to ever notice. `MatterTriggerComponent.checkOverlaps()`
55
+ * (already called once per tick by `Trigger2dEntity`, previously a no-op for matter-js since ordinary
56
+ * rigid-body overlaps are handled by those native events instead) now *additionally* polls every
57
+ * `MatterCharacterControllerComponent` currently in the world via `Query.collides` each time it's
58
+ * called, entirely independently of the native event path - this was the natural fit given
59
+ * `checkOverlaps()` already existed as a per-tick hook with nothing else needing it for matter-js,
60
+ * rather than inventing a second, differently-shaped mechanism.
61
+ *
62
+ * **Colliding with other character controllers**: `collectObstacles()` below includes every other
63
+ * `MatterCharacterControllerComponent` currently in the world (found via `this.world.children`, not
64
+ * `Composite.allBodies`, for the same reason as the previous paragraph) alongside ordinary bodies -
65
+ * two characters block each other's movement the same way any other obstacle does.
66
+ *
67
+ * **Divergence from the interface's own options**: `minStepWidth` is accepted but not honored - see
68
+ * `applyStepAssist`'s own doc for why.
69
+ */
70
+ export declare class MatterCharacterControllerComponent implements ICharacterController2dComponent<MatterPhysicsTypeDocRepo> {
71
+ protected readonly world: MatterWorldComponent;
72
+ entity: IEntity | null;
73
+ name: string;
74
+ readonly radius: number;
75
+ readonly centersDistance: number;
76
+ readonly nativeBody: Body;
77
+ private readonly options;
78
+ private _up;
79
+ get up(): Point2;
80
+ set up(value: Point2);
81
+ private _isGrounded;
82
+ get isGrounded(): boolean;
83
+ private _groundNormal;
84
+ get groundNormal(): Point2 | null;
85
+ /** See `ICharacterController2dComponent.ignoredBodies`'s doc. Consulted fresh by `collectObstacles`
86
+ * every `move()` call. */
87
+ readonly ignoredBodies: Set<MatterRigidBodyComponent>;
88
+ private _added;
89
+ readonly debugBodySettings: DebugBody2DSettings;
90
+ get position(): Point2;
91
+ set position(value: Point2);
92
+ get rotation(): number;
93
+ set rotation(value: number);
94
+ protected _ownCGsMask: number;
95
+ protected _interactWithCGsMask: number;
96
+ get ownCollisionGroups(): ReadonlyArray<CollisionGroup>;
97
+ set ownCollisionGroups(value: ReadonlyArray<CollisionGroup> | 'all');
98
+ get interactWithCollisionGroups(): ReadonlyArray<CollisionGroup>;
99
+ set interactWithCollisionGroups(value: ReadonlyArray<CollisionGroup> | 'all');
100
+ private updateCollisionFilter;
101
+ constructor(world: MatterWorldComponent, options: CharacterController2dOptions, transform?: {
102
+ position?: Point2;
103
+ rotation?: number;
104
+ });
105
+ /**
106
+ * Every other body currently in the world this character's own queries must consider - excludes
107
+ * sensors (triggers never physically block anything, see `ITrigger2dComponent`), everything in
108
+ * `ignoredBodies` (consulted fresh here, every call), and anything this character's own collision
109
+ * groups wouldn't interact with anyway (`Query.collides`/`Collision.collides` test raw geometry
110
+ * only and know nothing about `collisionFilter`, unlike matter's own `Detector` - so `canCollideWith`
111
+ * calls `Detector.canCollide` directly against this character's own `nativeBody.collisionFilter`,
112
+ * which the `ownCollisionGroups`/`interactWithCollisionGroups` setters keep in sync).
113
+ *
114
+ * Also includes every *other* `MatterCharacterControllerComponent` currently in the world, via its
115
+ * own phantom `nativeBody` - without this, two character controllers could freely overlap and pass
116
+ * straight through each other, since neither one's phantom body is ever added to
117
+ * `Composite`/`engine.world` (see this class's own doc) and so neither is ever a candidate for the
118
+ * other's queries through `Composite.allBodies` alone.
119
+ */
120
+ private collectObstacles;
121
+ private canCollideWith;
122
+ /** See this class's own doc for the sign convention this re-derives (`parentA`/`parentB`, not
123
+ * calling-argument order). Returns a normal pointing away from the obstacle, towards this
124
+ * character. */
125
+ private normalTowardCharacter;
126
+ private otherParent;
127
+ private isWalkable;
128
+ /**
129
+ * Pushes this character's phantom body out of any obstacle it currently overlaps at `pos`, via
130
+ * `Query.collides`, iterating a few times since resolving one contact can reveal/deepen another -
131
+ * a discrete query like this one can start a tick already embedded (e.g. from a previous tick's
132
+ * rounding/clamping). Must run before any marching this tick.
133
+ */
134
+ private recoverFromPenetration;
135
+ /**
136
+ * Marches this character's phantom body from `start` towards `start + delta` in small substeps
137
+ * (see this class's own doc for why substepping is needed at all, in place of a true sweep), moving
138
+ * the native body via `Body.setPosition` after every accepted substep, and stopping at the first
139
+ * substep whose query finds an obstacle with a meaningful component opposing the direction of
140
+ * travel. `overlappingBodies` in the result always reflects the query at wherever this call
141
+ * finished (the fully-displaced position if never blocked, or the last-accepted position if it
142
+ * was) - used by `move()` to find dynamic bodies to push.
143
+ */
144
+ private marchMove;
145
+ /** Like `marchMove`, but always leaves the native body at `start` before returning - for a
146
+ * speculative query (step-up assist, ground snap/landing checks) that must not commit any movement
147
+ * unless the caller explicitly applies the returned position itself. */
148
+ private probe;
149
+ /**
150
+ * If the horizontal leg was blocked, tries lifting the phantom body up by up to `maxStepHeight`,
151
+ * retrying the same horizontal move at that height, and probing back down - only accepting the step
152
+ * if it both clears more horizontal distance than the unraised attempt *and* actually lands on
153
+ * walkable ground, not just a curved/vertical surface that happens to allow more clearance a hair
154
+ * higher up (see the general `gg-engine-physics-adapter` skill's own caution on this exact
155
+ * failure mode).
156
+ *
157
+ * **`minStepWidth` is not honored** - a step is accepted purely on `maxStepHeight`/walkability,
158
+ * with no separate check for how much free space sits on top of the ledge. An attempt at that
159
+ * check (probing forward from the landing spot by `minStepWidth` and requiring the ledge to still
160
+ * be walkable there) was tried and reverted: this mover's own step-up sequence routinely *accepts*
161
+ * a landing spot that is itself only a marginal, partial advance still snug against the same
162
+ * obstacle corner that blocked the original horizontal move (`marchMove`'s substep-and-slide
163
+ * approach, see this class's own doc, naturally creeps forward across several ticks rather than
164
+ * clearing a corner in one) - a width probe from a landing spot like that immediately re-hits the
165
+ * same corner and rejects the step outright, which stalls the character completely instead of
166
+ * letting it creep across a perfectly normal ledge over the next few ticks. `minStepWidth` is
167
+ * still accepted into this class's own options (see `CharacterController2dOptions.minStepWidth`'s
168
+ * own doc: "not every backend can honor this exactly").
169
+ */
170
+ private applyStepAssist;
171
+ /**
172
+ * Shoves a dynamic body the horizontal leg bumped into this tick - see
173
+ * `CharacterController2dOptions.pushMass`'s doc for the formula (an inelastic collision against a
174
+ * virtual mass moving at the character's own speed), applied here through
175
+ * `MatterRigidBodyComponent.linearVelocity`'s existing getter/setter (which already carries the
176
+ * conversion between matter-js's internal per-step velocity units and this engine's public m/s
177
+ * units - see `MATTER_VELOCITY_SCALE`'s own doc) rather than touching `Body.velocity` directly.
178
+ */
179
+ private pushDynamicBodies;
180
+ /**
181
+ * Resolves `desiredTranslation` fully synchronously (see the interface's own doc) via
182
+ * `recoverFromPenetration` + two axis-separated `marchMove` legs (horizontal, then vertical) +
183
+ * a ground-snap probe - see this class's own doc for the overall approach and why each piece is
184
+ * needed.
185
+ */
186
+ move(desiredTranslation: Point2, dt?: number): void;
187
+ clone(): MatterCharacterControllerComponent;
188
+ addToWorld(world: MatterGgWorld): void;
189
+ removeFromWorld(world: MatterGgWorld, dispose?: boolean): void;
190
+ dispose(): void;
191
+ }