@maccesar/titools 4.14.0 → 4.16.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 +7 -1
- package/package.json +1 -1
- package/skills/ti-game/SKILL.md +80 -180
- package/skills/ti-game/references/api.md +83 -18
- package/skills/ti-game/references/debugging-performance.md +103 -0
- package/skills/ti-game/references/examples.md +54 -0
- package/skills/ti-game/references/patterns.md +699 -0
- package/skills/ti-game/references/project-setup.md +21 -11
- package/skills/ti-game/references/recipes-action.md +633 -0
- package/skills/ti-game/references/recipes-adventure-effects.md +340 -0
- package/skills/ti-game/references/roadmap.md +44 -63
- package/skills/ti-game/references/tilemaps.md +125 -0
- package/skills/ti-game/references/recipes.md +0 -1574
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Adventure, presentation, and effects recipes
|
|
2
|
+
|
|
3
|
+
Production-oriented patterns distilled from the upstream non-action demos at `ti.game@d587081`. Use [examples.md](examples.md) to locate the full runnable demo and [api.md](api.md) for exact defaults and payloads.
|
|
4
|
+
|
|
5
|
+
<!-- TOC-START -->
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- [Cards and board games](#cards-and-board-games)
|
|
9
|
+
- [Point & click adventure](#point--click-adventure)
|
|
10
|
+
- [Particles](#particles)
|
|
11
|
+
- [Ropes and chains](#ropes-and-chains)
|
|
12
|
+
- [Camera work](#camera-work)
|
|
13
|
+
- [Visual effects](#visual-effects)
|
|
14
|
+
|
|
15
|
+
<!-- TOC-END -->
|
|
16
|
+
|
|
17
|
+
## Cards and board games
|
|
18
|
+
|
|
19
|
+
No physics at all — sprite taps, native tweens, glow and idle wobble.
|
|
20
|
+
|
|
21
|
+
**Dealing** staggers one tween per card with `delay`, and flips the art on arrival:
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
cards.forEach((card, index) => {
|
|
25
|
+
card.state = 'dealing';
|
|
26
|
+
card.sprite.idleAnimation = false; // still cards land exactly on target
|
|
27
|
+
card.sprite.frame = BACK_FRAME;
|
|
28
|
+
card.sprite.x = DECK_X;
|
|
29
|
+
card.sprite.y = DECK_Y;
|
|
30
|
+
card.sprite.zIndex = 30 + index; // above the deck while flying
|
|
31
|
+
card.sprite.animate({
|
|
32
|
+
x: card.home.x,
|
|
33
|
+
y: card.home.y,
|
|
34
|
+
rotation: card.home.rotation,
|
|
35
|
+
duration: 380,
|
|
36
|
+
delay: index * 140, // one card after another
|
|
37
|
+
easing: Game.EASE_IN_OUT
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
sprite.addEventListener('complete', () => {
|
|
42
|
+
if (card.state === 'dealing') {
|
|
43
|
+
card.state = 'hand';
|
|
44
|
+
sprite.frame = card.face; // flip face up on arrival
|
|
45
|
+
sprite.zIndex = card.index;
|
|
46
|
+
sprite.idleAnimation = true; // wobble only once it has settled
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Selection** lifts the card and fades a glow in — `glowOpacity` is tweenable, so the halo appears with the movement instead of popping:
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
sprite.addEventListener('tap', () => {
|
|
55
|
+
if (card.selected) {
|
|
56
|
+
card.selected = false;
|
|
57
|
+
sprite.animate({ x: home.x, y: home.y, rotation: home.rotation, scale: 1, glowOpacity: 0, duration: 150, easing: Game.EASE_OUT });
|
|
58
|
+
} else if (selectedCards().length < MAX_SELECTED) {
|
|
59
|
+
card.selected = true;
|
|
60
|
+
sprite.glowColor = '#ffc94d';
|
|
61
|
+
sprite.glowBlur = CARD_W * 0.14;
|
|
62
|
+
sprite.glowOpacity = 0;
|
|
63
|
+
sprite.animate({ y: home.y - LIFT, scale: 1.12, glowOpacity: 1, duration: 150, easing: Game.EASE_OUT });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Drag & drop with snapping** (puzzle pieces, board tiles). The drag itself is native; you only decide where it lands:
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
const piece = Game.createSprite({ sheet: sheet, frame: index, x: homeX, y: homeY, width: PIECE, height: PIECE, draggable: true, zIndex: 10 });
|
|
72
|
+
|
|
73
|
+
piece.addEventListener('press', () => {
|
|
74
|
+
piece.zIndex = 100; // draw above everything while held
|
|
75
|
+
piece.animate({ scale: 1.15, duration: 120, easing: Game.EASE_OUT });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
piece.addEventListener('release', () => {
|
|
79
|
+
piece.animate({ scale: 1, duration: 120, easing: Game.EASE_OUT });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
piece.addEventListener('dragend', (e) => {
|
|
83
|
+
let best = null;
|
|
84
|
+
let bestDistance = SNAP_DISTANCE;
|
|
85
|
+
cells.forEach((cell) => {
|
|
86
|
+
if (cell.piece) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const d = Math.sqrt(Math.pow(e.x - cell.x, 2) + Math.pow(e.y - cell.y, 2));
|
|
90
|
+
if (d < bestDistance) {
|
|
91
|
+
bestDistance = d;
|
|
92
|
+
best = cell;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
piece.zIndex = 10;
|
|
96
|
+
if (best) {
|
|
97
|
+
best.piece = piece;
|
|
98
|
+
piece.animate({ x: best.x, y: best.y, duration: 150, easing: Game.EASE_OUT });
|
|
99
|
+
} else {
|
|
100
|
+
piece.animate({ x: homeX, y: homeY, duration: 400, easing: Game.EASE_IN_OUT });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Because every handler is per sprite, this is multi-touch for free: each finger grabs its own piece.
|
|
106
|
+
|
|
107
|
+
## Point & click adventure
|
|
108
|
+
|
|
109
|
+
Tap-to-walk routes around the scenery with `findPath` and walks the waypoints natively:
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
const WALK_BOUNDS = { // the floor, in sprite-CENTER coordinates
|
|
113
|
+
minX: W * 0.06, maxX: W * 0.94,
|
|
114
|
+
minY: WALK_TOP - PLAYER_H / 2, maxY: H * 0.95 - PLAYER_H / 2
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function walkTo(x, y) {
|
|
118
|
+
const targetX = Math.min(Math.max(x, WALK_BOUNDS.minX), WALK_BOUNDS.maxX);
|
|
119
|
+
const targetY = Math.min(Math.max(y - PLAYER_H / 2, WALK_BOUNDS.minY), WALK_BOUNDS.maxY);
|
|
120
|
+
const path = gameView.findPath({ x: player.x, y: player.y }, { x: targetX, y: targetY }, {
|
|
121
|
+
cellSize: Math.round(W * 0.04),
|
|
122
|
+
groups: ['obstacle'],
|
|
123
|
+
clearance: PLAYER_W * 0.35, // keep half a body off the trunk
|
|
124
|
+
bounds: WALK_BOUNDS
|
|
125
|
+
});
|
|
126
|
+
if (!path || path.length < 2) {
|
|
127
|
+
return; // tapped outside the floor
|
|
128
|
+
}
|
|
129
|
+
player.scaleX = path[1].x < player.x ? -1 : 1; // face the first leg
|
|
130
|
+
player.play('walk');
|
|
131
|
+
player.followPath(path, { speed: WALK_SPEED }); // constant px/s, cancels the previous route
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
player.addEventListener('pathcomplete', () => player.play('idle'));
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
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](patterns.md#pathfinding-around-obstacles--mazejs-pointclickjs) for the options.
|
|
138
|
+
|
|
139
|
+
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.
|
|
140
|
+
|
|
141
|
+
**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:
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
function over(sprite, x, y) {
|
|
145
|
+
return sprite.visible !== false
|
|
146
|
+
&& Math.abs(x - sprite.x) <= sprite.width / 2 + PAD
|
|
147
|
+
&& Math.abs(y - sprite.y) <= sprite.height / 2 + PAD;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
gameView.addEventListener('tap', (e) => {
|
|
151
|
+
if (over(bird, e.x, e.y) || over(handIcon, e.x, e.y) || over(talkIcon, e.x, e.y)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
hideIcons();
|
|
155
|
+
walkTo(e.x, e.y);
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
A verb coin is two icon sprites with `visible` toggled and `idleAnimation: true` for a bit of life while open.
|
|
160
|
+
|
|
161
|
+
## Particles
|
|
162
|
+
|
|
163
|
+
Three modes, all native. Tint white art at runtime so one 16×16 texture covers every effect in the game.
|
|
164
|
+
|
|
165
|
+
**Continuous fountain**:
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
const fountain = Game.createEmitter({
|
|
169
|
+
sheet: sparkSheet, frame: 1,
|
|
170
|
+
x: W / 2, y: H - UNIT * 0.05,
|
|
171
|
+
rate: 90,
|
|
172
|
+
lifetime: 1600,
|
|
173
|
+
speed: H * 0.75, // launch up...
|
|
174
|
+
angle: 0, // 0 = up, clockwise degrees
|
|
175
|
+
spread: 25,
|
|
176
|
+
gravity: H * 0.45, // ...and fall back
|
|
177
|
+
size: UNIT * 0.03,
|
|
178
|
+
startScale: 1, endScale: 0.4,
|
|
179
|
+
startOpacity: 1, endOpacity: 0,
|
|
180
|
+
tint: '#ffcc44',
|
|
181
|
+
maxParticles: 400
|
|
182
|
+
});
|
|
183
|
+
gameView.add(fountain);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Burst on demand** — `rate: 0`, position it and call `emit(n)`:
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
189
|
+
const firework = Game.createEmitter({
|
|
190
|
+
sheet: sparkSheet, frame: 1,
|
|
191
|
+
rate: 0,
|
|
192
|
+
lifetime: 900,
|
|
193
|
+
speed: UNIT * 0.6,
|
|
194
|
+
spread: 360,
|
|
195
|
+
gravity: H * 0.25,
|
|
196
|
+
startScale: 1, endScale: 0.3,
|
|
197
|
+
maxParticles: 500
|
|
198
|
+
});
|
|
199
|
+
gameView.add(firework);
|
|
200
|
+
|
|
201
|
+
gameView.addEventListener('tap', (e) => {
|
|
202
|
+
firework.x = e.x;
|
|
203
|
+
firework.y = e.y;
|
|
204
|
+
firework.tint = COLORS[colorIndex++ % COLORS.length];
|
|
205
|
+
firework.emit(45);
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Trail following a sprite** — `target` keeps up even mid-drag, with no bridge traffic:
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
const dust = Game.createEmitter({
|
|
213
|
+
sheet: sparkSheet, frame: 0,
|
|
214
|
+
target: player,
|
|
215
|
+
offsetX: -PLAYER * 0.32, // behind the rear wheels
|
|
216
|
+
offsetY: PLAYER * 0.42,
|
|
217
|
+
rate: 22,
|
|
218
|
+
lifetime: 450,
|
|
219
|
+
speed: W * 0.08,
|
|
220
|
+
angle: -35, // up and back
|
|
221
|
+
spread: 50,
|
|
222
|
+
startOpacity: 0.45, endOpacity: 0,
|
|
223
|
+
tint: '#8a8580',
|
|
224
|
+
zIndex: 9 // behind the player
|
|
225
|
+
});
|
|
226
|
+
gameView.add(dust);
|
|
227
|
+
|
|
228
|
+
dust.emitting = false; // the one property JS toggles at runtime
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Add `blend: 'add'` for fire, magic and sparks — overlaps bloom instead of covering.
|
|
232
|
+
|
|
233
|
+
## Ropes and chains
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
// hangs from a draggable ball
|
|
237
|
+
const ball = Game.createSprite({ sheet: ballSheet, x: W / 2, y: H * 0.22, width: BALL, height: BALL, zIndex: 10, draggable: true, hitboxShape: 'circle' });
|
|
238
|
+
gameView.add(ball);
|
|
239
|
+
|
|
240
|
+
gameView.add(Game.createRope({
|
|
241
|
+
sheet: ropeSheet,
|
|
242
|
+
segments: 14,
|
|
243
|
+
segmentLength: H * 0.04,
|
|
244
|
+
thickness: W * 0.03,
|
|
245
|
+
gravity: H * 1.6,
|
|
246
|
+
damping: 0.985,
|
|
247
|
+
iterations: 3,
|
|
248
|
+
head: ball, // pinned to the sprite — drag it and the rope follows
|
|
249
|
+
zIndex: 5
|
|
250
|
+
}));
|
|
251
|
+
|
|
252
|
+
// fixed anchor + a weight on a tether: drag it too far and it is pulled back
|
|
253
|
+
gameView.add(Game.createRope({
|
|
254
|
+
sheet: ropeSheet,
|
|
255
|
+
segments: 10,
|
|
256
|
+
segmentLength: SEG_LEN,
|
|
257
|
+
gravity: H * 1.6,
|
|
258
|
+
x: W * 0.15, y: H * 0.1, // fixed head anchor
|
|
259
|
+
tail: weight,
|
|
260
|
+
maxLength: 10 * SEG_LEN, // wrecking balls, leashes, yo-yos
|
|
261
|
+
zIndex: 5
|
|
262
|
+
}));
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
With sprites on both ends plus `maxLength`, dragging either one tows the other once the rope goes taut — carts, chained crates, a grappling hook whose tip you read from `rope.endX` / `rope.endY`.
|
|
266
|
+
|
|
267
|
+
## Camera work
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
gameView.follow(player, {
|
|
271
|
+
leftMargin: 0.4, rightMargin: 0.6, // horizontal follow is off without these
|
|
272
|
+
topMargin: 0.4, bottomMargin: 0.6,
|
|
273
|
+
smoothing: 0.12, // 0 snaps; higher eases
|
|
274
|
+
maxY: WORLD_H // default 0 clamps to the top of the world
|
|
275
|
+
});
|
|
276
|
+
gameView.cameraBounds = { minX: 0, minY: 0, maxX: WORLD_W, maxY: WORLD_H };
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Sprites live in world coordinates and touch input is mapped back to world space automatically, zoom included — taps and drags keep working while scrolled. Overlaid Titanium controls are screen-fixed and unaffected.
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
gameView.cameraScale = Math.min(2.5, Math.max(0.5, gameView.cameraScale * 1.25)); // zoom
|
|
283
|
+
gameView.shake({ strength: W * 0.02, duration: 500 }); // impact
|
|
284
|
+
gameView.cameraEffect = 'tint'; // night vision
|
|
285
|
+
gameView.cameraTint = '#4f8';
|
|
286
|
+
gameView.cameraEffectIntensity = 0.7;
|
|
287
|
+
gameView.cameraEffect = 'glitch'; // broken signal
|
|
288
|
+
gameView.cameraEffect = 'none'; // skips the pass entirely
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
For a big ground plane, one `tileRepeat` sprite covering the whole world beats a grid of tile sprites — the texture keeps its native density and the scene stays one draw call.
|
|
292
|
+
|
|
293
|
+
## Visual effects
|
|
294
|
+
|
|
295
|
+
**Blend modes** — four, on sprites and emitters alike. `blend.js` shows all of them side by side over a bright meadow strip:
|
|
296
|
+
|
|
297
|
+
| Mode | Does | Use for |
|
|
298
|
+
| --- | --- | --- |
|
|
299
|
+
| `'normal'` | Covers what is behind it | Everything else |
|
|
300
|
+
| `'add'` | Sums toward white | Glows, fire, lasers, light shafts, sparks |
|
|
301
|
+
| `'multiply'` | Darkens the backdrop | Contact shadows under sprites, stains, grime, smoke that dims |
|
|
302
|
+
| `'screen'` | Lightens without blowing out | Fog, haze, god rays, soft light |
|
|
303
|
+
|
|
304
|
+
Group same-blend sprites on their own `zIndex` band: every mode change in draw order costs a batch flush, so alternating them sprite by sprite degrades toward one draw call each. Unknown strings fall back to `'normal'` in silence and the names are case-sensitive — `blend: 'Add'` renders as normal with no warning.
|
|
305
|
+
|
|
306
|
+
**`flash(color, duration)`** — a solid-color silhouette that fades out natively. This is the damage flash a multiplicative `tintColor` cannot produce (white tint = no change). Retrigger it on a timer for invincibility blinking.
|
|
307
|
+
|
|
308
|
+
**`flipX` / `flipY` from movement** — mirror the drawn frame without touching physics:
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
// tween patrol: flip on each turn-around
|
|
312
|
+
function patrol() {
|
|
313
|
+
const goingRight = bird.x < W / 2;
|
|
314
|
+
bird.flipX = !goingRight;
|
|
315
|
+
bird.animate({ x: goingRight ? W - MARGIN : MARGIN, duration: 3000, easing: Game.EASE_IN_OUT });
|
|
316
|
+
}
|
|
317
|
+
bird.addEventListener('complete', patrol);
|
|
318
|
+
|
|
319
|
+
// velocity runner: a slow watchdog turns it around, never moves it
|
|
320
|
+
timer = setInterval(() => {
|
|
321
|
+
[dog, player].forEach((runner) => {
|
|
322
|
+
if (runner.velocityX > 0 && runner.x > W - MARGIN) {
|
|
323
|
+
runner.velocityX = -runner.velocityX;
|
|
324
|
+
} else if (runner.velocityX < 0 && runner.x < MARGIN) {
|
|
325
|
+
runner.velocityX = -runner.velocityX;
|
|
326
|
+
}
|
|
327
|
+
runner.flipX = runner.velocityX < 0;
|
|
328
|
+
});
|
|
329
|
+
}, 150);
|
|
330
|
+
|
|
331
|
+
// VVVVVV-style gravity inversion
|
|
332
|
+
gameView.addEventListener('tap', () => {
|
|
333
|
+
player.gravity = -player.gravity;
|
|
334
|
+
player.flipY = player.gravity < 0;
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**`timeScale`** — one property slows or freezes physics, sheet animations, tweens, particles, the camera and the game-clock timers together, while rendering and touch keep running. `0.5` for slow motion, `0` for a pause that still draws, short bursts of `0` for hit-stop. `timescale.js` makes the timer half visible: a clock on `gameView.every(1000, ...)` freezes with the scene while a `setInterval` clock beside it keeps ticking.
|
|
339
|
+
|
|
340
|
+
**`idleAnimation`** — a per-sprite organic sway that composes with tweens and drags. Cheap life for cards, icons, floating pickups and menu art. Disable it before a tween that must land precisely.
|
|
@@ -1,68 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Missing, partial, and out-of-scope features
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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.
|
|
3
|
+
Verified against `ti.game@d587081` (2026-08-28), especially upstream `TODO.md` and both native implementations. Planned is not shipped; partial means the exact boundary below matters.
|
|
6
4
|
|
|
7
5
|
## Not available today
|
|
8
6
|
|
|
9
|
-
|
|
|
7
|
+
| Tempting API or capability | Current status | Safe approach |
|
|
10
8
|
| --- | --- | --- |
|
|
11
|
-
|
|
|
12
|
-
| `sprite.parent`
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
| Tweening `
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
- **`collisionend`**: the exit half of the trigger lifecycle, including when the partner is removed, hidden or re-tagged mid-contact.
|
|
53
|
-
- **`followPath(points, opts)` and `play(name, { then })`** (2026-08-19): patrol routes, bullet arcs and animation chains run natively. Do not chain `animate()` legs from `complete`, and do not re-`play()` from an `animationcomplete` handler.
|
|
54
|
-
- **`gameView.raycast(x0, y0, x1, y1, groups)`** (2026-08-19): nearest hit as `{ x, y, distance, group, sprite, normal }`, or `null`. Line of sight, ledge probes and hitscan no longer need an invisible probe sprite — but it is a discrete query, not something to poll every frame.
|
|
55
|
-
- **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`.
|
|
56
|
-
- **`scrollFactor`** (2026-08-19): per-sprite parallax against camera travel. A background layer is one property, not a hand-scrolled pair of copies.
|
|
57
|
-
- **`blend: 'multiply'` and `'screen'`** (2026-08-19) join `'normal'` and `'add'`, on sprites and emitters alike.
|
|
58
|
-
- **`gameView.findPath(from, to, options)`** (2026-08-20): grid A* around the sprites carrying a `collisionGroup`, returning waypoints ready for `followPath` — the Godot `AStar2D` / GameMaker `mp_grid` equivalent. Tap-to-walk and chasing AI route around obstacles natively now; hand-authored waypoint arrays and straight-line walking are the old workaround. Discrete like `raycast`: taps and AI timers, not per frame.
|
|
59
|
-
- **Text word wrap** (2026-08-23): `maxWidth` on a text sprite breaks lines on word boundaries and re-wraps whenever `text` changes, so a dialog box is one long string instead of hand-broken `\n` lines. The width is in font-space px, before `scale`. Do not write a JS line-breaker any more.
|
|
60
|
-
- **`hitboxScaleX` / `hitboxScaleY`** (2026-08-23): per-axis corrections multiplied on top of `hitboxScale`, for art that fills its frame by a different fraction on each axis. The old workaround — settling for one compromise scale, or padding the frame so a single number fits — is obsolete.
|
|
61
|
-
- **`spriteSheet.unload()` / `font.unload()`** (2026-08-23): frees a GL texture on the next frame instead of holding it until the context dies. Level streaming no longer needs a fresh window to reclaim GPU memory. Permanent — it is not a cache eviction.
|
|
62
|
-
- **LiveView reloads retire the previous generation** (2026-08-23): stale render loops, the old `SoundPool`, `MediaPlayer`s and audio proxies are shut down by the module itself. Do not write your own reload guard, and do not read a scene stuttering after several reloads as an engine limit — it dates the build.
|
|
63
|
-
- **`sprite.attachTo(target, { offsetX, offsetY, rotate })` / `detach()`** (2026-08-26): a sprite pinned to another sprite, resolved natively after physics and before collisions. Name tags, health bars, shadows, turrets and hitboxes that must sit on a moving body stop needing a per-frame JS copy of `x`/`y` — which is what everyone wrote before, one bridge round trip per tag per tick, always a frame late. Chains, `rotate: true` for an offset that swings with the target, automatic conversion between world and `screenFixed` space, and recursive removal so a tag never outlives its owner. 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.
|
|
65
|
-
|
|
66
|
-
## Checking the current state
|
|
67
|
-
|
|
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.
|
|
9
|
+
| `gameView.panTo(x, y, options)` | Planned | Tween an invisible sprite, follow it for the cutscene, then follow the player again |
|
|
10
|
+
| Full `sprite.parent` transform inheritance | Partial | `attachTo()` supplies position, optional rotation, and inherited opacity. Scale, visibility, flips, tint, hit-test hierarchy, y-sort hierarchy, and a `parent` property do not exist |
|
|
11
|
+
| Native joystick/d-pad binding | Planned | Use overlaid Titanium controls that write native motion/input properties on `touchstart`/`touchend`; sibling views support simultaneous touches |
|
|
12
|
+
| Gamepad support | Planned | Touch input only unless the app adds its own native integration |
|
|
13
|
+
| Complete platformer slopes | Partial | `rotatedRect` geometry, normals, rider carrying, and sliding work. Smooth uphill walking, contact-only surface friction, and connected terrain are not implemented |
|
|
14
|
+
| Tile animation | Planned TileLayer follow-up | Change ids deliberately or use sprites for the animated cells; there is no per-id native frame cycle |
|
|
15
|
+
| `raycast()` against TileLayer cells | Planned TileLayer follow-up | Use sprite colliders for ray-visible obstacles or grid/cell queries appropriate to the game |
|
|
16
|
+
| Tile trigger events (`collision`/`collisionend` for water/lava) | Planned TileLayer follow-up | Use sparse invisible trigger sprites or map the mover/world point to cells on a coarse decision/event path |
|
|
17
|
+
| One-call Tiled loader for tilesets and multiple layers | Planned TileLayer follow-up | Parse the exported JSON in app code and create one TileLayer per layer/tileset |
|
|
18
|
+
| Per-frame animation events | Planned | Split an action into short non-looping animations and react to `animationcomplete`, or use an engine timer aligned to a deliberate cadence |
|
|
19
|
+
| Ping-pong, repeat/yoyo, random animation offset, per-sprite animation speed | Planned | Author ping-pong frames or re-launch a tween from `complete`; avoid a JS per-frame loop |
|
|
20
|
+
| `playbackRate`, pitch jitter, stereo pan, `fadeTo()` | Planned | Current sound API is `volume`, `loop`, `play`, `pause`, `stop`; use prepared variants or a game-clock volume cadence when necessary |
|
|
21
|
+
| TypeScript declarations | Planned | No upstream `ti.game.d.ts` exists |
|
|
22
|
+
| Aseprite JSON import | Planned | SpriteSheet atlases support TexturePacker JSON hash/array formats |
|
|
23
|
+
| Tweening `width`, `height`, or `tintColor` | Not implemented | Tween scale/opacity/glowOpacity; other properties are instant writes |
|
|
24
|
+
| A GameView-level `stop()` | Already covered differently | `timeScale = 0` freezes engine ticks while rendering/touch continue; `pause()` stops the render loop |
|
|
25
|
+
| Standard Titanium touch events on GameView | By design | Use GameView `press`/`tap`/`release`, sprite events, or Titanium views over the canvas |
|
|
26
|
+
| A per-frame JS event | Deliberately absent | React to discrete events and use native movement; coarse `every()` calls are for decisions, not motion |
|
|
27
|
+
|
|
28
|
+
## Already shipped — do not recreate in JS
|
|
29
|
+
|
|
30
|
+
- Native `TileLayer`: visible-cell drawing, strings/flat/nested data, Tiled GID offset, solid/one-way cells, live edits, cell lookup, parallax, tint/opacity, and `findPath` participation.
|
|
31
|
+
- Debug object form, screen-space performance HUD, optional bitmap `hudFont`, and the opt-in `performance` event.
|
|
32
|
+
- Camera follow on both axes, bounds, smoothing, zoom, shake, fullscreen tint/glitch, and per-object `scrollFactor`.
|
|
33
|
+
- Text/Font sprites, word wrap, `screenFixed`, attachment, named anchors, percentage ratios, and explicit sheet/font `unload()`.
|
|
34
|
+
- Sprite paths, animation chaining, raycasts, A*, game-clock timers, collision exit, swept movers, and four blend modes.
|
|
35
|
+
- Shape-aware solids: circle and rotated-rect geometry, one-way platforms, carried riders, containment, bilateral circle push, two-sided restitution, `gravityX`, and `linearDamping`.
|
|
36
|
+
- Native particles, ropes, effect/music sound backends, car physics, thrust, drag/pinch/rotate, and multi-touch sprites.
|
|
37
|
+
|
|
38
|
+
## Deliberately out of scope upstream
|
|
39
|
+
|
|
40
|
+
- Full 2D lighting/shadows.
|
|
41
|
+
- Skeletal animation such as Spine.
|
|
42
|
+
- Box2D-class rigid-body physics: masses, joints, torque, compound bodies, frictional contacts, spin, iterative stacking.
|
|
43
|
+
- General render-to-texture. The internal fullscreen camera effect is not a public render-target API.
|
|
44
|
+
|
|
45
|
+
## Checking a moving target
|
|
46
|
+
|
|
47
|
+
The manifest remains `0.5.0` across shape-aware solids, the HUD, and TileLayer, so the version cannot prove a feature exists. Prefer the build commit/date and safe feature probes such as `typeof Game.createTileLayer === 'function'`. Read a property before writing it; Kroll can preserve unknown writes on a proxy.
|
|
48
|
+
|
|
49
|
+
Before claiming an item is still absent, re-check upstream `TODO.md`, `README.md`, both platform proxies, and the examples. Source code wins over prose when they disagree.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Native tile maps
|
|
2
|
+
|
|
3
|
+
Verified against `ti.game@d587081`: upstream `README.md` § Tile maps, `example/tilemap.js`, both `TileLayerProxy` implementations, both native `TileLayer` engines, scene collision, and `Pathfinder`.
|
|
4
|
+
|
|
5
|
+
Use `Game.createTileLayer()` for a large or mostly static grid. The engine stores the whole map but renders only cells inside the camera, batches the layer through one sheet, checks only collision cells under a mover, and feeds solid cells into `findPath`. A small map whose tiles need independent sprite behavior can still use sprites; `topdown.js` is a 16×12 example of that older pattern.
|
|
6
|
+
|
|
7
|
+
## Create a layer
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const tileSheet = Game.createSpriteSheet({
|
|
11
|
+
image: 'assets/tiles.png',
|
|
12
|
+
frameWidth: 16,
|
|
13
|
+
frameHeight: 16,
|
|
14
|
+
smoothing: false
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const ground = Game.createTileLayer({
|
|
18
|
+
sheet: tileSheet,
|
|
19
|
+
tileWidth: 32,
|
|
20
|
+
tileHeight: 32,
|
|
21
|
+
data: [
|
|
22
|
+
'WWWWWW',
|
|
23
|
+
'W....W',
|
|
24
|
+
'W.FP.W',
|
|
25
|
+
'WWWWWW'
|
|
26
|
+
],
|
|
27
|
+
legend: { W: 3, '.': 0, F: 1, P: 2 },
|
|
28
|
+
collisionGroup: 'water',
|
|
29
|
+
solid: ['W'],
|
|
30
|
+
oneWay: [],
|
|
31
|
+
zIndex: 0
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
gameView.add(ground);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`data` accepts nested numeric rows, strings decoded through `legend`, or a flat row-major array accompanied by `cols` and optionally `rows`. Uneven nested rows are padded with empty cells. Negative/unlisted values are empty.
|
|
38
|
+
|
|
39
|
+
For Tiled JSON, pass the layer data with `cols: map.width`, `rows: map.height`, and the tileset's `firstgid` as `firstGid`. GID 0 becomes empty when `firstGid > 0`, and the three flip bits are stripped. The current renderer does not reproduce Tiled's flip transforms; it resolves the underlying frame id.
|
|
40
|
+
|
|
41
|
+
## Portable property timing
|
|
42
|
+
|
|
43
|
+
The upstream README says every property is live, but the native bindings differ. Use the portable contract below:
|
|
44
|
+
|
|
45
|
+
| Timing | Properties |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| Creation-time for cross-platform code | `legend`, `firstGid`, `cols`, `rows` |
|
|
48
|
+
| Live on Android and iOS | `data`, `solid`, `oneWay`, `sheet`, `tileWidth`, `tileHeight`, `x`, `y`, `zIndex`, `visible`, `opacity`, `scrollFactor`, `tintColor`, `collisionGroup`, `restitution`, `debug` |
|
|
49
|
+
| Read-only | `width`, `height`; `cols` and `rows` reflect the current parsed grid |
|
|
50
|
+
|
|
51
|
+
iOS rebuilds the grid when `legend`, `firstGid`, `cols`, or `rows` is assigned later. Android exposes no setters for those four. Supplying them in the factory keeps both platforms aligned.
|
|
52
|
+
|
|
53
|
+
`opacity`, `scrollFactor`, and `restitution` accept ratio numbers or percentage strings. `tileWidth`/`tileHeight`, coordinates, and dimensions do not.
|
|
54
|
+
|
|
55
|
+
## Collision cells
|
|
56
|
+
|
|
57
|
+
Give the layer a `collisionGroup`; movers list that group in `solidWith`:
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const player = Game.createSprite({
|
|
61
|
+
sheet: playerSheet,
|
|
62
|
+
solidWith: ['water'],
|
|
63
|
+
hitboxScale: '65%'
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `solid` ids block from every side.
|
|
68
|
+
- `oneWay` ids catch a mover falling onto the top and do not block sideways or upward movement.
|
|
69
|
+
- Rect, circle, and swept movers are supported.
|
|
70
|
+
- Layer `restitution` mixes with the mover like restitution on a solid sprite.
|
|
71
|
+
- A successful top landing sets `onGround` and fires `land`. A tile cell is not a sprite, so the event has no `other` object.
|
|
72
|
+
- Adjacent solid cells suppress their shared internal faces, avoiding seams while sliding along a floor.
|
|
73
|
+
|
|
74
|
+
Tile cells participate in **solid resolution**, not overlap events. `collidesWith` does not produce `collision`/`collisionend` for trigger tiles yet. `raycast()` also does not inspect tile cells. See [roadmap.md](roadmap.md) before designing those behaviors.
|
|
75
|
+
|
|
76
|
+
## Read and edit cells
|
|
77
|
+
|
|
78
|
+
| Method | Result |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `getTile(col, row)` | Frame id, or `-1` when empty/outside |
|
|
81
|
+
| `setTile(col, row, id)` | Replaces the art; solid/one-way flags are recalculated from the id lists |
|
|
82
|
+
| `isBlocked(col, row)` | Whether the cell is fully solid; one-way cells return `false` |
|
|
83
|
+
| `setBlocked(col, row, bool)` | Overrides the cell as fully solid or non-solid without changing its art |
|
|
84
|
+
| `tileAt(x, y)` | World point → `{ col, row, tile, solid, x, y }`, or `null` outside |
|
|
85
|
+
| `cellAt(col, row)` | Cell → the same structure; `x`/`y` are the cell center |
|
|
86
|
+
|
|
87
|
+
`setBlocked()` is a cell-level override. Replacing `data`, `solid`, or `oneWay` rebuilds flags and should be treated as ending those overrides.
|
|
88
|
+
|
|
89
|
+
## Tap-to-walk across a large map
|
|
90
|
+
|
|
91
|
+
`findPath()` includes fully solid cells when the layer's `collisionGroup` appears in `groups`. One-way cells remain walkable in the path grid.
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
function walkTo(player, destination, tileLayer, gameView) {
|
|
95
|
+
const start = { x: player.x, y: player.y };
|
|
96
|
+
const reach = tileLayer.tileWidth * 20;
|
|
97
|
+
const path = gameView.findPath(start, destination, {
|
|
98
|
+
cellSize: tileLayer.tileWidth,
|
|
99
|
+
groups: [tileLayer.collisionGroup],
|
|
100
|
+
clearance: player.width * 0.3,
|
|
101
|
+
bounds: {
|
|
102
|
+
minX: Math.max(tileLayer.x, start.x - reach),
|
|
103
|
+
minY: Math.max(tileLayer.y, start.y - reach),
|
|
104
|
+
maxX: Math.min(tileLayer.x + tileLayer.width, start.x + reach),
|
|
105
|
+
maxY: Math.min(tileLayer.y + tileLayer.height, start.y + reach)
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (path && path.length >= 2) {
|
|
110
|
+
player.followPath(path, { speed: tileLayer.tileWidth * 4 });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Bound the per-query A* search to the useful neighborhood. The layer can be enormous without rendering cost, but `findPath()` still builds a grid for the requested bounds on the calling thread.
|
|
116
|
+
|
|
117
|
+
## Layering and performance
|
|
118
|
+
|
|
119
|
+
- Stack separate layers for ground, decoration, and foreground. Layers draw below sprites at the same `zIndex`.
|
|
120
|
+
- Share one sheet when layers use the same tileset.
|
|
121
|
+
- Use `scrollFactor` for parallax layers; collision remains in world coordinates, just as with sprites.
|
|
122
|
+
- Use `visible: false` to exclude the layer from drawing and collision.
|
|
123
|
+
- Toggle `layer.debug` for one layer or `gameView.debug = { hitbox: true }` for every layer's solid cells.
|
|
124
|
+
- Use the performance HUD while scrolling: map dimensions should not change visible-cell draw cost.
|
|
125
|
+
|