@maccesar/titools 5.1.0 → 5.2.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.
Files changed (85) hide show
  1. package/README.md +44 -12
  2. package/agents/ti-pro.md +4 -0
  3. package/commands/ti-audit.md +4 -0
  4. package/lib/config.js +2 -0
  5. package/package.json +1 -1
  6. package/skills/purgetss/SKILL.md +4 -4
  7. package/skills/purgetss/references/EXAMPLES.md +3 -3
  8. package/skills/purgetss/references/adopting-purgetss.md +2 -2
  9. package/skills/purgetss/references/animation-advanced.md +2 -2
  10. package/skills/purgetss/references/arbitrary-values.md +8 -5
  11. package/skills/purgetss/references/class-categories.md +5 -5
  12. package/skills/purgetss/references/class-index-properties.md +6 -6
  13. package/skills/purgetss/references/class-index.md +2 -2
  14. package/skills/purgetss/references/cli-commands.md +2 -2
  15. package/skills/purgetss/references/dynamic-component-creation.md +2 -2
  16. package/skills/purgetss/references/grid-layout.md +1 -1
  17. package/skills/purgetss/references/migration-guide.md +34 -4
  18. package/skills/purgetss/references/multi-density-images.md +6 -6
  19. package/skills/purgetss/references/platform-modifiers.md +21 -0
  20. package/skills/purgetss/references/ui-ux-design.md +1 -1
  21. package/skills/purgetss/references/version-history.md +9 -4
  22. package/skills/ti-admob-unified/SKILL.md +44 -0
  23. package/skills/ti-admob-unified/agents/openai.yaml +4 -0
  24. package/skills/ti-admob-unified/assets/example/app.js +1003 -0
  25. package/skills/ti-admob-unified/assets/example/lib/ads.js +618 -0
  26. package/skills/ti-admob-unified/assets/example/library-usage.js +104 -0
  27. package/skills/ti-admob-unified/references/api-reference.md +592 -0
  28. package/skills/ti-admob-unified/references/banners.md +435 -0
  29. package/skills/ti-admob-unified/references/contributing.md +228 -0
  30. package/skills/ti-admob-unified/references/dependencies.md +85 -0
  31. package/skills/ti-admob-unified/references/fullscreen.md +223 -0
  32. package/skills/ti-admob-unified/references/game-monetization.md +17 -0
  33. package/skills/ti-admob-unified/references/getting-started.md +275 -0
  34. package/skills/ti-admob-unified/references/index.md +30 -0
  35. package/skills/ti-admob-unified/references/integration.md +85 -0
  36. package/skills/ti-admob-unified/references/library.md +189 -0
  37. package/skills/ti-admob-unified/references/mediation.md +132 -0
  38. package/skills/ti-admob-unified/references/migration.md +255 -0
  39. package/skills/ti-admob-unified/references/module-changelog.md +54 -0
  40. package/skills/ti-admob-unified/references/module-license.txt +21 -0
  41. package/skills/ti-admob-unified/references/privacy.md +254 -0
  42. package/skills/ti-admob-unified/references/roadmap.md +146 -0
  43. package/skills/ti-admob-unified/references/source-notes.md +33 -0
  44. package/skills/ti-admob-unified/references/source-snapshot.json +440 -0
  45. package/skills/ti-admob-unified/references/third-party-notices.md +22 -0
  46. package/skills/ti-game/SKILL.md +10 -1
  47. package/skills/ti-game/references/api.md +10 -2
  48. package/skills/ti-game/references/debugging-performance.md +6 -0
  49. package/skills/ti-game/references/examples.md +11 -1
  50. package/skills/ti-game/references/patterns.md +24 -2
  51. package/skills/ti-game/references/physics.md +502 -0
  52. package/skills/ti-game/references/project-setup.md +23 -1
  53. package/skills/ti-game/references/recipes-action.md +46 -14
  54. package/skills/ti-game/references/recipes-adventure-effects.md +12 -1
  55. package/skills/ti-game/references/roadmap.md +2 -2
  56. package/skills/ti-game/references/tilemaps.md +2 -1
  57. package/skills/ti-game-assets/SKILL.md +53 -0
  58. package/skills/ti-game-assets/agents/openai.yaml +4 -0
  59. package/skills/ti-game-assets/references/alpha.md +26 -0
  60. package/skills/ti-game-assets/references/art-direction.md +37 -0
  61. package/skills/ti-game-assets/references/integration.md +49 -0
  62. package/skills/ti-game-assets/references/manifest.example.json +429 -0
  63. package/skills/ti-game-assets/references/manifest.md +39 -0
  64. package/skills/ti-game-assets/references/offline-audio.md +33 -0
  65. package/skills/ti-game-assets/references/prompts.md +33 -0
  66. package/skills/ti-game-assets/references/repeating-textures.md +17 -0
  67. package/skills/ti-game-assets/references/responsible-ads.md +31 -0
  68. package/skills/ti-game-assets/references/sound-assets.md +42 -0
  69. package/skills/ti-game-assets/references/vector-sources.md +21 -0
  70. package/skills/ti-game-assets/scripts/alpha_tools.py +160 -0
  71. package/skills/ti-game-assets/scripts/requirements.txt +1 -0
  72. package/skills/ti-game-assets/scripts/validate_manifest.py +267 -0
  73. package/skills/ti-synthengine/SKILL.md +37 -13
  74. package/skills/ti-synthengine/references/api.md +397 -31
  75. package/skills/ti-synthengine/references/audio-engine.md +235 -14
  76. package/skills/ti-synthengine/references/examples.md +396 -17
  77. package/skills/ti-synthengine/references/guide.md +331 -12
  78. package/skills/ti-synthengine/references/recipes.md +212 -22
  79. package/skills/ti-synthengine/references/recommendations.md +53 -16
  80. package/skills/ti-synthengine/references/sound-design.md +359 -15
  81. package/skills/ti-synthengine/references/source-notes.md +121 -0
  82. package/skills/ti-synthengine/references/source-snapshot.json +105 -0
  83. package/skills/ti-synthengine/references/stats-performance.json +2992 -0
  84. package/skills/ti-synthengine/references/stats-performance.md +38 -0
  85. package/skills/ti-synthengine/references/troubleshooting.md +61 -4
@@ -0,0 +1,502 @@
1
+ # Physical models and equations
2
+
3
+ Use this reference to choose movement by its intended feel, derive parameters,
4
+ or explain frame-rate-dependent behavior. A camera angle is not a physics
5
+ model: a top-down character, car, spaceship and billiard ball use different
6
+ rules. The [34-demo catalog](examples.md) covers action, platforms, ball games,
7
+ adventures, board interactions and visual demonstrations.
8
+
9
+ Verified on 2026-10-12 against upstream
10
+ [`3bea2f4007d4aa515e432ec2a016dd14c5c0f2ba`](https://github.com/m1ga/ti.game/tree/3bea2f4007d4aa515e432ec2a016dd14c5c0f2ba).
11
+ Upstream HEAD still has that commit. Local forks and installed binaries can
12
+ contain additional changes; do not silently treat them as this snapshot.
13
+
14
+ Each equation is labeled **Engine** (native implementation), **Demo**
15
+ (application rule) or **Analytical** (a mathematical model for tuning).
16
+ An analytical equation does not imply a matching public property exists.
17
+ JS selects controls and reacts to events; frame-by-frame integration stays native.
18
+
19
+ <!-- TOC-START -->
20
+ ## Contents
21
+
22
+ - [Units and conventions](#units-and-conventions)
23
+ - [Model selection across the demos](#model-selection-across-the-demos)
24
+ - [Sprite integration and constant acceleration](#sprite-integration-and-constant-acceleration)
25
+ - [Thrust, inertia and linear damping](#thrust-inertia-and-linear-damping)
26
+ - [Projectiles and additive impulses](#projectiles-and-additive-impulses)
27
+ - [Arcade car equations](#arcade-car-equations)
28
+ - [Direct character control](#direct-character-control)
29
+ - [Jumps, flaps and scrolling runners](#jumps-flaps-and-scrolling-runners)
30
+ - [Landing and impact measurements](#landing-and-impact-measurements)
31
+ - [Static reflection and arcade ball sports](#static-reflection-and-arcade-ball-sports)
32
+ - [Equal-mass billiards and circular containment](#equal-mass-billiards-and-circular-containment)
33
+ - [Slopes and contact friction](#slopes-and-contact-friction)
34
+ - [Verlet ropes and tethers](#verlet-ropes-and-tethers)
35
+ - [Swept movement and periodic worlds](#swept-movement-and-periodic-worlds)
36
+ - [Paths, tweens and navigation](#paths-tweens-and-navigation)
37
+ - [Rhythm timing](#rhythm-timing)
38
+ - [Particle integration](#particle-integration)
39
+ - [Time step and frame rate](#time-step-and-frame-rate)
40
+ - [What needs a different model](#what-needs-a-different-model)
41
+
42
+ <!-- TOC-END -->
43
+
44
+ ## Units and conventions
45
+
46
+ | Quantity | Units |
47
+ | --- | --- |
48
+ | Position, width, height, path distance | Surface pixels, from guarded GameView `resize` |
49
+ | `velocityX/Y`, `maxSpeed` | px/s |
50
+ | `gravity`, `gravityX`, `thrust`, `enginePower` | px/s² |
51
+ | Native `dt` | Seconds |
52
+ | JS `duration`, `after`, `every`, particle lifetime | Milliseconds |
53
+ | `rotation` | Clockwise degrees; 0 points up |
54
+ | `angularVelocity`, `turnRate` | degrees/s |
55
+ | `linearDamping`, car `drag`, car `grip` | 1/s coefficients, not percentage strings |
56
+ | Rope `damping` | Retained displacement fraction per update |
57
+ | `restitution` | Dimensionless, 0..1 |
58
+
59
+ Origin is top-left; y increases downwards. Position is the sprite anchor.
60
+ For `θ = rotation·π/180`, forward `f=(sin θ,−cos θ)` and right
61
+ `r=(cos θ,sin θ)`. `n` below is a unit contact normal.
62
+
63
+ ## Model selection across the demos
64
+
65
+ | Model | Examples | What determines movement |
66
+ | --- | --- | --- |
67
+ | Inertial ship | Asteroids | Thrust adds velocity along heading; rotation does not redirect existing momentum |
68
+ | Gravity and replacement jump/flap | Platformer, Flappy, Skate, Flip | Vertical acceleration plus a chosen takeoff velocity |
69
+ | Direct character control | Topdown | Velocity follows input immediately, including diagonal normalization |
70
+ | Arcade car | Racing | Forward/lateral velocity, throttle, steering, drag and grip |
71
+ | Ball with static-surface reflection | Volley, Plinko, Circles, Slopes | Gravity, geometry normals and restitution |
72
+ | Equal-mass circle bodies | Pool, Plinko, Drum, Circles | Bilateral normal impulse and separation |
73
+ | Circular containment | Drum | Keep ball centers inside radius `R−r` |
74
+ | Position constraints | Rope | Verlet displacement and link-distance relaxation |
75
+ | Fast body or instant query | Swept, Raycast, Worldwrap | Swept movement versus a discrete ray query |
76
+ | Prescribed route | Path, Maze, Pointclick, Tilemap, Demoscene | Distance traveled along a path; A* chooses waypoints |
77
+ | Prescribed interpolation | Cards, Puzzle, Basic | Tween destination and easing; drag/snap rules |
78
+ | Timing window | Rhythm | Constant velocity and distance-to-pad acceptance |
79
+ | Ballistic effects | Particles | Native emitter rate, gravity and lifetime; no Sprite solid response |
80
+ | Cross-cutting systems | Camera, Blend, Text, Hitbox, Zones, Timescale | Presentation, geometry, events or clock; not separate locomotion models |
81
+
82
+ Names map to `example/<name>.js`, with lowercase spelling. These groups cover
83
+ all 34 launcher entries; Lunar Lander in the action recipes is a curated
84
+ composition, not an extra upstream demo. Use [examples.md](examples.md) for
85
+ the exact filenames and [api.md](api.md) for public properties.
86
+
87
+ ## Sprite integration and constant acceleration
88
+
89
+ **Engine**, without contacts or position overrides:
90
+
91
+ ```text
92
+ vNext = v + a·dt
93
+ pNext = p + vNext·dt
94
+ ```
95
+
96
+ This is semi-implicit Euler: acceleration before position. In Sprite, the
97
+ order is car integration, angular velocity, thrust and its speed cap,
98
+ gravity, linear damping, position, scroll wrap, path, animation, tween and
99
+ idle wobble. Scene collision/attachment passes can change that result again.
100
+ Do not combine several position writers expecting their displacements to add.
101
+
102
+ **Analytical**, constant acceleration with no cap/contact:
103
+
104
+ ```text
105
+ v(t) = v0 + a·t
106
+ p(t) = p0 + v0·t + 0.5·a·t²
107
+ ```
108
+
109
+ That position is the continuous solution, not the exact discrete output.
110
+ At constant step, `t=N·dt`, Euler gives
111
+ `pN=p0+v0·t+0.5·a·(t²+t·dt)`. Multiplying by dt is necessary but does
112
+ not eliminate integration error.
113
+
114
+ **Demo Wind**: gravityX values −900..900 px/s², leaves with vertical gravity
115
+ 260 and a puck with gravity 0. Constant sideways acceleration is an arcade
116
+ wind field; it is not drag calculated from relative air velocity.
117
+
118
+ Sources: [Android Sprite.update](https://github.com/m1ga/ti.game/blob/3bea2f4/android/src/ti/game/engine/Sprite.java),
119
+ [iOS TGSprite.update](https://github.com/m1ga/ti.game/blob/3bea2f4/ios/Classes/TGSprite.m).
120
+
121
+ ## Thrust, inertia and linear damping
122
+
123
+ **Engine**:
124
+
125
+ ```text
126
+ rotation += angularVelocity·dt
127
+ f = (sin θ,−cos θ) # updated heading
128
+ v += f·thrust·dt
129
+ if |v| > maxSpeed: v *= maxSpeed/|v|
130
+ v += (gravityX,gravity)·dt
131
+ if linearDamping > 0:
132
+ v *= max(0,1−linearDamping·dt)
133
+ if |v| < 4 px/s: v = (0,0)
134
+ p += v·dt
135
+ ```
136
+
137
+ The thrust cap runs only in its active branch; direct velocity writes are
138
+ not capped. Asteroids uses 220 degrees/s turning and both thrust and speed
139
+ cap `0.9·min(W,H)`, with no ship damping. Turning changes the nose, not the
140
+ velocity already accumulated. Assisted flight can deliberately add damping,
141
+ but it is resistance everywhere, not contact-only friction.
142
+
143
+ **Analytical** damping: `dv/dt=−cv`, hence `v(t)=v0·exp(−ct)` and speed
144
+ half-life `ln(2)/c`. With constant aligned acceleration, `dv/dt=a−cv`
145
+ tends to `a/c`. The engine approximates damping using `1−c·dt`, then
146
+ applies its rest threshold. Box2D documents the same continuous decay but
147
+ uses `1/(1+c·dt)`; do not substitute that for ti.game's implemented formula.
148
+ [Box2D damping reference](https://box2d.org/documentation/md_simulation.html).
149
+
150
+ ### Powered launch below the rest threshold
151
+
152
+ At rest, thrust220 and damping 1.2 at a 1/60 s step give
153
+ `220/60·(1−1.2/60)=3.593` px/s. The threshold erases it before position
154
+ integration, every step. At120 FPS the problem also occurs; thrust100 can
155
+ be erased even at 30 FPS. This follows from the same formula on both platforms.
156
+
157
+ For an application that should brake only while coasting, set damping 0
158
+ while thrust is active and restore its chosen coefficient without thrust.
159
+ This changes powered-flight drag. If resistance must remain during thrust,
160
+ the intended correction belongs in the native rest policy, with tests of
161
+ accelerating bodies and resting bodies on both platforms. Do not invent a
162
+ per-frame JS position loop, give an artificial initial velocity, or increase
163
+ thrust solely to escape the cutoff.
164
+
165
+ Acceleration and resistance together are physically valid. The cutoff is
166
+ the problematic numerical condition. A flame animation proves a visual
167
+ state, not nonzero motion; different device behavior still requires FPS,
168
+ surface, input and installed-build evidence.
169
+
170
+ ### Coasting brake feel reference
171
+
172
+ **Application profile**, Titanium Asteroids autopilot, recorded 2026-10-12:
173
+ set `linearDamping: 0` while thrust is active and `linearDamping: 1.2` s⁻¹
174
+ when thrust stops. Native braking then multiplies velocity by
175
+ `max(0, 1-1.2*dt)`, with the installed engine's rest cutoff.
176
+
177
+ César liked the speed of this braking as a reference for other games,
178
+ especially arcade racing, although he did not choose it as the preferred
179
+ feel for Asteroids. Keep the coefficient and activation condition together
180
+ when comparing the feel. This is an optional tuning example, not a default
181
+ for all games or an upstream demo preset. In a racer, evaluate longitudinal
182
+ braking and lateral grip separately before adopting it.
183
+
184
+ ## Projectiles and additive impulses
185
+
186
+ **Demo Asteroids**, nose launch:
187
+
188
+ ```text
189
+ pBullet = pShip + f·0.55·SHIP_SIZE
190
+ vBullet = vShip + f·BULLET_SPEED
191
+ BULLET_SPEED = 1.2·min(W,H)
192
+ ```
193
+
194
+ Inherited ship velocity is a deliberate application choice. A fixed
195
+ world-speed projectile instead assigns only `f·speed`.
196
+
197
+ **Analytical** impulse: `Δv=J/m`. The demos add a chosen velocity change
198
+ directly; Sprite has no configurable rigid-body mass. `v+=Δv` bends existing
199
+ motion, whereas `v=vNew` replaces it. Pool strikes are additive, Flappy's
200
+ vertical flap is a replacement, and Drum's SHAKE replaces velocities while
201
+ its contact-driven TUMBLE adds increments.
202
+
203
+ ## Arcade car equations
204
+
205
+ **Engine carMode**, forward/right axes from the old heading:
206
+
207
+ ```text
208
+ vForward = v·f; vLateral = v·r
209
+ steerFactor = clamp(vForward/(0.25·maxSpeed),−1,1)
210
+ rotation += steering·turnRate·steerFactor·dt
211
+ vForward += throttle·enginePower·dt
212
+ vForward *= max(0,1−drag·dt)
213
+ vLateral *= max(0,1−grip·dt)
214
+ vForward = clamp(vForward,−0.4·maxSpeed,maxSpeed)
215
+ v = fNew·vForward + rNew·vLateral
216
+ ```
217
+
218
+ Recomposition uses the updated heading. A stopped car with zero forward
219
+ speed does not steer; reverse forward speed reverses steering direction.
220
+ Lower grip retains more lateral drift; drag reduces forward speed.
221
+ Negative throttle can brake and then reverse. It is an arcade model,
222
+ without wheelbase geometry, individual tires or weight transfer.
223
+
224
+ **Demo Racing** uses `maxSpeed=0.8·min(W,TH)`, where TH is track height,
225
+ `enginePower=1.1·maxSpeed`, turnRate 210, grip 3.2 and drag 0.8. These numeric
226
+ values have the units in the table above, not an interchangeable percentage.
227
+ No JS loop should rotate the velocity to reproduce this native model.
228
+
229
+ Sources: Android Sprite.updateCar and iOS TGSprite.updateCar, at the same
230
+ links as [Sprite integration](#sprite-integration-and-constant-acceleration).
231
+
232
+ ## Direct character control
233
+
234
+ **Demo Topdown** uses speed `S=3.5·TILE`. For cardinal input it assigns
235
+ `v=(moveX,moveY)·S`; for diagonal input each axis uses `S/sqrt(2)` so
236
+ the combined magnitude remains S. Releasing input writes zero velocity:
237
+ there is no ship-like inertia. For a general nonzero input vector d, the
238
+ application formula is `v=S·d/|d|`; distinguish this from analog magnitude
239
+ control, which may intentionally retain the stick's partial travel.
240
+
241
+ The demo dog normalizes its target direction at `1.1S` on a decision timer.
242
+ The timer chooses direction; native velocity integrates movement between
243
+ decisions. [Top-down recipe](recipes-action.md#top-down--zelda).
244
+
245
+ ## Jumps, flaps and scrolling runners
246
+
247
+ **Demo** Platformer assigns `vx=direction·RUN_SPEED` and, on permitted
248
+ grounded input, `vy=−JUMP`. Flappy assigns `vy=−0.6H` per tap; it does not
249
+ accumulate upward impulses. Its tilt tween is visual, not aerodynamic lift.
250
+ Skate keeps player x fixed and moves terrain/obstacles at `vx=−0.5W`.
251
+
252
+ **Analytical**, gravity `g>0`, takeoff speed magnitude `J`:
253
+
254
+ ```text
255
+ timeToApex = J/g
256
+ height = J²/(2g)
257
+ sameLevelFlightTime = 2J/g
258
+ J for desired height h = sqrt(2gh)
259
+ g for height h and apex time T = 2h/T²; J = 2h/T
260
+ ```
261
+
262
+ | Demo | g | J | Continuous prediction |
263
+ | --- | --- | --- | --- |
264
+ | Platformer | 2.2H | 0.95H | Apex 0.432 s; rise 0.205H |
265
+ | Skate | 2.4H | 0.9H | Apex 0.375 s; same-level flight 0.75 s |
266
+ | Flappy | 1.9H | 0.6H | Isolated flap apex0.316 s; rise 0.095H |
267
+ | Volley players | 2.2H | 0.85H | Apex 0.386 s; rise 0.164H |
268
+
269
+ Contact geometry, ceilings and discrete integration change actual results.
270
+ `oneWay` allows top landings through otherwise passable platforms; exact
271
+ conditions differ by resolver. Moving solids carry grounded riders using
272
+ their frame delta. Skate's raised scrolling road disables `carryRiders` so
273
+ the player is not dragged off screen. [Action recipes](recipes-action.md).
274
+
275
+ ## Landing and impact measurements
276
+
277
+ **Engine** `land` reports a grounding transition after solid response.
278
+ Its payload does not include the original velocity. Live `velocityY` can
279
+ already be zero or reflected, and JS event delivery can be later still.
280
+ Do not classify a hard touchdown as gentle by reading that zero value.
281
+
282
+ For sprite solids, `solidimpact.speed` snapshots relative closing normal
283
+ speed before response, compensated for the frame's own acceleration.
284
+ Set `impactThreshold:0` to include gentle positive impacts and check the
285
+ contact normal/group. It is not full vector speed or an exact post-gravity
286
+ pre-contact sample. A rule involving tangential velocity needs separate
287
+ evidence; solid TileLayer cells do not emit `solidimpact`.
288
+
289
+ The [Lunar Lander recipe](recipes-action.md#lunar-lander) uses a static sprite
290
+ pad and this normal-impact contract. A resting body with no positive impact
291
+ does not receive an invented impact merely because its threshold is0.
292
+
293
+ Sources: Android `Scene.resolveSolids`, `dispatchSolidImpact`, `finishContacts`;
294
+ iOS `TGScene.resolveSolids`, `dispatchSolidImpactFor`, `finishContactsFor`;
295
+ see [event payload](api.md#sprite-events).
296
+
297
+ ## Static reflection and arcade ball sports
298
+
299
+ **Engine**, circle against a static solid, outward normal:
300
+
301
+ ```text
302
+ e = max(eMover,eSolid)
303
+ vn = v·n
304
+ if vn < 0:
305
+ if e > 0 and −e·vn > 40 px/s: v -= (1+e)·vn·n
306
+ else: v -= vn·n
307
+ ```
308
+
309
+ This preserves tangential velocity. The small-rebound cutoff belongs to
310
+ this response, not the equal-mass bilateral impulse below. No contact
311
+ friction or collision-generated spin is implied.
312
+
313
+ **Demo Volley** replaces the ball's velocity on player contact:
314
+ `u=normalize(pBall−pPlayer)`, `S=0.85H`,
315
+ `vx=u.x·S`, `vy=min(u.y·S,−0.45S)`. It guarantees an upward component
316
+ instead of simulating two masses. Scenario walls use native restitution;
317
+ player hits use an arcade rule. Plinko uses gravity 1100, restitution 0.42,
318
+ circle pegs and `push` chips.
319
+
320
+ Sources: [Android Scene.resolveCircleSolids](https://github.com/m1ga/ti.game/blob/3bea2f4/android/src/ti/game/engine/Scene.java),
321
+ [iOS TGScene.resolveCircleSolids](https://github.com/m1ga/ti.game/blob/3bea2f4/ios/Classes/TGScene.m).
322
+
323
+ ## Equal-mass billiards and circular containment
324
+
325
+ **Engine**, two visible circles, both `push`, each listing the other's group:
326
+
327
+ ```text
328
+ n = normalize(centerA−centerB)
329
+ penetration = radiusA+radiusB−distance
330
+ if penetration > slop: pA+=n·penetration/2; pB−=n·penetration/2
331
+ vn = (vA−vB)·n
332
+ e = max(eA,eB)
333
+ if vn < 0:
334
+ j = −(1+e)·vn/2
335
+ vA += j·n; vB -= j·n
336
+ ```
337
+
338
+ Each pair resolves once. Restitution1 exchanges normal components; tangent
339
+ components remain unchanged. Even differently sized circles are equal-mass
340
+ bodies. Without mutual groups the response falls back to a one-sided solid.
341
+
342
+ Pool uses restitution 0.96, damping 0.62, and a cue velocity increment
343
+ `2600·min(1,tapDistance/(0.55W))` along the tap direction. That limits the
344
+ strike increment, not total speed when a moving cue is hit again. Pockets
345
+ are triggers, not 3D drops.
346
+
347
+ **Analytical**, unequal masses would require
348
+ `j=−(1+e)·vn/(1/mA+1/mB)`, then `vA+=j·n/mA`, `vB−=j·n/mB`.
349
+ That is not the current `push` implementation.
350
+
351
+ **Engine contain**: a ball of radius r inside a container of radius R has
352
+ allowed center distance `R−r`. If exceeded, the correcting normal points
353
+ toward the container center and penetration is `distance−(R−r)`.
354
+ The ball must fit. Drum uses gravity 1400, restitution 0.55, damping 0.35 and
355
+ mutual `push` balls. It mixes impulses/contact response, not a spinning
356
+ rigid drum or fluid simulation. [Shaped-solid patterns](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs).
357
+
358
+ ## Slopes and contact friction
359
+
360
+ Rotated ramps require `rotatedRect` geometry for the real face normal.
361
+ The normal component is cancelled/reflected while tangent velocity remains.
362
+ **Analytical**, on a fixed frictionless slope at angle α from horizontal,
363
+ downslope acceleration magnitude is `g·abs(sin α)`.
364
+
365
+ Slopes uses gravity 1100, crate restitution 0, ball0.1 and floor/walls0.5.
366
+ Its ball slides; a circle shape does not add rolling torque. Sprite damping
367
+ acts off the slope too and cannot substitute contact-only tangent friction.
368
+ Smooth uphill walking, connected terrain, mass and torque remain outside
369
+ this model. [Limitations](roadmap.md).
370
+
371
+ ## Verlet ropes and tethers
372
+
373
+ **Engine**, a free node:
374
+
375
+ ```text
376
+ displacement = (pCurrent−pPrevious)·damping
377
+ pPrevious = pCurrent
378
+ pCurrent += displacement + (0,gravity·dt²)
379
+ ```
380
+
381
+ For adjacent nodes, `Δ=pB−pA`, `d=|Δ|`, target length L, correction
382
+ `Δ·(d−L)/d` is split between free nodes, or applied fully to the free node
383
+ when the other is pinned. Division has an epsilon guard. `iterations` repeats
384
+ relaxation: more passes stiffen the result at a CPU cost.
385
+
386
+ The first Rope demo uses 14 links of length 0.04H, gravity 1.6H,
387
+ damping 0.985 and 3 constraint passes. Nodes do not automatically receive
388
+ Sprite solid collisions. `tail` plus positive `maxLength` additionally
389
+ clamps head-to-tail distance and removes outward radial velocity. With two
390
+ free sprite ends the correction is split; dragging an end makes the other
391
+ yield. It is a positional tether, not a mass/stiffness Hooke spring.
392
+
393
+ Rope damping is **per update**, not1/s. Before forces and constraints, the
394
+ retained displacement factor over one second at 0.985 would be about 0.635
395
+ at 30 updates,0.404 at 60 and 0.163 at 120. These are not predictions of the
396
+ whole rope's amplitude. An analytical time-normalized alternative would be
397
+ `exp(−c·dt)` or `dRef^(dt/dtRef)`; the engine uses the fixed supplied factor.
398
+
399
+ At the pinned source, Rope.update has no zero-delta early return. Existing
400
+ Verlet displacement and constraints still run at dt 0; `timeScale=0` is not
401
+ a guarantee that rope nodes freeze. This differs from merely setting the
402
+ gravity term to zero. Scene/hardware acceptance is still needed for a
403
+ pause design; stopping the render loop with `pause()` is a different action
404
+ because it also stops rendering. Do not silently change the global pause
405
+ UX to avoid this boundary.
406
+
407
+ Sources: [Android Rope.update](https://github.com/m1ga/ti.game/blob/3bea2f4/android/src/ti/game/engine/Rope.java),
408
+ [iOS TGRope.update](https://github.com/m1ga/ti.game/blob/3bea2f4/ios/Classes/TGRope.m).
409
+
410
+ ## Swept movement and periodic worlds
411
+
412
+ **Analytical** per-step movement is `Δp=v·dt`:9600 px/s travels160 px
413
+ at 60 FPS. A final-position overlap can skip a thin wall.
414
+ Swept tests examine the traveled segment; circle pairs have circular sweeps,
415
+ other shape pairs use their respective geometry/approximations. The solid
416
+ sweep skips `contain`/`push`; those still use discrete responses. It is not
417
+ a general multiple-substep rigid-body simulation.
418
+
419
+ Raycast is an instantaneous query, not a gravity-driven projectile.
420
+ `wrapAround` uses screen edges; `wrapX/wrapShift` loops scrolling copies;
421
+ `worldWrapX` defines a periodic horizontal world with participating sprites.
422
+ **Analytical** normalization for `L=maxX−minX` is
423
+ `x=minX+((x−minX)%L+L)%L`; nearest image displacement is
424
+ `dx−L·round(dx/L)`. Raycast, A*, particles, ropes and skid trails do not
425
+ repeat at the seam. [Worldwrap contract](api.md#physics).
426
+
427
+ ## Paths, tweens and navigation
428
+
429
+ **Engine Path** accumulates `distance+=speed·dt`. Within a segment,
430
+ `u=(distance−segmentStart)/segmentLength`,
431
+ `p=pStart+u·(pEnd−pStart)`. Optional rotation is
432
+ `atan2(dx,−dy)·180/π`. A* chooses the waypoints; it does not apply a force.
433
+ Maze/Tilemap use4·TILE px/s. Path motion overrides integrated position;
434
+ a later position tween can override the path again.
435
+
436
+ **Engine Tween** uses `u=clamp(elapsed/duration,0,1)` after its delay,
437
+ then `value=from+(to−from)·Easing(u)`. Cards, puzzles and Basic use
438
+ prescribed destinations/gestures, not conservation of momentum.
439
+ Camera/parallax, blend, text, flip and pixel snapping are presentation;
440
+ they do not change those motion equations. Snapping rounds the drawn anchor,
441
+ not physical position, and cannot eliminate every rotational raster change.
442
+
443
+ Sources: [Path.advance](https://github.com/m1ga/ti.game/blob/3bea2f4/android/src/ti/game/engine/Path.java),
444
+ [Tween.update](https://github.com/m1ga/ti.game/blob/3bea2f4/android/src/ti/game/engine/Tween.java).
445
+
446
+ ## Rhythm timing
447
+
448
+ **Demo**, `vy=(PAD_Y+NOTE)/1.6`, starting at `y=−NOTE`, gives a 1.6 s
449
+ trip to the pad. GOOD allows 0.08H distance and PERFECT0.03H.
450
+ **Analytical** temporal error is `abs(y−PAD_Y)/abs(vy)`; changing fall speed
451
+ changes the milliseconds represented by a fixed pixel window.
452
+
453
+ At the demo's stated 150 BPM, quarter duration is `60000/150=400 ms`;
454
+ an eighth is200 ms. The400 ms spawn interval and 1.6s travel are one and four
455
+ quarters, respectively. The upstream comments call them eighths, and the
456
+ old action recipe repeated that error. The corrected recipe preserves timing.
457
+
458
+ This is a comment/recipe error, not evidence of a faulty native beat engine.
459
+ The demo uses `setInterval(spawn,SPAWN_MS)` and independent `music.play()`;
460
+ it has no audio-position-driven scheduling. Exact musical synchronization,
461
+ actual asset tempo and phase would need separate measurement. A comment-only
462
+ upstream patch should not halve SPAWN_MS or replace the motor.
463
+
464
+ Source: [Rhythm demo](https://github.com/m1ga/ti.game/blob/3bea2f4/example/rhythm.js).
465
+
466
+ ## Particle integration
467
+
468
+ **Engine** accumulates `rate·dt`, emits the integer part and keeps the
469
+ remainder. Live particles age, receive `vy+=gravity·dt`, then position
470
+ updates with the new velocity. At lifetime they retire. They are effects,
471
+ not Sprite solids or automatic damage sensors. [Particle recipes](recipes-adventure-effects.md#particles).
472
+
473
+ ## Time step and frame rate
474
+
475
+ Both renderers derive a variable real dt, cap it at 0.1s and then scale by
476
+ `max(0,timeScale)`. They do not run a fixed-step accumulator with physics
477
+ substeps. `maxFps` is a cap, not a guarantee of exact dt. Differences can
478
+ come from Euler error, rest/rebound thresholds, per-update Rope damping,
479
+ surface-based parameters, input/lifecycle or different installed binaries.
480
+
481
+ GameView timers follow scene time, conventional JS timers do not. Rendering
482
+ and touch continue under timeScale 0; the Rope exception above prevents an
483
+ unqualified promise that every position stays unchanged.
484
+
485
+ For tuning, capture the build, actual FPS, W/H, timeScale, forces and
486
+ velocities. Check rest-to-powered motion, releases, turns, contacts and pauses
487
+ at 30/60/120 FPS, then validate on the target hardware. Scalar equations and
488
+ source parity are useful evidence, not certificates of identical device feel.
489
+ [Telemetry workflow](debugging-performance.md).
490
+
491
+ Sources: [Android SceneRenderer](https://github.com/m1ga/ti.game/blob/3bea2f4/android/src/ti/game/engine/SceneRenderer.java),
492
+ [iOS TGSceneRenderer](https://github.com/m1ga/ti.game/blob/3bea2f4/ios/Classes/TGSceneRenderer.m).
493
+
494
+ ## What needs a different model
495
+
496
+ Assisted spacecraft flight can use deliberate resistance, but it is not
497
+ airplane aerodynamics. A fuller aircraft model needs relative airspeed,
498
+ lift, drag, stall, altitude and bank response. Ships, tires, springs,
499
+ variable masses and collision torque also need separate modeling choices.
500
+ Do not claim those simulations from a sprite's artwork or camera angle.
501
+ See [roadmap.md](roadmap.md) before naming a missing API, and retain the
502
+ distinction between analytical tuning, application rules and native support.
@@ -78,7 +78,7 @@ ti build -p ios --build-only # macOS only → ios/dist/ti.game-iphone-<ve
78
78
 
79
79
  ## Classic projects
80
80
 
81
- `require('ti.game')` anywhere and add the GameView to a window. The 33 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):
81
+ `require('ti.game')` anywhere and add the GameView to a window. The 34 demos in the module's `example/` folder are Classic, each one a CommonJS module exporting a start function (`app.js` is the launcher and `backnav.js` is a helper, neither is a demo):
82
82
 
83
83
  ```javascript
84
84
  // Resources/flappy.js
@@ -332,6 +332,28 @@ function unloadLevel(level) {
332
332
 
333
333
  A single-level game with a handful of sheets should not call it at all: the textures die with the window.
334
334
 
335
+ ### Retained iOS scenes and multiple GameViews
336
+
337
+ In the pinned iOS source, `TiGameGameView.didMoveToWindow` calls
338
+ `shutdownRendering` when an attached view loses its window. That teardown
339
+ sets a terminal shutdown flag and releases the renderer. Reattaching the
340
+ same proxy or calling resume is not a documented reconstruction mechanism.
341
+ If a navigation/modal presentation detaches a backdrop that must remain
342
+ alive, keep that GameView attached or create a fresh one on return; rebuilding
343
+ only its Sprite session is insufficient. Verify the actual UIKit presentation
344
+ and installed module on-device.
345
+
346
+ Multiple retained iOS GameViews also share each physical GCController's
347
+ single `extendedGamepad.valueChangedHandler`. At this pin, each view's
348
+ TGGamepadController installs its own handler and detach clears it. Another
349
+ view can therefore replace or clear an active scene's receiver. Use explicit
350
+ input ownership and test open/close transitions; do not assume independent
351
+ gamepad subscriptions or treat a local fork fix as shipped upstream.
352
+
353
+ Sources: upstream `ios/Classes/TiGameGameView.m` (`didMoveToWindow`,
354
+ `shutdownRendering`) and `ios/Classes/TGGamepadController.m`
355
+ (`attachController`, `detachController`) at `3bea2f4`.
356
+
335
357
  ### LiveView reloads
336
358
 
337
359
  A LiveView reload replaces Titanium's JS runtime while the native side survives, which used to leave the previous `GameView`'s render loop running — every reload added another GL thread, and the audio engine handed the new runtime sample ids the old one still owned.
@@ -2,6 +2,10 @@
2
2
 
3
3
  Production-oriented patterns distilled from the upstream action demos at `ti.game@3bea2f4`. Use [examples.md](examples.md) to locate the full runnable demo and [api.md](api.md) for exact defaults and payloads. The wall-contact API is source/README-backed because upstream has not added a wall-jump demo yet.
4
4
 
5
+ Use [physics.md](physics.md) for the implemented equations, units, jump tuning,
6
+ inertia versus direct control, and numerical limits. Recipes select native
7
+ mechanisms; they do not replace that model reference.
8
+
5
9
  <!-- TOC-START -->
6
10
  ## Contents
7
11
 
@@ -19,7 +23,7 @@ Production-oriented patterns distilled from the upstream action demos at `ti.gam
19
23
 
20
24
  ## Tap-to-flap
21
25
 
22
- Gravity plus an impulse per tap. Scoring and death are collisions with invisible zones. The rotation tween is what sells the arc.
26
+ Gravity plus a replacement vertical velocity per tap, not an accumulated impulse. Scoring and death are collisions with invisible zones. The rotation tween is what sells the arc.
23
27
 
24
28
  ```javascript
25
29
  const pig = Game.createSprite({
@@ -236,8 +240,9 @@ let lastFacing = 'down';
236
240
  let walking = false;
237
241
 
238
242
  function applyMovement() {
239
- player.velocityX = moveX * SPEED;
240
- player.velocityY = moveY * SPEED;
243
+ const speed = (moveX !== 0 && moveY !== 0) ? SPEED / Math.sqrt(2) : SPEED;
244
+ player.velocityX = moveX * speed;
245
+ player.velocityY = moveY * speed;
241
246
  if (moveX === 0 && moveY === 0) {
242
247
  player.stop();
243
248
  walking = false;
@@ -418,7 +423,13 @@ Rocks are round (`hitboxShape: 'circle'`), drift on random velocities, spin on `
418
423
 
419
424
  ## Lunar lander
420
425
 
421
- Same flight model as Asteroids, with gravity on and a landing test. There is no "landing" event — you judge the touchdown yourself in the `land` handler, where velocities are still readable.
426
+ Same flight model as Asteroids, with gravity and a touchdown rule. This is a
427
+ curated recipe, not one of the 34 upstream demos. `land` reports grounding
428
+ after the solid response: live velocity may already be zero or reflected.
429
+ For a static sprite pad, use the snapshotted normal speed in `solidimpact`.
430
+ It is compensated for this frame's acceleration, not the full incoming
431
+ velocity vector. Tiles do not emit that event; use a sprite pad when the
432
+ impact must be measured. See [landing evidence](physics.md#landing-and-impact-measurements).
422
433
 
423
434
  ```javascript
424
435
  const lander = Game.createSprite({
@@ -428,6 +439,7 @@ const lander = Game.createSprite({
428
439
  gravity: world.gravity, // per-world, e.g. H * 0.35 for the Moon
429
440
  maxSpeed: MAX_SPEED,
430
441
  hitboxScale: 0.8,
442
+ impactThreshold: 0, // include gentle physical impacts
431
443
  solidWith: ['terrain', 'pad'],
432
444
  collidesWith: ['hazard']
433
445
  });
@@ -435,12 +447,12 @@ const lander = Game.createSprite({
435
447
  const MAX_LANDING_SPEED = H * 0.18;
436
448
  const MAX_LANDING_TILT = 12; // degrees
437
449
 
438
- lander.addEventListener('land', (e) => {
439
- const speed = Math.abs(lander.velocityY);
450
+ lander.addEventListener('solidimpact', (e) => {
451
+ const speed = e.speed; // immutable contact payload, not live velocity
440
452
  const tilt = Math.abs(((lander.rotation % 360) + 540) % 360 - 180);
441
453
  const onPad = e.group === 'pad';
442
454
 
443
- if (onPad && speed <= MAX_LANDING_SPEED && tilt <= MAX_LANDING_TILT) {
455
+ if (onPad && e.normalY < -0.7 && speed <= MAX_LANDING_SPEED && tilt <= MAX_LANDING_TILT) {
444
456
  succeed();
445
457
  return;
446
458
  }
@@ -458,12 +470,17 @@ function crash() {
458
470
  }
459
471
  ```
460
472
 
461
- Thrust burns fuel on the same coarse tick that reads the controls — 80 ms is fast enough to feel responsive and slow enough to stay off the bridge:
473
+ Fuel and control decisions use a coarse game-clock tick. `BURN_RATE` is fuel
474
+ units per game second; each nominal tick removes the rate times its interval.
475
+ GameView timers can fire late and do not catch up after stalls, so this is a
476
+ cadence approximation, not exact elapsed-time accounting. Store the timer ID
477
+ and cancel it with `gameView.cancelTimer(timer)` on cleanup:
462
478
 
463
479
  ```javascript
464
- timer = setInterval(() => {
480
+ const CONTROL_MS = 80;
481
+ timer = gameView.every(CONTROL_MS, () => {
465
482
  if (mainThrusterHeld && fuel > 0) {
466
- fuel -= BURN_RATE;
483
+ fuel = Math.max(0, fuel - BURN_RATE * CONTROL_MS / 1000);
467
484
  lander.thrust = THRUST;
468
485
  exhaust.emitting = true;
469
486
  } else {
@@ -471,10 +488,13 @@ timer = setInterval(() => {
471
488
  exhaust.emitting = false;
472
489
  }
473
490
  fuelLabel.text = Math.max(0, Math.round(fuel)) + '%';
474
- }, 80);
491
+ });
475
492
  ```
476
493
 
477
- Wind is just a horizontal acceleration you re-apply on that same tick by nudging `velocityX`; per-world gravity, drag and wind are plain numbers in a catalog object.
494
+ Set `lander.gravityX = world.wind` in px/s² when the wind changes. Native
495
+ integration applies it every frame; do not add velocity repeatedly from a JS
496
+ timer. For lateral-velocity limits or exact vector touchdown tests, normal
497
+ impact speed alone is insufficient: design a separate measurement contract.
478
498
 
479
499
  ## Endless runner
480
500
 
@@ -599,7 +619,15 @@ function hitLane(lane, pad) {
599
619
  }
600
620
  ```
601
621
 
602
- An invisible `misszone` trigger below the pads catches gems that got away and counts the miss. Derive the fall speed from the beat: `FALL_SPEED = (PAD_Y + NOTE) / 1.6` with a 400 ms spawn interval puts four eighth notes between spawn and pad at 150 BPM.
622
+ An invisible `misszone` trigger below the pads catches gems that got away and
623
+ counts the miss. `FALL_SPEED = (PAD_Y + NOTE) / 1.6` gives a 1.6 s trip from
624
+ `-NOTE` to `PAD_Y`. At the stated 150 BPM, `60000 / 150 = 400` ms is a quarter
625
+ note; 1.6 s is four quarter notes (eight eighths). The upstream comments say
626
+ "eighth" but the timing constants describe quarters. Correct that wording,
627
+ not the engine or the demo's timing without a separate design decision.
628
+ Position windows convert to time with `windowSeconds = windowPx / FALL_SPEED`.
629
+ The demo uses a JS interval and independent music playback, not an audio-clock
630
+ beat scheduler; see [rhythm timing](physics.md#rhythm-timing).
603
631
 
604
632
  ## Ball sports / breakout physics
605
633
 
@@ -634,7 +662,11 @@ ball.addEventListener('collision', (e) => {
634
662
 
635
663
  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.
636
664
 
637
- 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](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs).
665
+ A `restitution: 1` ball reflects larger impacts without restitution loss;
666
+ the small-bounce cutoff can still settle it. Restitution can live on the
667
+ floor, so one springy platform can bounce otherwise non-bouncy riders.
668
+ Round obstacles, mutual push bodies and pool damping are in
669
+ [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs).
638
670
 
639
671
  **Opponent AI** on an 80 ms timer, reading a handful of live properties per tick:
640
672