@maccesar/titools 4.8.0 → 4.9.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 CHANGED
@@ -177,7 +177,7 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
177
177
  | ti-expert | Architecture and implementation | Starting point for most tasks |
178
178
  | purgetss | Utility-first styling | UI styling and animations |
179
179
  | ti-ui | UI/UX patterns | Complex layouts, ListViews, platform UI |
180
- | ti-game | 2D games with the `ti.game` module | Sprites, physics, collisions, text HUDs, particles, camera |
180
+ | ti-game | 2D games with the `ti.game` module | Sprites, physics, collisions, raycasts, paths, text HUDs, particles, camera |
181
181
  | ti-api | Complete Titanium API reference | Looking up properties, methods, events |
182
182
  | ti-guides | SDK fundamentals | tiapp.xml, Hyperloop, distribution, JDK/Xcode compatibility, release notes |
183
183
  | ti-howtos | Native feature integration | Location, push, media, platform APIs |
@@ -436,8 +436,11 @@ When it activates:
436
436
  - Sprites, sprite sheets, TexturePacker atlases, frame animations, native tweens
437
437
  - Physics: gravity and velocity, solid platforms, one-way floors, bouncing, the arcade car model, Newtonian thrust
438
438
  - Collision groups, invisible trigger zones, enter/exit events, swept AABB for fast bullets, hitbox tuning
439
+ - Raycasts for line of sight, ledge probes and hitscan shots
440
+ - Native patrol paths (`followPath`) and animation chaining (`play(name, { then })`)
439
441
  - Bitmap-font text sprites and screen-fixed HUDs inside the scene
440
- - Particle emitters, Verlet ropes, camera follow/zoom/shake/effects, low-latency sound
442
+ - Particle emitters, Verlet ropes, camera follow/zoom/shake/effects, `scrollFactor` parallax, low-latency sound
443
+ - Game-clock timers that obey pause and slow motion
441
444
  - Drag & drop, pinch, rotate, multitouch controls
442
445
 
443
446
  Example prompts:
@@ -448,6 +451,7 @@ Example prompts:
448
451
  "How do I make a top-down racer that drifts?"
449
452
  "Why does my game stutter when I move sprites from setInterval?"
450
453
  "Add a score HUD that stays put while the camera follows the player."
454
+ "Make the guard walk a patrol route and turn around before the ledge."
451
455
  ```
452
456
 
453
457
  Key rules:
@@ -457,6 +461,7 @@ Key rules:
457
461
  - A sprite with size but no sheet is an invisible trigger (scores, goals, walls)
458
462
  - Add a whole level with one `gameView.add([...])` call
459
463
  - HUDs are `Game.createText` sprites with `screenFixed: true`, not `Ti.UI.Label` overlays
464
+ - Timers that drive game logic belong on the game clock (`gameView.every`), so a paused game stops spawning
460
465
 
461
466
  ---
462
467
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "4.8.0",
3
+ "version": "4.9.0",
4
4
  "description": "Titanium SDK skills and agents for AI coding assistants (Claude Code, Gemini CLI, Codex CLI)",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: ti-game
3
- description: "Use when building or reviewing 2D games with the ti.game module for Titanium SDK (Android and iOS) — sprites, sprite sheets, frame animations, tweens, physics (gravity, solid platforms, carMode drifting, thrust), collision groups, trigger zones, swept AABB, bitmap-font text and HUDs, particles, Verlet ropes, camera follow/zoom/shake, low-latency sound, drag & drop, multitouch. AUTO-DETECT: if tiapp.xml declares ti.game or any file calls require('ti.game'), invoke BEFORE writing ANY game code. Also trigger on: game, juego, sprite, spritesheet, platformer, endless runner, top-down, shooter, racer, flappy, breakout, rhythm game, card game, puzzle, collision, hitbox, parallax, particles, HUD, score, game loop, 60 fps, GameView. ti.game is NOT Phaser, Godot or Box2D: you never write a game loop, never move a sprite from setInterval, and several familiar features (tilemap layer, sprite parenting, raycast, joystick) do not exist yet — check references/roadmap.md before inventing an API."
3
+ description: "Use when building or reviewing 2D games with the ti.game module for Titanium SDK (Android and iOS) — sprites, sprite sheets, animations, tweens, physics (gravity, solid platforms, carMode drifting, thrust), collision groups, trigger zones, swept AABB, raycast, paths, bitmap-font text, particles, Verlet ropes, camera follow/zoom/shake, parallax, sound, drag & drop, multitouch. AUTO-DETECT: if tiapp.xml declares ti.game or any file calls require('ti.game'), invoke BEFORE writing ANY game code. Also trigger on: game, juego, sprite, spritesheet, platformer, endless runner, top-down, shooter, racer, flappy, breakout, rhythm, cards, puzzle, collision, hitbox, raycast, line of sight, patrol, parallax, particles, HUD, score, game loop, 60 fps, GameView. ti.game is NOT Phaser, Godot or Box2D: you never write a game loop, never move a sprite from setInterval, and several familiar features (tilemap layer, sprite parenting, joystick, A* pathfinding) do not exist yet — check references/roadmap.md before inventing an API."
4
4
  argument-hint: "[genre or feature]"
5
5
  allowed-tools: Read, Grep, Glob, Edit, Write, Bash(node *)
6
6
  ---
7
7
 
8
8
  # ti.game — 2D game engine for Titanium SDK
9
9
 
10
- `ti.game` is a native OpenGL ES 2.0 sprite engine exposed to JavaScript, written and maintained by **Michael Gangolf (m1ga)** — upstream is https://github.com/m1ga/ti.game. This skill tracks upstream `main` as of **2026-08-18** — the manifest still reads `0.3.0`, but the text engine, `screenFixed`, `swept` collisions and `collisionend` landed after it, so a version number alone will not tell you what a checkout has. When upstream and any fork disagree, upstream wins. Same JS API on Android and iOS — the iOS side is a class-per-class Obj-C twin of the Android engine.
10
+ `ti.game` is a native OpenGL ES 2.0 sprite engine exposed to JavaScript, written and maintained by **Michael Gangolf (m1ga)** — upstream is https://github.com/m1ga/ti.game. This skill tracks upstream `main` as of **2026-08-19** — the manifest still reads `0.3.0`, but the text engine, `screenFixed`, `swept` collisions, `collisionend`, `followPath`, animation chaining, `raycast`, game-clock timers, `scrollFactor` parallax and the `multiply`/`screen` blend modes all landed after it, so a version number alone will not tell you what a checkout has. When upstream and any fork disagree, upstream wins. Same JS API on Android and iOS — the iOS side is a class-per-class Obj-C twin of the Android engine.
11
11
 
12
12
  Works in **both Classic and Alloy** projects. Nothing here depends on PurgeTSS.
13
13
 
@@ -29,11 +29,16 @@ The practical consequence, and the single biggest mistake to avoid:
29
29
  | A car that drives and drifts | your own physics | `carMode: true` + `throttle`/`steering` |
30
30
  | A ship with inertia | vector math per frame | `thrust` + `angularVelocity` |
31
31
  | Drag a sprite with the finger | `touchmove` handler | `draggable: true` |
32
- | Scrolling background | timer repositioning copies | `wrapX` / `wrapShift` + `velocityX` |
32
+ | Scrolling background under a fixed camera | timer repositioning copies | `wrapX` / `wrapShift` + `velocityX` |
33
+ | Parallax layers under a moving camera | hand-scrolled layers | `scrollFactor` on each layer |
33
34
  | Camera that follows the player | timer writing `cameraX` | `gameView.follow(sprite, options)` |
34
35
  | A score, a title, a label | `Ti.UI.Label` overlays | `Game.createText({ text, screenFixed: true })` |
35
36
  | A bullet fast enough to skip past a thin wall | shrinking the time step | `swept: true` on the bullet |
36
37
  | To know the player *left* a zone | polling on a timer | the `collisionend` event |
38
+ | An enemy on a fixed patrol route | chained `animate()` legs | `sprite.followPath(points, { loop })` |
39
+ | Play an attack, then go back to idle | an `animationcomplete` handler | `play('attack', { then: 'idle' })` |
40
+ | Line of sight, a ledge probe, a hitscan shot | an invisible probe sprite | `gameView.raycast(x0, y0, x1, y1, ['group'])` |
41
+ | A spawn wave or an AI tick that respects pause | `setInterval` | `gameView.every(ms, cb)` |
37
42
  | Smoke, sparks, explosions | pooled sprites in JS | `Game.createEmitter(...)` |
38
43
  | A swinging chain or cape | verlet solver in JS | `Game.createRope(...)` |
39
44
  | Slow motion or a pause that still draws | pausing your own timers | `gameView.timeScale = 0.5` / `0` |
@@ -128,6 +133,8 @@ The enter/exit pair is the trigger lifecycle other engines call `OnTriggerEnter`
128
133
 
129
134
  A fast mover can slip through a thin target between frames: it never overlaps on any single frame, so the discrete test misses. `swept: true` on the *mover* tests its movement as a path — `collision` fires for whatever the path crossed, and `solidWith` walls stop it at the impact point instead of letting it teleport through.
130
135
 
136
+ When the question is not "did we touch?" but "what is over there?", the answer is `gameView.raycast(x0, y0, x1, y1, ['group'])` — line of sight, a ledge probe ahead of a walker's feet, a hitscan shot. It returns `null` or the nearest hit (`{ x, y, distance, group, sprite, normal }`), needs nothing but a `collisionGroup` on the targets, and replaces the old trick of parking an invisible probe sprite where the ray would land. It is a discrete query: call it from a decision timer or a tap handler, never every frame.
137
+
131
138
  **A sprite with `width`/`height` but no `sheet` renders nothing and works as an invisible trigger.** Score zones, goals, checkpoints, ceilings, kill floors and walls are all built this way in the official demos. This is the idiomatic way to add logic to a scene without art.
132
139
 
133
140
  Tune fairness with `hitboxScale` (art rarely fills its frame; slightly small hitboxes feel better) and `hitboxShape: 'circle'` for balls and rocks — circles also resolve against solids along the contact normal, so they bounce off corners diagonally instead of like a box. Turn on `debug: true` per sprite, or on the GameView for everything, to see green collision AABBs, blue touch bounds and the orange anchor dot.
@@ -135,7 +142,7 @@ Tune fairness with `hitboxScale` (art rarely fills its frame; slightly small hit
135
142
  ## Performance rules that actually matter
136
143
 
137
144
  - **One texture = one draw call.** The batcher accumulates up to 1000 quads and flushes on texture switch. A scene whose sprites all share one sheet renders in a single draw call regardless of sprite count. Pack art into as few sheets as possible; reuse one white particle texture and tint it at runtime.
138
- - **Know what else breaks the batch**, since none of it is visible on screen: every `'normal'` ↔ `'add'` transition in draw order flushes (group additive sprites into their own `zIndex` band so it happens twice a frame, not constantly); each glowing sprite costs 2 extra draw calls per frame, plus 2 more while a `flash()` runs; ropes, skid marks and debug overlays always draw with normal blending, so interleaving them with additive content flushes too.
145
+ - **Know what else breaks the batch**, since none of it is visible on screen: every blend-mode transition in draw order flushes — there are four (`'normal'`, `'add'`, `'multiply'`, `'screen'`), so group same-blend sprites into their own `zIndex` band and the frame switches a handful of times instead of constantly; each glowing sprite costs 2 extra draw calls per frame, plus 2 more while a `flash()` runs; ropes, skid marks and debug overlays always draw with normal blending, so interleaving them with non-normal content flushes too.
139
146
  - **Bulk-add a level.** `gameView.add([spriteA, spriteB, emitter, rope])` crosses the bridge once and commits under one scene lock. Collect a level's objects in an array and add them in one call; `add(object)` still works for single objects.
140
147
  - **Keep collision group lists targeted.** Cost is O(colliders × candidates) per frame. A bullet checking `['asteroid']` tests 5 sprites, not the whole scene.
141
148
  - **Pool, don't create.** Bullets, notes, obstacles and enemies are created once (usually 3–10 of them), then recycled by setting `visible = false` and zeroing velocity. Creating sprites mid-game costs a bridge hop and a scene lock.
@@ -161,7 +168,9 @@ Standard Titanium touch events do not reach the GameView, but ordinary Titanium
161
168
 
162
169
  ## Clean up what you created
163
170
 
164
- The render loop follows the activity lifecycle automatically, but **your timers and sounds do not**. Every demo that starts an interval or plays looping music stops it on window close. In Alloy, wire this into the controller's `cleanup()`.
171
+ The render loop follows the activity lifecycle automatically, but **your JS timers and sounds do not**. Every demo that starts an interval or plays looping music stops it on window close. In Alloy, wire this into the controller's `cleanup()`.
172
+
173
+ Game-clock timers (`gameView.after` / `every`) are the exception that proves the rule: they run on the engine's clock, so they freeze at `timeScale: 0` and pause with the render loop instead of firing into a dead scene. That is why spawn waves and AI ticks belong there. A `setInterval` keeps running through a pause, which is right for a real-time countdown and wrong for everything else.
165
174
 
166
175
  ```javascript
167
176
  win.addEventListener('close', () => {
@@ -180,7 +189,7 @@ Read the one that matches the task instead of guessing — the API surface below
180
189
  | Need | Reference |
181
190
  | --- | --- |
182
191
  | Exact properties, methods, events, defaults for every object | [api.md](references/api.md) |
183
- | Working code per genre: flappy, platformer, top-down, racer, asteroids, endless runner, rhythm, cards, point & click, particles, ropes, camera | [recipes.md](references/recipes.md) |
192
+ | Working code per genre: flappy, platformer, top-down, racer, asteroids, endless runner, rhythm, cards, point & click, particles, ropes, camera — plus patrol paths, raycast probes, parallax and game-clock timers | [recipes.md](references/recipes.md) |
184
193
  | Install the module, tiapp.xml, Alloy vs Classic wiring, assets, sprite sheets, atlases, lifecycle, testing | [project-setup.md](references/project-setup.md) |
185
194
  | What does NOT exist yet, what is coming, and how to work around it today | [roadmap.md](references/roadmap.md) |
186
195
 
@@ -195,4 +204,5 @@ Read the one that matches the task instead of guessing — the API surface below
195
204
  7. Is every `createSound`, `setInterval` and `setTimeout` stopped when the window closes?
196
205
  8. Is the HUD `Game.createText` with `screenFixed: true`, rather than a stack of overlaid labels?
197
206
  9. Does anything travel far enough per frame to tunnel? Give it `swept: true`.
198
- 10. Are you using an API that actually exists? Anything involving tilemaps, sprite parenting, raycasting, joysticks or `blend: 'multiply'` — check [roadmap.md](references/roadmap.md) first.
207
+ 10. Does a timer drive game logic (spawn waves, AI ticks, respawns)? Move it to `gameView.every`/`after` so it obeys pause and slow motion.
208
+ 11. Are you using an API that actually exists? Anything involving tilemaps, sprite parenting, joysticks, A* pathfinding or word-wrapped text — check [roadmap.md](references/roadmap.md) first.
@@ -1,6 +1,6 @@
1
1
  # ti.game API reference
2
2
 
3
- Complete JS surface of `ti.game` as of upstream `main` on 2026-08-18, verified against the module source (`android/src/ti/game/*.java` and `engine/*.java`, mirrored by `ios/Classes/TG*`). Defaults come from the engine fields, not from prose. The manifest still reads `0.3.0`; the text engine (`createFont`/`createText`), `screenFixed`, `swept` and `collisionend` landed after it and are not in any numbered release yet.
3
+ Complete JS surface of `ti.game` as of upstream `main` on 2026-08-19, verified against the module source (`android/src/ti/game/*.java` and `engine/*.java`, mirrored by `ios/Classes/TG*`). Defaults come from the engine fields, not from prose. The manifest still reads `0.3.0`; the text engine (`createFont`/`createText`), `screenFixed`, `swept`, `collisionend`, `followPath`, animation chaining, `raycast`, the game-clock timers, `scrollFactor` and the `multiply`/`screen` blend modes all landed after it and are not in any numbered release yet.
4
4
 
5
5
  Every property listed is **live**: reading returns the current native value, even mid-drag or mid-tween. Every property can also be passed to its `create*` factory. All durations crossing the JS boundary are **milliseconds** (the engine converts to seconds internally).
6
6
 
@@ -57,6 +57,10 @@ A normal Titanium view — add it to a window or any container, size it with the
57
57
  | `follow(sprite, options)` | method | — | Native dead-zone follow, see below |
58
58
  | `stopFollow()` | method | — | Stop following; the camera stays where it is |
59
59
  | `shake({ strength, duration })` | method | `12` px, `400` ms | Detuned-sine rumble on the projection only — follow, bounds and touch mapping are unaffected |
60
+ | `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 |
61
+ | `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 }` |
62
+ | `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 |
63
+ | `cancelTimer(id)` | method | — | Cancels a timer from `after()` / `every()` |
60
64
  | `cameraEffect` | string | `'none'` | Fullscreen shader pass: `'none'`, `'tint'`, `'glitch'`. With `'none'` the extra pass is skipped entirely |
61
65
  | `cameraTint` | string | — | Color for the `'tint'` effect, e.g. `'#4f8'` (night vision, poison, flashback) |
62
66
  | `cameraEffectIntensity` | float | `1` | 0..1 — tint mix or glitch amount |
@@ -64,6 +68,8 @@ A normal Titanium view — add it to a window or any container, size it with the
64
68
 
65
69
  Events: `press`, `tap`, `release` — fired for **every** touch anywhere on the view, with payload `x`, `y` in scene coordinates (tap-anywhere controls, flappy-style). And `resize` with payload `width`, `height` — the real surface size in pixels.
66
70
 
71
+ 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.
72
+
67
73
  ### `follow(sprite, options)`
68
74
 
69
75
  | Option | Default | Effect |
@@ -88,7 +94,7 @@ Create one sheet per texture and share it across every sprite that uses it — s
88
94
  | `image` | — | Path to the texture, relative to the resources root (`'assets/hero.png'`) |
89
95
  | `frameWidth` / `frameHeight` | — | Grid mode: cell size in source pixels. Frames are numbered left-to-right, top-to-bottom, starting at 0 |
90
96
  | `atlas` | — | TexturePacker JSON (hash or array format) instead of a grid |
91
- | `smoothing` | `true` | `false` = GL_NEAREST, the pixel-art setting |
97
+ | `smoothing` | `true` | `false` = GL_NEAREST, the pixel-art setting. On a **grid** sheet, `true` also insets the interior frame UVs by half a texel so magnified edges cannot sample the neighbouring frame; outer edges keep the exact 0..1 range so `tileRepeat` still wraps. Atlas frames come from the JSON untouched |
92
98
  | `repeat` | `false` | GL_REPEAT wrap, required by sprites using `tileRepeat`. Needs power-of-two texture dimensions |
93
99
 
94
100
  | Member | Notes |
@@ -117,6 +123,7 @@ Textures upload to the GPU lazily on first use, from the render thread, and are
117
123
  | `ySort` | `false` | Within the same `zIndex`, sort by the sprite's **bottom edge** — top-down depth (walk behind a tree, in front of it below) |
118
124
  | `flipX`, `flipY` | `false` | Mirror the drawn frame only. Position, anchor, physics and hit testing are untouched |
119
125
  | `screenFixed` | `false` | `x`/`y` become **surface** coordinates and the sprite ignores camera position, zoom and shake — HUDs, on-screen buttons, overlays. Touch is mapped back automatically, so `tap` still works. Works on any sprite, not only text |
126
+ | `scrollFactor` | `1` | Parallax: how much camera travel (and shake) moves this sprite. `0.5` = a background layer at half speed, `1.5` = a foreground layer overtaking the camera, `0` = pinned to the view but **still zooming** with `cameraScale` (unlike `screenFixed`). Rendering and touch mapping only — `x`/`y`, physics and collisions stay in plain world coordinates. Ignored when `screenFixed` is true |
120
127
  | `pixelSnap` | `false` | Rounds only the rendered anchor to a framebuffer pixel after camera position and zoom. Physics and live `x`/`y` stay subpixel floats. Combine with `smoothing: false` when a moving pixel-art sprite must keep a stable texel phase |
121
128
 
122
129
  ### Sheet and animation
@@ -214,19 +221,20 @@ Every sprite gets its own phase, and the wobble unwinds exactly when disabled. T
214
221
  | `glowColor` | — | Tinted, blurred silhouette drawn behind the sprite by a shader pass |
215
222
  | `glowBlur` | `0` | Blur radius in px; `0` = off. An active glow switches to the silhouette shader and back: **2 extra draw calls per glowing sprite per frame** (2 more while a `flash()` runs), even on a shared texture. Fine for a few highlights, not for every coin in the level |
216
223
  | `glowOpacity` | `1` | Halo strength 0..1 — tweenable via `animate`, so a glow can fade in without touching the blur |
217
- | `blend` | `'normal'` | `'add'` brightens the backdrop instead of covering it (glows, fire, lasers). Costs one batch flush per mode change — group additive sprites by `zIndex` |
224
+ | `blend` | `'normal'` | `'add'` brightens the backdrop (glows, fire, lasers), `'multiply'` darkens it (contact shadows, stains, grime), `'screen'` lightens softly without blowing out to white (fog, god rays, soft light). Costs one batch flush per mode change — group same-blend sprites by `zIndex`. Unknown strings fall back to `'normal'` silently, and the names are case-sensitive |
218
225
 
219
226
  ### Methods
220
227
 
221
228
  | Method | Notes |
222
229
  | --- | --- |
223
- | `play(name)` | Start a named animation |
224
- | `stop()` | Stop the animation on the current frame |
230
+ | `play(name, options)` | Start a named animation; returns `false` for an unknown name. `options.then` (a name or an array of names) chains natively — each queued animation starts as the previous non-looping one finishes, and `animationcomplete` still fires per step. A looping animation ends the chain |
231
+ | `stop()` | Stop the animation on the current frame; also drops any queued `then` chain |
232
+ | `followPath(points, options)` | Walk the sprite along `points` — `{ x, y }` objects or `[x, y]` pairs, **at least two**, fewer is ignored with a warning. `options`: `speed` px/s (default `100`), `loop` (closed circuit), `rotate` (face along the path, 0 = up), `smoothing` (corner radius in px, precomputed once). Fires `pathcomplete` at the end of a non-looping run and clears itself. `followPath(null)` stops in place |
225
233
  | `animate(options)` | Native tween, fires `complete` |
226
234
  | `clearTweens()` | Cancel tweens in progress — call before starting a replacement tween |
227
235
  | `flash(color, duration)` | Fills the sprite's silhouette with `color` (default white) and fades it out over `duration` ms (default 150). The classic damage/invincibility flash that a multiplicative `tintColor` cannot do |
228
236
 
229
- `animate()` accepts exactly these keys — anything else is silently ignored: `x`, `y`, `scale`, `scaleX`, `scaleY`, `rotation`, `opacity`, `glowOpacity`, `duration` (ms), `delay` (ms), `easing`, `frame` (the sheet frame to set once the tween finishes). Chain moves by re-calling `animate` from the `complete` handler — that is how patrols and ping-pong motions are built.
237
+ `animate()` accepts exactly these keys — anything else is silently ignored: `x`, `y`, `scale`, `scaleX`, `scaleY`, `rotation`, `opacity`, `glowOpacity`, `duration` (ms), `delay` (ms), `easing`, `frame` (the sheet frame to set once the tween finishes). Re-calling `animate` from the `complete` handler is still how a ping-pong or a blink is built (there is no `repeat`/`yoyo` yet), but a fixed route is now `followPath` instead of a chain of tween legs.
230
238
 
231
239
  ### Sprite events
232
240
 
@@ -240,8 +248,9 @@ Every sprite gets its own phase, and the wobble unwinds exactly when disabled. T
240
248
  | `dragend` | `x`, `y` | Finger lifted; the sprite has already moved natively |
241
249
  | `pinch` | `scaleX`, `scaleY` | While two-finger scaling |
242
250
  | `rotate` | `rotation` | While two-finger rotating |
243
- | `animationcomplete` | `animation` | A non-looping sheet animation finished |
251
+ | `animationcomplete` | `animation` | A non-looping sheet animation finished — including each finished step of a `then` chain |
244
252
  | `complete` | final transform values | A tween finished |
253
+ | `pathcomplete` | `x`, `y` | A non-looping `followPath` run reached the end |
245
254
  | `collision` | `group`, `other`, `x`, `y` | Overlap with a `collidesWith` group began |
246
255
  | `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 |
247
256
  | `land` | `x`, `y`, `other`, `group` | Landed on top of a `solidWith` solid. `other` is the solid — read it to react to what you landed on (trampolines, hazards) |
@@ -349,7 +358,7 @@ Add and remove like a sprite: `gameView.add(emitter)` / `remove(emitter)`. Spawn
349
358
  | `startScale` / `endScale` | `1` / `1` | Interpolated over `lifetime` |
350
359
  | `startOpacity` / `endOpacity` | `1` / `0` | Interpolated over `lifetime` |
351
360
  | `tint` | white | Tint white particle art at runtime — one tiny texture covers every color in the game |
352
- | `blend` | `'normal'` | `'add'` for fire, sparks and magic |
361
+ | `blend` | `'normal'` | `'add'` for fire, sparks and magic; `'multiply'` for smoke and dust that darken what they cross; `'screen'` for soft haze |
353
362
  | `maxParticles` | `200` | Pool size, hard cap 1000 |
354
363
 
355
364
  Methods: `emit(n)` fires a one-shot burst on top of `rate`; `clear()` kills all live particles.
@@ -381,8 +390,8 @@ The tether yields at the end no finger owns: with a fixed head anchor the tail s
381
390
 
382
391
  Only these events exist. Nothing fires per frame, and events are only fired if a listener is registered.
383
392
 
384
- - **GameView**: `press`, `tap`, `release`, `resize`
385
- - **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `collision`, `collisionend`, `land`
393
+ - **GameView**: `press`, `tap`, `release`, `resize`, `timer`
394
+ - **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `pathcomplete`, `collision`, `collisionend`, `land`
386
395
  - **SpriteSheet, Font, Sound, Emitter, Rope**: none
387
396
 
388
397
  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.
@@ -392,7 +401,7 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
392
401
  - **`maxSpeed` caps `thrust` and `carMode` only — not plain velocity.** The clamp runs inside the thrust integration and inside the car model, so a sprite given `velocityX = 3000` travels at 3000 no matter what `maxSpeed` says. A ship accelerating on `thrust`, on the other hand, stops gaining speed at 500 px/s unless you raise its own `maxSpeed`, which is the cap people actually hit.
393
402
  - **`sprite.animation` survives `stop()`.** Comparing it to decide whether to restart an animation gives a false negative. The top-down demo tracks a `walking` boolean itself.
394
403
  - **`follow()` resets its options.** See the GameView section.
395
- - **`animate()` ignores unknown keys.** Tweening `width`, `height` or `tintColor` silently does nothing — those are instant writes only.
404
+ - **`animate()` ignores unknown keys.** Tweening `width`, `height` or `tintColor` silently does nothing — those are instant writes only. There is no `repeat` or `yoyo` either; re-launch from `complete`.
396
405
  - **`visible: false` removes a sprite from collision too.** That is why pooled bullets and rocks use it instead of removing them from the scene.
397
406
  - **`scaleX: -1` versus `flipX: true`.** Negative scale flips the physics and hit-test shape with the art; `flipX` mirrors only the drawing. Use `flipX` unless you specifically want the former.
398
407
  - **Tween positions are absolute.** A sprite with `idleAnimation` on will land off-target; disable the wobble before the tween and re-enable it on `complete`.
@@ -404,5 +413,15 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
404
413
  - **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.
405
414
  - **Text `width`/`height` are read-only in practice.** They report the laid-out block; to make text bigger use `scale`.
406
415
  - **`hitboxShape: 'circle'` also changes the touch area**, not just collisions.
407
- - **`blend: 'add'` costs a batch flush per mode change.** Group additive sprites on their own `zIndex` band.
416
+ - **Every blend-mode change costs a batch flush.** Group same-blend sprites on their own `zIndex` band; alternating modes sprite by sprite degrades toward one draw call each.
408
417
  - **`glowColor` alone draws nothing.** The glow pass only runs when `glowBlur > 0` *and* `glowOpacity > 0`; both default in a way that makes `glowColor` on its own a no-op (`glowBlur` is `0`). Always set the blur radius with the color.
418
+ - **`followPath` overrides position absolutely.** The path is applied after velocity and gravity have been integrated and writes `x`/`y` outright, so a path-driven sprite ignores its own physics for placement. Do not expect `velocityX` and a path to add up. A tween on `x`/`y` runs *after* the path in the same frame and wins outright — pick one.
419
+ - **A path needs at least two points.** One point (or a non-array) clears the path and logs a warning instead of parking the sprite there.
420
+ - **Path movement still carries riders.** It feeds the frame delta like a tweened or velocity-driven solid, so a platform on a `followPath` circuit moves whatever stands on it.
421
+ - **`raycast` groups: pass an array, not varargs.** Android accepts both `raycast(x0, y0, x1, y1, ['enemy'])` and loose arguments; iOS reads only the array. Write the array and it works on both.
422
+ - **`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.
423
+ - **`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.
424
+ - **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`.
425
+ - **`screenFixed` beats `scrollFactor`.** Setting both leaves the sprite screen-fixed; the parallax offset is skipped entirely.
426
+ - **Ghost lines along a frame's edge are a filtering artifact, not bad art.** A magnified `smoothing: true` sheet samples past the frame border — 1px seams, or the next row's heads at the bottom. Grid sheets have inset UVs upstream since 2026-08-19; an atlas needs padding and extrude at pack time, and an older module build needs either `smoothing: false` or a rebuild.
427
+ - **`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.
@@ -41,7 +41,7 @@ Then declare it in `tiapp.xml`. The iOS platform key is `iphone`, not `ios`:
41
41
 
42
42
  Pinning `version` is worth the extra attribute: with two module versions installed side by side, an unpinned entry silently picks the highest one.
43
43
 
44
- The version number lies right now: the manifest has read `0.3.0` through several feature landings. A zip built before 2026-08-18 has no `createText`, no `createFont`, no `screenFixed`, no `swept` and no `collisionend` while still calling itself 0.3.0. If a call that this skill documents is `undefined` at runtime, rebuild the module from upstream `main` rather than hunting for a typo.
44
+ The version number lies right now: the manifest has read `0.3.0` through several feature landings. A zip built before 2026-08-18 has no `createText`, no `createFont`, no `screenFixed`, no `swept` and no `collisionend`; one built before 2026-08-19 also lacks `followPath`, `play(name, { then })`, `raycast`, `after`/`every`, `scrollFactor` and the `multiply`/`screen` blend modes — all while still calling itself 0.3.0. A zip built before 2026-08-19 16:00 UTC+2 also bleeds at grid-frame edges on smoothed sheets. If a call that this skill documents is `undefined` at runtime, rebuild the module from upstream `main` rather than hunting for a typo.
45
45
 
46
46
  Building the module from source:
47
47
 
@@ -53,7 +53,7 @@ ti build -p ios --build-only # macOS only → ios/dist/ti.game-iphone-<ve
53
53
 
54
54
  ## Classic projects
55
55
 
56
- `require('ti.game')` anywhere and add the GameView to a window. The 21 demos in the module's `example/` folder are Classic, each one a CommonJS module exporting a start function:
56
+ `require('ti.game')` anywhere and add the GameView to a window. The 24 demos in the module's `example/` folder are Classic, each one a CommonJS module exporting a start function:
57
57
 
58
58
  ```javascript
59
59
  // Resources/flappy.js
@@ -218,6 +218,8 @@ Practical rules:
218
218
 
219
219
  - **One sheet per scene if you can manage it.** Sprites sharing a texture batch into a single draw call. The endless-runner demo even draws its crash burst from the *player's* sheet to keep the whole scene on one texture.
220
220
  - **`smoothing: false` for pixel art.** Combine with `pixelSnap: true` on moving sprites when the texel phase must stay stable.
221
+ - **Ship sheets as PNG, not JPG.** JPEG has no alpha channel, so every frame comes back on an opaque block, and its compression smears colour across frame borders — exactly where a sheet can least afford it. The module's own `basic.js` demo was converted from `hero.jpg` to `hero.png` on 2026-08-19 for this reason.
222
+ - **Edge bleeding on a smoothed grid is fixed upstream, on grids only.** Linear filtering samples past a frame's edge when the sprite is magnified, which shows up as 1px ghost lines or the next row's heads peeking in at the bottom. Since 2026-08-19 a grid sheet with `smoothing: true` pulls its *interior* frame edges in by half a texel automatically; outer edges stay at the texture border, so full-texture `tileRepeat` frames still wrap seamlessly, and `smoothing: false` sheets skip the inset entirely (NEAREST cannot bleed, and pixel art at 1:1 needs exact UVs). **Atlas sheets are not covered** — their UVs come straight from the JSON, so prevent bleeding when packing instead: 2px padding and extrude in TexturePacker.
221
223
  - **`repeat: true` for tiling.** Required by sprites using `tileRepeat`, and it needs power-of-two texture dimensions on ES 2.0 — a 512×64 street strip tiles, a 500×60 one does not.
222
224
  - **White art plus `tint`** covers every color variant of a particle or effect with one small texture.
223
225
 
@@ -1,13 +1,13 @@
1
1
  # ti.game recipes by genre
2
2
 
3
- Working patterns distilled from the 21 official demos in `example/` and from a shipped Alloy game (Titanium Lander). Each recipe shows the part that is specific to the genre; the shared scaffolding is in [The scaffolding](#the-scaffolding) and is not repeated.
3
+ Working patterns distilled from the 24 official demos in `example/` and from a shipped Alloy game (Titanium Lander). Each recipe shows the part that is specific to the genre; the shared scaffolding is in [The scaffolding](#the-scaffolding) and is not repeated.
4
4
 
5
- Every snippet uses only API that exists in upstream `main` as of 2026-08-18. Check [roadmap.md](roadmap.md) before reaching for anything not shown here.
5
+ Every snippet uses only API that exists in upstream `main` as of 2026-08-19. Check [roadmap.md](roadmap.md) before reaching for anything not shown here.
6
6
 
7
7
  ## Contents
8
8
 
9
9
  - [The scaffolding](#the-scaffolding)
10
- - [Cross-cutting patterns](#cross-cutting-patterns) — pooling, parallax, multitouch controls, HUD text, invisible triggers, enter/exit zones, hit-stop
10
+ - [Cross-cutting patterns](#cross-cutting-patterns) — pooling, parallax, multitouch controls, HUD text, invisible triggers, enter/exit zones, patrol routes, line of sight, game-clock timers, hit-stop
11
11
  - [Tap-to-flap](#tap-to-flap) (`flappy.js`)
12
12
  - [Platformer](#platformer) (`platformer.js`)
13
13
  - [Top-down / Zelda](#top-down--zelda) (`topdown.js`)
@@ -22,7 +22,7 @@ Every snippet uses only API that exists in upstream `main` as of 2026-08-18. Che
22
22
  - [Particles](#particles) (`particles.js`)
23
23
  - [Ropes and chains](#ropes-and-chains) (`rope.js`)
24
24
  - [Camera work](#camera-work) (`camera.js`)
25
- - [Visual effects](#visual-effects) (`blend.js`, `flip.js`, `timescale.js`)
25
+ - [Visual effects](#visual-effects) (`blend.js`, `flip.js`, `timescale.js`, `demoscene.js`)
26
26
 
27
27
  ## The scaffolding
28
28
 
@@ -139,9 +139,21 @@ The `generation` counter matters: without it, a timeout fired for an old shot de
139
139
 
140
140
  Recycling a sprite with `visible = false` fires `collisionend` on anything it was touching. Handlers that treat separation as "the player walked away" need to tolerate a despawn.
141
141
 
142
- ### Seamless parallax
142
+ ### Parallax
143
143
 
144
- Two screen-wide copies per layer, scrolled by native velocity. `wrapX`/`wrapShift` teleport a copy that left the screen back behind its sibling. No JS runs in the loop.
144
+ Two ways, and the choice is decided by whether the **camera** moves or the **world** does.
145
+
146
+ **A camera travelling through a static world** — platformer, top-down, anything using `follow()` — wants `scrollFactor`, one property per layer:
147
+
148
+ ```javascript
149
+ const clouds = Game.createSprite({ sheet: cloudSheet, x: W / 2, y: 120, zIndex: 0, scrollFactor: 0.25 });
150
+ const hills = Game.createSprite({ sheet: hillSheet, x: W / 2, y: 260, zIndex: 1, scrollFactor: 0.6 });
151
+ const sun = Game.createSprite({ sheet: sunSheet, x: 60, y: 60, zIndex: 0, scrollFactor: 0 });
152
+ ```
153
+
154
+ `0` pins the sun to the view while still zooming with `cameraScale` — that is the difference from `screenFixed`, which ignores zoom as well. Values above `1` overtake the camera for foreground grass. The sprite's `x`/`y` never change: `scrollFactor` shifts where it is *drawn* and maps touch back to match, so collisions still happen at the world coordinates you wrote.
155
+
156
+ **A world scrolling under a fixed camera** — endless runner, flappy — cannot use `scrollFactor`, because the camera never travels. Two screen-wide copies per layer, scrolled by native velocity; `wrapX`/`wrapShift` teleport a copy that left the screen back behind its sibling. No JS runs in the loop.
145
157
 
146
158
  ```javascript
147
159
  function makeLayer(sheet, y, height, speed, z) {
@@ -172,7 +184,7 @@ const skyline = makeLayer(skylineSheet, groundTop - SKY_H / 2, SKY_H, SPEED * 0.
172
184
  const street = makeLayer(streetSheet, (groundTop + H) / 2, H - groundTop, SPEED, 8);
173
185
  ```
174
186
 
175
- Slower layers read as further away. Three layers (clouds at 0.15×, hills at 0.4×, ground at 1×) is the flappy demo's recipe.
187
+ Slower layers read as further away. Three layers (clouds at 0.15×, hills at 0.4×, ground at 1×) is the flappy demo's recipe. `camera.js` demos the other half: `scrollFactor` cloud shadows at 1.35× and a `scrollFactor: 0` sun.
176
188
 
177
189
  ### Multitouch on-screen controls
178
190
 
@@ -317,6 +329,80 @@ The listener goes on the sprite that declares `collidesWith`. For a pressure pla
317
329
 
318
330
  Removing the partner from the scene, hiding it (`visible = false`) or changing its `collisionGroup` mid-contact all count as separation, so the exit still fires and the door cannot stick open when an object despawns.
319
331
 
332
+ ### Patrol routes and animation chains — `path.js`
333
+
334
+ A fixed route is `followPath`, not a chain of tween legs. The points are walked natively at `speed` px/s; `rotate: true` turns the sprite to face along the path (heading `0` = up, same convention as `rotation`), and `smoothing` rounds the corners with a radius in px.
335
+
336
+ ```javascript
337
+ guard.followPath([
338
+ { x: W * 0.22, y: H * 0.46 },
339
+ { x: W * 0.78, y: H * 0.46 },
340
+ { x: W * 0.78, y: H * 0.60 },
341
+ { x: W * 0.22, y: H * 0.60 }
342
+ ], { speed: W * 0.16, loop: true }); // sharp rectangle, walks forever
343
+
344
+ ship.followPath(diamond, {
345
+ speed: W * 0.35,
346
+ loop: true,
347
+ rotate: true, // nose follows the heading
348
+ smoothing: W * 0.12 // rounds the diamond into an oval
349
+ });
350
+ ```
351
+
352
+ A non-looping run fires `pathcomplete` and clears itself; `followPath(null)` stops in place. Two points is the minimum — one point is ignored with a warning, not treated as "go there".
353
+
354
+ The path writes `x`/`y` outright, after velocity and gravity have been integrated, so **a sprite on a path ignores its own physics for placement**. It still feeds the frame delta, though, which means a platform on a looping circuit carries whatever is standing on it — a moving platform is now a path, not a tween ping-pong.
355
+
356
+ Animation chains are native in the same spirit:
357
+
358
+ ```javascript
359
+ guard.play('hop', { then: 'walk' }); // one call, no animationcomplete handler
360
+ bird.play('flap', { then: ['glide', 'idle'] });
361
+ ```
362
+
363
+ Each queued animation starts as the previous non-looping one finishes, and a looping animation ends the chain. `animationcomplete` still fires per step if you want to hook one. `stop()` drops the queue along with the current animation.
364
+
365
+ ### Line of sight, ledge probes and hitscan — `raycast.js`
366
+
367
+ `gameView.raycast(x0, y0, x1, y1, groups)` answers "what is the first thing on this line?" with `null` or `{ x, y, distance, group, sprite, normal }`. The targets need nothing but a `collisionGroup` — no `collidesWith` on the asking side.
368
+
369
+ ```javascript
370
+ const hit = gameView.raycast(eyeX, eyeY, dog.x, dog.y, ['blocker']);
371
+ if (hit) {
372
+ aimBeam(sight, eyeX, eyeY, hit.x, hit.y, hit.distance);
373
+ sight.tintColor = '#f44'; // view blocked at the impact point
374
+ }
375
+ ```
376
+
377
+ Pass the groups as an **array**. Android also accepts loose arguments, iOS does not, so the array is the portable form. Only `visible`, non-`screenFixed` sprites carrying a `collisionGroup` are candidates — a pooled sprite parked at `visible = false` is correctly invisible to the ray, an untagged wall is invisible too (the usual mistake), and HUD sprites are skipped by design so a screen-fixed score never blocks a world-space sight line.
378
+
379
+ A ledge probe is the same call pointed straight down, one step ahead of the feet:
380
+
381
+ ```javascript
382
+ const dir = walker.velocityX >= 0 ? 1 : -1;
383
+ const probeX = walker.x + dir * W * 0.07;
384
+ if (!gameView.raycast(probeX, walker.y, probeX, walker.y + W * 0.12, ['ground'])) {
385
+ walker.velocityX = -walker.velocityX; // nothing underfoot: turn before falling
386
+ walker.flipX = walker.velocityX < 0;
387
+ }
388
+ ```
389
+
390
+ Run these from a coarse timer or a tap handler — the demo probes at 150 ms — never once per frame. A per-frame `raycast` from JS is exactly the bridge traffic the engine is built to avoid. Note that `raycast.js` drives its probes with `setInterval`: it was written two commits before game-clock timers existed, and `gameView.every` is the better home for an AI tick today.
391
+
392
+ ### Timers on the game clock
393
+
394
+ `gameView.after(ms, cb)` and `every(ms, cb)` run on the engine's clock: they stretch under `timeScale` slow motion, freeze at `0`, and pause with the render loop. Spawn waves, AI decision ticks and respawn delays belong here — a paused game that keeps spawning is the bug this fixes.
395
+
396
+ ```javascript
397
+ const wave = gameView.every(2000, () => spawnEnemy());
398
+ gameView.after(500, () => player.play('idle'));
399
+ gameView.cancelTimer(wave); // both return an int id
400
+ ```
401
+
402
+ The callback receives `{ id }`. Called without a callback, the view fires a `timer` event carrying the same id instead — pick one, not both. A repeating timer fires at most once per frame and restarts its interval after a stall rather than bursting to catch up.
403
+
404
+ What still belongs in `setTimeout`: anything that must keep running while the game is frozen. A pause menu counting down in real seconds is not on the game clock.
405
+
320
406
  ### Hit-stop on impact
321
407
 
322
408
  Freezing the scene for a few frames makes an impact read before the explosion starts. `timeScale = 0` stops the engine from advancing while it keeps drawing and accepting touches — no render loop to restart, no state to save. (Pattern from Titanium Lander: 140 ms on a crash, 90 ms when a shield absorbs a hit.)
@@ -1229,7 +1315,16 @@ For a big ground plane, one `tileRepeat` sprite covering the whole world beats a
1229
1315
 
1230
1316
  ## Visual effects
1231
1317
 
1232
- **`blend: 'add'`** — identical sprites, different blend: normal overlaps cover each other, additive overlaps sum and bloom toward white. Use it for glows, fire, lasers and light shafts, and group additive sprites on their own `zIndex` band since each blend change costs a batch flush.
1318
+ **Blend modes** — four, on sprites and emitters alike. `blend.js` shows all of them side by side over a bright meadow strip:
1319
+
1320
+ | Mode | Does | Use for |
1321
+ | --- | --- | --- |
1322
+ | `'normal'` | Covers what is behind it | Everything else |
1323
+ | `'add'` | Sums toward white | Glows, fire, lasers, light shafts, sparks |
1324
+ | `'multiply'` | Darkens the backdrop | Contact shadows under sprites, stains, grime, smoke that dims |
1325
+ | `'screen'` | Lightens without blowing out | Fog, haze, god rays, soft light |
1326
+
1327
+ Group same-blend sprites on their own `zIndex` band: every mode change in draw order costs a batch flush, so alternating them sprite by sprite degrades toward one draw call each. Unknown strings fall back to `'normal'` in silence and the names are case-sensitive — `blend: 'Add'` renders as normal with no warning.
1233
1328
 
1234
1329
  **`flash(color, duration)`** — a solid-color silhouette that fades out natively. This is the damage flash a multiplicative `tintColor` cannot produce (white tint = no change). Retrigger it on a timer for invincibility blinking.
1235
1330
 
@@ -1263,6 +1358,6 @@ gameView.addEventListener('tap', () => {
1263
1358
  });
1264
1359
  ```
1265
1360
 
1266
- **`timeScale`** — one property slows or freezes physics, sheet animations, tweens, particles and the camera together, while rendering and touch keep running. `0.5` for slow motion, `0` for a pause that still draws, short bursts of `0` for hit-stop.
1361
+ **`timeScale`** — one property slows or freezes physics, sheet animations, tweens, particles, the camera and the game-clock timers together, while rendering and touch keep running. `0.5` for slow motion, `0` for a pause that still draws, short bursts of `0` for hit-stop. `timescale.js` makes the timer half visible: a clock on `gameView.every(1000, ...)` freezes with the scene while a `setInterval` clock beside it keeps ticking.
1267
1362
 
1268
1363
  **`idleAnimation`** — a per-sprite organic sway that composes with tweens and drags. Cheap life for cards, icons, floating pickups and menu art. Disable it before a tween that must land precisely.
@@ -1,6 +1,6 @@
1
1
  # What does not exist yet — and what to do instead
2
2
 
3
- The most expensive mistake with `ti.game` is writing code against a feature that sounds like it should be there because Phaser, Godot or Cocos has it. This file is the guard rail: everything below is **absent from upstream `main` as of 2026-08-18**, with the workaround that is idiomatic today.
3
+ The most expensive mistake with `ti.game` is writing code against a feature that sounds like it should be there because Phaser, Godot or Cocos has it. This file is the guard rail: everything below is **absent from upstream `main` as of 2026-08-19**, with the workaround that is idiomatic today.
4
4
 
5
5
  Planned items come from the module's `TODO.md`. Planned is not shipped — when a game needs one of these, use the workaround and keep the code isolated enough to swap later.
6
6
 
@@ -10,20 +10,21 @@ Planned items come from the module's `TODO.md`. Planned is not shipped — when
10
10
  | --- | --- | --- |
11
11
  | A tilemap layer, Tiled JSON import, collision layer | Planned (priority 4) | One sprite per tile. Fine at 12×15, dead at 200×200. For large ground planes use a single `tileRepeat` sprite instead of a grid, and add sparse invisible solids for collision |
12
12
  | `sprite.parent`, attaching a turret to a tank, a hat to a hero, multi-part bosses | Planned (priority 7), the biggest structural item | Move the parts together from JS on the same coarse tick, or bake the combination into sheet frames. There is no transform inheritance |
13
- | `gameView.raycast(...)`, line-of-sight, ground probes | Planned (priority 3) | Compare positions in JS on a decision timer, or place an invisible trigger sprite where the probe would land and listen for `collision` |
13
+ | `gameView.panTo(x, y, { duration, easing })` for a cutscene camera move | Planned (priority 1) | `follow()` an invisible sprite and tween that sprite, then `follow()` the player again |
14
+ | `gameView.findPath(from, to)` A* over a collision grid | Planned (priority 4) | Hand-author the waypoint arrays and hand them to `followPath`. Straight-line walking is what the point-&-click and topdown demos do |
15
+ | `maxWidth` word wrap on text sprites | Planned (priority 5) | Break the lines yourself with `\n`. Measure with the font's `charWidth` on a monospace grid font; a proportional font needs a per-glyph tally |
16
+ | Ping-pong animation playback, a per-sprite animation speed multiplier, a random start offset | Planned (priority 9) | Author the ping-pong into the sheet as a longer animation. Desync a field of torches by giving each one its own `idleAnimation` timing, or start them from staggered timers |
14
17
  | Native virtual joystick or d-pad bound to a sprite | Planned (priority 6) | Overlaid Titanium views, one per button, writing `velocityX`/`steering`/`throttle` on `touchstart`/`touchend`. Sibling views get simultaneous pointers, so multitouch works |
15
18
  | Gamepad support | Planned (priority 6) | Nothing native. Touch controls only |
16
- | `blend: 'multiply'` or `'screen'` | Planned (priority 2) | Only `'normal'` and `'add'` exist. For darkening, use `tintColor` (multiplicative) on the sprite itself |
17
19
  | Slopes in platformer terrain | Planned (priority 3), conditional | Build slopes as stepped rectangles, or keep terrain flat |
18
20
  | Per-frame animation events (footstep on frame 3) | Planned (priority 9) | Use `animationcomplete` on a short non-looping animation, or a timer aligned to the animation's fps |
19
- | `play('attack', { then: 'idle' })` chaining | Planned (priority 9) | Handle `animationcomplete` and call `play()` from it |
20
- | `sprite.followPath([points], ...)` | Planned (priority 9) | Chain tweens: call `animate()` again from the `complete` handler, one leg per call |
21
21
  | `playbackRate`, pitch jitter, stereo pan, `fadeTo()` on sounds | Planned (priority 8) | Only `volume`, `loop`, `play`, `pause`, `stop` exist. Ship a few pre-pitched variants of a sample and alternate between them |
22
22
  | An fps / draw-call / sprite-count overlay | Planned (developer experience) | `debug: true` draws collision shapes only. There is no stats overlay and no `performance` event — measure with platform tooling |
23
23
  | TypeScript definitions (`ti.game.d.ts`) | Planned (developer experience) | None yet |
24
24
  | Aseprite JSON import | Planned (developer experience) | TexturePacker JSON (hash or array) is the only atlas format |
25
25
  | A `stop()` for the whole scene | Not planned — already covered | `gameView.timeScale = 0` freezes everything the engine ticks while rendering and touch keep running. `gameView.pause()` stops the render loop entirely |
26
- | Tweening `width`, `height`, `tintColor` | Not planned | `animate()` only handles `x`, `y`, `scale`, `scaleX`, `scaleY`, `rotation`, `opacity`, `glowOpacity`, plus `frame` at the end. Everything else is an instant write |
26
+ | Tweening `width`, `height` | Not planned | `animate()` only handles `x`, `y`, `scale`, `scaleX`, `scaleY`, `rotation`, `opacity`, `glowOpacity`, plus `frame` at the end. Everything else is an instant write — scale instead of sizing |
27
+ | Tweening `tintColor`, or `repeat`/`yoyo` on `animate()` | Planned (priority 9) | Re-launch the tween from its `complete` handler for a blink or a ping-pong, and step a tint by writing `tintColor` on a game-clock timer |
27
28
  | Standard Titanium touch events on the GameView | By design | The view fires its own `press`, `tap`, `release` with scene coordinates. Sprites fire the richer set. Overlaid Titanium views keep their normal events |
28
29
  | A per-frame event to hook your own logic into | By design, never | The whole architecture exists to keep JS out of the frame loop. Use `collision`, `land`, `complete`, `animationcomplete`, or a coarse timer for decisions |
29
30
 
@@ -31,7 +32,7 @@ Planned items come from the module's `TODO.md`. Planned is not shipped — when
31
32
 
32
33
  The module's own `TODO.md` rules these out, each being a project the size of everything built so far:
33
34
 
34
- - **2D lighting and shadows.** If it ever comes up: a screen-dim overlay plus additive light sprites (`blend: 'add'`) gets 90% of the look for 1% of the cost.
35
+ - **2D lighting and shadows.** If it ever comes up: a screen-dim overlay plus additive light sprites (`blend: 'add'`) gets 90% of the look for 1% of the cost, and `blend: 'multiply'` now does contact shadows under sprites.
35
36
  - **Skeletal animation (Spine).** Frame animations from sheets only.
36
37
  - **Full rigid-body physics (Box2D class).** What exists — velocity, gravity, solids with penetration resolution, restitution, circle and rect hitboxes, the car model, thrust — covers the target genres. Joints, torque, compound bodies and stacking do not exist.
37
38
  - **Render-to-texture.** The only offscreen pass is the fullscreen `cameraEffect`.
@@ -51,7 +52,12 @@ Recent additions that are easy to miss and easy to reimplement badly in JS:
51
52
  - **`screenFixed`** on any sprite: surface coordinates, camera position/zoom/shake ignored, touch mapped back. HUDs no longer need overlay views.
52
53
  - **`swept: true`** per sprite: the frame's movement is path-tested (Minkowski + slab) for both collision events and solid blocking. Fast bullets stop tunneling; `hitboxScale` tricks along the travel axis are obsolete.
53
54
  - **`collisionend`**: the exit half of the trigger lifecycle, including when the partner is removed, hidden or re-tagged mid-contact.
55
+ - **`followPath(points, opts)` and `play(name, { then })`** (2026-08-19): patrol routes, bullet arcs and animation chains run natively. Do not chain `animate()` legs from `complete`, and do not re-`play()` from an `animationcomplete` handler.
56
+ - **`gameView.raycast(x0, y0, x1, y1, groups)`** (2026-08-19): nearest hit as `{ x, y, distance, group, sprite, normal }`, or `null`. Line of sight, ledge probes and hitscan no longer need an invisible probe sprite — but it is a discrete query, not something to poll every frame.
57
+ - **Game-clock timers** (2026-08-19): `gameView.after(ms, cb)` / `every(ms, cb)` / `cancelTimer(id)` obey `timeScale` and pause with the render loop. Spawn waves and AI ticks belong here, not in `setTimeout`.
58
+ - **`scrollFactor`** (2026-08-19): per-sprite parallax against camera travel. A background layer is one property, not a hand-scrolled pair of copies.
59
+ - **`blend: 'multiply'` and `'screen'`** (2026-08-19) join `'normal'` and `'add'`, on sprites and emitters alike.
54
60
 
55
61
  ## Checking the current state
56
62
 
57
- This file describes upstream `main` on 2026-08-18 — a moving target: four items on this list shipped within two days of the skill being written, and the manifest version (`0.3.0`) did not change with them. Before relying on any "planned" item being still absent — or on a workaround still being necessary — read `TODO.md` and `README.md` at https://github.com/m1ga/ti.game, the module's upstream repo. `README.md` there is the canonical API documentation; the `documentation/` folder only points at it. The priorities quoted above are the maintainer's, not a roadmap this skill decides.
63
+ This file describes upstream `main` on 2026-08-19 — a moving target: eight items on this list shipped within three days of the skill being written, four of them in a single day, and the manifest version (`0.3.0`) did not change with any of them. Before relying on any "planned" item being still absent — or on a workaround still being necessary — read `TODO.md` and `README.md` at https://github.com/m1ga/ti.game, the module's upstream repo. `README.md` there is the canonical API documentation; the `documentation/` folder only points at it. The priorities quoted above are the maintainer's, not a roadmap this skill decides.