@maccesar/titools 4.15.0 → 4.16.1
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 +10 -1
- package/package.json +1 -1
- package/skills/ti-game/SKILL.md +90 -197
- package/skills/ti-game/references/api.md +52 -15
- package/skills/ti-game/references/debugging-performance.md +102 -0
- package/skills/ti-game/references/examples.md +53 -0
- package/skills/ti-game/references/patterns.md +699 -0
- package/skills/ti-game/references/project-setup.md +19 -6
- package/skills/ti-game/references/recipes-action.md +653 -0
- package/skills/ti-game/references/recipes-adventure-effects.md +340 -0
- package/skills/ti-game/references/roadmap.md +45 -65
- package/skills/ti-game/references/tilemaps.md +127 -0
- package/skills/ti-game/references/recipes.md +0 -1679
|
@@ -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 `c216e7f`, 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, native `TileLayer`, and wall-contact API landed afterward without another lasting 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,6 +62,7 @@ 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
|
|
|
@@ -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(
|
|
@@ -250,6 +253,8 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
|
|
|
250
253
|
| --- | --- | --- |
|
|
251
254
|
| `solidWith` | — | Array of group names that block this sprite. The engine pushes it out along the axis of least penetration |
|
|
252
255
|
| `onGround` | `false` | Read-only. Gate jumps on it |
|
|
256
|
+
| `onWallLeft`, `onWallRight` | `false` | Read-only. True while the mover is pressed against that side of a matching solid this frame. Rect side push-out counts; circle/rotated-rect contacts count when their normal is mostly horizontal. Sprite and TileLayer walls are supported |
|
|
257
|
+
| `wallSlideSpeed` | `0` | Maximum downward velocity in px/s while either wall flag is true. `0` disables the cap. Upward velocity and slower falls are unchanged |
|
|
253
258
|
| `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 |
|
|
254
259
|
| `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 |
|
|
255
260
|
| `oneWay` | `false` | On the **solid**: pass-through except for landings on its top edge — classic platformer floors. Honoured by `'block'` solids only |
|
|
@@ -367,7 +372,7 @@ The rest of the contract, all read from the engine:
|
|
|
367
372
|
- **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.
|
|
368
373
|
- **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.
|
|
369
374
|
- **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.
|
|
370
|
-
- **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
|
|
375
|
+
- **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.
|
|
371
376
|
- **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.
|
|
372
377
|
|
|
373
378
|
### Sprite events
|
|
@@ -387,7 +392,8 @@ The rest of the contract, all read from the engine:
|
|
|
387
392
|
| `pathcomplete` | `x`, `y` | A non-looping `followPath` run reached the end |
|
|
388
393
|
| `collision` | `group`, `other`, `x`, `y` | Overlap with a `collidesWith` group began |
|
|
389
394
|
| `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 |
|
|
390
|
-
| `land` | `x`, `y`, `other`, `group` | Landed on top of a `solidWith` solid. `other`
|
|
395
|
+
| `land` | `x`, `y`, optional `other`, optional `group` | Landed on top of a `solidWith` solid. `other` and `group` identify the solid sprite when one exists; TileLayer cells have no proxy and omit both |
|
|
396
|
+
| `wallhit` | `side`, `x`, `y`, optional `other`, optional `group` | Began pressing a matching wall or switched sides. `side` is `'left'` or `'right'`. TileLayer cells omit both `other` and `group`; there is no `wallend` event, so read the flags in the jump handler |
|
|
391
397
|
|
|
392
398
|
## Font
|
|
393
399
|
|
|
@@ -535,13 +541,39 @@ A native Verlet chain: integration and distance constraints run in the game loop
|
|
|
535
541
|
|
|
536
542
|
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.
|
|
537
543
|
|
|
544
|
+
## TileLayer
|
|
545
|
+
|
|
546
|
+
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`.
|
|
547
|
+
|
|
548
|
+
| Group | Members | Notes |
|
|
549
|
+
| --- | --- | --- |
|
|
550
|
+
| Grid input | `data` | Nested numeric rows, string rows decoded through `legend`, or flat row-major ids sized by `cols`/`rows`. Live on both platforms |
|
|
551
|
+
| 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 |
|
|
552
|
+
| Derived size | `width`, `height` | Read-only world dimensions |
|
|
553
|
+
| Placement | `x`, `y`, `tileWidth`, `tileHeight`, `zIndex`, `scrollFactor` | Tile sizes default to the sheet frame dimensions |
|
|
554
|
+
| Look | `sheet`, `visible`, `opacity`, `tintColor` | `opacity` and `scrollFactor` accept percentage strings |
|
|
555
|
+
| Collision | `collisionGroup`, `solid`, `oneWay`, `restitution`, `debug` | `solid`/`oneWay` accept tile ids or legend characters. `restitution` accepts a percentage |
|
|
556
|
+
|
|
557
|
+
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.
|
|
558
|
+
|
|
559
|
+
| Method | Result |
|
|
560
|
+
| --- | --- |
|
|
561
|
+
| `getTile(col, row)` | Frame id, `-1` when empty or outside |
|
|
562
|
+
| `setTile(col, row, id)` | Replaces art and recalculates its flag from `solid`/`oneWay` |
|
|
563
|
+
| `isBlocked(col, row)` | Fully solid status; one-way cells answer `false` |
|
|
564
|
+
| `setBlocked(col, row, bool)` | Per-cell full-solid override without changing art |
|
|
565
|
+
| `tileAt(x, y)` | World point → `{ col, row, tile, solid, x, y }`, or `null` outside |
|
|
566
|
+
| `cellAt(col, row)` | Cell → the same structure; `x`/`y` are the center |
|
|
567
|
+
|
|
568
|
+
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).
|
|
569
|
+
|
|
538
570
|
## Events at a glance
|
|
539
571
|
|
|
540
572
|
Only these events exist. Nothing fires per frame, and events are only fired if a listener is registered.
|
|
541
573
|
|
|
542
|
-
- **GameView**: `press`, `tap`, `release`, `resize`, `timer`
|
|
543
|
-
- **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `pathcomplete`, `collision`, `collisionend`, `land`
|
|
544
|
-
- **SpriteSheet, Font, Sound, Emitter, Rope**: none
|
|
574
|
+
- **GameView**: `press`, `tap`, `release`, `resize`, `timer`, `performance`
|
|
575
|
+
- **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `pathcomplete`, `collision`, `collisionend`, `land`, `wallhit`
|
|
576
|
+
- **SpriteSheet, Font, Sound, Emitter, Rope, TileLayer**: none
|
|
545
577
|
|
|
546
578
|
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.
|
|
547
579
|
|
|
@@ -564,7 +596,7 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
564
596
|
- **`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`.
|
|
565
597
|
- **`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.
|
|
566
598
|
- **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.
|
|
567
|
-
- **`'push'` pairs never ground
|
|
599
|
+
- **`'push'` pairs never publish ground or wall contact.** The bilateral resolver moves positions and exchanges velocity but does not update `onGround`, `onWallLeft` / `onWallRight`, `land`, or `wallhit`. A pile of balls resting on each other reports contact only with any separate `'block'` surface underneath it.
|
|
568
600
|
- **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.
|
|
569
601
|
- **`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.
|
|
570
602
|
- **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.
|
|
@@ -589,6 +621,7 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
589
621
|
- **`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.
|
|
590
622
|
- **`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.
|
|
591
623
|
- **`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.
|
|
624
|
+
- **`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.
|
|
592
625
|
- **`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.
|
|
593
626
|
- **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`.
|
|
594
627
|
- **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`.
|
|
@@ -603,5 +636,9 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
603
636
|
- **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.
|
|
604
637
|
- **`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.
|
|
605
638
|
- **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.
|
|
639
|
+
- **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.
|
|
640
|
+
- **Android string maps need `c216e7f` or newer.** Earlier Android builds rejected the ordinary nested JS object behind `legend` because they required a `KrollDict`; string rows then decoded as empty cells. The current factory uses `getKrollDict()` and matches iOS. Numeric nested/flat data was unaffected.
|
|
641
|
+
- **Wall contact is state, not a sticky event.** `onWallLeft` / `onWallRight` reset when resolution no longer pushes the mover sideways. `wallhit` fires only on entering a wall or switching sides; use it for sound/particles, but read the flags at the instant the player requests a wall jump. A wall slide only caps positive `velocityY`; it never slows an upward wall jump.
|
|
642
|
+
- **`removeAllSprites()` does not clear every scene list.** It removes Sprite/Text objects and resets their attachments; emitters, ropes, and TileLayers remain until individually removed.
|
|
606
643
|
- **`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.
|
|
607
644
|
- **`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,102 @@
|
|
|
1
|
+
# Debugging and performance telemetry
|
|
2
|
+
|
|
3
|
+
Verified against `ti.game@c216e7f`: 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.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Upstream example catalog
|
|
2
|
+
|
|
3
|
+
Verified against all 33 self-contained demos under `ti.game@c216e7f:example/` plus the launcher `example/app.js`. Wall contacts and `wallSlideSpeed` are not represented by a demo at this commit; use [the platformer recipe](recipes-action.md#platformer), which is verified against both native implementations and the README contract.
|
|
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.
|