@maccesar/titools 4.10.0 → 4.12.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
|
@@ -435,12 +435,12 @@ When it activates:
|
|
|
435
435
|
- `tiapp.xml` declares `ti.game`, or any file calls `require('ti.game')`
|
|
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
|
-
- Collision groups, invisible trigger zones, enter/exit events, swept AABB for fast bullets, hitbox tuning
|
|
438
|
+
- Collision groups, invisible trigger zones, enter/exit events, swept AABB for fast bullets, per-axis hitbox tuning with the `debug` overlays
|
|
439
439
|
- Raycasts for line of sight, ledge probes and hitscan shots
|
|
440
440
|
- A* pathfinding (`findPath`) around tagged obstacles, feeding native patrol paths (`followPath`) and animation chaining (`play(name, { then })`)
|
|
441
|
-
- Bitmap-font text sprites and screen-fixed HUDs inside the scene
|
|
441
|
+
- Bitmap-font text sprites with native word wrap, and screen-fixed HUDs inside the scene
|
|
442
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
|
|
443
|
+
- Game-clock timers that obey pause and slow motion, texture `unload()` for level streaming, and what LiveView reloads clean up on their own
|
|
444
444
|
- Drag & drop, pinch, rotate, multitouch controls
|
|
445
445
|
|
|
446
446
|
Example prompts:
|
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, 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,
|
|
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, gamepads) 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-23**, 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. The number lags the code in the other direction too: per-axis hitboxes (`hitboxScaleX`/`hitboxScaleY`), text word wrap (`maxWidth`), `unload()` on sheets and fonts, and the LiveView renderer lifecycle all landed on 2026-08-23 with the manifest still at `0.4.0`, so date the build rather than trusting the version. 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
|
|
|
@@ -33,6 +33,7 @@ The practical consequence, and the single biggest mistake to avoid:
|
|
|
33
33
|
| Parallax layers under a moving camera | hand-scrolled layers | `scrollFactor` on each layer |
|
|
34
34
|
| Camera that follows the player | timer writing `cameraX` | `gameView.follow(sprite, options)` |
|
|
35
35
|
| A score, a title, a label | `Ti.UI.Label` overlays | `Game.createText({ text, screenFixed: true })` |
|
|
36
|
+
| A dialog box that fits the screen | breaking the lines with `\n` after measuring in JS | `maxWidth` on the text sprite |
|
|
36
37
|
| A bullet fast enough to skip past a thin wall | shrinking the time step | `swept: true` on the bullet |
|
|
37
38
|
| To know the player *left* a zone | polling on a timer | the `collisionend` event |
|
|
38
39
|
| An enemy on a fixed patrol route | chained `animate()` legs | `sprite.followPath(points, { loop })` |
|
|
@@ -140,7 +141,7 @@ And when the question is "how do I get over there?", `gameView.findPath(from, to
|
|
|
140
141
|
|
|
141
142
|
**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.
|
|
142
143
|
|
|
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.
|
|
144
|
+
Tune fairness with `hitboxScale` (art rarely fills its frame; slightly small hitboxes feel better), `hitboxScaleX`/`hitboxScaleY` when the drawing fills its frame by a different fraction on each axis (a 20×44 hero in a 32×48 frame wants `0.62` wide and `0.92` tall — the two multiply `hitboxScale` rather than replace it), 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.
|
|
144
145
|
|
|
145
146
|
## Performance rules that actually matter
|
|
146
147
|
|
|
@@ -163,6 +164,8 @@ gameView.add(score);
|
|
|
163
164
|
score.text = 'SCORE 10'; // re-lays out natively
|
|
164
165
|
```
|
|
165
166
|
|
|
167
|
+
One long string wraps itself: `maxWidth` breaks lines on word boundaries and re-wraps every time `text` changes, so a dialog box needs no hand-broken `\n`. It is measured in font-space px, *before* `scale` — divide the screen width you want by the scale you are drawing at.
|
|
168
|
+
|
|
166
169
|
`screenFixed: true` works on **any** sprite: `x`/`y` become surface coordinates and camera position, zoom and shake are ignored, while touch still maps back. That is the HUD pattern — the score stays put while the camera follows the player. Without it, text is world-space and scrolls past like scenery (signposts, floating damage numbers).
|
|
167
170
|
|
|
168
171
|
Overlaid `Ti.UI.Label` views still work and are still right for anything the engine cannot draw: native inputs, system fonts, accented text the built-in font lacks, scrollable menus. Just do not reach for them for a score any more.
|
|
@@ -173,6 +176,8 @@ Standard Titanium touch events do not reach the GameView, but ordinary Titanium
|
|
|
173
176
|
|
|
174
177
|
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
178
|
|
|
179
|
+
Two things the engine now cleans up on its own, so do not hand-roll them: a **LiveView reload** retires the previous runtime's render loops, `SoundPool`, `MediaPlayer`s and audio proxies, so reloads stop stacking game loops; and releasing a sheet or font frees its GL texture. What you *can* now do explicitly is `spriteSheet.unload()` / `font.unload()` when streaming levels — permanent, so only after nothing draws from them.
|
|
180
|
+
|
|
176
181
|
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.
|
|
177
182
|
|
|
178
183
|
```javascript
|
|
@@ -196,16 +201,28 @@ Read the one that matches the task instead of guessing — the API surface below
|
|
|
196
201
|
| Install the module, tiapp.xml, Alloy vs Classic wiring, assets, sprite sheets, atlases, lifecycle, testing | [project-setup.md](references/project-setup.md) |
|
|
197
202
|
| What does NOT exist yet, what is coming, and how to work around it today | [roadmap.md](references/roadmap.md) |
|
|
198
203
|
|
|
204
|
+
When this skill runs out — a brand-new feature, a demo's exact code, a build question — read upstream itself at https://github.com/m1ga/ti.game. Each file there is authoritative for something different:
|
|
205
|
+
|
|
206
|
+
| Upstream file | What it is authoritative for |
|
|
207
|
+
| --- | --- |
|
|
208
|
+
| `README.md` | The module's own API documentation, and the demo index. Canonical, but it has lagged the code: verify a defaulted value against the source before relying on it |
|
|
209
|
+
| `example/` | 26 runnable demos, one per feature area — the reference implementations this skill's recipes are distilled from |
|
|
210
|
+
| `TODO.md` | What the maintainer has planned, with priorities. The source of [roadmap.md](references/roadmap.md) |
|
|
211
|
+
| `tutorial.md` | A step-by-step first scene (sprite, animation, tap-to-move). For a human learning the module, not for looking things up |
|
|
212
|
+
| `ios/README.md` | The iOS port's threading, coordinates and touch constants. Mostly for module developers; the app-facing facts are already here |
|
|
213
|
+
| `AGENTS.md` | Rules for changing the module itself — irrelevant to building a game with it |
|
|
214
|
+
| `android/src/ti/game/`, `ios/Classes/` | The truth. Defaults, clamping and edge cases in this skill were read here, not from prose |
|
|
215
|
+
|
|
199
216
|
## Before writing game code, check these
|
|
200
217
|
|
|
201
218
|
1. Is the level built inside a guarded `resize` handler, using `e.width`/`e.height`?
|
|
202
219
|
2. Does any timer move a sprite? Replace it with velocity, gravity, a tween, `carMode` or `thrust`.
|
|
203
220
|
3. Are all the level's objects added in one `gameView.add([...])` call?
|
|
204
221
|
4. Does every moving body have the right model — `solidWith` to be blocked, `collidesWith` to be notified?
|
|
205
|
-
5. Is `hitboxScale` set on sprites whose art does not fill the frame?
|
|
222
|
+
5. Is `hitboxScale` set on sprites whose art does not fill the frame — with `hitboxScaleX`/`hitboxScaleY` when the fit differs per axis?
|
|
206
223
|
6. Do triggers (scores, goals, kill zones) use sheet-less sprites instead of visible art?
|
|
207
224
|
7. Is every `createSound`, `setInterval` and `setTimeout` stopped when the window closes?
|
|
208
225
|
8. Is the HUD `Game.createText` with `screenFixed: true`, rather than a stack of overlaid labels?
|
|
209
226
|
9. Does anything travel far enough per frame to tunnel? Give it `swept: true`.
|
|
210
227
|
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
|
|
228
|
+
11. Are you using an API that actually exists? Anything involving tilemaps, sprite parenting, joysticks, gamepads or slopes — 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-23, 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. The version number is not a feature list either: `hitboxScaleX`/`hitboxScaleY`, text `maxWidth`, `SpriteSheet.unload()`/`Font.unload()` and the LiveView renderer lifecycle all landed on 2026-08-23 with the manifest still at `0.4.0`, so two builds calling themselves 0.4.0 can differ — check the build date, or feature-detect by reading the property **before** ever writing it (`typeof sprite.hitboxScaleX === 'undefined'` means the build predates it). Writing first proves nothing: an unknown property is stored on the Kroll proxy and reads back exactly as it was set.
|
|
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
|
|
|
@@ -128,8 +128,9 @@ Create one sheet per texture and share it across every sprite that uses it — s
|
|
|
128
128
|
| `frameCount` | Number of frames. `0` until the texture has loaded, for grid sheets |
|
|
129
129
|
| `frameNames` | Sorted frame names — atlas sheets only |
|
|
130
130
|
| `frameIndex(name)` | Index for an atlas frame name, `-1` if unknown |
|
|
131
|
+
| `unload()` | Frees the GL texture on the next rendered frame, from the render thread. **Permanent** — sprites still using the sheet stop drawing. For level streaming: unload the finished level's atlases instead of accumulating GPU memory. Since 2026-08-23 |
|
|
131
132
|
|
|
132
|
-
Textures upload to the GPU lazily on first use, from the render thread, and are re-created automatically after an EGL context loss.
|
|
133
|
+
Textures upload to the GPU lazily on first use, from the render thread, and are re-created automatically after an EGL context loss. Releasing the proxy unloads its texture too, so a sheet that goes out of scope does not strand GPU memory.
|
|
133
134
|
|
|
134
135
|
## Sprite
|
|
135
136
|
|
|
@@ -206,7 +207,8 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
|
|
|
206
207
|
| `collisionGroup` | — | The tag *this* sprite carries |
|
|
207
208
|
| `collidesWith` | — | Array of groups this sprite reports overlaps with |
|
|
208
209
|
| `hitboxScale` | `1` | Shrinks the collision box around the anchor |
|
|
209
|
-
| `
|
|
210
|
+
| `hitboxScaleX`, `hitboxScaleY` | `1` | Per-axis corrections **multiplied on top of** `hitboxScale`, for art that fills its frame by a different fraction on each axis. A 20×44 drawing in a 32×48 frame needs `0.62` wide and `0.92` tall — no single `hitboxScale` describes it. Ignored by circle hitboxes. Since 2026-08-23 |
|
|
211
|
+
| `hitboxShape` | `'rect'` | `'circle'` — radius = half the smaller drawn side × `hitboxScale` (the per-axis scales are skipped: a circle has no axes). Circles resolve against solids along the contact normal (corner bounces) and get a round touch area |
|
|
210
212
|
| `swept` | `false` | Test this sprite's movement as a **path** (swept AABB), not just at the end position, so a fast mover cannot tunnel between frames. Applies to both `collidesWith` events and `solidWith` blocking (the sprite is clamped to the impact point, then resolved by the normal static pass). Circle hitboxes sweep as their bounding box. Set it on the *mover* — the bullet, not the wall |
|
|
211
213
|
| `debug` | `false` | Draw this sprite's shapes: green = collision AABB, blue = touch bounds, orange dot = anchor |
|
|
212
214
|
|
|
@@ -306,6 +308,7 @@ const mono = Game.createFont({ image: 'assets/mono.png', charWidth: 9, charHeigh
|
|
|
306
308
|
| `characters` | ASCII 32..126 | Grid mode: which characters the cells map to, row-major |
|
|
307
309
|
| `smoothing` | `true` | Filters the glyph texture like a sheet. The built-in font is always crisp |
|
|
308
310
|
| `lineHeight` | — | Read-only: the font's natural line height in px |
|
|
311
|
+
| `unload()` | Method, not an option: frees the glyph texture exactly like `SpriteSheet.unload()`, and just as permanently. Since 2026-08-23 |
|
|
309
312
|
|
|
310
313
|
A descriptor that fails to parse falls back to the built-in font and logs the error — the text still renders, in the wrong face, which is the symptom to recognize.
|
|
311
314
|
|
|
@@ -335,8 +338,22 @@ score.text = 'SCORE 10'; // re-lays out natively on the next frame
|
|
|
335
338
|
| `align` | `'left'` | `'center'` / `'right'` — how multiple lines align against each other. Unknown values fall back to `'left'` |
|
|
336
339
|
| `letterSpacing` | `0` | Extra px between glyphs; negative tightens |
|
|
337
340
|
| `lineSpacing` | `1` | Multiplier on the font's line height |
|
|
341
|
+
| `maxWidth` | `0` | Wrap width in **font-space px** (`0` = no wrap). Lines break on word boundaries, re-wrapping whenever `text` is written. Since 2026-08-23 |
|
|
338
342
|
| `width`, `height` | laid-out size | **Derived, not settable** — the glyph layout drives the size, and with it the anchor, hit test, AABB and `ySort` bottom edge |
|
|
339
343
|
|
|
344
|
+
`maxWidth` wraps a single long string so dialog boxes stop needing hand-broken `\n` lines — hard `\n` breaks still apply on top of it. Measurement reuses the layout pen (kerning, `letterSpacing`, the missing-glyph advance), so a wrapped line never renders wider than it measured, and the spaces around a soft break are dropped.
|
|
345
|
+
|
|
346
|
+
```javascript
|
|
347
|
+
const dialog = Game.createText({
|
|
348
|
+
text: 'A WISE FROG ONCE SAID: THE POND LOOKS SMALL UNTIL YOU TRY TO HOP ACROSS IT.',
|
|
349
|
+
maxWidth: Math.round(W * 0.55 / UNIT), // font-space px, so divide by the scale
|
|
350
|
+
align: 'center',
|
|
351
|
+
scale: UNIT,
|
|
352
|
+
lineSpacing: 1.3
|
|
353
|
+
});
|
|
354
|
+
dialog.text = nextLine; // re-wraps natively
|
|
355
|
+
```
|
|
356
|
+
|
|
340
357
|
There is no font size: scale the sprite. With the built-in pixel font, use **integer** `scale` values (`Math.max(1, Math.round(W / 240))` is the demos' idiom) so texels stay square.
|
|
341
358
|
|
|
342
359
|
Every glyph is a quad in the same batch, so one label costs one draw call — a screen of labels sharing a font costs one too.
|
|
@@ -438,7 +455,14 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
438
455
|
- **`swept` belongs on the fast mover, not on the target.** A stationary thin wall with `swept: true` changes nothing; the bullet is the sprite whose path needs testing.
|
|
439
456
|
- **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.
|
|
440
457
|
- **Text `width`/`height` are read-only in practice.** They report the laid-out block; to make text bigger use `scale`.
|
|
458
|
+
- **`unload()` is permanent, not a cache eviction.** There is no reload: sprites and text still pointing at an unloaded sheet or font simply stop drawing. Unload a level's atlases after its sprites are gone, never to "free memory" on something still on screen.
|
|
459
|
+
- **A tap is a press released within 300 ms**, inside the touch slop (the platform's scaled slop on Android, 8 pt × screen scale on iOS). Hold a button longer and you get `press` and `release` but no `tap` — which is why the demos drive held controls from `press`/`release` and reserve `tap` for discrete hits.
|
|
441
460
|
- **`hitboxShape: 'circle'` also changes the touch area**, not just collisions.
|
|
461
|
+
- **The hitbox scales do not change the touch area.** `hitTest` runs against the full drawn frame, so a sprite with `hitboxScale: 0.6` still takes taps out to its art's edges. Only the hitbox *shape* reaches touch. Shrink `width`/`height` if a tap target must match the collision box.
|
|
462
|
+
- **`hitboxScaleX`/`hitboxScaleY` multiply `hitboxScale`, they do not replace it.** `hitboxScale: 0.85` with `hitboxScaleX: 0.66` gives an effective 0.561 on X and 0.85 on Y. Both default to `1`, so sprites that never set them are unaffected — and `0` still means zero, as everywhere else in the engine.
|
|
463
|
+
- **Text `maxWidth` is in font-space px, before `scale`.** The wrap width on screen is `maxWidth * scale`, so a label at `scale: 3` wrapping at 55% of the screen wants `maxWidth: W * 0.55 / 3`. Passing screen pixels straight in produces a block three times too wide.
|
|
464
|
+
- **`align` centers against the widest wrapped line, not against `maxWidth`.** The block's `width` is what the glyphs actually occupied, so a short wrapped line is not centered inside the wrap column — anchor the sprite where the block should sit.
|
|
465
|
+
- **A word longer than `maxWidth` overflows rather than breaking mid-word.** There is no hyphenation and no character-level break; a long unbroken token widens the block past the limit.
|
|
442
466
|
- **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.
|
|
443
467
|
- **`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
468
|
- **`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.
|
|
@@ -447,7 +471,7 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
447
471
|
- **`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
472
|
- **`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
473
|
- **`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
|
|
474
|
+
- **`findPath` walks the sprite's center, not its silhouette.** The waypoints are a line through free grid cells, so a body wider than a cell clips corners unless you pass `clearance` (roughly half the walker's width) or shrink `cellSize`. Obstacles are read from the **hitbox**, so `hitboxScale`, the per-axis scales and `hitboxShape: 'circle'` shape the walkable space too.
|
|
451
475
|
- **`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
476
|
- **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
477
|
- **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`.
|
|
@@ -41,7 +41,16 @@ 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 manifest went `0.3.0` → **`0.4.0`** on 2026-08-20, after months of feature landings under the old number
|
|
44
|
+
**The version string does not identify a feature set — the build date does.** The manifest went `0.3.0` → **`0.4.0`** on 2026-08-20, after months of feature landings under the old number, and then kept accepting features without moving again. So both numbers cover several incompatible builds:
|
|
45
|
+
|
|
46
|
+
| A build from | Missing, compared to what this skill documents |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `0.3.0` before 2026-08-18 | `createText`, `createFont`, `screenFixed`, `swept`, `collisionend` |
|
|
49
|
+
| `0.3.0` before 2026-08-19 | also `followPath`, `play(name, { then })`, `raycast`, `after`/`every`, `scrollFactor`, `multiply`/`screen` blends |
|
|
50
|
+
| `0.3.0` before 2026-08-19 16:00 UTC+2 | additionally bleeds at grid-frame edges on smoothed sheets |
|
|
51
|
+
| `0.4.0` — including the 2026-08-20 release zip | `hitboxScaleX`/`hitboxScaleY`, text `maxWidth`, `SpriteSheet.unload()`/`Font.unload()` and the LiveView renderer lifecycle, all added on 2026-08-23 with the manifest untouched |
|
|
52
|
+
|
|
53
|
+
The published releases are pre-releases and lag `main`: the newest one is `0.4.0` (2026-08-20). If a call that this skill documents is `undefined` at runtime, check the manifest version *and the build date* before hunting for a typo, and rebuild from upstream `main`. More reliable than either is feature-detecting in code: read the property **before** ever writing it (`typeof sprite.hitboxScaleX === 'undefined'` means the build predates it). Write-then-read always says yes, because an unknown property is stored on the Kroll proxy and reads back exactly as it was set.
|
|
45
54
|
|
|
46
55
|
Building the module from source:
|
|
47
56
|
|
|
@@ -287,9 +296,32 @@ function listen(target, eventName, handler) {
|
|
|
287
296
|
|
|
288
297
|
For restart-without-teardown (a retry button), `removeAllSprites()` plus rebuilding is simplest, but pooling and repositioning is cheaper: reset positions, zero velocities, `clearTweens()`, and re-`play()` the animations.
|
|
289
298
|
|
|
299
|
+
### GPU textures: `unload()`
|
|
300
|
+
|
|
301
|
+
Sheets and fonts hold GL textures, and until 2026-08-23 nothing but losing the context ever freed one. `spriteSheet.unload()` and `font.unload()` release the texture on the next rendered frame, from the render thread. Releasing the proxy does the same, so a sheet that goes out of scope no longer strands its memory.
|
|
302
|
+
|
|
303
|
+
This is for **level streaming** — a game that loads world 2's atlases while world 1's are still resident. It is not a cache: the unload is permanent, and any sprite still pointing at that sheet stops drawing.
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
function unloadLevel(level) {
|
|
307
|
+
level.sprites.forEach(s => gameView.remove(s));
|
|
308
|
+
level.sheets.forEach(sheet => sheet.unload()); // after the sprites are gone
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
A single-level game with a handful of sheets should not call it at all: the textures die with the window.
|
|
313
|
+
|
|
314
|
+
### LiveView reloads
|
|
315
|
+
|
|
316
|
+
A LiveView reload replaces Titanium's JS runtime while the native side survives, which used to leave the previous `GameView`'s render loop running — every reload added another GL thread, and the audio engine handed the new runtime sample ids the old one still owned.
|
|
317
|
+
|
|
318
|
+
Since 2026-08-23 the module retires the previous generation itself: stale renderers are shut down (the `GLSurfaceView` is detached, which stops its `GLThread`), the old `SoundPool`, `MediaPlayer`s and audio proxies are released, and on Android `GameViewProxy.releaseViews()` drops the view reference so the `GLSurfaceView` — and the Activity it holds — can be collected after a window close. iOS does the same by hooking the LiveView restart and shutting down the views registered to the retiring runtime context.
|
|
319
|
+
|
|
320
|
+
**Nothing in your code needs to change**, and this is not a substitute for the cleanup above: your own timers, sounds and listeners still belong to you on a normal window close. It does mean that a reload no longer accumulates game loops, so a stuttering scene after ten reloads is a symptom that dates the build.
|
|
321
|
+
|
|
290
322
|
## Debugging and tuning
|
|
291
323
|
|
|
292
|
-
- **`debug: true` on the GameView** draws collision shapes for the whole scene; on a single sprite, only that one. Green = collision AABB with `hitboxScale` applied, blue = sprite/touch bounds, orange dot = anchor. This is the fastest way to explain a hit that should not have registered.
|
|
324
|
+
- **`debug: true` on the GameView** draws collision shapes for the whole scene; on a single sprite, only that one. Green = collision AABB with `hitboxScale` and the per-axis `hitboxScaleX`/`hitboxScaleY` applied, blue = sprite/touch bounds (the full drawn frame, which is also the touch area), orange dot = anchor. This is the fastest way to explain a hit that should not have registered.
|
|
293
325
|
- **Physics that feels wrong is usually a units problem.** Speeds are px/s and accelerations px/s², both in surface pixels — which is why the demos express them as fractions of `W` and `H`. A gravity of `20` is imperceptible; `H * 2.2` is a snappy platformer.
|
|
294
326
|
- **Sprite is invisible?** Check in order: was it added to the GameView, does it have a sheet, is `width`/`height` non-zero, is it inside the surface bounds, and is its `zIndex` below a background sprite.
|
|
295
327
|
- **Collision never fires?** `collisionGroup` goes on the *target*, `collidesWith` on the *mover*. Both need to be set, and `visible: false` disables collision entirely.
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# ti.game recipes by genre
|
|
2
2
|
|
|
3
|
-
Working patterns distilled from the
|
|
3
|
+
Working patterns distilled from the 26 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`
|
|
5
|
+
Every snippet uses only API that exists in upstream `main` at `df66122`, 2026-08-23 (manifest `0.4.0`, unchanged since 2026-08-20 — the version number stopped tracking the code). Check [roadmap.md](roadmap.md) before reaching for anything not shown here.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
9
|
+
- [The 26 demos, and what each one is the reference for](#the-26-demos-and-what-each-one-is-the-reference-for)
|
|
9
10
|
- [The scaffolding](#the-scaffolding)
|
|
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
|
+
- [Cross-cutting patterns](#cross-cutting-patterns) — pooling, freeing textures, parallax, multitouch controls, HUD text, wrapped dialog, invisible triggers, seeing the hitbox, enter/exit zones, patrol routes, line of sight, pathfinding, game-clock timers, hit-stop
|
|
11
12
|
- [Tap-to-flap](#tap-to-flap) (`flappy.js`)
|
|
12
13
|
- [Platformer](#platformer) (`platformer.js`)
|
|
13
14
|
- [Top-down / Zelda](#top-down--zelda) (`topdown.js`)
|
|
@@ -24,6 +25,39 @@ Every snippet uses only API that exists in upstream `main` as of 2026-08-20 (man
|
|
|
24
25
|
- [Camera work](#camera-work) (`camera.js`)
|
|
25
26
|
- [Visual effects](#visual-effects) (`blend.js`, `flip.js`, `timescale.js`, `demoscene.js`)
|
|
26
27
|
|
|
28
|
+
## The 26 demos, and what each one is the reference for
|
|
29
|
+
|
|
30
|
+
`example/` is the module's real documentation: every demo is a runnable app, and upstream keeps them current with the engine. When a recipe here is not enough, open the demo — it compiles today.
|
|
31
|
+
|
|
32
|
+
| Demo | The reference implementation of | Covered here |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `basic.js` | Sheets, animations, drag/pinch/rotate, tween chaining | [The scaffolding](#the-scaffolding) |
|
|
35
|
+
| `puzzle.js` | Drag & drop with snapping, press-to-lift, one piece per finger | [Cards and board games](#cards-and-board-games) |
|
|
36
|
+
| `flappy.js` | Gravity + tap impulse, score zones, wrapping parallax | [Tap-to-flap](#tap-to-flap) |
|
|
37
|
+
| `platformer.js` | `solidWith`, `land`, `oneWay` stairs, a moving platform that carries you, d-pad | [Platformer](#platformer) |
|
|
38
|
+
| `volley.js` | `restitution`, JS hit response, an AI timer | [Ball sports / breakout physics](#ball-sports--breakout-physics) |
|
|
39
|
+
| `racing.js` | `carMode` drift, skid marks, lap and checkpoint logic | [Top-down racer](#top-down-racer) |
|
|
40
|
+
| `cards.js` | Deck dealing, fanned hand, selection tweens, idle wobble | [Cards and board games](#cards-and-board-games) |
|
|
41
|
+
| `asteroids.js` | `thrust`/`angularVelocity`, `wrapAround`, bullet pooling, additive bolts | [Asteroids / space shooter](#asteroids--space-shooter) |
|
|
42
|
+
| `topdown.js` | Tile map from a string array, `ySort` depth, follower NPC | [Top-down / Zelda](#top-down--zelda) |
|
|
43
|
+
| `skate.js` | Endless runner: pooled obstacles, jump button, pixel-art parallax | [Endless runner](#endless-runner) |
|
|
44
|
+
| `pointclick.js` | Tap-to-walk with `findPath` + `followPath`, verb coin, hotspots | [Point & click adventure](#point--click-adventure) |
|
|
45
|
+
| `particles.js` | Fountain, tap bursts, a smoke trail following a dragged sprite | [Particles](#particles) |
|
|
46
|
+
| `rhythm.js` | Pooled notes on native velocity, `press` pads, timing windows | [Rhythm game](#rhythm-game) |
|
|
47
|
+
| `camera.js` | Dead-zone follow, `cameraBounds`, zoom, shake, `cameraEffect`, `scrollFactor` | [Camera work](#camera-work) |
|
|
48
|
+
| `rope.js` | Verlet ropes from a dragged head and from a fixed anchor | [Ropes and chains](#ropes-and-chains) |
|
|
49
|
+
| `flip.js` | `flipX`/`flipY` driven by movement; tap inverts gravity | [Visual effects](#visual-effects) |
|
|
50
|
+
| `hitbox.js` | `debug: true` overlays, and why the per-axis hitbox scales exist | [Seeing the hitbox](#seeing-the-hitbox--hitboxjs) |
|
|
51
|
+
| `blend.js` | The four blend modes side by side, `flash()` variants | [Visual effects](#visual-effects) |
|
|
52
|
+
| `text.js` | Screen-fixed HUD, world-space labels, text buttons, wrapped dialog | [HUD](#hud--textjs-flappyjs) |
|
|
53
|
+
| `swept.js` | `swept: true` vs tunneling, same bullets, rising speed | [Object pooling](#object-pooling) |
|
|
54
|
+
| `path.js` | Smoothed circuits, sharp patrols, `play(name, { then })` chains | [Patrol routes and animation chains](#patrol-routes-and-animation-chains--pathjs) |
|
|
55
|
+
| `raycast.js` | Line of sight, ledge probes, tap-fired hitscan | [Line of sight, ledge probes and hitscan](#line-of-sight-ledge-probes-and-hitscan--raycastjs) |
|
|
56
|
+
| `zones.js` | The `collision`/`collisionend` lifecycle, including despawned partners | [Zones you can be inside of](#zones-you-can-be-inside-of-enter--exit--zonesjs) |
|
|
57
|
+
| `demoscene.js` | Sine scrollers and copper bars built from `followPath` circles | [Visual effects](#visual-effects) |
|
|
58
|
+
| `maze.js` | A* over a tile maze, `simplify: false` route visualization, a re-pathing hound | [Pathfinding around obstacles](#pathfinding-around-obstacles--mazejs-pointclickjs) |
|
|
59
|
+
| `timescale.js` | `timeScale` slow motion and freeze; game clock vs `setInterval` | [Timers on the game clock](#timers-on-the-game-clock) |
|
|
60
|
+
|
|
27
61
|
## The scaffolding
|
|
28
62
|
|
|
29
63
|
Every game repeats this shape. Sheets and sounds are created once, outside `resize`, because they are texture handles, not scene objects.
|
|
@@ -135,10 +169,23 @@ function fire(x, y, vx, vy) {
|
|
|
135
169
|
|
|
136
170
|
The `generation` counter matters: without it, a timeout fired for an old shot deactivates the *new* bullet occupying that slot.
|
|
137
171
|
|
|
138
|
-
`swept: true` (compared side by side in `swept.js`) is what keeps a fast bolt from crossing a rock between two frames without ever overlapping it. The asteroids demo predates it and inflates the hitbox instead (`hitboxScale: 1.4`) — that works, at the price of counting near misses as hits. Prefer `swept`, and reserve `hitboxScale` for fairness tuning.
|
|
172
|
+
`swept: true` (compared side by side in `swept.js`) is what keeps a fast bolt from crossing a rock between two frames without ever overlapping it. The asteroids demo predates it and inflates the hitbox instead (`hitboxScale: 1.4`) — that works, at the price of counting near misses as hits. Prefer `swept`, and reserve `hitboxScale` — with `hitboxScaleX`/`hitboxScaleY` when the art's fit differs per axis — for fairness tuning.
|
|
139
173
|
|
|
140
174
|
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
175
|
|
|
176
|
+
### Freeing a level's textures
|
|
177
|
+
|
|
178
|
+
Pooling recycles sprites; it does not free the **texture** behind them. A game that streams worlds accumulates every atlas it has ever loaded until the GL context dies. `unload()` releases one on the next rendered frame.
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
function unloadLevel(level) {
|
|
182
|
+
level.sprites.forEach((s) => gameView.remove(s));
|
|
183
|
+
level.sheets.forEach((sheet) => sheet.unload()); // only once nothing draws from them
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
It is permanent — not an eviction that reloads on demand. A sprite still pointing at an unloaded sheet stops drawing, silently. A single-level game should not call it: the textures die with the window.
|
|
188
|
+
|
|
142
189
|
### Parallax
|
|
143
190
|
|
|
144
191
|
Two ways, and the choice is decided by whether the **camera** moves or the **world** does.
|
|
@@ -266,6 +313,28 @@ Because a label is a sprite, the HUD gets the whole toolbox for free — `animat
|
|
|
266
313
|
|
|
267
314
|
Drop `screenFixed` and the same label becomes world furniture: signposts, floating damage numbers, a name over an NPC — it scrolls with everything else.
|
|
268
315
|
|
|
316
|
+
A dialog box is one long string with `maxWidth`: the engine breaks the lines on word boundaries and re-wraps every time `text` is written, so the source keeps no hand-placed `\n`.
|
|
317
|
+
|
|
318
|
+
```javascript
|
|
319
|
+
const UNIT = Math.max(1, Math.round(W / 240));
|
|
320
|
+
const dialog = Game.createText({
|
|
321
|
+
text: 'TAP FOR POND WISDOM',
|
|
322
|
+
maxWidth: Math.round(W * 0.55 / UNIT), // font-space px: divide by the scale
|
|
323
|
+
align: 'center',
|
|
324
|
+
lineSpacing: 1.3,
|
|
325
|
+
scale: UNIT,
|
|
326
|
+
x: W / 2,
|
|
327
|
+
y: H * 0.78,
|
|
328
|
+
zIndex: 3
|
|
329
|
+
});
|
|
330
|
+
dialog.addEventListener('tap', () => {
|
|
331
|
+
dialog.text = lines[i++ % lines.length]; // re-wraps natively
|
|
332
|
+
dialog.flash('#fff', 150);
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`maxWidth` is measured before `scale`, which is the only trap here — pass screen pixels straight in and the block comes out `scale` times too wide. `align` positions the lines against each other (the block's own width), not inside the wrap column, so anchor the sprite where the text should sit. A word longer than `maxWidth` overflows instead of breaking mid-word.
|
|
337
|
+
|
|
269
338
|
Overlaid `Ti.UI.Label` views remain the answer for text the engine's font cannot draw (accents, system fonts, right-to-left) and for real UI: menus, dialogs, text inputs.
|
|
270
339
|
|
|
271
340
|
### Invisible triggers
|
|
@@ -298,6 +367,19 @@ car.addEventListener('collision', (e) => {
|
|
|
298
367
|
});
|
|
299
368
|
```
|
|
300
369
|
|
|
370
|
+
### Seeing the hitbox — `hitbox.js`
|
|
371
|
+
|
|
372
|
+
`debug: true` on the GameView is the whole tuning workflow, and the demo exists to make the overlays legible: two identical adventurers walk into the same wall, one untuned and one with `hitboxScaleX: 0.62` / `hitboxScaleY: 0.92`. The untuned one stops a body's width short and hovers above the floor on his frame's padding; the tuned one goes flush and lands his feet. Tapping toggles the tuning live, so the green box snaps between frame and drawing.
|
|
373
|
+
|
|
374
|
+
```javascript
|
|
375
|
+
const gameView = Game.createGameView({ debug: true }); // green = collision AABB, blue = drawn frame, orange = anchor
|
|
376
|
+
|
|
377
|
+
hero.hitboxScaleX = 0.62; // both are live: write them and watch the box move
|
|
378
|
+
hero.hitboxScaleY = 0.92;
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Read the two boxes as a pair: green is what collides, blue is what draws **and what takes taps**. Green much smaller than blue on one axis only is exactly the case the per-axis scales exist for; green and blue identical on a sprite whose art has padding is the bug the demo dramatizes.
|
|
382
|
+
|
|
301
383
|
### Zones you can be inside of (enter / exit) — `zones.js`
|
|
302
384
|
|
|
303
385
|
`collision` and `collisionend` are the two halves of a trigger: entering water, standing on a pressure plate, holding a capture point. Nothing fires while you stay inside — the state lives in JS between the two events, and that is deliberate, since a per-frame "stay" would be bridge traffic every frame.
|
|
@@ -505,6 +587,7 @@ const pig = Game.createSprite({
|
|
|
505
587
|
height: PIG_SIZE,
|
|
506
588
|
zIndex: 10,
|
|
507
589
|
hitboxScale: 0.7, // the art does not fill the frame
|
|
590
|
+
hitboxScaleY: 0.85, // ...and the pig is wider than he is tall
|
|
508
591
|
collidesWith: ['pipe', 'ground', 'score', 'ceiling'],
|
|
509
592
|
animations: { fly: { frames: [0, 1, 2, 1], fps: 10, loop: true } }
|
|
510
593
|
});
|
|
@@ -554,6 +637,7 @@ const player = Game.createSprite({
|
|
|
554
637
|
zIndex: 10,
|
|
555
638
|
gravity: GRAVITY, // H * 2.2
|
|
556
639
|
hitboxScale: 0.85,
|
|
640
|
+
hitboxScaleX: 0.66, // the blob is 38px wide in a 64px frame, but nearly full height
|
|
557
641
|
solidWith: ['solid', 'trampoline'],
|
|
558
642
|
animations: {
|
|
559
643
|
idle: { frames: [0], fps: 1, loop: true },
|
|
@@ -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-23** (`df66122`, 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
|
|
|
@@ -11,7 +11,6 @@ Planned items come from the module's `TODO.md`. Planned is not shipped — when
|
|
|
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. `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
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
14
|
| 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 |
|
|
16
15
|
| 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 |
|
|
17
16
|
| Gamepad support | Planned (priority 6) | Nothing native. Touch controls only |
|
|
@@ -57,7 +56,11 @@ Recent additions that are easy to miss and easy to reimplement badly in JS:
|
|
|
57
56
|
- **`scrollFactor`** (2026-08-19): per-sprite parallax against camera travel. A background layer is one property, not a hand-scrolled pair of copies.
|
|
58
57
|
- **`blend: 'multiply'` and `'screen'`** (2026-08-19) join `'normal'` and `'add'`, on sprites and emitters alike.
|
|
59
58
|
- **`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.
|
|
59
|
+
- **Text word wrap** (2026-08-23): `maxWidth` on a text sprite breaks lines on word boundaries and re-wraps whenever `text` changes, so a dialog box is one long string instead of hand-broken `\n` lines. The width is in font-space px, before `scale`. Do not write a JS line-breaker any more.
|
|
60
|
+
- **`hitboxScaleX` / `hitboxScaleY`** (2026-08-23): per-axis corrections multiplied on top of `hitboxScale`, for art that fills its frame by a different fraction on each axis. The old workaround — settling for one compromise scale, or padding the frame so a single number fits — is obsolete.
|
|
61
|
+
- **`spriteSheet.unload()` / `font.unload()`** (2026-08-23): frees a GL texture on the next frame instead of holding it until the context dies. Level streaming no longer needs a fresh window to reclaim GPU memory. Permanent — it is not a cache eviction.
|
|
62
|
+
- **LiveView reloads retire the previous generation** (2026-08-23): stale render loops, the old `SoundPool`, `MediaPlayer`s and audio proxies are shut down by the module itself. Do not write your own reload guard, and do not read a scene stuttering after several reloads as an engine limit — it dates the build.
|
|
60
63
|
|
|
61
64
|
## Checking the current state
|
|
62
65
|
|
|
63
|
-
This file describes upstream `main` on 2026-08-
|
|
66
|
+
This file describes upstream `main` at `df66122` on 2026-08-23 — a moving target: thirteen items on this list shipped within a week of the skill being written, four of them in a single day, and the manifest tracks none of it precisely (`0.3.0` → `0.4.0` on 2026-08-20, after nine of them; word wrap, the per-axis hitbox, `unload()` and the LiveView lifecycle all landed on 2026-08-23 with the number unchanged). 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.
|