@maccesar/titools 4.11.0 → 4.13.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/README.md +4 -3
- package/package.json +1 -1
- package/skills/purgetss/references/EXAMPLES.md +1 -1
- package/skills/purgetss/references/dynamic-component-creation.md +1 -1
- package/skills/purgetss/references/multi-density-images.md +1 -1
- package/skills/ti-expert/references/alloy-builtins.md +4 -4
- package/skills/ti-expert/references/alloy-structure.md +1 -1
- package/skills/ti-expert/references/code-conventions.md +1 -1
- package/skills/ti-expert/references/examples.md +1 -1
- package/skills/ti-game/SKILL.md +21 -3
- package/skills/ti-game/references/api.md +49 -5
- package/skills/ti-game/references/project-setup.md +32 -4
- package/skills/ti-game/references/recipes.md +122 -17
- package/skills/ti-game/references/roadmap.md +6 -3
package/README.md
CHANGED
|
@@ -177,7 +177,7 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
|
|
|
177
177
|
| ti-expert | Architecture and implementation | Starting point for most tasks |
|
|
178
178
|
| purgetss | Utility-first styling | UI styling and animations |
|
|
179
179
|
| ti-ui | UI/UX patterns | Complex layouts, ListViews, platform UI |
|
|
180
|
-
| ti-game | 2D games with the `ti.game` module | Sprites, physics, collisions, raycasts, pathfinding, text HUDs, particles, camera |
|
|
180
|
+
| ti-game | 2D games with the `ti.game` module | Sprites, physics, collisions, raycasts, pathfinding, text HUDs, sprite attachment, particles, camera |
|
|
181
181
|
| ti-api | Complete Titanium API reference | Looking up properties, methods, events |
|
|
182
182
|
| ti-guides | SDK fundamentals | tiapp.xml, Hyperloop, distribution, JDK/Xcode compatibility, release notes |
|
|
183
183
|
| ti-howtos | Native feature integration | Location, push, media, platform APIs |
|
|
@@ -435,12 +435,13 @@ When it activates:
|
|
|
435
435
|
- `tiapp.xml` declares `ti.game`, or any file calls `require('ti.game')`
|
|
436
436
|
- Sprites, sprite sheets, TexturePacker atlases, frame animations, native tweens
|
|
437
437
|
- Physics: gravity and velocity, solid platforms, one-way floors, bouncing, the arcade car model, Newtonian thrust
|
|
438
|
-
- Collision groups, invisible trigger zones, enter/exit events, swept AABB for fast bullets, per-axis hitbox tuning
|
|
438
|
+
- Collision groups, invisible trigger zones, enter/exit events, swept AABB for fast bullets, per-axis hitbox tuning with the `debug` overlays
|
|
439
439
|
- Raycasts for line of sight, ledge probes and hitscan shots
|
|
440
440
|
- A* pathfinding (`findPath`) around tagged obstacles, feeding native patrol paths (`followPath`) and animation chaining (`play(name, { then })`)
|
|
441
441
|
- Bitmap-font text sprites with native word wrap, and screen-fixed HUDs inside the scene
|
|
442
|
+
- Sprites pinned to other sprites (`attachTo`) — name tags, health bars, shadows, turrets, tracked natively every frame
|
|
442
443
|
- Particle emitters, Verlet ropes, camera follow/zoom/shake/effects, `scrollFactor` parallax, low-latency sound
|
|
443
|
-
- Game-clock timers that obey pause and slow motion
|
|
444
|
+
- Game-clock timers that obey pause and slow motion, texture `unload()` for level streaming, and what LiveView reloads clean up on their own
|
|
444
445
|
- Drag & drop, pinch, rotate, multitouch controls
|
|
445
446
|
|
|
446
447
|
Example prompts:
|
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@ Complete examples of common patterns with anti-patterns and correct implementati
|
|
|
20
20
|
- [`rounded-full` Without Size](#rounded-full-without-size)
|
|
21
21
|
- [Square Brackets for Arbitrary Values](#square-brackets-for-arbitrary-values)
|
|
22
22
|
- [Layout Defaults](#layout-defaults)
|
|
23
|
-
- [ScrollView Without `content-w-screen` / `content-h-auto`](#scrollview-without-content-w-screen
|
|
23
|
+
- [ScrollView Without `content-w-screen` / `content-h-auto`](#scrollview-without-content-w-screen--content-h-auto)
|
|
24
24
|
- [Deprecated `theme.View.DEFAULT` vs `theme.extend.View`](#deprecated-themeviewdefault-vs-themeextendview)
|
|
25
25
|
- [Quick Reference Table](#quick-reference-table)
|
|
26
26
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- [Community-Discovered Patterns](#community-discovered-patterns)
|
|
13
13
|
- [Overview](#overview)
|
|
14
14
|
- [Method 1: `$.UI.create()` (Recommended)](#method-1-uicreate-recommended)
|
|
15
|
-
- [Method 2: `Alloy.createStyle()` + `applyProperties()`](#method-2-alloycreatestyle
|
|
15
|
+
- [Method 2: `Alloy.createStyle()` + `applyProperties()`](#method-2-alloycreatestyle--applyproperties)
|
|
16
16
|
- [Comparison: Which Method to Use?](#comparison-which-method-to-use)
|
|
17
17
|
- [Real-World Examples](#real-world-examples)
|
|
18
18
|
- [Important Notes](#important-notes)
|
|
@@ -18,7 +18,7 @@ For the terse flag reference, see the [`images` command reference](./cli-command
|
|
|
18
18
|
- [Why multi-density?](#why-multi-density)
|
|
19
19
|
- [Quick start](#quick-start)
|
|
20
20
|
- [The `purgetss/images/` convention](#the-purgetssimages-convention)
|
|
21
|
-
- [Source sizes — the 4× master convention](#source-sizes
|
|
21
|
+
- [Source sizes — the 4× master convention](#source-sizes--the-4-master-convention)
|
|
22
22
|
- [Pinning the output width with `--width`](#pinning-the-output-width-with---width)
|
|
23
23
|
- [The `images:` config section](#the-images-config-section)
|
|
24
24
|
- [Per-file overrides with `images.files`](#per-file-overrides-with-imagesfiles)
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
- [Compiler directives](#compiler-directives)
|
|
7
7
|
- [Alloy.Globals](#alloyglobals)
|
|
8
8
|
- [Alloy.CFG](#alloycfg)
|
|
9
|
-
- [Alloy.isTablet / Alloy.isHandheld](#alloyistablet
|
|
10
|
-
- [$.args , controller arguments](#args
|
|
11
|
-
- [Alloy.createController / Alloy.createWidget](#alloycreatecontroller
|
|
12
|
-
- [Alloy.createModel / Alloy.createCollection](#alloycreatemodel
|
|
9
|
+
- [Alloy.isTablet / Alloy.isHandheld](#alloyistablet--alloyishandheld)
|
|
10
|
+
- [$.args , controller arguments](#args--controller-arguments)
|
|
11
|
+
- [Alloy.createController / Alloy.createWidget](#alloycreatecontroller--alloycreatewidget)
|
|
12
|
+
- [Alloy.createModel / Alloy.createCollection](#alloycreatemodel--alloycreatecollection)
|
|
13
13
|
- [Conditional code in XML](#conditional-code-in-xml)
|
|
14
14
|
- [Quick reference](#quick-reference)
|
|
15
15
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- [lib/ folder and module require paths](#lib-folder-and-module-require-paths)
|
|
9
9
|
- [Data layer: two approaches](#data-layer-two-approaches)
|
|
10
10
|
- [Controller rules](#controller-rules)
|
|
11
|
-
- [Navigation & cleanup pattern](#navigation
|
|
11
|
+
- [Navigation & cleanup pattern](#navigation--cleanup-pattern)
|
|
12
12
|
- [i18n and accessibility rules](#i18n-and-accessibility-rules)
|
|
13
13
|
- [Widget structure](#widget-structure)
|
|
14
14
|
- [config.json reference](#configjson-reference)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
- [ES6+ features (Non-Module)](#es6-features-non-module)
|
|
8
8
|
- [No semicolons](#no-semicolons)
|
|
9
9
|
- [Styling with TSS](#styling-with-tss)
|
|
10
|
-
- [Platform & device modifiers](#platform
|
|
10
|
+
- [Platform & device modifiers](#platform--device-modifiers)
|
|
11
11
|
- [applyProperties() pattern](#applyproperties-pattern)
|
|
12
12
|
- [ListView templates](#listview-templates)
|
|
13
13
|
- [Memory cleanup](#memory-cleanup)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- [Native module wrapper service](#native-module-wrapper-service)
|
|
9
9
|
- [i18n helper](#i18n-helper)
|
|
10
10
|
- [Model with SQL adapter](#model-with-sql-adapter)
|
|
11
|
-
- [Fully styled & accessible view](#fully-styled
|
|
11
|
+
- [Fully styled & accessible view](#fully-styled--accessible-view)
|
|
12
12
|
- [Cleanup pattern in controller](#cleanup-pattern-in-controller)
|
|
13
13
|
- [Animation usage](#animation-usage)
|
|
14
14
|
- [Complete crud example](#complete-crud-example)
|
package/skills/ti-game/SKILL.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ti-game
|
|
3
|
-
description: "Use when building or reviewing 2D games with the ti.game module for Titanium SDK (Android and iOS) — sprites, sprite sheets, animations, tweens, physics (gravity, solids, carMode drifting, thrust), collision groups, trigger zones, swept AABB, raycast, paths, bitmap text, particles, ropes, camera follow/zoom/shake, parallax, sound, drag & drop, multitouch. AUTO-DETECT: if tiapp.xml declares ti.game or any file calls require('ti.game'), invoke BEFORE writing ANY game code. Also trigger on: game, juego,
|
|
3
|
+
description: "Use when building or reviewing 2D games with the ti.game module for Titanium SDK (Android and iOS) — sprites, sprite sheets, animations, tweens, physics (gravity, solids, carMode drifting, thrust), collision groups, trigger zones, swept AABB, raycast, paths, bitmap text, particles, ropes, camera follow/zoom/shake, parallax, sound, drag & drop, multitouch, attached labels, bars and turrets. AUTO-DETECT: if tiapp.xml declares ti.game or any file calls require('ti.game'), invoke BEFORE writing ANY game code. Also trigger on: game, juego, spritesheet, platformer, endless runner, top-down, shooter, racer, flappy, breakout, rhythm, cards, puzzle, hitbox, attachTo, line of sight, pathfinding, maze, patrol, HUD, score, game loop, 60 fps, GameView. ti.game is NOT Phaser, Godot or Box2D: you never write a game loop, never move a sprite from setInterval, and several familiar features (tilemap layer, parent/child transforms, joystick, gamepads) do not exist yet — check references/roadmap.md before inventing an API."
|
|
4
4
|
argument-hint: "[genre or feature]"
|
|
5
5
|
allowed-tools: Read, Grep, Glob, Edit, Write, Bash(node *)
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# ti.game — 2D game engine for Titanium SDK
|
|
9
9
|
|
|
10
|
-
`ti.game` is a native OpenGL ES 2.0 sprite engine exposed to JavaScript, written and maintained by **Michael Gangolf (m1ga)** — upstream is https://github.com/m1ga/ti.game. This skill tracks upstream `main` as of **2026-08-
|
|
10
|
+
`ti.game` is a native OpenGL ES 2.0 sprite engine exposed to JavaScript, written and maintained by **Michael Gangolf (m1ga)** — upstream is https://github.com/m1ga/ti.game. This skill tracks upstream `main` as of **2026-08-26**, manifest **`0.4.0`** — that bump is the first in a long run of feature landings (the text engine, `screenFixed`, `swept` collisions, `collisionend`, `followPath`, animation chaining, `raycast`, `findPath`, game-clock timers, `scrollFactor` parallax and the `multiply`/`screen` blend modes all shipped while it still said `0.3.0`), so anything still calling itself 0.3.0 has none of them. The number lags the code in the other direction too: per-axis hitboxes (`hitboxScaleX`/`hitboxScaleY`), text word wrap (`maxWidth`), `unload()` on sheets and fonts and the LiveView renderer lifecycle (2026-08-23), then sprite attachment (`attachTo`/`detach`, 2026-08-26), all landed with the manifest still at `0.4.0`, so date the build rather than trusting the version. When upstream and any fork disagree, upstream wins. Same JS API on Android and iOS — the iOS side is a class-per-class Obj-C twin of the Android engine.
|
|
11
11
|
|
|
12
12
|
Works in **both Classic and Alloy** projects. Nothing here depends on PurgeTSS.
|
|
13
13
|
|
|
@@ -33,6 +33,7 @@ The practical consequence, and the single biggest mistake to avoid:
|
|
|
33
33
|
| Parallax layers under a moving camera | hand-scrolled layers | `scrollFactor` on each layer |
|
|
34
34
|
| Camera that follows the player | timer writing `cameraX` | `gameView.follow(sprite, options)` |
|
|
35
35
|
| A score, a title, a label | `Ti.UI.Label` overlays | `Game.createText({ text, screenFixed: true })` |
|
|
36
|
+
| A name tag, health bar or turret that rides a sprite | a timer copying `owner.x`/`y` onto it | `tag.attachTo(owner, { offsetY: -40 })` |
|
|
36
37
|
| A dialog box that fits the screen | breaking the lines with `\n` after measuring in JS | `maxWidth` on the text sprite |
|
|
37
38
|
| A bullet fast enough to skip past a thin wall | shrinking the time step | `swept: true` on the bullet |
|
|
38
39
|
| To know the player *left* a zone | polling on a timer | the `collisionend` event |
|
|
@@ -168,6 +169,8 @@ One long string wraps itself: `maxWidth` breaks lines on word boundaries and re-
|
|
|
168
169
|
|
|
169
170
|
`screenFixed: true` works on **any** sprite: `x`/`y` become surface coordinates and camera position, zoom and shake are ignored, while touch still maps back. That is the HUD pattern — the score stays put while the camera follows the player. Without it, text is world-space and scrolls past like scenery (signposts, floating damage numbers).
|
|
170
171
|
|
|
172
|
+
A label that belongs *on* a moving sprite is neither: `tag.attachTo(hero, { offsetY: -40 })` pins it natively, resolved every frame after physics and before collisions, so it never lags by a frame and needs no JS. It works on any sprite, not just text — health bars, shadows, an invisible hitbox on a body, and with `rotate: true` a turret whose offset swings with the tank. Removing the owner removes what is attached to it. What it does *not* inherit is scale, opacity, visibility or tint — position and rotation only.
|
|
173
|
+
|
|
171
174
|
Overlaid `Ti.UI.Label` views still work and are still right for anything the engine cannot draw: native inputs, system fonts, accented text the built-in font lacks, scrollable menus. Just do not reach for them for a score any more.
|
|
172
175
|
|
|
173
176
|
Standard Titanium touch events do not reach the GameView, but ordinary Titanium buttons and views layered on top work normally, and sibling views receive simultaneous pointers — which is how the demos build multitouch d-pads (hold ▶ and press jump at the same time). One caveat from the demos: Android's `touchFeedback` ripple cannot animate over this canvas and spams `RippleDrawable` errors, so on-screen buttons set their own `backgroundColor` on `touchstart`/`touchend`.
|
|
@@ -176,6 +179,8 @@ Standard Titanium touch events do not reach the GameView, but ordinary Titanium
|
|
|
176
179
|
|
|
177
180
|
The render loop follows the activity lifecycle automatically, but **your JS timers and sounds do not**. Every demo that starts an interval or plays looping music stops it on window close. In Alloy, wire this into the controller's `cleanup()`.
|
|
178
181
|
|
|
182
|
+
Two things the engine now cleans up on its own, so do not hand-roll them: a **LiveView reload** retires the previous runtime's render loops, `SoundPool`, `MediaPlayer`s and audio proxies, so reloads stop stacking game loops; and releasing a sheet or font frees its GL texture. What you *can* now do explicitly is `spriteSheet.unload()` / `font.unload()` when streaming levels — permanent, so only after nothing draws from them.
|
|
183
|
+
|
|
179
184
|
Game-clock timers (`gameView.after` / `every`) are the exception that proves the rule: they run on the engine's clock, so they freeze at `timeScale: 0` and pause with the render loop instead of firing into a dead scene. That is why spawn waves and AI ticks belong there. A `setInterval` keeps running through a pause, which is right for a real-time countdown and wrong for everything else.
|
|
180
185
|
|
|
181
186
|
```javascript
|
|
@@ -199,6 +204,18 @@ Read the one that matches the task instead of guessing — the API surface below
|
|
|
199
204
|
| Install the module, tiapp.xml, Alloy vs Classic wiring, assets, sprite sheets, atlases, lifecycle, testing | [project-setup.md](references/project-setup.md) |
|
|
200
205
|
| What does NOT exist yet, what is coming, and how to work around it today | [roadmap.md](references/roadmap.md) |
|
|
201
206
|
|
|
207
|
+
When this skill runs out — a brand-new feature, a demo's exact code, a build question — read upstream itself at https://github.com/m1ga/ti.game. Each file there is authoritative for something different:
|
|
208
|
+
|
|
209
|
+
| Upstream file | What it is authoritative for |
|
|
210
|
+
| --- | --- |
|
|
211
|
+
| `README.md` | The module's own API documentation, and the demo index. Canonical, but it has lagged the code: verify a defaulted value against the source before relying on it |
|
|
212
|
+
| `example/` | 26 runnable demos, one per feature area — the reference implementations this skill's recipes are distilled from |
|
|
213
|
+
| `TODO.md` | What the maintainer has planned, with priorities. The source of [roadmap.md](references/roadmap.md) |
|
|
214
|
+
| `tutorial.md` | A step-by-step first scene (sprite, animation, tap-to-move). For a human learning the module, not for looking things up |
|
|
215
|
+
| `ios/README.md` | The iOS port's threading, coordinates and touch constants. Mostly for module developers; the app-facing facts are already here |
|
|
216
|
+
| `AGENTS.md` | Rules for changing the module itself — irrelevant to building a game with it |
|
|
217
|
+
| `android/src/ti/game/`, `ios/Classes/` | The truth. Defaults, clamping and edge cases in this skill were read here, not from prose |
|
|
218
|
+
|
|
202
219
|
## Before writing game code, check these
|
|
203
220
|
|
|
204
221
|
1. Is the level built inside a guarded `resize` handler, using `e.width`/`e.height`?
|
|
@@ -211,4 +228,5 @@ Read the one that matches the task instead of guessing — the API surface below
|
|
|
211
228
|
8. Is the HUD `Game.createText` with `screenFixed: true`, rather than a stack of overlaid labels?
|
|
212
229
|
9. Does anything travel far enough per frame to tunnel? Give it `swept: true`.
|
|
213
230
|
10. Does a timer drive game logic (spawn waves, AI ticks, respawns)? Move it to `gameView.every`/`after` so it obeys pause and slow motion.
|
|
214
|
-
11.
|
|
231
|
+
11. Does anything ride on top of a moving sprite — a name tag, a health bar, a turret, a hitbox? `attachTo` pins it natively; a timer copying coordinates is the old workaround.
|
|
232
|
+
12. Are you using an API that actually exists? Anything involving tilemaps, full parent/child transforms, joysticks, gamepads or slopes — check [roadmap.md](references/roadmap.md) first.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# ti.game API reference
|
|
2
2
|
|
|
3
|
-
Complete JS surface of `ti.game` as of upstream `main` on 2026-08-
|
|
3
|
+
Complete JS surface of `ti.game` as of upstream `main` on 2026-08-26, verified against the module source (`android/src/ti/game/*.java` and `engine/*.java`, mirrored by `ios/Classes/TG*`). Defaults come from the engine fields, not from prose. The manifest reads **`0.4.0`** since 2026-08-20, and that bump matters: the text engine (`createFont`/`createText`), `screenFixed`, `swept`, `collisionend`, `followPath`, animation chaining, `raycast`, `findPath`, the game-clock timers, `scrollFactor` and the `multiply`/`screen` blend modes all landed while the manifest still said `0.3.0`, so a build calling itself 0.3.0 has none of them. The version number is not a feature list either: `hitboxScaleX`/`hitboxScaleY`, text `maxWidth`, `SpriteSheet.unload()`/`Font.unload()`, the LiveView renderer lifecycle (all 2026-08-23) and `attachTo`/`detach` (2026-08-26) landed with the manifest still at `0.4.0`, so two builds calling themselves 0.4.0 can differ — check the build date, or feature-detect by reading the property **before** ever writing it (`typeof sprite.hitboxScaleX === 'undefined'` means the build predates it). Writing first proves nothing: an unknown property is stored on the Kroll proxy and reads back exactly as it was set. Methods are the reliable probe — `typeof sprite.attachTo === 'function'` cannot be faked by a previous write.
|
|
4
4
|
|
|
5
5
|
Every property listed is **live**: reading returns the current native value, even mid-drag or mid-tween. Every property can also be passed to its `create*` factory. All durations crossing the JS boundary are **milliseconds** (the engine converts to seconds internally).
|
|
6
6
|
|
|
7
|
+
<!-- TOC-START -->
|
|
7
8
|
## Contents
|
|
8
9
|
|
|
9
10
|
- [Module](#module)
|
|
@@ -18,6 +19,8 @@ Every property listed is **live**: reading returns the current native value, eve
|
|
|
18
19
|
- [Events at a glance](#events-at-a-glance)
|
|
19
20
|
- [Gotchas the property tables do not show](#gotchas-the-property-tables-do-not-show)
|
|
20
21
|
|
|
22
|
+
<!-- TOC-END -->
|
|
23
|
+
|
|
21
24
|
## Module
|
|
22
25
|
|
|
23
26
|
```javascript
|
|
@@ -44,8 +47,8 @@ A normal Titanium view — add it to a window or any container, size it with the
|
|
|
44
47
|
| Member | Type | Default | Notes |
|
|
45
48
|
| --- | --- | --- | --- |
|
|
46
49
|
| `add(object)` / `add([objects])` | method | — | Accepts Sprite, Emitter or Rope. An array crosses the bridge once and is committed under a single native scene lock — build a level into an array and add it in one call |
|
|
47
|
-
| `remove(object)` | method | — | Remove one object from the scene |
|
|
48
|
-
| `removeAllSprites()` | method | — | Clear the whole scene |
|
|
50
|
+
| `remove(object)` | method | — | Remove one object from the scene — **and, recursively, every sprite attached to it** (see [Attachment](#attachment)) |
|
|
51
|
+
| `removeAllSprites()` | method | — | Clear the whole scene, dropping every attachment with it |
|
|
49
52
|
| `pause()` / `resume()` | method | — | Render loop control. The activity/app lifecycle already pauses and resumes automatically |
|
|
50
53
|
| `backgroundColor` | string | — | GL clear color |
|
|
51
54
|
| `maxFps` | int | `0` | Frame cap. `60` stops 120 Hz ProMotion displays from doubling render work; `0` = display refresh rate |
|
|
@@ -86,6 +89,8 @@ Vertical follow is always on. **Horizontal follow is off until you pass `leftMar
|
|
|
86
89
|
|
|
87
90
|
Each call **resets every option to its default** before applying what you passed. Calling `follow(sprite)` with no options after configuring it wipes the configuration.
|
|
88
91
|
|
|
92
|
+
Anything that is not a sprite — `null`, a plain object, a number — clears the follow instead of throwing, which is also what `stopFollow()` does. Since 2026-08-26 that is guaranteed on Android too: the binding takes an untyped argument and checks the type itself, because a typed proxy parameter aborts the whole app on the JNI type check when JS sends a plain object (it crosses as a `HashMap`). Passing an options dict where the sprite goes now silently stops the camera rather than killing the process — a real behaviour difference from older builds.
|
|
93
|
+
|
|
89
94
|
### `findPath(from, to, options)`
|
|
90
95
|
|
|
91
96
|
| Option | Default | Effect |
|
|
@@ -120,7 +125,7 @@ Create one sheet per texture and share it across every sprite that uses it — s
|
|
|
120
125
|
| `image` | — | Path to the texture, relative to the resources root (`'assets/hero.png'`) |
|
|
121
126
|
| `frameWidth` / `frameHeight` | — | Grid mode: cell size in source pixels. Frames are numbered left-to-right, top-to-bottom, starting at 0 |
|
|
122
127
|
| `atlas` | — | TexturePacker JSON (hash or array format) instead of a grid |
|
|
123
|
-
| `smoothing` | `true` | `false` = GL_NEAREST, the pixel-art setting. On a **grid** sheet, `true` also insets the
|
|
128
|
+
| `smoothing` | `true` | `false` = GL_NEAREST, the pixel-art setting. On a **grid** sheet, `true` also insets the frame UVs by half a texel so magnified edges cannot sample the neighbouring frame — **both** edges of any axis that has more than one frame, since 2026-08-24. An axis with a single frame keeps the exact 0..1 range, which is what `tileRepeat` needs to wrap. Atlas frames come from the JSON untouched |
|
|
124
129
|
| `repeat` | `false` | GL_REPEAT wrap, required by sprites using `tileRepeat`. Needs power-of-two texture dimensions |
|
|
125
130
|
|
|
126
131
|
| Member | Notes |
|
|
@@ -128,8 +133,9 @@ Create one sheet per texture and share it across every sprite that uses it — s
|
|
|
128
133
|
| `frameCount` | Number of frames. `0` until the texture has loaded, for grid sheets |
|
|
129
134
|
| `frameNames` | Sorted frame names — atlas sheets only |
|
|
130
135
|
| `frameIndex(name)` | Index for an atlas frame name, `-1` if unknown |
|
|
136
|
+
| `unload()` | Frees the GL texture on the next rendered frame, from the render thread. **Permanent** — sprites still using the sheet stop drawing. For level streaming: unload the finished level's atlases instead of accumulating GPU memory. Since 2026-08-23 |
|
|
131
137
|
|
|
132
|
-
Textures upload to the GPU lazily on first use, from the render thread, and are re-created automatically after an EGL context loss.
|
|
138
|
+
Textures upload to the GPU lazily on first use, from the render thread, and are re-created automatically after an EGL context loss. Releasing the proxy unloads its texture too, so a sheet that goes out of scope does not strand GPU memory.
|
|
133
139
|
|
|
134
140
|
## Sprite
|
|
135
141
|
|
|
@@ -260,9 +266,39 @@ Every sprite gets its own phase, and the wobble unwinds exactly when disabled. T
|
|
|
260
266
|
| `animate(options)` | Native tween, fires `complete` |
|
|
261
267
|
| `clearTweens()` | Cancel tweens in progress — call before starting a replacement tween |
|
|
262
268
|
| `flash(color, duration)` | Fills the sprite's silhouette with `color` (default white) and fades it out over `duration` ms (default 150). The classic damage/invincibility flash that a multiplicative `tintColor` cannot do |
|
|
269
|
+
| `attachTo(target, options)` | Pin this sprite to another one natively — see [Attachment](#attachment) below. `options`: `offsetX`, `offsetY` (both `0`), `rotate` (`false`). `attachTo(null)` detaches |
|
|
270
|
+
| `detach()` | Release the sprite where it is; `x`/`y` are writable again |
|
|
263
271
|
|
|
264
272
|
`animate()` accepts exactly these keys — anything else is silently ignored: `x`, `y`, `scale`, `scaleX`, `scaleY`, `rotation`, `opacity`, `glowOpacity`, `duration` (ms), `delay` (ms), `easing`, `frame` (the sheet frame to set once the tween finishes). Re-calling `animate` from the `complete` handler is still how a ping-pong or a blink is built (there is no `repeat`/`yoyo` yet), but a fixed route is now `followPath` instead of a chain of tween legs.
|
|
265
273
|
|
|
274
|
+
### Attachment
|
|
275
|
+
|
|
276
|
+
Added 2026-08-26. `sprite.attachTo(target, { offsetX, offsetY, rotate })` pins a sprite to another sprite natively: every frame the engine writes the sprite's `x`/`y` from the target's **final** position plus the offset, with no per-frame JS. Name tags, health bars, shadows, turrets, a hitbox that must sit on a moving body.
|
|
277
|
+
|
|
278
|
+
```javascript
|
|
279
|
+
const tag = Game.createText({ text: 'PLAYER 1', zIndex: 6 });
|
|
280
|
+
tag.attachTo(hero, { offsetY: -40 }); // 40 px above the hero's anchor
|
|
281
|
+
gameView.add(tag); // still has to be in the scene
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
| Member | Notes |
|
|
285
|
+
| --- | --- |
|
|
286
|
+
| `attachTo(target, options)` | `options.offsetX` / `offsetY` (default `0`) are in the **attached sprite's** own coordinate space. `options.rotate` (default `false`) copies the target's `rotation` onto the sprite each frame *and* swings the offset around the target — a turret or a hat. Left `false`, the sprite stays upright while the target spins. Every call replaces the previous offset and flag; there is no partial update |
|
|
287
|
+
| `detach()` | Stops the pinning, leaving the sprite where it was. Same as `attachTo(null)` |
|
|
288
|
+
| `attachedTo` | **Read-only** property: the target sprite, or `null`. Setting it does nothing (it is a getter — the write lands on the Kroll proxy and is never read back by the engine) |
|
|
289
|
+
|
|
290
|
+
Where it runs in the frame, and why it matters: after physics integration and solid resolution, before collision checks. So a tag never trails its owner by a frame — including an owner riding a moving platform — and an attached invisible hitbox is tested at the position it was just moved to, not the previous one.
|
|
291
|
+
|
|
292
|
+
The rest of the contract, all read from the engine:
|
|
293
|
+
|
|
294
|
+
- **The attached sprite must be in the scene** (`gameView.add(...)`). The pass iterates the scene's sprite list; a sprite that was never added is never repositioned. So is one whose *target* is not in the scene — it simply freezes where it stands.
|
|
295
|
+
- **While attached, position is not yours.** Direct `x`/`y` writes, `velocityX`/`velocityY`, gravity and position tweens still run, and are then overwritten the same frame. `detach()` first if the sprite has to move on its own.
|
|
296
|
+
- **A drag outranks the attachment.** While a finger holds the sprite (`draggable: true`, past the touch slop) the pinning is skipped, exactly as it is for a rider on a moving platform, and resumes when the finger lifts.
|
|
297
|
+
- **Chains work**: attach a hat to a tag attached to a hero. Parents resolve first through recursion, with a depth cap of 8 that breaks accidental cycles — beyond eight links the far ancestor is not re-resolved that frame and the chain lags instead of hanging. Attaching a sprite to itself is ignored.
|
|
298
|
+
- **Cross-space attach converts automatically.** A `screenFixed` sprite attached to a world sprite (or the reverse) has the target's position mapped through the camera first, so the offset stays in the sprite's own units.
|
|
299
|
+
- **Removing the target removes everything attached to it**, recursively — a tag never outlives its owner, and a chain goes with it. `detach()` first to keep one alive. `removeAllSprites()` clears every attachment as it empties the scene.
|
|
300
|
+
- **A non-sprite target detaches** instead of crashing (Android logs a warning; iOS is silent). Like `follow()`, the binding takes an untyped argument on purpose — a typed proxy parameter aborts the app on the JNI type check when JS sends a plain object.
|
|
301
|
+
|
|
266
302
|
### Sprite events
|
|
267
303
|
|
|
268
304
|
| Event | Payload | Fires |
|
|
@@ -307,6 +343,7 @@ const mono = Game.createFont({ image: 'assets/mono.png', charWidth: 9, charHeigh
|
|
|
307
343
|
| `characters` | ASCII 32..126 | Grid mode: which characters the cells map to, row-major |
|
|
308
344
|
| `smoothing` | `true` | Filters the glyph texture like a sheet. The built-in font is always crisp |
|
|
309
345
|
| `lineHeight` | — | Read-only: the font's natural line height in px |
|
|
346
|
+
| `unload()` | Method, not an option: frees the glyph texture exactly like `SpriteSheet.unload()`, and just as permanently. Since 2026-08-23 |
|
|
310
347
|
|
|
311
348
|
A descriptor that fails to parse falls back to the built-in font and logs the error — the text still renders, in the wrong face, which is the symptom to recognize.
|
|
312
349
|
|
|
@@ -453,6 +490,8 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
453
490
|
- **`swept` belongs on the fast mover, not on the target.** A stationary thin wall with `swept: true` changes nothing; the bullet is the sprite whose path needs testing.
|
|
454
491
|
- **A swept bullet can enter and leave in consecutive frames.** If it crosses a thin target entirely, you get `collision` and then `collisionend` almost immediately — react on the enter.
|
|
455
492
|
- **Text `width`/`height` are read-only in practice.** They report the laid-out block; to make text bigger use `scale`.
|
|
493
|
+
- **`unload()` is permanent, not a cache eviction.** There is no reload: sprites and text still pointing at an unloaded sheet or font simply stop drawing. Unload a level's atlases after its sprites are gone, never to "free memory" on something still on screen.
|
|
494
|
+
- **A tap is a press released within 300 ms**, inside the touch slop (the platform's scaled slop on Android, 8 pt × screen scale on iOS). Hold a button longer and you get `press` and `release` but no `tap` — which is why the demos drive held controls from `press`/`release` and reserve `tap` for discrete hits.
|
|
456
495
|
- **`hitboxShape: 'circle'` also changes the touch area**, not just collisions.
|
|
457
496
|
- **The hitbox scales do not change the touch area.** `hitTest` runs against the full drawn frame, so a sprite with `hitboxScale: 0.6` still takes taps out to its art's edges. Only the hitbox *shape* reaches touch. Shrink `width`/`height` if a tap target must match the collision box.
|
|
458
497
|
- **`hitboxScaleX`/`hitboxScaleY` multiply `hitboxScale`, they do not replace it.** `hitboxScale: 0.85` with `hitboxScaleX: 0.66` gives an effective 0.561 on X and 0.85 on Y. Both default to `1`, so sprites that never set them are unaffected — and `0` still means zero, as everywhere else in the engine.
|
|
@@ -473,4 +512,9 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
473
512
|
- **Game-clock timers are not `setTimeout`.** `after`/`every` freeze at `timeScale: 0` and pause with the render loop — which is why spawn waves belong there — but a pause menu that needs a real countdown still needs `setTimeout`.
|
|
474
513
|
- **`screenFixed` beats `scrollFactor`.** Setting both leaves the sprite screen-fixed; the parallax offset is skipped entirely.
|
|
475
514
|
- **Ghost lines along a frame's edge are a filtering artifact, not bad art.** A magnified `smoothing: true` sheet samples past the frame border — 1px seams, or the next row's heads at the bottom. Grid sheets have inset UVs upstream since 2026-08-19; an atlas needs padding and extrude at pack time, and an older module build needs either `smoothing: false` or a rebuild.
|
|
515
|
+
- **An animation that rocks side to side on a smoothed grid sheet dates the build.** Until 2026-08-24 the inset was applied only to the edge facing a neighbour, so the first and last frame of a strip came out half a texel wider than the rest with their centres a quarter texel to opposite sides — a cycle through them wobbles, most visibly on hard-edged art. Fixed by insetting both edges of any multi-frame axis. Unreachable with `smoothing: false`, which is why it went unnoticed for so long: check the build date before redrawing the art.
|
|
516
|
+
- **An attached sprite matches its target's world `x`/`y`, not where the target is *drawn*.** Give the attachment the same `scrollFactor` as its target, or a tag on a parallax sprite drifts off it as the camera travels — the two are pinned in world space and then drawn at different offsets.
|
|
517
|
+
- **A cross-space attachment is converted with the camera from the previous frame.** Attachments are applied before the camera update, so a `screenFixed` tag on a world sprite (or the reverse) lags by one frame while the camera is moving. Same-space attachments — the normal case — are exact.
|
|
518
|
+
- **Removing a sprite removes everything attached to it.** Pooling the owner with `visible = false` does not, but `gameView.remove(owner)` takes the tag, the health bar and anything chained off them. `detach()` first to keep one.
|
|
519
|
+
- **`attachedTo` is read-only.** Assigning to it stores a value on the proxy that the engine never reads — use `attachTo()` / `detach()`.
|
|
476
520
|
- **`scrollFactor` does not move the sprite.** It shifts where the sprite is *drawn* and maps touch back to match. `x`, `y`, physics and collisions stay in world coordinates, so a `scrollFactor: 0.5` platform still collides where its world `x` says, not where it looks.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
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.
|
|
4
4
|
|
|
5
|
+
<!-- TOC-START -->
|
|
5
6
|
## Contents
|
|
6
7
|
|
|
7
8
|
- [Install the module](#install-the-module)
|
|
@@ -14,6 +15,8 @@ Installing the module, wiring it into Classic and Alloy projects, organizing art
|
|
|
14
15
|
- [Debugging and tuning](#debugging-and-tuning)
|
|
15
16
|
- [Platform differences](#platform-differences)
|
|
16
17
|
|
|
18
|
+
<!-- TOC-END -->
|
|
19
|
+
|
|
17
20
|
## Install the module
|
|
18
21
|
|
|
19
22
|
There is no registry and no `ti module install` command — Titanium modules are zips you drop into a project. Grab the platform zips from the module's `dist/` folders (or build them yourself) and put them where the CLI looks:
|
|
@@ -48,9 +51,11 @@ Pinning `version` is worth the extra attribute: with two module versions install
|
|
|
48
51
|
| `0.3.0` before 2026-08-18 | `createText`, `createFont`, `screenFixed`, `swept`, `collisionend` |
|
|
49
52
|
| `0.3.0` before 2026-08-19 | also `followPath`, `play(name, { then })`, `raycast`, `after`/`every`, `scrollFactor`, `multiply`/`screen` blends |
|
|
50
53
|
| `0.3.0` before 2026-08-19 16:00 UTC+2 | additionally bleeds at grid-frame edges on smoothed sheets |
|
|
51
|
-
| `0.4.0` — including the 2026-08-20 release zip | `hitboxScaleX`/`hitboxScaleY
|
|
54
|
+
| `0.4.0` — including the 2026-08-20 release zip | `hitboxScaleX`/`hitboxScaleY`, text `maxWidth`, `SpriteSheet.unload()`/`Font.unload()` and the LiveView renderer lifecycle, all added on 2026-08-23 with the manifest untouched |
|
|
55
|
+
| `0.4.0` built before 2026-08-24 | additionally rocks side to side through a multi-frame animation on a `smoothing: true` grid sheet — the half-texel inset was one-sided, so the end frames came out slightly wider and off-centre |
|
|
56
|
+
| `0.4.0` built before 2026-08-26 | additionally `sprite.attachTo()` / `detach()` / `attachedTo`, and `gameView.follow()` still aborts the app on Android if it is handed anything that is not a sprite |
|
|
52
57
|
|
|
53
|
-
The published releases are pre-releases and lag `main`: the newest one is `0.4.0` (2026-08-20). If a call that this skill documents is `undefined` at runtime, check the manifest version *and the build date* before hunting for a typo, and rebuild from upstream `main`. More reliable than either is feature-detecting in code: read the property **before** ever writing it (`typeof sprite.hitboxScaleX === 'undefined'` means the build predates it). Write-then-read always says yes, because an unknown property is stored on the Kroll proxy and reads back exactly as it was set.
|
|
58
|
+
The published releases are pre-releases and lag `main`: the newest one is `0.4.0` (2026-08-20). If a call that this skill documents is `undefined` at runtime, check the manifest version *and the build date* before hunting for a typo, and rebuild from upstream `main`. More reliable than either is feature-detecting in code: read the property **before** ever writing it (`typeof sprite.hitboxScaleX === 'undefined'` means the build predates it). Write-then-read always says yes, because an unknown property is stored on the Kroll proxy and reads back exactly as it was set. A method has no such trap — `typeof sprite.attachTo === 'function'` is a clean probe whatever the app did earlier.
|
|
54
59
|
|
|
55
60
|
Building the module from source:
|
|
56
61
|
|
|
@@ -62,7 +67,7 @@ ti build -p ios --build-only # macOS only → ios/dist/ti.game-iphone-<ve
|
|
|
62
67
|
|
|
63
68
|
## Classic projects
|
|
64
69
|
|
|
65
|
-
`require('ti.game')` anywhere and add the GameView to a window. The
|
|
70
|
+
`require('ti.game')` anywhere and add the GameView to a window. The 26 demos in the module's `example/` folder are Classic, each one a CommonJS module exporting a start function (`app.js` is the launcher, not a demo):
|
|
66
71
|
|
|
67
72
|
```javascript
|
|
68
73
|
// Resources/flappy.js
|
|
@@ -228,7 +233,7 @@ Practical rules:
|
|
|
228
233
|
- **One sheet per scene if you can manage it.** Sprites sharing a texture batch into a single draw call. The endless-runner demo even draws its crash burst from the *player's* sheet to keep the whole scene on one texture.
|
|
229
234
|
- **`smoothing: false` for pixel art.** Combine with `pixelSnap: true` on moving sprites when the texel phase must stay stable.
|
|
230
235
|
- **Ship sheets as PNG, not JPG.** JPEG has no alpha channel, so every frame comes back on an opaque block, and its compression smears colour across frame borders — exactly where a sheet can least afford it. The module's own `basic.js` demo was converted from `hero.jpg` to `hero.png` on 2026-08-19 for this reason.
|
|
231
|
-
- **Edge bleeding on a smoothed grid is fixed upstream, on grids only.** Linear filtering samples past a frame's edge when the sprite is magnified, which shows up as 1px ghost lines or the next row's heads peeking in at the bottom. Since 2026-08-19 a grid sheet with `smoothing: true` pulls its
|
|
236
|
+
- **Edge bleeding on a smoothed grid is fixed upstream, on grids only.** Linear filtering samples past a frame's edge when the sprite is magnified, which shows up as 1px ghost lines or the next row's heads peeking in at the bottom. Since 2026-08-19 a grid sheet with `smoothing: true` pulls its frame edges in by half a texel automatically, and since 2026-08-24 it does so on **both** edges of any axis that holds more than one frame — the first version inset only the side facing a neighbour, which left the end frames of a strip half a texel wider than the rest and their centres a quarter texel off, so an animation cycling through them visibly rocked. An axis with a single frame keeps the exact texture border, so full-texture `tileRepeat` frames still wrap seamlessly, and `smoothing: false` sheets skip the inset entirely (NEAREST cannot bleed, and pixel art at 1:1 needs exact UVs). **Atlas sheets are not covered** — their UVs come straight from the JSON, so prevent bleeding when packing instead: 2px padding and extrude in TexturePacker.
|
|
232
237
|
- **`repeat: true` for tiling.** Required by sprites using `tileRepeat`, and it needs power-of-two texture dimensions on ES 2.0 — a 512×64 street strip tiles, a 500×60 one does not.
|
|
233
238
|
- **White art plus `tint`** covers every color variant of a particle or effect with one small texture.
|
|
234
239
|
|
|
@@ -296,6 +301,29 @@ function listen(target, eventName, handler) {
|
|
|
296
301
|
|
|
297
302
|
For restart-without-teardown (a retry button), `removeAllSprites()` plus rebuilding is simplest, but pooling and repositioning is cheaper: reset positions, zero velocities, `clearTweens()`, and re-`play()` the animations.
|
|
298
303
|
|
|
304
|
+
### GPU textures: `unload()`
|
|
305
|
+
|
|
306
|
+
Sheets and fonts hold GL textures, and until 2026-08-23 nothing but losing the context ever freed one. `spriteSheet.unload()` and `font.unload()` release the texture on the next rendered frame, from the render thread. Releasing the proxy does the same, so a sheet that goes out of scope no longer strands its memory.
|
|
307
|
+
|
|
308
|
+
This is for **level streaming** — a game that loads world 2's atlases while world 1's are still resident. It is not a cache: the unload is permanent, and any sprite still pointing at that sheet stops drawing.
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
function unloadLevel(level) {
|
|
312
|
+
level.sprites.forEach(s => gameView.remove(s));
|
|
313
|
+
level.sheets.forEach(sheet => sheet.unload()); // after the sprites are gone
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
A single-level game with a handful of sheets should not call it at all: the textures die with the window.
|
|
318
|
+
|
|
319
|
+
### LiveView reloads
|
|
320
|
+
|
|
321
|
+
A LiveView reload replaces Titanium's JS runtime while the native side survives, which used to leave the previous `GameView`'s render loop running — every reload added another GL thread, and the audio engine handed the new runtime sample ids the old one still owned.
|
|
322
|
+
|
|
323
|
+
Since 2026-08-23 the module retires the previous generation itself: stale renderers are shut down (the `GLSurfaceView` is detached, which stops its `GLThread`), the old `SoundPool`, `MediaPlayer`s and audio proxies are released, and on Android `GameViewProxy.releaseViews()` drops the view reference so the `GLSurfaceView` — and the Activity it holds — can be collected after a window close. iOS does the same by hooking the LiveView restart and shutting down the views registered to the retiring runtime context.
|
|
324
|
+
|
|
325
|
+
**Nothing in your code needs to change**, and this is not a substitute for the cleanup above: your own timers, sounds and listeners still belong to you on a normal window close. It does mean that a reload no longer accumulates game loops, so a stuttering scene after ten reloads is a symptom that dates the build.
|
|
326
|
+
|
|
299
327
|
## Debugging and tuning
|
|
300
328
|
|
|
301
329
|
- **`debug: true` on the GameView** draws collision shapes for the whole scene; on a single sprite, only that one. Green = collision AABB with `hitboxScale` and the per-axis `hitboxScaleX`/`hitboxScaleY` applied, blue = sprite/touch bounds (the full drawn frame, which is also the touch area), orange dot = anchor. This is the fastest way to explain a hit that should not have registered.
|
|
@@ -1,28 +1,65 @@
|
|
|
1
1
|
# ti.game recipes by genre
|
|
2
2
|
|
|
3
|
-
Working patterns distilled from the
|
|
3
|
+
Working patterns distilled from the 26 official demos in `example/` and from a shipped Alloy game (Titanium Lander). Each recipe shows the part that is specific to the genre; the shared scaffolding is in [The scaffolding](#the-scaffolding) and is not repeated.
|
|
4
4
|
|
|
5
|
-
Every snippet uses only API that exists in upstream `main`
|
|
5
|
+
Every snippet uses only API that exists in upstream `main` at `201bcf9`, 2026-08-26 (manifest `0.4.0`, unchanged since 2026-08-20 — the version number stopped tracking the code). Check [roadmap.md](roadmap.md) before reaching for anything not shown here.
|
|
6
6
|
|
|
7
|
+
<!-- TOC-START -->
|
|
7
8
|
## Contents
|
|
8
9
|
|
|
10
|
+
- [The 26 demos, and what each one is the reference for](#the-26-demos-and-what-each-one-is-the-reference-for)
|
|
9
11
|
- [The scaffolding](#the-scaffolding)
|
|
10
|
-
- [Cross-cutting patterns](#cross-cutting-patterns)
|
|
11
|
-
- [Tap-to-flap](#tap-to-flap)
|
|
12
|
-
- [Platformer](#platformer)
|
|
13
|
-
- [Top-down / Zelda](#top-down--zelda)
|
|
14
|
-
- [Top-down racer](#top-down-racer)
|
|
15
|
-
- [Asteroids / space shooter](#asteroids--space-shooter)
|
|
12
|
+
- [Cross-cutting patterns](#cross-cutting-patterns)
|
|
13
|
+
- [Tap-to-flap](#tap-to-flap)
|
|
14
|
+
- [Platformer](#platformer)
|
|
15
|
+
- [Top-down / Zelda](#top-down--zelda)
|
|
16
|
+
- [Top-down racer](#top-down-racer)
|
|
17
|
+
- [Asteroids / space shooter](#asteroids--space-shooter)
|
|
16
18
|
- [Lunar lander](#lunar-lander)
|
|
17
|
-
- [Endless runner](#endless-runner)
|
|
18
|
-
- [Rhythm game](#rhythm-game)
|
|
19
|
-
- [Ball sports / breakout physics](#ball-sports--breakout-physics)
|
|
20
|
-
- [Cards and board games](#cards-and-board-games)
|
|
21
|
-
- [Point & click adventure](#point--click-adventure)
|
|
22
|
-
- [Particles](#particles)
|
|
23
|
-
- [Ropes and chains](#ropes-and-chains)
|
|
24
|
-
- [Camera work](#camera-work)
|
|
25
|
-
- [Visual effects](#visual-effects)
|
|
19
|
+
- [Endless runner](#endless-runner)
|
|
20
|
+
- [Rhythm game](#rhythm-game)
|
|
21
|
+
- [Ball sports / breakout physics](#ball-sports--breakout-physics)
|
|
22
|
+
- [Cards and board games](#cards-and-board-games)
|
|
23
|
+
- [Point & click adventure](#point--click-adventure)
|
|
24
|
+
- [Particles](#particles)
|
|
25
|
+
- [Ropes and chains](#ropes-and-chains)
|
|
26
|
+
- [Camera work](#camera-work)
|
|
27
|
+
- [Visual effects](#visual-effects)
|
|
28
|
+
|
|
29
|
+
<!-- TOC-END -->
|
|
30
|
+
|
|
31
|
+
## The 26 demos, and what each one is the reference for
|
|
32
|
+
|
|
33
|
+
`example/` is the module's real documentation: every demo is a runnable app, and upstream keeps them current with the engine. When a recipe here is not enough, open the demo — it compiles today.
|
|
34
|
+
|
|
35
|
+
| Demo | The reference implementation of | Covered here |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `basic.js` | Sheets, animations, drag/pinch/rotate, tween chaining | [The scaffolding](#the-scaffolding) |
|
|
38
|
+
| `puzzle.js` | Drag & drop with snapping, press-to-lift, one piece per finger | [Cards and board games](#cards-and-board-games) |
|
|
39
|
+
| `flappy.js` | Gravity + tap impulse, score zones, wrapping parallax | [Tap-to-flap](#tap-to-flap) |
|
|
40
|
+
| `platformer.js` | `solidWith`, `land`, `oneWay` stairs, a moving platform that carries you, d-pad | [Platformer](#platformer) |
|
|
41
|
+
| `volley.js` | `restitution`, JS hit response, an AI timer | [Ball sports / breakout physics](#ball-sports--breakout-physics) |
|
|
42
|
+
| `racing.js` | `carMode` drift, skid marks, lap and checkpoint logic | [Top-down racer](#top-down-racer) |
|
|
43
|
+
| `cards.js` | Deck dealing, fanned hand, selection tweens, idle wobble | [Cards and board games](#cards-and-board-games) |
|
|
44
|
+
| `asteroids.js` | `thrust`/`angularVelocity`, `wrapAround`, bullet pooling, additive bolts | [Asteroids / space shooter](#asteroids--space-shooter) |
|
|
45
|
+
| `topdown.js` | Tile map from a string array, `ySort` depth, follower NPC | [Top-down / Zelda](#top-down--zelda) |
|
|
46
|
+
| `skate.js` | Endless runner: pooled obstacles, jump button, pixel-art parallax | [Endless runner](#endless-runner) |
|
|
47
|
+
| `pointclick.js` | Tap-to-walk with `findPath` + `followPath`, verb coin, hotspots | [Point & click adventure](#point--click-adventure) |
|
|
48
|
+
| `particles.js` | Fountain, tap bursts, a smoke trail following a dragged sprite | [Particles](#particles) |
|
|
49
|
+
| `rhythm.js` | Pooled notes on native velocity, `press` pads, timing windows | [Rhythm game](#rhythm-game) |
|
|
50
|
+
| `camera.js` | Dead-zone follow, `cameraBounds`, zoom, shake, `cameraEffect`, `scrollFactor` | [Camera work](#camera-work) |
|
|
51
|
+
| `rope.js` | Verlet ropes from a dragged head and from a fixed anchor | [Ropes and chains](#ropes-and-chains) |
|
|
52
|
+
| `flip.js` | `flipX`/`flipY` driven by movement; tap inverts gravity | [Visual effects](#visual-effects) |
|
|
53
|
+
| `hitbox.js` | `debug: true` overlays, and why the per-axis hitbox scales exist | [Seeing the hitbox](#seeing-the-hitbox--hitboxjs) |
|
|
54
|
+
| `blend.js` | The four blend modes side by side, `flash()` variants | [Visual effects](#visual-effects) |
|
|
55
|
+
| `text.js` | Screen-fixed HUD, world-space labels, text buttons, wrapped dialog, a name tag pinned with `attachTo` | [HUD](#hud--textjs-flappyjs), [Labels pinned to a sprite](#labels-bars-and-turrets-pinned-to-a-sprite--attachto-textjs) |
|
|
56
|
+
| `swept.js` | `swept: true` vs tunneling, same bullets, rising speed | [Object pooling](#object-pooling) |
|
|
57
|
+
| `path.js` | Smoothed circuits, sharp patrols, `play(name, { then })` chains | [Patrol routes and animation chains](#patrol-routes-and-animation-chains--pathjs) |
|
|
58
|
+
| `raycast.js` | Line of sight, ledge probes, tap-fired hitscan | [Line of sight, ledge probes and hitscan](#line-of-sight-ledge-probes-and-hitscan--raycastjs) |
|
|
59
|
+
| `zones.js` | The `collision`/`collisionend` lifecycle, including despawned partners | [Zones you can be inside of](#zones-you-can-be-inside-of-enter--exit--zonesjs) |
|
|
60
|
+
| `demoscene.js` | Sine scrollers and copper bars built from `followPath` circles | [Visual effects](#visual-effects) |
|
|
61
|
+
| `maze.js` | A* over a tile maze, `simplify: false` route visualization, a re-pathing hound | [Pathfinding around obstacles](#pathfinding-around-obstacles--mazejs-pointclickjs) |
|
|
62
|
+
| `timescale.js` | `timeScale` slow motion and freeze; game clock vs `setInterval` | [Timers on the game clock](#timers-on-the-game-clock) |
|
|
26
63
|
|
|
27
64
|
## The scaffolding
|
|
28
65
|
|
|
@@ -77,6 +114,8 @@ Sizing everything as a fraction of `W`/`H` is not cosmetic — a jump tuned in a
|
|
|
77
114
|
|
|
78
115
|
## Cross-cutting patterns
|
|
79
116
|
|
|
117
|
+
The pieces every genre below reuses: pooling, freeing a level's textures, parallax, multitouch controls, HUD text, wrapped dialog, labels and bars attached to a sprite, invisible triggers, seeing the hitbox, enter/exit zones, patrol routes, line of sight, pathfinding, game-clock timers and hit-stop.
|
|
118
|
+
|
|
80
119
|
### Object pooling
|
|
81
120
|
|
|
82
121
|
Create a fixed set once, recycle forever. `visible = false` also removes the sprite from collision, which is what makes this work.
|
|
@@ -139,6 +178,19 @@ The `generation` counter matters: without it, a timeout fired for an old shot de
|
|
|
139
178
|
|
|
140
179
|
Recycling a sprite with `visible = false` fires `collisionend` on anything it was touching. Handlers that treat separation as "the player walked away" need to tolerate a despawn.
|
|
141
180
|
|
|
181
|
+
### Freeing a level's textures
|
|
182
|
+
|
|
183
|
+
Pooling recycles sprites; it does not free the **texture** behind them. A game that streams worlds accumulates every atlas it has ever loaded until the GL context dies. `unload()` releases one on the next rendered frame.
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
function unloadLevel(level) {
|
|
187
|
+
level.sprites.forEach((s) => gameView.remove(s));
|
|
188
|
+
level.sheets.forEach((sheet) => sheet.unload()); // only once nothing draws from them
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
It is permanent — not an eviction that reloads on demand. A sprite still pointing at an unloaded sheet stops drawing, silently. A single-level game should not call it: the textures die with the window.
|
|
193
|
+
|
|
142
194
|
### Parallax
|
|
143
195
|
|
|
144
196
|
Two ways, and the choice is decided by whether the **camera** moves or the **world** does.
|
|
@@ -290,6 +342,46 @@ dialog.addEventListener('tap', () => {
|
|
|
290
342
|
|
|
291
343
|
Overlaid `Ti.UI.Label` views remain the answer for text the engine's font cannot draw (accents, system fonts, right-to-left) and for real UI: menus, dialogs, text inputs.
|
|
292
344
|
|
|
345
|
+
### Labels, bars and turrets pinned to a sprite — `attachTo`, `text.js`
|
|
346
|
+
|
|
347
|
+
A HUD is `screenFixed`; a name tag, a health bar or a shadow is the opposite — it belongs *on* a sprite that moves. `attachTo` is the native version of the JS everyone wrote before it: a timer copying `owner.x` into `tag.x`, one bridge round trip per tag per tick, and always a frame behind.
|
|
348
|
+
|
|
349
|
+
```javascript
|
|
350
|
+
const tag = Game.createText({ text: 'THE BALL', scale: UNIT, tintColor: '#7fd4ff', zIndex: 6 });
|
|
351
|
+
tag.attachTo(ball, { offsetY: -(W * 0.09) }); // offset in the tag's own units
|
|
352
|
+
gameView.add(tag); // the tag is still a scene sprite
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
The tag now tracks the ball through gravity, tweens, a moving platform and a finger drag, with no JS in the loop, and disappears with it — `gameView.remove(ball)` removes everything attached to the ball, recursively.
|
|
356
|
+
|
|
357
|
+
A health bar is the same idea with two sprites, and shows what `attachTo` does *not* do:
|
|
358
|
+
|
|
359
|
+
```javascript
|
|
360
|
+
// blockSheet is any small opaque texture stretched and tinted — a sheet-less
|
|
361
|
+
// sprite draws nothing at all, that is the invisible-trigger idiom below
|
|
362
|
+
const barBack = Game.createSprite({ sheet: blockSheet, width: 40, height: 5, zIndex: 5, tintColor: '#222' });
|
|
363
|
+
const barFill = Game.createSprite({ sheet: blockSheet, width: 40, height: 5, zIndex: 6, anchorX: 0, tintColor: '#3f6' });
|
|
364
|
+
|
|
365
|
+
barBack.attachTo(hero, { offsetY: -34 });
|
|
366
|
+
barFill.attachTo(hero, { offsetX: -20, offsetY: -34 }); // anchorX 0 → grows to the right
|
|
367
|
+
gameView.add([barBack, barFill]);
|
|
368
|
+
|
|
369
|
+
// on damage: only the width changes — scale, opacity and visibility are not inherited
|
|
370
|
+
barFill.width = 40 * (hp / hpMax);
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
`rotate: true` is the other half: the offset swings around the target and the sprite copies its rotation, which is what a turret on a tank or a hat on a tumbling hero needs.
|
|
374
|
+
|
|
375
|
+
```javascript
|
|
376
|
+
turret.attachTo(tank, { offsetY: -12, rotate: true });
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Three things to keep in mind:
|
|
380
|
+
|
|
381
|
+
- **The attached sprite has to be added to the scene**, and so does its target. Neither pins otherwise — the sprite just sits at whatever `x`/`y` it was created with.
|
|
382
|
+
- **Its position stops being yours.** Velocity, gravity and `animate({ x })` still run and are overwritten the same frame. To fling a tag off a dying enemy, `detach()` first, then give it velocity.
|
|
383
|
+
- **Match `scrollFactor` between the two.** Attachment pins world coordinates; parallax changes only where a sprite is *drawn*. A tag at `scrollFactor: 1` on a background sprite at `0.5` stays pinned in world space and drifts across the screen anyway.
|
|
384
|
+
|
|
293
385
|
### Invisible triggers
|
|
294
386
|
|
|
295
387
|
A sprite with `width`/`height` and no `sheet` renders nothing but still collides. This is how scores, goals, ceilings, kill floors, walls and checkpoints are built.
|
|
@@ -320,6 +412,19 @@ car.addEventListener('collision', (e) => {
|
|
|
320
412
|
});
|
|
321
413
|
```
|
|
322
414
|
|
|
415
|
+
### Seeing the hitbox — `hitbox.js`
|
|
416
|
+
|
|
417
|
+
`debug: true` on the GameView is the whole tuning workflow, and the demo exists to make the overlays legible: two identical adventurers walk into the same wall, one untuned and one with `hitboxScaleX: 0.62` / `hitboxScaleY: 0.92`. The untuned one stops a body's width short and hovers above the floor on his frame's padding; the tuned one goes flush and lands his feet. Tapping toggles the tuning live, so the green box snaps between frame and drawing.
|
|
418
|
+
|
|
419
|
+
```javascript
|
|
420
|
+
const gameView = Game.createGameView({ debug: true }); // green = collision AABB, blue = drawn frame, orange = anchor
|
|
421
|
+
|
|
422
|
+
hero.hitboxScaleX = 0.62; // both are live: write them and watch the box move
|
|
423
|
+
hero.hitboxScaleY = 0.92;
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Read the two boxes as a pair: green is what collides, blue is what draws **and what takes taps**. Green much smaller than blue on one axis only is exactly the case the per-axis scales exist for; green and blue identical on a sprite whose art has padding is the bug the demo dramatizes.
|
|
427
|
+
|
|
323
428
|
### Zones you can be inside of (enter / exit) — `zones.js`
|
|
324
429
|
|
|
325
430
|
`collision` and `collisionend` are the two halves of a trigger: entering water, standing on a pressure plate, holding a capture point. Nothing fires while you stay inside — the state lives in JS between the two events, and that is deliberate, since a per-frame "stay" would be bridge traffic every frame.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# What does not exist yet — and what to do instead
|
|
2
2
|
|
|
3
|
-
The most expensive mistake with `ti.game` is writing code against a feature that sounds like it should be there because Phaser, Godot or Cocos has it. This file is the guard rail: everything below is **absent from upstream `main` as of 2026-08-
|
|
3
|
+
The most expensive mistake with `ti.game` is writing code against a feature that sounds like it should be there because Phaser, Godot or Cocos has it. This file is the guard rail: everything below is **absent from upstream `main` as of 2026-08-26** (`201bcf9`, manifest `0.4.0`), with the workaround that is idiomatic today.
|
|
4
4
|
|
|
5
5
|
Planned items come from the module's `TODO.md`. Planned is not shipped — when a game needs one of these, use the workaround and keep the code isolated enough to swap later.
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ Planned items come from the module's `TODO.md`. Planned is not shipped — when
|
|
|
9
9
|
| You might reach for | Status | What to do now |
|
|
10
10
|
| --- | --- | --- |
|
|
11
11
|
| A tilemap layer, Tiled JSON import, collision layer | Planned (priority 4) | One sprite per tile. Fine at 12×15, dead at 200×200. For large ground planes use a single `tileRepeat` sprite instead of a grid, and add sparse invisible solids for collision. `findPath` already rasterizes those per-tile sprites, so a tile maze pathfinds today — the planned collision layer is meant to feed the same grid |
|
|
12
|
-
| `sprite.parent
|
|
12
|
+
| `sprite.parent` and full transform inheritance — a child that scales, fades and flips with its parent | Partly shipped, the rest still planned (priority 7) | **Position and rotation are done**: `attachTo(target, { offsetX, offsetY, rotate })` pins a sprite to another natively, chains included, so turrets, hats, name tags and health bars need no JS. What is *not* inherited is everything else — `scale`, `opacity`, `visible`, `flipX`/`flipY`, `tintColor` — and there is no `parent` property, no hit-test or `ySort` integration. Set those on each part yourself, on the coarse tick that changes them |
|
|
13
13
|
| `gameView.panTo(x, y, { duration, easing })` for a cutscene camera move | Planned (priority 1) | `follow()` an invisible sprite and tween that sprite, then `follow()` the player again |
|
|
14
14
|
| Ping-pong animation playback, a per-sprite animation speed multiplier, a random start offset | Planned (priority 9) | Author the ping-pong into the sheet as a longer animation. Desync a field of torches by giving each one its own `idleAnimation` timing, or start them from staggered timers |
|
|
15
15
|
| Native virtual joystick or d-pad bound to a sprite | Planned (priority 6) | Overlaid Titanium views, one per button, writing `velocityX`/`steering`/`throttle` on `touchstart`/`touchend`. Sibling views get simultaneous pointers, so multitouch works |
|
|
@@ -58,7 +58,10 @@ Recent additions that are easy to miss and easy to reimplement badly in JS:
|
|
|
58
58
|
- **`gameView.findPath(from, to, options)`** (2026-08-20): grid A* around the sprites carrying a `collisionGroup`, returning waypoints ready for `followPath` — the Godot `AStar2D` / GameMaker `mp_grid` equivalent. Tap-to-walk and chasing AI route around obstacles natively now; hand-authored waypoint arrays and straight-line walking are the old workaround. Discrete like `raycast`: taps and AI timers, not per frame.
|
|
59
59
|
- **Text word wrap** (2026-08-23): `maxWidth` on a text sprite breaks lines on word boundaries and re-wraps whenever `text` changes, so a dialog box is one long string instead of hand-broken `\n` lines. The width is in font-space px, before `scale`. Do not write a JS line-breaker any more.
|
|
60
60
|
- **`hitboxScaleX` / `hitboxScaleY`** (2026-08-23): per-axis corrections multiplied on top of `hitboxScale`, for art that fills its frame by a different fraction on each axis. The old workaround — settling for one compromise scale, or padding the frame so a single number fits — is obsolete.
|
|
61
|
+
- **`spriteSheet.unload()` / `font.unload()`** (2026-08-23): frees a GL texture on the next frame instead of holding it until the context dies. Level streaming no longer needs a fresh window to reclaim GPU memory. Permanent — it is not a cache eviction.
|
|
62
|
+
- **LiveView reloads retire the previous generation** (2026-08-23): stale render loops, the old `SoundPool`, `MediaPlayer`s and audio proxies are shut down by the module itself. Do not write your own reload guard, and do not read a scene stuttering after several reloads as an engine limit — it dates the build.
|
|
63
|
+
- **`sprite.attachTo(target, { offsetX, offsetY, rotate })` / `detach()`** (2026-08-26): a sprite pinned to another sprite, resolved natively after physics and before collisions. Name tags, health bars, shadows, turrets and hitboxes that must sit on a moving body stop needing a per-frame JS copy of `x`/`y` — which is what everyone wrote before, one bridge round trip per tag per tick, always a frame late. Chains, `rotate: true` for an offset that swings with the target, automatic conversion between world and `screenFixed` space, and recursive removal so a tag never outlives its owner. It is not full parenting: only position and rotation are inherited.
|
|
61
64
|
|
|
62
65
|
## Checking the current state
|
|
63
66
|
|
|
64
|
-
This file describes upstream `main` on 2026-08-
|
|
67
|
+
This file describes upstream `main` at `201bcf9` on 2026-08-26 — a moving target: fourteen items on this list shipped within ten days of the skill being written, four of them in a single day, and the manifest tracks none of it precisely (`0.3.0` → `0.4.0` on 2026-08-20, after nine of them; word wrap, the per-axis hitbox, `unload()` and the LiveView lifecycle landed on 2026-08-23 and `attachTo` on 2026-08-26, all with the number unchanged). `attachTo` is also the first entry to move *out* of the "not available" table rather than simply appearing in the shipped list — a planned item can land partly done, so read what it does not cover before treating the whole row as solved. Before relying on any "planned" item being still absent — or on a workaround still being necessary — read `TODO.md` and `README.md` at https://github.com/m1ga/ti.game, the module's upstream repo. `README.md` there is the canonical API documentation; the `documentation/` folder only points at it. The priorities quoted above are the maintainer's, not a roadmap this skill decides.
|