@maccesar/titools 4.15.0 → 4.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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, shaped solids, collisions, raycasts, pathfinding, text HUDs, sprite attachment, particles, camera |
180
+ | ti-game | 2D games with the `ti.game` module | Sprites, physics, tile layers, wall jumps, pathfinding, text/performance HUDs, particles, camera |
181
181
  | ti-api | Complete Titanium API reference | Looking up properties, methods, events |
182
182
  | ti-guides | SDK fundamentals | tiapp.xml, Hyperloop, distribution, JDK/Xcode compatibility, release notes |
183
183
  | ti-howtos | Native feature integration | Location, push, media, platform APIs |
@@ -436,6 +436,7 @@ When it activates:
436
436
  - Sprites, sprite sheets, TexturePacker atlases, frame animations, native tweens
437
437
  - Physics: gravity and velocity, solid platforms, one-way floors, bouncing, the arcade car model, Newtonian thrust
438
438
  - Collision groups, invisible trigger zones, enter/exit events, swept AABB for fast bullets, per-axis hitbox tuning with the `debug` overlays
439
+ - Native side-wall state, `wallhit` transitions and `wallSlideSpeed` for wall jumps/slides without a JS frame loop
439
440
  - Raycasts for line of sight, ledge probes and hitscan shots
440
441
  - A* pathfinding (`findPath`) around tagged obstacles, feeding native patrol paths (`followPath`) and animation chaining (`play(name, { then })`)
441
442
  - Bitmap-font text sprites with native word wrap, and screen-fixed HUDs inside the scene
@@ -443,6 +444,8 @@ When it activates:
443
444
  - Named anchors and percentage strings (`anchor: 'bottom'`, `hitboxScaleY: '55%'`) wherever the engine takes a ratio
444
445
  - Particle emitters, Verlet ropes, camera follow/zoom/shake/effects, `scrollFactor` parallax, low-latency sound
445
446
  - Game-clock timers that obey pause and slow motion, texture `unload()` for level streaming, and what LiveView reloads clean up on their own
447
+ - Native `TileLayer` maps with visible-cell rendering, Tiled GIDs, solid/one-way cells, live edits and A* integration
448
+ - Collision-shape overlays plus the expandable performance HUD and opt-in `performance` telemetry event
446
449
  - Drag & drop, pinch, rotate, multitouch controls
447
450
 
448
451
  Example prompts:
@@ -455,15 +458,21 @@ Example prompts:
455
458
  "Add a score HUD that stays put while the camera follows the player."
456
459
  "Make the guard walk a patrol route and turn around before the ledge."
457
460
  "Tap to walk, but the player should go around the tree instead of through it."
461
+ "Render a 200x200 Tiled map without creating one sprite per tile."
462
+ "Show FPS, p95 frame time, draw calls and texture switches while I tune this scene."
463
+ "Add wall jumps and a slow wall slide without polling collisions."
458
464
  ```
459
465
 
460
466
  Key rules:
461
467
  - JS describes the scene and reacts to events; the engine runs every frame. Never move a sprite from a timer — use velocity, gravity, a tween, `carMode` or `thrust`
462
468
  - Build the level inside a guarded `resize` handler, never from `Ti.Platform.displayCaps`
463
469
  - `solidWith` blocks, `collidesWith` reports — they are independent
470
+ - Gate wall jumps on `onWallLeft` / `onWallRight`; reserve `wallhit` for transition effects
464
471
  - A sprite with size but no sheet is an invisible trigger (scores, goals, walls)
465
472
  - Add a whole level with one `gameView.add([...])` call
473
+ - Use `Game.createTileLayer()` for large/static grids; keep sprites for interactive cells and actors
466
474
  - HUDs are `Game.createText` sprites with `screenFixed: true`, not `Ti.UI.Label` overlays
475
+ - `debug: true` means collision shapes; use `debug: { hud: true }` or `performance` for telemetry
467
476
  - Timers that drive game logic belong on the game clock (`gameView.every`), so a paused game stops spawning
468
477
 
469
478
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "4.15.0",
3
+ "version": "4.16.1",
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,56 +1,61 @@
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/iOS) — sprites, sheets, animations, tweens, physics (gravity, gravityX, solids, solidMode, linearDamping, carMode, thrust), collision groups, trigger zones, hitbox shapes (circle, rotatedRect), swept, raycast, paths, bitmap text, particles, ropes, camera, parallax, sound, drag & drop, multitouch, attachments. 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, spritesheet, platformer, endless runner, top-down, shooter, racer, flappy, breakout, rhythm, cards, puzzle, hitbox, attachTo, slope, ramp, pool, plinko, bounce, friction, pathfinding, maze, patrol, HUD, score, game loop, GameView. ti.game is NOT Phaser, Godot or Box2D: you never write a game loop nor move a sprite from setInterval, and features like tilemaps, parent/child transforms and gamepads do not exist yet — check references/roadmap.md before inventing an API."
4
- argument-hint: "[genre or feature]"
3
+ description: "Use when a Titanium app declares ti.game in tiapp.xml, imports require('ti.game'), or needs a 2D game built or reviewed with that native engine: sprites, native movement, collisions, tile layers, camera, HUD, audio, particles, or touch. Covers Alloy and Classic on Android/iOS. Do not use for unrelated game engines or ordinary Titanium UI."
5
4
  allowed-tools: Read, Grep, Glob, Edit, Write, Bash(node *)
6
5
  ---
7
6
 
8
- # ti.game — 2D game engine for Titanium SDK
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-27**, manifest **`0.5.0`** (`ee69056`) — and that number has meant different things at different times. Under `0.3.0` the engine gained the text engine, `screenFixed`, `swept` collisions, `collisionend`, `followPath`, animation chaining, `raycast`, `findPath`, game-clock timers, `scrollFactor` parallax and the `multiply`/`screen` blend modes, so anything still calling itself 0.3.0 has none of them. Under `0.4.0` it gained per-axis hitboxes, text word wrap, `unload()` on sheets and fonts, the LiveView renderer lifecycle (2026-08-23), then `attachTo`/`detach`, inherited opacity on attachments and named/percentage values (2026-08-26). `0.5.0` is the first bump that carries a feature set of its own: shape-aware solids and lightweight circle physics — `hitboxShape: 'rotatedRect'`, circular solids, `solidMode`, `gravityX`, `linearDamping` and two-sided `restitution`. 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
-
12
- Works in **both Classic and Alloy** projects. Nothing here depends on PurgeTSS.
13
-
14
- ## The one rule everything follows
15
-
16
- **JS describes the scene and reacts to events. The engine runs every frame.**
17
-
18
- The Kroll bridge (JS ↔ native) is far too slow for per-frame work, so the API is a *scene description*: setting `sprite.x`, `sprite.velocityY` or `car.throttle` writes a volatile field the render thread reads on the next tick. Events flow back only when something discrete happens — `tap`, `collision`, `land`, `complete`. Nothing fires per frame by design.
19
-
20
- The practical consequence, and the single biggest mistake to avoid:
21
-
22
- > If you are about to move a sprite from a `setInterval` or a `requestAnimationFrame`, stop and find the native property that does it. Coarse timers for *decisions* (an AI picking a direction every 80–150 ms, an autofire cadence, a spawn schedule) are fine and the official demos use them. Timers for *motion* are not.
23
-
24
- | You want | Do NOT write | Use |
25
- | --- | --- | --- |
26
- | A sprite that falls | `setInterval(() => s.y += 5)` | `s.gravity = 2000` |
27
- | A sprite that moves at constant speed | timer updating `x` | `s.velocityX = 300` |
28
- | Move from A to B over time | manual easing loop | `s.animate({ x, y, duration, easing })` |
29
- | A car that drives and drifts | your own physics | `carMode: true` + `throttle`/`steering` |
30
- | A ship with inertia | vector math per frame | `thrust` + `angularVelocity` |
31
- | Drag a sprite with the finger | `touchmove` handler | `draggable: true` |
32
- | Scrolling background under a fixed camera | timer repositioning copies | `wrapX` / `wrapShift` + `velocityX` |
33
- | Parallax layers under a moving camera | hand-scrolled layers | `scrollFactor` on each layer |
34
- | Camera that follows the player | timer writing `cameraX` | `gameView.follow(sprite, options)` |
35
- | A score, a title, a label | `Ti.UI.Label` overlays | `Game.createText({ text, screenFixed: true })` |
36
- | A name tag, health bar or turret that rides a sprite | a timer copying `owner.x`/`y` onto it | `tag.attachTo(owner, { offsetY: -40 })` |
37
- | A dialog box that fits the screen | breaking the lines with `\n` after measuring in JS | `maxWidth` on the text sprite |
38
- | A bullet fast enough to skip past a thin wall | shrinking the time step | `swept: true` on the bullet |
39
- | To know the player *left* a zone | polling on a timer | the `collisionend` event |
40
- | An enemy on a fixed patrol route | chained `animate()` legs | `sprite.followPath(points, { loop })` |
41
- | Play an attack, then go back to idle | an `animationcomplete` handler | `play('attack', { then: 'idle' })` |
42
- | Line of sight, a ledge probe, a hitscan shot | an invisible probe sprite | `gameView.raycast(x0, y0, x1, y1, ['group'])` |
43
- | Walking around obstacles to a tapped point | your own A* in JS, or a straight line through the wall | `gameView.findPath(from, to, opts)` → `sprite.followPath(path)` |
44
- | A spawn wave or an AI tick that respects pause | `setInterval` | `gameView.every(ms, cb)` |
45
- | Smoke, sparks, explosions | pooled sprites in JS | `Game.createEmitter(...)` |
46
- | A swinging chain or cape | verlet solver in JS | `Game.createRope(...)` |
47
- | A tilted ramp a crate slides down | a staircase of small rectangles | `rotation` + `hitboxShape: 'rotatedRect'` on the ramp |
48
- | A round post or peg that deflects | a box solid and a JS correction | `hitboxShape: 'circle'` on the **solid**, not just on the ball |
49
- | Balls that knock each other around | separating pairs by hand in a loop | `solidMode: 'push'` on both, each listing the other's group |
50
- | A drum, bowl or cage holding balls in | a ring of wall sprites around the rim | one circle solid with `solidMode: 'contain'` |
51
- | A ball that rolls to a stop | decrementing `velocityX` on a timer | `linearDamping` |
52
- | Wind, or a conveyor pull | `velocityX -= k` every tick | `gravityX` |
53
- | Slow motion or a pause that still draws | pausing your own timers | `gameView.timeScale = 0.5` / `0` |
7
+ # ti.game — native 2D games for Titanium SDK
8
+
9
+ `ti.game` is Michael Gangolf's OpenGL ES 2.0 sprite engine for Titanium. JavaScript describes the scene and reacts to discrete events; Android and iOS run rendering, animation, physics, collision, gestures, audio, particles, ropes, tile layers, camera work, and telemetry natively.
10
+
11
+ This skill is verified against upstream `main` at **`c216e7f` (2026-08-28)**. Both manifests still say **`0.5.0`**, although important features landed after that version was introduced. Treat the commit or build date as the capability marker; feature-detect an installed build when the exact artifact is unknown.
12
+
13
+ The skill applies to **Classic and Alloy** projects. Nothing here requires PurgeTSS.
14
+
15
+ ## Required workflow
16
+
17
+ Open the references relevant to the task before proposing code. Do not answer from familiarity with Phaser, Godot, Box2D, or an older `ti.game` build.
18
+
19
+ | Task | Read first |
20
+ | --- | --- |
21
+ | Exact methods, properties, defaults, event payloads, percentages | [references/api.md](references/api.md) |
22
+ | Install, Classic/Alloy structure, assets, cleanup, lifecycle | [references/project-setup.md](references/project-setup.md) |
23
+ | Choose a runnable upstream example | [references/examples.md](references/examples.md) |
24
+ | Reusable level, pooling, HUD, collision, path, timer patterns | [references/patterns.md](references/patterns.md) |
25
+ | Platformer, racer, shooter, runner, rhythm, ball-game recipes | [references/recipes-action.md](references/recipes-action.md) |
26
+ | Cards, point-and-click, particles, ropes, camera, effects | [references/recipes-adventure-effects.md](references/recipes-adventure-effects.md) |
27
+ | Large maps, Tiled data, tile collision, live edits, pathfinding | [references/tilemaps.md](references/tilemaps.md) |
28
+ | Hitbox overlays, performance HUD, telemetry, tuning | [references/debugging-performance.md](references/debugging-performance.md) |
29
+ | Missing, partial, and deliberately out-of-scope features | [references/roadmap.md](references/roadmap.md) |
30
+
31
+ In the response, cite factual guidance with `[source: references/<file>.md]`. If a relevant reference cannot be consulted, prefix the claim with `FROM_MEMORY (unverified):` instead of silently filling the gap.
32
+
33
+ The module's demos are integration evidence, not production templates. Preserve the API combination they prove, but remove launcher plumbing, repeated navigation controls, and avoidable global state when adapting them.
34
+
35
+ ## The engine rule
36
+
37
+ **JS describes and reacts. The engine runs every frame.**
38
+
39
+ Do not move sprites from `setInterval`, `requestAnimationFrame`, or a JS loop. Property writes cross the Kroll bridge; native properties and methods keep frame-by-frame work on the render thread. Coarse timers are appropriate for decisions such as enemy targeting or spawn cadence, preferably through `gameView.after()` / `every()` when they must obey pause and `timeScale`.
40
+
41
+ | Intent | Native mechanism |
42
+ | --- | --- |
43
+ | Fall or move continuously | `gravity`, `gravityX`, `velocityX`, `velocityY` |
44
+ | Move to a point | `animate()` or `followPath()` |
45
+ | Drive or fly | `carMode` + `throttle`/`steering`, or `thrust` + `angularVelocity` |
46
+ | Drag, pinch, rotate | `draggable`, `pinchable`, `rotatable` |
47
+ | Follow the player | `gameView.follow()` |
48
+ | Walk around obstacles | `findPath()` → `followPath()` |
49
+ | Test line of sight or hitscan | `raycast()` |
50
+ | Fast projectile collision | `swept: true` on the mover |
51
+ | Detect a side wall or slide down it | `onWallLeft` / `onWallRight`, `wallhit`, `wallSlideSpeed` |
52
+ | Repeat a large map efficiently | `Game.createTileLayer()` |
53
+ | Attach a label, health bar, or turret | `attachTo()` |
54
+ | Score or in-game label | `Game.createText()`; usually `screenFixed: true` for a HUD |
55
+ | Smoke, sparks, explosions | `Game.createEmitter()` |
56
+ | Chain, tether, cape | `Game.createRope()` |
57
+ | Pause or slow the scene while still drawing | `gameView.timeScale` |
58
+ | Measure FPS, frame time, and draw calls | `debug: { hud: true }` or the `performance` event |
54
59
 
55
60
  ## Minimal scene
56
61
 
@@ -59,34 +64,31 @@ const Game = require('ti.game');
59
64
 
60
65
  const win = Ti.UI.createWindow({
61
66
  backgroundColor: '#000',
62
- navBarHidden: true, // iOS: no nav bar
63
- theme: 'Theme.Titanium.DayNight.NoTitleBar' // Android: no title bar
67
+ navBarHidden: true,
68
+ theme: 'Theme.Titanium.DayNight.NoTitleBar'
69
+ });
70
+ const gameView = Game.createGameView({
71
+ backgroundColor: '#8ed8f8',
72
+ maxFps: 60
64
73
  });
65
- const gameView = Game.createGameView({ backgroundColor: '#8ed8f8' });
66
-
67
74
  const sheet = Game.createSpriteSheet({
68
75
  image: 'assets/hero.png',
69
76
  frameWidth: 32,
70
77
  frameHeight: 48,
71
- smoothing: false // nearest-neighbor — crisp pixel art
78
+ smoothing: false
72
79
  });
73
80
 
74
81
  let initialized = false;
75
- gameView.addEventListener('resize', (e) => {
82
+ gameView.addEventListener('resize', ({ width, height }) => {
76
83
  if (initialized) {
77
84
  return;
78
85
  }
79
86
  initialized = true;
80
- buildLevel(e.width, e.height);
81
- });
82
87
 
83
- function buildLevel(W, H) {
84
88
  const hero = Game.createSprite({
85
- sheet: sheet,
86
- x: W / 2,
87
- y: H / 2,
88
- width: 96,
89
- height: 144,
89
+ sheet,
90
+ x: width / 2,
91
+ y: height / 2,
90
92
  animations: {
91
93
  idle: { frames: [0], fps: 1, loop: true },
92
94
  walk: { frames: [1, 2], fps: 6, loop: true }
@@ -94,150 +96,41 @@ function buildLevel(W, H) {
94
96
  });
95
97
  hero.play('idle');
96
98
  gameView.add(hero);
97
- }
99
+ });
98
100
 
99
101
  win.add(gameView);
100
102
  win.open();
101
103
  ```
102
104
 
103
- ### Build the level on `resize`, never from `displayCaps`
104
-
105
- `Ti.Platform.displayCaps` includes the system bars and reports **points** on iOS, while the scene lives in surface **pixels**. Sizing a level from it puts bottom-anchored sprites below the visible area. The GameView fires `resize` with the real surface size — that is the coordinate space. The guard flag matters: `resize` fires again on rotation and on some lifecycle transitions, and rebuilding the level there would duplicate every sprite.
106
-
107
- When you genuinely need dp→scene units (to keep a floor above 80dp on-screen buttons), measure the surface rather than trusting the density factor, because the iOS Simulator renders at 1x while still reporting the device scale:
108
-
109
- ```javascript
110
- const density = Ti.Platform.osname === 'android'
111
- ? Ti.Platform.displayCaps.logicalDensityFactor
112
- : H / Ti.Platform.displayCaps.platformHeight;
113
- const buttonZone = Math.round(130 * density);
114
- const groundTop = H - buttonZone;
115
- ```
116
-
117
- ### Coordinates
118
-
119
- Top-left origin, y-down, surface pixels. Touch coordinates map 1:1. `(x, y)` is the sprite's **anchor** (default `0.5/0.5` = center, so `y` is the middle of the sprite, not its top). Rotation in degrees, positive clockwise. All JS durations are milliseconds.
120
-
121
- ## Pick a movement model
122
-
123
- | Model | Properties | Fits |
124
- | --- | --- | --- |
125
- | Plain velocity | `velocityX/Y` (px/s), `gravity` (px/s²) | Flappy, projectiles, falling objects, notes |
126
- | Platformer | velocity + `solidWith`, read-only `onGround`, `land` event | Mario-style run & jump |
127
- | Bouncing body | `restitution` (0..1) on **either** side of the contact, on top of `solidWith` | Balls, pinball, breakout |
128
- | Rolling body | `hitboxShape: 'circle'` + `linearDamping`, and `solidMode: 'push'` when they hit each other | Pool, plinko, a bingo drum |
129
- | Car | `carMode: true`, `throttle`, `steering` (-1..1) | Top-down racer with emergent drift |
130
- | Newtonian flight | `thrust` (px/s² along heading), `angularVelocity` (deg/s), `maxSpeed` | Asteroids, lunar lander |
131
- | Tween | `animate({ x, y, duration, easing })` | Point-and-click walking, UI cards, patrols |
132
-
133
- Gravity is per sprite, not global — set it on each body that should fall. A sprite with no `gravity` and no velocity simply stays put.
134
-
135
- ## Block versus react: `solidWith` and `collidesWith` are independent
136
-
137
- - `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`.
138
- - `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.
139
-
140
- A sprite can use both — a player that stands on platforms (`solidWith`) and dies on spikes (`collidesWith`).
141
-
142
- 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.
143
-
144
- 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.
145
-
146
- When the question is not "did we touch?" but "what is over there?", the answer is `gameView.raycast(x0, y0, x1, y1, ['group'])` — line of sight, a ledge probe ahead of a walker's feet, a hitscan shot. It returns `null` or the nearest hit (`{ x, y, distance, group, sprite, normal }`), needs nothing but a `collisionGroup` on the targets, and replaces the old trick of parking an invisible probe sprite where the ray would land. It is a discrete query: call it from a decision timer or a tap handler, never every frame.
147
-
148
- And when the question is "how do I get over there?", `gameView.findPath(from, to, { cellSize, groups, clearance, bounds })` runs grid A* around the same tagged sprites and hands back waypoints for `followPath` — tap-to-walk that rounds a tree, a hound re-routing to the player on an 800 ms timer. Also discrete: the walk itself runs natively between queries, so pathfind on taps and AI ticks, never per frame. Pass `clearance` of about half the walker's width, since the route is a line for the sprite's **center**.
149
-
150
- **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.
151
-
152
- 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. Since 2026-08-27 the **solid's** shape counts too: a `'circle'` on the post makes the normal run centre to centre, and `'rotatedRect'` keeps a tilted platform's box turned with its art instead of re-boxing it square to the screen — which is what lets a crate slide down a ramp rather than stand on an invisible ledge. Two more properties live on the solid: `solidMode: 'contain'` holds matched circles *inside* a circular boundary with no ring of wall sprites, and `solidMode: 'push'` makes a pair of circles bodies that split the separation and trade momentum. `'push'` needs both sides to agree — both circles, both `'push'`, and each listing the other's `collisionGroup` — or it silently degrades to one ball shoving an immovable one. 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.
153
-
154
- The hitbox shrinks **around the anchor**, which is why the anchor is half of that tuning and why both now have readable forms: `anchor: 'bottom'` with `hitboxScaleY: '55%'` is the same sprite as `anchorY: 1` with `hitboxScaleY: 0.55`, and says what it is doing. Every ratio the engine exposes takes a percentage string, and the anchors take names (`'left'`, `'bottom-right'`, `'center'`); numbers keep working untouched. [api.md](references/api.md) lists exactly which properties are ratios — coordinates, sizes, degrees and speeds are not, and neither is the car model's `grip`/`drag`.
155
-
156
- ## Performance rules that actually matter
157
-
158
- - **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.
159
- - **Know what else breaks the batch**, since none of it is visible on screen: every blend-mode transition in draw order flushes — there are four (`'normal'`, `'add'`, `'multiply'`, `'screen'`), so group same-blend sprites into their own `zIndex` band and the frame switches a handful of times instead of constantly; each glowing sprite costs 2 extra draw calls per frame, plus 2 more while a `flash()` runs; ropes, skid marks and debug overlays always draw with normal blending, so interleaving them with non-normal content flushes too.
160
- - **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.
161
- - **Keep collision group lists targeted.** Cost is O(colliders × candidates) per frame. A bullet checking `['asteroid']` tests 5 sprites, not the whole scene.
162
- - **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.
163
- - **Read properties in handlers, not in loops.** `ball.x` is a synchronous snapshot of the live native value — perfect inside a `collision` handler or an 80 ms AI tick, wasteful in a tight loop.
164
- - **`maxFps: 60`** on the GameView keeps 120 Hz ProMotion displays from doubling render work. `0` (default) follows the display refresh rate.
165
- - **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.
166
-
167
- ## Text is a sprite
168
-
169
- 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.
170
-
171
- ```javascript
172
- const score = Game.createText({ text: 'SCORE 0', screenFixed: true, x: W / 2, y: H * 0.08, scale: 3, zIndex: 100 });
173
- gameView.add(score);
174
- score.text = 'SCORE 10'; // re-lays out natively
175
- ```
176
-
177
- 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.
178
-
179
- `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).
180
-
181
- A label that belongs *on* a moving sprite is neither: `tag.attachTo(hero, { offsetY: -40 })` pins it natively, resolved every frame after physics and before collisions, so it never lags by a frame and needs no JS. It works on any sprite, not just text — health bars, shadows, an invisible hitbox on a body, and with `rotate: true` a turret whose offset swings with the tank. Removing the owner removes what is attached to it, and fading the owner fades them with it: position, rotation and opacity are inherited. Scale, visibility and tint are not — set those per sprite.
182
-
183
- 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.
184
-
185
- 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`.
186
-
187
- ## Clean up what you created
105
+ ## Non-negotiable invariants
188
106
 
189
- 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()`.
107
+ - Build surface-dependent content from the GameView's guarded `resize` event. `Ti.Platform.displayCaps` is not the scene surface and can include system bars or different units.
108
+ - Coordinates use a top-left origin, y increases downward, and `(x, y)` is the sprite anchor. Durations crossing the JS boundary are milliseconds.
109
+ - `solidWith` blocks; `collidesWith` reports `collision`/`collisionend`. They are independent. A sized sprite without a sheet is a valid invisible solid or trigger.
110
+ - Put the hitbox shape on the object whose geometry matters. A round peg needs `hitboxShape: 'circle'`; a ramp needs `'rotatedRect'`.
111
+ - Gate wall jumps on `onWallLeft` / `onWallRight` inside the jump input handler. `wallhit` is the transition event for effects; `wallSlideSpeed` caps the downward speed natively.
112
+ - A `TileLayer` replaces one-sprite-per-tile maps when the map is large. Its visible-cell renderer, cell collision, live edits, and pathfinding integration are native.
113
+ - Collect initial sprites, emitters, ropes, and tile layers and add them in one `gameView.add([...])` call.
114
+ - Use `Game.createText()` for scene text. Use Titanium views only for UI the engine cannot draw, such as native input controls or system-font interfaces.
115
+ - Stop app-owned JS timers, looping sounds, and listeners on close/controller cleanup. `removeAllSprites()` clears Sprite/Text objects only; explicitly remove tracked emitters, ropes, and tile layers. `SpriteSheet.unload()` and `Font.unload()` are permanent level-streaming operations, not cache eviction.
116
+ - Check [references/roadmap.md](references/roadmap.md) before naming an API that merely exists in another engine.
190
117
 
191
- 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.
118
+ ## Source priority
192
119
 
193
- 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.
120
+ When sources disagree, use this order:
194
121
 
195
- ```javascript
196
- win.addEventListener('close', () => {
197
- clearInterval(aiTimer);
198
- clearTimeout(spawnTimer);
199
- music.stop(); // a looping track outlives the window otherwise
200
- gameView.pause();
201
- gameView.removeAllSprites();
202
- });
203
- ```
122
+ 1. Android and iOS native proxy/engine code for actual behavior and parity.
123
+ 2. `example/*.js` for combinations exercised by the module, not code-quality conventions.
124
+ 3. Upstream `README.md` and `TUTORIAL.md` for intended public usage.
125
+ 4. Upstream `TODO.md` for what remains partial or absent.
204
126
 
205
- ## Reference files
127
+ Known upstream prose drift at the pinned commit: the README says 26 demos although there are 33; its camera row names the wrong HUD corner; and it says every `TileLayer` property is live although Android treats `legend`, `firstGid`, `cols`, and `rows` as creation-time configuration. The references already normalize those differences. No demo exercises the wall-contact API yet, so its contract comes from the README and both native implementations.
206
128
 
207
- Read the one that matches the task instead of guessing — the API surface below was verified against the module source, not reconstructed from memory.
129
+ ## Before returning code
208
130
 
209
- | Need | Reference |
210
- | --- | --- |
211
- | Exact properties, methods, events, defaults for every object | [api.md](references/api.md) |
212
- | Working code per genre: flappy, platformer, top-down, racer, asteroids, endless runner, rhythm, cards, point & click, particles, ropes, camera — plus patrol paths, raycast probes, pathfinding, parallax and game-clock timers | [recipes.md](references/recipes.md) |
213
- | Install the module, tiapp.xml, Alloy vs Classic wiring, assets, sprite sheets, atlases, lifecycle, testing | [project-setup.md](references/project-setup.md) |
214
- | What does NOT exist yet, what is coming, and how to work around it today | [roadmap.md](references/roadmap.md) |
215
-
216
- 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:
217
-
218
- | Upstream file | What it is authoritative for |
219
- | --- | --- |
220
- | `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 |
221
- | `example/` | 32 runnable demos, one per feature area — the reference implementations this skill's recipes are distilled from |
222
- | `TODO.md` | What the maintainer has planned, with priorities. The source of [roadmap.md](references/roadmap.md) |
223
- | `tutorial.md` | A step-by-step first scene (sprite, animation, tap-to-move). For a human learning the module, not for looking things up |
224
- | `ios/README.md` | The iOS port's threading, coordinates and touch constants. Mostly for module developers; the app-facing facts are already here |
225
- | `AGENTS.md` | Rules for changing the module itself — irrelevant to building a game with it |
226
- | `android/src/ti/game/`, `ios/Classes/` | The truth. Defaults, clamping and edge cases in this skill were read here, not from prose |
227
-
228
- ## Before writing game code, check these
229
-
230
- 1. Is the level built inside a guarded `resize` handler, using `e.width`/`e.height`?
231
- 2. Does any timer move a sprite? Replace it with velocity, gravity, a tween, `carMode` or `thrust`.
232
- 3. Are all the level's objects added in one `gameView.add([...])` call?
233
- 4. Does every moving body have the right model — `solidWith` to be blocked, `collidesWith` to be notified?
234
- 5. Is `hitboxScale` set on sprites whose art does not fill the frame — with `hitboxScaleX`/`hitboxScaleY` when the fit differs per axis, and the `anchor` the box shrinks around named rather than left as a bare `1`?
235
- 6. Do triggers (scores, goals, kill zones) use sheet-less sprites instead of visible art?
236
- 7. Is every `createSound`, `setInterval` and `setTimeout` stopped when the window closes?
237
- 8. Is the HUD `Game.createText` with `screenFixed: true`, rather than a stack of overlaid labels?
238
- 9. Does anything travel far enough per frame to tunnel? Give it `swept: true`.
239
- 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.
240
- 11. Does anything ride on top of a moving sprite — a name tag, a health bar, a turret, a hitbox? `attachTo` pins it natively; a timer copying coordinates is the old workaround.
241
- 12. Is a solid's own **shape** doing the work, or is JS correcting for a box that is not what the art shows? A round post wants `hitboxShape: 'circle'` on itself; a tilted one wants `'rotatedRect'`.
242
- 13. Did anything get `restitution` on a *surface*? Every rider on it now bounces, and a hard landing on it stops setting `onGround` — check whatever gates jumps.
243
- 14. Are you using an API that actually exists? Anything involving tilemaps, full parent/child transforms, joysticks, gamepads, or the platformer *feel* on a slope (step-up, friction along the contact) — check [roadmap.md](references/roadmap.md) first.
131
+ - Confirm the installed module build exposes every API used; the manifest version alone is insufficient.
132
+ - Keep per-frame work native and JS event-driven.
133
+ - Prefer a demo-backed combination, then refactor it into focused, maintainable code.
134
+ - Preserve Android/iOS parity; do not rely on an Android-only permissive argument shape.
135
+ - State partial engine behavior honestly, especially slopes, tile triggers/raycast, parenting, audio polish, and input devices.
136
+ - Cite the references that support the result.