@ignifx/physics-2d 0.0.0 → 0.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ignifx/physics-2d",
3
- "version": "0.0.0",
3
+ "version": "0.2.0",
4
4
  "description": "ignifx 2D physics: Rigidbody2D, 2D colliders, CharacterController2D, queries.",
5
5
  "keywords": [
6
6
  "ignifx",
@@ -9,6 +9,11 @@
9
9
  "homepage": "https://ignifx.com",
10
10
  "license": "Apache-2.0",
11
11
  "author": "Astrum Forge Studios",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/astrum-forge/ignifx.git",
15
+ "directory": "packages/physics-2d"
16
+ },
12
17
  "files": [
13
18
  "dist",
14
19
  "skills"
@@ -24,37 +29,28 @@
24
29
  },
25
30
  "publishConfig": {
26
31
  "access": "public",
27
- "provenance": true
28
- },
29
- "scripts": {
30
- "build": "tsdown",
31
- "typecheck": "tsc --build tsconfig.json tsconfig.test.json",
32
- "test": "vitest run --project node --root ../.. packages/physics-2d",
33
- "api-report": "api-extractor run --verbose",
34
- "docs:api": "typedoc",
35
- "api-report:update": "api-extractor run --local --verbose",
36
- "pack-check": "publint && attw --pack . --profile esm-only"
32
+ "provenance": false
37
33
  },
38
34
  "dependencies": {
39
35
  "@dimforge/rapier2d-compat": "^0.20.0"
40
36
  },
41
37
  "devDependencies": {
42
- "@arethetypeswrong/cli": "catalog:",
43
- "@ignifx/2d": "workspace:*",
44
- "@ignifx/core": "workspace:*",
45
- "@microsoft/api-extractor": "catalog:",
46
- "@types/webxr": "catalog:",
47
- "@webgpu/types": "catalog:",
48
- "publint": "catalog:",
49
- "tsdown": "catalog:",
50
- "typedoc": "catalog:",
51
- "typedoc-plugin-markdown": "catalog:",
52
- "typescript-native": "catalog:",
53
- "vitest": "catalog:"
38
+ "@arethetypeswrong/cli": "^0.18.5",
39
+ "@microsoft/api-extractor": "^7.59.0",
40
+ "@types/webxr": "^0.5.0",
41
+ "@webgpu/types": "^0.1.72",
42
+ "publint": "^0.3.24",
43
+ "tsdown": "^0.23.0",
44
+ "typedoc": "^0.28.20",
45
+ "typedoc-plugin-markdown": "^4.13.0",
46
+ "typescript-native": "npm:typescript@7.0.2",
47
+ "vitest": "^5.0.0",
48
+ "@ignifx/core": "0.2.0",
49
+ "@ignifx/2d": "0.2.0"
54
50
  },
55
51
  "peerDependencies": {
56
- "@ignifx/2d": "workspace:*",
57
- "@ignifx/core": "workspace:*"
52
+ "@ignifx/2d": "0.2.0",
53
+ "@ignifx/core": "0.2.0"
58
54
  },
59
55
  "peerDependenciesMeta": {
60
56
  "@ignifx/2d": {
@@ -70,5 +66,14 @@
70
66
  ],
71
67
  "capabilities": [],
72
68
  "skill": "./skills/physics-2d/SKILL.md"
69
+ },
70
+ "scripts": {
71
+ "build": "tsdown",
72
+ "typecheck": "tsc --build tsconfig.json tsconfig.test.json",
73
+ "test": "vitest run --project node --root ../.. packages/physics-2d",
74
+ "api-report": "api-extractor run --verbose",
75
+ "docs:api": "typedoc",
76
+ "api-report:update": "api-extractor run --local --verbose",
77
+ "pack-check": "publint && attw --pack . --profile esm-only"
73
78
  }
74
- }
79
+ }
@@ -1,46 +1,281 @@
1
1
  ---
2
2
  name: physics-2d
3
- description: Adds 2D physics to an ignifx game with @ignifx/physics-2d: Rigidbody2D, 2D colliders, CharacterController2D, one-way platforms, 2D triggers, and 2D queries, layered on the @ignifx/2d toolkit. Use when adding or editing 2D physics bodies, 2D colliders, platformer movement, or 2D queries in an ignifx project, or when the user mentions @ignifx/physics-2d, Rigidbody2D, or CharacterController2D.
3
+ description: Adds 2D physics to an ignifx game with @ignifx/physics-2d: Rapier 2D simulation on the fixed loop, Rigidbody2D, box/circle/capsule/polygon/edge/tilemap colliders, physics materials, 2D triggers, CharacterController2D with slopes, autostep, snap-to-ground and one-way platforms, 2D raycasts and shape queries, the collision layer matrix, and render interpolation. Use when adding or editing 2D physics bodies, 2D colliders, platformer movement, 2D triggers, or 2D queries in an ignifx project, or when the user mentions @ignifx/physics-2d, Rigidbody2D, Rapier, or CharacterController2D.
4
4
  license: Apache-2.0
5
5
  metadata:
6
- ignifx-version: "0.0.0-unreleased"
6
+ ignifx-version: "0.2.0"
7
7
  ---
8
8
 
9
9
  # @ignifx/physics-2d
10
10
 
11
- > **Scaffold notice.** ignifx is in the planning phase and `@ignifx/physics-2d` is an empty package. Every section below is filled by Phase 6 of `docs/plan/engineering-plan.md`; until then this file makes no API claims (`CONSTITUTION.md` Article V).
12
-
13
11
  ## What this is / when to use
14
12
 
15
- `@ignifx/physics-2d` is the ignifx extension that provides 2D physics: `Rigidbody2D`, 2D colliders, `CharacterController2D`, and queries _(populated in Phase 6)_.
13
+ `@ignifx/physics-2d` is the ignifx extension that simulates 2D rigid bodies with **Rapier 2D**
14
+ (`@dimforge/rapier2d-compat`, Apache-2.0, ADR-0006). Add a 2D collider to make an entity solid, add
15
+ a `Rigidbody2D` to make it move, and read the result off `entity.transform.position2D` /
16
+ `rotation2D` like any other pose.
17
+
18
+ Units are **metres and seconds**, +Y is up, and `rotation2D` is degrees counter-clockwise about +Z.
19
+ Pixels-per-unit affects only rendering (`docs/architecture/11-2d-toolkit.md` §3). The simulation is
20
+ stepped directly by ignifx's fixed loop — there is no Babylon Lite scene involved — so it advances
21
+ at a fixed rate whatever the frame rate does, and it runs headless with no GPU.
22
+
23
+ A world uses **either** `physics()` **or** `physics2d()`. Registering both throws `IGX-1101`.
16
24
 
17
25
  ## Environment
18
26
 
19
- Engine version, the pinned Babylon Lite version, and the registration snippet for this extension _(populated in Phase 6)_.
27
+ - Engine: ignifx `0.0.0-unreleased`; this package targets `@ignifx/core` `>=0.0.0 <1.0.0`.
28
+ - Dependency: `@dimforge/rapier2d-compat` `^0.20.0`. The `-compat` build **inlines** its WebAssembly
29
+ as base64, so nothing has to be served and Node, the browser, and Electron all take the same path.
30
+ - `@ignifx/2d` is an **optional** peer: you need it only for `TilemapCollider2D`'s data type, so a
31
+ game that draws its own graphics can use 2D physics on its own.
32
+ - Register it like any extension; nothing happens at import time.
33
+
34
+ ```ts run
35
+ import { createApp } from "@ignifx/core";
36
+ import { physics2d } from "@ignifx/physics-2d";
37
+
38
+ const app = await createApp({ headless: true, extensions: [physics2d()] });
39
+ await app.start();
40
+ ```
41
+
42
+ `ignifx.config.ts` carries the settings under `physics2d`:
43
+
44
+ | Setting | Default | Meaning |
45
+ | -------------------- | --------------- | ------------------------------------------------------------------ |
46
+ | `gravity` | `{x:0,y:-9.81}` | World gravity, m/s²; +Y is up |
47
+ | `collisionMatrix` | `{}` | `{ layerName: [layerNames] }`; an unlisted layer collides with all |
48
+ | `defaultMaterial` | friction `0.6` | The surface a collider with no material of its own uses |
49
+ | `velocityIterations` | `0` | Rapier solver iterations; `0` leaves Rapier's own default of 4 |
50
+ | `interpolation` | `true` | Whether dynamic bodies interpolate between fixed steps |
51
+
52
+ `fixedDeltaTime` is **not** here: the step comes from the core `time` section and `app.time`.
20
53
 
21
54
  ## Mental model
22
55
 
23
- How this subsystem's components, services, and phases fit into the App → World → Entity model _(populated in Phase 6)_.
56
+ ```
57
+ app.physics2d (Physics2DService) gravity · raycast · raycastAll · overlapCircle/Box · shapeCast · rapier
58
+ │
59
+ ├─ Rigidbody2D dynamic | kinematic | static, one per entity
60
+ ├─ Box/Circle/Capsule/Polygon/Edge/TilemapCollider2D several per entity form one compound body
61
+ └─ CharacterController2D kinematic capsule or box, collide-and-slide
62
+
63
+ per fixed step:
64
+ Systems(FixedUpdate, -100) restore the authoritative pose of interpolated bodies
65
+ scripts.fixedUpdate(dt) forces, velocities, controller.move()
66
+ Systems(FixedUpdate, 100) step Rapier · snapshot poses · dispatch collision and trigger events
67
+ Systems(PreRender, -500) write lerp(previous, current, time.fixedStepAlpha)
68
+ ```
69
+
70
+ - Bodies and colliders are (re)built at the **start of the next fixed step**, never mid-frame, so
71
+ body creation order — and therefore Rapier's internal order — follows entity creation order.
72
+ - An entity with 2D colliders and no `Rigidbody2D` gets an implicit **static** body placed once.
73
+ Moving it afterwards logs `IGX-1151`; anything that moves needs a kinematic `Rigidbody2D`.
74
+ - A body's entity should be a **root** entity; a parented one logs `IGX-1157`.
75
+ - The callbacks are the **3D names** — `onCollisionEnter`, `onCollisionStay`, `onCollisionExit`,
76
+ `onTriggerEnter`, `onTriggerExit` — carrying the 2D payloads. There is no `onCollisionEnter2D`.
77
+ Type the parameter as the 2D payload — `onTriggerEnter(trigger: TriggerEvent2D): void` — rather
78
+ than accepting core's `unknown` and casting: method parameters are bivariant, so the narrow
79
+ signature satisfies `ScriptCallbacks`, and the cast trips `typescript/no-unsafe-type-assertion`
80
+ under the engine's own lint settings.
24
81
 
25
82
  ## First app
26
83
 
27
- A complete, compiling example that registers the extension and uses it once _(populated in Phase 6)_.
84
+ ```ts run
85
+ import { createApp } from "@ignifx/core";
86
+ import { BoxCollider2D, physics2d, Rigidbody2D } from "@ignifx/physics-2d";
87
+
88
+ const app = await createApp({ headless: true, extensions: [physics2d()] });
89
+ await app.start();
90
+
91
+ const floor = app.world.createEntity("Floor");
92
+ floor.transform.position = { x: 0, y: -0.5, z: 0 };
93
+ floor.addComponent(BoxCollider2D, { size: { x: 20, y: 1 } });
94
+
95
+ const crate = app.world.createEntity("Crate");
96
+ crate.transform.position = { x: 0, y: 5, z: 0 };
97
+ crate.addComponent(BoxCollider2D, { size: { x: 1, y: 1 } });
98
+ const body = crate.addComponent(Rigidbody2D, { mass: 2 });
99
+
100
+ for (let index = 0; index < 240; index += 1) {
101
+ app.step(1 / 60);
102
+ }
103
+ app.log.info("crate y:", crate.transform.position.y, "mass:", body.computedMass);
104
+ ```
28
105
 
29
106
  ## Core APIs
30
107
 
31
- Component and service tables generated from `skills/ignifx/references/api/physics-2d.md` _(populated in Phase 6)_.
108
+ `Rigidbody2D` — `bodyType` (`"dynamic" | "kinematic" | "static"`), `mass` (exact kilograms; `0`
109
+ means "weigh the colliders at 1 kg/m²"), `gravityScale`, `linearDamping`, `angularDamping`,
110
+ `freezeRotation`, `interpolation`, `collisionEvents` (`"auto" | "on" | "off"`). At runtime:
111
+ `linearVelocity` (m/s), `angularVelocity` (**degrees**/s), `computedMass`, `addForce`, `addImpulse`,
112
+ `addTorque`, `teleport(position, rotationDegrees?)`, and the `rapier` escape hatch.
113
+
114
+ Colliders share `offset`, `isTrigger`, `material` (a `.physicsmaterial.json` asset), the
115
+ `inlineMaterial` fallback, `frictionCombine` / `restitutionCombine`, `oneWay`, and `layerOverride`:
116
+
117
+ | Collider | Geometry |
118
+ | ------------------- | ----------------------------------------------------------- |
119
+ | `BoxCollider2D` | `size` — an axis-aligned rectangle |
120
+ | `CircleCollider2D` | `radius`; the larger scale axis wins |
121
+ | `CapsuleCollider2D` | `radius`, `height` (tip to tip), `direction` (`"x" \| "y"`) |
122
+ | `PolygonCollider2D` | `points` → the **convex hull** of the list |
123
+ | `EdgeCollider2D` | `points` → an open chain; infinitely thin |
124
+ | `TilemapCollider2D` | `collisionData` from a `Tilemap` |
125
+
126
+ `CharacterController2D` — `shape` (`"capsule" | "box"`), `radius`, `height`, `offset`, `slopeLimit`
127
+ (degrees), `stepOffset`, `snapToGround`, `skinWidth`, `onOneWayPlatforms`, `pushBodies`,
128
+ `interpolation`. Drive it with `move(delta)` from `fixedUpdate`; read `isGrounded`, `velocity`,
129
+ `groundNormal`, and `onCollided`.
130
+
131
+ `app.physics2d` — `gravity`, `raycast`, `raycastAll`, `overlapCircle`, `overlapBox`, `shapeCast`,
132
+ and `rapier` (the unstable escape hatch). Full signatures:
133
+ `skills/ignifx/references/api/physics-2d.md`.
32
134
 
33
135
  ## Recipes
34
136
 
35
- Task-oriented examples extracted from `examples/recipes/` _(populated in Phase 6)_.
137
+ ### A trigger zone
138
+
139
+ ```ts run
140
+ import { Script } from "@ignifx/core";
141
+ import type { ScriptCallbacks } from "@ignifx/core";
142
+ import type { TriggerEvent2D } from "@ignifx/physics-2d";
143
+
144
+ export class Coin extends Script implements ScriptCallbacks {
145
+ static typeId = "mygame/Coin";
146
+
147
+ onTriggerEnter(trigger: TriggerEvent2D): void {
148
+ if (trigger.other?.name === "Player") {
149
+ this.entity.destroy();
150
+ }
151
+ }
152
+ }
153
+ ```
154
+
155
+ Give the coin a collider with `isTrigger: true`; give the player anything that moves. Both entities
156
+ receive the callback, and `trigger.other` and `trigger.otherCollider` are always the real objects —
157
+ Rapier reports both colliders, so 2D has none of the identity gaps 3D physics documents.
158
+
159
+ ### Platformer movement
160
+
161
+ ```ts run
162
+ import { Script, Vec2 } from "@ignifx/core";
163
+ import type { ScriptCallbacks } from "@ignifx/core";
164
+ import { CharacterController2D } from "@ignifx/physics-2d";
165
+
166
+ export class Platformer extends Script implements ScriptCallbacks {
167
+ static typeId = "mygame/Platformer";
168
+
169
+ /** Metres per second. */
170
+ speed = 5;
171
+
172
+ /** The vertical speed a jump starts at. */
173
+ jumpSpeed = 9;
174
+
175
+ readonly #velocity = new Vec2();
176
+
177
+ fixedUpdate(dt: number): void {
178
+ const controller = this.entity.requireComponent(CharacterController2D);
179
+ this.#velocity.x = this.speed;
180
+ if (controller.isGrounded) {
181
+ this.#velocity.y = this.jumpSpeed;
182
+ } else {
183
+ this.#velocity.y -= 9.81 * dt;
184
+ }
185
+ controller.move({ x: this.#velocity.x * dt, y: this.#velocity.y * dt });
186
+ }
187
+ }
188
+ ```
189
+
190
+ Gravity is **not** applied by the controller: a bare `CharacterController2D` is purely kinematic, so
191
+ the script owns the vertical velocity, coyote time, and variable jump height.
192
+
193
+ ### A raycast
194
+
195
+ ```ts run
196
+ import { createApp } from "@ignifx/core";
197
+ import { BoxCollider2D, physics2d } from "@ignifx/physics-2d";
198
+
199
+ const app = await createApp({ headless: true, extensions: [physics2d()] });
200
+ await app.start();
201
+ const ground = app.world.createEntity("Ground");
202
+ ground.addComponent(BoxCollider2D, { size: { x: 20, y: 1 } });
203
+ app.step(1 / 60);
204
+
205
+ const hit = app.physics2d.raycast({ x: 0, y: 5 }, { x: 0, y: -1 }, 20);
206
+ if (hit !== null) {
207
+ app.log.info("hit:", hit.entity.name, "y:", hit.point.y);
208
+ }
209
+ ```
210
+
211
+ ### Tilemap collision
212
+
213
+ ```ts run
214
+ import { createApp } from "@ignifx/core";
215
+ import { physics2d, TilemapCollider2D } from "@ignifx/physics-2d";
216
+
217
+ const app = await createApp({ headless: true, extensions: [physics2d()] });
218
+ await app.start();
219
+ const map = app.world.createEntity("Map");
220
+ const collider = map.addComponent(TilemapCollider2D);
221
+ collider.collisionData = {
222
+ cellSize: 1,
223
+ chunkSize: 8,
224
+ version: 1,
225
+ chunks: [
226
+ {
227
+ chunkX: 0,
228
+ chunkY: 0,
229
+ polygons: [
230
+ [
231
+ { x: -4, y: -1 },
232
+ { x: 4, y: -1 },
233
+ { x: 4, y: 0 },
234
+ { x: -4, y: 0 },
235
+ ],
236
+ ],
237
+ oneWayEdges: [],
238
+ },
239
+ ],
240
+ };
241
+ app.step(1 / 60);
242
+ ```
243
+
244
+ Assign `tilemap.collisionData` from `@ignifx/2d` instead of building it by hand. The runtime watches
245
+ the data's `version` every fixed step and rebuilds the shapes when it changes, so a tile edit reaches
246
+ physics with no subscription of your own.
36
247
 
37
248
  ## File formats
38
249
 
39
- The file formats this subsystem reads and writes, with links to `skills/ignifx/references/formats/` _(populated in Phase 6)_.
250
+ `ignifx.physicsmaterial` — the **same** `.physicsmaterial.json` document 3D physics reads
251
+ (`skills/ignifx/references/formats/ignifx.physicsmaterial.md`): `{ "format":
252
+ "ignifx.physicsmaterial", "friction": 0.6, "restitution": 0 }`. `staticFriction` is ignored, because
253
+ Rapier 2D has one friction coefficient. The combine rules are collider fields, not asset fields.
254
+
255
+ Component field tables: `skills/ignifx/references/formats/components.md`.
40
256
 
41
257
  ## Gotchas
42
258
 
43
- The traps this subsystem has, each with the replacement to use instead _(populated in Phase 6)_.
259
+ - **Only sixteen layers filter.** Rapier packs membership and filter into 16 bits each, so a
260
+ collider on layer 16 or above logs `IGX-1152` and falls back to layer 0. Keep the layers 2D
261
+ physics uses in the first sixteen of `ignifx.config.ts`.
262
+ - **Queries need one completed step.** Rapier builds its broadphase inside `step`, so a query before
263
+ the first fixed step throws `IGX-1153`. Call `app.step(1 / 60)` once first.
264
+ - **Autostep needs a box.** `stepOffset` clears a 0.3 m step with `shape: "box"`; with the default
265
+ capsule of radius 0.2 it clears about 0.15 m and no more (measured, ADR-0006 Validation). Give a
266
+ stair-climbing character `shape: "box"`.
267
+ - **One-way platforms are a character-controller feature.** `oneWay` changes what
268
+ `CharacterController2D` collides with; rigid bodies fall through such a collider in both
269
+ directions in the MVP.
270
+ - **`PolygonCollider2D` is convex.** Rapier builds the convex hull of the points, so a concave
271
+ outline is silently filled in. Use several polygons on one entity, or an `EdgeCollider2D`.
272
+ - **`EdgeCollider2D` is infinitely thin.** A fast body can tunnel through it and a body that starts
273
+ inside it is not pushed out. It is for static level geometry.
274
+ - **Determinism is same-machine only.** `@dimforge/rapier2d-compat` is Rapier's main build, which
275
+ promises local determinism, not cross-platform bit-exactness; the
276
+ `-deterministic-` builds are the ones that do (ADR-0006 Validation).
277
+ - **`angularVelocity` is degrees per second**, matching `Transform.rotation2D`, while Rapier's own
278
+ API is radians. The `rapier` escape hatch gives you radians.
44
279
 
45
280
  ## Deprecated (current window)
46
281
 
@@ -48,4 +283,5 @@ None (pre-1.0: no deprecation window; breaking changes are listed in the changel
48
283
 
49
284
  ## Where to look next
50
285
 
51
- `skills/ignifx/SKILL.md` for the engine entry skill · `docs/architecture/` for design rationale.
286
+ `skills/ignifx/SKILL.md` for the engine entry skill · `docs/architecture/11-2d-toolkit.md` §8 and
287
+ ADR-0006 for design rationale.