@waica/engine 0.14.1 → 0.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 CHANGED
@@ -6,6 +6,52 @@ Waica's public engine core: entities and components, the game loop, scene and pr
6
6
  import { Component, Game, loadScene } from '@waica/engine'
7
7
  ```
8
8
 
9
+ ## Hitbox Collision Layers and Masks
10
+
11
+ Every `Hitbox` belongs to one named Collision Layer and declares the other layers in which it is interested through a Collision Mask:
12
+
13
+ ```ts
14
+ import { Hitbox } from '@waica/engine'
15
+
16
+ entity.add(Hitbox, {
17
+ layer: 'projectile',
18
+ collidesWith: ['enemy'],
19
+ })
20
+ ```
21
+
22
+ A layer must match `^[a-z][a-z0-9-]*$`. Layer names form an open, project-owned vocabulary; matching is exact and case-sensitive. `"*"` is valid only in a mask and matches every valid layer. An empty mask means no outgoing interest. Invalid layers cannot be targeted, and invalid or non-string mask entries are ignored without runtime coercion or warnings.
23
+
24
+ A pair reaches exact overlap testing when either side's mask names the other side's layer. After an overlap, only an interested side receives `onCollide`; a one-way projectile mask therefore does not notify its target. Omitted fields retain the compatibility defaults `layer: 'default'` and `collidesWith: ['*']`, so two unconfigured Hitboxes still notify both sides.
25
+
26
+ Layers and masks are read while the collision-pair snapshot is consumed. A component can change them during `onUpdate` or one callback and affect later pairs in that Simulation Step. Pair order remains the original lexicographic Entity order, component callbacks remain in insertion order, and destroyed entities are still skipped between sides.
27
+
28
+ ### Shipped taxonomy and migration
29
+
30
+ Waica's shipped prefabs use this directional policy:
31
+
32
+ | Hitbox owner | `layer` | `collidesWith` |
33
+ | --- | --- | --- |
34
+ | Player | `player` | `['*']` |
35
+ | Slime, blob, or orc / `Hazard` | `enemy` | `['player']` |
36
+ | Coin, potion, or crate / `Collectible` | `collectible` | `['player']` |
37
+ | Overlap `SceneTransition` | `scene-transition` | `['player']` |
38
+ | Platformer projectile | `projectile` | `['enemy']` |
39
+
40
+ `Collectible`, `Hazard`, overlap `SceneTransition`, and the example projectile trust those masks and no longer recheck the other Entity's role inside `onCollide`. Existing external projects using any of these handlers must explicitly migrate their sibling Hitboxes before relying on the new behavior. The defaults preserve engine-level callback delivery, but a wildcard-backed migrated handler can act on unintended default-layer entities.
41
+
42
+ 1. Assign `player` / `['*']` to player Hitboxes.
43
+ 2. Assign the relevant row above to each shipped handler carrier, or choose equivalent project-owned names.
44
+ 3. Run MCP `validate_project`; malformed values are errors and duplicate mask entries are warnings.
45
+ 4. Keep `trigger: 'interact'` Scene Transitions unchanged—their Hitbox mask is relevant only to overlap mode.
46
+
47
+ The editor and MCP author explicit categories for newly generated player/enemy identities. NPCs, custom or identity-less characters, generic objects, and existing external project files are not inferred or rewritten. `public/waica.params.json` may override either field with the same exact values, including a `string[]` mask.
48
+
49
+ ## Collision broadphase
50
+
51
+ The Game uses fresh internal uniform grids to accelerate automatic Hitbox dispatch, Hitbox-backed `area`/`point` queries, and Solid-backed `ray` queries. Hitboxes and Solids stay in separate domains. `nearest`, `DynamicBody` physical-contact solving, Pointer picking, navigation, and unrelated scans remain linear or otherwise unchanged.
52
+
53
+ The grid changes candidate discovery only: existing exact geometry is still authoritative. Each operation observes candidates alive at its start, preserves Entity or Solid source order and tie behavior, and recomputes after scene swaps, spawns, movement, shape edits, or Tilemap-derived Solid changes. Oversized bodies and query regions fall back conservatively, so they may cost more but cannot lose results. Grid sizing, occupancy limits, indices, and rebuild controls are deliberately package-internal and have no public tuning API.
54
+
9
55
  ## Logical spatial queries
10
56
 
11
57
  Every `Game` owns one stable `game.query` service. Queries use logical XY coordinates, including in isometric scenes, and return typed live engine objects:
@@ -45,7 +91,7 @@ Calls eagerly snapshot candidates that are alive at call start. Results preserve
45
91
 
46
92
  Invalid inputs fail closed without throwing: invalid `area` bodies and non-finite point coordinates return `[]`; invalid nearest coordinates or radii return `null`; and ray returns `null` for non-finite values, a zero direction, or a negative distance. Nearest permits positive `Infinity`; ray distance must be finite and may be zero. Zero-area candidate geometry never matches.
47
93
 
48
- Area and point intentionally use the collision system's polygonally approximated circle/ellipse outline. Ray queries intersect circle-shaped Solids as analytic ellipses and return their exact outward unit normal.
94
+ Area and point intentionally use the collision system's polygonally approximated circle/ellipse outline. Ray queries intersect circle-shaped Solids as analytic ellipses and return their exact outward unit normal. Collision Layers and Masks never filter `area` or `point`; use a query filter when category-like eligibility is needed.
49
95
 
50
96
  ## Component lifecycle
51
97
 
@@ -124,3 +170,26 @@ class PathFinder extends Component {
124
170
  ```
125
171
 
126
172
  The return value still passes through the bounded safe projector; it is not serialized with arbitrary `toJSON()`. The package root exports the Runtime Snapshot, projection marker, metadata, control and activation types plus `RUNTIME_BRIDGE_PROTOCOL_VERSION` and `RUNTIME_PROJECTION_LIMITS`.
173
+
174
+ ## game.time: simulated timers and tweens
175
+
176
+ Every `Game` owns a `time: GameTime` service — `after`, `every`, a single-number `tween`, and a `now` reader — advanced only by Simulation Steps (ADR 0014), never by the wall clock:
177
+
178
+ ```ts
179
+ const stun = game.time.after(0.3, () => fsm.goto('idle'), { owner: entity })
180
+ game.time.every(1, () => game.stats.set('clock', game.time.now), { scope: 'session' })
181
+ game.time.tween({
182
+ from: 0, to: 1, seconds: 0.5, easing: 'quadOut',
183
+ onUpdate: (v) => { overlay.opacity = v },
184
+ onComplete: () => game.loadSceneByName('b'),
185
+ owner: entity,
186
+ })
187
+ stun.remaining // seconds left, counting down
188
+ game.time.now // seconds of Game Time since the Game started
189
+ ```
190
+
191
+ - **`after(seconds, callback, options?)`** runs `callback` once, `seconds` of Game Time later (0 or a negative duration: the start of the next step). **`every(seconds, callback, options?)`** runs it every `max(seconds, 1/60)`, first one interval after creation, with no drift. **`tween(options)`** carries `from` to `to` over `seconds`, calling `onUpdate(value)` synchronously on creation and again on every later step, finishing with exactly `to` then `onComplete()`. Easing is `'linear'` (default), `'quadIn'`/`'quadOut'`/`'quadInOut'`, `'cubicIn'`/`'cubicOut'`/`'cubicInOut'`, `'sineInOut'`, or a custom `(t) => number`.
192
+ - All three return a `TimerHandle`: `cancel()`, `active`, `elapsed`, `remaining` (seconds of Game Time). `cancel()` — and owner/scene cancellation — leaves a tween's last applied value in place and never calls `onComplete`.
193
+ - **Step placement.** At the start of every Simulation Step, before the Component Update Schedule, `game.time` advances `now`, runs every due timer (due time, then creation order), then advances every tween that existed before that step (creation order). Work a callback creates is never run or advanced in that same step.
194
+ - **Scope.** A timer or tween is scene-scoped by default: `unloadScene()` and every scene load — including a Game's first — cancel it, running no callback. `{ scope: 'session' }` survives a scene change. `{ owner: entity }` cancels it immediately when that entity is destroyed, whatever its scope; an owner already dead at scheduling time yields an inactive handle. `game.dispose()` cancels everything in both scopes. This is the opposite default from `game.onUpdate`/`game.events` (ADR 0011), which survive a scene change by construction, and the same one `game.audio.play()` uses (ADR 0012) — see ADR 0017 for why timers follow audio's rule rather than the host-subscription one: a timer's callback almost always closes over the scene that scheduled it.
195
+ - **No Promises.** Nothing here returns one, and nothing is async — a `.then` continuation is not step-exact (it runs after the whole synchronous frame), which is exactly what `game.time` exists to avoid. Compose delays with `after`, not `await`.
@@ -0,0 +1,4 @@
1
+ /** Package-internal runtime syntax check for one Collision Layer. */
2
+ export declare function validCollisionLayer(value: unknown): value is string;
3
+ /** Package-internal exact directional interest check. */
4
+ export declare function collisionMaskTargets(mask: unknown, targetLayer: unknown): boolean;
@@ -0,0 +1,11 @@
1
+ const COLLISION_LAYER_PATTERN = /^[a-z][a-z0-9-]*$/;
2
+ /** Package-internal runtime syntax check for one Collision Layer. */
3
+ export function validCollisionLayer(value) {
4
+ return typeof value === 'string' && COLLISION_LAYER_PATTERN.test(value);
5
+ }
6
+ /** Package-internal exact directional interest check. */
7
+ export function collisionMaskTargets(mask, targetLayer) {
8
+ if (!validCollisionLayer(targetLayer) || !Array.isArray(mask))
9
+ return false;
10
+ return mask.some((entry) => typeof entry === 'string' && (entry === '*' || entry === targetLayer));
11
+ }
@@ -0,0 +1,8 @@
1
+ import type { Game } from './game.js';
2
+ /** Package-internal deterministic work receipt used by focused tests. */
3
+ export interface CollisionDispatchStats {
4
+ readonly candidatePairs: number;
5
+ readonly narrowphaseCalls: number;
6
+ }
7
+ /** Game's package-internal trigger dispatch implementation. */
8
+ export declare function dispatchCollisions(game: Game): CollisionDispatchStats;
@@ -0,0 +1,36 @@
1
+ import { collisionBody } from './collision-body.js';
2
+ import { collisionMaskTargets } from './collision-category.js';
3
+ import { collisionOverlap } from './collision-shape.js';
4
+ import { Hitbox } from './components/hitbox.js';
5
+ import { createHitboxBroadphase } from './spatial-broadphase.js';
6
+ /** Game's package-internal trigger dispatch implementation. */
7
+ export function dispatchCollisions(game) {
8
+ const pairs = createHitboxBroadphase(game).pairs();
9
+ let narrowphaseCalls = 0;
10
+ for (const [first, second] of pairs) {
11
+ const a = first.entity;
12
+ const b = second.entity;
13
+ if (!a.alive || !b.alive)
14
+ continue;
15
+ if (a.get(Hitbox) !== first.hitbox || b.get(Hitbox) !== second.hitbox)
16
+ continue;
17
+ const notifyA = collisionMaskTargets(first.hitbox.collidesWith, second.hitbox.layer);
18
+ const notifyB = collisionMaskTargets(second.hitbox.collidesWith, first.hitbox.layer);
19
+ if (!notifyA && !notifyB)
20
+ continue;
21
+ narrowphaseCalls += 1;
22
+ if (!collisionOverlap(collisionBody(first.hitbox), collisionBody(second.hitbox)))
23
+ continue;
24
+ if (notifyA) {
25
+ for (const component of [...a.components])
26
+ component.onCollide?.(b);
27
+ }
28
+ if (!a.alive || !b.alive)
29
+ continue;
30
+ if (notifyB) {
31
+ for (const component of [...b.components])
32
+ component.onCollide?.(a);
33
+ }
34
+ }
35
+ return { candidatePairs: pairs.length, narrowphaseCalls };
36
+ }
@@ -7,6 +7,8 @@ export interface ParamSpec {
7
7
  min?: number;
8
8
  max?: number;
9
9
  step?: number;
10
+ /** Specialized editor control for values that are otherwise plain JSON. */
11
+ kind?: 'string-list';
10
12
  /** Allowed values for a string param; rendered as a dropdown. Takes precedence over ref. */
11
13
  options?: string[];
12
14
  /** Project value this string param names; rendered and validated as a typed reference. */
@@ -65,7 +67,7 @@ export declare abstract class Component {
65
67
  onUpdate?(dt: number): void;
66
68
  /** Runs after the scene changes between identity and projected rendering. */
67
69
  onProjectionChange?(projection: 'isometric' | null): void;
68
- /** Runs when this entity's Hitbox overlaps another one's. */
70
+ /** Runs on overlap when this entity's Hitbox mask names the other's layer. */
69
71
  onCollide?(other: Entity): void;
70
72
  /** Runs when this entity's DynamicBody physically contacts a Solid. */
71
73
  onContact?(contact: SolidContact): void;
@@ -1,13 +1,21 @@
1
1
  import { Component } from '../component.js';
2
2
  import { type CollisionPoint, type CollisionShape } from '../collision-shape.js';
3
3
  /**
4
- * Trigger collider: the Game detects overlaps between Hitboxes and calls
5
- * onCollide(other) on both entities' components. For static physical
4
+ * Trigger collider. A pair is tested when either Hitbox's Collision Mask
5
+ * names the other's Collision Layer; only each interested owner receives
6
+ * `onCollide`. Spatial Queries ignore categories. For static physical
6
7
  * collision see Solid.
7
8
  */
8
9
  export declare class Hitbox extends Component {
9
10
  static componentName: string;
10
11
  static params: {
12
+ layer: {
13
+ label: string;
14
+ };
15
+ collidesWith: {
16
+ label: string;
17
+ kind: 'string-list';
18
+ };
11
19
  offsetX: {
12
20
  label: string;
13
21
  };
@@ -15,6 +23,10 @@ export declare class Hitbox extends Component {
15
23
  label: string;
16
24
  };
17
25
  };
26
+ /** One exact, case-sensitive `^[a-z][a-z0-9-]*$` membership name. */
27
+ layer: string;
28
+ /** Outgoing layer interest; `'*'` means every valid layer and `[]` means none. */
29
+ collidesWith: string[];
18
30
  shape: CollisionShape;
19
31
  width: number;
20
32
  height: number;
@@ -1,16 +1,23 @@
1
1
  import { Component } from '../component.js';
2
2
  import { resolveCollisionPoints, } from '../collision-shape.js';
3
3
  /**
4
- * Trigger collider: the Game detects overlaps between Hitboxes and calls
5
- * onCollide(other) on both entities' components. For static physical
4
+ * Trigger collider. A pair is tested when either Hitbox's Collision Mask
5
+ * names the other's Collision Layer; only each interested owner receives
6
+ * `onCollide`. Spatial Queries ignore categories. For static physical
6
7
  * collision see Solid.
7
8
  */
8
9
  export class Hitbox extends Component {
9
10
  static componentName = 'Hitbox';
10
11
  static params = {
12
+ layer: { label: 'Collision Layer' },
13
+ collidesWith: { label: 'Collision Mask', kind: 'string-list' },
11
14
  offsetX: { label: 'x offset' },
12
15
  offsetY: { label: 'y offset' },
13
16
  };
17
+ /** One exact, case-sensitive `^[a-z][a-z0-9-]*$` membership name. */
18
+ layer = 'default';
19
+ /** Outgoing layer interest; `'*'` means every valid layer and `[]` means none. */
20
+ collidesWith = ['*'];
14
21
  shape = 'rectangle';
15
22
  width = 1;
16
23
  height = 1;
package/dist/entity.js CHANGED
@@ -55,6 +55,10 @@ export class Entity {
55
55
  if (this.destroyed)
56
56
  return;
57
57
  this.destroyed = true;
58
+ // Immediately, before any onDestroy hook runs, whatever the scope
59
+ // (ADR 0017, CA-5) — `alive` is already false by the time any of this
60
+ // entity's own timers/tweens could observe it.
61
+ this.game.time.cancelOwnedBy(this);
58
62
  for (const c of [...this.components])
59
63
  c.onDestroy?.();
60
64
  this.components.length = 0;
@@ -0,0 +1,189 @@
1
+ /** The closed set of named Penner curves a Tween can ease through. */
2
+ export type EasingName = 'linear' | 'quadIn' | 'quadOut' | 'quadInOut' | 'cubicIn' | 'cubicOut' | 'cubicInOut' | 'sineInOut';
3
+ /** What `after`/`every`/`tween` hand back: see CA-6. */
4
+ export interface TimerHandle {
5
+ /** Idempotent and silent, even on an already-inactive handle. */
6
+ cancel(): void;
7
+ readonly active: boolean;
8
+ /** Seconds of Game Time, per-kind semantics in CA-6. Frozen once inactive. */
9
+ readonly elapsed: number;
10
+ /** Seconds of Game Time until the next due time; 0 once inactive. */
11
+ readonly remaining: number;
12
+ }
13
+ export interface TimerOptions {
14
+ /**
15
+ * Structural, not `instanceof Entity` (spec inference 19) — an object with
16
+ * a boolean `alive` qualifies, whatever else it is, so behaviors-test stub
17
+ * entities do too. An owner already dead at scheduling time yields an
18
+ * inactive handle that never runs, silently (CA-5) — a real Entity's
19
+ * `destroy()` cancels a live one the same way, whatever its scope.
20
+ * `alive` is read exactly once, right here at scheduling time; nothing
21
+ * polls it afterwards. Automatic cancellation of a live handle is driven
22
+ * entirely by `Entity.destroy()` calling `cancelOwnedBy()`, so an owner
23
+ * that is not a real `Entity` (never calls `destroy()`) only ever gets
24
+ * this one dead-at-scheduling check — its handle keeps firing even if
25
+ * something later flips its `alive` to false by hand.
26
+ */
27
+ owner?: {
28
+ readonly alive: boolean;
29
+ };
30
+ /**
31
+ * `'session'` survives a scene change; anything else — including leaving
32
+ * it unset — means scene-scoped (ADR 0017), the opposite default from
33
+ * `game.onUpdate`/`game.events` (ADR 0011). Narrowed to the two literal
34
+ * values (mirroring `AudioPlayOptions.scope` in `audio/types.ts`) so a
35
+ * typo is a compile error instead of a silent downgrade to scene scope.
36
+ */
37
+ scope?: 'scene' | 'session';
38
+ }
39
+ export interface TweenOptions {
40
+ from: number;
41
+ to: number;
42
+ /** <= 0 (a negative clamped to 0) or below one step: `from` on creation, `to` + onComplete next step. */
43
+ seconds: number;
44
+ /** A known name (default `'linear'`) or a custom `(t) => number`. */
45
+ easing?: EasingName | ((t: number) => number);
46
+ onUpdate: (value: number) => void;
47
+ onComplete?: () => void;
48
+ /**
49
+ * Structural, not `instanceof Entity` (spec inference 19) — an object with
50
+ * a boolean `alive` qualifies. `alive` is read once, at scheduling time,
51
+ * to reject an owner already dead; automatic cancellation afterwards is
52
+ * driven entirely by `Entity.destroy()` calling `cancelOwnedBy()`, never
53
+ * by polling `alive` again, so a non-`Entity` owner only ever gets that
54
+ * one dead-at-scheduling check.
55
+ */
56
+ owner?: {
57
+ readonly alive: boolean;
58
+ };
59
+ /** Same as `TimerOptions.scope`: `'session'` survives a scene change, anything else means scene. */
60
+ scope?: 'scene' | 'session';
61
+ }
62
+ /**
63
+ * Module-private key for the per-step advance pass. Not exported, so
64
+ * `time[ADVANCE]()` cannot be spelled outside this file — `advanceGameTime`
65
+ * (below, exported) is the only reachable entry point from project code.
66
+ */
67
+ declare const ADVANCE: unique symbol;
68
+ /**
69
+ * The `game.time` service (CA-1..CA-9, CA-14): `after`, `every` and `tween`
70
+ * scheduled against Game Time (`now`), advanced only by Simulation Steps —
71
+ * never the wall clock, never synchronously, never while paused or not
72
+ * simulating. Constructible standalone, like `Stats`: a `Game` owns one via
73
+ * `game.time`, and a behaviors-test stub game can carry its own, advanced by
74
+ * the same engine-internal hook `Game` uses (`advanceGameTime`, exported
75
+ * below — not part of this class's documented API).
76
+ */
77
+ export declare class GameTime {
78
+ private stepCount;
79
+ private nextId;
80
+ private timers;
81
+ private tweens;
82
+ /**
83
+ * Set whenever a timer/tween goes inactive since the matching array was
84
+ * last pruned, whether by firing/completing the per-step advance pass or
85
+ * by a plain `handle.cancel()` from outside it; consumed (and cleared) by
86
+ * the next `reclaim()` that actually reassigns that array (CA-3 perf: no
87
+ * reassignment when nothing died). Invariant: a flag is false only when
88
+ * its array holds no inactive entries — every path that clears a flag
89
+ * prunes that array first, and every path that deactivates an entry sets
90
+ * the matching flag. `reclaim()` relies on this to skip a prune safely.
91
+ */
92
+ private timersDirty;
93
+ private tweensDirty;
94
+ /** 0 on a new GameTime; `N * SIMULATION_STEP` exactly after N steps — computed, never summed (CA-9). */
95
+ get now(): number;
96
+ /** Active timers plus active tweens across both scopes (CA-10). */
97
+ get pending(): number;
98
+ /** Smallest positive integer n s.t. n more steps fires/completes something; null when nothing is pending (CA-10). */
99
+ get nextInSteps(): number | null;
100
+ /** Shared traversal for `pending`: how many entries in `entries` are active. */
101
+ private countActive;
102
+ /** Shared traversal for `nextInSteps`: folds `stepsUntilDue` over every active entry, starting from `min`. */
103
+ private minStepsUntilDue;
104
+ /**
105
+ * Runs `callback` once, `seconds` of Game Time from now (0 or a negative
106
+ * duration: the start of the next step). Never synchronous (CA-1).
107
+ */
108
+ after(seconds: number, callback: () => void, options?: TimerOptions): TimerHandle;
109
+ /**
110
+ * Runs `callback` every `max(seconds, SIMULATION_STEP)` of Game Time,
111
+ * first one interval after creation, with no drift: each due time is
112
+ * `start + n * interval`, computed directly rather than by repeated
113
+ * addition (CA-2).
114
+ */
115
+ every(seconds: number, callback: () => void, options?: TimerOptions): TimerHandle;
116
+ /**
117
+ * Carries a single number from `from` to `to` over `seconds` of Game
118
+ * Time, reporting it through `onUpdate` on every step including creation
119
+ * (synchronous, before this returns — CA-7). Owner already dead, or any
120
+ * invalid input, skips even that first call.
121
+ */
122
+ tween(options: TweenOptions): TimerHandle;
123
+ /**
124
+ * Internal: the start-of-step pass (CA-3) — advances `now`, then runs
125
+ * every due timer (due time, then creation order), then advances every
126
+ * tween that existed before this pass (creation order). Reachable only as
127
+ * `time[ADVANCE]()`, and `ADVANCE` is not exported, so `advanceGameTime()`
128
+ * (below, exported) is genuinely the only way in from project code. With
129
+ * nothing scheduled (the common case once a scene settles) this does no
130
+ * allocation beyond the step count itself; with timers but no tweens, the
131
+ * tween snapshot and `advanceTweens()` are skipped too.
132
+ */
133
+ [ADVANCE](): void;
134
+ /**
135
+ * Engine-internal by convention, like `Game.removeEntity`: a public
136
+ * method, not access-controlled, that `Game.unloadScene()` and every
137
+ * scene load are expected to call, including the first (CA-4).
138
+ */
139
+ cancelSceneScoped(): void;
140
+ /**
141
+ * Engine-internal by convention, like `Game.removeEntity`: a public
142
+ * method, not access-controlled, that `Entity.destroy()` (CA-5) is
143
+ * expected to be the only caller of. Nothing re-reads `owner.alive` on
144
+ * later steps; a non-`Entity` owner whose `alive` flips without ever
145
+ * going through a real `destroy()` call never reaches here, so its
146
+ * timers/tweens keep running (see `TimerOptions.owner`).
147
+ */
148
+ cancelOwnedBy(owner: {
149
+ readonly alive: boolean;
150
+ }): void;
151
+ /**
152
+ * Engine-internal by convention, like `Game.removeEntity`: a public
153
+ * method, not access-controlled, that `Game.dispose()` is expected to
154
+ * call (CA-4).
155
+ */
156
+ cancelAll(): void;
157
+ /**
158
+ * Prunes each array only when its dirty flag is set, then clears that
159
+ * flag — safe because of the invariant documented on `timersDirty`
160
+ * /`tweensDirty`: a false flag means the array already holds no inactive
161
+ * entries, so skipping the filter changes no observable behavior. Called
162
+ * both at the tail of the per-step advance pass and by
163
+ * `cancelSceneScoped`/`cancelOwnedBy`/`cancelAll`, which are also
164
+ * reachable with `simulate === false` (e.g. the editor's edit mode
165
+ * calling `loadScene` on every edit), when no step ever runs to reclaim
166
+ * otherwise.
167
+ */
168
+ private reclaim;
169
+ private runDueTimers;
170
+ private advanceTweens;
171
+ private deactivate;
172
+ /** Smallest n >= 1 such that `n` more steps reaches `dueTime`, guarding against float noise near a step boundary. */
173
+ private stepsUntilDue;
174
+ private invalid;
175
+ private handleFor;
176
+ }
177
+ /**
178
+ * Engine-internal: advances `time` by exactly one Simulation Step (CA-3).
179
+ * `Game.simulateStep()` calls this as its very first statement, so nothing
180
+ * advances with `simulate === false`, on a zero-step frame, or while the
181
+ * Runtime Bridge is paused and not stepping. A standalone `GameTime` — a
182
+ * behaviors-test stub game, or this package's own game-time.test.ts (CA-14)
183
+ * — must call this the same way. This is the only way in: the pass itself
184
+ * lives behind the module-private `ADVANCE` symbol key, unreachable from
185
+ * outside this file, so project code has no `advanceStep()`-shaped method
186
+ * to call directly and desynchronize Game Time with.
187
+ */
188
+ export declare function advanceGameTime(time: GameTime): void;
189
+ export {};