@maccesar/titools 4.14.0 → 4.15.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
|
@@ -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, sprite attachment, particles, camera |
|
|
180
|
+
| ti-game | 2D games with the `ti.game` module | Sprites, physics, shaped solids, 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 |
|
package/package.json
CHANGED
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
|
|
3
|
+
description: "Use when building or reviewing 2D games with the ti.game module for Titanium SDK (Android/iOS) — sprites, sheets, animations, tweens, physics (gravity, gravityX, solids, solidMode, linearDamping, carMode, thrust), collision groups, trigger zones, hitbox shapes (circle, rotatedRect), swept, raycast, paths, bitmap text, particles, ropes, camera, parallax, sound, drag & drop, multitouch, attachments. 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, slope, ramp, pool, plinko, bounce, friction, pathfinding, maze, patrol, HUD, score, game loop, GameView. ti.game is NOT Phaser, Godot or Box2D: you never write a game loop nor move a sprite from setInterval, and features like tilemaps, parent/child transforms and 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-27**, manifest **`0.5.0`** (`ee69056`) — and that number has meant different things at different times. Under `0.3.0` the engine gained the text engine, `screenFixed`, `swept` collisions, `collisionend`, `followPath`, animation chaining, `raycast`, `findPath`, game-clock timers, `scrollFactor` parallax and the `multiply`/`screen` blend modes, so anything still calling itself 0.3.0 has none of them. Under `0.4.0` it gained per-axis hitboxes, text word wrap, `unload()` on sheets and fonts, the LiveView renderer lifecycle (2026-08-23), then `attachTo`/`detach`, inherited opacity on attachments and named/percentage values (2026-08-26). `0.5.0` is the first bump that carries a feature set of its own: shape-aware solids and lightweight circle physics — `hitboxShape: 'rotatedRect'`, circular solids, `solidMode`, `gravityX`, `linearDamping` and two-sided `restitution`. 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
|
|
|
@@ -44,6 +44,12 @@ The practical consequence, and the single biggest mistake to avoid:
|
|
|
44
44
|
| A spawn wave or an AI tick that respects pause | `setInterval` | `gameView.every(ms, cb)` |
|
|
45
45
|
| Smoke, sparks, explosions | pooled sprites in JS | `Game.createEmitter(...)` |
|
|
46
46
|
| A swinging chain or cape | verlet solver in JS | `Game.createRope(...)` |
|
|
47
|
+
| A tilted ramp a crate slides down | a staircase of small rectangles | `rotation` + `hitboxShape: 'rotatedRect'` on the ramp |
|
|
48
|
+
| A round post or peg that deflects | a box solid and a JS correction | `hitboxShape: 'circle'` on the **solid**, not just on the ball |
|
|
49
|
+
| Balls that knock each other around | separating pairs by hand in a loop | `solidMode: 'push'` on both, each listing the other's group |
|
|
50
|
+
| A drum, bowl or cage holding balls in | a ring of wall sprites around the rim | one circle solid with `solidMode: 'contain'` |
|
|
51
|
+
| A ball that rolls to a stop | decrementing `velocityX` on a timer | `linearDamping` |
|
|
52
|
+
| Wind, or a conveyor pull | `velocityX -= k` every tick | `gravityX` |
|
|
47
53
|
| Slow motion or a pause that still draws | pausing your own timers | `gameView.timeScale = 0.5` / `0` |
|
|
48
54
|
|
|
49
55
|
## Minimal scene
|
|
@@ -118,7 +124,8 @@ Top-left origin, y-down, surface pixels. Touch coordinates map 1:1. `(x, y)` is
|
|
|
118
124
|
| --- | --- | --- |
|
|
119
125
|
| Plain velocity | `velocityX/Y` (px/s), `gravity` (px/s²) | Flappy, projectiles, falling objects, notes |
|
|
120
126
|
| Platformer | velocity + `solidWith`, read-only `onGround`, `land` event | Mario-style run & jump |
|
|
121
|
-
| Bouncing body | `restitution` (0..1) on top of `solidWith` | Balls, pinball, breakout |
|
|
127
|
+
| Bouncing body | `restitution` (0..1) on **either** side of the contact, on top of `solidWith` | Balls, pinball, breakout |
|
|
128
|
+
| Rolling body | `hitboxShape: 'circle'` + `linearDamping`, and `solidMode: 'push'` when they hit each other | Pool, plinko, a bingo drum |
|
|
122
129
|
| Car | `carMode: true`, `throttle`, `steering` (-1..1) | Top-down racer with emergent drift |
|
|
123
130
|
| Newtonian flight | `thrust` (px/s² along heading), `angularVelocity` (deg/s), `maxSpeed` | Asteroids, lunar lander |
|
|
124
131
|
| Tween | `animate({ x, y, duration, easing })` | Point-and-click walking, UI cards, patrols |
|
|
@@ -142,7 +149,7 @@ And when the question is "how do I get over there?", `gameView.findPath(from, to
|
|
|
142
149
|
|
|
143
150
|
**A sprite with `width`/`height` but no `sheet` renders nothing and works as an invisible trigger.** Score zones, goals, checkpoints, ceilings, kill floors and walls are all built this way in the official demos. This is the idiomatic way to add logic to a scene without art.
|
|
144
151
|
|
|
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.
|
|
152
|
+
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. Since 2026-08-27 the **solid's** shape counts too: a `'circle'` on the post makes the normal run centre to centre, and `'rotatedRect'` keeps a tilted platform's box turned with its art instead of re-boxing it square to the screen — which is what lets a crate slide down a ramp rather than stand on an invisible ledge. Two more properties live on the solid: `solidMode: 'contain'` holds matched circles *inside* a circular boundary with no ring of wall sprites, and `solidMode: 'push'` makes a pair of circles bodies that split the separation and trade momentum. `'push'` needs both sides to agree — both circles, both `'push'`, and each listing the other's `collisionGroup` — or it silently degrades to one ball shoving an immovable one. 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
153
|
|
|
147
154
|
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
155
|
|
|
@@ -211,7 +218,7 @@ When this skill runs out — a brand-new feature, a demo's exact code, a build q
|
|
|
211
218
|
| Upstream file | What it is authoritative for |
|
|
212
219
|
| --- | --- |
|
|
213
220
|
| `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 |
|
|
214
|
-
| `example/` |
|
|
221
|
+
| `example/` | 32 runnable demos, one per feature area — the reference implementations this skill's recipes are distilled from |
|
|
215
222
|
| `TODO.md` | What the maintainer has planned, with priorities. The source of [roadmap.md](references/roadmap.md) |
|
|
216
223
|
| `tutorial.md` | A step-by-step first scene (sprite, animation, tap-to-move). For a human learning the module, not for looking things up |
|
|
217
224
|
| `ios/README.md` | The iOS port's threading, coordinates and touch constants. Mostly for module developers; the app-facing facts are already here |
|
|
@@ -231,4 +238,6 @@ When this skill runs out — a brand-new feature, a demo's exact code, a build q
|
|
|
231
238
|
9. Does anything travel far enough per frame to tunnel? Give it `swept: true`.
|
|
232
239
|
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.
|
|
233
240
|
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.
|
|
234
|
-
12.
|
|
241
|
+
12. Is a solid's own **shape** doing the work, or is JS correcting for a box that is not what the art shows? A round post wants `hitboxShape: 'circle'` on itself; a tilted one wants `'rotatedRect'`.
|
|
242
|
+
13. Did anything get `restitution` on a *surface*? Every rider on it now bounces, and a hard landing on it stops setting `onGround` — check whatever gates jumps.
|
|
243
|
+
14. Are you using an API that actually exists? Anything involving tilemaps, full parent/child transforms, joysticks, gamepads, or the platformer *feel* on a slope (step-up, friction along the contact) — check [roadmap.md](references/roadmap.md) first.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ti.game API reference
|
|
2
2
|
|
|
3
|
-
Complete JS surface of `ti.game` as of upstream `main` at `
|
|
3
|
+
Complete JS surface of `ti.game` as of upstream `main` at `ee69056`, 2026-08-27, 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.5.0`** since 2026-08-27, and that bump is the shape-aware solids: `hitboxShape: 'rotatedRect'`, circular solids, `solidMode`, `gravityX`, `linearDamping` and two-sided `restitution`. Read the number as a floor, never as a feature list. Under `0.3.0` the engine gained the text engine (`createFont`/`createText`), `screenFixed`, `swept`, `collisionend`, `followPath`, animation chaining, `raycast`, `findPath`, the game-clock timers, `scrollFactor` and the `multiply`/`screen` blend modes; under `0.4.0` it gained `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). Two builds calling themselves 0.4.0 therefore differ by more than two builds calling themselves 0.4.0 and 0.5.0 do — check the build date, or feature-detect by reading the property **before** ever writing it (`typeof sprite.linearDamping === '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. So is a property the engine **normalizes**: writing `sprite.solidMode = 'nonsense'` reads back `'block'` on a build that implements it and `'nonsense'` on one that only stored it on the proxy, which makes it the one probe that survives having been written to.
|
|
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
|
|
|
@@ -63,7 +63,7 @@ Game.createSprite({ anchor: 'bottom-left', hitboxScaleY: '55%', opacity: '80%' }
|
|
|
63
63
|
|
|
64
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
65
|
|
|
66
|
-
They do **not** apply to coordinates, sizes, degrees, speeds, or the car model's `grip` and `drag
|
|
66
|
+
They do **not** apply to coordinates, sizes, degrees, speeds, or the friction coefficients — the car model's `grip` and `drag`, and `linearDamping`, are all per-second rates, not fractions.
|
|
67
67
|
|
|
68
68
|
**Anchor names** (`anchorX`, `anchorY`, and `anchor` for both at once):
|
|
69
69
|
|
|
@@ -236,6 +236,8 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
|
|
|
236
236
|
| --- | --- | --- |
|
|
237
237
|
| `velocityX`, `velocityY` | `0` | px/s |
|
|
238
238
|
| `gravity` | `0` | px/s², added to `velocityY`. Per sprite — there is no global gravity |
|
|
239
|
+
| `gravityX` | `0` | px/s², added to `velocityX`. The **sibling** of `gravity`, not half of a vector: `gravity` keeps its exact vertical meaning. Wind, a conveyor, or a top-down game whose "down" is sideways (`gravity: 0`, `gravityX` non-zero). Since 2026-08-27 |
|
|
240
|
+
| `linearDamping` | `0` | Fraction of speed shed per second, applied to both velocity axes before the position integration — rolling friction for ordinary sprites, which `drag` never was (that one only runs inside `carMode`). ~`0.6` is a pool ball on felt. Proportional, so a fast body sheds a lot and a slow one very little; below **4 px/s** the engine zeroes both axes outright, because a proportion never reaches zero on its own. Negative values are clamped to 0. Since 2026-08-27 |
|
|
239
241
|
| `maxSpeed` | `500` | px/s cap applied **only** to `thrust` acceleration and the `carMode` model. Writing `velocityX`/`velocityY` directly is never clamped |
|
|
240
242
|
| `angularVelocity` | `0` | deg/s |
|
|
241
243
|
| `thrust` | `0` | px/s² along the current heading (Newtonian flight) |
|
|
@@ -248,10 +250,30 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
|
|
|
248
250
|
| --- | --- | --- |
|
|
249
251
|
| `solidWith` | — | Array of group names that block this sprite. The engine pushes it out along the axis of least penetration |
|
|
250
252
|
| `onGround` | `false` | Read-only. Gate jumps on it |
|
|
251
|
-
| `restitution` | `0` | 0..1 bounce factor
|
|
252
|
-
| `
|
|
253
|
+
| `restitution` | `0` | 0..1 bounce factor. Read off **both** sides of the contact and mixed as `max(mover, solid)`, the way Box2D does it — so a springy floor bounces riders that are not themselves bouncy. Solids default to 0, so a scene that never sets it on a surface behaves exactly as it did before this existed. A bounce whose closing speed would come out under **40 px/s** is damped to a dead stop instead, so a settling body grounds rather than buzzing |
|
|
254
|
+
| `solidMode` | `'block'` | On the **solid**. `'block'` is an immovable wall (what every solid was). `'contain'` is an inward circular boundary — matched circles are kept *inside* its circumference (drums, bowls, lottery cages). `'push'` makes it a body in its own right: a matched circle and this one split the separation and exchange the closing velocity at equal mass. `'contain'` and `'push'` are **circle-on-circle only**; anything else, and any unknown string, falls back to `'block'`. Since 2026-08-27 |
|
|
255
|
+
| `oneWay` | `false` | On the **solid**: pass-through except for landings on its top edge — classic platformer floors. Honoured by `'block'` solids only |
|
|
253
256
|
| `carryRiders` | `true` | On the **solid**: a moving solid carries whoever stands on it (velocity, tweens, idle wobble — wrap teleports excluded). Set `false` for world-scroll terrain that moves while the player is meant to stay put |
|
|
254
257
|
|
|
258
|
+
#### Which resolver runs, and what each pair produces
|
|
259
|
+
|
|
260
|
+
The **mover's** hitbox shape picks the resolver; the **solid's** shape and `solidMode` decide the contact normal inside it. Both sides matter, and the pairing is not symmetric.
|
|
261
|
+
|
|
262
|
+
| Mover | Solid | Resolution |
|
|
263
|
+
| --- | --- | --- |
|
|
264
|
+
| `rect` | `rect` | Axis of least penetration — the original platformer path |
|
|
265
|
+
| `rect` or `rotatedRect` | `rotatedRect` (or the mover is one) | Separating axes over the four box axes; the smallest overlap is the way out, so a crate slides down a tilted ramp instead of standing on an invisible ledge |
|
|
266
|
+
| `circle` | `rect` | Closest point on the AABB → contact normal (corner bounces) |
|
|
267
|
+
| `circle` | `rotatedRect` | The circle is taken into the box's frame, resolved there, and the normal rotated back out |
|
|
268
|
+
| `circle` | `circle` | Centre to centre — a round post deflects instead of showing faces and corners it does not have |
|
|
269
|
+
| `circle` | `circle` + `solidMode: 'contain'` | The mover is kept **inside** `R - r`, analytically. No ring of wall sprites, so no seams to squeeze through |
|
|
270
|
+
| `circle` + `push` | `circle` + `push`, mutually listed | Resolved **once**, before every other pass: the separation is split in half and the closing velocity exchanged at equal mass |
|
|
271
|
+
|
|
272
|
+
Two consequences that cost an afternoon each if you meet them by surprise:
|
|
273
|
+
|
|
274
|
+
- **`solidMode` is only read when the mover is a circle.** A `rect` mover runs the axis/SAT path, which never looks at it — so a square ball inside a `'contain'` drum is pushed *out* of the boundary, not held in, and a `rect` against a `'push'` body shoves it like a wall. Give both sides `hitboxShape: 'circle'`.
|
|
275
|
+
- **`'push'` is the only mode that needs agreement from both sprites.** Each must be `solidMode: 'push'`, each must carry a circle hitbox, and **each must list the other's `collisionGroup` in its own `solidWith`**. Miss any one of those and the pair silently degrades to the ordinary one-sided resolver: one body shoves an immovable one, which looks like a bug in the momentum and is really a missing group. `'contain'` is one-directional by design — only the ball lists the drum.
|
|
276
|
+
|
|
255
277
|
### Collision
|
|
256
278
|
|
|
257
279
|
| Property | Default | Notes |
|
|
@@ -260,8 +282,8 @@ Hit-testing runs against the transformed shape (rotation and scale included), to
|
|
|
260
282
|
| `collidesWith` | — | Array of groups this sprite reports overlaps with |
|
|
261
283
|
| `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%'` |
|
|
262
284
|
| `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 |
|
|
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 |
|
|
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).
|
|
285
|
+
| `hitboxShape` | `'rect'` | `'circle'` — radius = half the smaller drawn side × `hitboxScale` (the per-axis scales are skipped: a circle has no axes). Circles resolve against solids along the contact normal (corner bounces) and get a round touch area. `'rotatedRect'` keeps the collision rect **turned with the sprite** (an OBB) instead of re-boxing it square to the screen: a plain `'rect'` at 45° is re-boxed around its turned corners and comes out 41% wider with a flat top that is not there. Only matters once `rotation` is non-zero, and it does honour `hitboxScaleX`/`hitboxScaleY`, unlike a circle. Covers solids, `collidesWith` overlap, `raycast`, the swept pass and the debug overlay. Since 2026-08-27 |
|
|
286
|
+
| `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). Two **circle** hitboxes now sweep as circles (the Minkowski sum of two circles is a circle of radius r1+r2); a circle against a rectangular or turned solid sweeps as a box, and against a turned one the box is grown into a *rounded* rect so the diagonal does not poke out by 1.41r. Set it on the *mover* — the bullet, not the wall. The sweep skips solids that are not `solidMode: 'block'` |
|
|
265
287
|
| `debug` | `false` | Draw this sprite's shapes: green = collision AABB, blue = touch bounds, orange dot = anchor |
|
|
266
288
|
|
|
267
289
|
### Car (`carMode`)
|
|
@@ -536,6 +558,17 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
536
558
|
- **`collision` fires once per overlap-enter**, re-arming only after the shapes separate. A sprite resting inside a trigger does not re-fire — `collisionend` tells you when it left.
|
|
537
559
|
- **Pooling a sprite with `visible = false` fires `collisionend`** on whoever was touching it. That is usually what you want (a plate whose ball was despawned closes its door), but a handler that assumes separation means "the player walked away" will misfire.
|
|
538
560
|
- **The built-in pixel font only covers ASCII 32..126.** Em dashes, accents and `ñ` render as blanks or garbage — the module's own demos rewrote `—` as `-` for this reason. Ship a BMFont atlas (`tools/genfont.py` rasterizes any TTF) when the game needs accented Spanish text.
|
|
561
|
+
- **A bouncy floor switches `onGround` off.** In every resolver the grounding and the `land` event live in the branch taken when the bounce is *damped away*; a contact whose rebound would exceed 40 px/s reflects and grounds nothing. That was always true of a bouncy rider, but `restitution` now mixes as `max(mover, solid)`, so giving a *floor* `restitution: 0.5` silently stops every rider that lands hard on it from ever reporting `onGround` — and a jump gated on `onGround` stops working on that one platform.
|
|
562
|
+
- **A two-sided `restitution` cannot be opted out of.** The mix is `max`, not a product or an average, so the springier surface always wins: a deliberately dead `restitution: 0` crate still bounces off a floor set to `0.5`. If one rider must not bounce, the bounce has to live on the riders, not on the surface.
|
|
563
|
+
- **A resting body is allowed to sink half a pixel — in some resolvers.** Separation is skipped when the overlap is under 0.5 px, because shoving a gravity-loaded body back out sixty times a second is what makes a settled pile tremble; the closing velocity is still cancelled, so the sink cannot grow past it. It applies to the circle path, the turned-rect path and `'push'` pairs. Plain rect-against-rect still separates in full. Do not derive an exact contact position from `y` after a landing.
|
|
564
|
+
- **`linearDamping` is not surface friction.** Nothing checks whether the sprite is touching anything: it bleeds speed on both axes every frame, in the air as much as on the felt, and along a slope as much as across it. It is right for a pool table and wrong for a hill, which is why upstream still lists contact-only friction as unbuilt. A projectile that must keep its arc wants `0`.
|
|
565
|
+
- **`linearDamping` kills a slow deliberate drift.** Under a combined 4 px/s the engine zeroes *both* axes outright, since a proportion never reaches zero on its own. A conveyor nudging something along at 3 px/s stops dead the moment that sprite also carries any damping.
|
|
566
|
+
- **A swept sprite is not contained and not pushed.** The sweep only considers solids in `solidMode: 'block'` — a `'contain'` boundary would stop the ball against its *outside*, and a `'push'` body is meant to move. So `swept: true` does not stop a fast enough ball from crossing a drum wall between frames; only the static pass contains it. Keep the speeds inside a container below its wall thickness per frame.
|
|
567
|
+
- **`'push'` pairs never ground and never fire `land`.** The bilateral resolver moves positions and exchanges velocity and touches nothing else, so a pile of balls resting on each other all read `onGround: false` unless something underneath them is a `'block'` solid.
|
|
568
|
+
- **The `'push'` exchange has no masses, no spin and no friction.** A marble and a boulder trade velocity identically, and nothing ever starts rotating from a contact. `restitution` mixes as `max` here too, and with no mass to weight it against, that is the whole model.
|
|
569
|
+
- **`swept: true` no longer drags a resting body backwards** (since 2026-08-27). A hit at t = 0 is a contact the sprite is already in, and the static resolver owns it; the sweep used to clamp the sprite back to where the frame started, every frame, while its along-surface speed kept climbing until it broke loose looking launched. On an older build, that is what a swept sprite parked on a slope or a floor does — it is a build date, not your code.
|
|
570
|
+
- **The hitbox shapes are exclusive, and an unknown name is `'rect'`.** Setting `hitboxShape` clears whichever of the other two was set, so switching a sprite between `'circle'` and `'rotatedRect'` at runtime is safe, and a typo lands on the default rather than throwing. Reading the property back is therefore also the one clean way to tell whether a build understands `'rotatedRect'` at all.
|
|
571
|
+
- **`'rotatedRect'` costs something and only earns it once `rotation` is non-zero.** At zero degrees it produces the same box the default already produces, by a more expensive route. Turn it on for the tilted things, not for everything.
|
|
539
572
|
- **`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.
|
|
540
573
|
- **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.
|
|
541
574
|
- **Text `width`/`height` are read-only in practice.** They report the laid-out block; to make text bigger use `scale`.
|
|
@@ -25,8 +25,8 @@ There is no registry and no `ti module install` command — Titanium modules are
|
|
|
25
25
|
|
|
26
26
|
```
|
|
27
27
|
modules/
|
|
28
|
-
├── android/ti.game/0.
|
|
29
|
-
└── iphone/ti.game/0.
|
|
28
|
+
├── android/ti.game/0.5.0/
|
|
29
|
+
└── iphone/ti.game/0.5.0/
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Unzipping the module zip at the project root creates exactly that layout.
|
|
@@ -37,14 +37,14 @@ Then declare it in `tiapp.xml`. The iOS platform key is `iphone`, not `ios`:
|
|
|
37
37
|
|
|
38
38
|
```xml
|
|
39
39
|
<modules>
|
|
40
|
-
<module platform="android" version="0.
|
|
41
|
-
<module platform="iphone" version="0.
|
|
40
|
+
<module platform="android" version="0.5.0">ti.game</module>
|
|
41
|
+
<module platform="iphone" version="0.5.0">ti.game</module>
|
|
42
42
|
</modules>
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Pinning `version` is worth the extra attribute: with two module versions installed side by side, an unpinned entry silently picks the highest one.
|
|
46
46
|
|
|
47
|
-
**The version string does not identify a feature set — the build date does.** The manifest went `0.3.0` → **`0.4.0`** on 2026-08-20
|
|
47
|
+
**The version string does not identify a feature set — the build date does.** The manifest went `0.3.0` → **`0.4.0`** on 2026-08-20 and **`0.5.0`** on 2026-08-27, and in between each number kept accepting features without moving. So every one of them covers several incompatible builds:
|
|
48
48
|
|
|
49
49
|
| A build from | Missing, compared to what this skill documents |
|
|
50
50
|
| --- | --- |
|
|
@@ -55,8 +55,9 @@ Pinning `version` is worth the extra attribute: with two module versions install
|
|
|
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
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 |
|
|
58
|
+
| `0.4.0` at its last commit (`b780051`) | additionally everything the `0.5.0` bump carries: `hitboxShape: 'rotatedRect'`, circular solids resolved as circles, `solidMode`, `gravityX`, `linearDamping`, and `restitution` read off both sides of a contact. It also still pulls a swept sprite back to where its frame started when it begins the frame already touching a solid, so a `swept: true` body parked on a slope creeps and then breaks loose as if launched |
|
|
58
59
|
|
|
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.
|
|
60
|
+
The published releases are pre-releases and lag `main`: the newest one is still `0.4.0` (2026-08-20), which is now two manifest numbers and a physics release behind. 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.
|
|
60
61
|
|
|
61
62
|
Building the module from source:
|
|
62
63
|
|
|
@@ -68,7 +69,7 @@ ti build -p ios --build-only # macOS only → ios/dist/ti.game-iphone-<ve
|
|
|
68
69
|
|
|
69
70
|
## Classic projects
|
|
70
71
|
|
|
71
|
-
`require('ti.game')` anywhere and add the GameView to a window. The
|
|
72
|
+
`require('ti.game')` anywhere and add the GameView to a window. The 32 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):
|
|
72
73
|
|
|
73
74
|
```javascript
|
|
74
75
|
// Resources/flappy.js
|
|
@@ -2,12 +2,12 @@
|
|
|
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 `ee69056`, 2026-08-27 (manifest `0.5.0` — the number moved with this release, but it has sat still through whole feature sets before, so date the build). Check [roadmap.md](roadmap.md) before reaching for anything not shown here.
|
|
6
6
|
|
|
7
7
|
<!-- TOC-START -->
|
|
8
8
|
## Contents
|
|
9
9
|
|
|
10
|
-
- [The
|
|
10
|
+
- [The 32 demos, and what each one is the reference for](#the-32-demos-and-what-each-one-is-the-reference-for)
|
|
11
11
|
- [The scaffolding](#the-scaffolding)
|
|
12
12
|
- [Cross-cutting patterns](#cross-cutting-patterns)
|
|
13
13
|
- [Tap-to-flap](#tap-to-flap)
|
|
@@ -28,7 +28,7 @@ Every snippet uses only API that exists in upstream `main` at `83b7863`, 2026-08
|
|
|
28
28
|
|
|
29
29
|
<!-- TOC-END -->
|
|
30
30
|
|
|
31
|
-
## The
|
|
31
|
+
## The 32 demos, and what each one is the reference for
|
|
32
32
|
|
|
33
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
34
|
|
|
@@ -60,6 +60,12 @@ Every snippet uses only API that exists in upstream `main` at `83b7863`, 2026-08
|
|
|
60
60
|
| `demoscene.js` | Sine scrollers and copper bars built from `followPath` circles | [Visual effects](#visual-effects) |
|
|
61
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
62
|
| `timescale.js` | `timeScale` slow motion and freeze; game clock vs `setInterval` | [Timers on the game clock](#timers-on-the-game-clock) |
|
|
63
|
+
| `circles.js` | The three hitbox shapes as **solids**, side by side, plus `'block'` vs `'push'` on the same shot | [Shaped solids](#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
|
|
64
|
+
| `slopes.js` | Tilted `'rotatedRect'` ramps with a rect rider and a circle rider, and `restitution` living on the surface | [Shaped solids](#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
|
|
65
|
+
| `plinko.js` | 86 circular solids: why a peg has to be a circle and not its bounding box | [Shaped solids](#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
|
|
66
|
+
| `pool.js` | `solidMode: 'push'` bodies exchanging momentum, `linearDamping` as felt | [Shaped solids](#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
|
|
67
|
+
| `drum.js` | `solidMode: 'contain'`: 25 balls inside one analytic circle, no wall segments | [Shaped solids](#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
|
|
68
|
+
| `wind.js` | `gravityX` as wind, and as the lean of a top-down table | [Shaped solids](#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
|
|
63
69
|
|
|
64
70
|
## The scaffolding
|
|
65
71
|
|
|
@@ -439,6 +445,105 @@ Game.createSprite({ sheet, anchor: 'bottom', hitboxScaleY: '55%' });
|
|
|
439
445
|
|
|
440
446
|
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
447
|
|
|
448
|
+
### Shaped solids: ramps, round posts and bodies that push back — `slopes.js`, `circles.js`, `pool.js`, `drum.js`
|
|
449
|
+
|
|
450
|
+
Until 2026-08-27 a solid was always resolved as an axis-aligned box no matter what it looked like, so a round post showed flat faces and a tilted ramp blocked along a phantom horizontal ledge. Now the **solid's** declared shape takes part, which turns four hand-written workarounds into one property.
|
|
451
|
+
|
|
452
|
+
**A ramp is one turned rectangle**, not a staircase of small ones:
|
|
453
|
+
|
|
454
|
+
```javascript
|
|
455
|
+
const ramp = Game.createSprite({
|
|
456
|
+
sheet: wallSheet, x: W * 0.28, y: H * 0.24,
|
|
457
|
+
width: W * 0.30, height: 14,
|
|
458
|
+
rotation: 12,
|
|
459
|
+
hitboxShape: 'rotatedRect', // the box turns with the art
|
|
460
|
+
collisionGroup: 'ramp'
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const crate = Game.createSprite({
|
|
464
|
+
sheet: crateSheet, x: W * 0.2, y: 0,
|
|
465
|
+
gravity: 1400, swept: true,
|
|
466
|
+
solidWith: ['ramp']
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
The rider needs nothing new — `'rect'` against a `'rotatedRect'` goes through separating axes on its own, and a circle rider is taken into the ramp's frame. Both settle on the real face and slide. Keep ramps **short**: a rider that slides a long one arrives at the bottom carrying everything the drop gave it and leaves like a projectile, which reads as the ramp firing it.
|
|
471
|
+
|
|
472
|
+
**A round obstacle wants a circle on the solid, not just on the ball.** `hitboxShape: 'circle'` on the mover has been there for a while; putting it on the *peg* is what makes the contact normal run centre to centre:
|
|
473
|
+
|
|
474
|
+
```javascript
|
|
475
|
+
const peg = Game.createSprite({ sheet, x, y, hitboxShape: 'circle', collisionGroup: 'peg' });
|
|
476
|
+
|
|
477
|
+
const chip = Game.createSprite({
|
|
478
|
+
sheet, hitboxShape: 'circle', gravity: 1200,
|
|
479
|
+
restitution: 0.42,
|
|
480
|
+
swept: true, // small pegs, fast chip: no stepping over one
|
|
481
|
+
collisionGroup: 'chip',
|
|
482
|
+
solidWith: ['peg', 'chip'],
|
|
483
|
+
solidMode: 'push' // …and the chips are bodies to each other
|
|
484
|
+
});
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
As bounding boxes every peg is a square with a flat top and the chips stack instead of scattering. That is the whole Plinko board.
|
|
488
|
+
|
|
489
|
+
**Balls that push each other** need agreement on both sides — this is the one setting that fails silently when it is half-configured:
|
|
490
|
+
|
|
491
|
+
```javascript
|
|
492
|
+
const ball = Game.createSprite({
|
|
493
|
+
sheet: ballSheet, hitboxShape: 'circle',
|
|
494
|
+
restitution: 0.96,
|
|
495
|
+
linearDamping: 0.62, // the felt: it trickles to a halt
|
|
496
|
+
swept: true,
|
|
497
|
+
collisionGroup: 'ball',
|
|
498
|
+
solidWith: ['ball', 'rail'], // ← its own group is in there
|
|
499
|
+
solidMode: 'push'
|
|
500
|
+
});
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Every clause matters: both sprites `'push'`, both circles, and **each listing the other's group**. Drop any one and the pair quietly degrades to one ball shoving an immovable one, which looks like broken momentum and is a missing string. There are no masses and no spin — a marble and a boulder trade velocity identically.
|
|
504
|
+
|
|
505
|
+
**A container is one sprite, not a ring of walls.** `solidMode: 'contain'` keeps matched circles *inside* a circle, analytically, so there are no seams to squeeze through and nothing to place by hand:
|
|
506
|
+
|
|
507
|
+
```javascript
|
|
508
|
+
// sheetless: it collides and contains, and draws nothing — draw the barrel over it
|
|
509
|
+
gameView.add(Game.createSprite({
|
|
510
|
+
x: CX, y: CY, width: DRUM * 0.88, height: DRUM * 0.88,
|
|
511
|
+
touchEnabled: false,
|
|
512
|
+
hitboxShape: 'circle',
|
|
513
|
+
solidMode: 'contain',
|
|
514
|
+
collisionGroup: 'drum',
|
|
515
|
+
debug: true // green circle = exactly where the wall is
|
|
516
|
+
}));
|
|
517
|
+
|
|
518
|
+
// the balls list the drum; the drum lists nothing — containment is one-directional
|
|
519
|
+
const ball = Game.createSprite({
|
|
520
|
+
sheet: ballSheet, hitboxShape: 'circle',
|
|
521
|
+
gravity: 900, restitution: 0.55, linearDamping: 0.35,
|
|
522
|
+
collisionGroup: 'ball', solidWith: ['drum', 'ball'], solidMode: 'push'
|
|
523
|
+
});
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Two things bite here. The sweep **ignores** anything that is not `solidMode: 'block'`, so `swept: true` will not stop a fast enough ball crossing the boundary between frames — keep the speeds inside a container sane. And a `'contain'` solid only holds *circles*: a rect mover runs a resolver that never reads `solidMode` and gets pushed **out** of the boundary instead of held in.
|
|
527
|
+
|
|
528
|
+
**`linearDamping` is the missing friction, with a caveat.** `drag` only ever worked inside `carMode`; this is the same idea for ordinary sprites, and it is what brings a pool table to rest. But nothing checks contact — it bleeds speed on both axes, in the air as much as on the felt, and along a slope as much as across it. Right for a table, wrong for a hill. Below a combined 4 px/s the engine zeroes the velocity outright, so a deliberate 3 px/s drift dies the moment that sprite also carries damping.
|
|
529
|
+
|
|
530
|
+
**A bouncy surface is now a property of the surface.** `restitution` is read off both sides and mixed as `max`, so a springy floor rebounds riders that are not themselves bouncy:
|
|
531
|
+
|
|
532
|
+
```javascript
|
|
533
|
+
addSurface({ color: '#4caf50', restitution: 0.5 }); // the floor and the side walls
|
|
534
|
+
// the ball keeps restitution: 0.1 and still bounces off them at 0.5
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
The cost of `max` is that it cannot be opted out of — a deliberately dead crate bounces off that floor too — and a rebound over 40 px/s takes the reflecting branch, which never sets `onGround` and never fires `land`. A jump gated on `onGround` stops working on the one platform somebody made bouncy.
|
|
538
|
+
|
|
539
|
+
**`gravityX` is the sibling of `gravity`, not half of a vector.** `gravity` keeps its exact vertical meaning; this is a second constant acceleration on X, read natively every tick, so a field of leaves answers a wind change at once with no per-frame JS:
|
|
540
|
+
|
|
541
|
+
```javascript
|
|
542
|
+
leaves.forEach((leaf) => { leaf.gravityX = wind; }); // on a button, not on a timer
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
The other use has nothing to do with wind: a top-down table with `gravity: 0` and a non-zero `gravityX` is a board seen from above with a lean.
|
|
546
|
+
|
|
442
547
|
### Zones you can be inside of (enter / exit) — `zones.js`
|
|
443
548
|
|
|
444
549
|
`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.
|
|
@@ -1230,7 +1335,7 @@ ball.addEventListener('collision', (e) => {
|
|
|
1230
1335
|
|
|
1231
1336
|
Note the split: walls and the net are `solidWith` (the engine bounces the ball off them for free), while the players and the floor are `collidesWith` (you decide what happens). For breakout, bricks go in `collidesWith` and are removed with `visible = false` on hit while the paddle and walls stay solid.
|
|
1232
1337
|
|
|
1233
|
-
A `restitution: 1` ball on an invisible floor bounces forever with no JS at all.
|
|
1338
|
+
A `restitution: 1` ball on an invisible floor bounces forever with no JS at all — and since 2026-08-27 the bounce can live on the *floor* instead, which is what makes one springy platform in an otherwise dead level a one-property change. Round obstacles, bumpers that knock each other around and a table that brings everything to rest are all in [Shaped solids](#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs).
|
|
1234
1339
|
|
|
1235
1340
|
**Opponent AI** on an 80 ms timer, reading a handful of live properties per tick:
|
|
1236
1341
|
|
|
@@ -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-27** (`ee69056`, manifest `0.5.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
|
|
|
@@ -14,7 +14,7 @@ Planned items come from the module's `TODO.md`. Planned is not shipped — when
|
|
|
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 |
|
|
16
16
|
| Gamepad support | Planned (priority 6) | Nothing native. Touch controls only |
|
|
17
|
-
| Slopes in platformer terrain |
|
|
17
|
+
| Slopes in platformer terrain | Partly shipped, the platformer feel still planned (priority 3) | **The geometry is done**: a tilted `hitboxShape: 'rotatedRect'` solid resolves along its real face, carries a rider and lets a crate or a ball slide down it. What is missing is the *feel* on top of it — walking up a slope without the step-up stutter, friction that acts **along the contact only** (`linearDamping` bleeds speed in every direction, right for a pool table and wrong for a hill), and terrain built from more than one flat ramp. Stepped rectangles are no longer the workaround for a single ramp; they still are for a curve |
|
|
18
18
|
| Per-frame animation events (footstep on frame 3) | Planned (priority 9) | Use `animationcomplete` on a short non-looping animation, or a timer aligned to the animation's fps |
|
|
19
19
|
| `playbackRate`, pitch jitter, stereo pan, `fadeTo()` on sounds | Planned (priority 8) | Only `volume`, `loop`, `play`, `pause`, `stop` exist. Ship a few pre-pitched variants of a sample and alternate between them |
|
|
20
20
|
| An fps / draw-call / sprite-count overlay | Planned (developer experience) | `debug: true` draws collision shapes only. There is no stats overlay and no `performance` event — measure with platform tooling |
|
|
@@ -32,7 +32,7 @@ The module's own `TODO.md` rules these out, each being a project the size of eve
|
|
|
32
32
|
|
|
33
33
|
- **2D lighting and shadows.** If it ever comes up: a screen-dim overlay plus additive light sprites (`blend: 'add'`) gets 90% of the look for 1% of the cost, and `blend: 'multiply'` now does contact shadows under sprites.
|
|
34
34
|
- **Skeletal animation (Spine).** Frame animations from sheets only.
|
|
35
|
-
- **Full rigid-body physics (Box2D class).** What exists — velocity, gravity, solids with penetration resolution, restitution, circle and rect hitboxes, the car model, thrust — covers the target genres. Joints, torque, compound bodies and stacking do not exist.
|
|
35
|
+
- **Full rigid-body physics (Box2D class).** What exists — velocity, gravity on both axes, solids with penetration resolution, two-sided restitution, circle, rect and turned-rect hitboxes, `linearDamping`, the car model, thrust, and since 2026-08-27 a bilateral circle-on-circle response that splits the separation and exchanges the closing velocity — covers the target genres. What that last one is **not** is a solver: it assumes equal masses, runs one pass per frame with no iteration, and carries no friction or spin, so a tall stack settles rather than balancing. Joints, torque, compound bodies and real stacking still do not exist, and a game that needs them needs a physics engine, not this.
|
|
36
36
|
- **Render-to-texture.** The only offscreen pass is the fullscreen `cameraEffect`.
|
|
37
37
|
|
|
38
38
|
## Already shipped (do not "work around" these)
|
|
@@ -62,7 +62,9 @@ Recent additions that are easy to miss and easy to reimplement badly in JS:
|
|
|
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
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
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.
|
|
65
|
+
- **Shape-aware solids and circle-body physics** (2026-08-27, the `0.5.0` bump): the solid's own shape finally takes part in the resolution. `hitboxShape: 'rotatedRect'` keeps a rect turned with its sprite for solids, overlap events, `raycast`, the swept pass and the debug overlay; a circular solid deflects along the centre-to-centre normal instead of showing faces and corners it does not have; `solidMode: 'contain'` holds matched circles inside a circular boundary analytically, with no ring of wall sprites to squeeze between; `solidMode: 'push'` makes a pair of circles bodies that split the separation and trade the closing velocity; `gravityX` is a second constant acceleration on X; `linearDamping` is the rolling friction ordinary sprites never had, `drag` having only ever worked inside `carMode`; and `restitution` is read off **both** sides of a contact so a floor can supply the bounce. Every one of them defaults to today's behaviour, so nothing that exists moves differently. The workarounds this retires are the expensive ones: a ring of wall segments around a drum, a JS loop separating balls by hand, `velocityX -= k` on a timer for wind, and stepped rectangles for a single ramp.
|
|
66
|
+
|
|
65
67
|
|
|
66
68
|
## Checking the current state
|
|
67
69
|
|
|
68
|
-
This file describes upstream `main` at `
|
|
70
|
+
This file describes upstream `main` at `ee69056` on 2026-08-27 — a moving target: the fourteen dated entries in "Already shipped" below all landed between 2026-08-18 and 2026-08-27, and the manifest tracks almost 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, before `0.5.0` on 2026-08-27 finally carried a feature set of its own). `attachTo` was 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. Slopes moved the next day, and moved **partly**: the geometry shipped and the platformer feel did not, which is the shape most of this table will take from here on. 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.
|