@maccesar/titools 4.14.0 → 4.16.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/README.md +7 -1
- package/package.json +1 -1
- package/skills/ti-game/SKILL.md +80 -180
- package/skills/ti-game/references/api.md +83 -18
- package/skills/ti-game/references/debugging-performance.md +103 -0
- package/skills/ti-game/references/examples.md +54 -0
- package/skills/ti-game/references/patterns.md +699 -0
- package/skills/ti-game/references/project-setup.md +21 -11
- package/skills/ti-game/references/recipes-action.md +633 -0
- package/skills/ti-game/references/recipes-adventure-effects.md +340 -0
- package/skills/ti-game/references/roadmap.md +44 -63
- package/skills/ti-game/references/tilemaps.md +125 -0
- package/skills/ti-game/references/recipes.md +0 -1574
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# ti.game API reference
|
|
2
2
|
|
|
3
|
-
Complete JS surface of `ti.game` as of upstream `main` at `
|
|
3
|
+
Complete JS surface of `ti.game` as of upstream `main` at `d587081`, 2026-08-28, verified against every Android proxy/engine and its iOS twin. Defaults come from native fields, not prose. The manifests still read **`0.5.0`**, but the performance HUD/telemetry and native `TileLayer` landed afterward without another bump. Read the number as a floor, never as a feature list. Older `0.3.0`/`0.4.0` artifacts also vary by build date; feature-detect when the artifact is unknown. Read a property before writing it, because Kroll can retain an unknown write on the proxy. Methods are safer probes (`typeof Game.createTileLayer === 'function'`), as are normalized properties (`solidMode` reads an unknown name back as `'block'` only when the engine implements it).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Properties are live unless their table says creation-time or read-only. Every writable property can be passed to its `create*` factory. `TileLayer` has four cross-platform creation-time inputs (`legend`, `firstGid`, `cols`, `rows`) because Android does not expose the live setters that iOS has. All durations crossing the JS boundary are **milliseconds**.
|
|
6
6
|
|
|
7
7
|
<!-- TOC-START -->
|
|
8
8
|
## Contents
|
|
@@ -17,6 +17,7 @@ Every property listed is **live**: reading returns the current native value, eve
|
|
|
17
17
|
- [Sound](#sound)
|
|
18
18
|
- [Emitter](#emitter)
|
|
19
19
|
- [Rope](#rope)
|
|
20
|
+
- [TileLayer](#tilelayer)
|
|
20
21
|
- [Events at a glance](#events-at-a-glance)
|
|
21
22
|
- [Gotchas the property tables do not show](#gotchas-the-property-tables-do-not-show)
|
|
22
23
|
|
|
@@ -38,6 +39,7 @@ const Game = require('ti.game');
|
|
|
38
39
|
| `Game.createSound(options)` | Sound |
|
|
39
40
|
| `Game.createEmitter(options)` | Emitter — particle system |
|
|
40
41
|
| `Game.createRope(options)` | Rope — Verlet chain |
|
|
42
|
+
| `Game.createTileLayer(options)` | TileLayer — visible-cell tile renderer and collision grid |
|
|
41
43
|
|
|
42
44
|
Easing constants (for `sprite.animate({ easing })`): `Game.EASE_LINEAR`, `Game.EASE_IN`, `Game.EASE_OUT`, `Game.EASE_IN_OUT`, `Game.EASE_BOUNCE`, `Game.EASE_ELASTIC`.
|
|
43
45
|
|
|
@@ -60,10 +62,11 @@ Game.createSprite({ anchor: 'bottom-left', hitboxScaleY: '55%', opacity: '80%' }
|
|
|
60
62
|
| Emitter | `startScale`, `endScale`, `startOpacity`, `endOpacity` |
|
|
61
63
|
| Rope | `damping` |
|
|
62
64
|
| Sound | `volume` |
|
|
65
|
+
| TileLayer | `opacity`, `scrollFactor`, `restitution` |
|
|
63
66
|
|
|
64
67
|
Over 100% is meaningful where the ratio is a multiplier — `scaleX: '200%'` is twice as wide. Text sprites are sprites, so they take all of it (`Game.createText({ anchor: 'top-left', opacity: '60%' })`).
|
|
65
68
|
|
|
66
|
-
They do **not** apply to coordinates, sizes, degrees, speeds, or the car model's `grip` and `drag
|
|
69
|
+
They do **not** apply to coordinates, sizes, degrees, speeds, or the friction coefficients — the car model's `grip` and `drag`, and `linearDamping`, are all per-second rates, not fractions.
|
|
67
70
|
|
|
68
71
|
**Anchor names** (`anchorX`, `anchorY`, and `anchor` for both at once):
|
|
69
72
|
|
|
@@ -92,9 +95,9 @@ A normal Titanium view — add it to a window or any container, size it with the
|
|
|
92
95
|
|
|
93
96
|
| Member | Type | Default | Notes |
|
|
94
97
|
| --- | --- | --- | --- |
|
|
95
|
-
| `add(object)` / `add([objects])` | method | — | Accepts Sprite, Emitter or
|
|
98
|
+
| `add(object)` / `add([objects])` | method | — | Accepts Sprite/Text, Emitter, Rope, or TileLayer. An array crosses the bridge once and is committed under native scene locks — build a level into an array and add it in one call |
|
|
96
99
|
| `remove(object)` | method | — | Remove one object from the scene — **and, recursively, every sprite attached to it** (see [Attachment](#attachment)) |
|
|
97
|
-
| `removeAllSprites()` | method | — |
|
|
100
|
+
| `removeAllSprites()` | method | — | Clears Sprite/Text objects and their attachments. It does **not** remove emitters, ropes, or tile layers; track and remove those separately |
|
|
98
101
|
| `pause()` / `resume()` | method | — | Render loop control. The activity/app lifecycle already pauses and resumes automatically |
|
|
99
102
|
| `backgroundColor` | string | — | GL clear color |
|
|
100
103
|
| `maxFps` | int | `0` | Frame cap. `60` stops 120 Hz ProMotion displays from doubling render work; `0` = display refresh rate |
|
|
@@ -107,16 +110,16 @@ A normal Titanium view — add it to a window or any container, size it with the
|
|
|
107
110
|
| `stopFollow()` | method | — | Stop following; the camera stays where it is |
|
|
108
111
|
| `shake({ strength, duration })` | method | `12` px, `400` ms | Detuned-sine rumble on the projection only — follow, bounds and touch mapping are unaffected |
|
|
109
112
|
| `raycast(x0, y0, x1, y1, groups)` | method | — | One-shot nearest-hit query along the segment against **visible, non-`screenFixed`** sprites carrying a `collisionGroup` in `groups` (omit for any tagged sprite). Returns `null` for a clear ray, else `{ x, y, distance, group, sprite, normal: { x, y } }`. Rect hitboxes are tested as their AABB, circle hitboxes exactly; a ray starting inside a hitbox reports it at distance 0 |
|
|
110
|
-
| `findPath(from, to, options)` | method | — | Grid A\* from `from` to `to` (`{ x, y }` world points) around
|
|
113
|
+
| `findPath(from, to, options)` | method | — | Grid A\* from `from` to `to` (`{ x, y }` world points) around visible, non-`screenFixed` sprites and fully solid TileLayer cells carrying a `collisionGroup` in `options.groups` (omit for any tagged obstacle). Returns `{ x, y }` waypoints ready for `sprite.followPath()`, or `null`. Options below |
|
|
111
114
|
| `after(ms, callback)` | method | — | Runs `callback` once after `ms` of **game time** — the delay stretches under `timeScale` slow motion, freezes at `0` and pauses with the render loop, unlike `setTimeout`. Returns an int id. The callback receives `{ id }` |
|
|
112
115
|
| `every(ms, callback)` | method | — | Like `after()`, repeating until cancelled. Fires at most once per frame, and restarts its interval after a stall instead of bursting to catch up |
|
|
113
116
|
| `cancelTimer(id)` | method | — | Cancels a timer from `after()` / `every()` |
|
|
114
117
|
| `cameraEffect` | string | `'none'` | Fullscreen shader pass: `'none'`, `'tint'`, `'glitch'`. With `'none'` the extra pass is skipped entirely |
|
|
115
118
|
| `cameraTint` | string | — | Color for the `'tint'` effect, e.g. `'#4f8'` (night vision, poison, flashback) |
|
|
116
119
|
| `cameraEffectIntensity` | float | `1` | 0..1 — tint mix or glitch amount |
|
|
117
|
-
| `debug` | bool | `false` |
|
|
120
|
+
| `debug` | bool / dict | `false` | `true` draws all collision shapes. Object form: `{ hitbox, hud, hudFont }`; `hud` is `true` or a corner name. Readback is normalized to `{ hitbox, hud: false | corner }`. See [debugging-performance.md](debugging-performance.md) |
|
|
118
121
|
|
|
119
|
-
Events: `press`, `tap`, `release` — fired for **every** touch anywhere on the view, with payload `x`, `y` in scene coordinates (tap-anywhere controls, flappy-style).
|
|
122
|
+
Events: `press`, `tap`, `release` — fired for **every** touch anywhere on the view, with payload `x`, `y` in scene coordinates (tap-anywhere controls, flappy-style). `resize` carries the real surface `width`, `height` in pixels. `performance` carries a one-second telemetry snapshot only while a listener exists; see [debugging-performance.md](debugging-performance.md).
|
|
120
123
|
|
|
121
124
|
Plus `timer` with payload `id`, fired only for `after()` / `every()` calls made **without** a callback — pass a callback or listen for the event, not both.
|
|
122
125
|
|
|
@@ -142,7 +145,7 @@ Anything that is not a sprite — `null`, a plain object, a number — clears th
|
|
|
142
145
|
| Option | Default | Effect |
|
|
143
146
|
| --- | --- | --- |
|
|
144
147
|
| `cellSize` | `32` | Grid resolution in px. Match it to the tile size on a tile map so the grid lines up with the walls; a coarser grid is faster and blockier |
|
|
145
|
-
| `groups` | all tagged
|
|
148
|
+
| `groups` | all tagged obstacles | Array of sprite or TileLayer `collisionGroup` names to treat as obstacles |
|
|
146
149
|
| `clearance` | `0` | Extra obstacle inflation in px. About half the walker's width keeps it from scraping corners, since the path is a line for the sprite's **center** |
|
|
147
150
|
| `bounds` | the whole surface | `{ minX, minY, maxX, maxY }` search rect — also the walkable area, so a floor clamp goes here |
|
|
148
151
|
| `diagonals` | `true` | Octile moves. Diagonals never cut a blocked corner |
|
|
@@ -150,7 +153,7 @@ Anything that is not a sprite — `null`, a plain object, a number — clears th
|
|
|
150
153
|
|
|
151
154
|
Waypoints come back with the **exact** start and goal as first and last point, so the walk begins under the sprite's feet and ends on the tapped pixel. A blocked start or goal snaps outward to the nearest free cell (up to 4 rings), which is why tapping an obstacle walks to its edge instead of returning `null`. `null` means: no route, degenerate `bounds`, `cellSize <= 0`, or a grid over ~1M cells (a runaway `bounds`/`cellSize` combination fails the query rather than allocating).
|
|
152
155
|
|
|
153
|
-
Everything is built per call on the calling thread — no persistent grid, no GL work. Like `raycast` it is a **discrete query**: run it on taps and AI timers (`gameView.every`), never per frame.
|
|
156
|
+
Everything is built per call on the calling thread — no persistent grid, no GL work. Fully solid TileLayer cells are rasterized; one-way cells are intentionally walkable. Like `raycast` it is a **discrete query**: run it on taps and AI timers (`gameView.every`), never per frame.
|
|
154
157
|
|
|
155
158
|
```javascript
|
|
156
159
|
const path = gameView.findPath(
|
|
@@ -236,6 +239,8 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
|
|
|
236
239
|
| --- | --- | --- |
|
|
237
240
|
| `velocityX`, `velocityY` | `0` | px/s |
|
|
238
241
|
| `gravity` | `0` | px/s², added to `velocityY`. Per sprite — there is no global gravity |
|
|
242
|
+
| `gravityX` | `0` | px/s², added to `velocityX`. The **sibling** of `gravity`, not half of a vector: `gravity` keeps its exact vertical meaning. Wind, a conveyor, or a top-down game whose "down" is sideways (`gravity: 0`, `gravityX` non-zero). Since 2026-08-27 |
|
|
243
|
+
| `linearDamping` | `0` | Fraction of speed shed per second, applied to both velocity axes before the position integration — rolling friction for ordinary sprites, which `drag` never was (that one only runs inside `carMode`). ~`0.6` is a pool ball on felt. Proportional, so a fast body sheds a lot and a slow one very little; below **4 px/s** the engine zeroes both axes outright, because a proportion never reaches zero on its own. Negative values are clamped to 0. Since 2026-08-27 |
|
|
239
244
|
| `maxSpeed` | `500` | px/s cap applied **only** to `thrust` acceleration and the `carMode` model. Writing `velocityX`/`velocityY` directly is never clamped |
|
|
240
245
|
| `angularVelocity` | `0` | deg/s |
|
|
241
246
|
| `thrust` | `0` | px/s² along the current heading (Newtonian flight) |
|
|
@@ -248,10 +253,30 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
|
|
|
248
253
|
| --- | --- | --- |
|
|
249
254
|
| `solidWith` | — | Array of group names that block this sprite. The engine pushes it out along the axis of least penetration |
|
|
250
255
|
| `onGround` | `false` | Read-only. Gate jumps on it |
|
|
251
|
-
| `restitution` | `0` | 0..1 bounce factor
|
|
252
|
-
| `
|
|
256
|
+
| `restitution` | `0` | 0..1 bounce factor. Read off **both** sides of the contact and mixed as `max(mover, solid)`, the way Box2D does it — so a springy floor bounces riders that are not themselves bouncy. Solids default to 0, so a scene that never sets it on a surface behaves exactly as it did before this existed. A bounce whose closing speed would come out under **40 px/s** is damped to a dead stop instead, so a settling body grounds rather than buzzing |
|
|
257
|
+
| `solidMode` | `'block'` | On the **solid**. `'block'` is an immovable wall (what every solid was). `'contain'` is an inward circular boundary — matched circles are kept *inside* its circumference (drums, bowls, lottery cages). `'push'` makes it a body in its own right: a matched circle and this one split the separation and exchange the closing velocity at equal mass. `'contain'` and `'push'` are **circle-on-circle only**; anything else, and any unknown string, falls back to `'block'`. Since 2026-08-27 |
|
|
258
|
+
| `oneWay` | `false` | On the **solid**: pass-through except for landings on its top edge — classic platformer floors. Honoured by `'block'` solids only |
|
|
253
259
|
| `carryRiders` | `true` | On the **solid**: a moving solid carries whoever stands on it (velocity, tweens, idle wobble — wrap teleports excluded). Set `false` for world-scroll terrain that moves while the player is meant to stay put |
|
|
254
260
|
|
|
261
|
+
#### Which resolver runs, and what each pair produces
|
|
262
|
+
|
|
263
|
+
The **mover's** hitbox shape picks the resolver; the **solid's** shape and `solidMode` decide the contact normal inside it. Both sides matter, and the pairing is not symmetric.
|
|
264
|
+
|
|
265
|
+
| Mover | Solid | Resolution |
|
|
266
|
+
| --- | --- | --- |
|
|
267
|
+
| `rect` | `rect` | Axis of least penetration — the original platformer path |
|
|
268
|
+
| `rect` or `rotatedRect` | `rotatedRect` (or the mover is one) | Separating axes over the four box axes; the smallest overlap is the way out, so a crate slides down a tilted ramp instead of standing on an invisible ledge |
|
|
269
|
+
| `circle` | `rect` | Closest point on the AABB → contact normal (corner bounces) |
|
|
270
|
+
| `circle` | `rotatedRect` | The circle is taken into the box's frame, resolved there, and the normal rotated back out |
|
|
271
|
+
| `circle` | `circle` | Centre to centre — a round post deflects instead of showing faces and corners it does not have |
|
|
272
|
+
| `circle` | `circle` + `solidMode: 'contain'` | The mover is kept **inside** `R - r`, analytically. No ring of wall sprites, so no seams to squeeze through |
|
|
273
|
+
| `circle` + `push` | `circle` + `push`, mutually listed | Resolved **once**, before every other pass: the separation is split in half and the closing velocity exchanged at equal mass |
|
|
274
|
+
|
|
275
|
+
Two consequences that cost an afternoon each if you meet them by surprise:
|
|
276
|
+
|
|
277
|
+
- **`solidMode` is only read when the mover is a circle.** A `rect` mover runs the axis/SAT path, which never looks at it — so a square ball inside a `'contain'` drum is pushed *out* of the boundary, not held in, and a `rect` against a `'push'` body shoves it like a wall. Give both sides `hitboxShape: 'circle'`.
|
|
278
|
+
- **`'push'` is the only mode that needs agreement from both sprites.** Each must be `solidMode: 'push'`, each must carry a circle hitbox, and **each must list the other's `collisionGroup` in its own `solidWith`**. Miss any one of those and the pair silently degrades to the ordinary one-sided resolver: one body shoves an immovable one, which looks like a bug in the momentum and is really a missing group. `'contain'` is one-directional by design — only the ball lists the drum.
|
|
279
|
+
|
|
255
280
|
### Collision
|
|
256
281
|
|
|
257
282
|
| Property | Default | Notes |
|
|
@@ -260,8 +285,8 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
|
|
|
260
285
|
| `collidesWith` | — | Array of groups this sprite reports overlaps with |
|
|
261
286
|
| `hitboxScale` | `1` | Shrinks the collision box **around the anchor** — which is what makes `anchor` the other half of the tuning: `anchor: 'bottom'` with `hitboxScaleY: '55%'` covers the lower half without lifting the box off the floor. Also takes `'80%'` |
|
|
262
287
|
| `hitboxScaleX`, `hitboxScaleY` | `1` | Per-axis corrections **multiplied on top of** `hitboxScale`, for art that fills its frame by a different fraction on each axis. A 20×44 drawing in a 32×48 frame needs `0.62` wide and `0.92` tall — no single `hitboxScale` describes it. Ignored by circle hitboxes. Since 2026-08-23 |
|
|
263
|
-
| `hitboxShape` | `'rect'` | `'circle'` — radius = half the smaller drawn side × `hitboxScale` (the per-axis scales are skipped: a circle has no axes). Circles resolve against solids along the contact normal (corner bounces) and get a round touch area |
|
|
264
|
-
| `swept` | `false` | Test this sprite's movement as a **path** (swept AABB), not just at the end position, so a fast mover cannot tunnel between frames. Applies to both `collidesWith` events and `solidWith` blocking (the sprite is clamped to the impact point, then resolved by the normal static pass).
|
|
288
|
+
| `hitboxShape` | `'rect'` | `'circle'` — radius = half the smaller drawn side × `hitboxScale` (the per-axis scales are skipped: a circle has no axes). Circles resolve against solids along the contact normal (corner bounces) and get a round touch area. `'rotatedRect'` keeps the collision rect **turned with the sprite** (an OBB) instead of re-boxing it square to the screen: a plain `'rect'` at 45° is re-boxed around its turned corners and comes out 41% wider with a flat top that is not there. Only matters once `rotation` is non-zero, and it does honour `hitboxScaleX`/`hitboxScaleY`, unlike a circle. Covers solids, `collidesWith` overlap, `raycast`, the swept pass and the debug overlay. Since 2026-08-27 |
|
|
289
|
+
| `swept` | `false` | Test this sprite's movement as a **path** (swept AABB), not just at the end position, so a fast mover cannot tunnel between frames. Applies to both `collidesWith` events and `solidWith` blocking (the sprite is clamped to the impact point, then resolved by the normal static pass). Two **circle** hitboxes now sweep as circles (the Minkowski sum of two circles is a circle of radius r1+r2); a circle against a rectangular or turned solid sweeps as a box, and against a turned one the box is grown into a *rounded* rect so the diagonal does not poke out by 1.41r. Set it on the *mover* — the bullet, not the wall. The sweep skips solids that are not `solidMode: 'block'` |
|
|
265
290
|
| `debug` | `false` | Draw this sprite's shapes: green = collision AABB, blue = touch bounds, orange dot = anchor |
|
|
266
291
|
|
|
267
292
|
### Car (`carMode`)
|
|
@@ -345,7 +370,7 @@ The rest of the contract, all read from the engine:
|
|
|
345
370
|
- **A drag outranks the attachment — for position only.** While a finger holds the sprite (`draggable: true`, past the touch slop) the pinning is skipped, exactly as it is for a rider on a moving platform, and resumes when the finger lifts. The inherited opacity keeps applying throughout.
|
|
346
371
|
- **Chains work**: attach a hat to a tag attached to a hero. Parents resolve first through recursion, with a depth cap of 8 that breaks accidental cycles — beyond eight links the far ancestor is not re-resolved that frame and the chain lags instead of hanging. Attaching a sprite to itself is ignored.
|
|
347
372
|
- **Cross-space attach converts automatically.** A `screenFixed` sprite attached to a world sprite (or the reverse) has the target's position mapped through the camera first, so the offset stays in the sprite's own units.
|
|
348
|
-
- **Removing the target removes everything attached to it**, recursively — a tag never outlives its owner, and a chain goes with it. `detach()` first to keep one alive. `removeAllSprites()` clears every attachment as it empties the
|
|
373
|
+
- **Removing the target removes everything attached to it**, recursively — a tag never outlives its owner, and a chain goes with it. `detach()` first to keep one alive. `removeAllSprites()` clears every attachment as it empties the Sprite/Text list; emitters, ropes, and tile layers remain.
|
|
349
374
|
- **A non-sprite target detaches** instead of crashing (Android logs a warning; iOS is silent). Like `follow()`, the binding takes an untyped argument on purpose — a typed proxy parameter aborts the app on the JNI type check when JS sends a plain object.
|
|
350
375
|
|
|
351
376
|
### Sprite events
|
|
@@ -365,7 +390,7 @@ The rest of the contract, all read from the engine:
|
|
|
365
390
|
| `pathcomplete` | `x`, `y` | A non-looping `followPath` run reached the end |
|
|
366
391
|
| `collision` | `group`, `other`, `x`, `y` | Overlap with a `collidesWith` group began |
|
|
367
392
|
| `collisionend` | `group`, `other`, `x`, `y` | That overlap ended: the shapes separated, **or** the partner was removed from the scene, hidden (`visible = false`) or stopped matching the group filter |
|
|
368
|
-
| `land` | `x`, `y`, `other`, `group` | Landed on top of a `solidWith` solid. `other` is the solid
|
|
393
|
+
| `land` | `x`, `y`, optional `other`, `group` | Landed on top of a `solidWith` solid. `other` is the solid sprite when one exists; TileLayer cells have no proxy and omit it |
|
|
369
394
|
|
|
370
395
|
## Font
|
|
371
396
|
|
|
@@ -513,13 +538,39 @@ A native Verlet chain: integration and distance constraints run in the game loop
|
|
|
513
538
|
|
|
514
539
|
The tether yields at the end no finger owns: with a fixed head anchor the tail sprite is simply leashed, but with sprites on both ends you can drag either one and the other is towed once the rope goes taut.
|
|
515
540
|
|
|
541
|
+
## TileLayer
|
|
542
|
+
|
|
543
|
+
One native grid replaces a sprite per tile. Only cells inside the camera are drawn, collision checks visit cells under the mover, and fully solid cells can feed `findPath`. Add/remove a layer through the GameView like other scene objects. It draws below sprites at the same `zIndex`.
|
|
544
|
+
|
|
545
|
+
| Group | Members | Notes |
|
|
546
|
+
| --- | --- | --- |
|
|
547
|
+
| Grid input | `data` | Nested numeric rows, string rows decoded through `legend`, or flat row-major ids sized by `cols`/`rows`. Live on both platforms |
|
|
548
|
+
| Creation-time grid configuration | `legend`, `firstGid`, `cols`, `rows` | Set in `createTileLayer()` for portability. `cols`/`rows` read back from the parsed grid; Android has no live setters for the four configuration inputs |
|
|
549
|
+
| Derived size | `width`, `height` | Read-only world dimensions |
|
|
550
|
+
| Placement | `x`, `y`, `tileWidth`, `tileHeight`, `zIndex`, `scrollFactor` | Tile sizes default to the sheet frame dimensions |
|
|
551
|
+
| Look | `sheet`, `visible`, `opacity`, `tintColor` | `opacity` and `scrollFactor` accept percentage strings |
|
|
552
|
+
| Collision | `collisionGroup`, `solid`, `oneWay`, `restitution`, `debug` | `solid`/`oneWay` accept tile ids or legend characters. `restitution` accepts a percentage |
|
|
553
|
+
|
|
554
|
+
Tiled data uses `firstGid`: gid 0 becomes empty when an offset is active, and flip bits are stripped before the frame id is calculated. The flip transform itself is not rendered.
|
|
555
|
+
|
|
556
|
+
| Method | Result |
|
|
557
|
+
| --- | --- |
|
|
558
|
+
| `getTile(col, row)` | Frame id, `-1` when empty or outside |
|
|
559
|
+
| `setTile(col, row, id)` | Replaces art and recalculates its flag from `solid`/`oneWay` |
|
|
560
|
+
| `isBlocked(col, row)` | Fully solid status; one-way cells answer `false` |
|
|
561
|
+
| `setBlocked(col, row, bool)` | Per-cell full-solid override without changing art |
|
|
562
|
+
| `tileAt(x, y)` | World point → `{ col, row, tile, solid, x, y }`, or `null` outside |
|
|
563
|
+
| `cellAt(col, row)` | Cell → the same structure; `x`/`y` are the center |
|
|
564
|
+
|
|
565
|
+
Movers list the layer's `collisionGroup` in `solidWith`. Rect, circle, swept, restitution, grounding, and `land` are supported, but the tile cell is not a proxy: a tile landing has no `other` sprite. Tile cells do not produce `collidesWith` trigger events and are not included in `raycast()` yet. Full workflow, Tiled input, live edits, and bounded A* are in [tilemaps.md](tilemaps.md).
|
|
566
|
+
|
|
516
567
|
## Events at a glance
|
|
517
568
|
|
|
518
569
|
Only these events exist. Nothing fires per frame, and events are only fired if a listener is registered.
|
|
519
570
|
|
|
520
|
-
- **GameView**: `press`, `tap`, `release`, `resize`, `timer`
|
|
571
|
+
- **GameView**: `press`, `tap`, `release`, `resize`, `timer`, `performance`
|
|
521
572
|
- **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `pathcomplete`, `collision`, `collisionend`, `land`
|
|
522
|
-
- **SpriteSheet, Font, Sound, Emitter, Rope**: none
|
|
573
|
+
- **SpriteSheet, Font, Sound, Emitter, Rope, TileLayer**: none
|
|
523
574
|
|
|
524
575
|
There is `collision` (enter) and `collisionend` (exit), but deliberately **no** per-frame "stay" event — that would be bridge traffic every frame. Hold the in-between state in JS: you heard the enter, you will hear the end.
|
|
525
576
|
|
|
@@ -536,6 +587,17 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
536
587
|
- **`collision` fires once per overlap-enter**, re-arming only after the shapes separate. A sprite resting inside a trigger does not re-fire — `collisionend` tells you when it left.
|
|
537
588
|
- **Pooling a sprite with `visible = false` fires `collisionend`** on whoever was touching it. That is usually what you want (a plate whose ball was despawned closes its door), but a handler that assumes separation means "the player walked away" will misfire.
|
|
538
589
|
- **The built-in pixel font only covers ASCII 32..126.** Em dashes, accents and `ñ` render as blanks or garbage — the module's own demos rewrote `—` as `-` for this reason. Ship a BMFont atlas (`tools/genfont.py` rasterizes any TTF) when the game needs accented Spanish text.
|
|
590
|
+
- **A bouncy floor switches `onGround` off.** In every resolver the grounding and the `land` event live in the branch taken when the bounce is *damped away*; a contact whose rebound would exceed 40 px/s reflects and grounds nothing. That was always true of a bouncy rider, but `restitution` now mixes as `max(mover, solid)`, so giving a *floor* `restitution: 0.5` silently stops every rider that lands hard on it from ever reporting `onGround` — and a jump gated on `onGround` stops working on that one platform.
|
|
591
|
+
- **A two-sided `restitution` cannot be opted out of.** The mix is `max`, not a product or an average, so the springier surface always wins: a deliberately dead `restitution: 0` crate still bounces off a floor set to `0.5`. If one rider must not bounce, the bounce has to live on the riders, not on the surface.
|
|
592
|
+
- **A resting body is allowed to sink half a pixel — in some resolvers.** Separation is skipped when the overlap is under 0.5 px, because shoving a gravity-loaded body back out sixty times a second is what makes a settled pile tremble; the closing velocity is still cancelled, so the sink cannot grow past it. It applies to the circle path, the turned-rect path and `'push'` pairs. Plain rect-against-rect still separates in full. Do not derive an exact contact position from `y` after a landing.
|
|
593
|
+
- **`linearDamping` is not surface friction.** Nothing checks whether the sprite is touching anything: it bleeds speed on both axes every frame, in the air as much as on the felt, and along a slope as much as across it. It is right for a pool table and wrong for a hill, which is why upstream still lists contact-only friction as unbuilt. A projectile that must keep its arc wants `0`.
|
|
594
|
+
- **`linearDamping` kills a slow deliberate drift.** Under a combined 4 px/s the engine zeroes *both* axes outright, since a proportion never reaches zero on its own. A conveyor nudging something along at 3 px/s stops dead the moment that sprite also carries any damping.
|
|
595
|
+
- **A swept sprite is not contained and not pushed.** The sweep only considers solids in `solidMode: 'block'` — a `'contain'` boundary would stop the ball against its *outside*, and a `'push'` body is meant to move. So `swept: true` does not stop a fast enough ball from crossing a drum wall between frames; only the static pass contains it. Keep the speeds inside a container below its wall thickness per frame.
|
|
596
|
+
- **`'push'` pairs never ground and never fire `land`.** The bilateral resolver moves positions and exchanges velocity and touches nothing else, so a pile of balls resting on each other all read `onGround: false` unless something underneath them is a `'block'` solid.
|
|
597
|
+
- **The `'push'` exchange has no masses, no spin and no friction.** A marble and a boulder trade velocity identically, and nothing ever starts rotating from a contact. `restitution` mixes as `max` here too, and with no mass to weight it against, that is the whole model.
|
|
598
|
+
- **`swept: true` no longer drags a resting body backwards** (since 2026-08-27). A hit at t = 0 is a contact the sprite is already in, and the static resolver owns it; the sweep used to clamp the sprite back to where the frame started, every frame, while its along-surface speed kept climbing until it broke loose looking launched. On an older build, that is what a swept sprite parked on a slope or a floor does — it is a build date, not your code.
|
|
599
|
+
- **The hitbox shapes are exclusive, and an unknown name is `'rect'`.** Setting `hitboxShape` clears whichever of the other two was set, so switching a sprite between `'circle'` and `'rotatedRect'` at runtime is safe, and a typo lands on the default rather than throwing. Reading the property back is therefore also the one clean way to tell whether a build understands `'rotatedRect'` at all.
|
|
600
|
+
- **`'rotatedRect'` costs something and only earns it once `rotation` is non-zero.** At zero degrees it produces the same box the default already produces, by a more expensive route. Turn it on for the tilted things, not for everything.
|
|
539
601
|
- **`swept` belongs on the fast mover, not on the target.** A stationary thin wall with `swept: true` changes nothing; the bullet is the sprite whose path needs testing.
|
|
540
602
|
- **A swept bullet can enter and leave in consecutive frames.** If it crosses a thin target entirely, you get `collision` and then `collisionend` almost immediately — react on the enter.
|
|
541
603
|
- **Text `width`/`height` are read-only in practice.** They report the laid-out block; to make text bigger use `scale`.
|
|
@@ -556,6 +618,7 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
556
618
|
- **`raycast` only sees `visible`, non-`screenFixed` sprites carrying a `collisionGroup`.** A hidden pooled sprite is invisible to the ray, which is usually what you want; an untagged wall is invisible too, which usually is not. HUD sprites are skipped by design, so a screen-fixed score never blocks a world-space line of sight.
|
|
557
619
|
- **`raycast` is a discrete query, not a sensor.** Calling it from a game-clock timer or a tap handler is the intended use. Calling it every frame from JS puts exactly the bridge traffic in the loop that the whole engine exists to avoid.
|
|
558
620
|
- **`findPath` walks the sprite's center, not its silhouette.** The waypoints are a line through free grid cells, so a body wider than a cell clips corners unless you pass `clearance` (roughly half the walker's width) or shrink `cellSize`. Obstacles are read from the **hitbox**, so `hitboxScale`, the per-axis scales and `hitboxShape: 'circle'` shape the walkable space too.
|
|
621
|
+
- **`findPath` sees fully solid TileLayer cells, not one-way cells.** One-way platforms answer non-solid to the path grid so a top-down route may cross them. If that is not correct for the game's navigation rules, maintain a separate fully solid navigation layer or cell overrides.
|
|
559
622
|
- **`findPath` returning `null` is not always "no route".** Degenerate `bounds`, `cellSize <= 0` and a grid over ~1M cells all return `null` as well — a `bounds` rect of the whole world at `cellSize: 4` fails silently that way. Check the numbers before blaming the maze.
|
|
560
623
|
- **A path of one or two points is the straight-line case, not a bug.** With nothing in the way `findPath` returns just `[start, goal]`; `path.length < 2` is the guard the demos use to detect a tap outside `bounds`.
|
|
561
624
|
- **Game-clock timers are not `setTimeout`.** `after`/`every` freeze at `timeScale: 0` and pause with the render loop — which is why spawn waves belong there — but a pause menu that needs a real countdown still needs `setTimeout`.
|
|
@@ -570,5 +633,7 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
570
633
|
- **A faded owner stops its tags taking taps.** The inherited opacity goes through the same hit test as a sprite's own, so a tag on an owner at `opacity: 0` is untouchable even though its own `opacity` still says 1 — the pooling idiom of hiding an owner now silently disarms its attached buttons too.
|
|
571
634
|
- **`animate()` takes a percentage only on `scale`.** Inside the tween options `scaleX`, `scaleY` and `opacity` are read as plain numbers on both platforms, so `animate({ opacity: '50%' })` does not do what the property setter would. Percentages are for property writes; keep tween targets numeric.
|
|
572
635
|
- **A bad ratio or anchor name fails differently per platform.** Android keeps the value the property already held; iOS resets it to the property's default. Neither throws, both log a warning, and at creation time they agree — the split only shows on a write to a property already set to something else.
|
|
636
|
+
- **Four TileLayer inputs are creation-time for portable code.** iOS rebuilds after live writes to `legend`, `firstGid`, `cols`, and `rows`; Android exposes only creation-dict handling (plus `cols`/`rows` getters). Put all four in `createTileLayer()` and replace `data` for a live rebuild.
|
|
637
|
+
- **`removeAllSprites()` does not clear every scene list.** It removes Sprite/Text objects and resets their attachments; emitters, ropes, and TileLayers remain until individually removed.
|
|
573
638
|
- **`anchor` beats `anchorX`/`anchorY` in the same `createSprite` call**, whatever the key order in the object, because the engine applies the preset after the two axes. Pass one or the other, not both.
|
|
574
639
|
- **`scrollFactor` does not move the sprite.** It shifts where the sprite is *drawn* and maps touch back to match. `x`, `y`, physics and collisions stay in world coordinates, so a `scrollFactor: 0.5` platform still collides where its world `x` says, not where it looks.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Debugging and performance telemetry
|
|
2
|
+
|
|
3
|
+
Verified against `ti.game@d587081`: both `GameViewProxy` implementations, `DebugHud`, `FrameStats`, both scene renderers, `example/camera.js`, `example/particles.js`, and `example/tilemap.js`.
|
|
4
|
+
|
|
5
|
+
## The two debug aids
|
|
6
|
+
|
|
7
|
+
`GameView.debug` accepts a legacy boolean or an object:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const gameView = Game.createGameView({
|
|
11
|
+
debug: {
|
|
12
|
+
hitbox: true,
|
|
13
|
+
hud: 'topRight'
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Form | Meaning |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `debug: true` | Global collision shapes only; shorthand for `{ hitbox: true }` |
|
|
21
|
+
| `{ hitbox: true }` | Global sprite and tile-cell collision visualization |
|
|
22
|
+
| `{ hud: true }` | Performance HUD in `topLeft` |
|
|
23
|
+
| `{ hud: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' }` | HUD in an explicit corner |
|
|
24
|
+
| `{ hud: true, hudFont: font }` | HUD drawn with a `Game.createFont()` bitmap font |
|
|
25
|
+
|
|
26
|
+
Writing the object replaces both modes: omit `hitbox` and it becomes false; omit `hud` and the HUD turns off. Reading `gameView.debug` returns `{ hitbox: boolean, hud: false | corner }`; `hudFont` is configuration, not part of the readback. Sprite-level `sprite.debug` remains a boolean.
|
|
27
|
+
|
|
28
|
+
The HUD is screen-space and is drawn after the camera effect. Camera scroll, zoom, shake, and glitch do not move or distort it. It starts compact (`FPS`, `MS`, `DC`); tapping it expands/collapses the panel, and the panel consumes its own touches.
|
|
29
|
+
|
|
30
|
+
## Hitbox colors and tuning
|
|
31
|
+
|
|
32
|
+
- Green: collision shape after `hitboxScale`, `hitboxScaleX`, and `hitboxScaleY`.
|
|
33
|
+
- Blue: full sprite/touch bounds.
|
|
34
|
+
- Orange: anchor.
|
|
35
|
+
- Tile debug: outlines fully solid cells in view.
|
|
36
|
+
|
|
37
|
+
Tune geometry in this order:
|
|
38
|
+
|
|
39
|
+
1. Put the correct shape on both mover and solid (`rect`, `circle`, or `rotatedRect`).
|
|
40
|
+
2. Set the anchor to the semantic contact point, often `anchor: 'bottom'` for feet.
|
|
41
|
+
3. Apply the global hitbox scale.
|
|
42
|
+
4. Correct one axis only when the frame padding differs by axis.
|
|
43
|
+
|
|
44
|
+
The scale properties affect collision, not the touch rectangle. `hitboxShape: 'circle'` does change the touch shape.
|
|
45
|
+
|
|
46
|
+
## Performance HUD fields
|
|
47
|
+
|
|
48
|
+
| HUD | Event key | Meaning |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `FPS` | `fps` | Frames presented in the closed one-second window |
|
|
51
|
+
| `MS` | `averageCpuMs` | Average engine CPU time per frame; the `maxFps` wait is excluded |
|
|
52
|
+
| `P95` | `p95CpuMs` | 95th percentile CPU frame time |
|
|
53
|
+
| `MAX` | `maxCpuMs` | Worst CPU frame in the window |
|
|
54
|
+
| `DROP` | `droppedFrames` | Presentation intervals longer than one target refresh |
|
|
55
|
+
| `SPRITES` | `visibleSprites` / `sprites` | Drawn sprites / sprites in the scene |
|
|
56
|
+
| `EMITTERS` | `emitters` | Emitters in the scene |
|
|
57
|
+
| `PARTICLES` | `particles` | Live particles |
|
|
58
|
+
| `DRAWCALLS` | `drawCalls` | Scene draw calls; HUD draw calls excluded |
|
|
59
|
+
| `TEXSWITCH` | `textureSwitches` | Texture switches |
|
|
60
|
+
| `UPDATE` | `averageUpdateMs` | Physics, animation, tween, and scene update time |
|
|
61
|
+
| `TEXTURE` | `averageTexturePrepareMs` | Texture preparation/upload time |
|
|
62
|
+
| `BATCH` | `averageBatchMs` | Batching and drawing time |
|
|
63
|
+
| `PRESENT` | `averagePresentMs` | iOS only: buffer presentation time |
|
|
64
|
+
| `PRESENTFAIL` | `presentFailures` | iOS only: failed presentations |
|
|
65
|
+
|
|
66
|
+
The event also includes `surfaceWidth` and `surfaceHeight`. Android omits `averagePresentMs` and `presentFailures`; do not interpret missing keys as zero.
|
|
67
|
+
|
|
68
|
+
## Programmatic telemetry
|
|
69
|
+
|
|
70
|
+
The `performance` event fires at most once per second and only while a listener exists:
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
function onPerformance({ fps, averageCpuMs, drawCalls, textureSwitches }) {
|
|
74
|
+
Ti.API.info(`${fps} fps; ${averageCpuMs.toFixed(1)} ms; ${drawCalls} calls; ${textureSwitches} switches`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
gameView.addEventListener('performance', onPerformance);
|
|
78
|
+
|
|
79
|
+
function cleanup() {
|
|
80
|
+
gameView.removeEventListener('performance', onPerformance);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Telemetry is opt-in inside the renderers. With the HUD disabled and no `performance` listener, frame-stat clock reads are disabled. Use the HUD while developing; use the event for short diagnostic sessions or a deliberate in-app performance screen, not permanent per-frame business logic.
|
|
85
|
+
|
|
86
|
+
## Reading the numbers
|
|
87
|
+
|
|
88
|
+
- High draw calls with stable sprite count: inspect texture order, blend changes, glow/flash, ropes, skid marks, and debug overlays.
|
|
89
|
+
- High texture switches: pack related art into fewer sheets and group sprites using the same sheet/blend into coherent `zIndex` bands.
|
|
90
|
+
- High update time: narrow collision groups and reduce active colliders/particles; hidden sprites are excluded from render and collision.
|
|
91
|
+
- A large `TileLayer` should keep draw calls and visible work stable while the camera moves; one sprite per tile scales with the whole scene instead.
|
|
92
|
+
- `maxFps: 60` can prevent a 120 Hz display from doubling work when the game does not need it.
|
|
93
|
+
- Judge final performance on hardware. The iOS Simulator's translated OpenGL path is intentionally rendered at 1× and remains slower than a device.
|
|
94
|
+
|
|
95
|
+
## Common diagnostic order
|
|
96
|
+
|
|
97
|
+
1. Confirm the object was added and is visible/in bounds.
|
|
98
|
+
2. Turn on the relevant sprite/layer debug shape, then the global overlay if relationships are unclear.
|
|
99
|
+
3. Verify `collisionGroup` is on the target and `solidWith`/`collidesWith` on the mover.
|
|
100
|
+
4. Inspect HUD counts and draw calls before changing code.
|
|
101
|
+
5. Compare the smallest relevant upstream demo from [examples.md](examples.md).
|
|
102
|
+
6. Disable instrumentation after the issue is understood.
|
|
103
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Upstream example catalog
|
|
2
|
+
|
|
3
|
+
Verified against all 33 self-contained demos under `ti.game@d587081:example/` plus the launcher `example/app.js`.
|
|
4
|
+
|
|
5
|
+
The demos prove that API combinations run together on the current module. They are **not** production templates: most keep state in one file, repeat launcher/back-button plumbing, and still use pre-ES6 syntax. Start from the closest demo, retain the demonstrated engine calls, then apply the focused structure and cleanup patterns in this skill.
|
|
6
|
+
|
|
7
|
+
| Demo | What it proves | Production-oriented reference |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `basic.js` | Sheets, frame animation, drag/pinch/rotate, tween completion | [Scaffolding](patterns.md#the-scaffolding) |
|
|
10
|
+
| `puzzle.js` | Multi-touch drag, press-to-lift, snapping and tween-back | [Cards and board games](recipes-adventure-effects.md#cards-and-board-games) |
|
|
11
|
+
| `flappy.js` | Gravity impulse, invisible score/death zones, wrapping parallax | [Tap-to-flap](recipes-action.md#tap-to-flap) |
|
|
12
|
+
| `platformer.js` | `solidWith`, `land`, `oneWay`, carried rider, camera follow, multi-touch controls | [Platformer](recipes-action.md#platformer) |
|
|
13
|
+
| `volley.js` | Restitution ball, `land`, collision response, coarse AI timer | [Ball sports](recipes-action.md#ball-sports--breakout-physics) |
|
|
14
|
+
| `racing.js` | `carMode`, drift, skid marks, checkpoints and lap state | [Top-down racer](recipes-action.md#top-down-racer) |
|
|
15
|
+
| `cards.js` | Dealing, fanned layout, selection tweens and idle wobble | [Cards and board games](recipes-adventure-effects.md#cards-and-board-games) |
|
|
16
|
+
| `asteroids.js` | Thrust, angular velocity, wrapping, pooling, additive bolts, effect/music audio | [Space shooter](recipes-action.md#asteroids--space-shooter) |
|
|
17
|
+
| `topdown.js` | Small string map made from sprites, `ySort`, eight-way input, decision timer | [Top-down / Zelda](recipes-action.md#top-down--zelda) |
|
|
18
|
+
| `skate.js` | Endless-runner pooling, carried/uncarried terrain, parallax, emitter and audio hooks | [Endless runner](recipes-action.md#endless-runner) |
|
|
19
|
+
| `pointclick.js` | Tap-to-walk with `findPath` → `followPath`, hotspots and `ySort` | [Point-and-click](recipes-adventure-effects.md#point--click-adventure) |
|
|
20
|
+
| `particles.js` | Continuous/burst emitters, sprite target, HUD telemetry | [Particles](recipes-adventure-effects.md#particles) and [debugging](debugging-performance.md) |
|
|
21
|
+
| `rhythm.js` | Pooled notes, native velocity, `press` pads, timing and audio feedback | [Rhythm game](recipes-action.md#rhythm-game) |
|
|
22
|
+
| `camera.js` | Two-axis follow, bounds, zoom, shake, camera effects, parallax and debug HUD | [Camera work](recipes-adventure-effects.md#camera-work) |
|
|
23
|
+
| `rope.js` | Rope pinned to a sprite, fixed head, tail weight | [Ropes and chains](recipes-adventure-effects.md#ropes-and-chains) |
|
|
24
|
+
| `flip.js` | Draw-only flips driven by direction and inverted gravity | [Visual effects](recipes-adventure-effects.md#visual-effects) |
|
|
25
|
+
| `hitbox.js` | Global/sprite debug overlays and per-axis hitbox correction | [Seeing the hitbox](patterns.md#seeing-the-hitbox--hitboxjs) |
|
|
26
|
+
| `blend.js` | Normal/add/multiply/screen modes and `flash()` | [Visual effects](recipes-adventure-effects.md#visual-effects) |
|
|
27
|
+
| `text.js` | Screen-fixed HUD, world labels, text buttons, word wrap and `attachTo` | [HUD](patterns.md#hud--textjs-flappyjs) |
|
|
28
|
+
| `swept.js` | Identical fast bullets with and without continuous sweep | [Object pooling](patterns.md#object-pooling) and [API gotchas](api.md#gotchas-the-property-tables-do-not-show) |
|
|
29
|
+
| `path.js` | Looped/smoothed paths, one-shot paths and animation chains | [Patrol routes](patterns.md#patrol-routes-and-animation-chains--pathjs) |
|
|
30
|
+
| `raycast.js` | Line of sight, ledge probes and tap-fired hitscan | [Raycast pattern](patterns.md#line-of-sight-ledge-probes-and-hitscan--raycastjs) |
|
|
31
|
+
| `zones.js` | `collision`/`collisionend`, pressure plate and removal during contact | [Zones](patterns.md#zones-you-can-be-inside-of-enter--exit--zonesjs) |
|
|
32
|
+
| `demoscene.js` | Follow-path circles, per-character scroller, glow, blend and music | [Visual effects](recipes-adventure-effects.md#visual-effects) |
|
|
33
|
+
| `maze.js` | Raw/simplified A*, re-pathing AI and path visualization | [Pathfinding](patterns.md#pathfinding-around-obstacles--mazejs-pointclickjs) |
|
|
34
|
+
| `timescale.js` | Slow/frozen scene, engine timer versus real-time interval | [Game-clock timers](patterns.md#timers-on-the-game-clock) |
|
|
35
|
+
| `circles.js` | Rect/circle/rotated-rect solids and `block` versus bilateral `push` | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
|
|
36
|
+
| `slopes.js` | Rotated-rect ramp normals and restitution supplied by a surface | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
|
|
37
|
+
| `plinko.js` | Circular pegs, swept balls and trigger slots | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
|
|
38
|
+
| `pool.js` | Bilateral circle momentum, rails, pockets and linear damping | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
|
|
39
|
+
| `drum.js` | Analytic circular containment and contact-driven impulses | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
|
|
40
|
+
| `wind.js` | `gravityX` as wind and as top-down constant acceleration | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
|
|
41
|
+
| `tilemap.js` | One 120×90 `TileLayer`, visible-cell rendering, cell collision, live edits, A* and HUD | [Tile maps](tilemaps.md) |
|
|
42
|
+
|
|
43
|
+
## Choosing the right map example
|
|
44
|
+
|
|
45
|
+
`topdown.js` deliberately creates one sprite per tile for a small 16×12 scene; that remains reasonable when individual tiles need sprite behavior. `tilemap.js` is the reference for large or mostly static grids: one native `TileLayer` avoids ticking, sorting, drawing, and collision-scanning off-screen tile sprites.
|
|
46
|
+
|
|
47
|
+
## Reading a demo safely
|
|
48
|
+
|
|
49
|
+
1. Identify the engine-owned behavior being demonstrated.
|
|
50
|
+
2. Copy the smallest connected API sequence, not the entire file.
|
|
51
|
+
3. Replace `var` and function-expression callbacks with the project's current ES6+ style.
|
|
52
|
+
4. Keep resources outside `resize`; create surface-sized scene objects inside one guarded `resize` path.
|
|
53
|
+
5. Separate reusable state from launcher UI and add explicit cleanup for app-owned listeners, timers, and sounds.
|
|
54
|
+
|