@maccesar/titools 4.9.0 → 4.10.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,
|
|
180
|
+
| ti-game | 2D games with the `ti.game` module | Sprites, physics, collisions, raycasts, pathfinding, text HUDs, 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 |
|
|
@@ -437,7 +437,7 @@ When it activates:
|
|
|
437
437
|
- Physics: gravity and velocity, solid platforms, one-way floors, bouncing, the arcade car model, Newtonian thrust
|
|
438
438
|
- Collision groups, invisible trigger zones, enter/exit events, swept AABB for fast bullets, hitbox tuning
|
|
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 and screen-fixed HUDs inside the scene
|
|
442
442
|
- Particle emitters, Verlet ropes, camera follow/zoom/shake/effects, `scrollFactor` parallax, low-latency sound
|
|
443
443
|
- Game-clock timers that obey pause and slow motion
|
|
@@ -452,6 +452,7 @@ Example prompts:
|
|
|
452
452
|
"Why does my game stutter when I move sprites from setInterval?"
|
|
453
453
|
"Add a score HUD that stays put while the camera follows the player."
|
|
454
454
|
"Make the guard walk a patrol route and turn around before the ledge."
|
|
455
|
+
"Tap to walk, but the player should go around the tree instead of through it."
|
|
455
456
|
```
|
|
456
457
|
|
|
457
458
|
Key rules:
|
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 and iOS) — sprites, sprite sheets, animations, tweens, physics (gravity,
|
|
3
|
+
description: "Use when building or reviewing 2D games with the ti.game module for Titanium SDK (Android and iOS) — sprites, sprite sheets, animations, tweens, physics (gravity, solids, carMode drifting, thrust), collision groups, trigger zones, swept AABB, raycast, paths, bitmap text, particles, ropes, camera follow/zoom/shake, parallax, sound, drag & drop, multitouch. AUTO-DETECT: if tiapp.xml declares ti.game or any file calls require('ti.game'), invoke BEFORE writing ANY game code. Also trigger on: game, juego, sprite, spritesheet, platformer, endless runner, top-down, shooter, racer, flappy, breakout, rhythm, cards, puzzle, collision, hitbox, raycast, line of sight, pathfinding, maze, patrol, parallax, particles, HUD, score, game loop, 60 fps, GameView. ti.game is NOT Phaser, Godot or Box2D: you never write a game loop, never move a sprite from setInterval, and several familiar features (tilemap layer, sprite parenting, joystick, word wrap) 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-20**, 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. 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
|
|
|
@@ -38,6 +38,7 @@ The practical consequence, and the single biggest mistake to avoid:
|
|
|
38
38
|
| An enemy on a fixed patrol route | chained `animate()` legs | `sprite.followPath(points, { loop })` |
|
|
39
39
|
| Play an attack, then go back to idle | an `animationcomplete` handler | `play('attack', { then: 'idle' })` |
|
|
40
40
|
| Line of sight, a ledge probe, a hitscan shot | an invisible probe sprite | `gameView.raycast(x0, y0, x1, y1, ['group'])` |
|
|
41
|
+
| Walking around obstacles to a tapped point | your own A* in JS, or a straight line through the wall | `gameView.findPath(from, to, opts)` → `sprite.followPath(path)` |
|
|
41
42
|
| A spawn wave or an AI tick that respects pause | `setInterval` | `gameView.every(ms, cb)` |
|
|
42
43
|
| Smoke, sparks, explosions | pooled sprites in JS | `Game.createEmitter(...)` |
|
|
43
44
|
| A swinging chain or cape | verlet solver in JS | `Game.createRope(...)` |
|
|
@@ -135,6 +136,8 @@ A fast mover can slip through a thin target between frames: it never overlaps on
|
|
|
135
136
|
|
|
136
137
|
When the question is not "did we touch?" but "what is over there?", the answer is `gameView.raycast(x0, y0, x1, y1, ['group'])` — line of sight, a ledge probe ahead of a walker's feet, a hitscan shot. It returns `null` or the nearest hit (`{ x, y, distance, group, sprite, normal }`), needs nothing but a `collisionGroup` on the targets, and replaces the old trick of parking an invisible probe sprite where the ray would land. It is a discrete query: call it from a decision timer or a tap handler, never every frame.
|
|
137
138
|
|
|
139
|
+
And when the question is "how do I get over there?", `gameView.findPath(from, to, { cellSize, groups, clearance, bounds })` runs grid A* around the same tagged sprites and hands back waypoints for `followPath` — tap-to-walk that rounds a tree, a hound re-routing to the player on an 800 ms timer. Also discrete: the walk itself runs natively between queries, so pathfind on taps and AI ticks, never per frame. Pass `clearance` of about half the walker's width, since the route is a line for the sprite's **center**.
|
|
140
|
+
|
|
138
141
|
**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.
|
|
139
142
|
|
|
140
143
|
Tune fairness with `hitboxScale` (art rarely fills its frame; slightly small hitboxes feel better) 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.
|
|
@@ -189,7 +192,7 @@ Read the one that matches the task instead of guessing — the API surface below
|
|
|
189
192
|
| Need | Reference |
|
|
190
193
|
| --- | --- |
|
|
191
194
|
| Exact properties, methods, events, defaults for every object | [api.md](references/api.md) |
|
|
192
|
-
| Working code per genre: flappy, platformer, top-down, racer, asteroids, endless runner, rhythm, cards, point & click, particles, ropes, camera — plus patrol paths, raycast probes, parallax and game-clock timers | [recipes.md](references/recipes.md) |
|
|
195
|
+
| Working code per genre: flappy, platformer, top-down, racer, asteroids, endless runner, rhythm, cards, point & click, particles, ropes, camera — plus patrol paths, raycast probes, pathfinding, parallax and game-clock timers | [recipes.md](references/recipes.md) |
|
|
193
196
|
| Install the module, tiapp.xml, Alloy vs Classic wiring, assets, sprite sheets, atlases, lifecycle, testing | [project-setup.md](references/project-setup.md) |
|
|
194
197
|
| What does NOT exist yet, what is coming, and how to work around it today | [roadmap.md](references/roadmap.md) |
|
|
195
198
|
|
|
@@ -205,4 +208,4 @@ Read the one that matches the task instead of guessing — the API surface below
|
|
|
205
208
|
8. Is the HUD `Game.createText` with `screenFixed: true`, rather than a stack of overlaid labels?
|
|
206
209
|
9. Does anything travel far enough per frame to tunnel? Give it `swept: true`.
|
|
207
210
|
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.
|
|
208
|
-
11. Are you using an API that actually exists? Anything involving tilemaps, sprite parenting, joysticks,
|
|
211
|
+
11. Are you using an API that actually exists? Anything involving tilemaps, sprite parenting, joysticks, gamepads or word-wrapped text — 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` on 2026-08-
|
|
3
|
+
Complete JS surface of `ti.game` as of upstream `main` on 2026-08-20, 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.
|
|
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
|
|
|
@@ -58,6 +58,7 @@ A normal Titanium view — add it to a window or any container, size it with the
|
|
|
58
58
|
| `stopFollow()` | method | — | Stop following; the camera stays where it is |
|
|
59
59
|
| `shake({ strength, duration })` | method | `12` px, `400` ms | Detuned-sine rumble on the projection only — follow, bounds and touch mapping are unaffected |
|
|
60
60
|
| `raycast(x0, y0, x1, y1, groups)` | method | — | One-shot nearest-hit query along the segment against **visible, non-`screenFixed`** sprites carrying a `collisionGroup` in `groups` (omit for any tagged sprite). Returns `null` for a clear ray, else `{ x, y, distance, group, sprite, normal: { x, y } }`. Rect hitboxes are tested as their AABB, circle hitboxes exactly; a ray starting inside a hitbox reports it at distance 0 |
|
|
61
|
+
| `findPath(from, to, options)` | method | — | Grid A\* from `from` to `to` (`{ x, y }` world points) around the same sprites `raycast` sees — **visible, non-`screenFixed`**, carrying a `collisionGroup` in `options.groups` (omit for any tagged sprite). Returns an array of `{ x, y }` waypoints ready for `sprite.followPath()`, or `null` when no route exists. Options below |
|
|
61
62
|
| `after(ms, callback)` | method | — | Runs `callback` once after `ms` of **game time** — the delay stretches under `timeScale` slow motion, freezes at `0` and pauses with the render loop, unlike `setTimeout`. Returns an int id. The callback receives `{ id }` |
|
|
62
63
|
| `every(ms, callback)` | method | — | Like `after()`, repeating until cancelled. Fires at most once per frame, and restarts its interval after a stall instead of bursting to catch up |
|
|
63
64
|
| `cancelTimer(id)` | method | — | Cancels a timer from `after()` / `every()` |
|
|
@@ -85,6 +86,31 @@ Vertical follow is always on. **Horizontal follow is off until you pass `leftMar
|
|
|
85
86
|
|
|
86
87
|
Each call **resets every option to its default** before applying what you passed. Calling `follow(sprite)` with no options after configuring it wipes the configuration.
|
|
87
88
|
|
|
89
|
+
### `findPath(from, to, options)`
|
|
90
|
+
|
|
91
|
+
| Option | Default | Effect |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| `cellSize` | `32` | Grid resolution in px. Match it to the tile size on a tile map so the grid lines up with the walls; a coarser grid is faster and blockier |
|
|
94
|
+
| `groups` | all tagged sprites | Array of `collisionGroup` names to treat as obstacles |
|
|
95
|
+
| `clearance` | `0` | Extra obstacle inflation in px. About half the walker's width keeps it from scraping corners, since the path is a line for the sprite's **center** |
|
|
96
|
+
| `bounds` | the whole surface | `{ minX, minY, maxX, maxY }` search rect — also the walkable area, so a floor clamp goes here |
|
|
97
|
+
| `diagonals` | `true` | Octile moves. Diagonals never cut a blocked corner |
|
|
98
|
+
| `simplify` | `true` | Line-of-sight string pulling: the cell chain collapses to the few corner waypoints `followPath` needs. `false` returns every grid cell (useful to visualize or debug the route) |
|
|
99
|
+
|
|
100
|
+
Waypoints come back with the **exact** start and goal as first and last point, so the walk begins under the sprite's feet and ends on the tapped pixel. A blocked start or goal snaps outward to the nearest free cell (up to 4 rings), which is why tapping an obstacle walks to its edge instead of returning `null`. `null` means: no route, degenerate `bounds`, `cellSize <= 0`, or a grid over ~1M cells (a runaway `bounds`/`cellSize` combination fails the query rather than allocating).
|
|
101
|
+
|
|
102
|
+
Everything is built per call on the calling thread — no persistent grid, no GL work. Like `raycast` it is a **discrete query**: run it on taps and AI timers (`gameView.every`), never per frame.
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
const path = gameView.findPath(
|
|
106
|
+
{ x: player.x, y: player.y },
|
|
107
|
+
{ x: e.x, y: e.y },
|
|
108
|
+
{ cellSize: TILE, groups: ['wall'], clearance: player.width * 0.35, bounds: WALK_BOUNDS });
|
|
109
|
+
if (path) {
|
|
110
|
+
player.followPath(path, { speed: 160 });
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
88
114
|
## SpriteSheet
|
|
89
115
|
|
|
90
116
|
Create one sheet per texture and share it across every sprite that uses it — sprites sharing a sheet batch into a single draw call.
|
|
@@ -421,6 +447,9 @@ There is `collision` (enter) and `collisionend` (exit), but deliberately **no**
|
|
|
421
447
|
- **`raycast` groups: pass an array, not varargs.** Android accepts both `raycast(x0, y0, x1, y1, ['enemy'])` and loose arguments; iOS reads only the array. Write the array and it works on both.
|
|
422
448
|
- **`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.
|
|
423
449
|
- **`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.
|
|
450
|
+
- **`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` and `hitboxShape: 'circle'` shape the walkable space too.
|
|
451
|
+
- **`findPath` returning `null` is not always "no route".** Degenerate `bounds`, `cellSize <= 0` and a grid over ~1M cells all return `null` as well — a `bounds` rect of the whole world at `cellSize: 4` fails silently that way. Check the numbers before blaming the maze.
|
|
452
|
+
- **A path of one or two points is the straight-line case, not a bug.** With nothing in the way `findPath` returns just `[start, goal]`; `path.length < 2` is the guard the demos use to detect a tap outside `bounds`.
|
|
424
453
|
- **Game-clock timers are not `setTimeout`.** `after`/`every` freeze at `timeScale: 0` and pause with the render loop — which is why spawn waves belong there — but a pause menu that needs a real countdown still needs `setTimeout`.
|
|
425
454
|
- **`screenFixed` beats `scrollFactor`.** Setting both leaves the sprite screen-fixed; the parallax offset is skipped entirely.
|
|
426
455
|
- **Ghost lines along a frame's edge are a filtering artifact, not bad art.** A magnified `smoothing: true` sheet samples past the frame border — 1px seams, or the next row's heads at the bottom. Grid sheets have inset UVs upstream since 2026-08-19; an atlas needs padding and extrude at pack time, and an older module build needs either `smoothing: false` or a rebuild.
|
|
@@ -22,8 +22,8 @@ There is no registry and no `ti module install` command — Titanium modules are
|
|
|
22
22
|
|
|
23
23
|
```
|
|
24
24
|
modules/
|
|
25
|
-
├── android/ti.game/0.
|
|
26
|
-
└── iphone/ti.game/0.
|
|
25
|
+
├── android/ti.game/0.4.0/
|
|
26
|
+
└── iphone/ti.game/0.4.0/
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Unzipping the module zip at the project root creates exactly that layout.
|
|
@@ -34,14 +34,14 @@ Then declare it in `tiapp.xml`. The iOS platform key is `iphone`, not `ios`:
|
|
|
34
34
|
|
|
35
35
|
```xml
|
|
36
36
|
<modules>
|
|
37
|
-
<module platform="android" version="0.
|
|
38
|
-
<module platform="iphone" version="0.
|
|
37
|
+
<module platform="android" version="0.4.0">ti.game</module>
|
|
38
|
+
<module platform="iphone" version="0.4.0">ti.game</module>
|
|
39
39
|
</modules>
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Pinning `version` is worth the extra attribute: with two module versions installed side by side, an unpinned entry silently picks the highest one.
|
|
43
43
|
|
|
44
|
-
The
|
|
44
|
+
The manifest went `0.3.0` → **`0.4.0`** on 2026-08-20, after months of feature landings under the old number — so `0.4.0` is the first version string that actually means something: it carries everything this skill documents. `0.3.0` does not, and there are several different `0.3.0` builds in the wild. One built before 2026-08-18 has no `createText`, no `createFont`, no `screenFixed`, no `swept` and no `collisionend`; one built before 2026-08-19 also lacks `followPath`, `play(name, { then })`, `raycast`, `after`/`every`, `scrollFactor` and the `multiply`/`screen` blend modes; one built before 2026-08-19 16:00 UTC+2 additionally bleeds at grid-frame edges on smoothed sheets. `findPath` exists only in `0.4.0`. If a call that this skill documents is `undefined` at runtime, check the manifest version first and rebuild from upstream `main` rather than hunting for a typo.
|
|
45
45
|
|
|
46
46
|
Building the module from source:
|
|
47
47
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# ti.game recipes by genre
|
|
2
2
|
|
|
3
|
-
Working patterns distilled from the
|
|
3
|
+
Working patterns distilled from the 25 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` as of 2026-08-
|
|
5
|
+
Every snippet uses only API that exists in upstream `main` as of 2026-08-20 (manifest `0.4.0`). Check [roadmap.md](roadmap.md) before reaching for anything not shown here.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
9
9
|
- [The scaffolding](#the-scaffolding)
|
|
10
|
-
- [Cross-cutting patterns](#cross-cutting-patterns) — pooling, parallax, multitouch controls, HUD text, invisible triggers, enter/exit zones, patrol routes, line of sight, game-clock timers, hit-stop
|
|
10
|
+
- [Cross-cutting patterns](#cross-cutting-patterns) — pooling, parallax, multitouch controls, HUD text, invisible triggers, enter/exit zones, patrol routes, line of sight, pathfinding, game-clock timers, hit-stop
|
|
11
11
|
- [Tap-to-flap](#tap-to-flap) (`flappy.js`)
|
|
12
12
|
- [Platformer](#platformer) (`platformer.js`)
|
|
13
13
|
- [Top-down / Zelda](#top-down--zelda) (`topdown.js`)
|
|
@@ -18,7 +18,7 @@ Every snippet uses only API that exists in upstream `main` as of 2026-08-19. Che
|
|
|
18
18
|
- [Rhythm game](#rhythm-game) (`rhythm.js`)
|
|
19
19
|
- [Ball sports / breakout physics](#ball-sports--breakout-physics) (`volley.js`)
|
|
20
20
|
- [Cards and board games](#cards-and-board-games) (`cards.js`, `puzzle.js`)
|
|
21
|
-
- [Point & click adventure](#point--click-adventure) (`pointclick.js`)
|
|
21
|
+
- [Point & click adventure](#point--click-adventure) (`pointclick.js`, `maze.js`)
|
|
22
22
|
- [Particles](#particles) (`particles.js`)
|
|
23
23
|
- [Ropes and chains](#ropes-and-chains) (`rope.js`)
|
|
24
24
|
- [Camera work](#camera-work) (`camera.js`)
|
|
@@ -389,6 +389,68 @@ if (!gameView.raycast(probeX, walker.y, probeX, walker.y + W * 0.12, ['ground'])
|
|
|
389
389
|
|
|
390
390
|
Run these from a coarse timer or a tap handler — the demo probes at 150 ms — never once per frame. A per-frame `raycast` from JS is exactly the bridge traffic the engine is built to avoid. Note that `raycast.js` drives its probes with `setInterval`: it was written two commits before game-clock timers existed, and `gameView.every` is the better home for an AI tick today.
|
|
391
391
|
|
|
392
|
+
### Pathfinding around obstacles — `maze.js`, `pointclick.js`
|
|
393
|
+
|
|
394
|
+
`gameView.findPath(from, to, options)` is grid A\* over the same sprites `raycast` sees: visible, tagged with a `collisionGroup`, nothing else to set up. It returns `{ x, y }` waypoints — or `null` — and those waypoints go straight into `followPath`, so the walk itself runs natively.
|
|
395
|
+
|
|
396
|
+
```javascript
|
|
397
|
+
gameView.addEventListener('tap', (e) => {
|
|
398
|
+
const path = gameView.findPath(
|
|
399
|
+
{ x: player.x, y: player.y },
|
|
400
|
+
{ x: e.x, y: e.y },
|
|
401
|
+
{ cellSize: TILE, groups: ['wall'], bounds: MAZE_BOUNDS });
|
|
402
|
+
if (!path || path.length < 2) {
|
|
403
|
+
return; // tapped outside the walkable rect
|
|
404
|
+
}
|
|
405
|
+
player.scaleX = path[1].x < player.x ? -1 : 1; // face the first leg
|
|
406
|
+
player.play('walk');
|
|
407
|
+
player.followPath(path, { speed: SPEED });
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
`cellSize` is the one option that decides everything else. On a tile map, pass the tile size and the grid lines up with the walls exactly; on a free-form scene, something near the walker's width. Too coarse and doorways disappear, too fine and the query gets expensive (over ~1M cells it returns `null` rather than allocating).
|
|
412
|
+
|
|
413
|
+
**The path is a line for the sprite's *center*.** A body wider than a cell scrapes corners unless you pass `clearance` — about half the walker's width:
|
|
414
|
+
|
|
415
|
+
```javascript
|
|
416
|
+
const path = gameView.findPath(from, to, {
|
|
417
|
+
cellSize: Math.round(W * 0.04),
|
|
418
|
+
groups: ['obstacle'],
|
|
419
|
+
clearance: PLAYER_W * 0.35, // keep half a body off the trunk
|
|
420
|
+
bounds: WALK_BOUNDS // the walkable floor, in center coordinates
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
`bounds` doubles as the walkable area, which is how the point-&-click floor clamp survives: `WALK_BOUNDS` is the old `WALK_TOP`/margin clamp expressed as a rect, in **sprite-center coordinates** (tap points are feet positions, so subtract half the sprite height before searching).
|
|
425
|
+
|
|
426
|
+
**Obstacles do not have to be the art.** `pointclick.js` blocks only the oak's trunk with an invisible sprite — `opacity: 0`, `touchEnabled: false`, no collision wiring — because `findPath` reads the hitbox, not the pixels. The canopy stays walkable, so the player still passes behind the tree with `ySort` doing the depth:
|
|
427
|
+
|
|
428
|
+
```javascript
|
|
429
|
+
gameView.add(Game.createSprite({
|
|
430
|
+
sheet: treeSheet, opacity: 0, touchEnabled: false,
|
|
431
|
+
x: tree.x, y: groundY, width: TREE_W * 0.35, height: PLAYER_H * 0.8,
|
|
432
|
+
collisionGroup: 'obstacle'
|
|
433
|
+
}));
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
A blocked goal snaps to the nearest free cell a few rings out, so tapping a wall walks to its edge instead of doing nothing. Tapping outside `bounds` is the case worth guarding — that is what `path.length < 2` catches.
|
|
437
|
+
|
|
438
|
+
**Chasing AI is a timer, not a frame loop.** Re-path on a coarse game-clock tick and let `followPath` cover the gap:
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
const chase = gameView.every(800, () => {
|
|
442
|
+
const path = gameView.findPath({ x: hound.x, y: hound.y }, { x: player.x, y: player.y }, PATH_OPTIONS);
|
|
443
|
+
if (path && path.length > 1) {
|
|
444
|
+
hound.followPath(path, { speed: TILE * 2.6 });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
win.addEventListener('close', () => gameView.cancelTimer(chase));
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`followPath(null)` cancels an in-flight route — `maze.js` uses it to yank the hound back to its den after it catches you.
|
|
451
|
+
|
|
452
|
+
To *see* what A\* did, ask twice: `simplify: false` returns every grid cell it walked, `simplify: true` (the default) the line-of-sight-reduced corners the sprite actually gets. Dropping a dot on each is the whole route visualization in the maze demo.
|
|
453
|
+
|
|
392
454
|
### Timers on the game clock
|
|
393
455
|
|
|
394
456
|
`gameView.after(ms, cb)` and `every(ms, cb)` run on the engine's clock: they stretch under `timeScale` slow motion, freeze at `0`, and pause with the render loop. Spawn waves, AI decision ticks and respawn delays belong here — a paused game that keeps spawning is the bug this fixes.
|
|
@@ -1135,31 +1197,37 @@ Because every handler is per sprite, this is multi-touch for free: each finger g
|
|
|
1135
1197
|
|
|
1136
1198
|
## Point & click adventure
|
|
1137
1199
|
|
|
1138
|
-
Tap-to-walk
|
|
1200
|
+
Tap-to-walk routes around the scenery with `findPath` and walks the waypoints natively:
|
|
1139
1201
|
|
|
1140
1202
|
```javascript
|
|
1203
|
+
const WALK_BOUNDS = { // the floor, in sprite-CENTER coordinates
|
|
1204
|
+
minX: W * 0.06, maxX: W * 0.94,
|
|
1205
|
+
minY: WALK_TOP - PLAYER_H / 2, maxY: H * 0.95 - PLAYER_H / 2
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1141
1208
|
function walkTo(x, y) {
|
|
1142
|
-
const targetX = Math.min(Math.max(x,
|
|
1143
|
-
const targetY = Math.min(Math.max(y
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1209
|
+
const targetX = Math.min(Math.max(x, WALK_BOUNDS.minX), WALK_BOUNDS.maxX);
|
|
1210
|
+
const targetY = Math.min(Math.max(y - PLAYER_H / 2, WALK_BOUNDS.minY), WALK_BOUNDS.maxY);
|
|
1211
|
+
const path = gameView.findPath({ x: player.x, y: player.y }, { x: targetX, y: targetY }, {
|
|
1212
|
+
cellSize: Math.round(W * 0.04),
|
|
1213
|
+
groups: ['obstacle'],
|
|
1214
|
+
clearance: PLAYER_W * 0.35, // keep half a body off the trunk
|
|
1215
|
+
bounds: WALK_BOUNDS
|
|
1216
|
+
});
|
|
1217
|
+
if (!path || path.length < 2) {
|
|
1218
|
+
return; // tapped outside the floor
|
|
1147
1219
|
}
|
|
1148
|
-
player.scaleX =
|
|
1149
|
-
player.clearTweens(); // cancel a walk in progress
|
|
1220
|
+
player.scaleX = path[1].x < player.x ? -1 : 1; // face the first leg
|
|
1150
1221
|
player.play('walk');
|
|
1151
|
-
player.
|
|
1152
|
-
x: targetX,
|
|
1153
|
-
y: targetY,
|
|
1154
|
-
duration: distance / WALK_SPEED * 1000, // constant px/s
|
|
1155
|
-
easing: Game.EASE_LINEAR // no ease on a walk
|
|
1156
|
-
});
|
|
1222
|
+
player.followPath(path, { speed: WALK_SPEED }); // constant px/s, cancels the previous route
|
|
1157
1223
|
}
|
|
1158
1224
|
|
|
1159
|
-
player.addEventListener('
|
|
1225
|
+
player.addEventListener('pathcomplete', () => player.play('idle'));
|
|
1160
1226
|
```
|
|
1161
1227
|
|
|
1162
|
-
The walkable floor is a clamp (`
|
|
1228
|
+
The walkable floor is still a clamp, not geometry — but it is now expressed as the `bounds` rect the search runs in. Obstacles are separate: `pointclick.js` adds an invisible box over the oak's *trunk* only (`opacity: 0`, `touchEnabled: false`, `collisionGroup: 'obstacle'`), so the player circles the trunk and still walks behind the canopy with `ySort`. See [Pathfinding around obstacles](#pathfinding-around-obstacles--mazejs-pointclickjs) for the options.
|
|
1229
|
+
|
|
1230
|
+
With nothing in the scene to route around, `findPath` returns `[start, goal]` and `followPath` walks the straight line — the same result as the distance-sized `animate()` tween this demo used before, with one less speed calculation to get wrong.
|
|
1163
1231
|
|
|
1164
1232
|
**Hotspots and the tap conflict.** The GameView fires `tap` for *every* touch, including ones a sprite already handled, so a naive walk handler also walks when you click the bird. Hit-test the interactive sprites in JS and bail out:
|
|
1165
1233
|
|
|
@@ -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-20** (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
|
|
|
@@ -8,10 +8,9 @@ Planned items come from the module's `TODO.md`. Planned is not shipped — when
|
|
|
8
8
|
|
|
9
9
|
| You might reach for | Status | What to do now |
|
|
10
10
|
| --- | --- | --- |
|
|
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 |
|
|
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
12
|
| `sprite.parent`, attaching a turret to a tank, a hat to a hero, multi-part bosses | Planned (priority 7), the biggest structural item | Move the parts together from JS on the same coarse tick, or bake the combination into sheet frames. There is no transform inheritance |
|
|
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
|
-
| `gameView.findPath(from, to)` A* over a collision grid | Planned (priority 4) | Hand-author the waypoint arrays and hand them to `followPath`. Straight-line walking is what the point-&-click and topdown demos do |
|
|
15
14
|
| `maxWidth` word wrap on text sprites | Planned (priority 5) | Break the lines yourself with `\n`. Measure with the font's `charWidth` on a monospace grid font; a proportional font needs a per-glyph tally |
|
|
16
15
|
| 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 |
|
|
17
16
|
| 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 |
|
|
@@ -57,7 +56,8 @@ Recent additions that are easy to miss and easy to reimplement badly in JS:
|
|
|
57
56
|
- **Game-clock timers** (2026-08-19): `gameView.after(ms, cb)` / `every(ms, cb)` / `cancelTimer(id)` obey `timeScale` and pause with the render loop. Spawn waves and AI ticks belong here, not in `setTimeout`.
|
|
58
57
|
- **`scrollFactor`** (2026-08-19): per-sprite parallax against camera travel. A background layer is one property, not a hand-scrolled pair of copies.
|
|
59
58
|
- **`blend: 'multiply'` and `'screen'`** (2026-08-19) join `'normal'` and `'add'`, on sprites and emitters alike.
|
|
59
|
+
- **`gameView.findPath(from, to, options)`** (2026-08-20): grid A* around the sprites carrying a `collisionGroup`, returning waypoints ready for `followPath` — the Godot `AStar2D` / GameMaker `mp_grid` equivalent. Tap-to-walk and chasing AI route around obstacles natively now; hand-authored waypoint arrays and straight-line walking are the old workaround. Discrete like `raycast`: taps and AI timers, not per frame.
|
|
60
60
|
|
|
61
61
|
## Checking the current state
|
|
62
62
|
|
|
63
|
-
This file describes upstream `main` on 2026-08-
|
|
63
|
+
This file describes upstream `main` on 2026-08-20 — a moving target: nine items on this list shipped within four days of the skill being written, four of them in a single day, and the manifest only caught up at the end (`0.3.0` → `0.4.0` on 2026-08-20, after all of them). 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.
|