@maccesar/titools 4.13.0 → 4.14.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
CHANGED
|
@@ -439,7 +439,8 @@ When it activates:
|
|
|
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
|
+
- Sprites pinned to other sprites (`attachTo`) — name tags, health bars, shadows, turrets, tracked natively every frame, inheriting the owner's opacity
|
|
443
|
+
- Named anchors and percentage strings (`anchor: 'bottom'`, `hitboxScaleY: '55%'`) wherever the engine takes a ratio
|
|
443
444
|
- Particle emitters, Verlet ropes, camera follow/zoom/shake/effects, `scrollFactor` parallax, low-latency sound
|
|
444
445
|
- Game-clock timers that obey pause and slow motion, texture `unload()` for level streaming, and what LiveView reloads clean up on their own
|
|
445
446
|
- Drag & drop, pinch, rotate, multitouch controls
|
package/package.json
CHANGED
package/skills/ti-game/SKILL.md
CHANGED
|
@@ -7,7 +7,7 @@ allowed-tools: Read, Grep, Glob, Edit, Write, Bash(node *)
|
|
|
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-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
|
|
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`), inherited opacity on attachments and named/percentage values (2026-08-26, `83b7863`), 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
|
|
|
@@ -144,6 +144,8 @@ And when the question is "how do I get over there?", `gameView.findPath(from, to
|
|
|
144
144
|
|
|
145
145
|
Tune fairness with `hitboxScale` (art rarely fills its frame; slightly small hitboxes feel better), `hitboxScaleX`/`hitboxScaleY` when the drawing fills its frame by a different fraction on each axis (a 20×44 hero in a 32×48 frame wants `0.62` wide and `0.92` tall — the two multiply `hitboxScale` rather than replace it), and `hitboxShape: 'circle'` for balls and rocks — circles also resolve against solids along the contact normal, so they bounce off corners diagonally instead of like a box. Turn on `debug: true` per sprite, or on the GameView for everything, to see green collision AABBs, blue touch bounds and the orange anchor dot.
|
|
146
146
|
|
|
147
|
+
The hitbox shrinks **around the anchor**, which is why the anchor is half of that tuning and why both now have readable forms: `anchor: 'bottom'` with `hitboxScaleY: '55%'` is the same sprite as `anchorY: 1` with `hitboxScaleY: 0.55`, and says what it is doing. Every ratio the engine exposes takes a percentage string, and the anchors take names (`'left'`, `'bottom-right'`, `'center'`); numbers keep working untouched. [api.md](references/api.md) lists exactly which properties are ratios — coordinates, sizes, degrees and speeds are not, and neither is the car model's `grip`/`drag`.
|
|
148
|
+
|
|
147
149
|
## Performance rules that actually matter
|
|
148
150
|
|
|
149
151
|
- **One texture = one draw call.** The batcher accumulates up to 1000 quads and flushes on texture switch. A scene whose sprites all share one sheet renders in a single draw call regardless of sprite count. Pack art into as few sheets as possible; reuse one white particle texture and tint it at runtime.
|
|
@@ -169,7 +171,7 @@ One long string wraps itself: `maxWidth` breaks lines on word boundaries and re-
|
|
|
169
171
|
|
|
170
172
|
`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).
|
|
171
173
|
|
|
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
|
|
174
|
+
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, and fading the owner fades them with it: position, rotation and opacity are inherited. Scale, visibility and tint are not — set those per sprite.
|
|
173
175
|
|
|
174
176
|
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.
|
|
175
177
|
|
|
@@ -222,7 +224,7 @@ When this skill runs out — a brand-new feature, a demo's exact code, a build q
|
|
|
222
224
|
2. Does any timer move a sprite? Replace it with velocity, gravity, a tween, `carMode` or `thrust`.
|
|
223
225
|
3. Are all the level's objects added in one `gameView.add([...])` call?
|
|
224
226
|
4. Does every moving body have the right model — `solidWith` to be blocked, `collidesWith` to be notified?
|
|
225
|
-
5. Is `hitboxScale` set on sprites whose art does not fill the frame — with `hitboxScaleX`/`hitboxScaleY` when the fit differs per axis
|
|
227
|
+
5. Is `hitboxScale` set on sprites whose art does not fill the frame — with `hitboxScaleX`/`hitboxScaleY` when the fit differs per axis, and the `anchor` the box shrinks around named rather than left as a bare `1`?
|
|
226
228
|
6. Do triggers (scores, goals, kill zones) use sheet-less sprites instead of visible art?
|
|
227
229
|
7. Is every `createSound`, `setInterval` and `setTimeout` stopped when the window closes?
|
|
228
230
|
8. Is the HUD `Game.createText` with `screenFixed: true`, rather than a stack of overlaid labels?
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ti.game API reference
|
|
2
2
|
|
|
3
|
-
Complete JS surface of `ti.game` as of upstream `main`
|
|
3
|
+
Complete JS surface of `ti.game` as of upstream `main` at `83b7863`, 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), `attachTo`/`detach`, inherited opacity on attachments and the named/percentage values (all 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
|
|
|
@@ -8,6 +8,7 @@ Every property listed is **live**: reading returns the current native value, eve
|
|
|
8
8
|
## Contents
|
|
9
9
|
|
|
10
10
|
- [Module](#module)
|
|
11
|
+
- [Names and percentages](#names-and-percentages)
|
|
11
12
|
- [GameView](#gameview)
|
|
12
13
|
- [SpriteSheet](#spritesheet)
|
|
13
14
|
- [Sprite](#sprite)
|
|
@@ -40,6 +41,51 @@ const Game = require('ti.game');
|
|
|
40
41
|
|
|
41
42
|
Easing constants (for `sprite.animate({ easing })`): `Game.EASE_LINEAR`, `Game.EASE_IN`, `Game.EASE_OUT`, `Game.EASE_IN_OUT`, `Game.EASE_BOUNCE`, `Game.EASE_ELASTIC`.
|
|
42
43
|
|
|
44
|
+
## Names and percentages
|
|
45
|
+
|
|
46
|
+
Added 2026-08-26, and purely additive: every number that worked before still works. Anything the engine treats as a **ratio** also accepts a percentage string, and the **anchors** also accept names.
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
// The same sprite, twice.
|
|
50
|
+
Game.createSprite({ anchorX: 0, anchorY: 1, hitboxScaleY: 0.55, opacity: 0.8 });
|
|
51
|
+
Game.createSprite({ anchor: 'bottom-left', hitboxScaleY: '55%', opacity: '80%' });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Percentages** work on exactly these, and nowhere else:
|
|
55
|
+
|
|
56
|
+
| Object | Properties |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| Sprite | `scale`, `scaleX`, `scaleY`, `opacity`, `glowOpacity`, `scrollFactor`, `hitboxScale`, `hitboxScaleX`, `hitboxScaleY`, `restitution`, `throttle`, `steering` |
|
|
59
|
+
| GameView | `cameraScale`, `timeScale`, `cameraEffectIntensity`, and inside `follow()`: `topMargin`, `bottomMargin`, `leftMargin`, `rightMargin`, `smoothing` |
|
|
60
|
+
| Emitter | `startScale`, `endScale`, `startOpacity`, `endOpacity` |
|
|
61
|
+
| Rope | `damping` |
|
|
62
|
+
| Sound | `volume` |
|
|
63
|
+
|
|
64
|
+
Over 100% is meaningful where the ratio is a multiplier — `scaleX: '200%'` is twice as wide. Text sprites are sprites, so they take all of it (`Game.createText({ anchor: 'top-left', opacity: '60%' })`).
|
|
65
|
+
|
|
66
|
+
They do **not** apply to coordinates, sizes, degrees, speeds, or the car model's `grip` and `drag` — those last two are 1/s friction coefficients, not fractions.
|
|
67
|
+
|
|
68
|
+
**Anchor names** (`anchorX`, `anchorY`, and `anchor` for both at once):
|
|
69
|
+
|
|
70
|
+
| Property | Accepts |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `anchorX` | `'left'` (0), `'center'` / `'centre'` / `'middle'` (0.5), `'right'` (1) |
|
|
73
|
+
| `anchorY` | `'top'` (0), `'center'` / `'centre'` / `'middle'` (0.5), `'bottom'` (1) |
|
|
74
|
+
| `anchor` | Any of the nine presets, either order, separated by `-`, `_` or a space: `'bottom-left'`, `'left bottom'`, `'top'`, `'center'`. An axis the name does not mention lands on 0.5, so `'left'` is `(0, 0.5)` |
|
|
75
|
+
|
|
76
|
+
`anchor` is the only one that reads back as a name: it returns the preset the sprite currently sits on, or `'custom'` when the anchors are somewhere else — so `b.anchor = a.anchor` copies a preset. `anchorX` and `anchorY` read back as numbers, always.
|
|
77
|
+
|
|
78
|
+
Why it exists, from the case that prompted it: a prop whose hitbox should cover its lower half while still reaching the ground is `anchorY = 1` plus `hitboxScaleY = 0.55`, and neither number says so — `hitboxScale` shrinks **around the anchor**, so moving the anchor is what lets the box start lower without lifting off the floor. `anchor: 'bottom'` with `hitboxScaleY: '55%'` says it out loud.
|
|
79
|
+
|
|
80
|
+
**A value the engine cannot parse logs a warning and does not throw** — but what it falls back to differs by platform, which upstream's README does not mention:
|
|
81
|
+
|
|
82
|
+
| | A bad value (`'half'`, `'50 %'`, `'abc'`) leaves the property at |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| Android | **the value it already held** |
|
|
85
|
+
| iOS | **the property's default** (`opacity` → 1, `restitution` → 0, `hitboxScale` → 1…) |
|
|
86
|
+
|
|
87
|
+
At creation time the two agree, because the current value *is* the default. They part company when the write lands on a property already set to something else: `sprite.opacity = 0.3` then `sprite.opacity = 'half'` leaves 0.3 on Android and 1 on iOS. Do not lean on either — a typo here is a bug on one platform and a silent no-op on the other.
|
|
88
|
+
|
|
43
89
|
## GameView
|
|
44
90
|
|
|
45
91
|
A normal Titanium view — add it to a window or any container, size it with the usual Titanium properties.
|
|
@@ -148,8 +194,9 @@ Textures upload to the GPU lazily on first use, from the render thread, and are
|
|
|
148
194
|
| `scale` | `1` | Write-only convenience that sets both axes |
|
|
149
195
|
| `scaleX`, `scaleY` | `1` | Negative values flip *and* affect physics/hit testing — prefer `flipX`/`flipY` for pure mirroring |
|
|
150
196
|
| `rotation` | `0` | Degrees, clockwise |
|
|
151
|
-
| `anchorX`, `anchorY` | `0.5` | `0/0` = top-left, `0.5/0.5` = center, `0.5/1` = bottom-center (useful for feet) |
|
|
152
|
-
| `
|
|
197
|
+
| `anchorX`, `anchorY` | `0.5` | `0/0` = top-left, `0.5/0.5` = center, `0.5/1` = bottom-center (useful for feet). Also take names — `'left'`/`'center'`/`'right'` and `'top'`/`'middle'`/`'bottom'` — but always read back as numbers. See [Names and percentages](#names-and-percentages) |
|
|
198
|
+
| `anchor` | — | Both anchors from one preset: `'bottom-left'`, `'top'`, `'center'`. Reads back as the preset the sprite is on, or `'custom'`. Applied after `anchorX`/`anchorY`, so passing both to `createSprite` lets `anchor` win |
|
|
199
|
+
| `opacity` | `1` | Also takes `'80%'`. `0` disables touch as well. While attached to another sprite, the target's opacity multiplies into this one — see [Attachment](#attachment) |
|
|
153
200
|
| `visible` | `true` | `false` = no render **and no collision** — the pooling idiom |
|
|
154
201
|
| `zIndex` | `0` | Draw order |
|
|
155
202
|
| `ySort` | `false` | Within the same `zIndex`, sort by the sprite's **bottom edge** — top-down depth (walk behind a tree, in front of it below) |
|
|
@@ -211,7 +258,7 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
|
|
|
211
258
|
| --- | --- | --- |
|
|
212
259
|
| `collisionGroup` | — | The tag *this* sprite carries |
|
|
213
260
|
| `collidesWith` | — | Array of groups this sprite reports overlaps with |
|
|
214
|
-
| `hitboxScale` | `1` | Shrinks the collision box around the anchor |
|
|
261
|
+
| `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%'` |
|
|
215
262
|
| `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 |
|
|
216
263
|
| `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 |
|
|
217
264
|
| `swept` | `false` | Test this sprite's movement as a **path** (swept AABB), not just at the end position, so a fast mover cannot tunnel between frames. Applies to both `collidesWith` events and `solidWith` blocking (the sprite is clamped to the impact point, then resolved by the normal static pass). Circle hitboxes sweep as their bounding box. Set it on the *mover* — the bullet, not the wall |
|
|
@@ -289,11 +336,13 @@ gameView.add(tag); // still has to be in the scene
|
|
|
289
336
|
|
|
290
337
|
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
338
|
|
|
339
|
+
**Opacity is inherited** (2026-08-26), and it is the only thing besides position and rotation that is. Each frame the target's *effective* opacity is multiplied into the attached sprite wherever it is drawn or hit-tested, so fading an owner — an `animate({ opacity })` tween included — fades its tags and bars with it, down a whole chain, without ever writing to their own `opacity`. Detaching, being removed, or `removeAllSprites()` resets the inherited factor to 1.
|
|
340
|
+
|
|
292
341
|
The rest of the contract, all read from the engine:
|
|
293
342
|
|
|
294
343
|
- **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
344
|
- **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.
|
|
345
|
+
- **A drag outranks the attachment — for position only.** 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. The inherited opacity keeps applying throughout.
|
|
297
346
|
- **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
347
|
- **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
348
|
- **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.
|
|
@@ -503,7 +552,7 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
503
552
|
- **`followPath` overrides position absolutely.** The path is applied after velocity and gravity have been integrated and writes `x`/`y` outright, so a path-driven sprite ignores its own physics for placement. Do not expect `velocityX` and a path to add up. A tween on `x`/`y` runs *after* the path in the same frame and wins outright — pick one.
|
|
504
553
|
- **A path needs at least two points.** One point (or a non-array) clears the path and logs a warning instead of parking the sprite there.
|
|
505
554
|
- **Path movement still carries riders.** It feeds the frame delta like a tweened or velocity-driven solid, so a platform on a `followPath` circuit moves whatever stands on it.
|
|
506
|
-
- **`raycast` groups: pass an array, not varargs.** Android accepts both `raycast(x0, y0, x1, y1, ['enemy'])` and loose arguments; iOS reads only the
|
|
555
|
+
- **`raycast` groups: pass an array, not varargs.** Android accepts both `raycast(x0, y0, x1, y1, ['enemy'])` and loose arguments; iOS reads the fifth argument **only if it is an array**, and otherwise leaves the filter empty — which does not mean "no hit", it means the ray tests **every** sprite carrying a `collisionGroup`. So `raycast(x0, y0, x1, y1, 'enemy')` works on Android and, on iOS, silently answers a different question: a wall the ray was never meant to see now blocks the line of sight. Write the array and it works on both.
|
|
507
556
|
- **`raycast` only sees `visible`, non-`screenFixed` sprites carrying a `collisionGroup`.** A hidden pooled sprite is invisible to the ray, which is usually what you want; an untagged wall is invisible too, which usually is not. HUD sprites are skipped by design, so a screen-fixed score never blocks a world-space line of sight.
|
|
508
557
|
- **`raycast` is a discrete query, not a sensor.** Calling it from a game-clock timer or a tap handler is the intended use. Calling it every frame from JS puts exactly the bridge traffic in the loop that the whole engine exists to avoid.
|
|
509
558
|
- **`findPath` walks the sprite's center, not its silhouette.** The waypoints are a line through free grid cells, so a body wider than a cell clips corners unless you pass `clearance` (roughly half the walker's width) or shrink `cellSize`. Obstacles are read from the **hitbox**, so `hitboxScale`, the per-axis scales and `hitboxShape: 'circle'` shape the walkable space too.
|
|
@@ -517,4 +566,9 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
517
566
|
- **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
567
|
- **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
568
|
- **`attachedTo` is read-only.** Assigning to it stores a value on the proxy that the engine never reads — use `attachTo()` / `detach()`.
|
|
569
|
+
- **Inherited opacity is invisible from JS.** An attached sprite's `opacity` still reads back as its own value, however faded its owner is; the product is computed natively at draw time and is not exposed. There is nothing to read to find out what is actually on screen.
|
|
570
|
+
- **A faded owner stops its tags taking taps.** The inherited opacity goes through the same hit test as a sprite's own, so a tag on an owner at `opacity: 0` is untouchable even though its own `opacity` still says 1 — the pooling idiom of hiding an owner now silently disarms its attached buttons too.
|
|
571
|
+
- **`animate()` takes a percentage only on `scale`.** Inside the tween options `scaleX`, `scaleY` and `opacity` are read as plain numbers on both platforms, so `animate({ opacity: '50%' })` does not do what the property setter would. Percentages are for property writes; keep tween targets numeric.
|
|
572
|
+
- **A bad ratio or anchor name fails differently per platform.** Android keeps the value the property already held; iOS resets it to the property's default. Neither throws, both log a warning, and at creation time they agree — the split only shows on a write to a property already set to something else.
|
|
573
|
+
- **`anchor` beats `anchorX`/`anchorY` in the same `createSprite` call**, whatever the key order in the object, because the engine applies the preset after the two axes. Pass one or the other, not both.
|
|
520
574
|
- **`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.
|
|
@@ -54,6 +54,7 @@ Pinning `version` is worth the extra attribute: with two module versions install
|
|
|
54
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
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
56
|
| `0.4.0` built before 2026-08-26 | additionally `sprite.attachTo()` / `detach()` / `attachedTo`, and `gameView.follow()` still aborts the app on Android if it is handed anything that is not a sprite |
|
|
57
|
+
| `0.4.0` built on 2026-08-26 but before `83b7863` | additionally the opacity an attachment inherits from its target, the `anchor` property, and percentage strings on every ratio — a build in this window has `attachTo` but treats `'55%'` as a number it cannot read |
|
|
57
58
|
|
|
58
59
|
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.
|
|
59
60
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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` at `
|
|
5
|
+
Every snippet uses only API that exists in upstream `main` at `83b7863`, 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
7
|
<!-- TOC-START -->
|
|
8
8
|
## Contents
|
|
@@ -354,20 +354,23 @@ gameView.add(tag); // the tag is still a scene spri
|
|
|
354
354
|
|
|
355
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
356
|
|
|
357
|
-
A health bar is the same idea with two sprites, and shows
|
|
357
|
+
A health bar is the same idea with two sprites, and shows where inheritance stops:
|
|
358
358
|
|
|
359
359
|
```javascript
|
|
360
360
|
// blockSheet is any small opaque texture stretched and tinted — a sheet-less
|
|
361
361
|
// sprite draws nothing at all, that is the invisible-trigger idiom below
|
|
362
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,
|
|
363
|
+
const barFill = Game.createSprite({ sheet: blockSheet, width: 40, height: 5, zIndex: 6, anchor: 'left', tintColor: '#3f6' });
|
|
364
364
|
|
|
365
365
|
barBack.attachTo(hero, { offsetY: -34 });
|
|
366
|
-
barFill.attachTo(hero, { offsetX: -20, offsetY: -34 }); //
|
|
366
|
+
barFill.attachTo(hero, { offsetX: -20, offsetY: -34 }); // anchored left → grows to the right
|
|
367
367
|
gameView.add([barBack, barFill]);
|
|
368
368
|
|
|
369
|
-
// on damage:
|
|
369
|
+
// on damage: the width is yours to set — scale, visibility and tint are not inherited
|
|
370
370
|
barFill.width = 40 * (hp / hpMax);
|
|
371
|
+
|
|
372
|
+
// on death: one tween fades the hero AND both bars, because opacity does cascade
|
|
373
|
+
hero.animate({ opacity: 0, duration: 400 });
|
|
371
374
|
```
|
|
372
375
|
|
|
373
376
|
`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.
|
|
@@ -376,11 +379,12 @@ barFill.width = 40 * (hp / hpMax);
|
|
|
376
379
|
turret.attachTo(tank, { offsetY: -12, rotate: true });
|
|
377
380
|
```
|
|
378
381
|
|
|
379
|
-
|
|
382
|
+
Four things to keep in mind:
|
|
380
383
|
|
|
381
384
|
- **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
385
|
- **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
386
|
- **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.
|
|
387
|
+
- **A faded owner also stops its tags taking taps.** The inherited opacity runs through the hit test as well as the draw, so an owner at `opacity: 0` leaves an attached text button untouchable — while that button's own `opacity` still reads 1. Hide an owner with `visible = false` if the tags should keep working, or detach them.
|
|
384
388
|
|
|
385
389
|
### Invisible triggers
|
|
386
390
|
|
|
@@ -425,6 +429,16 @@ hero.hitboxScaleY = 0.92;
|
|
|
425
429
|
|
|
426
430
|
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
431
|
|
|
432
|
+
The orange dot is the third variable, not decoration: the box shrinks **around the anchor**, so where the anchor sits decides which edge stays put. A prop that should collide from mid-height down while still resting on the floor is the anchor and the scale together, and both have a readable form now:
|
|
433
|
+
|
|
434
|
+
```javascript
|
|
435
|
+
// the same thing, twice — the second one says what it is doing
|
|
436
|
+
Game.createSprite({ sheet, anchorY: 1, hitboxScaleY: 0.55 });
|
|
437
|
+
Game.createSprite({ sheet, anchor: 'bottom', hitboxScaleY: '55%' });
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Every ratio takes a percentage this way (`opacity`, `scale`, `scrollFactor`, `restitution`, `volume`, the `follow` margins…) and both anchors take names. Coordinates, sizes, degrees and speeds do not — they were never fractions.
|
|
441
|
+
|
|
428
442
|
### Zones you can be inside of (enter / exit) — `zones.js`
|
|
429
443
|
|
|
430
444
|
`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-26** (`
|
|
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** (`83b7863`, 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` and full transform inheritance — a child that scales
|
|
12
|
+
| `sprite.parent` and full transform inheritance — a child that scales and flips with its parent | Partly shipped, the rest still planned (priority 7) | **Position, rotation and opacity are done**: `attachTo(target, { offsetX, offsetY, rotate })` pins a sprite to another natively, chains included, and a fade on the owner cascades down, so turrets, hats, name tags and health bars need no JS. What is *not* inherited is `scale`, `visible`, `flipX`/`flipY` and `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 |
|
|
@@ -60,8 +60,9 @@ Recent additions that are easy to miss and easy to reimplement badly in JS:
|
|
|
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
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
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:
|
|
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. Later the same day the target's opacity started multiplying into attached sprites, so fading an owner fades its tags without touching their own `opacity`. It is not full parenting: position, rotation and opacity are inherited, `scale`, `visible`, the flips and `tintColor` are not.
|
|
64
|
+
- **Names and percentages on the numeric properties** (2026-08-26): every ratio the engine exposes also accepts `'55%'`, and the anchors accept `'left'`, `'bottom-right'`, `'center'`. Purely additive — numbers are untouched — and it retires the habit of a comment explaining what `anchorY: 1` plus `hitboxScaleY: 0.55` was supposed to mean. Only ratios: coordinates, sizes, degrees, speeds and the car model's `grip`/`drag` stay numeric.
|
|
64
65
|
|
|
65
66
|
## Checking the current state
|
|
66
67
|
|
|
67
|
-
This file describes upstream `main` at `
|
|
68
|
+
This file describes upstream `main` at `83b7863` on 2026-08-26 — a moving target: fifteen items on this list shipped within ten days of the skill being written, 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, then `attachTo`, its inherited opacity and the named values all on 2026-08-26, 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 — and it moved twice in one day, since opacity inheritance landed hours after the feature. A planned item can land partly done and then grow: read what a row does not cover before treating it as solved, and check the date before treating it as final. 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.
|