@maccesar/titools 4.7.0 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -177,7 +177,7 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
177
177
  | ti-expert | Architecture and implementation | Starting point for most tasks |
178
178
  | purgetss | Utility-first styling | UI styling and animations |
179
179
  | ti-ui | UI/UX patterns | Complex layouts, ListViews, platform UI |
180
- | ti-game | 2D games with the `ti.game` module | Sprites, physics, collisions, particles, camera |
180
+ | ti-game | 2D games with the `ti.game` module | Sprites, physics, collisions, 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 |
@@ -186,7 +186,7 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
186
186
 
187
187
  Notes:
188
188
  - The first four (`ti-expert`, `purgetss`, `ti-ui`, `ti-game`) are opinionated workflow skills reflecting TiTools' Titanium conventions.
189
- - `ti-game` documents the `ti.game` native module (2D sprite engine on OpenGL ES 2.0, Android and iOS). Its API reference was verified against the module source; it applies to Alloy and Classic projects alike and does not depend on PurgeTSS.
189
+ - `ti-game` documents the `ti.game` native module (2D sprite engine on OpenGL ES 2.0, Android and iOS). Its API reference was verified against the module source and tracks upstream `main` (the module ships features ahead of its manifest version); it applies to Alloy and Classic projects alike and does not depend on PurgeTSS.
190
190
  - The latter five are documentation-mirror skills sourced from the official Titanium SDK docs (`tidev/titanium-docs`) and audited against them via the internal `titools-skill-auditor`.
191
191
  - `purgetss` reference files are audited against the official PurgeTSS documentation, but its workflow conventions are opinionated.
192
192
 
@@ -435,7 +435,8 @@ 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, hitbox tuning
438
+ - Collision groups, invisible trigger zones, enter/exit events, swept AABB for fast bullets, hitbox tuning
439
+ - Bitmap-font text sprites and screen-fixed HUDs inside the scene
439
440
  - Particle emitters, Verlet ropes, camera follow/zoom/shake/effects, low-latency sound
440
441
  - Drag & drop, pinch, rotate, multitouch controls
441
442
 
@@ -446,6 +447,7 @@ Example prompts:
446
447
  "Add a camera that follows the player with a dead zone."
447
448
  "How do I make a top-down racer that drifts?"
448
449
  "Why does my game stutter when I move sprites from setInterval?"
450
+ "Add a score HUD that stays put while the camera follows the player."
449
451
  ```
450
452
 
451
453
  Key rules:
@@ -454,7 +456,7 @@ Key rules:
454
456
  - `solidWith` blocks, `collidesWith` reports — they are independent
455
457
  - A sprite with size but no sheet is an invisible trigger (scores, goals, walls)
456
458
  - Add a whole level with one `gameView.add([...])` call
457
- - There is no text in the engine: HUDs are `Ti.UI.Label` overlays
459
+ - HUDs are `Game.createText` sprites with `screenFixed: true`, not `Ti.UI.Label` overlays
458
460
 
459
461
  ---
460
462
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "description": "Titanium SDK skills and agents for AI coding assistants (Claude Code, Gemini CLI, Codex CLI)",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: ti-game
3
- description: "Use when building or reviewing 2D games with the ti.game module for Titanium SDK (Android and iOS) — sprites, sprite sheets, frame animations, tweens, physics (gravity, solid platforms, carMode drifting, thrust), collision groups, particles, Verlet ropes, camera follow/zoom/shake, low-latency sound, drag & drop, multitouch. AUTO-DETECT: if tiapp.xml declares ti.game or any file calls require('ti.game'), invoke BEFORE writing ANY game code. Also trigger on: game, juego, sprite, spritesheet, platformer, endless runner, top-down, shooter, racer, flappy, breakout, rhythm game, card game, puzzle, collision, hitbox, parallax, particles, 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 (bitmap text, tilemap layer, sprite parenting, raycast) do not exist yet — check references/roadmap.md before inventing an API."
3
+ description: "Use when building or reviewing 2D games with the ti.game module for Titanium SDK (Android and iOS) — sprites, sprite sheets, frame animations, tweens, physics (gravity, solid platforms, carMode drifting, thrust), collision groups, trigger zones, swept AABB, bitmap-font text and HUDs, particles, Verlet ropes, camera follow/zoom/shake, low-latency sound, drag & drop, multitouch. AUTO-DETECT: if tiapp.xml declares ti.game or any file calls require('ti.game'), invoke BEFORE writing ANY game code. Also trigger on: game, juego, sprite, spritesheet, platformer, endless runner, top-down, shooter, racer, flappy, breakout, rhythm game, card game, puzzle, collision, hitbox, parallax, particles, HUD, score, game loop, 60 fps, GameView. ti.game is NOT Phaser, Godot or Box2D: you never write a game loop, never move a sprite from setInterval, and several familiar features (tilemap layer, sprite parenting, raycast, joystick) do not exist yet — check references/roadmap.md before inventing an API."
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 was written against version 0.3.0 as it stands in the `macCesar/ti.game` contribution fork; when upstream and the fork disagree, upstream wins. Same JS API on Android and iOS — the iOS side is a class-per-class Obj-C twin of the Android engine.
10
+ `ti.game` is a native OpenGL ES 2.0 sprite engine exposed to JavaScript, written and maintained by **Michael Gangolf (m1ga)** — upstream is https://github.com/m1ga/ti.game. This skill tracks upstream `main` as of **2026-08-18** — the manifest still reads `0.3.0`, but the text engine, `screenFixed`, `swept` collisions and `collisionend` landed after it, so a version number alone will not tell you what a checkout has. When upstream and any fork disagree, upstream wins. Same JS API on Android and iOS — the iOS side is a class-per-class Obj-C twin of the Android engine.
11
11
 
12
12
  Works in **both Classic and Alloy** projects. Nothing here depends on PurgeTSS.
13
13
 
@@ -31,6 +31,9 @@ The practical consequence, and the single biggest mistake to avoid:
31
31
  | Drag a sprite with the finger | `touchmove` handler | `draggable: true` |
32
32
  | Scrolling background | timer repositioning copies | `wrapX` / `wrapShift` + `velocityX` |
33
33
  | Camera that follows the player | timer writing `cameraX` | `gameView.follow(sprite, options)` |
34
+ | A score, a title, a label | `Ti.UI.Label` overlays | `Game.createText({ text, screenFixed: true })` |
35
+ | A bullet fast enough to skip past a thin wall | shrinking the time step | `swept: true` on the bullet |
36
+ | To know the player *left* a zone | polling on a timer | the `collisionend` event |
34
37
  | Smoke, sparks, explosions | pooled sprites in JS | `Game.createEmitter(...)` |
35
38
  | A swinging chain or cape | verlet solver in JS | `Game.createRope(...)` |
36
39
  | Slow motion or a pause that still draws | pausing your own timers | `gameView.timeScale = 0.5` / `0` |
@@ -117,10 +120,14 @@ Gravity is per sprite, not global — set it on each body that should fall. A sp
117
120
  ## Block versus react: `solidWith` and `collidesWith` are independent
118
121
 
119
122
  - `solidWith: ['ground']` **blocks** movement against sprites tagged `collisionGroup: 'ground'`. The engine pushes the sprite out along the axis of least penetration, sets read-only `onGround` and fires `land`.
120
- - `collidesWith: ['coin', 'enemy']` **reports** overlap. Fires `collision` once per overlap-enter (payload `group`, `other`, `x`, `y`), re-arming after separation. Nothing is blocked.
123
+ - `collidesWith: ['coin', 'enemy']` **reports** overlap. Fires `collision` once per overlap-enter and `collisionend` once on separation (both carry `group`, `other`, `x`, `y`). Nothing is blocked.
121
124
 
122
125
  A sprite can use both — a player that stands on platforms (`solidWith`) and dies on spikes (`collidesWith`).
123
126
 
127
+ The enter/exit pair is the trigger lifecycle other engines call `OnTriggerEnter`/`Exit`: water that tints the hero while he is in it, a pressure plate holding a door open exactly while something rests on it. Removing or hiding the partner mid-contact also counts as separation, so a despawned object still closes its door. There is deliberately **no** per-frame "stay" event — keep that state in JS.
128
+
129
+ 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
+
124
131
  **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.
125
132
 
126
133
  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.
@@ -128,6 +135,7 @@ Tune fairness with `hitboxScale` (art rarely fills its frame; slightly small hit
128
135
  ## Performance rules that actually matter
129
136
 
130
137
  - **One texture = one draw call.** The batcher accumulates up to 1000 quads and flushes on texture switch. A scene whose sprites all share one sheet renders in a single draw call regardless of sprite count. Pack art into as few sheets as possible; reuse one white particle texture and tint it at runtime.
138
+ - **Know what else breaks the batch**, since none of it is visible on screen: every `'normal'` ↔ `'add'` transition in draw order flushes (group additive sprites into their own `zIndex` band so it happens twice a frame, not constantly); each glowing sprite costs 2 extra draw calls per frame, plus 2 more while a `flash()` runs; ropes, skid marks and debug overlays always draw with normal blending, so interleaving them with additive content flushes too.
131
139
  - **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.
132
140
  - **Keep collision group lists targeted.** Cost is O(colliders × candidates) per frame. A bullet checking `['asteroid']` tests 5 sprites, not the whole scene.
133
141
  - **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.
@@ -135,9 +143,19 @@ Tune fairness with `hitboxScale` (art rarely fills its frame; slightly small hit
135
143
  - **`maxFps: 60`** on the GameView keeps 120 Hz ProMotion displays from doubling render work. `0` (default) follows the display refresh rate.
136
144
  - **Judge performance on device.** The iOS Simulator's OpenGL translation layer is disproportionately slow and renders at a 1x drawable; real devices keep native screen scale and run far smoother.
137
145
 
138
- ## There is no text in the engine
146
+ ## Text is a sprite
147
+
148
+ Text draws **inside** the scene: `Game.createText({ text: 'SCORE 0' })` returns a sprite made of glyph quads, so it z-sorts, tweens, tints, flashes, glows, wobbles on `idleAnimation` and takes `tap` events like any other sprite — a text button needs no overlay view. With no `font` it uses a pixel font embedded in the module (nothing to ship); `Game.createFont({...})` loads a BMFont/AngelCode atlas or a monospace grid image. There is no font size — scale the sprite, with integer values for crisp pixels.
149
+
150
+ ```javascript
151
+ const score = Game.createText({ text: 'SCORE 0', screenFixed: true, x: W / 2, y: H * 0.08, scale: 3, zIndex: 100 });
152
+ gameView.add(score);
153
+ score.text = 'SCORE 10'; // re-lays out natively
154
+ ```
155
+
156
+ `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).
139
157
 
140
- The engine draws sprites, particles and ropes — **not text**. HUDs, scores and labels are ordinary `Ti.UI.Label` views overlaid on the window, on top of the GameView. That is how every official demo does it. Consequences worth knowing before designing a HUD: overlaid Titanium views are screen-fixed, so they do not scroll with the camera, do not z-sort against sprites and cannot be tweened by the engine. Bitmap-font text sprites are planned but do not exist — see [roadmap.md](references/roadmap.md).
158
+ 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.
141
159
 
142
160
  Standard Titanium touch events do not reach the GameView, but ordinary Titanium buttons and views layered on top work normally, and sibling views receive simultaneous pointers — which is how the demos build multitouch d-pads (hold ▶ and press jump at the same time). One caveat from the demos: Android's `touchFeedback` ripple cannot animate over this canvas and spams `RippleDrawable` errors, so on-screen buttons set their own `backgroundColor` on `touchstart`/`touchend`.
143
161
 
@@ -175,5 +193,6 @@ Read the one that matches the task instead of guessing — the API surface below
175
193
  5. Is `hitboxScale` set on sprites whose art does not fill the frame?
176
194
  6. Do triggers (scores, goals, kill zones) use sheet-less sprites instead of visible art?
177
195
  7. Is every `createSound`, `setInterval` and `setTimeout` stopped when the window closes?
178
- 8. Is the HUD a `Ti.UI.Label` overlay, not an imagined engine text API?
179
- 9. Are you using an API that actually exists? Anything involving text, tilemaps, sprite parenting, raycasting, joysticks or `blend: 'multiply'` — check [roadmap.md](references/roadmap.md) first.
196
+ 8. Is the HUD `Game.createText` with `screenFixed: true`, rather than a stack of overlaid labels?
197
+ 9. Does anything travel far enough per frame to tunnel? Give it `swept: true`.
198
+ 10. Are you using an API that actually exists? Anything involving tilemaps, sprite parenting, raycasting, joysticks or `blend: 'multiply'` — check [roadmap.md](references/roadmap.md) first.
@@ -1,6 +1,6 @@
1
1
  # ti.game API reference
2
2
 
3
- Complete JS surface of `ti.game` 0.3.0, 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.
3
+ Complete JS surface of `ti.game` as of upstream `main` on 2026-08-18, verified against the module source (`android/src/ti/game/*.java` and `engine/*.java`, mirrored by `ios/Classes/TG*`). Defaults come from the engine fields, not from prose. The manifest still reads `0.3.0`; the text engine (`createFont`/`createText`), `screenFixed`, `swept` and `collisionend` landed after it and are not in any numbered release yet.
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
 
@@ -10,6 +10,8 @@ Every property listed is **live**: reading returns the current native value, eve
10
10
  - [GameView](#gameview)
11
11
  - [SpriteSheet](#spritesheet)
12
12
  - [Sprite](#sprite)
13
+ - [Font](#font)
14
+ - [Text](#text)
13
15
  - [Sound](#sound)
14
16
  - [Emitter](#emitter)
15
17
  - [Rope](#rope)
@@ -27,6 +29,8 @@ const Game = require('ti.game');
27
29
  | `Game.createGameView(options)` | GameView — the canvas, a Titanium view |
28
30
  | `Game.createSpriteSheet(options)` | SpriteSheet — a texture cut into frames |
29
31
  | `Game.createSprite(options)` | Sprite |
32
+ | `Game.createFont(options)` | Font — a bitmap font for text sprites |
33
+ | `Game.createText(options)` | Text — a sprite that draws a string |
30
34
  | `Game.createSound(options)` | Sound |
31
35
  | `Game.createEmitter(options)` | Emitter — particle system |
32
36
  | `Game.createRope(options)` | Rope — Verlet chain |
@@ -112,6 +116,7 @@ Textures upload to the GPU lazily on first use, from the render thread, and are
112
116
  | `zIndex` | `0` | Draw order |
113
117
  | `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) |
114
118
  | `flipX`, `flipY` | `false` | Mirror the drawn frame only. Position, anchor, physics and hit testing are untouched |
119
+ | `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 |
115
120
  | `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 |
116
121
 
117
122
  ### Sheet and animation
@@ -145,7 +150,7 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
145
150
  | --- | --- | --- |
146
151
  | `velocityX`, `velocityY` | `0` | px/s |
147
152
  | `gravity` | `0` | px/s², added to `velocityY`. Per sprite — there is no global gravity |
148
- | `maxSpeed` | `500` | px/s cap |
153
+ | `maxSpeed` | `500` | px/s cap applied **only** to `thrust` acceleration and the `carMode` model. Writing `velocityX`/`velocityY` directly is never clamped |
149
154
  | `angularVelocity` | `0` | deg/s |
150
155
  | `thrust` | `0` | px/s² along the current heading (Newtonian flight) |
151
156
  | `wrapAround` | `false` | Re-enter from the opposite screen edge (Asteroids) |
@@ -169,6 +174,7 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
169
174
  | `collidesWith` | — | Array of groups this sprite reports overlaps with |
170
175
  | `hitboxScale` | `1` | Shrinks the collision box around the anchor |
171
176
  | `hitboxShape` | `'rect'` | `'circle'` — radius = half the smaller drawn side × `hitboxScale`. Circles resolve against solids along the contact normal (corner bounces) and get a round touch area |
177
+ | `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 |
172
178
  | `debug` | `false` | Draw this sprite's shapes: green = collision AABB, blue = touch bounds, orange dot = anchor |
173
179
 
174
180
  ### Car (`carMode`)
@@ -206,7 +212,7 @@ Every sprite gets its own phase, and the wobble unwinds exactly when disabled. T
206
212
  | --- | --- | --- |
207
213
  | `tintColor` | — | Multiplies the frame's colors (team colors, day/night, damage states). `null` or `'#fff'` = unchanged. Multiplicative, so it can only darken |
208
214
  | `glowColor` | — | Tinted, blurred silhouette drawn behind the sprite by a shader pass |
209
- | `glowBlur` | `0` | Blur radius in px; `0` = off |
215
+ | `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 |
210
216
  | `glowOpacity` | `1` | Halo strength 0..1 — tweenable via `animate`, so a glow can fade in without touching the blur |
211
217
  | `blend` | `'normal'` | `'add'` brightens the backdrop instead of covering it (glows, fire, lasers). Costs one batch flush per mode change — group additive sprites by `zIndex` |
212
218
 
@@ -237,8 +243,69 @@ Every sprite gets its own phase, and the wobble unwinds exactly when disabled. T
237
243
  | `animationcomplete` | `animation` | A non-looping sheet animation finished |
238
244
  | `complete` | final transform values | A tween finished |
239
245
  | `collision` | `group`, `other`, `x`, `y` | Overlap with a `collidesWith` group began |
246
+ | `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 |
240
247
  | `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) |
241
248
 
249
+ ## Font
250
+
251
+ A glyph atlas for text sprites. Create it once, outside `resize`, like a SpriteSheet — it holds a GL texture.
252
+
253
+ ```javascript
254
+ // 1. Built-in: nothing to ship. A 9x15 pixel font embedded in the module.
255
+ // You do not even need a Font object — createText without `font` uses it.
256
+ const builtin = Game.createFont({});
257
+
258
+ // 2. BMFont / AngelCode: .fnt text format or its JSON export, kerning included.
259
+ // Produced by BMFont, Hiero, fontbm or the module's tools/genfont.py.
260
+ const hud = Game.createFont({ font: 'assets/hud.fnt' });
261
+
262
+ // 3. Monospace grid image: cells row-major, ASCII 32..126 by default.
263
+ const mono = Game.createFont({ image: 'assets/mono.png', charWidth: 9, charHeight: 15 });
264
+ ```
265
+
266
+ | Option | Default | Notes |
267
+ | --- | --- | --- |
268
+ | `font` | — | Path to a `.fnt` descriptor (AngelCode text or JSON). The page image is loaded from **next to the descriptor** unless `image` overrides it |
269
+ | `image` | — | Grid mode: the glyph sheet. Also overrides a BMFont page path |
270
+ | `charWidth`, `charHeight` | — | **Required** in grid mode. Missing or ≤ 0 silently falls back to the built-in font |
271
+ | `characters` | ASCII 32..126 | Grid mode: which characters the cells map to, row-major |
272
+ | `smoothing` | `true` | Filters the glyph texture like a sheet. The built-in font is always crisp |
273
+ | `lineHeight` | — | Read-only: the font's natural line height in px |
274
+
275
+ 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.
276
+
277
+ ## Text
278
+
279
+ `Game.createText(options)` returns a **Sprite**. Everything in the Sprite section applies — `zIndex`, `ySort`, `animate()`, `flash()`, `tintColor`, glow, `idleAnimation`, `screenFixed`, touch events, `collidesWith`. Add it to the view like any sprite: `gameView.add(label)`.
280
+
281
+ ```javascript
282
+ const score = Game.createText({
283
+ text: 'SCORE 0',
284
+ x: 16,
285
+ y: 40,
286
+ anchorX: 0, // left-aligned to a margin
287
+ anchorY: 0,
288
+ scale: 3, // bitmap fonts size by scale, not by fontSize
289
+ screenFixed: true, // ignore the camera
290
+ zIndex: 100
291
+ });
292
+ gameView.add(score);
293
+ score.text = 'SCORE 10'; // re-lays out natively on the next frame
294
+ ```
295
+
296
+ | Property | Default | Notes |
297
+ | --- | --- | --- |
298
+ | `text` | `''` | The string. `\n` breaks lines. Writing it re-lays out natively |
299
+ | `font` | built-in | A Font object. Omitted or `null` = the embedded pixel font, which the scene assigns per GameView |
300
+ | `align` | `'left'` | `'center'` / `'right'` — how multiple lines align against each other. Unknown values fall back to `'left'` |
301
+ | `letterSpacing` | `0` | Extra px between glyphs; negative tightens |
302
+ | `lineSpacing` | `1` | Multiplier on the font's line height |
303
+ | `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 |
304
+
305
+ 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.
306
+
307
+ 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.
308
+
242
309
  ## Sound
243
310
 
244
311
  ```javascript
@@ -315,12 +382,14 @@ The tether yields at the end no finger owns: with a fixed head anchor the tail s
315
382
  Only these events exist. Nothing fires per frame, and events are only fired if a listener is registered.
316
383
 
317
384
  - **GameView**: `press`, `tap`, `release`, `resize`
318
- - **Sprite**: `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `collision`, `land`
319
- - **SpriteSheet, Sound, Emitter, Rope**: none
385
+ - **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `collision`, `collisionend`, `land`
386
+ - **SpriteSheet, Font, Sound, Emitter, Rope**: none
387
+
388
+ 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.
320
389
 
321
390
  ## Gotchas the property tables do not show
322
391
 
323
- - **`maxSpeed` defaults to 500 px/s and silently caps anything fast.** A bullet given `velocityX = 1400` travels at 500 unless you raise its own `maxSpeed`. The symptom is a projectile that looks sluggish on a large surface and fine on a small one, with nothing in the code hinting at a cap.
392
+ - **`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.
324
393
  - **`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.
325
394
  - **`follow()` resets its options.** See the GameView section.
326
395
  - **`animate()` ignores unknown keys.** Tweening `width`, `height` or `tintColor` silently does nothing — those are instant writes only.
@@ -328,6 +397,12 @@ Only these events exist. Nothing fires per frame, and events are only fired if a
328
397
  - **`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.
329
398
  - **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`.
330
399
  - **`music` is chosen at creation.** Creating an effect sound and flipping `music` later does not switch backends.
331
- - **`collision` fires once per overlap-enter**, re-arming only after the shapes separate. A sprite resting inside a trigger does not re-fire.
400
+ - **`collision` fires once per overlap-enter**, re-arming only after the shapes separate. A sprite resting inside a trigger does not re-fire — `collisionend` tells you when it left.
401
+ - **Pooling a sprite with `visible = false` fires `collisionend`** on whoever was touching it. That is usually what you want (a plate whose ball was despawned closes its door), but a handler that assumes separation means "the player walked away" will misfire.
402
+ - **The built-in pixel font only covers ASCII 32..126.** Em dashes, accents and `ñ` render as blanks or garbage — the module's own demos rewrote `—` as `-` for this reason. Ship a BMFont atlas (`tools/genfont.py` rasterizes any TTF) when the game needs accented Spanish text.
403
+ - **`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.
404
+ - **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
+ - **Text `width`/`height` are read-only in practice.** They report the laid-out block; to make text bigger use `scale`.
332
406
  - **`hitboxShape: 'circle'` also changes the touch area**, not just collisions.
333
407
  - **`blend: 'add'` costs a batch flush per mode change.** Group additive sprites on their own `zIndex` band.
408
+ - **`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.
@@ -9,6 +9,7 @@ Installing the module, wiring it into Classic and Alloy projects, organizing art
9
9
  - [Alloy projects](#alloy-projects)
10
10
  - [Where art and sound live](#where-art-and-sound-live)
11
11
  - [Sprite sheets and atlases](#sprite-sheets-and-atlases)
12
+ - [Fonts](#fonts)
12
13
  - [Lifecycle and cleanup](#lifecycle-and-cleanup)
13
14
  - [Debugging and tuning](#debugging-and-tuning)
14
15
  - [Platform differences](#platform-differences)
@@ -40,6 +41,8 @@ Then declare it in `tiapp.xml`. The iOS platform key is `iphone`, not `ios`:
40
41
 
41
42
  Pinning `version` is worth the extra attribute: with two module versions installed side by side, an unpinned entry silently picks the highest one.
42
43
 
44
+ The version number lies right now: the manifest has read `0.3.0` through several feature landings. A zip built before 2026-08-18 has no `createText`, no `createFont`, no `screenFixed`, no `swept` and no `collisionend` while still calling itself 0.3.0. If a call that this skill documents is `undefined` at runtime, rebuild the module from upstream `main` rather than hunting for a typo.
45
+
43
46
  Building the module from source:
44
47
 
45
48
  ```bash
@@ -50,7 +53,7 @@ ti build -p ios --build-only # macOS only → ios/dist/ti.game-iphone-<ve
50
53
 
51
54
  ## Classic projects
52
55
 
53
- `require('ti.game')` anywhere and add the GameView to a window. The 18 demos in the module's `example/` folder are Classic, each one a CommonJS module exporting a start function:
56
+ `require('ti.game')` anywhere and add the GameView to a window. The 21 demos in the module's `example/` folder are Classic, each one a CommonJS module exporting a start function:
54
57
 
55
58
  ```javascript
56
59
  // Resources/flappy.js
@@ -85,7 +88,6 @@ The standard Alloy element for a native module view. `method` names the factory
85
88
  <Alloy>
86
89
  <Window id="index" navBarHidden="true" exitOnClose="true" onOpen="onOpen" onClose="onClose">
87
90
  <Module id="gameView" module="ti.game" method="createGameView" backgroundColor="#8ed8f8" />
88
- <Label id="scoreLabel" />
89
91
  <Button id="jumpButton" title="JUMP" onTouchstart="jump" />
90
92
  </Window>
91
93
  </Alloy>
@@ -112,6 +114,8 @@ function onResize(e) {
112
114
  }
113
115
  ```
114
116
 
117
+ Only views that must be native — buttons, menus, dialogs — belong in the XML. The HUD does not: scores and labels are `Game.createText` sprites created in the controller and added to the game view, so they batch with the scene and can tween and flash.
118
+
115
119
  Style the view in TSS like any other component (`'#gameView': { width: Ti.UI.FILL, height: Ti.UI.FILL }`). The `<Module>` element itself is documented Alloy behavior; the pattern below is the one verified in a shipped ti.game app.
116
120
 
117
121
  ### B: an empty container in XML, the GameView created in JS
@@ -219,6 +223,28 @@ Practical rules:
219
223
 
220
224
  `frameCount` is `0` until a grid sheet's texture has loaded, so do not compute animation ranges from it at creation time.
221
225
 
226
+ ## Fonts
227
+
228
+ Nothing to ship for the default look: `Game.createText({ text: 'SCORE 0' })` uses a 9×15 pixel font embedded in the module, on both platforms.
229
+
230
+ A custom face is an asset like any other, created once outside `resize`:
231
+
232
+ ```javascript
233
+ // BMFont / AngelCode descriptor + its page image, side by side in assets/
234
+ const hud = Game.createFont({ font: 'fonts/hud.fnt' }); // hud.png resolved next to it
235
+
236
+ // or a monospace grid image
237
+ const mono = Game.createFont({ image: 'fonts/mono.png', charWidth: 9, charHeight: 15 });
238
+ ```
239
+
240
+ | File | Referenced as (Alloy) |
241
+ | --- | --- |
242
+ | `app/assets/fonts/hud.fnt` + `app/assets/fonts/hud.png` | `'fonts/hud.fnt'` |
243
+
244
+ These are **bitmap** fonts — do not put them in `app/assets/fonts/` expecting Titanium's native font handling, and do not use a `.ttf` here. To go from a TTF to either format, the module ships `tools/genfont.py`, which rasterizes it into a grid or BMFont atlas.
245
+
246
+ The built-in font covers ASCII 32..126 only. Any game whose text includes `ñ`, accents or `¿` needs a generated font — or a `Ti.UI.Label` overlay for that specific string.
247
+
222
248
  ## Lifecycle and cleanup
223
249
 
224
250
  The engine handles what it owns: the render loop pauses and resumes with the activity or app, music pauses and resumes with it, and textures survive an EGL context loss. **Everything you created in JS is yours to stop.**
@@ -1,13 +1,13 @@
1
1
  # ti.game recipes by genre
2
2
 
3
- Working patterns distilled from the 18 official demos in `example/` and from a shipped Alloy game (Titanium Lander). Each recipe shows the part that is specific to the genre; the shared scaffolding is in [The scaffolding](#the-scaffolding) and is not repeated.
3
+ Working patterns distilled from the 21 official demos in `example/` and from a shipped Alloy game (Titanium Lander). Each recipe shows the part that is specific to the genre; the shared scaffolding is in [The scaffolding](#the-scaffolding) and is not repeated.
4
4
 
5
- Every snippet uses only API that exists in 0.3.0. Check [roadmap.md](roadmap.md) before reaching for anything not shown here.
5
+ Every snippet uses only API that exists in upstream `main` as of 2026-08-18. 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, invisible triggers, hit-stop
10
+ - [Cross-cutting patterns](#cross-cutting-patterns) — pooling, parallax, multitouch controls, HUD text, invisible triggers, enter/exit zones, 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`)
@@ -91,7 +91,7 @@ for (let i = 0; i < 10; i++) {
91
91
  height: 16,
92
92
  visible: false,
93
93
  wrapAround: true,
94
- hitboxScale: 1.4, // generous — bolts are small and fast
94
+ swept: true, // path-tested: a fast bolt cannot skip past a rock
95
95
  collidesWith: ['asteroid']
96
96
  });
97
97
  const state = { sprite: sprite, active: false, generation: 0 };
@@ -135,6 +135,10 @@ function fire(x, y, vx, vy) {
135
135
 
136
136
  The `generation` counter matters: without it, a timeout fired for an old shot deactivates the *new* bullet occupying that slot.
137
137
 
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.
139
+
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
+
138
142
  ### Seamless parallax
139
143
 
140
144
  Two screen-wide copies per layer, scrolled by native velocity. `wrapX`/`wrapShift` teleport a copy that left the screen back behind its sibling. No JS runs in the loop.
@@ -220,24 +224,37 @@ bindHold(leftButton, () => {
220
224
 
221
225
  The guard in the release handler is what keeps two opposite buttons from fighting: releasing left only zeroes steering if left is still the active direction.
222
226
 
223
- ### HUD
227
+ ### HUD — `text.js`, `flappy.js`
224
228
 
225
- There is no text in the engine. Overlay `Ti.UI.Label` views on the window, above the GameView.
229
+ Text sprites with `screenFixed: true`. They live in the scene — one batch, no overlay views — but ignore the camera, so a score stays in the corner while the world scrolls under it.
226
230
 
227
231
  ```javascript
228
- const scoreLabel = Ti.UI.createLabel({
229
- text: '0',
230
- color: '#fff',
231
- font: { fontSize: 28, fontWeight: 'bold' },
232
- shadowColor: '#000',
233
- shadowOffset: { x: 0, y: 2 },
234
- top: 40
235
- });
236
- win.add(gameView);
237
- win.add(scoreLabel); // added after the game view, so it draws on top
232
+ // Created outside resize (no size known yet), positioned once it is
233
+ const scoreLabel = Game.createText({ text: '0', screenFixed: true, zIndex: 100 });
234
+ const statusLabel = Game.createText({ text: 'Tap to start!', screenFixed: true, zIndex: 100 });
235
+
236
+ function buildLevel(W, H) {
237
+ const TEXT_SCALE = Math.max(1, Math.round(W / 240)); // integer: crisp pixels
238
+ Object.assign(scoreLabel, { scale: TEXT_SCALE, x: W / 2, y: H * 0.08 });
239
+ Object.assign(statusLabel, { scale: TEXT_SCALE, x: W / 2, y: H * 0.45 });
240
+ gameView.add([scoreLabel, statusLabel]);
241
+ // ...
242
+ }
243
+
244
+ function score() {
245
+ points += 1;
246
+ scoreLabel.text = points + ' / ' + GATE_COUNT;
247
+ scoreLabel.flash('#4dff88', 250); // text flashes like any sprite
248
+ }
238
249
  ```
239
250
 
240
- The shadow is not decoration — it keeps the label readable over any background the scene happens to scroll underneath.
251
+ Scaling from `W` is what replaces `fontSize`: a bitmap font has one native size, and an integer multiplier is the only way to keep texels square. `Math.max(1, ...)` guards the smallest screens.
252
+
253
+ Because a label is a sprite, the HUD gets the whole toolbox for free — `animate()` to pop a score on change, `tintColor` per state, `glowColor` on a title, `idleAnimation` for a gentle wobble, and `tap` handlers so a `[ RESET ]` label works as a button with no Titanium view behind it.
254
+
255
+ Drop `screenFixed` and the same label becomes world furniture: signposts, floating damage numbers, a name over an NPC — it scrolls with everything else.
256
+
257
+ 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.
241
258
 
242
259
  ### Invisible triggers
243
260
 
@@ -269,6 +286,37 @@ car.addEventListener('collision', (e) => {
269
286
  });
270
287
  ```
271
288
 
289
+ ### Zones you can be inside of (enter / exit) — `zones.js`
290
+
291
+ `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.
292
+
293
+ ```javascript
294
+ const pool = Game.createSprite({
295
+ x: W * 0.32, y: H * 0.3, width: W * 0.44, height: H * 0.18,
296
+ sheet: waterSheet, tintColor: '#3d6fd4', opacity: 0.35,
297
+ touchEnabled: false, // drags pass through to whatever is inside
298
+ collisionGroup: 'water'
299
+ });
300
+
301
+ hero.collidesWith = ['water'];
302
+ hero.addEventListener('collision', (e) => {
303
+ if (e.group === 'water') {
304
+ hero.tintColor = '#7fb2ff';
305
+ swimming = true;
306
+ }
307
+ });
308
+ hero.addEventListener('collisionend', (e) => {
309
+ if (e.group === 'water') {
310
+ hero.tintColor = null;
311
+ swimming = false;
312
+ }
313
+ });
314
+ ```
315
+
316
+ The listener goes on the sprite that declares `collidesWith`. For a pressure plate holding a door open exactly while a crate rests on it, that is the **plate** (`collidesWith: ['crate']`), not the crate — then `collision` opens the door and `collisionend` closes it, with no polling anywhere.
317
+
318
+ 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
+
272
320
  ### Hit-stop on impact
273
321
 
274
322
  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.)
@@ -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 0.3.0**, with the workaround that is idiomatic today.
3
+ The most expensive mistake with `ti.game` is writing code against a feature that sounds like it should be there because Phaser, Godot or Cocos has it. This file is the guard rail: everything below is **absent from upstream `main` as of 2026-08-18**, 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,14 +8,12 @@ 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
- | `Game.createText()`, bitmap fonts, text sprites | Planned (priority 5) | Overlay `Ti.UI.Label` views on the window, above the GameView. Costs: labels are screen-fixed, so they do not scroll with the camera, do not z-sort against sprites and cannot be tweened by the engine |
12
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 |
13
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 |
14
13
  | `gameView.raycast(...)`, line-of-sight, ground probes | Planned (priority 3) | Compare positions in JS on a decision timer, or place an invisible trigger sprite where the probe would land and listen for `collision` |
15
14
  | 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 |
16
15
  | Gamepad support | Planned (priority 6) | Nothing native. Touch controls only |
17
16
  | `blend: 'multiply'` or `'screen'` | Planned (priority 2) | Only `'normal'` and `'add'` exist. For darkening, use `tintColor` (multiplicative) on the sprite itself |
18
- | Swept AABB so fast bullets stop tunneling | Planned (priority 3) | Cap bullet speed, or enlarge the hitbox along the travel axis with `hitboxScale` — the asteroids demo uses `hitboxScale: 1.4` on its bolts for exactly this |
19
17
  | Slopes in platformer terrain | Planned (priority 3), conditional | Build slopes as stepped rectangles, or keep terrain flat |
20
18
  | 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 |
21
19
  | `play('attack', { then: 'idle' })` chaining | Planned (priority 9) | Handle `animationcomplete` and call `play()` from it |
@@ -49,7 +47,11 @@ Recent additions that are easy to miss and easy to reimplement badly in JS:
49
47
  - `createSound` with separate low-latency effect and streaming music backends.
50
48
  - `timeScale`, `maxFps`, `flipX`/`flipY`, `pixelSnap`, `idleAnimation`, `ySort`, `glowColor`/`glowBlur`/`glowOpacity`.
51
49
  - `gameView.add([array])` committing a whole level in one bridge crossing.
50
+ - **Text is in the engine now** (2026-08-18): `createText` glyph sprites with an embedded pixel font, `createFont` for BMFont/AngelCode or monospace grid atlases, `align`/`letterSpacing`/`lineSpacing`, and `tools/genfont.py` to rasterize a TTF. Text objects are sprites — they tween, tint, flash, glow, z-sort and take taps. Do **not** fall back to `Ti.UI.Label` overlays for scores.
51
+ - **`screenFixed`** on any sprite: surface coordinates, camera position/zoom/shake ignored, touch mapped back. HUDs no longer need overlay views.
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
+ - **`collisionend`**: the exit half of the trigger lifecycle, including when the partner is removed, hidden or re-tagged mid-contact.
52
54
 
53
55
  ## Checking the current state
54
56
 
55
- This file describes 0.3.0. 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.
57
+ This file describes upstream `main` on 2026-08-18 — a moving target: four items on this list shipped within two days of the skill being written, and the manifest version (`0.3.0`) did not change with them. Before relying on any "planned" item being still absent — or on a workaround still being necessary — read `TODO.md` and `README.md` at https://github.com/m1ga/ti.game, the module's upstream repo. `README.md` there is the canonical API documentation; the `documentation/` folder only points at it. The priorities quoted above are the maintainer's, not a roadmap this skill decides.