@maccesar/titools 4.16.0 → 4.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -177,7 +177,7 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
177
177
  | ti-expert | Architecture and implementation | Starting point for most tasks |
178
178
  | purgetss | Utility-first styling | UI styling and animations |
179
179
  | ti-ui | UI/UX patterns | Complex layouts, ListViews, platform UI |
180
- | ti-game | 2D games with the `ti.game` module | Sprites, physics, tile layers, collisions, pathfinding, text/performance HUDs, particles, camera |
180
+ | ti-game | 2D games with the `ti.game` module | Sprites, physics, tile layers, wall jumps, pathfinding, text/performance HUDs, particles, camera |
181
181
  | ti-api | Complete Titanium API reference | Looking up properties, methods, events |
182
182
  | ti-guides | SDK fundamentals | tiapp.xml, Hyperloop, distribution, JDK/Xcode compatibility, release notes |
183
183
  | ti-howtos | Native feature integration | Location, push, media, platform APIs |
@@ -436,6 +436,7 @@ When it activates:
436
436
  - Sprites, sprite sheets, TexturePacker atlases, frame animations, native tweens
437
437
  - Physics: gravity and velocity, solid platforms, one-way floors, bouncing, the arcade car model, Newtonian thrust
438
438
  - Collision groups, invisible trigger zones, enter/exit events, swept AABB for fast bullets, per-axis hitbox tuning with the `debug` overlays
439
+ - Native side-wall state, `wallhit` transitions and `wallSlideSpeed` for wall jumps/slides without a JS frame loop
439
440
  - Raycasts for line of sight, ledge probes and hitscan shots
440
441
  - A* pathfinding (`findPath`) around tagged obstacles, feeding native patrol paths (`followPath`) and animation chaining (`play(name, { then })`)
441
442
  - Bitmap-font text sprites with native word wrap, and screen-fixed HUDs inside the scene
@@ -459,12 +460,14 @@ Example prompts:
459
460
  "Tap to walk, but the player should go around the tree instead of through it."
460
461
  "Render a 200x200 Tiled map without creating one sprite per tile."
461
462
  "Show FPS, p95 frame time, draw calls and texture switches while I tune this scene."
463
+ "Add wall jumps and a slow wall slide without polling collisions."
462
464
  ```
463
465
 
464
466
  Key rules:
465
467
  - JS describes the scene and reacts to events; the engine runs every frame. Never move a sprite from a timer — use velocity, gravity, a tween, `carMode` or `thrust`
466
468
  - Build the level inside a guarded `resize` handler, never from `Ti.Platform.displayCaps`
467
469
  - `solidWith` blocks, `collidesWith` reports — they are independent
470
+ - Gate wall jumps on `onWallLeft` / `onWallRight`; reserve `wallhit` for transition effects
468
471
  - A sprite with size but no sheet is an invisible trigger (scores, goals, walls)
469
472
  - Add a whole level with one `gameView.add([...])` call
470
473
  - Use `Game.createTileLayer()` for large/static grids; keep sprites for interactive cells and actors
package/lib/installer.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  getClaudeCommandsDir,
21
21
  } from './config.js';
22
22
  import { removeSkills, removeAgents, removeCommands } from './cleanup.js';
23
+ import { createSymlinkOrCopy } from './symlink.js';
23
24
 
24
25
  /**
25
26
  * Recursively copy a directory
@@ -42,6 +43,8 @@ export async function copyDirectory(src, dest) {
42
43
  * @param {string} repoDir - Repository directory
43
44
  * @param {string} skillName - Name of the skill
44
45
  * @param {string} baseDir - Base directory for installation
46
+ * Development checkouts are linked so `npm link` users see edits immediately.
47
+ * Published npm packages are copied so installed skills remain independent.
45
48
  * @returns {Promise<boolean>} True if installed successfully
46
49
  */
47
50
  export async function installSkill(repoDir, skillName, baseDir = os.homedir()) {
@@ -59,6 +62,10 @@ export async function installSkill(repoDir, skillName, baseDir = os.homedir()) {
59
62
  return false;
60
63
  }
61
64
 
65
+ if (existsSync(join(repoDir, '.git'))) {
66
+ return createSymlinkOrCopy(skillSrc, skillDest, true);
67
+ }
68
+
62
69
  // Remove existing if present
63
70
  if (existsSync(skillDest)) {
64
71
  await remove(skillDest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "4.16.0",
3
+ "version": "4.16.2",
4
4
  "description": "Titanium SDK skills and agents for AI coding assistants (Claude Code, Gemini CLI, Codex CLI)",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -8,7 +8,7 @@ allowed-tools: Read, Grep, Glob, Edit, Write, Bash(node *)
8
8
 
9
9
  `ti.game` is Michael Gangolf's OpenGL ES 2.0 sprite engine for Titanium. JavaScript describes the scene and reacts to discrete events; Android and iOS run rendering, animation, physics, collision, gestures, audio, particles, ropes, tile layers, camera work, and telemetry natively.
10
10
 
11
- This skill is verified against upstream `main` at **`d587081` (2026-08-28)**. Both manifests still say **`0.5.0`**, although important features landed after that version was introduced. Treat the commit or build date as the capability marker; feature-detect an installed build when the exact artifact is unknown.
11
+ This skill is verified against upstream `main` at **`c216e7f` (2026-08-28)**. Both manifests still say **`0.5.0`**, although important features landed after that version was introduced. Treat the commit or build date as the capability marker; feature-detect an installed build when the exact artifact is unknown.
12
12
 
13
13
  The skill applies to **Classic and Alloy** projects. Nothing here requires PurgeTSS.
14
14
 
@@ -48,6 +48,7 @@ Do not move sprites from `setInterval`, `requestAnimationFrame`, or a JS loop. P
48
48
  | Walk around obstacles | `findPath()` → `followPath()` |
49
49
  | Test line of sight or hitscan | `raycast()` |
50
50
  | Fast projectile collision | `swept: true` on the mover |
51
+ | Detect a side wall or slide down it | `onWallLeft` / `onWallRight`, `wallhit`, `wallSlideSpeed` |
51
52
  | Repeat a large map efficiently | `Game.createTileLayer()` |
52
53
  | Attach a label, health bar, or turret | `attachTo()` |
53
54
  | Score or in-game label | `Game.createText()`; usually `screenFixed: true` for a HUD |
@@ -107,6 +108,7 @@ win.open();
107
108
  - Coordinates use a top-left origin, y increases downward, and `(x, y)` is the sprite anchor. Durations crossing the JS boundary are milliseconds.
108
109
  - `solidWith` blocks; `collidesWith` reports `collision`/`collisionend`. They are independent. A sized sprite without a sheet is a valid invisible solid or trigger.
109
110
  - Put the hitbox shape on the object whose geometry matters. A round peg needs `hitboxShape: 'circle'`; a ramp needs `'rotatedRect'`.
111
+ - Gate wall jumps on `onWallLeft` / `onWallRight` inside the jump input handler. `wallhit` is the transition event for effects; `wallSlideSpeed` caps the downward speed natively.
110
112
  - A `TileLayer` replaces one-sprite-per-tile maps when the map is large. Its visible-cell renderer, cell collision, live edits, and pathfinding integration are native.
111
113
  - Collect initial sprites, emitters, ropes, and tile layers and add them in one `gameView.add([...])` call.
112
114
  - Use `Game.createText()` for scene text. Use Titanium views only for UI the engine cannot draw, such as native input controls or system-font interfaces.
@@ -122,7 +124,7 @@ When sources disagree, use this order:
122
124
  3. Upstream `README.md` and `TUTORIAL.md` for intended public usage.
123
125
  4. Upstream `TODO.md` for what remains partial or absent.
124
126
 
125
- Known upstream prose drift at the pinned commit: the README says 26 demos although there are 33; its camera row names the wrong HUD corner; and it says every `TileLayer` property is live although Android treats `legend`, `firstGid`, `cols`, and `rows` as creation-time configuration. The references already normalize those differences.
127
+ Known upstream prose drift at the pinned commit: the README says 26 demos although there are 33; its camera row names the wrong HUD corner; and it says every `TileLayer` property is live although Android treats `legend`, `firstGid`, `cols`, and `rows` as creation-time configuration. The references already normalize those differences. No demo exercises the wall-contact API yet, so its contract comes from the README and both native implementations.
126
128
 
127
129
  ## Before returning code
128
130
 
@@ -1,6 +1,6 @@
1
1
  # ti.game API reference
2
2
 
3
- Complete JS surface of `ti.game` as of upstream `main` at `d587081`, 2026-08-28, verified against every Android proxy/engine and its iOS twin. Defaults come from native fields, not prose. The manifests still read **`0.5.0`**, but the performance HUD/telemetry and native `TileLayer` landed afterward without another bump. Read the number as a floor, never as a feature list. 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
+ Complete JS surface of `ti.game` as of upstream `main` at `c216e7f`, 2026-08-28, verified against every Android proxy/engine and its iOS twin. Defaults come from native fields, not prose. The manifests still read **`0.5.0`**, but the performance HUD/telemetry, native `TileLayer`, and wall-contact API landed afterward without another lasting bump. Read the number as a floor, never as a feature list. 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).
4
4
 
5
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**.
6
6
 
@@ -253,6 +253,8 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
253
253
  | --- | --- | --- |
254
254
  | `solidWith` | — | Array of group names that block this sprite. The engine pushes it out along the axis of least penetration |
255
255
  | `onGround` | `false` | Read-only. Gate jumps on it |
256
+ | `onWallLeft`, `onWallRight` | `false` | Read-only. True while the mover is pressed against that side of a matching solid this frame. Rect side push-out counts; circle/rotated-rect contacts count when their normal is mostly horizontal. Sprite and TileLayer walls are supported |
257
+ | `wallSlideSpeed` | `0` | Maximum downward velocity in px/s while either wall flag is true. `0` disables the cap. Upward velocity and slower falls are unchanged |
256
258
  | `restitution` | `0` | 0..1 bounce factor. Read off **both** sides of the contact and mixed as `max(mover, solid)`, the way Box2D does it — so a springy floor bounces riders that are not themselves bouncy. Solids default to 0, so a scene that never sets it on a surface behaves exactly as it did before this existed. A bounce whose closing speed would come out under **40 px/s** is damped to a dead stop instead, so a settling body grounds rather than buzzing |
257
259
  | `solidMode` | `'block'` | On the **solid**. `'block'` is an immovable wall (what every solid was). `'contain'` is an inward circular boundary — matched circles are kept *inside* its circumference (drums, bowls, lottery cages). `'push'` makes it a body in its own right: a matched circle and this one split the separation and exchange the closing velocity at equal mass. `'contain'` and `'push'` are **circle-on-circle only**; anything else, and any unknown string, falls back to `'block'`. Since 2026-08-27 |
258
260
  | `oneWay` | `false` | On the **solid**: pass-through except for landings on its top edge — classic platformer floors. Honoured by `'block'` solids only |
@@ -390,7 +392,8 @@ The rest of the contract, all read from the engine:
390
392
  | `pathcomplete` | `x`, `y` | A non-looping `followPath` run reached the end |
391
393
  | `collision` | `group`, `other`, `x`, `y` | Overlap with a `collidesWith` group began |
392
394
  | `collisionend` | `group`, `other`, `x`, `y` | That overlap ended: the shapes separated, **or** the partner was removed from the scene, hidden (`visible = false`) or stopped matching the group filter |
393
- | `land` | `x`, `y`, optional `other`, `group` | Landed on top of a `solidWith` solid. `other` is the solid sprite when one exists; TileLayer cells have no proxy and omit it |
395
+ | `land` | `x`, `y`, optional `other`, optional `group` | Landed on top of a `solidWith` solid. `other` and `group` identify the solid sprite when one exists; TileLayer cells have no proxy and omit both |
396
+ | `wallhit` | `side`, `x`, `y`, optional `other`, optional `group` | Began pressing a matching wall or switched sides. `side` is `'left'` or `'right'`. TileLayer cells omit both `other` and `group`; there is no `wallend` event, so read the flags in the jump handler |
394
397
 
395
398
  ## Font
396
399
 
@@ -569,7 +572,7 @@ Movers list the layer's `collisionGroup` in `solidWith`. Rect, circle, swept, re
569
572
  Only these events exist. Nothing fires per frame, and events are only fired if a listener is registered.
570
573
 
571
574
  - **GameView**: `press`, `tap`, `release`, `resize`, `timer`, `performance`
572
- - **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `pathcomplete`, `collision`, `collisionend`, `land`
575
+ - **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `pathcomplete`, `collision`, `collisionend`, `land`, `wallhit`
573
576
  - **SpriteSheet, Font, Sound, Emitter, Rope, TileLayer**: none
574
577
 
575
578
  There is `collision` (enter) and `collisionend` (exit), but deliberately **no** per-frame "stay" event — that would be bridge traffic every frame. Hold the in-between state in JS: you heard the enter, you will hear the end.
@@ -593,7 +596,7 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
593
596
  - **`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`.
594
597
  - **`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.
595
598
  - **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.
596
- - **`'push'` pairs never ground and never fire `land`.** The bilateral resolver moves positions and exchanges velocity and touches nothing else, so a pile of balls resting on each other all read `onGround: false` unless something underneath them is a `'block'` solid.
599
+ - **`'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.
597
600
  - **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.
598
601
  - **`swept: true` no longer drags a resting body backwards** (since 2026-08-27). A hit at t = 0 is a contact the sprite is already in, and the static resolver owns it; the sweep used to clamp the sprite back to where the frame started, every frame, while its along-surface speed kept climbing until it broke loose looking launched. On an older build, that is what a swept sprite parked on a slope or a floor does — it is a build date, not your code.
599
602
  - **The hitbox shapes are exclusive, and an unknown name is `'rect'`.** Setting `hitboxShape` clears whichever of the other two was set, so switching a sprite between `'circle'` and `'rotatedRect'` at runtime is safe, and a typo lands on the default rather than throwing. Reading the property back is therefore also the one clean way to tell whether a build understands `'rotatedRect'` at all.
@@ -634,6 +637,8 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
634
637
  - **`animate()` takes a percentage only on `scale`.** Inside the tween options `scaleX`, `scaleY` and `opacity` are read as plain numbers on both platforms, so `animate({ opacity: '50%' })` does not do what the property setter would. Percentages are for property writes; keep tween targets numeric.
635
638
  - **A bad ratio or anchor name fails differently per platform.** Android keeps the value the property already held; iOS resets it to the property's default. Neither throws, both log a warning, and at creation time they agree — the split only shows on a write to a property already set to something else.
636
639
  - **Four TileLayer inputs are creation-time for portable code.** iOS rebuilds after live writes to `legend`, `firstGid`, `cols`, and `rows`; Android exposes only creation-dict handling (plus `cols`/`rows` getters). Put all four in `createTileLayer()` and replace `data` for a live rebuild.
640
+ - **Android string maps need `c216e7f` or newer.** Earlier Android builds rejected the ordinary nested JS object behind `legend` because they required a `KrollDict`; string rows then decoded as empty cells. The current factory uses `getKrollDict()` and matches iOS. Numeric nested/flat data was unaffected.
641
+ - **Wall contact is state, not a sticky event.** `onWallLeft` / `onWallRight` reset when resolution no longer pushes the mover sideways. `wallhit` fires only on entering a wall or switching sides; use it for sound/particles, but read the flags at the instant the player requests a wall jump. A wall slide only caps positive `velocityY`; it never slows an upward wall jump.
637
642
  - **`removeAllSprites()` does not clear every scene list.** It removes Sprite/Text objects and resets their attachments; emitters, ropes, and TileLayers remain until individually removed.
638
643
  - **`anchor` beats `anchorX`/`anchorY` in the same `createSprite` call**, whatever the key order in the object, because the engine applies the preset after the two axes. Pass one or the other, not both.
639
644
  - **`scrollFactor` does not move the sprite.** It shifts where the sprite is *drawn* and maps touch back to match. `x`, `y`, physics and collisions stay in world coordinates, so a `scrollFactor: 0.5` platform still collides where its world `x` says, not where it looks.
@@ -1,6 +1,6 @@
1
1
  # Debugging and performance telemetry
2
2
 
3
- Verified against `ti.game@d587081`: both `GameViewProxy` implementations, `DebugHud`, `FrameStats`, both scene renderers, `example/camera.js`, `example/particles.js`, and `example/tilemap.js`.
3
+ Verified against `ti.game@c216e7f`: both `GameViewProxy` implementations, `DebugHud`, `FrameStats`, both scene renderers, `example/camera.js`, `example/particles.js`, and `example/tilemap.js`.
4
4
 
5
5
  ## The two debug aids
6
6
 
@@ -100,4 +100,3 @@ Telemetry is opt-in inside the renderers. With the HUD disabled and no `performa
100
100
  4. Inspect HUD counts and draw calls before changing code.
101
101
  5. Compare the smallest relevant upstream demo from [examples.md](examples.md).
102
102
  6. Disable instrumentation after the issue is understood.
103
-
@@ -1,6 +1,6 @@
1
1
  # Upstream example catalog
2
2
 
3
- Verified against all 33 self-contained demos under `ti.game@d587081:example/` plus the launcher `example/app.js`.
3
+ Verified against all 33 self-contained demos under `ti.game@c216e7f:example/` plus the launcher `example/app.js`. Wall contacts and `wallSlideSpeed` are not represented by a demo at this commit; use [the platformer recipe](recipes-action.md#platformer), which is verified against both native implementations and the README contract.
4
4
 
5
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.
6
6
 
@@ -51,4 +51,3 @@ The demos prove that API combinations run together on the current module. They a
51
51
  3. Replace `var` and function-expression callbacks with the project's current ES6+ style.
52
52
  4. Keep resources outside `resize`; create surface-sized scene objects inside one guarded `resize` path.
53
53
  5. Separate reusable state from launcher UI and add explicit cleanup for app-owned listeners, timers, and sounds.
54
-
@@ -1,6 +1,6 @@
1
1
  # Cross-cutting ti.game patterns
2
2
 
3
- Verified against `ti.game@d587081` and the runnable examples under `example/`. These are production-oriented extractions of the API combinations demonstrated upstream; they intentionally omit demo-launcher and back-navigation plumbing.
3
+ Verified against `ti.game@c216e7f` and the runnable examples under `example/`. These are production-oriented extractions of the API combinations demonstrated upstream; they intentionally omit demo-launcher and back-navigation plumbing.
4
4
 
5
5
  See [examples.md](examples.md) to choose the source demo, [api.md](api.md) for exact members, and the genre recipe files for complete game loops.
6
6
 
@@ -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@d587081` (2026-08-28).
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@c216e7f` (2026-08-28).
4
4
 
5
5
  <!-- TOC-START -->
6
6
  ## Contents
@@ -58,6 +58,9 @@ Pinning `version` is worth the extra attribute: with two module versions install
58
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
59
  | `0.5.0` built before `d042060` | the performance HUD object form (`debug: { hud, hitbox, hudFont }`) and the `performance` event |
60
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 |
61
64
 
62
65
  The newest local release tag is still `0.4.0` (2026-08-20), so tagged artifacts lag one manifest bump plus the HUD and TileLayer work that landed after `0.5.0` was introduced. 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.
63
66
 
@@ -352,5 +355,6 @@ The JS API is identical on Android and iOS. What differs is around it:
352
355
  - **Android's `touchFeedback` ripple** cannot animate over the GL canvas and logs `RippleDrawable` errors; give on-screen buttons manual `backgroundColor` feedback on `touchstart`/`touchend`.
353
356
  - **Sound formats**: WAV, MP3 and OGG on Android; WAV, MP3 and M4A on iOS. WAV for effects, MP3 for music covers both.
354
357
  - **TileLayer grid configuration**: assign `legend`, `firstGid`, `cols`, and `rows` in `createTileLayer()`. iOS accepts later writes; Android treats them as creation-time inputs.
358
+ - **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.
355
359
  - **Performance telemetry**: `averagePresentMs` and `presentFailures` exist only on iOS and are omitted on Android.
356
360
  - **`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).
@@ -1,6 +1,6 @@
1
1
  # Action-game recipes
2
2
 
3
- Production-oriented patterns distilled from the upstream action demos at `ti.game@d587081`. Use [examples.md](examples.md) to locate the full runnable demo and [api.md](api.md) for exact defaults and payloads.
3
+ Production-oriented patterns distilled from the upstream action demos at `ti.game@c216e7f`. 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
4
 
5
5
  <!-- TOC-START -->
6
6
  ## Contents
@@ -71,6 +71,9 @@ Obstacles are pooled gates: three sprites each (top pipe, bottom pipe, invisible
71
71
  `solidWith` handles the blocking; you only decide when to jump.
72
72
 
73
73
  ```javascript
74
+ const WALL_KICK_X = RUN_SPEED * 1.35;
75
+ const WALL_KICK_Y = JUMP * 0.9;
76
+
74
77
  const player = Game.createSprite({
75
78
  sheet: playerSheet,
76
79
  x: W * 0.15,
@@ -82,6 +85,7 @@ const player = Game.createSprite({
82
85
  hitboxScale: 0.85,
83
86
  hitboxScaleX: 0.66, // the blob is 38px wide in a 64px frame, but nearly full height
84
87
  solidWith: ['solid', 'trampoline'],
88
+ wallSlideSpeed: H * 0.18, // 0 disables the native downward cap
85
89
  animations: {
86
90
  idle: { frames: [0], fps: 1, loop: true },
87
91
  walk: { frames: [1, 2], fps: 8, loop: true }
@@ -107,6 +111,16 @@ function jump() {
107
111
  player.velocityY = -JUMP; // H * 0.95 ≈ 0.2 * H of height
108
112
  player.stop();
109
113
  player.frame = 3; // hold the jump pose
114
+ return;
115
+ }
116
+
117
+ const away = player.onWallLeft ? 1 : player.onWallRight ? -1 : 0;
118
+ if (away !== 0) { // read state when the jump is requested
119
+ player.velocityX = away * WALL_KICK_X;
120
+ player.velocityY = -WALL_KICK_Y;
121
+ player.scaleX = away;
122
+ player.stop();
123
+ player.frame = 3;
110
124
  }
111
125
  }
112
126
 
@@ -123,10 +137,16 @@ player.addEventListener('land', (e) => {
123
137
  }
124
138
  applyMovement(); // back from the jump pose
125
139
  });
140
+
141
+ player.addEventListener('wallhit', () => {
142
+ player.flash('#d7f4ff', 80); // transition effect, not jump gating
143
+ });
126
144
  ```
127
145
 
128
146
  Jump height is `JUMP² / (2 × GRAVITY)`. Space your platforms comfortably below it — the demo uses steps of `0.14 * H` against a `0.2 * H` jump.
129
147
 
148
+ **Wall jumps and slides**: `onWallLeft` / `onWallRight` are read-only contact state and only stay true while the resolver is pushing the player sideways. Read them in `jump()`; do not poll `wallhit` or make it sticky in JS. `wallhit` fires on first contact or a side switch and is ideal for sound/dust. `wallSlideSpeed` caps downward velocity only, so the upward kick remains intact. With touch controls that continuously reapply horizontal movement, let the wall-jump impulse win briefly before writing the held direction again. TileLayer walls set the same flags, but their `wallhit` payload has no `other` or `group`.
149
+
130
150
  **One-way platforms**: `oneWay: true` on the solid lets riders jump up through it and land on top, never blocking sideways or from below. This is what makes a staircase climbable.
131
151
 
132
152
  **Moving platforms carry riders for free** — a solid moved by `velocityX` or a tween drags whoever stands on it, with no re-landing jitter:
@@ -1,6 +1,6 @@
1
1
  # Adventure, presentation, and effects recipes
2
2
 
3
- Production-oriented patterns distilled from the upstream non-action demos at `ti.game@d587081`. Use [examples.md](examples.md) to locate the full runnable demo and [api.md](api.md) for exact defaults and payloads.
3
+ Production-oriented patterns distilled from the upstream non-action demos at `ti.game@c216e7f`. Use [examples.md](examples.md) to locate the full runnable demo and [api.md](api.md) for exact defaults and payloads.
4
4
 
5
5
  <!-- TOC-START -->
6
6
  ## Contents
@@ -1,6 +1,6 @@
1
1
  # Missing, partial, and out-of-scope features
2
2
 
3
- Verified against `ti.game@d587081` (2026-08-28), especially upstream `TODO.md` and both native implementations. Planned is not shipped; partial means the exact boundary below matters.
3
+ Verified against `ti.game@c216e7f` (2026-08-28), especially upstream `TODO.md` and both native implementations. Planned is not shipped; partial means the exact boundary below matters.
4
4
 
5
5
  ## Not available today
6
6
 
@@ -33,6 +33,7 @@ Verified against `ti.game@d587081` (2026-08-28), especially upstream `TODO.md` a
33
33
  - Text/Font sprites, word wrap, `screenFixed`, attachment, named anchors, percentage ratios, and explicit sheet/font `unload()`.
34
34
  - Sprite paths, animation chaining, raycasts, A*, game-clock timers, collision exit, swept movers, and four blend modes.
35
35
  - Shape-aware solids: circle and rotated-rect geometry, one-way platforms, carried riders, containment, bilateral circle push, two-sided restitution, `gravityX`, and `linearDamping`.
36
+ - Native wall contacts: `onWallLeft` / `onWallRight`, `wallhit`, TileLayer-wall parity, and `wallSlideSpeed`.
36
37
  - Native particles, ropes, effect/music sound backends, car physics, thrust, drag/pinch/rotate, and multi-touch sprites.
37
38
 
38
39
  ## Deliberately out of scope upstream
@@ -1,6 +1,6 @@
1
1
  # Native tile maps
2
2
 
3
- Verified against `ti.game@d587081`: upstream `README.md` § Tile maps, `example/tilemap.js`, both `TileLayerProxy` implementations, both native `TileLayer` engines, scene collision, and `Pathfinder`.
3
+ Verified against `ti.game@c216e7f`: upstream `README.md` § Tile maps, `example/tilemap.js`, both `TileLayerProxy` implementations, both native `TileLayer` engines, scene collision, and `Pathfinder`.
4
4
 
5
5
  Use `Game.createTileLayer()` for a large or mostly static grid. The engine stores the whole map but renders only cells inside the camera, batches the layer through one sheet, checks only collision cells under a mover, and feeds solid cells into `findPath`. A small map whose tiles need independent sprite behavior can still use sprites; `topdown.js` is a 16×12 example of that older pattern.
6
6
 
@@ -68,9 +68,12 @@ const player = Game.createSprite({
68
68
  - `oneWay` ids catch a mover falling onto the top and do not block sideways or upward movement.
69
69
  - Rect, circle, and swept movers are supported.
70
70
  - Layer `restitution` mixes with the mover like restitution on a solid sprite.
71
- - A successful top landing sets `onGround` and fires `land`. A tile cell is not a sprite, so the event has no `other` object.
71
+ - A successful top landing sets `onGround` and fires `land`. A tile cell is not a sprite, so the event has no `other` or `group`.
72
+ - A sideways push sets `onWallLeft` / `onWallRight`, applies `wallSlideSpeed`, and can fire `wallhit`. Tile-wall events likewise omit `other` and `group`.
72
73
  - Adjacent solid cells suppress their shared internal faces, avoiding seams while sliding along a floor.
73
74
 
75
+ Android builds before `c216e7f` ignore an ordinary nested JS object passed as `legend`, so string rows decode as empty there. Upgrade or use numeric data on those builds. The fix does not make `legend` live on Android; it remains a factory-time portability input.
76
+
74
77
  Tile cells participate in **solid resolution**, not overlap events. `collidesWith` does not produce `collision`/`collisionend` for trigger tiles yet. `raycast()` also does not inspect tile cells. See [roadmap.md](roadmap.md) before designing those behaviors.
75
78
 
76
79
  ## Read and edit cells
@@ -122,4 +125,3 @@ Bound the per-query A* search to the useful neighborhood. The layer can be enorm
122
125
  - Use `visible: false` to exclude the layer from drawing and collision.
123
126
  - Toggle `layer.debug` for one layer or `gameView.debug = { hitbox: true }` for every layer's solid cells.
124
127
  - Use the performance HUD while scrolling: map dimensions should not change visible-cell draw cost.
125
-