@maccesar/titools 4.19.0 → 4.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "4.19.0",
3
+ "version": "4.20.0",
4
4
  "description": "Titanium SDK skills and agents for AI coding assistants, including native game and sound synthesis modules",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -468,7 +468,7 @@ Load these only when needed:
468
468
  - [PurgeTSS UI in Classic](references/purgetss-ui-classic.md) - Native JavaScript setup, public exports, animation objects, cleanup, and platform notes
469
469
 
470
470
  ### Release Notes
471
- - [Version History](references/version-history.md) - Release-by-release feature additions and behavior changes (v7.4.0 → v7.16.2)
471
+ - [Version History](references/version-history.md) - Release-by-release feature additions and behavior changes (v7.4.0 → v7.17.0)
472
472
 
473
473
  > **💡 TEXT FONTS (Google Fonts, Roboto, etc.)**
474
474
  > For text fonts, see [Custom Fonts](references/custom-fonts.md).
@@ -86,10 +86,16 @@ module.exports = {
86
86
  splashIcon: { enabled: false }, notificationIcon: { enabled: false },
87
87
  ninePatch: { enabled: false }
88
88
  },
89
+ // Sources in purgetss/images/ are 4x masters: a 1024px file yields
90
+ // 256 (mdpi/@1x), 384 (hdpi), 512 (xhdpi/@2x), 768 (xxhdpi/@3x), 1024 (xxxhdpi).
91
+ // There is no width to configure here — the source's own pixels decide.
92
+ // SVGs have no natural pixels; pin theirs in files: [] below.
89
93
  images: {
90
- quality: 85, // JPEG/WebP/AVIF quality (0-100)
94
+ quality: 85, // webp/jpeg/avif/tiff quality (0-100); PNG and GIF ignore it
91
95
  format: null, // null = keep original; 'webp' | 'jpeg' | 'png' to convert every image
92
- confirmOverwrites: true // prompt before overwriting files (set false to skip)
96
+ autoSync: true, // false = SVG pipeline computes dims but doesn't write to images.files
97
+ confirmOverwrites: true, // prompt before overwriting files (set false to skip)
98
+ files: [] // per-file overrides: [{ filename: 'images/<sub>/<name>.<ext>', width, height? }]
93
99
  },
94
100
  theme: {
95
101
  extend: {}
@@ -171,7 +177,7 @@ Recommended VSCode extensions:
171
177
 
172
178
  ### Commands Executed Internally
173
179
 
174
- `purgetss create "Name of the Project" [--dependencies --vendor=fa,mi,ms,f7]` runs: `ti config` (reads idprefix/workspace), `ti create -t app -p all -n "Name"`, `alloy new`, `purgetss w`, `purgetss b`, optional `--vendor` (copies fonts + CommonJS module), optional `--dependencies` (installs Tailwind CSS, ESLint with Titanium plugins, and config files), then opens the project in VS Code, Sublime Text, or Finder.
180
+ `purgetss create "Name of the Project" [--dependencies --vendor=fa,mi,ms,f7]` runs: `ti config` (reads idprefix/workspace), `ti create -t app -p all -n "Name"`, `alloy new`, `purgetss w`, `purgetss b`, optional `--vendor` (copies fonts + CommonJS module), optional `--dependencies` (installs Tailwind CSS and ESLint, and copies the config files — see [`install-dependencies`](#install-dependencies-command)), then opens the project in VS Code, Sublime Text, or Finder.
175
181
 
176
182
  ## `brand` Command
177
183
 
@@ -331,7 +337,7 @@ purgetss images background/ # re-process one subfolder
331
337
  | `--android` | Only emit Android density variants. Mutually exclusive with `--ios`. |
332
338
  | `--ios` | Only emit iPhone scale variants. Mutually exclusive with `--android`. |
333
339
  | `--format <ext>` | Convert all outputs to `webp`, `jpeg`, `png`, `avif`, `gif`, or `tiff`. Default: keep source format. |
334
- | `--quality <n>` | Quality `0-100` for lossy formats. Default `85`. |
340
+ | `--quality <n>` | Quality `0-100` for `webp`, `jpeg`, `avif` and `tiff`. PNG and GIF ignore it. Default `85`. |
335
341
  | `--width <n>` | (v7.8.0) Pin Android `mdpi` (= iPhone `@1x`) to `<n>` pixels wide. Larger scales derive as ×1.5, ×2, ×3, ×4 from that base; height stays proportional to the source's aspect ratio. Integer in `[1, 8192]`. |
336
342
  | `--opacity <n>` | (v7.10.0) Multiply alpha of every density by `n/100`. Integer in `[0, 100]`. With `--format jpeg`, alpha flattens on white. |
337
343
  | `--padding <n>` | (v7.10.0) Shrink the rendered image inside each density canvas by `n%` symmetric borders. Integer in `[0, 40]`. |
@@ -369,12 +375,16 @@ Defaults live under `images:` in `purgetss/config.cjs`:
369
375
 
370
376
  ```javascript
371
377
  images: {
372
- quality: 85, // JPEG/WebP/AVIF quality (0-100)
378
+ quality: 85, // webp/jpeg/avif/tiff quality (0-100); PNG and GIF ignore it
373
379
  format: null, // null = keep original; 'webp' | 'jpeg' | 'png' | 'avif' | 'gif' | 'tiff'
374
- confirmOverwrites: true // prompt before overwriting files
380
+ autoSync: true, // false = SVG pipeline computes dims but doesn't write to images.files
381
+ confirmOverwrites: true, // prompt before overwriting files
382
+ files: [] // per-file overrides: [{ filename, width, height? }]
375
383
  }
376
384
  ```
377
385
 
386
+ Those five keys are the whole section. Since v7.17.0 any other key — including `width`, `opacity`, `padding` and `output`, which are CLI flags — aborts the run before writing anything, and so does a `files[]` entry with an unknown key or no `filename`. See [Unknown keys are an error](./multi-density-images.md#unknown-keys-are-an-error-v7170).
387
+
378
388
  Like `brand`, `images` writes in place and asks `Continue? [y/N/a]` before overwriting. Selecting `a` flips `confirmOverwrites: false` in `config.cjs`. Skipped automatically when `stdin` is not a TTY, when `-y`/`--yes` is passed, or when `PURGETSS_YES=1` is set.
379
389
 
380
390
  ### Examples
@@ -481,6 +491,15 @@ purgetss install-dependencies
481
491
  purgetss id
482
492
  ```
483
493
 
494
+ ### What it installs (v7.17.0)
495
+
496
+ `npm i -D eslint @eslint/js` plus a flat `eslint.config.mjs` in the project root, `.editorconfig`, `.vscode/{extensions,settings}.json` when the `code` command exists, and `tailwindcss@3` for IntelliSense. `create --dependencies` runs the same ESLint and Tailwind steps. `eslint-config-axway` and `eslint-plugin-alloy` are no longer installed.
497
+
498
+ The shipped config declares the Titanium and Alloy globals itself (`Ti`, `Titanium`, `Alloy`, `Backbone`, `$`, `$model`, `_`, `L`, `Widget`, `OS_*`, `ENV_*`, `DIST_*`, `console`, `alert`, the timer functions, and `task` for `alloy.jmk`). It lints `app/**/*.js` and ignores `Resources/`, `build/`, `node_modules/`, `purgetss/`, plus the six libraries PurgeTSS copies into `app/lib/`, listed by name so the developer's own files there keep being linted. `no-unused-vars` is a **warning**: Alloy wires handlers from the XML view, so a controller function with no caller in the JS may still be in use.
499
+
500
+ > **Warning**
501
+ > Before v7.17.0 this command shipped an `.eslintrc.js` extending `eslint-config-axway/env-alloy` and installed `eslint-plugin-alloy`. That setup cannot run under ESLint 9 — it stopped reading `.eslintrc.*`, axway removed `env-alloy` in `eslint-config-axway@10.0.0`, and the unmaintained plugin throws `This method cannot be used with flat config`. Projects scaffolded between December 2025 and v7.17.0 have a lint that never ran; re-run `purgetss install-dependencies` to replace it.
502
+
484
503
  > **Caution**
485
504
  > This command overwrites any existing `extensions.json` and `settings.json` files. Back them up if you want to keep your current versions.
486
505
 
@@ -75,14 +75,19 @@ module.exports = {
75
75
  notificationIcon: { enabled: false }, // drawable-*/ic_stat_notify.png × 5
76
76
  ninePatch: { enabled: false } // background.9.png (not implemented yet)
77
77
  },
78
+ // Sources in purgetss/images/ are 4x masters: a 1024px file yields
79
+ // 256 (mdpi/@1x), 384 (hdpi), 512 (xhdpi/@2x), 768 (xxhdpi/@3x), 1024 (xxxhdpi).
80
+ // There is no width to configure here — the source's own pixels decide.
81
+ // SVGs have no natural pixels; pin theirs in files: [] below.
78
82
  images: {
79
- autoSync: true, // false = SVG pipeline computes dims but doesn't write to images.files (v7.11.0)
80
- quality: 85, // JPEG/WebP/AVIF quality (0-100)
83
+ quality: 85, // webp/jpeg/avif/tiff quality (0-100); PNG and GIF ignore it
81
84
  format: null, // null = keep original; 'webp' | 'jpeg' | 'png' to convert every image
85
+ autoSync: true, // false = SVG pipeline computes dims but doesn't write to images.files
82
86
  confirmOverwrites: true, // prompt before overwriting files (set false to skip)
83
- files: [] // per-file overrides: [{ filename, width, height? }] (v7.11.0)
87
+ files: [] // per-file overrides: [{ filename: 'images/<sub>/<name>.<ext>', width, height? }]
84
88
  // Note: --width (v7.8.0) and --opacity / --padding / --output (v7.10.0) are CLI-only
85
- // by design — those decisions are per-asset, not project-wide.
89
+ // by design — those decisions are per-asset, not project-wide. Since v7.17.0
90
+ // writing any of them here aborts the run instead of being ignored.
86
91
  },
87
92
  theme: {
88
93
  extend: {}
@@ -189,25 +189,55 @@ This is a hint, **not an error**. The legacy 4×-from-viewBox behavior still run
189
189
  On the first run, `purgetss images` injects an `images:` block into your existing `purgetss/config.cjs` (between `brand:` and `theme:`) with these defaults:
190
190
 
191
191
  ```javascript
192
+ // Sources in purgetss/images/ are 4x masters: a 1024px file yields
193
+ // 256 (mdpi/@1x), 384 (hdpi), 512 (xhdpi/@2x), 768 (xxhdpi/@3x), 1024 (xxxhdpi).
194
+ // There is no width to configure here — the source's own pixels decide.
195
+ // SVGs have no natural pixels; pin theirs in files: [] below.
192
196
  images: {
193
- autoSync: true, // false = SVG pipeline computes dims but doesn't write to images.files
194
- quality: 85, // JPEG/WebP/AVIF quality (0-100)
197
+ quality: 85, // webp/jpeg/avif/tiff quality (0-100); PNG and GIF ignore it
195
198
  format: null, // null = keep original; 'webp' | 'jpeg' | 'png' to convert every image
199
+ autoSync: true, // false = SVG pipeline computes dims but doesn't write to images.files
196
200
  confirmOverwrites: true, // prompt before overwriting files (set false to skip)
197
201
  files: [] // per-file overrides: [{ filename: 'images/<sub>/<name>.<ext>', width, height? }]
198
202
  }
199
203
  ```
200
204
 
205
+ Since v7.17.0 the four comment lines above the section ship with the block itself, and the same text seeds through `init`. Reproduce the key order above when writing a config by hand — it is the order both generated copies use.
206
+
201
207
  | Key | Default | Purpose |
202
208
  | --- | --- | --- |
203
- | `autoSync` | `true` | When `false`, the SVG pipeline still computes dimensions and generates PNGs but never writes back to `images.files`. See [SVG-aware compile-time pipeline](./svg-pipeline.md). |
204
- | `quality` | `85` | Quality for lossy formats (JPEG, WebP, AVIF). Range `0–100`. |
209
+ | `quality` | `85` | Range `0–100`. Reaches `webp`, `jpeg`, `avif` and `tiff` only. PNG is written with `compressionLevel: 9` and GIF takes no quality parameter, so the value never applies to either. |
205
210
  | `format` | `null` | `null` keeps each source's original format. Set `'webp'`, `'jpeg'`, or `'png'` to convert every output. |
211
+ | `autoSync` | `true` | When `false`, the SVG pipeline still computes dimensions and generates PNGs but never writes back to `images.files`. See [SVG-aware compile-time pipeline](./svg-pipeline.md). |
206
212
  | `confirmOverwrites` | `true` | When `false`, the `[y/N/a]` prompt is skipped. |
207
213
  | `files` | `[]` | Per-file dimension overrides. See [Per-file overrides](#per-file-overrides-with-imagesfiles). |
208
214
 
209
215
  Change whatever you want to override globally; CLI flags still win for one-off runs. `autoSync` and `files` arrived in v7.11.0 to support the [SVG-aware compile-time pipeline](./svg-pipeline.md).
210
216
 
217
+ ### Unknown keys are an error (v7.17.0)
218
+
219
+ Those five keys are the entire section. Since v7.17.0 anything else aborts the run before a single file is written, the same validation `brand:` has had since v7.13.0:
220
+
221
+ ```text
222
+ > purgetss images
223
+ Error running images: Unknown key(s) in the images: section of purgetss/config.cjs:
224
+ • images.qualty
225
+ • images.files[0].widht
226
+
227
+ Top-level keys: quality, format, autoSync, confirmOverwrites, files
228
+ Inside files[]: filename, width, height
229
+
230
+ Check the spelling. No images were generated.
231
+ ```
232
+
233
+ A `qualty: 95` that gets ignored is indistinguishable from the default, so before v7.17.0 the typo produced the wrong output silently. The check runs at both levels and reports every problem in one pass:
234
+
235
+ - **Top level** — only `quality`, `format`, `autoSync`, `confirmOverwrites`, `files`.
236
+ - **Inside each `files[]` entry** — only `filename`, `width`, `height`. `filename` is **required** and must be a string; an entry without one matches no file and used to be skipped in the same silence.
237
+ - `files` itself must be an array, and each entry an object.
238
+
239
+ Do not suggest `width`, `opacity`, `padding` or `output` as `images:` keys. They are CLI flags and were never part of the section — the validator now names them as errors instead of ignoring them. When one file needs a fixed width, pin it in `files`; the other three describe a single run.
240
+
211
241
  ## Per-file overrides with `images.files`
212
242
 
213
243
  `images.files` (v7.11.0) is an array of per-file dimension pins. It does **not** filter which images get processed — `purgetss images` always walks every supported file under `purgetss/images/`. Entries in `files` only override the source and derived sizing for files whose `filename` matches.
@@ -484,7 +514,7 @@ If you only tweaked CSS classes (no image changes), you don't need to re-run `pu
484
514
  | Flag | Purpose |
485
515
  | --- | --- |
486
516
  | `--format <ext>` | Convert all outputs to: `webp`, `jpeg`, `png`, `avif`, `gif`, `tiff`. Default: keep source format. |
487
- | `--quality <n>` | Quality `0–100` for lossy formats. Default `85`. |
517
+ | `--quality <n>` | Quality `0–100` for `webp`, `jpeg`, `avif` and `tiff`. PNG and GIF ignore it. Default `85`. |
488
518
  | `--width <n>` | (v7.8.0) Pin `mdpi` / `@1x` output width to `n` pixels; `[1, 8192]`. Other densities derive from this base (×1.5 / ×2 / ×3 / ×4). Most useful for SVG sources with non-standard viewBoxes. CLI-only — no `config.cjs` equivalent because width is per-asset. |
489
519
  | `--opacity <n>` | (v7.10.0) Multiply the alpha channel of every generated density by `n/100`. Range `[0, 100]`. Combine with `--format jpeg` and the alpha is flattened on white instead of producing a transparent JPEG. CLI-only. |
490
520
  | `--padding <n>` | (v7.10.0) Shrink the rendered image inside each density canvas by `n%` symmetric borders. Range `[0, 40]`. CLI-only. |
@@ -8,6 +8,11 @@ When in doubt about whether a class, flag, or config key exists in the user's in
8
8
 
9
9
  ---
10
10
 
11
+ ## v7.17.0
12
+ - The `images:` section rejects unknown keys instead of ignoring them, at both levels: the five top-level keys (`quality`, `format`, `autoSync`, `confirmOverwrites`, `files`) and the three inside each `files[]` entry (`filename`, `width`, `height`, with `filename` required). Nothing is generated when validation fails. Never suggest `width`, `opacity`, `padding` or `output` as config keys — they are CLI flags and now abort the run. See [multi-density-images.md](multi-density-images.md).
13
+ - The generated `images:` block gained four comment lines explaining the 4× master convention, and the `quality` comment now names the formats it reaches: `webp`, `jpeg`, `avif`, `tiff`. PNG uses `compressionLevel: 9` and GIF takes no quality parameter, so neither is affected. Both copies changed — the one patched into an existing config and the one `init` seeds.
14
+ - `install-dependencies` and `create --dependencies` now install only `eslint` and `@eslint/js` and copy a flat `eslint.config.mjs`. `eslint-config-axway` and `eslint-plugin-alloy` are gone: the old `.eslintrc.js` could not run under ESLint 9. See [cli-commands.md](cli-commands.md#install-dependencies-command).
15
+
11
16
  ## v7.16.2
12
17
  - `build-fonts --module` now exports every processed TTF/OTF PostScript name through `families`, including text-only collections; icon CSS additionally populates `icons`. Filename-derived keys such as `poppinsSemiBold` are supported. See [custom-fonts.md](custom-fonts.md).
13
18
  - In clean Classic projects, saved `shades` palettes and `color-module` no longer scaffold empty brand, font, or image source folders. `shades` still refreshes an existing generated color module. See [classic-projects.md](classic-projects.md).
@@ -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 **`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.
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.
12
12
 
13
13
  The skill applies to **Classic and Alloy** projects. Nothing here requires PurgeTSS.
14
14
 
@@ -56,6 +56,9 @@ Do not move sprites from `setInterval`, `requestAnimationFrame`, or a JS loop. P
56
56
  | Chain, tether, cape | `Game.createRope()` |
57
57
  | Pause or slow the scene while still drawing | `gameView.timeScale` |
58
58
  | Measure FPS, frame time, and draw calls | `debug: { hud: true }` or the `performance` event |
59
+ | Read a game controller | GameView `buttondown`/`buttonup`, `stick`, `trigger` (names normalized across pads) |
60
+ | Wrap the world around horizontally | `gameView.worldWrapX` + per-sprite `wrapWorldX` |
61
+ | React to the force of a hit | `solidimpact` with a per-sprite `impactThreshold` |
59
62
 
60
63
  ## Minimal scene
61
64
 
@@ -124,7 +127,7 @@ When sources disagree, use this order:
124
127
  3. Upstream `README.md` and `TUTORIAL.md` for intended public usage.
125
128
  4. Upstream `TODO.md` for what remains partial or absent.
126
129
 
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.
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.
128
131
 
129
132
  ## Before returning code
130
133
 
@@ -1,6 +1,6 @@
1
1
  # ti.game API reference
2
2
 
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).
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).
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
 
@@ -10,6 +10,7 @@ Properties are live unless their table says creation-time or read-only. Every wr
10
10
  - [Module](#module)
11
11
  - [Names and percentages](#names-and-percentages)
12
12
  - [GameView](#gameview)
13
+ - [Gamepads](#gamepads)
13
14
  - [SpriteSheet](#spritesheet)
14
15
  - [Sprite](#sprite)
15
16
  - [Font](#font)
@@ -106,6 +107,7 @@ A normal Titanium view — add it to a window or any container, size it with the
106
107
  | `cameraX` / `cameraY` | float | `0` | World-space offset of the view |
107
108
  | `cameraScale` | float | `1` | Zoom, anchored on the view center |
108
109
  | `cameraBounds` | dict / null | `null` | `{ minX, minY, maxX, maxY }` world rect the visible area is clamped into |
110
+ | `worldWrapX` | dict / null | `null` | `{ minX, maxX }` turns on a finite circular horizontal world; sprites opt in with `wrapWorldX`. Invalid or non-positive intervals disable it. **Horizontal `cameraBounds` are ignored while it is active**; the vertical limits still apply |
109
111
  | `follow(sprite, options)` | method | — | Native dead-zone follow, see below |
110
112
  | `stopFollow()` | method | — | Stop following; the camera stays where it is |
111
113
  | `shake({ strength, duration })` | method | `12` px, `400` ms | Detuned-sine rumble on the projection only — follow, bounds and touch mapping are unaffected |
@@ -117,12 +119,18 @@ A normal Titanium view — add it to a window or any container, size it with the
117
119
  | `cameraEffect` | string | `'none'` | Fullscreen shader pass: `'none'`, `'tint'`, `'glitch'`. With `'none'` the extra pass is skipped entirely |
118
120
  | `cameraTint` | string | — | Color for the `'tint'` effect, e.g. `'#4f8'` (night vision, poison, flashback) |
119
121
  | `cameraEffectIntensity` | float | `1` | 0..1 — tint mix or glitch amount |
122
+ | `gamepads` | array | `[]` | Read-only. Connected controllers as `[{ id, name }]` |
123
+ | `gamepad` | dict / null | `null` | Read-only snapshot of the most recently used pad: `{ id, name, leftX, leftY, rightX, rightY, l2, r2, buttons }` |
124
+ | `gamepadDeadzone` | float | `0.2` | Radial stick dead zone with rescaling, clamped to 0..0.9, so small values start at 0 instead of jumping |
125
+ | `gamepadStickPress` / `gamepadStickRelease` | float | `0.5` / `0.4` | Left-stick thresholds for reporting a direction as a button. Press clamps to 0.1..0.95; release stays between 0.05 and press. The gap is hysteresis against flicker |
120
126
  | `debug` | bool / dict | `false` | `true` draws all collision shapes. Object form: `{ hitbox, hud, hudFont }`; `hud` is `true` or a corner name. Readback is normalized to `{ hitbox, hud: false | corner }`. See [debugging-performance.md](debugging-performance.md) |
121
127
 
122
128
  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).
123
129
 
124
130
  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.
125
131
 
132
+ 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.
133
+
126
134
  ### `follow(sprite, options)`
127
135
 
128
136
  | Option | Default | Effect |
@@ -165,6 +173,25 @@ if (path) {
165
173
  }
166
174
  ```
167
175
 
176
+ ## Gamepads
177
+
178
+ A Bluetooth or USB controller (Xbox, PlayStation, Stadia, any MFi pad) reaches JS as discrete GameView events. There is no polling and no per-frame traffic; `gameView.gamepad` exists for the rare case where reading from a `gameView.every()` timer fits better than reacting to events.
179
+
180
+ | Event | Payload | Notes |
181
+ | --- | --- | --- |
182
+ | `buttondown` / `buttonup` | `button`, `input`, `gamepad`, `keyCode` | `button` is normalized across pads: `a` `b` `x` `y` `l1` `r1` `l2` `r2` `l3` `r3` `start` `select` `home` `up` `down` `left` `right`. iOS reports `keyCode: 0` |
183
+ | `stick` | `stick` (`'left'`/`'right'`), `x`, `y`, `gamepad` | Analog, throttled to ~20 Hz per channel while the value changes |
184
+ | `trigger` | `trigger` (`'l2'`/`'r2'`), `value`, `gamepad` | Analog travel, same ~20 Hz throttle |
185
+ | `gamepadconnected` / `gamepaddisconnected` | `gamepad`, `name` | A pad already paired when the view was created announces itself on its first input, so a connected event always precedes that pad's buttons |
186
+
187
+ The four directions arrive **both** from the d-pad (however the pad reports it, keys or hat) and from the left stick once it crosses `gamepadStickPress`, releasing below `gamepadStickRelease`. Each name is reported down once, however many physical controls map to it, so one `buttondown` table covers both without asking which the player used; `input` (`'dpad'`, `'leftstick'`, `'button'`, `'trigger'`) tells them apart when that matters. Analog triggers count as `l2`/`r2` buttons past half travel *and* fire `trigger` events.
188
+
189
+ Transitions skip the throttle: leaving rest, returning to rest and crossing zero are reported the moment they happen, and a channel always ends on its rest value — a released stick reports `0, 0` rather than leaving the hero drifting. `y` follows the engine's y-down convention, so `-1` is up. Everything held is released (a `buttonup`, plus a final rest-value `stick`/`trigger`) when the app backgrounds or the pad disconnects, so a direction never sticks.
190
+
191
+ On Android the buttons are captured at the activity window: the game view needs no focus, and mapped buttons are consumed so `b` does not act as Back and the d-pad does not move focus between overlay buttons. Unmapped keys (Back, volume) pass through. On Android 11+ the module opts the window out of joystick motion batching, so stick changes arrive as the pad reports them instead of once per display frame.
192
+
193
+ `platformer.js` runs on either the on-screen buttons or a pad.
194
+
168
195
  ## SpriteSheet
169
196
 
170
197
  Create one sheet per texture and share it across every sprite that uses it — sprites sharing a sheet batch into a single draw call.
@@ -231,6 +258,10 @@ Methods: `play(name)` starts an animation, `stop()` halts it on the current fram
231
258
  | `rotatable` | `false` | Two-finger rotate |
232
259
  | `touchEnabled` | `true` | `false` = touches pass through to sprites underneath |
233
260
 
261
+ `drag`, `pinch` and `rotate` all cross the bridge at ~10 Hz, and each one always delivers its final value when the gesture ends, so the value a handler last saw is the value the sprite kept. Before `9c45cf6` only `drag` was throttled and the other two fired per motion event. The two-finger rotation delta also takes the short way round the +/-180 degree flip from that commit on; earlier builds could add a whole turn in one move.
262
+
263
+ The touch slop that separates a tap from a drag is a screen-pixel distance divided by `cameraScale`, so a tap stays a tap at any zoom. `screenFixed` sprites keep the raw pixel slop, since they already live in screen space. Earlier builds compared a pixel constant against world units, which made taps too forgiving when zoomed out and too strict when zoomed in.
264
+
234
265
  Hit-testing runs against the transformed shape (rotation and scale included), topmost first, and is multi-touch: each finger runs its own gesture and a sprite belongs to at most one finger. A second finger landing on empty space — or on the sprite already held — pinches/rotates that sprite instead, per its flags.
235
266
 
236
267
  ### Physics
@@ -245,6 +276,7 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
245
276
  | `angularVelocity` | `0` | deg/s |
246
277
  | `thrust` | `0` | px/s² along the current heading (Newtonian flight) |
247
278
  | `wrapAround` | `false` | Re-enter from the opposite screen edge (Asteroids) |
279
+ | `wrapWorldX` | `false` | Opt this sprite into the GameView's circular `worldWrapX` interval. Native movement normalizes `x` inside it; rendering, touch, sprite overlap, solid resolution and swept movement all choose the nearest periodic image when **both** sprites participate. Takes precedence over `wrapAround`, `wrapX` and `wrapShift`; `screenFixed` sprites are excluded. A `TileLayer` repeats across the seam only when its `x` equals `minX` and its width equals `maxX - minX`. `raycast()`, `findPath()`, particles, ropes and skid trails do **not** repeat periodically |
248
280
  | `wrapX`, `wrapShift` | `0` | Scroll looping: at `x < wrapX`, add `wrapShift`. Two screen-wide copies with `{ wrapX: -W/2, wrapShift: 2*W }` and a negative `velocityX` make a seamless parallax layer with no JS in the loop |
249
281
 
250
282
  ### Solids
@@ -257,6 +289,7 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
257
289
  | `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 |
258
290
  | `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 |
259
291
  | `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 |
292
+ | `impactThreshold` | `40` | Minimum compensated relative normal speed in px/s before this receiver fires `solidimpact`. Minimum 0. Each participant applies its own threshold and owns its own gate, which rearms only after more than 100 ms of confirmed separation in game time |
260
293
  | `oneWay` | `false` | On the **solid**: pass-through except for landings on its top edge — classic platformer floors. Honoured by `'block'` solids only |
261
294
  | `carryRiders` | `true` | On the **solid**: a moving solid carries whoever stands on it (velocity, tweens, idle wobble — wrap teleports excluded). Set `false` for world-scroll terrain that moves while the player is meant to stay put |
262
295
 
@@ -284,7 +317,7 @@ Two consequences that cost an afternoon each if you meet them by surprise:
284
317
  | Property | Default | Notes |
285
318
  | --- | --- | --- |
286
319
  | `collisionGroup` | — | The tag *this* sprite carries |
287
- | `collidesWith` | — | Array of groups this sprite reports overlaps with |
320
+ | `collidesWith` | — | Array of groups this sprite reports overlaps with. Since `9c45cf6` a single group name is accepted as shorthand for a one-element array, on this property and on `solidWith`; `null` clears either. Earlier builds required the array and threw on a bare string |
288
321
  | `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%'` |
289
322
  | `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 |
290
323
  | `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 |
@@ -393,6 +426,7 @@ The rest of the contract, all read from the engine:
393
426
  | `collision` | `group`, `other`, `x`, `y` | Overlap with a `collidesWith` group began |
394
427
  | `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 |
395
428
  | `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 |
429
+ | `solidimpact` | `group`, `other`, `x`, `y`, `contactX`, `contactY`, `normalX`, `normalY`, `speed`, `restitution` | A sprite-to-sprite `solidWith` response met this receiver's `impactThreshold`. Both sprites in the pair get the event, sharing the contact point and `speed` and carrying **opposite** normals; `x`/`y` is the receiver's own position. `speed` is the relative closing normal speed *before* restitution, compensated for the gravity, thrust, car model and damping applied that frame, and `restitution` is the effective mixed value. `other` can be `null` if its proxy was released before delivery. **TileLayer cells never emit it.** Contact points are exact for circles and face contacts; an OBB corner is a support-point approximation. Spin, surface velocity, tweens and `followPath` add no physical normal velocity, so they never generate it on their own. With no listener on either sprite the resolver returns before computing any of this, so an unused event costs nothing |
396
430
  | `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 |
397
431
 
398
432
  ## Font
@@ -571,8 +605,8 @@ Movers list the layer's `collisionGroup` in `solidWith`. Rect, circle, swept, re
571
605
 
572
606
  Only these events exist. Nothing fires per frame, and events are only fired if a listener is registered.
573
607
 
574
- - **GameView**: `press`, `tap`, `release`, `resize`, `timer`, `performance`
575
- - **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `pathcomplete`, `collision`, `collisionend`, `land`, `wallhit`
608
+ - **GameView**: `press`, `tap`, `release`, `resize`, `timer`, `performance`, `buttondown`, `buttonup`, `stick`, `trigger`, `gamepadconnected`, `gamepaddisconnected`
609
+ - **Sprite** (and therefore **Text**): `press`, `release`, `tap`, `dragstart`, `drag`, `dragend`, `pinch`, `rotate`, `animationcomplete`, `complete`, `pathcomplete`, `collision`, `collisionend`, `land`, `wallhit`, `solidimpact`
576
610
  - **SpriteSheet, Font, Sound, Emitter, Rope, TileLayer**: none
577
611
 
578
612
  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.
@@ -1,6 +1,6 @@
1
1
  # Debugging and performance telemetry
2
2
 
3
- Verified against `ti.game@c216e7f`: both `GameViewProxy` implementations, `DebugHud`, `FrameStats`, both scene renderers, `example/camera.js`, `example/particles.js`, and `example/tilemap.js`.
3
+ Verified against `ti.game@3bea2f4`: 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
 
@@ -1,6 +1,6 @@
1
1
  # Upstream example catalog
2
2
 
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.
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.
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
 
@@ -9,7 +9,7 @@ The demos prove that API combinations run together on the current module. They a
9
9
  | `basic.js` | Sheets, frame animation, drag/pinch/rotate, tween completion | [Scaffolding](patterns.md#the-scaffolding) |
10
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
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 | [Platformer](recipes-action.md#platformer) |
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
13
  | `volley.js` | Restitution ball, `land`, collision response, coarse AI timer | [Ball sports](recipes-action.md#ball-sports--breakout-physics) |
14
14
  | `racing.js` | `carMode`, drift, skid marks, checkpoints and lap state | [Top-down racer](recipes-action.md#top-down-racer) |
15
15
  | `cards.js` | Dealing, fanned layout, selection tweens and idle wobble | [Cards and board games](recipes-adventure-effects.md#cards-and-board-games) |
@@ -18,6 +18,7 @@ The demos prove that API combinations run together on the current module. They a
18
18
  | `skate.js` | Endless-runner pooling, carried/uncarried terrain, parallax, emitter and audio hooks | [Endless runner](recipes-action.md#endless-runner) |
19
19
  | `pointclick.js` | Tap-to-walk with `findPath` → `followPath`, hotspots and `ySort` | [Point-and-click](recipes-adventure-effects.md#point--click-adventure) |
20
20
  | `particles.js` | Continuous/burst emitters, sprite target, HUD telemetry | [Particles](recipes-adventure-effects.md#particles) and [debugging](debugging-performance.md) |
21
+ | `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) |
21
22
  | `rhythm.js` | Pooled notes, native velocity, `press` pads, timing and audio feedback | [Rhythm game](recipes-action.md#rhythm-game) |
22
23
  | `camera.js` | Two-axis follow, bounds, zoom, shake, camera effects, parallax and debug HUD | [Camera work](recipes-adventure-effects.md#camera-work) |
23
24
  | `rope.js` | Rope pinned to a sprite, fixed head, tail weight | [Ropes and chains](recipes-adventure-effects.md#ropes-and-chains) |
@@ -34,9 +35,9 @@ The demos prove that API combinations run together on the current module. They a
34
35
  | `timescale.js` | Slow/frozen scene, engine timer versus real-time interval | [Game-clock timers](patterns.md#timers-on-the-game-clock) |
35
36
  | `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) |
36
37
  | `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) |
37
- | `plinko.js` | Circular pegs, swept balls and trigger slots | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
38
- | `pool.js` | Bilateral circle momentum, rails, pockets and linear damping | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
39
- | `drum.js` | Analytic circular containment and contact-driven impulses | [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) |
40
41
  | `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) |
41
42
  | `tilemap.js` | One 120×90 `TileLayer`, visible-cell rendering, cell collision, live edits, A* and HUD | [Tile maps](tilemaps.md) |
42
43
 
@@ -1,6 +1,6 @@
1
1
  # Cross-cutting ti.game patterns
2
2
 
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.
3
+ Verified against `ti.game@3bea2f4` 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@c216e7f` (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@3bea2f4` (2026-09-02).
4
4
 
5
5
  <!-- TOC-START -->
6
6
  ## Contents
@@ -44,7 +44,7 @@ Then declare it in `tiapp.xml`. The iOS platform key is `iphone`, not `ios`:
44
44
 
45
45
  Pinning `version` is worth the extra attribute: with two module versions installed side by side, an unpinned entry silently picks the highest one.
46
46
 
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 and **`0.5.0`** on 2026-08-27, and in between each number kept accepting features without moving. So every one of them covers several incompatible builds:
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:
48
48
 
49
49
  | A build from | Missing, compared to what this skill documents |
50
50
  | --- | --- |
@@ -61,8 +61,12 @@ Pinning `version` is worth the extra attribute: with two module versions install
61
61
  | `0.5.0` built before `85a723d` | additionally read-only `onWallLeft` / `onWallRight` and the `wallhit` transition event for sprite and TileLayer walls |
62
62
  | `0.5.0` built before `05cb60c` | additionally `wallSlideSpeed`, the native downward-speed cap while pressed against a wall |
63
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 |
64
68
 
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.
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.
66
70
 
67
71
  Building the module from source:
68
72
 
@@ -1,6 +1,6 @@
1
1
  # Action-game recipes
2
2
 
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.
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.
4
4
 
5
5
  <!-- TOC-START -->
6
6
  ## Contents
@@ -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@c216e7f`. 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@3bea2f4`. 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@c216e7f` (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@3bea2f4` (2026-09-02), 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
 
@@ -8,12 +8,13 @@ Verified against `ti.game@c216e7f` (2026-08-28), especially upstream `TODO.md` a
8
8
  | --- | --- | --- |
9
9
  | `gameView.panTo(x, y, options)` | Planned | Tween an invisible sprite, follow it for the cutscene, then follow the player again |
10
10
  | Full `sprite.parent` transform inheritance | Partial | `attachTo()` supplies position, optional rotation, and inherited opacity. Scale, visibility, flips, tint, hit-test hierarchy, y-sort hierarchy, and a `parent` property do not exist |
11
- | Native joystick/d-pad binding | Planned | Use overlaid Titanium controls that write native motion/input properties on `touchstart`/`touchend`; sibling views support simultaneous touches |
12
- | Gamepad support | Planned | Touch input only unless the app adds its own native integration |
11
+ | Native joystick/d-pad binding (`joystick.bind(sprite)`) | Planned | Use overlaid Titanium controls that write native motion/input properties on `touchstart`/`touchend`; sibling views support simultaneous touches. Upstream intends the same binding to accept a gamepad's left stick, which also does not exist yet |
13
12
  | Complete platformer slopes | Partial | `rotatedRect` geometry, normals, rider carrying, and sliding work. Smooth uphill walking, contact-only surface friction, and connected terrain are not implemented |
14
13
  | Tile animation | Planned TileLayer follow-up | Change ids deliberately or use sprites for the animated cells; there is no per-id native frame cycle |
15
14
  | `raycast()` against TileLayer cells | Planned TileLayer follow-up | Use sprite colliders for ray-visible obstacles or grid/cell queries appropriate to the game |
16
15
  | Tile trigger events (`collision`/`collisionend` for water/lava) | Planned TileLayer follow-up | Use sparse invisible trigger sprites or map the mover/world point to cells on a coarse decision/event path |
16
+ | `solidimpact` from TileLayer cells | Not implemented | Solid cells block, bounce and ground movers but emit no impact event. Put a sprite solid where the hit must be audible |
17
+ | Periodic repetition of `raycast()`, `findPath()`, particles, ropes and skid trails across a `worldWrapX` seam | Not implemented | Those queries and effects see a single linear world. Keep them away from the seam, or resolve the nearest image in JS before calling |
17
18
  | One-call Tiled loader for tilesets and multiple layers | Planned TileLayer follow-up | Parse the exported JSON in app code and create one TileLayer per layer/tileset |
18
19
  | Per-frame animation events | Planned | Split an action into short non-looping animations and react to `animationcomplete`, or use an engine timer aligned to a deliberate cadence |
19
20
  | Ping-pong, repeat/yoyo, random animation offset, per-sprite animation speed | Planned | Author ping-pong frames or re-launch a tween from `complete`; avoid a JS per-frame loop |
@@ -35,6 +36,9 @@ Verified against `ti.game@c216e7f` (2026-08-28), especially upstream `TODO.md` a
35
36
  - Shape-aware solids: circle and rotated-rect geometry, one-way platforms, carried riders, containment, bilateral circle push, two-sided restitution, `gravityX`, and `linearDamping`.
36
37
  - Native wall contacts: `onWallLeft` / `onWallRight`, `wallhit`, TileLayer-wall parity, and `wallSlideSpeed`.
37
38
  - Native particles, ropes, effect/music sound backends, car physics, thrust, drag/pinch/rotate, and multi-touch sprites.
39
+ - Gamepads: normalized `buttondown`/`buttonup` names, throttled `stick`/`trigger`, connect/disconnect events, `gamepads`/`gamepad` snapshots, dead zone and stick-press hysteresis, and release-everything on background or disconnect.
40
+ - Circular horizontal worlds: `gameView.worldWrapX` plus per-sprite `wrapWorldX`, seam-aware camera, rendering, touch, overlap, solid resolution and swept movement, and a full-width TileLayer that repeats across the seam.
41
+ - Discrete physical impacts: the `solidimpact` event with per-receiver `impactThreshold`, compensated closing speed, shared contact point, opposite normals and mixed restitution, for `block`, `contain` and bilateral `push` responses between sprites.
38
42
 
39
43
  ## Deliberately out of scope upstream
40
44
 
@@ -45,6 +49,6 @@ Verified against `ti.game@c216e7f` (2026-08-28), especially upstream `TODO.md` a
45
49
 
46
50
  ## Checking a moving target
47
51
 
48
- The manifest remains `0.5.0` across shape-aware solids, the HUD, and TileLayer, so the version cannot prove a feature exists. Prefer the build commit/date and safe feature probes such as `typeof Game.createTileLayer === 'function'`. Read a property before writing it; Kroll can preserve unknown writes on a proxy.
52
+ The manifest number cannot prove a feature exists: it stayed at `0.5.0` across shape-aware solids, the HUD, TileLayer, gamepads, world wrapping and `solidimpact`, and only moved to `0.6.0` in the commit right after the `0.5.0` release was tagged. A locally built artifact stamped `0.5.0` may therefore be weeks older than the published release of the same number. Prefer the build commit/date and safe feature probes such as `typeof Game.createTileLayer === 'function'`. Read a property before writing it; Kroll can preserve unknown writes on a proxy.
49
53
 
50
54
  Before claiming an item is still absent, re-check upstream `TODO.md`, `README.md`, both platform proxies, and the examples. Source code wins over prose when they disagree.
@@ -1,6 +1,6 @@
1
1
  # Native tile maps
2
2
 
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`.
3
+ Verified against `ti.game@3bea2f4`: 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