@maccesar/titools 4.6.1 → 4.8.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 +46 -8
- package/commands/ti-audit.md +1 -0
- package/commands/ti-check.md +3 -2
- package/lib/config.js +1 -0
- package/package.json +1 -1
- package/skills/ti-game/SKILL.md +198 -0
- package/skills/ti-game/references/api.md +408 -0
- package/skills/ti-game/references/project-setup.md +306 -0
- package/skills/ti-game/references/recipes.md +1268 -0
- package/skills/ti-game/references/roadmap.md +57 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Setting up a ti.game project
|
|
2
|
+
|
|
3
|
+
Installing the module, wiring it into Classic and Alloy projects, organizing art and sound, and the lifecycle work the engine does not do for you.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- [Install the module](#install-the-module)
|
|
8
|
+
- [Classic projects](#classic-projects)
|
|
9
|
+
- [Alloy projects](#alloy-projects)
|
|
10
|
+
- [Where art and sound live](#where-art-and-sound-live)
|
|
11
|
+
- [Sprite sheets and atlases](#sprite-sheets-and-atlases)
|
|
12
|
+
- [Fonts](#fonts)
|
|
13
|
+
- [Lifecycle and cleanup](#lifecycle-and-cleanup)
|
|
14
|
+
- [Debugging and tuning](#debugging-and-tuning)
|
|
15
|
+
- [Platform differences](#platform-differences)
|
|
16
|
+
|
|
17
|
+
## Install the module
|
|
18
|
+
|
|
19
|
+
There is no registry and no `ti module install` command — Titanium modules are zips you drop into a project. Grab the platform zips from the module's `dist/` folders (or build them yourself) and put them where the CLI looks:
|
|
20
|
+
|
|
21
|
+
**Project-local** (recommended — the version travels with the repo):
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
modules/
|
|
25
|
+
├── android/ti.game/0.3.0/
|
|
26
|
+
└── iphone/ti.game/0.3.0/
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Unzipping the module zip at the project root creates exactly that layout.
|
|
30
|
+
|
|
31
|
+
**Global** (shared across projects): `~/Library/Application Support/Titanium/modules/<platform>/ti.game/<version>/`.
|
|
32
|
+
|
|
33
|
+
Then declare it in `tiapp.xml`. The iOS platform key is `iphone`, not `ios`:
|
|
34
|
+
|
|
35
|
+
```xml
|
|
36
|
+
<modules>
|
|
37
|
+
<module platform="android" version="0.3.0">ti.game</module>
|
|
38
|
+
<module platform="iphone" version="0.3.0">ti.game</module>
|
|
39
|
+
</modules>
|
|
40
|
+
```
|
|
41
|
+
|
|
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
|
+
|
|
44
|
+
The version number lies right now: the manifest has read `0.3.0` through several feature landings. A zip built before 2026-08-18 has no `createText`, no `createFont`, no `screenFixed`, no `swept` and no `collisionend` while still calling itself 0.3.0. If a call that this skill documents is `undefined` at runtime, rebuild the module from upstream `main` rather than hunting for a typo.
|
|
45
|
+
|
|
46
|
+
Building the module from source:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cd android
|
|
50
|
+
ti build -p android --build-only # → android/dist/ti.game-android-<version>.zip
|
|
51
|
+
ti build -p ios --build-only # macOS only → ios/dist/ti.game-iphone-<version>.zip
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Classic projects
|
|
55
|
+
|
|
56
|
+
`require('ti.game')` anywhere and add the GameView to a window. The 21 demos in the module's `example/` folder are Classic, each one a CommonJS module exporting a start function:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
// Resources/flappy.js
|
|
60
|
+
const Game = require('ti.game');
|
|
61
|
+
|
|
62
|
+
module.exports = function () {
|
|
63
|
+
const win = Ti.UI.createWindow({ backgroundColor: '#000', theme: 'Theme.Titanium.DayNight.NoTitleBar' });
|
|
64
|
+
const gameView = Game.createGameView({ backgroundColor: '#8ed8f8' });
|
|
65
|
+
// ... build the game
|
|
66
|
+
win.add(gameView);
|
|
67
|
+
win.open();
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
// Resources/app.js
|
|
73
|
+
require('/flappy')();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
One module per screen or per game mode keeps things separable, and a window per mode means closing it tears the scene down.
|
|
77
|
+
|
|
78
|
+
## Alloy projects
|
|
79
|
+
|
|
80
|
+
Two ways to get a GameView on screen. Both work; pick by whether the scene is permanent or comes and goes.
|
|
81
|
+
|
|
82
|
+
### A: declare it in XML (`<Module>`)
|
|
83
|
+
|
|
84
|
+
The standard Alloy element for a native module view. `method` names the factory to call and every attribute is passed as a creation property.
|
|
85
|
+
|
|
86
|
+
```xml
|
|
87
|
+
<!-- app/views/index.xml -->
|
|
88
|
+
<Alloy>
|
|
89
|
+
<Window id="index" navBarHidden="true" exitOnClose="true" onOpen="onOpen" onClose="onClose">
|
|
90
|
+
<Module id="gameView" module="ti.game" method="createGameView" backgroundColor="#8ed8f8" />
|
|
91
|
+
<Button id="jumpButton" title="JUMP" onTouchstart="jump" />
|
|
92
|
+
</Window>
|
|
93
|
+
</Alloy>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
// app/controllers/index.js
|
|
98
|
+
const Game = require('ti.game');
|
|
99
|
+
|
|
100
|
+
const heroSheet = Game.createSpriteSheet({ image: 'hero.png', frameWidth: 64, frameHeight: 64 });
|
|
101
|
+
|
|
102
|
+
let initialized = false;
|
|
103
|
+
|
|
104
|
+
function onOpen() {
|
|
105
|
+
$.gameView.addEventListener('resize', onResize);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function onResize(e) {
|
|
109
|
+
if (initialized) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
initialized = true;
|
|
113
|
+
buildLevel(e.width, e.height);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Only views that must be native — buttons, menus, dialogs — belong in the XML. The HUD does not: scores and labels are `Game.createText` sprites created in the controller and added to the game view, so they batch with the scene and can tween and flash.
|
|
118
|
+
|
|
119
|
+
Style the view in TSS like any other component (`'#gameView': { width: Ti.UI.FILL, height: Ti.UI.FILL }`). The `<Module>` element itself is documented Alloy behavior; the pattern below is the one verified in a shipped ti.game app.
|
|
120
|
+
|
|
121
|
+
### B: an empty container in XML, the GameView created in JS
|
|
122
|
+
|
|
123
|
+
Preferred when the game is created and destroyed repeatedly — switching between a menu and a run, changing levels, or restarting cleanly. The XML holds a plain container and the game code owns everything inside it.
|
|
124
|
+
|
|
125
|
+
```xml
|
|
126
|
+
<Alloy>
|
|
127
|
+
<Window id="index" navBarHidden="true" exitOnClose="true" onOpen="onOpen" onClose="onClose">
|
|
128
|
+
<View id="menuState" />
|
|
129
|
+
<View id="gameContainer" visible="false" />
|
|
130
|
+
</Window>
|
|
131
|
+
</Alloy>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
// app/controllers/index.js
|
|
136
|
+
const LanderGame = require('/game/LanderGame');
|
|
137
|
+
|
|
138
|
+
let game = null;
|
|
139
|
+
|
|
140
|
+
function startCampaign() {
|
|
141
|
+
destroyGame();
|
|
142
|
+
game = new LanderGame({ container: $.gameContainer, onExit: showMenu });
|
|
143
|
+
$.menuState.visible = false;
|
|
144
|
+
$.gameContainer.visible = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function destroyGame() {
|
|
148
|
+
if (!game) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
game.destroy();
|
|
152
|
+
game = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function onClose() {
|
|
156
|
+
destroyGame();
|
|
157
|
+
$.destroy();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
$.cleanup = onClose;
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Structure for anything bigger than a demo
|
|
164
|
+
|
|
165
|
+
Alloy Tier 2 applies unchanged: a slim controller that owns navigation and menu state, with the game itself in `app/lib/`. A shipped layout that works:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
app/lib/game/
|
|
169
|
+
├── LanderGame.js session orchestration and the whole JS↔native boundary
|
|
170
|
+
├── TerrainGenerator.js deterministic level geometry
|
|
171
|
+
├── WorldCatalog.js immutable world/level data
|
|
172
|
+
├── AudioManager.js owns every createSound handle
|
|
173
|
+
├── GamePreferences.js Ti.App.Properties persistence
|
|
174
|
+
└── LeaderboardStore.js scores
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Catalogs, generators and stores are plain modules with no Titanium UI in them, so they are unit-testable in plain Node. The one class that touches sprites is the boundary — keep it explicit and keep the rest pure.
|
|
178
|
+
|
|
179
|
+
Alloy specifics worth remembering: `OS_IOS`/`OS_ANDROID` are compile-time constants (the dead branch is stripped, so an iOS-only API inside `if (OS_IOS)` never reaches an Android build), `Alloy.CFG` reads `config.json`, and `$.destroy()` releases Alloy's own bindings but knows nothing about your timers, sounds or sprites — see the cleanup section below.
|
|
180
|
+
|
|
181
|
+
## Where art and sound live
|
|
182
|
+
|
|
183
|
+
**Alloy**: everything under `app/assets/` is copied to the resources root, so the path in code drops the `app/assets/` prefix.
|
|
184
|
+
|
|
185
|
+
| File | Referenced as |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `app/assets/lander.png` | `'lander.png'` |
|
|
188
|
+
| `app/assets/game/stars_far.png` | `'game/stars_far.png'` |
|
|
189
|
+
| `app/assets/sounds/jump.wav` | `'sounds/jump.wav'` |
|
|
190
|
+
|
|
191
|
+
**Classic**: paths are relative to `Resources/`, so `Resources/assets/hero.png` is `'assets/hero.png'`.
|
|
192
|
+
|
|
193
|
+
Do not use `@2x`/`@3x` density suffixes for game art. The scene is measured in surface pixels and you size every sprite yourself from the `resize` dimensions — density variants would fight that. Ship one texture at a resolution high enough for the largest target and let the engine scale it.
|
|
194
|
+
|
|
195
|
+
## Sprite sheets and atlases
|
|
196
|
+
|
|
197
|
+
**Grid** — frames numbered left-to-right, top-to-bottom from 0:
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
const sheet = Game.createSpriteSheet({
|
|
201
|
+
image: 'hero.png',
|
|
202
|
+
frameWidth: 64,
|
|
203
|
+
frameHeight: 64,
|
|
204
|
+
smoothing: false // pixel art
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**TexturePacker atlas** — hash or array JSON format:
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
const sheet = Game.createSpriteSheet({ image: 'hero.png', atlas: 'hero.json' });
|
|
212
|
+
const runFrame = sheet.frameIndex('run_01'); // -1 if unknown
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Atlases are the better default for hand-authored art with varied frame sizes; grids are simpler for uniform pixel art.
|
|
216
|
+
|
|
217
|
+
Practical rules:
|
|
218
|
+
|
|
219
|
+
- **One sheet per scene if you can manage it.** Sprites sharing a texture batch into a single draw call. The endless-runner demo even draws its crash burst from the *player's* sheet to keep the whole scene on one texture.
|
|
220
|
+
- **`smoothing: false` for pixel art.** Combine with `pixelSnap: true` on moving sprites when the texel phase must stay stable.
|
|
221
|
+
- **`repeat: true` for tiling.** Required by sprites using `tileRepeat`, and it needs power-of-two texture dimensions on ES 2.0 — a 512×64 street strip tiles, a 500×60 one does not.
|
|
222
|
+
- **White art plus `tint`** covers every color variant of a particle or effect with one small texture.
|
|
223
|
+
|
|
224
|
+
`frameCount` is `0` until a grid sheet's texture has loaded, so do not compute animation ranges from it at creation time.
|
|
225
|
+
|
|
226
|
+
## Fonts
|
|
227
|
+
|
|
228
|
+
Nothing to ship for the default look: `Game.createText({ text: 'SCORE 0' })` uses a 9×15 pixel font embedded in the module, on both platforms.
|
|
229
|
+
|
|
230
|
+
A custom face is an asset like any other, created once outside `resize`:
|
|
231
|
+
|
|
232
|
+
```javascript
|
|
233
|
+
// BMFont / AngelCode descriptor + its page image, side by side in assets/
|
|
234
|
+
const hud = Game.createFont({ font: 'fonts/hud.fnt' }); // hud.png resolved next to it
|
|
235
|
+
|
|
236
|
+
// or a monospace grid image
|
|
237
|
+
const mono = Game.createFont({ image: 'fonts/mono.png', charWidth: 9, charHeight: 15 });
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
| File | Referenced as (Alloy) |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| `app/assets/fonts/hud.fnt` + `app/assets/fonts/hud.png` | `'fonts/hud.fnt'` |
|
|
243
|
+
|
|
244
|
+
These are **bitmap** fonts — do not put them in `app/assets/fonts/` expecting Titanium's native font handling, and do not use a `.ttf` here. To go from a TTF to either format, the module ships `tools/genfont.py`, which rasterizes it into a grid or BMFont atlas.
|
|
245
|
+
|
|
246
|
+
The built-in font covers ASCII 32..126 only. Any game whose text includes `ñ`, accents or `¿` needs a generated font — or a `Ti.UI.Label` overlay for that specific string.
|
|
247
|
+
|
|
248
|
+
## Lifecycle and cleanup
|
|
249
|
+
|
|
250
|
+
The engine handles what it owns: the render loop pauses and resumes with the activity or app, music pauses and resumes with it, and textures survive an EGL context loss. **Everything you created in JS is yours to stop.**
|
|
251
|
+
|
|
252
|
+
A leaked `setInterval` keeps running after the window closes, keeps reading sprite properties from a dead scene and keeps the whole closure alive. A looping `createSound` keeps playing over the next screen.
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
function cleanup() {
|
|
256
|
+
clearInterval(aiTimer);
|
|
257
|
+
clearTimeout(spawnTimer);
|
|
258
|
+
clearHitStop(); // restore timeScale — a frozen scene must not persist
|
|
259
|
+
music.stop();
|
|
260
|
+
thrustSound.stop(); // looping effects too
|
|
261
|
+
listeners.forEach(({ target, eventName, handler }) => {
|
|
262
|
+
target.removeEventListener(eventName, handler);
|
|
263
|
+
});
|
|
264
|
+
gameView.pause();
|
|
265
|
+
gameView.removeAllSprites();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Classic
|
|
269
|
+
win.addEventListener('close', cleanup);
|
|
270
|
+
|
|
271
|
+
// Alloy
|
|
272
|
+
$.cleanup = cleanup;
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Tracking listeners as you add them makes the teardown mechanical instead of a memory exercise:
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
const listeners = [];
|
|
279
|
+
|
|
280
|
+
function listen(target, eventName, handler) {
|
|
281
|
+
target.addEventListener(eventName, handler);
|
|
282
|
+
listeners.push({ target, eventName, handler });
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
For restart-without-teardown (a retry button), `removeAllSprites()` plus rebuilding is simplest, but pooling and repositioning is cheaper: reset positions, zero velocities, `clearTweens()`, and re-`play()` the animations.
|
|
287
|
+
|
|
288
|
+
## Debugging and tuning
|
|
289
|
+
|
|
290
|
+
- **`debug: true` on the GameView** draws collision shapes for the whole scene; on a single sprite, only that one. Green = collision AABB with `hitboxScale` applied, blue = sprite/touch bounds, orange dot = anchor. This is the fastest way to explain a hit that should not have registered.
|
|
291
|
+
- **Physics that feels wrong is usually a units problem.** Speeds are px/s and accelerations px/s², both in surface pixels — which is why the demos express them as fractions of `W` and `H`. A gravity of `20` is imperceptible; `H * 2.2` is a snappy platformer.
|
|
292
|
+
- **Sprite is invisible?** Check in order: was it added to the GameView, does it have a sheet, is `width`/`height` non-zero, is it inside the surface bounds, and is its `zIndex` below a background sprite.
|
|
293
|
+
- **Collision never fires?** `collisionGroup` goes on the *target*, `collidesWith` on the *mover*. Both need to be set, and `visible: false` disables collision entirely.
|
|
294
|
+
- **Never scrolls / never follows?** `follow()` only does horizontal tracking when `leftMargin` or `rightMargin` is passed, and `maxY` defaults to `0`.
|
|
295
|
+
- Testing: game logic that lives in plain modules (catalogs, generators, scoring, persistence) tests in Node with the built-in `node:test` runner. The scene layer needs a device — there is no headless renderer.
|
|
296
|
+
|
|
297
|
+
## Platform differences
|
|
298
|
+
|
|
299
|
+
The JS API is identical on Android and iOS. What differs is around it:
|
|
300
|
+
|
|
301
|
+
- **`Ti.UI.createNotification` (Android Toast) does not exist on iOS.** The demos branch on `Ti.Platform.osname` and fall back to `Ti.UI.createAlertDialog`.
|
|
302
|
+
- **The iOS Simulator renders at a 1x logical drawable** (its translated OpenGL path is disproportionately expensive at 3x Retina) and is noticeably slower than any real device. Real iPhone and iPad builds keep the device's native screen scale. Judge performance on device.
|
|
303
|
+
- **`logicalDensityFactor` lies in the iOS Simulator** — it reports the device scale while the surface renders at 1x. Derive dp→scene units from the surface instead: `H / Ti.Platform.displayCaps.platformHeight`.
|
|
304
|
+
- **Android's `touchFeedback` ripple** cannot animate over the GL canvas and logs `RippleDrawable` errors; give on-screen buttons manual `backgroundColor` feedback on `touchstart`/`touchend`.
|
|
305
|
+
- **Sound formats**: WAV, MP3 and OGG on Android; WAV, MP3 and M4A on iOS. WAV for effects, MP3 for music covers both.
|
|
306
|
+
- **`theme: 'Theme.Titanium.DayNight.NoTitleBar'`** on the window is the Android idiom for a fullscreen game surface; on iOS use `navBarHidden`/`statusBarHidden` (or the Alloy XML equivalents).
|