@maccesar/titools 4.8.0 → 4.10.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 +8 -2
- package/package.json +1 -1
- package/skills/ti-game/SKILL.md +20 -7
- package/skills/ti-game/references/api.md +60 -12
- package/skills/ti-game/references/project-setup.md +8 -6
- package/skills/ti-game/references/recipes.md +189 -26
- package/skills/ti-game/references/roadmap.md +15 -9
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, pathfinding, 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
|
+
- A* pathfinding (`findPath`) around tagged obstacles, feeding 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,8 @@ 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."
|
|
455
|
+
"Tap to walk, but the player should go around the tree instead of through it."
|
|
451
456
|
```
|
|
452
457
|
|
|
453
458
|
Key rules:
|
|
@@ -457,6 +462,7 @@ Key rules:
|
|
|
457
462
|
- A sprite with size but no sheet is an invisible trigger (scores, goals, walls)
|
|
458
463
|
- Add a whole level with one `gameView.add([...])` call
|
|
459
464
|
- HUDs are `Game.createText` sprites with `screenFixed: true`, not `Ti.UI.Label` overlays
|
|
465
|
+
- Timers that drive game logic belong on the game clock (`gameView.every`), so a paused game stops spawning
|
|
460
466
|
|
|
461
467
|
---
|
|
462
468
|
|
package/package.json
CHANGED
package/skills/ti-game/SKILL.md
CHANGED
|
@@ -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,
|
|
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, solids, carMode drifting, thrust), collision groups, trigger zones, swept AABB, raycast, paths, bitmap text, particles, 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, pathfinding, maze, 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, word wrap) 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-
|
|
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-20**, manifest **`0.4.0`** — that bump is the first in a long run of feature landings (the text engine, `screenFixed`, `swept` collisions, `collisionend`, `followPath`, animation chaining, `raycast`, `findPath`, game-clock timers, `scrollFactor` parallax and the `multiply`/`screen` blend modes all shipped while it still said `0.3.0`), so anything still calling itself 0.3.0 has none of them. 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,17 @@ 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
|
+
| Walking around obstacles to a tapped point | your own A* in JS, or a straight line through the wall | `gameView.findPath(from, to, opts)` → `sprite.followPath(path)` |
|
|
42
|
+
| A spawn wave or an AI tick that respects pause | `setInterval` | `gameView.every(ms, cb)` |
|
|
37
43
|
| Smoke, sparks, explosions | pooled sprites in JS | `Game.createEmitter(...)` |
|
|
38
44
|
| A swinging chain or cape | verlet solver in JS | `Game.createRope(...)` |
|
|
39
45
|
| Slow motion or a pause that still draws | pausing your own timers | `gameView.timeScale = 0.5` / `0` |
|
|
@@ -128,6 +134,10 @@ The enter/exit pair is the trigger lifecycle other engines call `OnTriggerEnter`
|
|
|
128
134
|
|
|
129
135
|
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
136
|
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
And when the question is "how do I get over there?", `gameView.findPath(from, to, { cellSize, groups, clearance, bounds })` runs grid A* around the same tagged sprites and hands back waypoints for `followPath` — tap-to-walk that rounds a tree, a hound re-routing to the player on an 800 ms timer. Also discrete: the walk itself runs natively between queries, so pathfind on taps and AI ticks, never per frame. Pass `clearance` of about half the walker's width, since the route is a line for the sprite's **center**.
|
|
140
|
+
|
|
131
141
|
**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
142
|
|
|
133
143
|
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 +145,7 @@ Tune fairness with `hitboxScale` (art rarely fills its frame; slightly small hit
|
|
|
135
145
|
## Performance rules that actually matter
|
|
136
146
|
|
|
137
147
|
- **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'`
|
|
148
|
+
- **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
149
|
- **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
150
|
- **Keep collision group lists targeted.** Cost is O(colliders × candidates) per frame. A bullet checking `['asteroid']` tests 5 sprites, not the whole scene.
|
|
141
151
|
- **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 +171,9 @@ Standard Titanium touch events do not reach the GameView, but ordinary Titanium
|
|
|
161
171
|
|
|
162
172
|
## Clean up what you created
|
|
163
173
|
|
|
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()`.
|
|
174
|
+
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()`.
|
|
175
|
+
|
|
176
|
+
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
177
|
|
|
166
178
|
```javascript
|
|
167
179
|
win.addEventListener('close', () => {
|
|
@@ -180,7 +192,7 @@ Read the one that matches the task instead of guessing — the API surface below
|
|
|
180
192
|
| Need | Reference |
|
|
181
193
|
| --- | --- |
|
|
182
194
|
| 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) |
|
|
195
|
+
| Working code per genre: flappy, platformer, top-down, racer, asteroids, endless runner, rhythm, cards, point & click, particles, ropes, camera — plus patrol paths, raycast probes, pathfinding, parallax and game-clock timers | [recipes.md](references/recipes.md) |
|
|
184
196
|
| Install the module, tiapp.xml, Alloy vs Classic wiring, assets, sprite sheets, atlases, lifecycle, testing | [project-setup.md](references/project-setup.md) |
|
|
185
197
|
| What does NOT exist yet, what is coming, and how to work around it today | [roadmap.md](references/roadmap.md) |
|
|
186
198
|
|
|
@@ -195,4 +207,5 @@ Read the one that matches the task instead of guessing — the API surface below
|
|
|
195
207
|
7. Is every `createSound`, `setInterval` and `setTimeout` stopped when the window closes?
|
|
196
208
|
8. Is the HUD `Game.createText` with `screenFixed: true`, rather than a stack of overlaid labels?
|
|
197
209
|
9. Does anything travel far enough per frame to tunnel? Give it `swept: true`.
|
|
198
|
-
10.
|
|
210
|
+
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.
|
|
211
|
+
11. Are you using an API that actually exists? Anything involving tilemaps, sprite parenting, joysticks, gamepads 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-
|
|
3
|
+
Complete JS surface of `ti.game` as of upstream `main` on 2026-08-20, 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 reads **`0.4.0`** since 2026-08-20, and that bump matters: the text engine (`createFont`/`createText`), `screenFixed`, `swept`, `collisionend`, `followPath`, animation chaining, `raycast`, `findPath`, the game-clock timers, `scrollFactor` and the `multiply`/`screen` blend modes all landed while the manifest still said `0.3.0`, so a build calling itself 0.3.0 has none of them.
|
|
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,11 @@ 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
|
+
| `findPath(from, to, options)` | method | — | Grid A\* from `from` to `to` (`{ x, y }` world points) around the same sprites `raycast` sees — **visible, non-`screenFixed`**, carrying a `collisionGroup` in `options.groups` (omit for any tagged sprite). Returns an array of `{ x, y }` waypoints ready for `sprite.followPath()`, or `null` when no route exists. Options below |
|
|
62
|
+
| `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 }` |
|
|
63
|
+
| `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 |
|
|
64
|
+
| `cancelTimer(id)` | method | — | Cancels a timer from `after()` / `every()` |
|
|
60
65
|
| `cameraEffect` | string | `'none'` | Fullscreen shader pass: `'none'`, `'tint'`, `'glitch'`. With `'none'` the extra pass is skipped entirely |
|
|
61
66
|
| `cameraTint` | string | — | Color for the `'tint'` effect, e.g. `'#4f8'` (night vision, poison, flashback) |
|
|
62
67
|
| `cameraEffectIntensity` | float | `1` | 0..1 — tint mix or glitch amount |
|
|
@@ -64,6 +69,8 @@ A normal Titanium view — add it to a window or any container, size it with the
|
|
|
64
69
|
|
|
65
70
|
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
71
|
|
|
72
|
+
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.
|
|
73
|
+
|
|
67
74
|
### `follow(sprite, options)`
|
|
68
75
|
|
|
69
76
|
| Option | Default | Effect |
|
|
@@ -79,6 +86,31 @@ Vertical follow is always on. **Horizontal follow is off until you pass `leftMar
|
|
|
79
86
|
|
|
80
87
|
Each call **resets every option to its default** before applying what you passed. Calling `follow(sprite)` with no options after configuring it wipes the configuration.
|
|
81
88
|
|
|
89
|
+
### `findPath(from, to, options)`
|
|
90
|
+
|
|
91
|
+
| Option | Default | Effect |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| `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 |
|
|
94
|
+
| `groups` | all tagged sprites | Array of `collisionGroup` names to treat as obstacles |
|
|
95
|
+
| `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** |
|
|
96
|
+
| `bounds` | the whole surface | `{ minX, minY, maxX, maxY }` search rect — also the walkable area, so a floor clamp goes here |
|
|
97
|
+
| `diagonals` | `true` | Octile moves. Diagonals never cut a blocked corner |
|
|
98
|
+
| `simplify` | `true` | Line-of-sight string pulling: the cell chain collapses to the few corner waypoints `followPath` needs. `false` returns every grid cell (useful to visualize or debug the route) |
|
|
99
|
+
|
|
100
|
+
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).
|
|
101
|
+
|
|
102
|
+
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.
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
const path = gameView.findPath(
|
|
106
|
+
{ x: player.x, y: player.y },
|
|
107
|
+
{ x: e.x, y: e.y },
|
|
108
|
+
{ cellSize: TILE, groups: ['wall'], clearance: player.width * 0.35, bounds: WALK_BOUNDS });
|
|
109
|
+
if (path) {
|
|
110
|
+
player.followPath(path, { speed: 160 });
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
82
114
|
## SpriteSheet
|
|
83
115
|
|
|
84
116
|
Create one sheet per texture and share it across every sprite that uses it — sprites sharing a sheet batch into a single draw call.
|
|
@@ -88,7 +120,7 @@ Create one sheet per texture and share it across every sprite that uses it — s
|
|
|
88
120
|
| `image` | — | Path to the texture, relative to the resources root (`'assets/hero.png'`) |
|
|
89
121
|
| `frameWidth` / `frameHeight` | — | Grid mode: cell size in source pixels. Frames are numbered left-to-right, top-to-bottom, starting at 0 |
|
|
90
122
|
| `atlas` | — | TexturePacker JSON (hash or array format) instead of a grid |
|
|
91
|
-
| `smoothing` | `true` | `false` = GL_NEAREST, the pixel-art setting |
|
|
123
|
+
| `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
124
|
| `repeat` | `false` | GL_REPEAT wrap, required by sprites using `tileRepeat`. Needs power-of-two texture dimensions |
|
|
93
125
|
|
|
94
126
|
| Member | Notes |
|
|
@@ -117,6 +149,7 @@ Textures upload to the GPU lazily on first use, from the render thread, and are
|
|
|
117
149
|
| `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
150
|
| `flipX`, `flipY` | `false` | Mirror the drawn frame only. Position, anchor, physics and hit testing are untouched |
|
|
119
151
|
| `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 |
|
|
152
|
+
| `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
153
|
| `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
154
|
|
|
122
155
|
### Sheet and animation
|
|
@@ -214,19 +247,20 @@ Every sprite gets its own phase, and the wobble unwinds exactly when disabled. T
|
|
|
214
247
|
| `glowColor` | — | Tinted, blurred silhouette drawn behind the sprite by a shader pass |
|
|
215
248
|
| `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
249
|
| `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
|
|
250
|
+
| `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
251
|
|
|
219
252
|
### Methods
|
|
220
253
|
|
|
221
254
|
| Method | Notes |
|
|
222
255
|
| --- | --- |
|
|
223
|
-
| `play(name)` | Start a named animation |
|
|
224
|
-
| `stop()` | Stop the animation on the current frame |
|
|
256
|
+
| `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 |
|
|
257
|
+
| `stop()` | Stop the animation on the current frame; also drops any queued `then` chain |
|
|
258
|
+
| `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
259
|
| `animate(options)` | Native tween, fires `complete` |
|
|
226
260
|
| `clearTweens()` | Cancel tweens in progress — call before starting a replacement tween |
|
|
227
261
|
| `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
262
|
|
|
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).
|
|
263
|
+
`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
264
|
|
|
231
265
|
### Sprite events
|
|
232
266
|
|
|
@@ -240,8 +274,9 @@ Every sprite gets its own phase, and the wobble unwinds exactly when disabled. T
|
|
|
240
274
|
| `dragend` | `x`, `y` | Finger lifted; the sprite has already moved natively |
|
|
241
275
|
| `pinch` | `scaleX`, `scaleY` | While two-finger scaling |
|
|
242
276
|
| `rotate` | `rotation` | While two-finger rotating |
|
|
243
|
-
| `animationcomplete` | `animation` | A non-looping sheet animation finished |
|
|
277
|
+
| `animationcomplete` | `animation` | A non-looping sheet animation finished — including each finished step of a `then` chain |
|
|
244
278
|
| `complete` | final transform values | A tween finished |
|
|
279
|
+
| `pathcomplete` | `x`, `y` | A non-looping `followPath` run reached the end |
|
|
245
280
|
| `collision` | `group`, `other`, `x`, `y` | Overlap with a `collidesWith` group began |
|
|
246
281
|
| `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
282
|
| `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 +384,7 @@ Add and remove like a sprite: `gameView.add(emitter)` / `remove(emitter)`. Spawn
|
|
|
349
384
|
| `startScale` / `endScale` | `1` / `1` | Interpolated over `lifetime` |
|
|
350
385
|
| `startOpacity` / `endOpacity` | `1` / `0` | Interpolated over `lifetime` |
|
|
351
386
|
| `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 |
|
|
387
|
+
| `blend` | `'normal'` | `'add'` for fire, sparks and magic; `'multiply'` for smoke and dust that darken what they cross; `'screen'` for soft haze |
|
|
353
388
|
| `maxParticles` | `200` | Pool size, hard cap 1000 |
|
|
354
389
|
|
|
355
390
|
Methods: `emit(n)` fires a one-shot burst on top of `rate`; `clear()` kills all live particles.
|
|
@@ -381,8 +416,8 @@ The tether yields at the end no finger owns: with a fixed head anchor the tail s
|
|
|
381
416
|
|
|
382
417
|
Only these events exist. Nothing fires per frame, and events are only fired if a listener is registered.
|
|
383
418
|
|
|
384
|
-
- **GameView**: `press`, `tap`, `release`, `resize`
|
|
385
|
-
- **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `collision`, `collisionend`, `land`
|
|
419
|
+
- **GameView**: `press`, `tap`, `release`, `resize`, `timer`
|
|
420
|
+
- **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `pathcomplete`, `collision`, `collisionend`, `land`
|
|
386
421
|
- **SpriteSheet, Font, Sound, Emitter, Rope**: none
|
|
387
422
|
|
|
388
423
|
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 +427,7 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
392
427
|
- **`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
428
|
- **`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
429
|
- **`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.
|
|
430
|
+
- **`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
431
|
- **`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
432
|
- **`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
433
|
- **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 +439,18 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
404
439
|
- **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
440
|
- **Text `width`/`height` are read-only in practice.** They report the laid-out block; to make text bigger use `scale`.
|
|
406
441
|
- **`hitboxShape: 'circle'` also changes the touch area**, not just collisions.
|
|
407
|
-
-
|
|
442
|
+
- **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
443
|
- **`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.
|
|
444
|
+
- **`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.
|
|
445
|
+
- **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.
|
|
446
|
+
- **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.
|
|
447
|
+
- **`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.
|
|
448
|
+
- **`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.
|
|
449
|
+
- **`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.
|
|
450
|
+
- **`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` and `hitboxShape: 'circle'` shape the walkable space too.
|
|
451
|
+
- **`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.
|
|
452
|
+
- **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`.
|
|
453
|
+
- **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`.
|
|
454
|
+
- **`screenFixed` beats `scrollFactor`.** Setting both leaves the sprite screen-fixed; the parallax offset is skipped entirely.
|
|
455
|
+
- **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.
|
|
456
|
+
- **`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.
|
|
@@ -22,8 +22,8 @@ There is no registry and no `ti module install` command — Titanium modules are
|
|
|
22
22
|
|
|
23
23
|
```
|
|
24
24
|
modules/
|
|
25
|
-
├── android/ti.game/0.
|
|
26
|
-
└── iphone/ti.game/0.
|
|
25
|
+
├── android/ti.game/0.4.0/
|
|
26
|
+
└── iphone/ti.game/0.4.0/
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Unzipping the module zip at the project root creates exactly that layout.
|
|
@@ -34,14 +34,14 @@ Then declare it in `tiapp.xml`. The iOS platform key is `iphone`, not `ios`:
|
|
|
34
34
|
|
|
35
35
|
```xml
|
|
36
36
|
<modules>
|
|
37
|
-
<module platform="android" version="0.
|
|
38
|
-
<module platform="iphone" version="0.
|
|
37
|
+
<module platform="android" version="0.4.0">ti.game</module>
|
|
38
|
+
<module platform="iphone" version="0.4.0">ti.game</module>
|
|
39
39
|
</modules>
|
|
40
40
|
```
|
|
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
|
|
44
|
+
The manifest went `0.3.0` → **`0.4.0`** on 2026-08-20, after months of feature landings under the old number — so `0.4.0` is the first version string that actually means something: it carries everything this skill documents. `0.3.0` does not, and there are several different `0.3.0` builds in the wild. One 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; one built before 2026-08-19 16:00 UTC+2 additionally bleeds at grid-frame edges on smoothed sheets. `findPath` exists only in `0.4.0`. If a call that this skill documents is `undefined` at runtime, check the manifest version first and rebuild 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
|
|
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
|
|
3
|
+
Working patterns distilled from the 25 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-
|
|
5
|
+
Every snippet uses only API that exists in upstream `main` as of 2026-08-20 (manifest `0.4.0`). 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, pathfinding, 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`)
|
|
@@ -18,11 +18,11 @@ Every snippet uses only API that exists in upstream `main` as of 2026-08-18. Che
|
|
|
18
18
|
- [Rhythm game](#rhythm-game) (`rhythm.js`)
|
|
19
19
|
- [Ball sports / breakout physics](#ball-sports--breakout-physics) (`volley.js`)
|
|
20
20
|
- [Cards and board games](#cards-and-board-games) (`cards.js`, `puzzle.js`)
|
|
21
|
-
- [Point & click adventure](#point--click-adventure) (`pointclick.js`)
|
|
21
|
+
- [Point & click adventure](#point--click-adventure) (`pointclick.js`, `maze.js`)
|
|
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
|
-
###
|
|
142
|
+
### Parallax
|
|
143
143
|
|
|
144
|
-
Two
|
|
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,142 @@ 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
|
+
### Pathfinding around obstacles — `maze.js`, `pointclick.js`
|
|
393
|
+
|
|
394
|
+
`gameView.findPath(from, to, options)` is grid A\* over the same sprites `raycast` sees: visible, tagged with a `collisionGroup`, nothing else to set up. It returns `{ x, y }` waypoints — or `null` — and those waypoints go straight into `followPath`, so the walk itself runs natively.
|
|
395
|
+
|
|
396
|
+
```javascript
|
|
397
|
+
gameView.addEventListener('tap', (e) => {
|
|
398
|
+
const path = gameView.findPath(
|
|
399
|
+
{ x: player.x, y: player.y },
|
|
400
|
+
{ x: e.x, y: e.y },
|
|
401
|
+
{ cellSize: TILE, groups: ['wall'], bounds: MAZE_BOUNDS });
|
|
402
|
+
if (!path || path.length < 2) {
|
|
403
|
+
return; // tapped outside the walkable rect
|
|
404
|
+
}
|
|
405
|
+
player.scaleX = path[1].x < player.x ? -1 : 1; // face the first leg
|
|
406
|
+
player.play('walk');
|
|
407
|
+
player.followPath(path, { speed: SPEED });
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
`cellSize` is the one option that decides everything else. On a tile map, pass the tile size and the grid lines up with the walls exactly; on a free-form scene, something near the walker's width. Too coarse and doorways disappear, too fine and the query gets expensive (over ~1M cells it returns `null` rather than allocating).
|
|
412
|
+
|
|
413
|
+
**The path is a line for the sprite's *center*.** A body wider than a cell scrapes corners unless you pass `clearance` — about half the walker's width:
|
|
414
|
+
|
|
415
|
+
```javascript
|
|
416
|
+
const path = gameView.findPath(from, to, {
|
|
417
|
+
cellSize: Math.round(W * 0.04),
|
|
418
|
+
groups: ['obstacle'],
|
|
419
|
+
clearance: PLAYER_W * 0.35, // keep half a body off the trunk
|
|
420
|
+
bounds: WALK_BOUNDS // the walkable floor, in center coordinates
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
`bounds` doubles as the walkable area, which is how the point-&-click floor clamp survives: `WALK_BOUNDS` is the old `WALK_TOP`/margin clamp expressed as a rect, in **sprite-center coordinates** (tap points are feet positions, so subtract half the sprite height before searching).
|
|
425
|
+
|
|
426
|
+
**Obstacles do not have to be the art.** `pointclick.js` blocks only the oak's trunk with an invisible sprite — `opacity: 0`, `touchEnabled: false`, no collision wiring — because `findPath` reads the hitbox, not the pixels. The canopy stays walkable, so the player still passes behind the tree with `ySort` doing the depth:
|
|
427
|
+
|
|
428
|
+
```javascript
|
|
429
|
+
gameView.add(Game.createSprite({
|
|
430
|
+
sheet: treeSheet, opacity: 0, touchEnabled: false,
|
|
431
|
+
x: tree.x, y: groundY, width: TREE_W * 0.35, height: PLAYER_H * 0.8,
|
|
432
|
+
collisionGroup: 'obstacle'
|
|
433
|
+
}));
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
A blocked goal snaps to the nearest free cell a few rings out, so tapping a wall walks to its edge instead of doing nothing. Tapping outside `bounds` is the case worth guarding — that is what `path.length < 2` catches.
|
|
437
|
+
|
|
438
|
+
**Chasing AI is a timer, not a frame loop.** Re-path on a coarse game-clock tick and let `followPath` cover the gap:
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
const chase = gameView.every(800, () => {
|
|
442
|
+
const path = gameView.findPath({ x: hound.x, y: hound.y }, { x: player.x, y: player.y }, PATH_OPTIONS);
|
|
443
|
+
if (path && path.length > 1) {
|
|
444
|
+
hound.followPath(path, { speed: TILE * 2.6 });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
win.addEventListener('close', () => gameView.cancelTimer(chase));
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`followPath(null)` cancels an in-flight route — `maze.js` uses it to yank the hound back to its den after it catches you.
|
|
451
|
+
|
|
452
|
+
To *see* what A\* did, ask twice: `simplify: false` returns every grid cell it walked, `simplify: true` (the default) the line-of-sight-reduced corners the sprite actually gets. Dropping a dot on each is the whole route visualization in the maze demo.
|
|
453
|
+
|
|
454
|
+
### Timers on the game clock
|
|
455
|
+
|
|
456
|
+
`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.
|
|
457
|
+
|
|
458
|
+
```javascript
|
|
459
|
+
const wave = gameView.every(2000, () => spawnEnemy());
|
|
460
|
+
gameView.after(500, () => player.play('idle'));
|
|
461
|
+
gameView.cancelTimer(wave); // both return an int id
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
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.
|
|
465
|
+
|
|
466
|
+
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.
|
|
467
|
+
|
|
320
468
|
### Hit-stop on impact
|
|
321
469
|
|
|
322
470
|
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.)
|
|
@@ -1049,31 +1197,37 @@ Because every handler is per sprite, this is multi-touch for free: each finger g
|
|
|
1049
1197
|
|
|
1050
1198
|
## Point & click adventure
|
|
1051
1199
|
|
|
1052
|
-
Tap-to-walk
|
|
1200
|
+
Tap-to-walk routes around the scenery with `findPath` and walks the waypoints natively:
|
|
1053
1201
|
|
|
1054
1202
|
```javascript
|
|
1203
|
+
const WALK_BOUNDS = { // the floor, in sprite-CENTER coordinates
|
|
1204
|
+
minX: W * 0.06, maxX: W * 0.94,
|
|
1205
|
+
minY: WALK_TOP - PLAYER_H / 2, maxY: H * 0.95 - PLAYER_H / 2
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1055
1208
|
function walkTo(x, y) {
|
|
1056
|
-
const targetX = Math.min(Math.max(x,
|
|
1057
|
-
const targetY = Math.min(Math.max(y
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1209
|
+
const targetX = Math.min(Math.max(x, WALK_BOUNDS.minX), WALK_BOUNDS.maxX);
|
|
1210
|
+
const targetY = Math.min(Math.max(y - PLAYER_H / 2, WALK_BOUNDS.minY), WALK_BOUNDS.maxY);
|
|
1211
|
+
const path = gameView.findPath({ x: player.x, y: player.y }, { x: targetX, y: targetY }, {
|
|
1212
|
+
cellSize: Math.round(W * 0.04),
|
|
1213
|
+
groups: ['obstacle'],
|
|
1214
|
+
clearance: PLAYER_W * 0.35, // keep half a body off the trunk
|
|
1215
|
+
bounds: WALK_BOUNDS
|
|
1216
|
+
});
|
|
1217
|
+
if (!path || path.length < 2) {
|
|
1218
|
+
return; // tapped outside the floor
|
|
1061
1219
|
}
|
|
1062
|
-
player.scaleX =
|
|
1063
|
-
player.clearTweens(); // cancel a walk in progress
|
|
1220
|
+
player.scaleX = path[1].x < player.x ? -1 : 1; // face the first leg
|
|
1064
1221
|
player.play('walk');
|
|
1065
|
-
player.
|
|
1066
|
-
x: targetX,
|
|
1067
|
-
y: targetY,
|
|
1068
|
-
duration: distance / WALK_SPEED * 1000, // constant px/s
|
|
1069
|
-
easing: Game.EASE_LINEAR // no ease on a walk
|
|
1070
|
-
});
|
|
1222
|
+
player.followPath(path, { speed: WALK_SPEED }); // constant px/s, cancels the previous route
|
|
1071
1223
|
}
|
|
1072
1224
|
|
|
1073
|
-
player.addEventListener('
|
|
1225
|
+
player.addEventListener('pathcomplete', () => player.play('idle'));
|
|
1074
1226
|
```
|
|
1075
1227
|
|
|
1076
|
-
The walkable floor is a clamp (`
|
|
1228
|
+
The walkable floor is still a clamp, not geometry — but it is now expressed as the `bounds` rect the search runs in. Obstacles are separate: `pointclick.js` adds an invisible box over the oak's *trunk* only (`opacity: 0`, `touchEnabled: false`, `collisionGroup: 'obstacle'`), so the player circles the trunk and still walks behind the canopy with `ySort`. See [Pathfinding around obstacles](#pathfinding-around-obstacles--mazejs-pointclickjs) for the options.
|
|
1229
|
+
|
|
1230
|
+
With nothing in the scene to route around, `findPath` returns `[start, goal]` and `followPath` walks the straight line — the same result as the distance-sized `animate()` tween this demo used before, with one less speed calculation to get wrong.
|
|
1077
1231
|
|
|
1078
1232
|
**Hotspots and the tap conflict.** The GameView fires `tap` for *every* touch, including ones a sprite already handled, so a naive walk handler also walks when you click the bird. Hit-test the interactive sprites in JS and bail out:
|
|
1079
1233
|
|
|
@@ -1229,7 +1383,16 @@ For a big ground plane, one `tileRepeat` sprite covering the whole world beats a
|
|
|
1229
1383
|
|
|
1230
1384
|
## Visual effects
|
|
1231
1385
|
|
|
1232
|
-
|
|
1386
|
+
**Blend modes** — four, on sprites and emitters alike. `blend.js` shows all of them side by side over a bright meadow strip:
|
|
1387
|
+
|
|
1388
|
+
| Mode | Does | Use for |
|
|
1389
|
+
| --- | --- | --- |
|
|
1390
|
+
| `'normal'` | Covers what is behind it | Everything else |
|
|
1391
|
+
| `'add'` | Sums toward white | Glows, fire, lasers, light shafts, sparks |
|
|
1392
|
+
| `'multiply'` | Darkens the backdrop | Contact shadows under sprites, stains, grime, smoke that dims |
|
|
1393
|
+
| `'screen'` | Lightens without blowing out | Fog, haze, god rays, soft light |
|
|
1394
|
+
|
|
1395
|
+
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
1396
|
|
|
1234
1397
|
**`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
1398
|
|
|
@@ -1263,6 +1426,6 @@ gameView.addEventListener('tap', () => {
|
|
|
1263
1426
|
});
|
|
1264
1427
|
```
|
|
1265
1428
|
|
|
1266
|
-
**`timeScale`** — one property slows or freezes physics, sheet animations, tweens, particles and the
|
|
1429
|
+
**`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
1430
|
|
|
1268
1431
|
**`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-
|
|
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-20** (manifest `0.4.0`), 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
|
|
|
@@ -8,22 +8,22 @@ Planned items come from the module's `TODO.md`. Planned is not shipped — when
|
|
|
8
8
|
|
|
9
9
|
| You might reach for | Status | What to do now |
|
|
10
10
|
| --- | --- | --- |
|
|
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 |
|
|
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. `findPath` already rasterizes those per-tile sprites, so a tile maze pathfinds today — the planned collision layer is meant to feed the same grid |
|
|
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.
|
|
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
|
+
| `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 |
|
|
15
|
+
| 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
16
|
| 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
17
|
| 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
18
|
| Slopes in platformer terrain | Planned (priority 3), conditional | Build slopes as stepped rectangles, or keep terrain flat |
|
|
18
19
|
| 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
20
|
| `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
21
|
| 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
22
|
| TypeScript definitions (`ti.game.d.ts`) | Planned (developer experience) | None yet |
|
|
24
23
|
| Aseprite JSON import | Planned (developer experience) | TexturePacker JSON (hash or array) is the only atlas format |
|
|
25
24
|
| 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
|
|
25
|
+
| 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 |
|
|
26
|
+
| 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
27
|
| 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
28
|
| 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
29
|
|
|
@@ -31,7 +31,7 @@ Planned items come from the module's `TODO.md`. Planned is not shipped — when
|
|
|
31
31
|
|
|
32
32
|
The module's own `TODO.md` rules these out, each being a project the size of everything built so far:
|
|
33
33
|
|
|
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.
|
|
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, and `blend: 'multiply'` now does contact shadows under sprites.
|
|
35
35
|
- **Skeletal animation (Spine).** Frame animations from sheets only.
|
|
36
36
|
- **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
37
|
- **Render-to-texture.** The only offscreen pass is the fullscreen `cameraEffect`.
|
|
@@ -51,7 +51,13 @@ Recent additions that are easy to miss and easy to reimplement badly in JS:
|
|
|
51
51
|
- **`screenFixed`** on any sprite: surface coordinates, camera position/zoom/shake ignored, touch mapped back. HUDs no longer need overlay views.
|
|
52
52
|
- **`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
53
|
- **`collisionend`**: the exit half of the trigger lifecycle, including when the partner is removed, hidden or re-tagged mid-contact.
|
|
54
|
+
- **`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.
|
|
55
|
+
- **`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.
|
|
56
|
+
- **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`.
|
|
57
|
+
- **`scrollFactor`** (2026-08-19): per-sprite parallax against camera travel. A background layer is one property, not a hand-scrolled pair of copies.
|
|
58
|
+
- **`blend: 'multiply'` and `'screen'`** (2026-08-19) join `'normal'` and `'add'`, on sprites and emitters alike.
|
|
59
|
+
- **`gameView.findPath(from, to, options)`** (2026-08-20): grid A* around the sprites carrying a `collisionGroup`, returning waypoints ready for `followPath` — the Godot `AStar2D` / GameMaker `mp_grid` equivalent. Tap-to-walk and chasing AI route around obstacles natively now; hand-authored waypoint arrays and straight-line walking are the old workaround. Discrete like `raycast`: taps and AI timers, not per frame.
|
|
54
60
|
|
|
55
61
|
## Checking the current state
|
|
56
62
|
|
|
57
|
-
This file describes upstream `main` on 2026-08-
|
|
63
|
+
This file describes upstream `main` on 2026-08-20 — a moving target: nine items on this list shipped within four days of the skill being written, four of them in a single day, and the manifest only caught up at the end (`0.3.0` → `0.4.0` on 2026-08-20, after all 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.
|