@maccesar/titools 4.19.0 → 4.21.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.
Files changed (30) hide show
  1. package/README.md +11 -6
  2. package/agents/ti-pro.md +3 -0
  3. package/commands/ti-audit.md +11 -10
  4. package/commands/ti-new-screen.md +1 -0
  5. package/lib/config.js +1 -0
  6. package/package.json +1 -1
  7. package/skills/purgetss/SKILL.md +1 -1
  8. package/skills/purgetss/references/cli-commands.md +25 -6
  9. package/skills/purgetss/references/customization-deep-dive.md +9 -4
  10. package/skills/purgetss/references/multi-density-images.md +35 -5
  11. package/skills/purgetss/references/version-history.md +5 -0
  12. package/skills/ti-expert/SKILL.md +5 -2
  13. package/skills/ti-expert/references/patterns.md +2 -0
  14. package/skills/ti-game/SKILL.md +5 -2
  15. package/skills/ti-game/references/api.md +38 -4
  16. package/skills/ti-game/references/debugging-performance.md +1 -1
  17. package/skills/ti-game/references/examples.md +6 -5
  18. package/skills/ti-game/references/patterns.md +1 -1
  19. package/skills/ti-game/references/project-setup.md +7 -3
  20. package/skills/ti-game/references/recipes-action.md +1 -1
  21. package/skills/ti-game/references/recipes-adventure-effects.md +1 -1
  22. package/skills/ti-game/references/roadmap.md +8 -4
  23. package/skills/ti-game/references/tilemaps.md +1 -1
  24. package/skills/ti-reuse-first/SKILL.md +88 -0
  25. package/skills/ti-reuse-first/references/alloy-reuse.md +127 -0
  26. package/skills/ti-reuse-first/references/diagnostics.md +137 -0
  27. package/skills/ti-reuse-first/references/indirection.md +89 -0
  28. package/skills/ti-reuse-first/references/purgetss-reuse.md +84 -0
  29. package/skills/ti-reuse-first/references/the-ladder.md +123 -0
  30. package/skills/ti-ui/SKILL.md +2 -0
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  </div>
8
8
 
9
- TiTools is a Titanium SDK toolkit for AI coding assistants. It ships 10 skills — 5 specialist skills (`ti-expert`, `purgetss`, `ti-ui`, `ti-game`, `ti-synthengine`) plus 5 documentation-mirror skills (`ti-api`, `ti-guides`, `ti-howtos`, `alloy-guides`, `alloy-howtos`) — a research agent, and reference files covering Titanium architecture, API reference, native how-tos, Alloy MVC, PurgeTSS styling and resource automation, UI/UX patterns, 2D games, and native sound synthesis.
9
+ TiTools is a Titanium SDK toolkit for AI coding assistants. It ships 11 skills — 6 specialist skills (`ti-reuse-first`, `ti-expert`, `purgetss`, `ti-ui`, `ti-game`, `ti-synthengine`) plus 5 documentation-mirror skills (`ti-api`, `ti-guides`, `ti-howtos`, `alloy-guides`, `alloy-howtos`) — a research agent, and reference files covering reuse and simplification, Titanium architecture, API reference, native how-tos, Alloy MVC, PurgeTSS styling and resource automation, UI/UX patterns, 2D games, and native sound synthesis.
10
10
 
11
11
  The reference files are maintained against official documentation whenever an official source exists, so the assistant can retrieve current framework behavior instead of guessing from generic training data.
12
12
 
@@ -36,7 +36,7 @@ The fastest way to get started with Claude Code. One command to add the marketpl
36
36
  ```
37
37
 
38
38
  What you get:
39
- - All 10 TiTools skills (`ti-expert`, `purgetss`, `ti-ui`, `ti-game`, `ti-synthengine`, `ti-api`, `ti-guides`, `ti-howtos`, `alloy-guides`, `alloy-howtos`)
39
+ - All 11 TiTools skills (`ti-reuse-first`, `ti-expert`, `purgetss`, `ti-ui`, `ti-game`, `ti-synthengine`, `ti-api`, `ti-guides`, `ti-howtos`, `alloy-guides`, `alloy-howtos`)
40
40
  - ti-pro research agent
41
41
  - Session hook that auto-detects Titanium projects (`tiapp.xml`, Alloy, PurgeTSS, `ti.game`, `ti.synthengine`)
42
42
  - Slash commands: `/ti-check`, `/ti-new-screen`, `/ti-audit`
@@ -174,6 +174,7 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
174
174
 
175
175
  | Skill | Purpose | Best For |
176
176
  | ------------ | ----------------------------------- | --------------------------------------- |
177
+ | ti-reuse-first | Reuse before writing new code | Whether it should exist and what already does it; duplication sweeps |
177
178
  | ti-expert | Architecture and implementation | Starting point for most tasks |
178
179
  | purgetss | Alloy styling + asset automation | Utility classes; Alloy/Classic branding, images, colors, fonts and modules |
179
180
  | ti-ui | UI/UX patterns | Complex layouts, ListViews, platform UI |
@@ -186,7 +187,8 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
186
187
  | alloy-howtos | Alloy CLI, configuration, debugging | alloy.jmk, config.json, custom XML tags |
187
188
 
188
189
  Notes:
189
- - The first five (`ti-expert`, `purgetss`, `ti-ui`, `ti-game`, `ti-synthengine`) are specialist workflow skills reflecting TiTools' Titanium conventions and native-module contracts.
190
+ - The first six (`ti-reuse-first`, `ti-expert`, `purgetss`, `ti-ui`, `ti-game`, `ti-synthengine`) are specialist workflow skills reflecting TiTools' Titanium conventions and native-module contracts.
191
+ - `ti-reuse-first` runs before the others. It asks whether a thing needs to exist and what already does it — a search order that stops at the first rung that holds — and sweeps an existing `app/` for near-identical controllers, single-listener events, wrappers around one call, and layout recomputed in JavaScript. It is deliberately two-sided: it says when adding a module or a Widget is the right answer, so the bias does not simply invert.
190
192
  - `ti-game` documents the `ti.game` native module (2D sprite engine on OpenGL ES 2.0, Android and iOS). Its API reference was verified against the module source and tracks upstream `main` (the module ships features ahead of its manifest version); it applies to Alloy and Classic projects alike and does not depend on PurgeTSS.
191
193
  - `ti-synthengine` translates aesthetic sound requests into strict, production-ready JavaScript for the cross-platform `ti.synthengine` 1.0.0 module. Seven references preserve the official documentation and an eighth adds curated retro, xylophone, memory-pad, polyphony and timer-ownership recipes; the API contract was cross-checked against both native implementations.
192
194
  - The latter five are documentation-mirror skills sourced from the official Titanium SDK docs (`tidev/titanium-docs`) and audited against them via the internal `titools-skill-auditor`.
@@ -249,7 +251,7 @@ TiTools provides slash commands for common tasks. Both channels ship them: the p
249
251
  These commands automatically invoke the relevant skills:
250
252
  - `/ti-check` reads `tiapp.xml`, validates declared native modules, and reports project health
251
253
  - `/ti-new-screen` uses `ti-expert` + `purgetss` to create properly structured files
252
- - `/ti-audit` routes all 10 skills: core Titanium checks always, Alloy/PurgeTSS checks when detected, integration how-tos when used, and `ti-game` / `ti-synthengine` checks when those modules are declared
254
+ - `/ti-audit` routes all 11 skills: the reuse sweep first, core Titanium checks always, Alloy/PurgeTSS checks when detected, integration how-tos when used, and `ti-game` / `ti-synthengine` checks when those modules are declared
253
255
 
254
256
  ---
255
257
 
@@ -289,6 +291,7 @@ All skills include automatic project detection to ensure compatibility:
289
291
  | Skill | What It Detects | How It Works |
290
292
  | --------- | --------------------- | ------------------------------------------------------------- |
291
293
  | purgetss | PurgeTSS mode and project type | Checks PurgeTSS markers, then separates Alloy utilities from Classic standalone commands |
294
+ | ti-reuse-first | Existing app structure | Searches `app/lib/`, `app/controllers/`, `app/widgets/`, `purgetss/config.cjs` before proposing anything new |
292
295
  | ti-expert | Alloy vs Classic | Checks for `app/` (Alloy) vs `Resources/` (Classic) structure |
293
296
  | ti-ui | Titanium projects | Checks for `tiapp.xml` (both Alloy and Classic) |
294
297
  | ti-game | ti.game module | Checks for `<module>ti.game</module>` in `tiapp.xml` or `require('ti.game')` |
@@ -301,16 +304,17 @@ Why this matters:
301
304
 
302
305
  ### Skill hierarchy
303
306
 
304
- `ti-expert` is the architectural starting point and coordinates with the rest of the bundled catalog:
307
+ `ti-reuse-first` runs first — it decides whether there is anything to build — and `ti-expert` is the architectural starting point for what survives that question. Together they coordinate the rest of the bundled catalog:
305
308
 
306
309
  | Scope | Skills |
307
310
  | --- | --- |
311
+ | Whether to build it at all | `ti-reuse-first`, first |
308
312
  | Architecture and UI | `ti-expert`, `ti-ui`, `purgetss` when configured |
309
313
  | Native modules | `ti-game`, `ti-synthengine` when declared |
310
314
  | Exact SDK/API guidance | `ti-api`, `ti-guides`, `ti-howtos` |
311
315
  | Alloy MVC and tooling | `alloy-guides`, `alloy-howtos` for Alloy projects |
312
316
 
313
- All ten ship with TiTools; no second skill package is required.
317
+ All eleven ship with TiTools; no second skill package is required.
314
318
 
315
319
  ---
316
320
 
@@ -804,6 +808,7 @@ This pattern is documented across four bundled TiTools skills: `ti-ui`, `ti-expe
804
808
 
805
809
  | Skill | SKILL.md | References |
806
810
  | --------- | ----------------------------- | ----------------------------------------------------- |
811
+ | ti-reuse-first | Reuse Before You Write | 5 files (the ladder, Alloy reuse, indirection, PurgeTSS reuse, diagnostics) |
807
812
  | ti-expert | Architecture + Implementation | 26 files (patterns, feedback surfaces, file type association, sharing, testing, security, etc.) |
808
813
  | purgetss | Setup + Critical Rules | 34 files (Classic compatibility, grid, animations, icons, class-index, SVG pipeline, etc.) |
809
814
  | ti-ui | UI Rules + Platform Diffs | 14 files (layouts, lists, gestures, etc.) |
package/agents/ti-pro.md CHANGED
@@ -2,6 +2,7 @@
2
2
  name: ti-pro
3
3
  description: Titanium SDK and Alloy research specialist. Loads the complete TiTools skill catalog and uses the relevant scopes together for research-style questions. Use for codebase analysis, architecture reviews, Titanium SDK implementation research, cross-feature questions, native game or sound modules, and platform-specific differences. Returns concrete findings with file and line references.
4
4
  skills:
5
+ - ti-reuse-first
5
6
  - ti-expert
6
7
  - purgetss
7
8
  - ti-ui
@@ -24,6 +25,7 @@ You are a research specialist for Titanium SDK and Alloy mobile development. Wit
24
25
 
25
26
  When invoked, research complex topics by consulting the applicable skills from the complete TiTools catalog:
26
27
 
28
+ - `ti-reuse-first` - Whether the thing should exist at all, and what already does it; duplication and dead-structure sweeps
27
29
  - `ti-expert` - Architecture, patterns, conventions
28
30
  - `purgetss` - Utility-first styling classes (optional add-on; use when the project already has it or the user asks about it)
29
31
  - `ti-ui` - UI/UX patterns, layouts, gestures
@@ -56,6 +58,7 @@ Do not force conditional skills onto an unrelated project. State which scopes we
56
58
 
57
59
  | Use Instead | Reason |
58
60
  | -------------------------------------------- | -------------------------------------- |
61
+ | `/ti-reuse-first` before writing new code | Inline reuse check during development |
59
62
  | `/ti-expert` for architecture guidance | Inline consultation during development |
60
63
  | `/purgetss` to verify a class | Quick inline reference |
61
64
  | `/ti-ui` for UI/UX patterns and layouts | Task-oriented guidance |
@@ -7,22 +7,23 @@ Audit the current Titanium project by invoking every applicable TiTools skill. F
7
7
 
8
8
  Core Titanium audit:
9
9
 
10
- 1. Invoke `ti-expert` — check architecture, navigation ownership, event/timer cleanup, memory leaks, bridge pressure, testing boundaries, adaptive layouts and code conventions.
11
- 2. Invoke `ti-guides` — check `tiapp.xml`, CommonJS/Hyperloop usage, Titanium CLI/build assumptions, memory and bridge guidance, distribution settings, and toolchain compatibility.
12
- 3. Invoke `ti-api` — verify every audited `Ti.*` API, event, property, constant and third-party module call against its exact signature. Do not report a suspected API defect from memory.
13
- 4. Invoke `ti-ui` when the project creates Titanium UI — check layouts, list performance, event bubbling, gestures, animations, accessibility, orientation and platform-specific UI.
14
- 5. Invoke `ti-howtos` when the project implements a native integration such as location, maps, push, camera, media, SQLite, HTTPClient, WebView, intents, Keychain/iCloud, Hyperloop or CI/CD — audit the complete integration and platform setup, not only the JavaScript call site.
10
+ 1. Invoke `ti-reuse-first` — before judging how anything is built, establish whether it should exist and what already does it: near-identical controllers, panels copied instead of parameterized, events with a single listener, wrappers around one call, repeated class strings, and layout recomputed in JavaScript that TSS resolves declaratively. Report each finding with the command that measured it, and with what consolidating it would cost as well as save — duplication kept on purpose is a valid answer.
11
+ 2. Invoke `ti-expert` — check architecture, navigation ownership, event/timer cleanup, memory leaks, bridge pressure, testing boundaries, adaptive layouts and code conventions.
12
+ 3. Invoke `ti-guides` — check `tiapp.xml`, CommonJS/Hyperloop usage, Titanium CLI/build assumptions, memory and bridge guidance, distribution settings, and toolchain compatibility.
13
+ 4. Invoke `ti-api` — verify every audited `Ti.*` API, event, property, constant and third-party module call against its exact signature. Do not report a suspected API defect from memory.
14
+ 5. Invoke `ti-ui` when the project creates Titanium UI — check layouts, list performance, event bubbling, gestures, animations, accessibility, orientation and platform-specific UI.
15
+ 6. Invoke `ti-howtos` when the project implements a native integration such as location, maps, push, camera, media, SQLite, HTTPClient, WebView, intents, Keychain/iCloud, Hyperloop or CI/CD — audit the complete integration and platform setup, not only the JavaScript call site.
15
16
 
16
17
  Project-type audit:
17
18
 
18
- 6. If the project is Alloy (`app/views/`, `app/controllers/`, `app/styles/`), invoke `alloy-guides` — check controllers, XML/TSS, models, collections, widgets, sync adapters and cleanup patterns.
19
- 7. If the Alloy project uses `alloy.jmk`, `config.json`, `widget.json`, conditional views, custom XML tags or Alloy CLI workflows, invoke `alloy-howtos` — check configuration precedence, build hooks, generated structures and compilation/debugging conventions.
20
- 8. If the project uses PurgeTSS (`purgetss/`, `purgetss/config.cjs`, a PurgeTSS-generated `app.tss`, or `purgetss.{ui,colors,fonts}.js`), invoke `purgetss` and detect Alloy vs. Classic before applying findings. In Alloy, audit unsupported utility classes, padding on Views, unnecessary manual TSS, dynamic components, grids, icons and animations. In Classic, audit only the supported standalone asset/CommonJS commands and their `Resources/` outputs; do not require or recommend `alloy.jmk`, generated TSS, utility classes, or `$.UI.create()`.
19
+ 7. If the project is Alloy (`app/views/`, `app/controllers/`, `app/styles/`), invoke `alloy-guides` — check controllers, XML/TSS, models, collections, widgets, sync adapters and cleanup patterns.
20
+ 8. If the Alloy project uses `alloy.jmk`, `config.json`, `widget.json`, conditional views, custom XML tags or Alloy CLI workflows, invoke `alloy-howtos` — check configuration precedence, build hooks, generated structures and compilation/debugging conventions.
21
+ 9. If the project uses PurgeTSS (`purgetss/`, `purgetss/config.cjs`, a PurgeTSS-generated `app.tss`, or `purgetss.{ui,colors,fonts}.js`), invoke `purgetss` and detect Alloy vs. Classic before applying findings. In Alloy, audit unsupported utility classes, padding on Views, unnecessary manual TSS, dynamic components, grids, icons and animations. In Classic, audit only the supported standalone asset/CommonJS commands and their `Resources/` outputs; do not require or recommend `alloy.jmk`, generated TSS, utility classes, or `$.UI.create()`.
21
22
 
22
23
  Native-module audit:
23
24
 
24
- 9. If the project declares `ti.game`, invoke `ti-game` — check for sprites moved from timers instead of native properties, levels sized from `Ti.Platform.displayCaps` instead of the `resize` event, sprites added one by one instead of in one array, and timers or sounds left running when the window closes.
25
- 10. If the project declares `ti.synthengine`, invoke `ti-synthengine` — check strict option names and types, mutually exclusive pitch fields, valid envelope intersections, checked Boolean results, one clear lifecycle owner, delayed startup after layout, app-owned timer cancellation, and `shutdown()` cleanup.
25
+ 10. If the project declares `ti.game`, invoke `ti-game` — check for sprites moved from timers instead of native properties, levels sized from `Ti.Platform.displayCaps` instead of the `resize` event, sprites added one by one instead of in one array, and timers or sounds left running when the window closes.
26
+ 11. If the project declares `ti.synthengine`, invoke `ti-synthengine` — check strict option names and types, mutually exclusive pitch fields, valid envelope intersections, checked Boolean results, one clear lifecycle owner, delayed startup after layout, app-owned timer cancellation, and `shutdown()` cleanup.
26
27
 
27
28
  If a conditional skill does not apply, record it as `not applicable` with the detected reason instead of inventing findings from it.
28
29
 
@@ -6,6 +6,7 @@ argument-hint: "<screenName>"
6
6
 
7
7
  Create a new Alloy screen with the given name. Before creating files:
8
8
 
9
+ 0. Invoke the `ti-reuse-first` skill and confirm the screen has to be new. Run rung 1 against this project: list `app/controllers/` and `app/widgets/` and check whether an existing screen covers the case with a parameter, or whether a `<Require>` into an existing window is enough. If the answer is that an existing screen should be parameterized instead, say so, name it, and **stop** — do not generate the trio. Only continue when a new screen is genuinely the answer.
9
10
  1. Invoke the `alloy-guides` skill to follow Alloy MVC patterns
10
11
  2. If PurgeTSS is detected (purgetss/ folder exists), invoke the `purgetss` skill — all styling MUST use utility classes in the XML, NOT custom TSS
11
12
  3. If PurgeTSS is detected, invoke the `ti-ui` skill for layout patterns
package/lib/config.js CHANGED
@@ -30,6 +30,7 @@ export const REPO_API_URL = 'https://api.github.com/repos/macCesar/titools';
30
30
 
31
31
  // Skills to install
32
32
  export const SKILLS = [
33
+ 'ti-reuse-first',
33
34
  'ti-expert',
34
35
  'purgetss',
35
36
  'ti-ui',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "4.19.0",
3
+ "version": "4.21.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).
@@ -9,12 +9,15 @@ allowed-tools: Read, Grep, Glob, Edit, Write, Bash(git *), Bash(node *)
9
9
 
10
10
  Practical architecture and implementation guidance for Titanium SDK apps (Alloy and Classic). Focus on maintainability, clear boundaries, and low-friction testing.
11
11
 
12
+ > **Before anything new exists, run the `ti-reuse-first` skill.** It answers *whether* a controller, view, Widget, `app/lib/` module, event or wrapper should exist at all and what already does it; this guide answers *how* to build whatever survives that question. Copying an existing controller as a template counts as creating something new.
13
+
12
14
  ## Required workflow
13
15
 
14
16
  This file is an index. Read the references that govern the task before making a recommendation or changing code.
15
17
 
16
18
  | Task | Required reading |
17
19
  | --- | --- |
20
+ | Anything new — a controller/view/style trio, Widget, `app/lib/` module, event, wrapper, or a file copied as a template | Run the `ti-reuse-first` skill first, then the row below that matches what survives |
18
21
  | Architecture tier, folders, or boundaries | [Architecture tiers](references/architecture-tiers.md), [Alloy structure](references/alloy-structure.md) |
19
22
  | Feedback, alerts, choices, or app-owned versus system UI | [Feedback surfaces](references/feedback-surfaces.md) |
20
23
  | Reusable Snackbar, Dialog, Bottom Sheet, Widget, or `<Require>` | [Feedback Widget contracts](references/feedback-widget-contracts.md), [Alloy structure](references/alloy-structure.md) |
@@ -151,14 +154,14 @@ For the complete reference with examples, see [Alloy builtins and globals](refer
151
154
  | ---------------------------------- | -------------------------------------------------------------- |
152
155
  | How to create a new Alloy project? | `ti create -t app --alloy` (not `--classic` + `alloy new`) |
153
156
  | Fastest way to build? | `tn <recipe>` (using TiNy CLI wrapper) |
154
- | Controller > 100 lines? | Extract to Tier 2 (Services) |
157
+ | Controller > 100 lines? | A smell, not a threshold — see `ti-reuse-first` |
155
158
  | More than 50 IDs in XML? | Use Tier 3 (ID Scoping) |
156
159
  | Where does API call go? | `lib/api/` |
157
160
  | Where does business logic go? | `lib/services/` |
158
161
  | How deep should `lib` folders be? | One level: `lib/<type>/<file>.js` |
159
162
  | Where do I store auth tokens? | Keychain (iOS) / KeyStore (Android) via service |
160
163
  | Models or Collections? | Collections for API data, Models for SQLite persistence |
161
- | Ti.App.fireEvent or EventBus? | Always EventBus (Backbone.Events) |
164
+ | Ti.App.fireEvent or EventBus? | EventBus — once `ti-reuse-first` says it needs an event |
162
165
  | Direct navigation or service? | Always Navigation service (auto cleanup) |
163
166
  | Snackbar, Dialog, or Bottom Sheet? | Classify urgency, persistence, reversibility, and choice first |
164
167
  | Widget or `<Require>`? | Widget for a self-contained API/lifecycle; `<Require>` for app composition |
@@ -15,6 +15,8 @@
15
15
 
16
16
  <!-- TOC-END -->
17
17
 
18
+ > **Before introducing any of these:** each pattern below adds a layer, and a layer only pays for itself where there is a boundary — a place the implementation could plausibly change, or where the two sides genuinely should not know each other. Around a single call site with one caller, the same pattern is a detour that costs on every read. The `ti-reuse-first` skill runs that check; use it first, and treat the patterns here as the answer to *how* once it has established *whether*.
19
+
18
20
  ## 1. Native module abstraction (Wrapper)
19
21
 
20
22
  **Use when:** Using native modules like Maps, Biometrics, or specialized Media players.
@@ -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