@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,6 +1,6 @@
1
1
  # Setting up a ti.game project
2
2
 
3
- Installing the module, wiring it into Classic and Alloy projects, organizing art and sound, and the lifecycle work the engine does not do for you. Verified against `ti.game@3bea2f4` (2026-09-02).
3
+ Installing the module, wiring it into Classic and Alloy projects, organizing art and sound, and the lifecycle work the engine does not do for you. Reviewed against `macCesar/ti.game@2bf5f82` (2026-10-23), based on `m1ga/ti.game@88e181c`. See [source-notes.md](source-notes.md) for provenance and verification limits.
4
4
 
5
5
  <!-- TOC-START -->
6
6
  ## Contents
@@ -14,6 +14,8 @@ Installing the module, wiring it into Classic and Alloy projects, organizing art
14
14
  - [Lifecycle and cleanup](#lifecycle-and-cleanup)
15
15
  - [Debugging and tuning](#debugging-and-tuning)
16
16
  - [Platform differences](#platform-differences)
17
+ - [Window resizing, focus and app configuration](#window-resizing-focus-and-app-configuration)
18
+ - [Apple diagnostic workflows](#apple-diagnostic-workflows)
17
19
 
18
20
  <!-- TOC-END -->
19
21
 
@@ -25,8 +27,8 @@ There is no registry and no `ti module install` command — Titanium modules are
25
27
 
26
28
  ```
27
29
  modules/
28
- ├── android/ti.game/0.5.0/
29
- └── iphone/ti.game/0.5.0/
30
+ ├── android/ti.game/1.0.0/
31
+ └── iphone/ti.game/1.0.0/
30
32
  ```
31
33
 
32
34
  Unzipping the module zip at the project root creates exactly that layout.
@@ -37,48 +39,53 @@ Then declare it in `tiapp.xml`. The iOS platform key is `iphone`, not `ios`:
37
39
 
38
40
  ```xml
39
41
  <modules>
40
- <module platform="android" version="0.5.0">ti.game</module>
41
- <module platform="iphone" version="0.5.0">ti.game</module>
42
+ <module platform="android" version="1.0.0">ti.game</module>
43
+ <module platform="iphone" version="1.0.0">ti.game</module>
42
44
  </modules>
43
45
  ```
44
46
 
45
47
  Pinning `version` is worth the extra attribute: with two module versions installed side by side, an unpinned entry silently picks the highest one.
46
48
 
47
- **The version string does not identify a feature set — the build date does.** The manifest went `0.3.0` → **`0.4.0`** on 2026-08-20, **`0.5.0`** on 2026-08-27 and **`0.6.0`** on 2026-09-02, and in between each number kept accepting features without moving. So every one of them covers several incompatible builds:
49
+ ### Requirements and source builds
48
50
 
49
- | A build from | Missing, compared to what this skill documents |
50
- | --- | --- |
51
- | `0.3.0` before 2026-08-18 | `createText`, `createFont`, `screenFixed`, `swept`, `collisionend` |
52
- | `0.3.0` before 2026-08-19 | also `followPath`, `play(name, { then })`, `raycast`, `after`/`every`, `scrollFactor`, `multiply`/`screen` blends |
53
- | `0.3.0` before 2026-08-19 16:00 UTC+2 | additionally bleeds at grid-frame edges on smoothed sheets |
54
- | `0.4.0` — including the 2026-08-20 release zip | `hitboxScaleX`/`hitboxScaleY`, text `maxWidth`, `SpriteSheet.unload()`/`Font.unload()` and the LiveView renderer lifecycle, all added on 2026-08-23 with the manifest untouched |
55
- | `0.4.0` built before 2026-08-24 | additionally rocks side to side through a multi-frame animation on a `smoothing: true` grid sheet — the half-texel inset was one-sided, so the end frames came out slightly wider and off-centre |
56
- | `0.4.0` built before 2026-08-26 | additionally `sprite.attachTo()` / `detach()` / `attachedTo`, and `gameView.follow()` still aborts the app on Android if it is handed anything that is not a sprite |
57
- | `0.4.0` built on 2026-08-26 but before `83b7863` | additionally the opacity an attachment inherits from its target, the `anchor` property, and percentage strings on every ratio — a build in this window has `attachTo` but treats `'55%'` as a number it cannot read |
58
- | `0.4.0` at its last commit (`b780051`) | additionally everything the `0.5.0` bump carries: `hitboxShape: 'rotatedRect'`, circular solids resolved as circles, `solidMode`, `gravityX`, `linearDamping`, and `restitution` read off both sides of a contact. It also still pulls a swept sprite back to where its frame started when it begins the frame already touching a solid, so a `swept: true` body parked on a slope creeps and then breaks loose as if launched |
59
- | `0.5.0` built before `d042060` | the performance HUD object form (`debug: { hud, hitbox, hudFont }`) and the `performance` event |
60
- | `0.5.0` built before `10a046e` | additionally `Game.createTileLayer()`, native visible-cell tile rendering, tile collision/live edits, and TileLayer participation in `findPath` |
61
- | `0.5.0` built before `85a723d` | additionally read-only `onWallLeft` / `onWallRight` and the `wallhit` transition event for sprite and TileLayer walls |
62
- | `0.5.0` built before `05cb60c` | additionally `wallSlideSpeed`, the native downward-speed cap while pressed against a wall |
63
- | `0.5.0` built before `c216e7f` | on Android only, string-row TileLayers with a nested JS `legend` decode as empty because the proxy rejects the ordinary map object; numeric data is unaffected |
64
- | `0.5.0` built before `d7d471e` (2026-08-28) | additionally the whole gamepad surface: `buttondown`/`buttonup`, `stick`, `trigger`, `gamepadconnected`/`gamepaddisconnected`, and the `gamepads`/`gamepad`/dead-zone properties |
65
- | `0.5.0` built before `af2e544` (2026-08-29) | additionally circular horizontal worlds: `gameView.worldWrapX` and per-sprite `wrapWorldX` |
66
- | `0.5.0` built before `80d8f2b` (2026-08-30) | additionally the `solidimpact` event and `impactThreshold` |
67
- | `0.5.0` built before `9c45cf6` (2026-09-02) — **this includes the artifacts most people have** | no crash fixes: writing `y`/`zIndex` from JS can abort the GL thread with *Comparison method violates its general contract*; a sound whose load failed throws `IllegalStateException` on the next `play()`; passing a mistyped value to `sheet`/`target`/`head`/`tail`/`font` aborts the process in JNI instead of raising a JS error. Also `pinch`/`rotate` fire per motion event instead of ~10 Hz, two-finger rotation can add a whole turn at the ±180° flip, the tap/drag slop ignores `cameraScale`, `cameraX`/`cameraY`/`cameraScale`/`cameraBounds`/`throttle`/`steering` are dropped when passed to a `create*` call, a removed sprite stays retained as the camera's follow target, `collidesWith`/`solidWith` reject a bare string, and the iOS glitch shader goes NaN after roughly 27 minutes |
68
-
69
- The newest upstream release is `0.5.0` (2026-09-02), and for once the tag is ahead of the local habit rather than behind it: it sits on `9c45cf6`, so the published zip carries every fix in the row above, while a module built locally from source any time in the previous two weeks reports the same `0.5.0` and does not. Same number, different engine — prefer the release artifact, or rebuild from upstream `main` and let the manifest read `0.6.0`. If a documented call is `undefined`, check the manifest and build commit before hunting for a typo, and rebuild from upstream `main`. More reliable than either is feature detection: read a property **before** writing it (`typeof sprite.hitboxScaleX === 'undefined'`) or probe a method/factory (`typeof Game.createTileLayer === 'function'`). Write-then-read is not proof because Kroll can retain an unknown property on the proxy.
70
-
71
- Building the module from source:
72
-
73
- ```bash
74
- cd android
75
- ti build -p android --build-only # → android/dist/ti.game-android-<version>.zip
76
- ti build -p ios --build-only # macOS only → ios/dist/ti.game-iphone-<version>.zip
51
+ Both manifests declare version `1.0.0` and minimum Titanium SDK `13.4.0`.
52
+ The documented build SDK is `13.4.1.GA`. Android uses OpenGL ES 2.0.
53
+ Apple uses Metal and requires iOS/iPadOS 15 or later; the package contains
54
+ arm64 devices, arm64/x86_64 Simulator and arm64/x86_64 Mac Catalyst slices.
55
+ Architecture presence does not establish execution on that hardware.
56
+
57
+ From the module repository root:
58
+
59
+ ```sh
60
+ (cd android && ti build -p android --build-only --sdk 13.4.1.GA)
61
+ (cd ios && ti build -p ios --build-only --sdk 13.4.1.GA)
62
+ ```
63
+
64
+ Apple requires macOS/Xcode. Its `ios/titanium.xcconfig` selects
65
+ `13.4.1.GA`; selecting another SDK in the CLI also requires changing that
66
+ Xcode SDK setting or providing an external override:
67
+
68
+ ```sh
69
+ cat > /tmp/ti-game-sdk.xcconfig <<'CONFIG'
70
+ TITANIUM_SDK_VERSION = 13.4.1.GA
71
+ CONFIG
72
+ XCODE_XCCONFIG_FILE=/tmp/ti-game-sdk.xcconfig ti build -p ios --build-only --sdk 13.4.1.GA
77
73
  ```
78
74
 
75
+ Run the latter command inside `ios/`. Applications need no `default.metallib`:
76
+ shaders are embedded and compiled once during device initialization. A native
77
+ initialization error logs a diagnostic and stops the view; there is no public
78
+ backend selector or production Apple OpenGL fallback.
79
+
80
+ Upstream's nightly is an automated `main` build, not a stable release. Record
81
+ source commit and ZIP SHA-256 for any installed artifact: version `1.0.0`
82
+ alone does not establish the reviewed fork's feature set. Probe a method before
83
+ calling it; read a property before writing it. Unknown Kroll properties can
84
+ round-trip without native support, so write-then-read is not proof.
85
+
79
86
  ## Classic projects
80
87
 
81
- `require('ti.game')` anywhere and add the GameView to a window. The 33 demos in the module's `example/` folder are Classic, each one a CommonJS module exporting a start function (`app.js` is the launcher, not a demo):
88
+ `require('ti.game')` anywhere and add the GameView to a window. The launcher has 34 mobile touch demos and a 35th Mac input demo; the demos in the module's `example/` folder are Classic, each one a CommonJS module exporting a start function (`app.js` is the launcher; `backnav.js`, `keyboard.js`, and `pool-viewport.js` are helpers):
82
89
 
83
90
  ```javascript
84
91
  // Resources/flappy.js
@@ -111,8 +118,8 @@ The standard Alloy element for a native module view. `method` names the factory
111
118
  ```xml
112
119
  <!-- app/views/index.xml -->
113
120
  <Alloy>
114
- <Window id="index" navBarHidden="true" exitOnClose="true" onOpen="onOpen" onClose="onClose">
115
- <Module id="gameView" module="ti.game" method="createGameView" backgroundColor="#8ed8f8" />
121
+ <Window id="index" navBarHidden="true" exitOnClose="true" onClose="onClose">
122
+ <Module id="gameView" module="ti.game" method="createGameView" backgroundColor="#8ed8f8" onResize="onResize" />
116
123
  <Button id="jumpButton" title="JUMP" onTouchstart="jump" />
117
124
  </Window>
118
125
  </Alloy>
@@ -126,10 +133,6 @@ const heroSheet = Game.createSpriteSheet({ image: 'hero.png', frameWidth: 64, fr
126
133
 
127
134
  let initialized = false;
128
135
 
129
- function onOpen() {
130
- $.gameView.addEventListener('resize', onResize);
131
- }
132
-
133
136
  function onResize(e) {
134
137
  if (initialized) {
135
138
  return;
@@ -270,11 +273,17 @@ const mono = Game.createFont({ image: 'fonts/mono.png', charWidth: 9, charHeight
270
273
 
271
274
  These are **bitmap** fonts — do not put them in `app/assets/fonts/` expecting Titanium's native font handling, and do not use a `.ttf` here. To go from a TTF to either format, the module ships `tools/genfont.py`, which rasterizes it into a grid or BMFont atlas.
272
275
 
273
- The built-in font covers ASCII 32..126 only. Any game whose text includes `ñ`, accents or `¿` needs a generated font — or a `Ti.UI.Label` overlay for that specific string.
276
+ The built-in Press Start 2P font covers ASCII 32..126 plus
277
+ `ÁÉÍÓÚÜÑáéíóúüñ¿¡ÄÖäößËÏëïÈèÊêÀà`. Use precomposed characters;
278
+ combining accents and capital `ẞ` are unsupported (`SS` is the documented
279
+ uppercase alternative). Custom grid fonts still default to ASCII. Generate
280
+ a custom font for other characters. The README's older Draw text paragraph
281
+ still says ASCII-only; its current Font contract and both embedded native
282
+ character maps establish this expanded repertoire.
274
283
 
275
284
  ## Lifecycle and cleanup
276
285
 
277
- The engine handles what it owns: the render loop pauses and resumes with the activity or app, music pauses and resumes with it, and textures survive an EGL context loss. **Everything you created in JS is yours to stop.**
286
+ The engine handles what it owns: the render loop pauses and resumes with the activity or app, music pauses and resumes with it, and Android textures survive an EGL context loss. Apple retains in-flight GPU resources until Metal commands complete. **Everything you created in JS is yours to stop.**
278
287
 
279
288
  A leaked `setInterval` keeps running after the window closes, keeps reading sprite properties from a dead scene and keeps the whole closure alive. A looping `createSound` keeps playing over the next screen.
280
289
 
@@ -319,7 +328,7 @@ function listen(target, eventName, handler) {
319
328
 
320
329
  ### GPU textures: `unload()`
321
330
 
322
- Sheets and fonts hold GL textures, and until 2026-08-23 nothing but losing the context ever freed one. `spriteSheet.unload()` and `font.unload()` release the texture on the next rendered frame, from the render thread. Releasing the proxy does the same, so a sheet that goes out of scope no longer strands its memory.
331
+ Sheets and fonts hold GPU textures. `spriteSheet.unload()` and `font.unload()` schedule texture disposal on the render thread; Apple retains resources already encoded in pending Metal commands until completion. Releasing the proxy does the same, so a sheet that goes out of scope no longer strands its memory.
323
332
 
324
333
  This is for **level streaming** — a game that loads world 2's atlases while world 1's are still resident. It is not a cache: the unload is permanent, and any sprite still pointing at that sheet stops drawing.
325
334
 
@@ -332,6 +341,29 @@ function unloadLevel(level) {
332
341
 
333
342
  A single-level game with a handful of sheets should not call it at all: the textures die with the window.
334
343
 
344
+ ### Retained iOS scenes and multiple GameViews
345
+
346
+ In the pinned iOS source, `TiGameGameView.didMoveToWindow` calls
347
+ `shutdownRendering` when an attached view loses its window. That teardown
348
+ sets a terminal shutdown flag and releases the renderer. Reattaching the
349
+ same proxy or calling resume is not a documented reconstruction mechanism.
350
+ If a navigation/modal presentation detaches a backdrop that must remain
351
+ alive, keep that GameView attached or create a fresh one on return; rebuilding
352
+ only its Sprite session is insufficient. Verify the actual UIKit presentation
353
+ and installed module on-device.
354
+
355
+ The reviewed Apple controller implementation uses a shared dispatcher with
356
+ weak per-view receivers (`TGAttachGamepadDevice` / `TGDetachGamepadDevice`).
357
+ Closing one receiver no longer clears another view's physical controller
358
+ handler. Define gameplay input ownership explicitly: retained views may still
359
+ observe gamepad events. Focused keyboard/actions have separate ownership rules
360
+ in [input.md](input.md).
361
+
362
+ Sources: `ios/Classes/TiGameGameView.m` (`didMoveToWindow`,
363
+ `shutdownRendering`) and `ios/Classes/TGGamepadController.m` at the reviewed
364
+ pin. Native inspection verifies lifecycle contracts, not a specific navigation
365
+ presentation on a device.
366
+
335
367
  ### LiveView reloads
336
368
 
337
369
  A LiveView reload replaces Titanium's JS runtime while the native side survives, which used to leave the previous `GameView`'s render loop running — every reload added another GL thread, and the audio engine handed the new runtime sample ids the old one still owned.
@@ -351,14 +383,66 @@ Since 2026-08-23 the module retires the previous generation itself: stale render
351
383
 
352
384
  ## Platform differences
353
385
 
354
- The JS API is identical on Android and iOS. What differs is around it:
386
+ The public API is mostly shared. Preserve these platform contracts:
355
387
 
356
388
  - **`Ti.UI.createNotification` (Android Toast) does not exist on iOS.** The demos branch on `Ti.Platform.osname` and fall back to `Ti.UI.createAlertDialog`.
357
- - **The iOS Simulator renders at a 1x logical drawable** (its translated OpenGL path is disproportionately expensive at 3x Retina) and is noticeably slower than any real device. Real iPhone and iPad builds keep the device's native screen scale. Judge performance on device.
389
+ - **The iOS Simulator renders at a 1x logical drawable** (the Metal port retains this policy for comparisons). Simulator timing is not a controlled comparison with physical hardware. Real iPhone and iPad builds keep the device's native screen scale. Judge performance on device.
358
390
  - **`logicalDensityFactor` lies in the iOS Simulator** — it reports the device scale while the surface renders at 1x. Derive dp→scene units from the surface instead: `H / Ti.Platform.displayCaps.platformHeight`.
359
391
  - **Android's `touchFeedback` ripple** cannot animate over the GL canvas and logs `RippleDrawable` errors; give on-screen buttons manual `backgroundColor` feedback on `touchstart`/`touchend`.
360
392
  - **Sound formats**: WAV, MP3 and OGG on Android; WAV, MP3 and M4A on iOS. WAV for effects, MP3 for music covers both.
361
393
  - **TileLayer grid configuration**: assign `legend`, `firstGid`, `cols`, and `rows` in `createTileLayer()`. iOS accepts later writes; Android treats them as creation-time inputs.
362
394
  - **Android `legend` compatibility**: string-row maps require build `c216e7f` or newer. Older Android proxies ignore a normal nested JS legend object; use numeric data or update the module.
363
- - **Performance telemetry**: `averagePresentMs` and `presentFailures` exist only on iOS and are omitted on Android.
395
+ - **Performance telemetry**: `averagePresentMs` and `presentFailures` exist only on Apple and are omitted on Android. They measure CPU drawable acquisition/submission and drawable/command failures, not GPU time or confirmed physical presentation.
364
396
  - **`theme: 'Theme.Titanium.DayNight.NoTitleBar'`** on the window is the Android idiom for a fullscreen game surface; on iOS use `navBarHidden`/`statusBarHidden` (or the Alloy XML equivalents).
397
+
398
+ ## Window resizing, focus and app configuration
399
+
400
+ The guarded first `resize` creates the level once. It does not make that level
401
+ responsive: handle later dimensions to reposition HUD/controls and update
402
+ bounds without resetting gameplay. Native touch points are surface pixels;
403
+ Titanium overlay dimensions are points/dp. Derive their conversion from the
404
+ actual canvas, including safe-area and window changes. Pool's source helper
405
+ uses a centered 380×680 point portrait stage; other demos generally use the
406
+ full available canvas. See [examples.md](examples.md).
407
+
408
+ `extendSafeArea` changes whether window children draw behind system bars.
409
+ For keyboard/mouse, configure the consuming app's indirect-input plist and
410
+ focus ownership before testing; see [input.md](input.md). A fullscreen theme
411
+ alone does not configure launch orientation or guarantee hidden system bars.
412
+
413
+ Orientation and launch-screen policy are Titanium app configuration, absent
414
+ from the module tutorial. Use `ti-ui` and `ti-guides` before adding a fixed
415
+ orientation: check the app's `tiapp.xml`, iPhone/iPad supported orientations,
416
+ launch screen, root window and applicable Android activities before opening
417
+ the first game window. Do not treat the older `ti-ui` orientation summary as
418
+ proof of complete multi-activity or current launch-screen configuration; that
419
+ cross-skill audit remains outside this module refresh.
420
+
421
+ ## Apple diagnostic workflows
422
+
423
+ From the source checkout, `python3 tests/apple-renderer/run.py` builds native
424
+ renderer XCTest fixtures (Xcode and XcodeGen required). Its default target is
425
+ iPhone 16e/iOS 18.6; choose another installed runtime with `--destination` and
426
+ `--deployment-target`. Catalyst uses
427
+ `--destination 'platform=macOS,variant=Mac Catalyst' --deployment-target 15.0`.
428
+ `--baseline` compares the preserved OpenGL baseline `127a08c`; this does not
429
+ add an OpenGL backend to the module. Device execution needs
430
+ `--signing-profile`, `--signing-team` and `--signing-identity` together.
431
+ Outputs remain under `/tmp` by default.
432
+
433
+ After building the Apple ZIP, generate an isolated Classic smoke-test app:
434
+
435
+ ```sh
436
+ python3 tests/apple-renderer/prepare_demos.py --icon /path/to/DefaultIcon.png
437
+ python3 tests/apple-renderer/prepare_demos.py --icon /path/to/DefaultIcon.png --include-input --output /tmp/ti-game-mac-demos
438
+ python3 tests/apple-renderer/test_prepare_demos.py
439
+ ```
440
+
441
+ The helper discovers launcher demos, defaults to SDK `13.4.1.GA` (`--sdk`
442
+ overrides), enables indirect pointer events, and writes diagnostics to
443
+ `metal-demos.json` in the generated app's data directory. Native fixture and
444
+ smoke checks do not replace visual/gameplay review. Current upstream evidence
445
+ records 18 renderer cases on Apple Silicon Catalyst and a physical iPad;
446
+ physical iPhone, Intel execution, complete interactive acceptance and controlled
447
+ performance comparisons remain pending. This skill audit does not rerun or
448
+ extend that hardware evidence.
@@ -1,10 +1,15 @@
1
1
  # Action-game recipes
2
2
 
3
- Production-oriented patterns distilled from the upstream action demos at `ti.game@3bea2f4`. Use [examples.md](examples.md) to locate the full runnable demo and [api.md](api.md) for exact defaults and payloads. The wall-contact API is source/README-backed because upstream has not added a wall-jump demo yet.
3
+ Production-oriented patterns distilled from the action demos and public contracts at `macCesar/ti.game@2bf5f82` (documentation checkout above upstream `m1ga/ti.game@88e181c`; previous skill pin `3bea2f4`). Use [examples.md](examples.md) to locate the full runnable demo and [api.md](api.md) for exact defaults and payloads. The wall-contact API is source/README-backed because upstream has not added a wall-jump demo yet.
4
+
5
+ Use [physics.md](physics.md) for the implemented equations, units, jump tuning,
6
+ inertia versus direct control, and numerical limits. Recipes select native
7
+ mechanisms; they do not replace that model reference.
4
8
 
5
9
  <!-- TOC-START -->
6
10
  ## Contents
7
11
 
12
+ - [Shared digital actions](#shared-digital-actions)
8
13
  - [Tap-to-flap](#tap-to-flap)
9
14
  - [Platformer](#platformer)
10
15
  - [Top-down / Zelda](#top-down--zelda)
@@ -17,9 +22,83 @@ Production-oriented patterns distilled from the upstream action demos at `ti.gam
17
22
 
18
23
  <!-- TOC-END -->
19
24
 
25
+ ## Shared digital actions
26
+
27
+ The current `inputmap.js` demonstrates movement, grounded jumps, normalized
28
+ straight/diagonal shots, grounded crouching with fixed feet, separate
29
+ instructions/feedback, and native text-field ownership. [The catalog](examples.md#keyboard-shortcuts-and-shared-helpers)
30
+ lists every shortcut and the Mac-only right/middle mouse limitation. Its
31
+ last-pressed-direction policy differs deliberately from Platformer/Volley,
32
+ where opposite directions cancel and releasing one resumes the other.
33
+
34
+ For keyboard and digital gamepad actions, write native velocity on action
35
+ transitions; choose one gameplay route per action. This curated extraction
36
+ assumes `player`, `RUN_SPEED`, `jump()` and `win` already exist:
37
+
38
+ ```javascript
39
+ const held = { left: false, right: false };
40
+ gameView.inputMap = {
41
+ gameplay: {
42
+ left: [{ device: 'keyboard', code: 'ArrowLeft' }, { device: 'gamepad', button: 'left' }],
43
+ right: [{ device: 'keyboard', code: 'ArrowRight' }, { device: 'gamepad', button: 'right' }],
44
+ jump: [{ device: 'keyboard', code: 'Space' }, { device: 'gamepad', button: 'a' }]
45
+ }
46
+ };
47
+ function applyDirection() {
48
+ player.velocityX = (Number(held.right) - Number(held.left)) * RUN_SPEED;
49
+ }
50
+ function down(e) {
51
+ if (Object.prototype.hasOwnProperty.call(held, e.action)) {
52
+ held[e.action] = true;
53
+ applyDirection();
54
+ } else if (e.action === 'jump') {
55
+ jump();
56
+ }
57
+ }
58
+ function up(e) {
59
+ if (Object.prototype.hasOwnProperty.call(held, e.action)) {
60
+ held[e.action] = false;
61
+ applyDirection();
62
+ }
63
+ }
64
+ function focus() {
65
+ gameView.inputContext = 'gameplay';
66
+ gameView.focusInput();
67
+ }
68
+ function release() {
69
+ gameView.inputContext = null;
70
+ held.left = held.right = false;
71
+ applyDirection();
72
+ }
73
+ gameView.addEventListener('actiondown', down);
74
+ gameView.addEventListener('actionup', up);
75
+ win.addEventListener('open', focus);
76
+ win.addEventListener('focus', focus);
77
+ win.addEventListener('blur', release);
78
+ win.addEventListener('close', () => {
79
+ release();
80
+ gameView.removeEventListener('actiondown', down);
81
+ gameView.removeEventListener('actionup', up);
82
+ });
83
+ ```
84
+
85
+ Native actions aggregate all bindings: releasing one key/controller leaves
86
+ the action held until the last source releases; repeat does not retrigger it.
87
+ A context change releases actions and requires a fresh press. Do not route
88
+ mapped pad buttons through individual button listeners as well. When keeping
89
+ touch buttons, track each touch source separately from the aggregated native
90
+ action, as Platformer/Volley do, so a key release cannot cancel a held touch.
91
+
92
+ With a text field, disable the action context on field focus and clear held
93
+ state; restore it and call `focusInput()` only on explicit canvas/focus-button
94
+ return. A paused context can retain just Escape to resume, as in the demo;
95
+ `timeScale = 0` alone does not disable input. Track/cancel owned game timers
96
+ on close. [Project setup](project-setup.md) owns platform and plist prerequisites;
97
+ [API](api.md) owns map validation, raw events and focus contracts.
98
+
20
99
  ## Tap-to-flap
21
100
 
22
- Gravity plus an impulse per tap. Scoring and death are collisions with invisible zones. The rotation tween is what sells the arc.
101
+ Gravity plus a replacement vertical velocity per tap, not an accumulated impulse. Scoring and death are collisions with invisible zones. The rotation tween is what sells the arc.
23
102
 
24
103
  ```javascript
25
104
  const pig = Game.createSprite({
@@ -64,7 +143,8 @@ pig.addEventListener('collision', (e) => {
64
143
  });
65
144
  ```
66
145
 
67
- Obstacles are pooled gates: three sprites each (top pipe, bottom pipe, invisible score zone) that scroll left on `velocityX` and get repositioned when they leave the screen. Freezing the game means setting every scroller's `velocityX = 0` and the player's `gravity = 0` — the engine has no global "stop", but `gameView.timeScale = 0` is one if you want the scene frozen mid-flight.
146
+ Obstacles are pooled gates: three sprites each (top pipe, bottom pipe, invisible score zone) that scroll left on `velocityX` and get repositioned when they leave the screen. The demo wins after five gates; a collision with a pipe or ground ends the
147
+ run, and the next press resets. Freezing the game means setting every scroller's `velocityX = 0` and the player's `gravity = 0` — the engine has no global "stop", but `gameView.timeScale = 0` is one if you want the scene frozen mid-flight.
68
148
 
69
149
  ## Platformer
70
150
 
@@ -172,7 +252,10 @@ Set `carryRiders: false` on terrain that scrolls under a player who should stay
172
252
 
173
253
  ## Top-down / Zelda
174
254
 
175
- A tile map from a string array, plus `ySort` for depth. Only water is solid; trees are pure decoration you can walk behind.
255
+ A tile map from a string array, plus `ySort` for depth. Water tiles and the separate house block movement; trees are nonblocking
256
+ decoration you can walk behind. The abbreviated map below shows the tile
257
+ portion; add the house as a separate `collisionGroup: 'solid'`, `ySort: true`
258
+ sprite at the actors' `zIndex`, as in the demo.
176
259
 
177
260
  ```javascript
178
261
  const MAP = [
@@ -200,7 +283,7 @@ for (let row = 0; row < ROWS; row++) {
200
283
  width: TILE,
201
284
  height: TILE,
202
285
  zIndex: 0,
203
- collisionGroup: (kind === 'W') ? 'solid' : null // only water blocks
286
+ collisionGroup: (kind === 'W') ? 'solid' : null // only water tiles block; house is a separate solid
204
287
  }));
205
288
  }
206
289
  }
@@ -236,8 +319,9 @@ let lastFacing = 'down';
236
319
  let walking = false;
237
320
 
238
321
  function applyMovement() {
239
- player.velocityX = moveX * SPEED;
240
- player.velocityY = moveY * SPEED;
322
+ const speed = (moveX !== 0 && moveY !== 0) ? SPEED / Math.sqrt(2) : SPEED;
323
+ player.velocityX = moveX * speed;
324
+ player.velocityY = moveY * speed;
241
325
  if (moveX === 0 && moveY === 0) {
242
326
  player.stop();
243
327
  walking = false;
@@ -278,6 +362,18 @@ timer = setInterval(() => {
278
362
  }, 100);
279
363
  ```
280
364
 
365
+ The full companion AI in `topdown.js` adds a timed state machine to the
366
+ simple follow extraction above: sit while the player is idle, stand while
367
+ near a moving player, and walk when outside follow distance. After a startup
368
+ grace period it may approach a nearby tree, face the trunk and play `pee`
369
+ for 1800 ms, then wait 12000 ms before another visit. If the player leaves
370
+ more than five tiles away while it approaches, cancel that visit and use a
371
+ 4000 ms retry delay. A started `pee` finishes before following resumes.
372
+ Its 100 ms JS decision interval is cleared on window close; native velocity
373
+ and animation still perform all motion. Water tiles also play staggered
374
+ `flow` animations so the shore does not ripple in sync; track any startup
375
+ JS timeouts when adapting the demo for repeated screen entry.
376
+
281
377
  ## Top-down racer
282
378
 
283
379
  `carMode` gives you throttle, steering and emergent drift. You supply the track and the lap logic.
@@ -415,10 +511,21 @@ warnTimer = setInterval(() => {
415
511
  ```
416
512
 
417
513
  Rocks are round (`hitboxShape: 'circle'`), drift on random velocities, spin on `angularVelocity`, and are recycled by `visible = false` rather than removed.
514
+ The demo starts five rocks; clearing them starts another wave. The ship keeps
515
+ flying after a hit, loses one of three lives and receives brief invulnerability.
516
+ Its last life blinks red, and zero lives resets the game. The nose-fired pooled
517
+ bolts inherit ship velocity; hold-to-fire only schedules shots, never movement.
518
+ Stop the looping thruster sound on release and every owned sound/timer on close.
418
519
 
419
520
  ## Lunar lander
420
521
 
421
- Same flight model as Asteroids, with gravity on and a landing test. There is no "landing" event — you judge the touchdown yourself in the `land` handler, where velocities are still readable.
522
+ Same flight model as Asteroids, with gravity and a touchdown rule. This is a
523
+ curated recipe, not one of the 35 module demos. `land` reports grounding
524
+ after the solid response: live velocity may already be zero or reflected.
525
+ For a static sprite pad, use the snapshotted normal speed in `solidimpact`.
526
+ It is compensated for this frame's acceleration, not the full incoming
527
+ velocity vector. Tiles do not emit that event; use a sprite pad when the
528
+ impact must be measured. See [landing evidence](physics.md#landing-and-impact-measurements).
422
529
 
423
530
  ```javascript
424
531
  const lander = Game.createSprite({
@@ -428,6 +535,7 @@ const lander = Game.createSprite({
428
535
  gravity: world.gravity, // per-world, e.g. H * 0.35 for the Moon
429
536
  maxSpeed: MAX_SPEED,
430
537
  hitboxScale: 0.8,
538
+ impactThreshold: 0, // include gentle physical impacts
431
539
  solidWith: ['terrain', 'pad'],
432
540
  collidesWith: ['hazard']
433
541
  });
@@ -435,12 +543,12 @@ const lander = Game.createSprite({
435
543
  const MAX_LANDING_SPEED = H * 0.18;
436
544
  const MAX_LANDING_TILT = 12; // degrees
437
545
 
438
- lander.addEventListener('land', (e) => {
439
- const speed = Math.abs(lander.velocityY);
546
+ lander.addEventListener('solidimpact', (e) => {
547
+ const speed = e.speed; // immutable contact payload, not live velocity
440
548
  const tilt = Math.abs(((lander.rotation % 360) + 540) % 360 - 180);
441
549
  const onPad = e.group === 'pad';
442
550
 
443
- if (onPad && speed <= MAX_LANDING_SPEED && tilt <= MAX_LANDING_TILT) {
551
+ if (onPad && e.normalY < -0.7 && speed <= MAX_LANDING_SPEED && tilt <= MAX_LANDING_TILT) {
444
552
  succeed();
445
553
  return;
446
554
  }
@@ -458,12 +566,17 @@ function crash() {
458
566
  }
459
567
  ```
460
568
 
461
- Thrust burns fuel on the same coarse tick that reads the controls — 80 ms is fast enough to feel responsive and slow enough to stay off the bridge:
569
+ Fuel and control decisions use a coarse game-clock tick. `BURN_RATE` is fuel
570
+ units per game second; each nominal tick removes the rate times its interval.
571
+ GameView timers can fire late and do not catch up after stalls, so this is a
572
+ cadence approximation, not exact elapsed-time accounting. Store the timer ID
573
+ and cancel it with `gameView.cancelTimer(timer)` on cleanup:
462
574
 
463
575
  ```javascript
464
- timer = setInterval(() => {
576
+ const CONTROL_MS = 80;
577
+ timer = gameView.every(CONTROL_MS, () => {
465
578
  if (mainThrusterHeld && fuel > 0) {
466
- fuel -= BURN_RATE;
579
+ fuel = Math.max(0, fuel - BURN_RATE * CONTROL_MS / 1000);
467
580
  lander.thrust = THRUST;
468
581
  exhaust.emitting = true;
469
582
  } else {
@@ -471,10 +584,13 @@ timer = setInterval(() => {
471
584
  exhaust.emitting = false;
472
585
  }
473
586
  fuelLabel.text = Math.max(0, Math.round(fuel)) + '%';
474
- }, 80);
587
+ });
475
588
  ```
476
589
 
477
- Wind is just a horizontal acceleration you re-apply on that same tick by nudging `velocityX`; per-world gravity, drag and wind are plain numbers in a catalog object.
590
+ Set `lander.gravityX = world.wind` in px/s² when the wind changes. Native
591
+ integration applies it every frame; do not add velocity repeatedly from a JS
592
+ timer. For lateral-velocity limits or exact vector touchdown tests, normal
593
+ impact speed alone is insufficient: design a separate measurement contract.
478
594
 
479
595
  ## Endless runner
480
596
 
@@ -599,7 +715,14 @@ function hitLane(lane, pad) {
599
715
  }
600
716
  ```
601
717
 
602
- An invisible `misszone` trigger below the pads catches gems that got away and counts the miss. Derive the fall speed from the beat: `FALL_SPEED = (PAD_Y + NOTE) / 1.6` with a 400 ms spawn interval puts four eighth notes between spawn and pad at 150 BPM.
718
+ An invisible `misszone` trigger below the pads catches gems that got away and
719
+ counts the miss. `FALL_SPEED = (PAD_Y + NOTE) / 1.6` gives a 1.6 s trip from
720
+ `-NOTE` to `PAD_Y`. At the stated 150 BPM, `60000 / 150 = 400` ms is a quarter
721
+ note; 1.6 s is four quarter notes (eight eighths). The current source corrects the older "eighth" comments
722
+ while preserving the timing constants; an old copy of the demo may still have that wording.
723
+ Position windows convert to time with `windowSeconds = windowPx / FALL_SPEED`.
724
+ The demo uses a JS interval and independent music playback, not an audio-clock
725
+ beat scheduler; see [rhythm timing](physics.md#rhythm-timing).
603
726
 
604
727
  ## Ball sports / breakout physics
605
728
 
@@ -632,9 +755,17 @@ ball.addEventListener('collision', (e) => {
632
755
  });
633
756
  ```
634
757
 
758
+ In Volley, touching the floor scores for the opposite side and serves from
759
+ the center toward the side that conceded. The player/opponent stay on their
760
+ own sides of the net; the AI follows the ball on its side before jumping.
761
+
635
762
  Note the split: walls and the net are `solidWith` (the engine bounces the ball off them for free), while the players and the floor are `collidesWith` (you decide what happens). For breakout, bricks go in `collidesWith` and are removed with `visible = false` on hit while the paddle and walls stay solid.
636
763
 
637
- A `restitution: 1` ball on an invisible floor bounces forever with no JS at all — and since 2026-08-27 the bounce can live on the *floor* instead, which is what makes one springy platform in an otherwise dead level a one-property change. Round obstacles, bumpers that knock each other around and a table that brings everything to rest are all in [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs).
764
+ A `restitution: 1` ball reflects larger impacts without restitution loss;
765
+ the small-bounce cutoff can still settle it. Restitution can live on the
766
+ floor, so one springy platform can bounce otherwise non-bouncy riders.
767
+ Round obstacles, mutual push bodies and pool damping are in
768
+ [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs).
638
769
 
639
770
  **Opponent AI** on an 80 ms timer, reading a handful of live properties per tick:
640
771