@waica/behaviors 0.18.0 → 0.19.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.
@@ -1,4 +1,4 @@
1
- import { Component, type Entity } from '@waica/engine';
1
+ import { Component, type CameraEffectColor, type Entity } from '@waica/engine';
2
2
  /**
3
3
  * Replaces the live scene with `scene` (its file's stem, e.g.
4
4
  * "cave.scene.json" -> "cave") when fired — a door, or any entity a player
@@ -13,6 +13,12 @@ import { Component, type Entity } from '@waica/engine';
13
13
  * it needs a sibling Interactable and fires from the shared nearest-wins
14
14
  * interact scan (interactable.ts's fireInteract), so a door and an NPC in
15
15
  * range arbitrate themselves with no new rule.
16
+ *
17
+ * With fadeSeconds > 0 it hides the swap behind a Fade (issue #74): the
18
+ * view fades to fadeColor over fadeSeconds, the destination loads, and the
19
+ * Fade clears over the same fadeSeconds in the incoming scene. Simulation
20
+ * and input keep running; while its outgoing fade runs, this component
21
+ * ignores further triggers. fadeSeconds 0 (the default) is a hard cut.
16
22
  */
17
23
  export declare class SceneTransition extends Component {
18
24
  static componentName: string;
@@ -24,10 +30,25 @@ export declare class SceneTransition extends Component {
24
30
  label: string;
25
31
  options: string[];
26
32
  };
33
+ fadeSeconds: {
34
+ label: string;
35
+ min: number;
36
+ max: number;
37
+ step: number;
38
+ };
39
+ fadeColor: {
40
+ label: string;
41
+ };
27
42
  };
28
43
  /** Destination scene name. */
29
44
  scene: string;
30
45
  trigger: 'overlap' | 'interact';
46
+ /** Seconds of each half of the fade (out, then clear); 0 = hard cut. */
47
+ fadeSeconds: number;
48
+ /** `'black'`, `'white'` or `'#rrggbb'`. */
49
+ fadeColor: CameraEffectColor;
50
+ /** True while the outgoing fade runs: further triggers do nothing. */
51
+ private _fadingOut;
31
52
  onReady(): void;
32
53
  onCollide(_other: Entity): void;
33
54
  onInteract(_initiator: Entity): void;
@@ -14,16 +14,30 @@ import { Interactable } from './interactable.js';
14
14
  * it needs a sibling Interactable and fires from the shared nearest-wins
15
15
  * interact scan (interactable.ts's fireInteract), so a door and an NPC in
16
16
  * range arbitrate themselves with no new rule.
17
+ *
18
+ * With fadeSeconds > 0 it hides the swap behind a Fade (issue #74): the
19
+ * view fades to fadeColor over fadeSeconds, the destination loads, and the
20
+ * Fade clears over the same fadeSeconds in the incoming scene. Simulation
21
+ * and input keep running; while its outgoing fade runs, this component
22
+ * ignores further triggers. fadeSeconds 0 (the default) is a hard cut.
17
23
  */
18
24
  export class SceneTransition extends Component {
19
25
  static componentName = 'SceneTransition';
20
26
  static params = {
21
27
  scene: { label: 'Scene' },
22
28
  trigger: { label: 'Trigger', options: ['overlap', 'interact'] },
29
+ fadeSeconds: { label: 'Fade seconds', min: 0, max: 5, step: 0.05 },
30
+ fadeColor: { label: 'Fade color' },
23
31
  };
24
32
  /** Destination scene name. */
25
33
  scene = '';
26
34
  trigger = 'overlap';
35
+ /** Seconds of each half of the fade (out, then clear); 0 = hard cut. */
36
+ fadeSeconds = 0;
37
+ /** `'black'`, `'white'` or `'#rrggbb'`. */
38
+ fadeColor = 'black';
39
+ /** True while the outgoing fade runs: further triggers do nothing. */
40
+ _fadingOut = false;
27
41
  onReady() {
28
42
  if (this.trigger === 'interact' && !this.entity.has(Interactable)) {
29
43
  console.warn(`[waica] SceneTransition on "${this.entity.name}" has trigger:"interact" but no ` +
@@ -41,6 +55,72 @@ export class SceneTransition extends Component {
41
55
  this.fire();
42
56
  }
43
57
  fire() {
44
- this.game.loadSceneByName(this.scene);
58
+ if (!(this.fadeSeconds > 0)) {
59
+ this.game.loadSceneByName(this.scene);
60
+ return;
61
+ }
62
+ if (this._fadingOut)
63
+ return;
64
+ const { game, fadeSeconds } = this;
65
+ const sceneAtFadeStart = game.sceneName;
66
+ const fadeHandle = game.cameraEffects.fade({ to: this.fadeColor, seconds: fadeSeconds });
67
+ this._fadingOut = true;
68
+ // Scheduled on Game Time, not on `fade.done`: a promise settles in a
69
+ // microtask, after a whole multi-step frame or Runtime Bridge `step`,
70
+ // while this timer fires on the exact step the fade has covered the view.
71
+ //
72
+ // Session-scoped and un-owned (issue #74 review): a mid-fade scene
73
+ // change (another transition, a Runtime Bridge `scene` op, a host
74
+ // reset) cancels scene-scoped work, and Entity.destroy() cancels an
75
+ // owned timer whatever its scope. Either would otherwise strand the
76
+ // Fade at opacity 1 with nothing left to clear it, since the Fade
77
+ // itself is session-scoped and keeps advancing on its own. This timer
78
+ // always runs; it swaps only if the door is still alive and the scene
79
+ // it started the fade in is still live, and it always queues the
80
+ // 'clear'.
81
+ game.time.after(fadeSeconds, () => {
82
+ // Superseded (PR #98 review): the Fade is one shared, session-scoped
83
+ // layer — a second door, or any other `cameraEffects.fade(...)`
84
+ // call, replaces this fade outright instead of queueing behind it
85
+ // (CameraEffects.fade() cancels whatever is running). `fadeHandle
86
+ // .superseded` is read synchronously here, never `.done`: that
87
+ // Promise only notifies its `.then()` in a microtask, and nothing
88
+ // flushes the microtask queue between this Game Time-scheduled
89
+ // callback and the many Simulation Steps that led up to it (same
90
+ // reasoning as scheduling the swap itself on Game Time instead of
91
+ // on `fade.done`, above). It is set once, synchronously, only when
92
+ // a *later* fade call actually replaces this one while it is still
93
+ // running — never for this door's own fade completing on its own,
94
+ // whatever step that lands on (`fire()` runs mid-`dispatchCollisions`
95
+ // for a real overlap, one Simulation Step "behind" where `game.time`
96
+ // sees this same call start, which would otherwise look identical
97
+ // to a supersession). If it is true, somebody else now owns the
98
+ // shared layer and its eventual 'clear': this door must not swap
99
+ // under their partial cover, and must not queue a 'clear' of its
100
+ // own that would cancel their fade. Nothing is in flight to
101
+ // protect, so the latch resets right away — the door can be
102
+ // triggered again if the player is still on it.
103
+ if (fadeHandle.superseded) {
104
+ this._fadingOut = false;
105
+ return;
106
+ }
107
+ if (this.entity.alive && game.sceneName === sceneAtFadeStart) {
108
+ game.loadSceneByName(this.scene);
109
+ }
110
+ // `_fadingOut` stays true here (PR #98 review): loadSceneByName
111
+ // above only *queues* the swap mid-frame, it doesn't apply it until
112
+ // the next frame. Dropping the guard now would let the rest of
113
+ // this same step's dispatchCollisions re-fire on the door, still
114
+ // alive and still overlapped, in the outgoing scene — a second
115
+ // fire() that schedules an orphan timer whose 'clear' lands in the
116
+ // new scene and cancels whatever legitimate fade is running there.
117
+ // Reset it only in the zero-delay follow-up below, once the queued
118
+ // swap has applied and the door has died with its scene (or, if
119
+ // the swap was skipped, once there is nothing left to re-arm).
120
+ game.time.after(0, () => {
121
+ this._fadingOut = false;
122
+ game.cameraEffects.fade({ to: 'clear', seconds: fadeSeconds });
123
+ }, { scope: 'session' });
124
+ }, { scope: 'session' });
45
125
  }
46
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waica/behaviors",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Waica built-in behaviors — the curated game-feel library",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,6 +19,6 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@waica/engine": "^0.18.0"
22
+ "@waica/engine": "^0.19.0"
23
23
  }
24
24
  }