@maccesar/titools 5.1.1 → 5.2.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.
Files changed (98) hide show
  1. package/CONTRIBUTING.md +69 -0
  2. package/README.md +112 -72
  3. package/agents/ti-pro.md +5 -1
  4. package/commands/ti-audit.md +5 -1
  5. package/docs/examples.md +91 -0
  6. package/docs/knowledge-index.md +57 -0
  7. package/lib/config.js +2 -0
  8. package/lib/downloader.js +1 -1
  9. package/package.json +4 -2
  10. package/skills/purgetss/references/multi-density-images.md +1 -1
  11. package/skills/ti-admob-unified/SKILL.md +58 -0
  12. package/skills/ti-admob-unified/agents/openai.yaml +4 -0
  13. package/skills/ti-admob-unified/assets/example/app.js +1207 -0
  14. package/skills/ti-admob-unified/assets/example/lib/ads.js +646 -0
  15. package/skills/ti-admob-unified/assets/example/library-usage.js +136 -0
  16. package/skills/ti-admob-unified/references/api-reference.md +615 -0
  17. package/skills/ti-admob-unified/references/banners.md +435 -0
  18. package/skills/ti-admob-unified/references/contributing.md +233 -0
  19. package/skills/ti-admob-unified/references/dependencies.md +91 -0
  20. package/skills/ti-admob-unified/references/fullscreen.md +238 -0
  21. package/skills/ti-admob-unified/references/game-monetization.md +148 -0
  22. package/skills/ti-admob-unified/references/getting-started.md +319 -0
  23. package/skills/ti-admob-unified/references/index.md +33 -0
  24. package/skills/ti-admob-unified/references/integration.md +87 -0
  25. package/skills/ti-admob-unified/references/library.md +220 -0
  26. package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
  27. package/skills/ti-admob-unified/references/mediation.md +145 -0
  28. package/skills/ti-admob-unified/references/migration.md +259 -0
  29. package/skills/ti-admob-unified/references/module-changelog.md +84 -0
  30. package/skills/ti-admob-unified/references/module-license.txt +21 -0
  31. package/skills/ti-admob-unified/references/privacy.md +281 -0
  32. package/skills/ti-admob-unified/references/production.md +67 -0
  33. package/skills/ti-admob-unified/references/rewarded-interstitial.md +101 -0
  34. package/skills/ti-admob-unified/references/roadmap.md +143 -0
  35. package/skills/ti-admob-unified/references/source-notes.md +80 -0
  36. package/skills/ti-admob-unified/references/source-snapshot.json +644 -0
  37. package/skills/ti-admob-unified/references/third-party-notices.md +22 -0
  38. package/skills/ti-game/SKILL.md +31 -5
  39. package/skills/ti-game/references/api.md +36 -24
  40. package/skills/ti-game/references/debugging-performance.md +22 -7
  41. package/skills/ti-game/references/examples.md +113 -20
  42. package/skills/ti-game/references/input.md +101 -0
  43. package/skills/ti-game/references/patterns.md +56 -10
  44. package/skills/ti-game/references/physics.md +530 -0
  45. package/skills/ti-game/references/project-setup.md +131 -47
  46. package/skills/ti-game/references/recipes-action.md +149 -18
  47. package/skills/ti-game/references/recipes-adventure-effects.md +47 -4
  48. package/skills/ti-game/references/roadmap.md +11 -4
  49. package/skills/ti-game/references/source-notes.md +81 -0
  50. package/skills/ti-game/references/tilemaps.md +13 -4
  51. package/skills/ti-game-assets/SKILL.md +53 -0
  52. package/skills/ti-game-assets/agents/openai.yaml +4 -0
  53. package/skills/ti-game-assets/references/alpha.md +26 -0
  54. package/skills/ti-game-assets/references/art-direction.md +37 -0
  55. package/skills/ti-game-assets/references/integration.md +49 -0
  56. package/skills/ti-game-assets/references/manifest.example.json +429 -0
  57. package/skills/ti-game-assets/references/manifest.md +39 -0
  58. package/skills/ti-game-assets/references/offline-audio.md +33 -0
  59. package/skills/ti-game-assets/references/prompts.md +33 -0
  60. package/skills/ti-game-assets/references/repeating-textures.md +17 -0
  61. package/skills/ti-game-assets/references/responsible-ads.md +31 -0
  62. package/skills/ti-game-assets/references/sound-assets.md +42 -0
  63. package/skills/ti-game-assets/references/vector-sources.md +21 -0
  64. package/skills/ti-game-assets/scripts/alpha_tools.py +160 -0
  65. package/skills/ti-game-assets/scripts/requirements.txt +1 -0
  66. package/skills/ti-game-assets/scripts/validate_manifest.py +267 -0
  67. package/skills/ti-synthengine/SKILL.md +77 -67
  68. package/skills/ti-synthengine/assets/example/README.md +84 -0
  69. package/skills/ti-synthengine/assets/example/app.js +1178 -0
  70. package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +83 -0
  71. package/skills/ti-synthengine/assets/example/lib/sequenceScore.js +25 -0
  72. package/skills/ti-synthengine/references/advanced-examples.md +174 -0
  73. package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
  74. package/skills/ti-synthengine/references/android-sim80-correction.json +162 -0
  75. package/skills/ti-synthengine/references/api.md +573 -153
  76. package/skills/ti-synthengine/references/audio-engine.md +272 -124
  77. package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
  78. package/skills/ti-synthengine/references/examples.md +355 -106
  79. package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
  80. package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
  81. package/skills/ti-synthengine/references/frontier-physical-validation.json +1918 -0
  82. package/skills/ti-synthengine/references/guide.md +426 -75
  83. package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
  84. package/skills/ti-synthengine/references/performance-review.json +1146 -0
  85. package/skills/ti-synthengine/references/physical-validation.json +1457 -0
  86. package/skills/ti-synthengine/references/recipes-interaction.md +549 -0
  87. package/skills/ti-synthengine/references/recipes.md +434 -420
  88. package/skills/ti-synthengine/references/recommendations.md +72 -85
  89. package/skills/ti-synthengine/references/sound-design.md +375 -155
  90. package/skills/ti-synthengine/references/source-notes.md +109 -0
  91. package/skills/ti-synthengine/references/source-snapshot.json +452 -0
  92. package/skills/ti-synthengine/references/stats-performance.json +2992 -0
  93. package/skills/ti-synthengine/references/stats-performance.md +82 -0
  94. package/skills/ti-synthengine/references/studio-validation.json +6202 -0
  95. package/skills/ti-synthengine/references/troubleshooting.md +123 -78
  96. package/skills/ti-synthengine/references/unified-bypass-performance.json +22425 -0
  97. package/skills/ti-synthengine/references/unified-validation.json +314 -0
  98. package/AGENTS-VERCEL-RESEARCH.md +0 -175
@@ -1,17 +1,29 @@
1
1
  ---
2
2
  name: ti-game
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."
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, touch, keyboard/mouse actions or gamepads. Covers Alloy and Classic on Android, iOS and Mac Catalyst. Do not use for unrelated game engines or ordinary Titanium UI."
4
4
  allowed-tools: Read, Grep, Glob, Edit, Write, Bash(node *)
5
5
  ---
6
6
 
7
7
  # ti.game — native 2D games for Titanium SDK
8
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.
9
+ `ti.game` is Michael Gangolf's native sprite engine for Titanium: OpenGL ES 2.0 on Android and Metal on iPhone, iPad and Mac Catalyst. 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
10
 
11
- This skill is verified against upstream `main` at **`3bea2f4` (2026-09-02)**. Both manifests say **`0.6.0`**, which is unreleased; the published **`0.5.0`** release carries the same engine code, because its tag sits on the commit immediately before the version bump. Treat the commit or build date as the capability marker, never the version string — a locally built artifact stamped `0.5.0` can predate that release by weeks. Feature-detect an installed build when the exact artifact is unknown.
11
+ Reviewed on **2026-10-23** against `macCesar/ti.game@2bf5f82`, based on
12
+ upstream `m1ga/ti.game@88e181c`. Both manifests declare **`1.0.0`** and
13
+ minimum Titanium SDK **`13.4.0`**; documented builds use **`13.4.1.GA`**.
14
+ The fork's documentation/build adjustments and upstream engine are recorded
15
+ separately in [references/source-notes.md](references/source-notes.md).
16
+ Identify installed artifacts by source commit and ZIP hash; the version alone
17
+ does not prove these capabilities or publication of the fork.
12
18
 
13
19
  The skill applies to **Classic and Alloy** projects. Nothing here requires PurgeTSS.
14
20
 
21
+ ## Visual and sound asset collaboration
22
+
23
+ Use `ti-game-assets` to create/edit complete visual and sound asset sets, preserve art/acoustic palettes and provenance, validate alpha/frames/manifests, prepare exports, and coordinate variants and stereo event placement. `ti-game` governs native representation/motion/events; `ti-synthengine` governs synthesis and sound behavior with `ti.synthengine`. Both have equal domain authority. Read their relevant current references rather than duplicating manuals. Art alone does not require installing modules or adding sounds/mechanics.
24
+
25
+ For requested advertising or existing `ti.admob.unified`, use `ti-admob-unified` for the official module/privacy/provider contract and deliberate game-ad boundaries. Preserve native clock, inputs and audio ownership; graphics work alone does not authorize monetization.
26
+
15
27
  ## Required workflow
16
28
 
17
29
  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.
@@ -19,8 +31,11 @@ Open the references relevant to the task before proposing code. Do not answer fr
19
31
  | Task | Read first |
20
32
  | --- | --- |
21
33
  | Exact methods, properties, defaults, event payloads, percentages | [references/api.md](references/api.md) |
34
+ | Keyboard, mouse, named actions, contexts, focus and controller ownership | [references/input.md](references/input.md) |
35
+ | Source pins, upstream/fork differences, audit limits | [references/source-notes.md](references/source-notes.md) |
22
36
  | Install, Classic/Alloy structure, assets, cleanup, lifecycle | [references/project-setup.md](references/project-setup.md) |
23
37
  | Choose a runnable upstream example | [references/examples.md](references/examples.md) |
38
+ | Choose or tune a physical model, derive equations/units, diagnose FPS-dependent motion | [references/physics.md](references/physics.md) |
24
39
  | Reusable level, pooling, HUD, collision, path, timer patterns | [references/patterns.md](references/patterns.md) |
25
40
  | Platformer, racer, shooter, runner, rhythm, ball-game recipes | [references/recipes-action.md](references/recipes-action.md) |
26
41
  | Cards, point-and-click, particles, ropes, camera, effects | [references/recipes-adventure-effects.md](references/recipes-adventure-effects.md) |
@@ -56,6 +71,7 @@ Do not move sprites from `setInterval`, `requestAnimationFrame`, or a JS loop. P
56
71
  | Chain, tether, cape | `Game.createRope()` |
57
72
  | Pause or slow the scene while still drawing | `gameView.timeScale` |
58
73
  | Measure FPS, frame time, and draw calls | `debug: { hud: true }` or the `performance` event |
74
+ | Map keyboard, mouse and gamepad buttons to actions | `inputMap`, `inputContext`, `actiondown` / `actionup`, `focusInput()` |
59
75
  | Read a game controller | GameView `buttondown`/`buttonup`, `stick`, `trigger` (names normalized across pads) |
60
76
  | Wrap the world around horizontally | `gameView.worldWrapX` + per-sprite `wrapWorldX` |
61
77
  | React to the force of a hit | `solidimpact` with a per-sprite `impactThreshold` |
@@ -107,6 +123,7 @@ win.open();
107
123
 
108
124
  ## Non-negotiable invariants
109
125
 
126
+ - For keyboard/mouse, read the focused input contract and consuming-app plist requirements before coding; keep native text fields and overlaid controls usable.
110
127
  - 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.
111
128
  - Coordinates use a top-left origin, y increases downward, and `(x, y)` is the sprite anchor. Durations crossing the JS boundary are milliseconds.
112
129
  - `solidWith` blocks; `collidesWith` reports `collision`/`collisionend`. They are independent. A sized sprite without a sheet is a valid invisible solid or trigger.
@@ -124,10 +141,19 @@ When sources disagree, use this order:
124
141
 
125
142
  1. Android and iOS native proxy/engine code for actual behavior and parity.
126
143
  2. `example/*.js` for combinations exercised by the module, not code-quality conventions.
127
- 3. Upstream `README.md` and `TUTORIAL.md` for intended public usage.
144
+ 3. Upstream `README.md` and `tutorial.md` for intended public usage.
128
145
  4. Upstream `TODO.md` for what remains partial or absent.
129
146
 
130
- The prose drift recorded at the previous pin is gone: upstream now states 34 demos (correct), names the HUD corners correctly, and documents `legend`, `firstGid`, `cols` and `rows` as creation-time on Android. What remains uncovered by any demo is `wallSlideSpeed` and the `onWallLeft`/`onWallRight` flags — `wallhit` itself is now exercised by `worldwrap.js` — so that part of the contract still comes from the README and both native implementations.
147
+ The reviewed launcher has **35 demos**: 34 mobile touch scenes plus
148
+ Configurable Input on Mac. Helpers and layout/focus behavior are cataloged in
149
+ [references/examples.md](references/examples.md). Source inspection and host
150
+ regressions do not certify installed binaries or physical-device behavior.
151
+
152
+ Current damping lets effective unopposed acceleration build speed from rest, and a zero-delta rope update
153
+ freezes its initialized geometry. Older `3bea2f4` behavior is historical; read
154
+ [references/physics.md](references/physics.md) for the current equations,
155
+ remaining step-size limits and regression boundaries. Input focus and Mac
156
+ right/middle mouse limitations belong in [references/input.md](references/input.md).
131
157
 
132
158
  ## Before returning code
133
159
 
@@ -1,8 +1,8 @@
1
1
  # ti.game API reference
2
2
 
3
- Complete JS surface of `ti.game` as of upstream `main` at `3bea2f4`, 2026-09-02, verified against every Android proxy/engine and its iOS twin. Defaults come from native fields, not prose. The manifests read **`0.6.0`**, which is unreleased: the published **`0.5.0`** release carries the same engine code, because its tag sits on the commit right before the version bump. Read the number as a floor, never as a feature list — a locally built artifact stamped `0.5.0` may predate that release by weeks. Older `0.3.0`/`0.4.0` artifacts also vary by build date; feature-detect when the artifact is unknown. Read a property before writing it, because Kroll can retain an unknown write on the proxy. Methods are safer probes (`typeof Game.createTileLayer === 'function'`), as are normalized properties (`solidMode` reads an unknown name back as `'block'` only when the engine implements it).
3
+ Portable JS surface reviewed against `m1ga/ti.game` main at `88e181cb85526d8146d0be6e09b2b90c2551baad` and the documentation refresh in `macCesar/ti.game@2bf5f828a3bb8028eed20d6ba27b97dd3aa9a753`, 2026-10-23. Android uses OpenGL ES 2.0; Apple uses Metal on iPhone, iPad and Mac Catalyst. Both manifests declare `1.0.0` and minimum Titanium SDK `13.4.0`; this source review does not establish the version or behavior of an installed artifact. Check the artifact's source commit, especially nightly builds sharing a version. Feature-detect when the artifact is unknown: methods are safer probes than unknown property writes retained on a Kroll proxy.
4
4
 
5
- Properties are live unless their table says creation-time or read-only. Every writable property can be passed to its `create*` factory. `TileLayer` has four cross-platform creation-time inputs (`legend`, `firstGid`, `cols`, `rows`) because Android does not expose the live setters that iOS has. All durations crossing the JS boundary are **milliseconds**.
5
+ Properties are live unless their table says creation-time or read-only. Every writable property can be passed to its `create*` factory. Exceptions for portable code: `animations` must be supplied at Sprite creation, and TileLayer `legend`, `firstGid`, `cols`, `rows` must be supplied at TileLayer creation. All durations crossing the JS boundary are **milliseconds**. For keyboard, mouse, action maps and focus, read [input.md](input.md).
6
6
 
7
7
  <!-- TOC-START -->
8
8
  ## Contents
@@ -100,10 +100,13 @@ A normal Titanium view — add it to a window or any container, size it with the
100
100
  | `remove(object)` | method | — | Remove one object from the scene — **and, recursively, every sprite attached to it** (see [Attachment](#attachment)) |
101
101
  | `removeAllSprites()` | method | — | Clears Sprite/Text objects and their attachments. It does **not** remove emitters, ropes, or tile layers; track and remove those separately |
102
102
  | `pause()` / `resume()` | method | — | Render loop control. The activity/app lifecycle already pauses and resumes automatically |
103
- | `backgroundColor` | string | — | GL clear color |
103
+ | `backgroundColor` | string | — | Native scene clear color |
104
104
  | `maxFps` | int | `0` | Frame cap. `60` stops 120 Hz ProMotion displays from doubling render work; `0` = display refresh rate |
105
- | `timeScale` | float | `1` | Global multiplier for everything the engine ticks: physics, sheet animations, tweens, particles, camera. `0.5` = slow motion, `0` freezes the scene while rendering and touch keep running (pause menus, hit-stop) |
105
+ | `timeScale` | float | `1` | Multiplies the scene dt for physics, sheet animations, tweens, particles and camera. `0.5` = slow motion; at `0` rendering/touch continue while rope nodes, their history, constraints and tether response freeze. New rope geometry can still initialize for drawing; see [physics.md](physics.md#verlet-ropes-and-tethers) |
106
106
  | `surfaceWidth` / `surfaceHeight` | int | — | Read-only surface size in pixels |
107
+ | `inputMap` | dict | `{}` | Contexts → named actions → binding arrays. Invalid maps throw without replacing the current map or active state; see [input.md](input.md) |
108
+ | `inputContext` | string / null | `'gameplay'` | Active action context; `null` disables actions. A changed context releases active actions and requires a fresh press |
109
+ | `focusInput()` | method | — | Request canvas keyboard focus on the UI thread; does not open the software keyboard or capture keys globally |
107
110
  | `cameraX` / `cameraY` | float | `0` | World-space offset of the view |
108
111
  | `cameraScale` | float | `1` | Zoom, anchored on the view center |
109
112
  | `cameraBounds` | dict / null | `null` | `{ minX, minY, maxX, maxY }` world rect the visible area is clamped into |
@@ -111,7 +114,7 @@ A normal Titanium view — add it to a window or any container, size it with the
111
114
  | `follow(sprite, options)` | method | — | Native dead-zone follow, see below |
112
115
  | `stopFollow()` | method | — | Stop following; the camera stays where it is |
113
116
  | `shake({ strength, duration })` | method | `12` px, `400` ms | Detuned-sine rumble on the projection only — follow, bounds and touch mapping are unaffected |
114
- | `raycast(x0, y0, x1, y1, groups)` | method | — | One-shot nearest-hit query along the segment against **visible, non-`screenFixed`** sprites carrying a `collisionGroup` in `groups` (omit for any tagged sprite). Returns `null` for a clear ray, else `{ x, y, distance, group, sprite, normal: { x, y } }`. Rect hitboxes are tested as their AABB, circle hitboxes exactly; a ray starting inside a hitbox reports it at distance 0 |
117
+ | `raycast(x0, y0, x1, y1, groups)` | method | — | One-shot nearest-hit query along the segment against **visible, non-`screenFixed`** sprites carrying a `collisionGroup` in `groups` (omit for any tagged sprite). Returns `null` for a clear ray, else `{ x, y, distance, group, sprite, normal: { x, y } }`. Plain rect hitboxes use their AABB; circle and `rotatedRect` hitboxes use their actual shapes; a ray starting inside a hitbox reports it at distance 0 |
115
118
  | `findPath(from, to, options)` | method | — | Grid A\* from `from` to `to` (`{ x, y }` world points) around visible, non-`screenFixed` sprites and fully solid TileLayer cells carrying a `collisionGroup` in `options.groups` (omit for any tagged obstacle). Returns `{ x, y }` waypoints ready for `sprite.followPath()`, or `null`. Options below |
116
119
  | `after(ms, callback)` | method | — | Runs `callback` once after `ms` of **game time** — the delay stretches under `timeScale` slow motion, freezes at `0` and pauses with the render loop, unlike `setTimeout`. Returns an int id. The callback receives `{ id }` |
117
120
  | `every(ms, callback)` | method | — | Like `after()`, repeating until cancelled. Fires at most once per frame, and restarts its interval after a stall instead of bursting to catch up |
@@ -127,6 +130,8 @@ A normal Titanium view — add it to a window or any container, size it with the
127
130
 
128
131
  Events: `press`, `tap`, `release` — fired for **every** touch anywhere on the view, with payload `x`, `y` in scene coordinates (tap-anywhere controls, flappy-style). `resize` carries the real surface `width`, `height` in pixels. `performance` carries a one-second telemetry snapshot only while a listener exists; see [debugging-performance.md](debugging-performance.md).
129
132
 
133
+ Keyboard `keydown` / `keyup` carry `code`, `character`, and `modifiers: { shift, control, alt, meta }`. Action `actiondown` / `actionup` carry `action` and `context`; cancellation may release actions without raw `keyup`. See [input.md](input.md).
134
+
130
135
  Plus `timer` with payload `id`, fired only for `after()` / `every()` calls made **without** a callback — pass a callback or listen for the event, not both.
131
136
 
132
137
  Kroll never runs a `@Kroll.setProperty` setter for a key passed to `createGameView()`: each one needs its own branch in the native creation handler. `cameraX`, `cameraY`, `cameraScale` and `cameraBounds` were silently dropped at creation before `3bea2f4` and are honored from it on. Assigning them after creation always worked and still does — prefer that when the installed artifact is unknown.
@@ -192,6 +197,8 @@ On Android the buttons are captured at the activity window: the game view needs
192
197
 
193
198
  `platformer.js` runs on either the on-screen buttons or a pad.
194
199
 
200
+ Every live GameView receives controller input independently, including retained views under a modal scene. Closing one releases its input and unregisters only that receiver. Apple receivers share the GameController profile callback with registration, dispatch and cleanup on the main queue. Route input to the active screen in app code; see [lifecycle and input ownership](project-setup.md#retained-ios-scenes-and-multiple-gameviews).
201
+
195
202
  ## SpriteSheet
196
203
 
197
204
  Create one sheet per texture and share it across every sprite that uses it — sprites sharing a sheet batch into a single draw call.
@@ -201,17 +208,17 @@ Create one sheet per texture and share it across every sprite that uses it — s
201
208
  | `image` | — | Path to the texture, relative to the resources root (`'assets/hero.png'`) |
202
209
  | `frameWidth` / `frameHeight` | — | Grid mode: cell size in source pixels. Frames are numbered left-to-right, top-to-bottom, starting at 0 |
203
210
  | `atlas` | — | TexturePacker JSON (hash or array format) instead of a grid |
204
- | `smoothing` | `true` | `false` = GL_NEAREST, the pixel-art setting. On a **grid** sheet, `true` also insets the frame UVs by half a texel so magnified edges cannot sample the neighbouring frame — **both** edges of any axis that has more than one frame, since 2026-08-24. An axis with a single frame keeps the exact 0..1 range, which is what `tileRepeat` needs to wrap. Atlas frames come from the JSON untouched |
205
- | `repeat` | `false` | GL_REPEAT wrap, required by sprites using `tileRepeat`. Needs power-of-two texture dimensions |
211
+ | `smoothing` | `true` | `false` = nearest-neighbor filtering, the pixel-art setting. On a **grid** sheet, `true` also insets the frame UVs by half a texel so magnified edges cannot sample the neighbouring frame — **both** edges of any axis that has more than one frame, since 2026-08-24. An axis with a single frame keeps the exact 0..1 range, which is what `tileRepeat` needs to wrap. Atlas frames come from the JSON untouched |
212
+ | `repeat` | `false` | Texture repeat, required by sprites using `tileRepeat`. Android OpenGL ES 2.0 requires power-of-two texture dimensions |
206
213
 
207
214
  | Member | Notes |
208
215
  | --- | --- |
209
216
  | `frameCount` | Number of frames. `0` until the texture has loaded, for grid sheets |
210
217
  | `frameNames` | Sorted frame names — atlas sheets only |
211
218
  | `frameIndex(name)` | Index for an atlas frame name, `-1` if unknown |
212
- | `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 |
219
+ | `unload()` | Permanently disposes the GPU texture on the render thread. 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 |
213
220
 
214
- 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.
221
+ Textures upload to the GPU lazily on first use, from the render thread, and are re-created automatically after an Android EGL context loss. Apple resources are Metal textures; do not apply EGL-specific lifecycle assumptions to Apple views. Releasing the proxy unloads its texture too, so a sheet that goes out of scope does not strand GPU memory.
215
222
 
216
223
  ## Sprite
217
224
 
@@ -240,12 +247,12 @@ Textures upload to the GPU lazily on first use, from the render thread, and are
240
247
  | Property | Default | Notes |
241
248
  | --- | --- | --- |
242
249
  | `sheet` | — | The SpriteSheet |
243
- | `frame` | `0` | Current frame index; writing it stops nothing, it just shows that frame |
244
- | `animations` | — | Named definitions: `{ walk: { frames: [1, 2], fps: 6, loop: true } }` |
250
+ | `frame` | `0` | Current frame index; writing it stops the running sheet animation and its queued chain, then shows that frame |
251
+ | `animations` | — | Creation-only for portable code; iOS also accepts later assignment, Android has no live setter. Definitions: `{ walk: { frames: [1, 2], fps: 6, loop: true } }` |
245
252
  | `animation` | — | Read-only name of the current animation. **Keeps its value after `stop()`** — track "is walking" yourself if you need it |
246
253
  | `tileRepeat` | `false` | `true` / `'x'` / `'y'` — tile the frame at native size instead of stretching. Needs `repeat: true` on the sheet and a frame spanning the whole texture |
247
254
 
248
- An animation definition takes `frames` (array of indices), `fps`, `loop`, and an optional `frame` — the sheet frame to show once a non-looping animation finishes (default: hold the last animation frame).
255
+ An animation definition takes `frames` (array of indices), `fps`, `loop`, and, on Android only, an optional completion `frame` (default: hold the last animation frame). iOS ignores that definition key. Both platforms support a completion `frame` in `animate()`.
249
256
 
250
257
  Methods: `play(name)` starts an animation, `stop()` halts it on the current frame.
251
258
 
@@ -271,7 +278,7 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
271
278
  | `velocityX`, `velocityY` | `0` | px/s |
272
279
  | `gravity` | `0` | px/s², added to `velocityY`. Per sprite — there is no global gravity |
273
280
  | `gravityX` | `0` | px/s², added to `velocityX`. The **sibling** of `gravity`, not half of a vector: `gravity` keeps its exact vertical meaning. Wind, a conveyor, or a top-down game whose "down" is sideways (`gravity: 0`, `gravityX` non-zero). Since 2026-08-27 |
274
- | `linearDamping` | `0` | Fraction of speed shed per second, applied to both velocity axes before the position integration — rolling friction for ordinary sprites, which `drag` never was (that one only runs inside `carMode`). ~`0.6` is a pool ball on felt. Proportional, so a fast body sheds a lot and a slow one very little; below **4 px/s** the engine zeroes both axes outright, because a proportion never reaches zero on its own. Negative values are clamped to 0. Since 2026-08-27 |
281
+ | `linearDamping` | `0` | Coefficient in 1/s: each step retains `max(0, 1−linearDamping·dt)` of both velocity axes, after thrust/gravity and before position. The **4 px/s** rest cutoff stops coasting and blocked residual motion, but effective thrust, gravity or powered car input can build velocity from rest. The post-contact decision excludes blocked motion and cancelled forces; damping still attenuates acceleration and depends on dt. ~`0.6` is used for pool felt, but damping acts even without contact. Negative values clamp to0. See [physics.md](physics.md#thrust-inertia-and-linear-damping) |
275
282
  | `maxSpeed` | `500` | px/s cap applied **only** to `thrust` acceleration and the `carMode` model. Writing `velocityX`/`velocityY` directly is never clamped |
276
283
  | `angularVelocity` | `0` | deg/s |
277
284
  | `thrust` | `0` | px/s² along the current heading (Newtonian flight) |
@@ -321,7 +328,7 @@ Two consequences that cost an afternoon each if you meet them by surprise:
321
328
  | `hitboxScale` | `1` | Shrinks the collision box **around the anchor** — which is what makes `anchor` the other half of the tuning: `anchor: 'bottom'` with `hitboxScaleY: '55%'` covers the lower half without lifting the box off the floor. Also takes `'80%'` |
322
329
  | `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 |
323
330
  | `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. `'rotatedRect'` keeps the collision rect **turned with the sprite** (an OBB) instead of re-boxing it square to the screen: a plain `'rect'` at 45° is re-boxed around its turned corners and comes out 41% wider with a flat top that is not there. Only matters once `rotation` is non-zero, and it does honour `hitboxScaleX`/`hitboxScaleY`, unlike a circle. Covers solids, `collidesWith` overlap, `raycast`, the swept pass and the debug overlay. Since 2026-08-27 |
324
- | `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). Two **circle** hitboxes now sweep as circles (the Minkowski sum of two circles is a circle of radius r1+r2); a circle against a rectangular or turned solid sweeps as a box, and against a turned one the box is grown into a *rounded* rect so the diagonal does not poke out by 1.41r. Set it on the *mover* — the bullet, not the wall. The sweep skips solids that are not `solidMode: 'block'` |
331
+ | `swept` | `false` | Test this sprite's movement as a **path**, 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). Two **circle** hitboxes now sweep as circles (the Minkowski sum of two circles is a circle of radius r1+r2); trigger sweeps for circle/rectangle pairs use a rounded box (including turned boxes), while rectangle pairs use AABBs. The `solidWith` blocking sweep is separate: circle pairs use circles; circle-versus-rect blocking keeps its own expanded-box/rounded-OBB path. Do not infer identical trigger and blocking geometry. Set it on the *mover* — the bullet, not the wall. The sweep skips solids that are not `solidMode: 'block'` |
325
332
  | `debug` | `false` | Draw this sprite's shapes: green = collision AABB, blue = touch bounds, orange dot = anchor |
326
333
 
327
334
  ### Car (`carMode`)
@@ -369,7 +376,7 @@ Every sprite gets its own phase, and the wobble unwinds exactly when disabled. T
369
376
  | --- | --- |
370
377
  | `play(name, options)` | Start a named animation; returns `false` for an unknown name. `options.then` (a name or an array of names) chains natively — each queued animation starts as the previous non-looping one finishes, and `animationcomplete` still fires per step. A looping animation ends the chain |
371
378
  | `stop()` | Stop the animation on the current frame; also drops any queued `then` chain |
372
- | `followPath(points, options)` | Walk the sprite along `points` — `{ x, y }` objects or `[x, y]` pairs, **at least two**, fewer is ignored with a warning. `options`: `speed` px/s (default `100`), `loop` (closed circuit), `rotate` (face along the path, 0 = up), `smoothing` (corner radius in px, precomputed once). Fires `pathcomplete` at the end of a non-looping run and clears itself. `followPath(null)` stops in place |
379
+ | `followPath(points, options)` | Walk the sprite along `points` — `{ x, y }` objects or `[x, y]` pairs, **at least two**; fewer logs a warning and clears the path. `options`: `speed` px/s (default `100`), `loop` (closed circuit), `rotate` (face along the path, 0 = up), `smoothing` (corner radius in px, precomputed once). Fires `pathcomplete` at the end of a non-looping run and clears itself. `followPath(null)` stops in place |
373
380
  | `animate(options)` | Native tween, fires `complete` |
374
381
  | `clearTweens()` | Cancel tweens in progress — call before starting a replacement tween |
375
382
  | `flash(color, duration)` | Fills the sprite's silhouette with `color` (default white) and fades it out over `duration` ms (default 150). The classic damage/invincibility flash that a multiplicative `tintColor` cannot do |
@@ -431,7 +438,7 @@ The rest of the contract, all read from the engine:
431
438
 
432
439
  ## Font
433
440
 
434
- A glyph atlas for text sprites. Create it once, outside `resize`, like a SpriteSheet — it holds a GL texture.
441
+ A glyph atlas for text sprites. Create it once, outside `resize`, like a SpriteSheet — it holds a GPU texture.
435
442
 
436
443
  ```javascript
437
444
  // 1. Built-in: nothing to ship. A 9x15 pixel font embedded in the module.
@@ -456,6 +463,8 @@ const mono = Game.createFont({ image: 'assets/mono.png', charWidth: 9, charHeigh
456
463
  | `lineHeight` | — | Read-only: the font's natural line height in px |
457
464
  | `unload()` | Method, not an option: frees the glyph texture exactly like `SpriteSheet.unload()`, and just as permanently. Since 2026-08-23 |
458
465
 
466
+ The built-in font is Press Start 2P with 9×15 cells: ASCII 32..126 plus `ÁÉÍÓÚÜÑáéíóúüñ¿¡ÄÖäößËÏëïÈèÊêÀà`. It accepts these precomposed characters, not combining accents; `ẞ` is absent, so uppercase German text uses `SS`. Custom grid fonts still default to ASCII only. The source TTF and SIL Open Font License are in `tools/default-font/`; regenerate both embedded images with `uv run --with pillow --with fonttools python tools/build_default_font.py`.
467
+
459
468
  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.
460
469
 
461
470
  ## Text
@@ -516,12 +525,12 @@ music.play();
516
525
 
517
526
  | Option | Default | Notes |
518
527
  | --- | --- | --- |
519
- | `url` | required | App resource or file path |
528
+ | `url` | required | Creation-only app resource or file path |
520
529
  | `volume` | `1` | 0..1, live |
521
530
  | `loop` | `false` | Live |
522
531
  | `music` | `false` | Chooses the backend — **set at creation, not changeable later** |
523
532
 
524
- Effect mode (the default) is built for low latency: a shared `SoundPool` on Android, a small pool of preloaded players on iOS. Call `play()` from any handler and repeated plays overlap instead of cutting each other off. `music: true` picks the streaming backend (`MediaPlayer` / `AVAudioPlayer`) for longer tracks; music pauses when the app backgrounds and resumes with it.
533
+ Effect mode (the default) is built for low latency: a shared `SoundPool` on Android, a small pool of preloaded players on iOS. Call `play()` from any handler and repeated plays overlap instead of cutting each other off; a play requested during loading is queued. `music: true` picks the streaming backend (`MediaPlayer` / `AVAudioPlayer`) for longer tracks; music pauses when the app backgrounds and resumes with it.
525
534
 
526
535
  Methods: `play()`, `pause()` (resumes where it stopped), `stop()` (rewinds to the beginning). Formats: WAV, MP3, OGG on Android; WAV, MP3, M4A on iOS.
527
536
 
@@ -546,7 +555,7 @@ Add and remove like a sprite: `gameView.add(emitter)` / `remove(emitter)`. Spawn
546
555
  | `size` | `0` | Base particle width in px; `0` = frame size |
547
556
  | `startScale` / `endScale` | `1` / `1` | Interpolated over `lifetime` |
548
557
  | `startOpacity` / `endOpacity` | `1` / `0` | Interpolated over `lifetime` |
549
- | `tint` | white | Tint white particle art at runtime — one tiny texture covers every color in the game |
558
+ | `tint` | white | Write-only. Tint white particle art at runtime — one tiny texture covers every color in the game |
550
559
  | `blend` | `'normal'` | `'add'` for fire, sparks and magic; `'multiply'` for smoke and dust that darken what they cross; `'screen'` for soft haze |
551
560
  | `maxParticles` | `200` | Pool size, hard cap 1000 |
552
561
 
@@ -573,6 +582,9 @@ A native Verlet chain: integration and distance constraints run in the game loop
573
582
  | `visible` | `true` | |
574
583
  | `endX`, `endY` | — | Read-only live position of the loose end (grappling-hook tips) |
575
584
 
585
+ Per-step retention is not a per-second rate. At `dt<=0`, node positions, previous positions, constraint relaxation and tether response stay frozen. New geometry still initializes so a newly created rope can draw while paused;
586
+ see [Verlet equations and pause behavior](physics.md#verlet-ropes-and-tethers).
587
+
576
588
  The tether yields at the end no finger owns: with a fixed head anchor the tail sprite is simply leashed, but with sprites on both ends you can drag either one and the other is towed once the rope goes taut.
577
589
 
578
590
  ## TileLayer
@@ -581,7 +593,7 @@ One native grid replaces a sprite per tile. Only cells inside the camera are dra
581
593
 
582
594
  | Group | Members | Notes |
583
595
  | --- | --- | --- |
584
- | Grid input | `data` | Nested numeric rows, string rows decoded through `legend`, or flat row-major ids sized by `cols`/`rows`. Live on both platforms |
596
+ | Grid input | `data` | Live write-only input. Nested numeric rows, string rows decoded through `legend`, or flat row-major ids sized by `cols`/`rows`. Live on both platforms |
585
597
  | Creation-time grid configuration | `legend`, `firstGid`, `cols`, `rows` | Set in `createTileLayer()` for portability. `cols`/`rows` read back from the parsed grid; Android has no live setters for the four configuration inputs |
586
598
  | Derived size | `width`, `height` | Read-only world dimensions |
587
599
  | Placement | `x`, `y`, `tileWidth`, `tileHeight`, `zIndex`, `scrollFactor` | Tile sizes default to the sheet frame dimensions |
@@ -595,7 +607,7 @@ Tiled data uses `firstGid`: gid 0 becomes empty when an offset is active, and fl
595
607
  | `getTile(col, row)` | Frame id, `-1` when empty or outside |
596
608
  | `setTile(col, row, id)` | Replaces art and recalculates its flag from `solid`/`oneWay` |
597
609
  | `isBlocked(col, row)` | Fully solid status; one-way cells answer `false` |
598
- | `setBlocked(col, row, bool)` | Per-cell full-solid override without changing art |
610
+ | `setBlocked(col, row, bool)` | Per-cell full-solid override without changing art, until `data`, `solid` or `oneWay` changes |
599
611
  | `tileAt(x, y)` | World point → `{ col, row, tile, solid, x, y }`, or `null` outside |
600
612
  | `cellAt(col, row)` | Cell → the same structure; `x`/`y` are the center |
601
613
 
@@ -605,7 +617,7 @@ Movers list the layer's `collisionGroup` in `solidWith`. Rect, circle, swept, re
605
617
 
606
618
  Only these events exist. Nothing fires per frame, and events are only fired if a listener is registered.
607
619
 
608
- - **GameView**: `press`, `tap`, `release`, `resize`, `timer`, `performance`, `buttondown`, `buttonup`, `stick`, `trigger`, `gamepadconnected`, `gamepaddisconnected`
620
+ - **GameView**: `press`, `tap`, `release`, `resize`, `timer`, `performance`, `buttondown`, `buttonup`, `stick`, `trigger`, `gamepadconnected`, `gamepaddisconnected`, `keydown`, `keyup`, `actiondown`, `actionup`
609
621
  - **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `pathcomplete`, `collision`, `collisionend`, `land`, `wallhit`, `solidimpact`
610
622
  - **SpriteSheet, Font, Sound, Emitter, Rope, TileLayer**: none
611
623
 
@@ -623,12 +635,12 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
623
635
  - **`music` is chosen at creation.** Creating an effect sound and flipping `music` later does not switch backends.
624
636
  - **`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.
625
637
  - **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.
626
- - **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.
638
+ - **The built-in font includes a specific accented Latin repertoire, not arbitrary Unicode.** Precomposed Spanish `ñ` and the listed accented characters work; combining accents, em dashes and unlisted glyphs need a custom BMFont/grid atlas. Custom grid fonts default to ASCII 32..126 unless `characters` is supplied.
627
639
  - **A bouncy floor switches `onGround` off.** In every resolver the grounding and the `land` event live in the branch taken when the bounce is *damped away*; a contact whose rebound would exceed 40 px/s reflects and grounds nothing. That was always true of a bouncy rider, but `restitution` now mixes as `max(mover, solid)`, so giving a *floor* `restitution: 0.5` silently stops every rider that lands hard on it from ever reporting `onGround` — and a jump gated on `onGround` stops working on that one platform.
628
640
  - **A two-sided `restitution` cannot be opted out of.** The mix is `max`, not a product or an average, so the springier surface always wins: a deliberately dead `restitution: 0` crate still bounces off a floor set to `0.5`. If one rider must not bounce, the bounce has to live on the riders, not on the surface.
629
641
  - **A resting body is allowed to sink half a pixel — in some resolvers.** Separation is skipped when the overlap is under 0.5 px, because shoving a gravity-loaded body back out sixty times a second is what makes a settled pile tremble; the closing velocity is still cancelled, so the sink cannot grow past it. It applies to the circle path, the turned-rect path and `'push'` pairs. Plain rect-against-rect still separates in full. Do not derive an exact contact position from `y` after a landing.
630
642
  - **`linearDamping` is not surface friction.** Nothing checks whether the sprite is touching anything: it bleeds speed on both axes every frame, in the air as much as on the felt, and along a slope as much as across it. It is right for a pool table and wrong for a hill, which is why upstream still lists contact-only friction as unbuilt. A projectile that must keep its arc wants `0`.
631
- - **`linearDamping` kills a slow deliberate drift.** Under a combined 4 px/s the engine zeroes *both* axes outright, since a proportion never reaches zero on its own. A conveyor nudging something along at 3 px/s stops dead the moment that sprite also carries any damping.
643
+ - **`linearDamping` stops slow coasting.** Under 4 px/s it stops unpowered drift; an effective acceleration can now build speed from rest. Forces cancelled by opposing forces or a solid do not keep residual tangential coasting alive. This rest policy does not make Euler damping independent of frame rate.
632
644
  - **A swept sprite is not contained and not pushed.** The sweep only considers solids in `solidMode: 'block'` — a `'contain'` boundary would stop the ball against its *outside*, and a `'push'` body is meant to move. So `swept: true` does not stop a fast enough ball from crossing a drum wall between frames; only the static pass contains it. Keep the speeds inside a container below its wall thickness per frame.
633
645
  - **`'push'` pairs never publish ground or wall contact.** The bilateral resolver moves positions and exchanges velocity but does not update `onGround`, `onWallLeft` / `onWallRight`, `land`, or `wallhit`. A pile of balls resting on each other reports contact only with any separate `'block'` surface underneath it.
634
646
  - **The `'push'` exchange has no masses, no spin and no friction.** A marble and a boulder trade velocity identically, and nothing ever starts rotating from a contact. `restitution` mixes as `max` here too, and with no mass to weight it against, that is the whole model.
@@ -1,6 +1,7 @@
1
1
  # Debugging and performance telemetry
2
2
 
3
- Verified against `ti.game@3bea2f4`: both `GameViewProxy` implementations, `DebugHud`, `FrameStats`, both scene renderers, `example/camera.js`, `example/particles.js`, and `example/tilemap.js`.
3
+ Reviewed on 2026-10-23 against upstream native `88e181c` and
4
+ `macCesar/ti.game@2bf5f82` documentation: both `GameViewProxy` implementations, `DebugHud`, `FrameStats`, both scene renderers, `example/camera.js`, `example/particles.js`, and `example/tilemap.js`.
4
5
 
5
6
  ## The two debug aids
6
7
 
@@ -32,7 +33,7 @@ The HUD is screen-space and is drawn after the camera effect. Camera scroll, zoo
32
33
  - Green: collision shape after `hitboxScale`, `hitboxScaleX`, and `hitboxScaleY`.
33
34
  - Blue: full sprite/touch bounds.
34
35
  - Orange: anchor.
35
- - Tile debug: outlines fully solid cells in view.
36
+ - Tile debug: green outlines fully solid cells; yellow marks one-way top faces.
36
37
 
37
38
  Tune geometry in this order:
38
39
 
@@ -47,7 +48,7 @@ The scale properties affect collision, not the touch rectangle. `hitboxShape: 'c
47
48
 
48
49
  | HUD | Event key | Meaning |
49
50
  | --- | --- | --- |
50
- | `FPS` | `fps` | Frames presented in the closed one-second window |
51
+ | `FPS` | `fps` | Frames recorded in the measurement window; Metal submission does not confirm physical presentation |
51
52
  | `MS` | `averageCpuMs` | Average engine CPU time per frame; the `maxFps` wait is excluded |
52
53
  | `P95` | `p95CpuMs` | 95th percentile CPU frame time |
53
54
  | `MAX` | `maxCpuMs` | Worst CPU frame in the window |
@@ -60,8 +61,8 @@ The scale properties affect collision, not the touch rectangle. `hitboxShape: 'c
60
61
  | `UPDATE` | `averageUpdateMs` | Physics, animation, tween, and scene update time |
61
62
  | `TEXTURE` | `averageTexturePrepareMs` | Texture preparation/upload time |
62
63
  | `BATCH` | `averageBatchMs` | Batching and drawing time |
63
- | `PRESENT` | `averagePresentMs` | iOS only: buffer presentation time |
64
- | `PRESENTFAIL` | `presentFailures` | iOS only: failed presentations |
64
+ | `PRESENT` | `averagePresentMs` | Apple only: CPU time acquiring a Metal drawable and submitting presentation; excludes GPU execution |
65
+ | `PRESENTFAIL` | `presentFailures` | Apple only: missing drawables and reported command errors |
65
66
 
66
67
  The event also includes `surfaceWidth` and `surfaceHeight`. Android omits `averagePresentMs` and `presentFailures`; do not interpret missing keys as zero.
67
68
 
@@ -81,7 +82,7 @@ function cleanup() {
81
82
  }
82
83
  ```
83
84
 
84
- Telemetry is opt-in inside the renderers. With the HUD disabled and no `performance` listener, frame-stat clock reads are disabled. Use the HUD while developing; use the event for short diagnostic sessions or a deliberate in-app performance screen, not permanent per-frame business logic.
85
+ Telemetry is opt-in inside the renderers. With the HUD disabled and no `performance` listener, telemetry timing and HUD drawing are skipped. The simulation clock still runs. Use the HUD while developing; use the event for short diagnostic sessions or a deliberate in-app performance screen, not permanent per-frame business logic.
85
86
 
86
87
  ## Reading the numbers
87
88
 
@@ -90,10 +91,24 @@ Telemetry is opt-in inside the renderers. With the HUD disabled and no `performa
90
91
  - High update time: narrow collision groups and reduce active colliders/particles; hidden sprites are excluded from render and collision.
91
92
  - A large `TileLayer` should keep draw calls and visible work stable while the camera moves; one sprite per tile scales with the whole scene instead.
92
93
  - `maxFps: 60` can prevent a 120 Hz display from doubling work when the game does not need it.
93
- - Judge final performance on hardware. The iOS Simulator's translated OpenGL path is intentionally rendered at 1× and remains slower than a device.
94
+ - Judge final performance on hardware. The Apple backend uses Metal on iPhone, iPad, Simulator and Mac Catalyst. Simulator/host fixture results do not establish physical-device GPU timing or presentation.
95
+
96
+ The Apple Simulator keeps a 1× logical drawable; physical iPhone/iPad builds
97
+ use native screen scale. A frame-cap request is platform scheduling, and a
98
+ Metal frame can be skipped when its in-flight slot or drawable is unavailable.
99
+ The next successful simulation frame includes elapsed time capped at 0.1 s.
100
+ Do not compare telemetry across different drawable scales or call submission
101
+ counts a measurement of physically presented frames.
94
102
 
95
103
  ## Common diagnostic order
96
104
 
105
+ For a physics discrepancy, record the installed build/commit, surface size,
106
+ `timeScale`, input values and actual FPS first. `maxFps` is a cap, not a fixed
107
+ physics step. A visible thruster proves an animation state, not acceleration.
108
+ Older builds could erase a small first thrust step; current source preserves
109
+ effective acceleration and settles coasting after contacts. Rope still uses
110
+ per-update damping, but now freezes at zero game delta. See [physics.md](physics.md#time-step-and-frame-rate).
111
+
97
112
  1. Confirm the object was added and is visible/in bounds.
98
113
  2. Turn on the relevant sprite/layer debug shape, then the global overlay if relationships are unclear.
99
114
  3. Verify `collisionGroup` is on the target and `solidWith`/`collidesWith` on the mover.
@@ -1,49 +1,60 @@
1
- # Upstream example catalog
1
+ # Module example catalog
2
2
 
3
- Verified against all 34 self-contained demos under `ti.game@3bea2f4:example/`, plus the launcher `example/app.js` and the shared back-control helper `example/backnav.js` (neither is a demo). `wallhit` now has one user — `worldwrap.js` counts bolt hits with it — but `wallSlideSpeed` and the `onWallLeft`/`onWallRight` flags still have no demo; for those use [the platformer recipe](recipes-action.md#platformer), verified against both native implementations and the README contract.
3
+ Catalog checked against `macCesar/ti.game@2bf5f82:README.md` (Learn from the examples) and `example/app.js`: 35 demos at the reviewed snapshot, 34 touch-ready entries on phones/tablets and the additional Configurable Input entry on Mac Catalyst (`osname === 'macos'`). The launcher and `backnav.js`, `keyboard.js`, and `pool-viewport.js` are helpers, not demos. The current upstream branch is `m1ga/ti.game@88e181c`; the reviewed checkout adds documentation commit `2bf5f82`. These features are not established by the older `3bea2f4` skill pin. `wallhit` now has one user — `worldwrap.js` counts bolt hits with it — but `wallSlideSpeed` and the `onWallLeft`/`onWallRight` flags still have no demo; for those use [the platformer recipe](recipes-action.md#platformer), verified against both native implementations and the README contract.
4
4
 
5
- The demos prove that API combinations run together on the current module. They are **not** production templates: most keep state in one file, repeat launcher/back-button plumbing, and still use pre-ES6 syntax. Start from the closest demo, retain the demonstrated engine calls, then apply the focused structure and cleanup patterns in this skill.
5
+ The demos show intended API combinations; reading them does not verify an
6
+ installed module or device. They are **not** production templates: most keep
7
+ state in one file, repeat launcher/back-button plumbing, and still use pre-ES6
8
+ syntax. Start from the closest demo, retain the demonstrated engine calls,
9
+ then apply the focused structure and cleanup patterns in this skill.
10
+
11
+ For equations, units, genre selection and frame-rate limits, use
12
+ [physics.md](physics.md). Rhythm's 400 ms interval is a quarter note at its
13
+ stated 150 BPM; the current source now corrects the older "eighth" comments. Flappy replaces
14
+ vertical velocity per tap; it does not add repeated impulses. The curated
15
+ Lunar Lander recipe is not an additional upstream demo.
6
16
 
7
17
  | Demo | What it proves | Production-oriented reference |
8
18
  | --- | --- | --- |
19
+ | `inputmap.js` | Native configurable actions shared across keys/mouse/gamepads; focus ownership, pause contexts, movement/aim/crouch/projectiles and raw input monitor | [Shared digital actions](recipes-action.md#shared-digital-actions) |
9
20
  | `basic.js` | Sheets, frame animation, drag/pinch/rotate, tween completion | [Scaffolding](patterns.md#the-scaffolding) |
10
- | `puzzle.js` | Multi-touch drag, press-to-lift, snapping and tween-back | [Cards and board games](recipes-adventure-effects.md#cards-and-board-games) |
11
- | `flappy.js` | Gravity impulse, invisible score/death zones, wrapping parallax | [Tap-to-flap](recipes-action.md#tap-to-flap) |
12
- | `platformer.js` | `solidWith`, `land`, `oneWay`, carried rider, camera follow, multi-touch controls, and the same actions driven by a gamepad | [Platformer](recipes-action.md#platformer) |
13
- | `volley.js` | Restitution ball, `land`, collision response, coarse AI timer | [Ball sports](recipes-action.md#ball-sports--breakout-physics) |
21
+ | `puzzle.js` | Multi-touch drag, press-to-lift, free-cell snapping/re-drag, tween-back, solved spin and Restart | [Cards and board games](recipes-adventure-effects.md#cards-and-board-games) |
22
+ | `flappy.js` | Gravity with replacement flap velocity, invisible score/death zones, five-gate win/reset and wrapping parallax | [Tap-to-flap](recipes-action.md#tap-to-flap) |
23
+ | `platformer.js` | `solidWith`, `land`, `oneWay`, carried rider, camera follow, multi-touch controls, and shared keyboard/gamepad actions | [Platformer](recipes-action.md#platformer) |
24
+ | `volley.js` | Restitution ball, `land`, biased player reflection, opponent score/serve, coarse AI and shared keyboard/gamepad actions | [Ball sports](recipes-action.md#ball-sports--breakout-physics) |
14
25
  | `racing.js` | `carMode`, drift, skid marks, checkpoints and lap state | [Top-down racer](recipes-action.md#top-down-racer) |
15
- | `cards.js` | Dealing, fanned layout, selection tweens and idle wobble | [Cards and board games](recipes-adventure-effects.md#cards-and-board-games) |
16
- | `asteroids.js` | Thrust, angular velocity, wrapping, pooling, additive bolts, effect/music audio | [Space shooter](recipes-action.md#asteroids--space-shooter) |
17
- | `topdown.js` | Small string map made from sprites, `ySort`, eight-way input, decision timer | [Top-down / Zelda](recipes-action.md#top-down--zelda) |
26
+ | `cards.js` | Five-card deal, three-card selection, glow/wobble, Done play/re-fan and Reset sweep/redeal | [Cards and board games](recipes-adventure-effects.md#cards-and-board-games) |
27
+ | `asteroids.js` | Thrust, angular velocity, wrapping, momentum-inheriting pooled/additive bolts, five-rock waves, three-hit lives/invulnerability/reset and audio | [Space shooter](recipes-action.md#asteroids--space-shooter) |
28
+ | `topdown.js` | 12-column/15-row sprite map, animated water/solid house, nonblocking depth-sorted trees, eight-way input and companion follow/tree-state AI | [Top-down / Zelda](recipes-action.md#top-down--zelda) |
18
29
  | `skate.js` | Endless-runner pooling, carried/uncarried terrain, parallax, emitter and audio hooks | [Endless runner](recipes-action.md#endless-runner) |
19
30
  | `pointclick.js` | Tap-to-walk with `findPath` → `followPath`, hotspots and `ySort` | [Point-and-click](recipes-adventure-effects.md#point--click-adventure) |
20
31
  | `particles.js` | Continuous/burst emitters, sprite target, HUD telemetry | [Particles](recipes-adventure-effects.md#particles) and [debugging](debugging-performance.md) |
21
32
  | `worldwrap.js` | `worldWrapX` end to end: seam-aware camera, touch, overlap, solid resolution, swept movement, `wallhit`, and a full-width TileLayer that repeats across the seam | [Circular worlds](api.md#gameview) |
22
33
  | `rhythm.js` | Pooled notes, native velocity, `press` pads, timing and audio feedback | [Rhythm game](recipes-action.md#rhythm-game) |
23
- | `camera.js` | Two-axis follow, bounds, zoom, shake, camera effects, parallax and debug HUD | [Camera work](recipes-adventure-effects.md#camera-work) |
24
- | `rope.js` | Rope pinned to a sprite, fixed head, tail weight | [Ropes and chains](recipes-adventure-effects.md#ropes-and-chains) |
34
+ | `camera.js` | Two-axis follow, bounds, zoom, shake, camera effects, parallax and screen-space `topRight` debug HUD | [Camera work](recipes-adventure-effects.md#camera-work) |
35
+ | `rope.js` | Rope pinned to a sprite, fixed head/tail weight and two draggable ends | [Ropes and chains](recipes-adventure-effects.md#ropes-and-chains) |
25
36
  | `flip.js` | Draw-only flips driven by direction and inverted gravity | [Visual effects](recipes-adventure-effects.md#visual-effects) |
26
37
  | `hitbox.js` | Global/sprite debug overlays and per-axis hitbox correction | [Seeing the hitbox](patterns.md#seeing-the-hitbox--hitboxjs) |
27
38
  | `blend.js` | Normal/add/multiply/screen modes and `flash()` | [Visual effects](recipes-adventure-effects.md#visual-effects) |
28
- | `text.js` | Screen-fixed HUD, world labels, text buttons, word wrap and `attachTo` | [HUD](patterns.md#hud--textjs-flappyjs) |
39
+ | `text.js` | Screen-fixed HUD, world labels, text buttons, word wrap, precomposed ES/DE/NL characters and `attachTo` | [HUD](patterns.md#hud--textjs-flappyjs) |
29
40
  | `swept.js` | Identical fast bullets with and without continuous sweep | [Object pooling](patterns.md#object-pooling) and [API gotchas](api.md#gotchas-the-property-tables-do-not-show) |
30
41
  | `path.js` | Looped/smoothed paths, one-shot paths and animation chains | [Patrol routes](patterns.md#patrol-routes-and-animation-chains--pathjs) |
31
42
  | `raycast.js` | Line of sight, ledge probes and tap-fired hitscan | [Raycast pattern](patterns.md#line-of-sight-ledge-probes-and-hitscan--raycastjs) |
32
43
  | `zones.js` | `collision`/`collisionend`, pressure plate and removal during contact | [Zones](patterns.md#zones-you-can-be-inside-of-enter--exit--zonesjs) |
33
- | `demoscene.js` | Follow-path circles, per-character scroller, glow, blend and music | [Visual effects](recipes-adventure-effects.md#visual-effects) |
34
- | `maze.js` | Raw/simplified A*, re-pathing AI and path visualization | [Pathfinding](patterns.md#pathfinding-around-obstacles--mazejs-pointclickjs) |
44
+ | `demoscene.js` | Arc-offset per-character closed-path sine scroller, additive circle-driven copper bars, glowing logo, chained opacity pulse, period-scrolled starfield and music cleanup | [Visual effects](recipes-adventure-effects.md#visual-effects) |
45
+ | `maze.js` | Raw/simplified A* dots, tap routes, 800 ms game-clock re-pathing chase and catch/den reset | [Pathfinding](patterns.md#pathfinding-around-obstacles--mazejs-pointclickjs) |
35
46
  | `timescale.js` | Slow/frozen scene, engine timer versus real-time interval | [Game-clock timers](patterns.md#timers-on-the-game-clock) |
36
47
  | `circles.js` | Rect/circle/rotated-rect solids and `block` versus bilateral `push` | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
37
48
  | `slopes.js` | Rotated-rect ramp normals and restitution supplied by a surface | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
38
- | `plinko.js` | Circular pegs, swept balls, trigger slots and `solidimpact` audio | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
39
- | `pool.js` | Bilateral circle momentum, rails, pockets, linear damping and `solidimpact` | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
40
- | `drum.js` | Analytic circular containment, contact-driven impulses and `solidimpact` | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
49
+ | `plinko.js` | 86 circular pegs, swept/push chips, rect dividers, trigger payouts, chosen/random one/ten drops and deduplicated impact audio | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
50
+ | `pool.js` | 16 bilateral push balls, rect rails, circular trigger pockets, felt damping, additive anytime shot, pair-deduplicated impact audio/reset and fitted portrait stage | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
51
+ | `drum.js` | Analytic circular containment, 25 push balls, shake, enter/exit-rearmed tumble strip, separate glass/ball impact audio and pair deduplication | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
41
52
  | `wind.js` | `gravityX` as wind and as top-down constant acceleration | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
42
- | `tilemap.js` | One 120×90 `TileLayer`, visible-cell rendering, cell collision, live edits, A* and HUD | [Tile maps](tilemaps.md) |
53
+ | `tilemap.js` | One 120×90 `TileLayer`, visible-cell rendering, cell collision, live edits, A* and HUD relayout on later resizes | [Tile maps](tilemaps.md) |
43
54
 
44
55
  ## Choosing the right map example
45
56
 
46
- `topdown.js` deliberately creates one sprite per tile for a small 16×12 scene; that remains reasonable when individual tiles need sprite behavior. `tilemap.js` is the reference for large or mostly static grids: one native `TileLayer` avoids ticking, sorting, drawing, and collision-scanning off-screen tile sprites.
57
+ `topdown.js` deliberately creates one sprite per tile for a small 12-column by 15-row scene; that remains reasonable when individual tiles need sprite behavior. `tilemap.js` is the reference for large or mostly static grids: one native `TileLayer` avoids ticking, sorting, drawing, and collision-scanning off-screen tile sprites.
47
58
 
48
59
  ## Reading a demo safely
49
60
 
@@ -52,3 +63,85 @@ The demos prove that API combinations run together on the current module. They a
52
63
  3. Replace `var` and function-expression callbacks with the project's current ES6+ style.
53
64
  4. Keep resources outside `resize`; create surface-sized scene objects inside one guarded `resize` path.
54
65
  5. Separate reusable state from launcher UI and add explicit cleanup for app-owned listeners, timers, and sounds.
66
+
67
+ ## Keyboard shortcuts and shared helpers
68
+
69
+ `keyboard.js` builds a native `inputMap`, dispatches `actiondown`/`actionup`
70
+ to the existing gameplay functions, activates/focuses the canvas on window
71
+ open/focus, and sets `inputContext = null` on blur/close. It removes its two
72
+ action listeners on close. It also focuses immediately because a guarded
73
+ scene `resize` may initialize after `Window.open`. This helper replaces the
74
+ entire map; merge bindings in app code if the view already owns other actions.
75
+ Platformer and Volley route mapped gamepad buttons through the same actions;
76
+ do not also dispatch those actions from `buttondown`/`buttonup`.
77
+
78
+ | Demos | Keyboard shortcuts |
79
+ | --- | --- |
80
+ | Platformer, Volley | Left/Right, A/D or J/L move; Space/Up/W/I jump; pad d-pad/left stick and A/up use the same actions |
81
+ | Flappy, Flip | Space/Up/W flap or restart / invert gravity |
82
+ | Skate | Space/Up/W jump or restart |
83
+ | Plinko | Space/1 drops one chip; 0 drops ten |
84
+ | Pool | R resets; mouse/touch aims |
85
+ | Drum | Space shakes; T toggles tumble |
86
+ | Circles | Space shoots; D toggles shapes |
87
+ | Slopes | R resets; D toggles shapes |
88
+ | Zones | Space/R removes or respawns the ball |
89
+ | Text | Space scores; R resets; N advances dialog |
90
+ | Tilemap | B toggles building; D toggles debug; mouse/touch selects cells |
91
+ | Wind | Space drops; Left/Right or A/D changes wind |
92
+ | Particles, Blend | Space creates a centered burst / flashes ships |
93
+ | Rhythm | A/S/D or Left/Down/Right hits the lanes |
94
+ | Swept | Space shoots both lanes; Left/Right or A/D changes speed |
95
+ | Camera | Z/X zooms out/in; Space shakes |
96
+ | Timescale | 1/2/3/4 selects 1/0.5/0.1/0 speed |
97
+
98
+ Configurable Input has its own map: A/D, J/L or arrows move; last pressed
99
+ left/right wins and releasing it restores the other held direction. Space
100
+ or pad A jumps once from the ground. W/I/Up aims up (with horizontal movement
101
+ for diagonal shots), S/K/Down crouches while grounded, left canvas click or
102
+ pad X fires. The normalized direction keeps diagonal projectile speed equal
103
+ to straight shots; crouching changes height while preserving foot position.
104
+ Escape switches between gameplay/paused contexts and toggles `timeScale`.
105
+ The right/middle mouse bindings are test entries: the reviewed docs report
106
+ that they do not trigger on Mac; left click is the verified path.
107
+
108
+ Instructions remain separate from changing action feedback. The monitor
109
+ shows physical key codes/modifiers and pad buttons/sticks/triggers. Focusing
110
+ the native text field disables actions and clears held movement; window
111
+ focus does not steal that text focus. The focus button or first canvas click
112
+ restores game input; that first click does not shoot. Blur/close disables
113
+ actions, close cancels projectile-expiry game timers and unloads the four
114
+ owned sheets. Run the indirect-input configuration from [project setup](project-setup.md)
115
+ before mouse testing on the supported older Apple systems. Physical peripheral
116
+ and overlay acceptance is separate from compilation and resolver fixtures.
117
+
118
+ ## Resizable scenes and portrait Pool
119
+
120
+ The current demos size foreground art against both surface dimensions rather
121
+ than letting a wide Mac window enlarge actors beyond its height. Scenes with
122
+ native overlay controls derive pixels per view point from
123
+ `resize.width / gameView.rect.width`, with a positive finite check and a
124
+ logical-density fallback; monitor/display height is not the GameView size.
125
+
126
+ Pool alone uses `pool-viewport.js`: a 380 by 680 point portrait stage contains
127
+ both GameView and native Reset control. Host `postlayout` applies a uniform
128
+ `min(hostWidth / 380, hostHeight / 680)` transform and removes that listener on
129
+ close. Window resizing fits the running stage; it does not recreate balls,
130
+ timers, collision bounds or drag state. Tilemap instead retains its native
131
+ map and relays later `resize` values to HUD layout. Plinko fits chip/peg sizes
132
+ to horizontal, diagonal and two-row gaps while preserving their proportions;
133
+ wide/short scenes must still leave chip-sized passages.
134
+
135
+ ## Running and verification boundaries
136
+
137
+ From the module's `android/`, use `ti build -p android --sdk 13.4.1.GA` for
138
+ the launcher. On Apple, build the module, copy `example/` into a Classic app's
139
+ `Resources/` and register the `iphone` module; keep the three helpers when
140
+ copying demos that require them. Mac lists Configurable Input; mobile retains
141
+ 34 touch-ready entries, with the input demo source available for external
142
+ peripheral tests. Back uses action-bar Up on Android and a shared overlay on Apple.
143
+
144
+ The catalog is not exhaustive API acceptance: no dedicated wall-slide/jump or
145
+ `unload()` demo exists, nor coverage for every gamepad event or creation-time
146
+ option. `worldwrap.js` does exercise `wallhit`. Use native-contract references
147
+ for uncovered members and verify the actual installed binary on the target.