@futdevpro/fsm-dynamo 1.20.102 → 1.20.104
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/build/_modules/game/_models/guided-onboarding.control-model.d.ts +81 -0
- package/build/_modules/game/_models/guided-onboarding.control-model.d.ts.map +1 -0
- package/build/_modules/game/_models/guided-onboarding.control-model.js +491 -0
- package/build/_modules/game/_models/guided-onboarding.control-model.js.map +1 -0
- package/build/_modules/game/_models/guided-onboarding.interface.d.ts +102 -0
- package/build/_modules/game/_models/guided-onboarding.interface.d.ts.map +1 -0
- package/build/_modules/game/_models/guided-onboarding.interface.js +3 -0
- package/build/_modules/game/_models/guided-onboarding.interface.js.map +1 -0
- package/build/_modules/game/_models/narration-player.control-model.d.ts +53 -0
- package/build/_modules/game/_models/narration-player.control-model.d.ts.map +1 -0
- package/build/_modules/game/_models/narration-player.control-model.js +223 -0
- package/build/_modules/game/_models/narration-player.control-model.js.map +1 -0
- package/build/_modules/game/_models/narration-player.interface.d.ts +64 -0
- package/build/_modules/game/_models/narration-player.interface.d.ts.map +1 -0
- package/build/_modules/game/_models/narration-player.interface.js +3 -0
- package/build/_modules/game/_models/narration-player.interface.js.map +1 -0
- package/build/_modules/game/index.d.ts +4 -0
- package/build/_modules/game/index.d.ts.map +1 -1
- package/build/_modules/game/index.js +4 -0
- package/build/_modules/game/index.js.map +1 -1
- package/build-esm/_modules/game/_models/guided-onboarding.control-model.js +486 -0
- package/build-esm/_modules/game/_models/guided-onboarding.interface.js +1 -0
- package/build-esm/_modules/game/_models/narration-player.control-model.js +218 -0
- package/build-esm/_modules/game/_models/narration-player.interface.js +1 -0
- package/build-esm/_modules/game/index.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { DyFM_Log } from '../../../_collections/utils/log.util';
|
|
2
|
+
/**
|
|
3
|
+
* BFR-WARFACTORY-014 — the narration adapter canon (war-factory `Tutorial_Narration_Service` + portalhold voice lines):
|
|
4
|
+
*
|
|
5
|
+
* - **asset-id core**, catalog as an optional layer (`playLine` / `playCatalog`);
|
|
6
|
+
* - `Promise<boolean>` — a refusal is REPORTED, with its reason in `lastRefusal` (disabled · muted · no-gesture ·
|
|
7
|
+
* autoplay-blocked · error · unknown-line) — a narration that silently does not play is undiagnosable;
|
|
8
|
+
* - optional **gesture gate with exposed state** (`sessionActivated`);
|
|
9
|
+
* - **exactly one active line** — every play stops the previous one;
|
|
10
|
+
* - a **staleness guard** (revision counter): a late `ended` / `error` / `play()` rejection of a replaced line never
|
|
11
|
+
* touches the current one;
|
|
12
|
+
* - **`blocked` ≠ `failed`**: a `NotAllowedError` (browser autoplay block) is the case the player must be told about;
|
|
13
|
+
* - `replay()`; optional settings persistence; the subtitle stays available while blocked / failed (text fallback).
|
|
14
|
+
*
|
|
15
|
+
* Framework-agnostic: the audio comes from a factory (`(src) => new Audio(src)` in a browser); a UI layer mirrors
|
|
16
|
+
* `getState()` via `onChange`.
|
|
17
|
+
*/
|
|
18
|
+
export class DyFM_NarrationPlayer_ControlModel {
|
|
19
|
+
options;
|
|
20
|
+
listeners = [];
|
|
21
|
+
audio = null;
|
|
22
|
+
revision = 0;
|
|
23
|
+
lastLine = null;
|
|
24
|
+
state = {
|
|
25
|
+
playback: 'idle', activeLineId: null, subtitle: null, enabled: true, volume: 0.85, sessionActivated: false,
|
|
26
|
+
lastRefusal: null,
|
|
27
|
+
};
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.loadSettings();
|
|
31
|
+
}
|
|
32
|
+
/** A copy of the current state. */
|
|
33
|
+
getState() {
|
|
34
|
+
return { ...this.state };
|
|
35
|
+
}
|
|
36
|
+
/** Subscribe to state changes; returns the unsubscribe. A throwing listener is logged and isolated. */
|
|
37
|
+
onChange(listener) {
|
|
38
|
+
this.listeners.push(listener);
|
|
39
|
+
return () => {
|
|
40
|
+
const index = this.listeners.indexOf(listener);
|
|
41
|
+
if (index >= 0) {
|
|
42
|
+
this.listeners.splice(index, 1);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Marks a user gesture — from now on automatic narration may play. */
|
|
47
|
+
activate() {
|
|
48
|
+
if (!this.state.sessionActivated) {
|
|
49
|
+
this.set({ sessionActivated: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Plays a line (automatic path: respects the gesture gate). */
|
|
53
|
+
async playLine(line) {
|
|
54
|
+
return this.play(line);
|
|
55
|
+
}
|
|
56
|
+
/** Plays a catalog entry; an unknown key refuses with `unknown-line`. */
|
|
57
|
+
async playCatalog(key) {
|
|
58
|
+
const line = this.options.catalog?.[key];
|
|
59
|
+
if (!line) {
|
|
60
|
+
this.set({ lastRefusal: 'unknown-line' });
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return this.play(line);
|
|
64
|
+
}
|
|
65
|
+
/** From a user gesture: activates the session, then plays. */
|
|
66
|
+
async activateAndPlay(line) {
|
|
67
|
+
this.activate();
|
|
68
|
+
return this.play(line);
|
|
69
|
+
}
|
|
70
|
+
/** Replays the last requested line (a replay button is a gesture). */
|
|
71
|
+
async replay() {
|
|
72
|
+
if (!this.lastLine) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
this.activate();
|
|
76
|
+
return this.play(this.lastLine);
|
|
77
|
+
}
|
|
78
|
+
/** Stops the active line (or clears a blocked / failed one). */
|
|
79
|
+
stop() {
|
|
80
|
+
this.revision++;
|
|
81
|
+
this.detach();
|
|
82
|
+
this.set({ playback: 'idle', activeLineId: null, subtitle: null });
|
|
83
|
+
}
|
|
84
|
+
setEnabled(enabled) {
|
|
85
|
+
this.set({ enabled: enabled });
|
|
86
|
+
this.saveSettings();
|
|
87
|
+
if (!enabled && this.audio) {
|
|
88
|
+
this.stop();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
setVolume(volume) {
|
|
92
|
+
this.set({ volume: Math.min(1, Math.max(0, Number.isFinite(volume) ? volume : 0)) });
|
|
93
|
+
this.saveSettings();
|
|
94
|
+
this.applyVolume();
|
|
95
|
+
}
|
|
96
|
+
/** Re-applies the effective volume (call it when the game's master volume / mute changes); 0 stops the line. */
|
|
97
|
+
applyVolume() {
|
|
98
|
+
const effective = this.effectiveVolume();
|
|
99
|
+
if (this.audio) {
|
|
100
|
+
this.audio.volume = effective;
|
|
101
|
+
if (effective <= 0) {
|
|
102
|
+
this.stop();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async play(line) {
|
|
107
|
+
this.lastLine = line;
|
|
108
|
+
const refusal = !this.state.enabled
|
|
109
|
+
? 'disabled'
|
|
110
|
+
: this.effectiveVolume() <= 0
|
|
111
|
+
? 'muted'
|
|
112
|
+
: this.options.requireGesture && !this.state.sessionActivated ? 'no-gesture' : null;
|
|
113
|
+
if (refusal) {
|
|
114
|
+
this.set({ lastRefusal: refusal });
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
// exactly one active line: the previous one is stopped and its late callbacks become stale
|
|
118
|
+
const revision = ++this.revision;
|
|
119
|
+
this.detach();
|
|
120
|
+
let audio;
|
|
121
|
+
try {
|
|
122
|
+
audio = this.options.audioFactory(line.src);
|
|
123
|
+
audio.volume = this.effectiveVolume();
|
|
124
|
+
audio.onended = () => {
|
|
125
|
+
if (revision === this.revision) {
|
|
126
|
+
this.audio = null;
|
|
127
|
+
this.set({ playback: 'idle', activeLineId: null, subtitle: null });
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
audio.onerror = () => {
|
|
131
|
+
if (revision === this.revision) {
|
|
132
|
+
this.audio = null;
|
|
133
|
+
this.set({ playback: 'failed', lastRefusal: 'error' });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
this.audio = audio;
|
|
137
|
+
this.set({ activeLineId: line.lineId, subtitle: line.subtitle ?? null, lastRefusal: null });
|
|
138
|
+
await audio.play();
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (revision !== this.revision) {
|
|
142
|
+
return false; // a newer line replaced this one while play() was pending
|
|
143
|
+
}
|
|
144
|
+
this.audio = null;
|
|
145
|
+
const blocked = error?.name === 'NotAllowedError';
|
|
146
|
+
this.set({ playback: blocked ? 'blocked' : 'failed', lastRefusal: blocked ? 'autoplay-blocked' : 'error' });
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (revision !== this.revision) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
this.set({ playback: 'playing' });
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
detach() {
|
|
156
|
+
if (this.audio) {
|
|
157
|
+
const audio = this.audio;
|
|
158
|
+
this.audio = null;
|
|
159
|
+
audio.onended = null;
|
|
160
|
+
audio.onerror = null;
|
|
161
|
+
try {
|
|
162
|
+
audio.pause();
|
|
163
|
+
audio.currentTime = 0;
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
DyFM_Log.error('DyFM_NarrationPlayer: stopping the previous line failed', error);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
effectiveVolume() {
|
|
171
|
+
const master = this.options.masterVolume ? this.options.masterVolume() : 1;
|
|
172
|
+
return Math.min(1, Math.max(0, this.state.volume * (Number.isFinite(master) ? master : 0)));
|
|
173
|
+
}
|
|
174
|
+
set(patch) {
|
|
175
|
+
this.state = { ...this.state, ...patch };
|
|
176
|
+
const snapshot = this.getState();
|
|
177
|
+
for (const listener of [...this.listeners]) {
|
|
178
|
+
try {
|
|
179
|
+
listener(snapshot);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
DyFM_Log.error('DyFM_NarrationPlayer: an onChange listener threw', error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
loadSettings() {
|
|
187
|
+
const settings = this.options.settings;
|
|
188
|
+
if (!settings) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const stored = settings.store.getItem(settings.key);
|
|
193
|
+
const parsed = stored ? JSON.parse(stored) : {};
|
|
194
|
+
if (typeof parsed.enabled === 'boolean') {
|
|
195
|
+
this.state.enabled = parsed.enabled;
|
|
196
|
+
}
|
|
197
|
+
if (typeof parsed.volume === 'number' && Number.isFinite(parsed.volume)) {
|
|
198
|
+
this.state.volume = Math.min(1, Math.max(0, parsed.volume));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
// a broken stored value falls back to the defaults (the session still works)
|
|
203
|
+
DyFM_Log.error(`DyFM_NarrationPlayer: unreadable settings under '${settings.key}' — defaults used`, error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
saveSettings() {
|
|
207
|
+
const settings = this.options.settings;
|
|
208
|
+
if (!settings) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
settings.store.setItem(settings.key, JSON.stringify({ enabled: this.state.enabled, volume: this.state.volume }));
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
DyFM_Log.error(`DyFM_NarrationPlayer: settings could not be saved under '${settings.key}'`, error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -22,6 +22,10 @@ export * from './_models/viewport.interface';
|
|
|
22
22
|
export * from './_models/viewport.control-model';
|
|
23
23
|
export * from './_models/input-action.interface';
|
|
24
24
|
export * from './_models/input-action-registry.control-model';
|
|
25
|
+
export * from './_models/guided-onboarding.interface';
|
|
26
|
+
export * from './_models/guided-onboarding.control-model';
|
|
27
|
+
export * from './_models/narration-player.interface';
|
|
28
|
+
export * from './_models/narration-player.control-model';
|
|
25
29
|
// COLLECTIONS
|
|
26
30
|
export * from './_collections/audio-mixer.util';
|
|
27
31
|
export * from './_collections/audio-scale.util';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@futdevpro/fsm-dynamo",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.104",
|
|
4
4
|
"description": "Full Stack Model Collection for Dynamic (NodeJS-Typescript) Framework called Dynamo, by Future Development Ltd.",
|
|
5
5
|
"DyBu_settings": {
|
|
6
6
|
"packageType": "full-stack-package",
|