@futdevpro/fsm-dynamo 1.20.102 → 1.20.103
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/index.d.ts +2 -0
- package/build/_modules/game/index.d.ts.map +1 -1
- package/build/_modules/game/index.js +2 -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/index.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { DyFM_OnboardingCapabilities, DyFM_OnboardingCatalog, DyFM_OnboardingEvaluation_Type, DyFM_OnboardingEvent, DyFM_OnboardingFinding, DyFM_OnboardingMigrationReport, DyFM_OnboardingPosition, DyFM_OnboardingState, DyFM_OnboardingStatus_Type } from './guided-onboarding.interface';
|
|
2
|
+
/**
|
|
3
|
+
* BFR-WARFACTORY-014 — content-agnostic guided onboarding: chapters of steps with STABLE ids, declarative commands and
|
|
4
|
+
* predicates resolved against the game's capability surface, state-regression recovery, bounded timeouts,
|
|
5
|
+
* skip / restart / resume, and a versioned, migratable progress state.
|
|
6
|
+
*
|
|
7
|
+
* It owns no timer and no DOM: the driver (a game loop tick, a state-change event, an Angular effect) calls
|
|
8
|
+
* `evaluate()`; the UI layer renders `position()` and listens to `onEvent()`. The clock is injectable, so timeouts are
|
|
9
|
+
* deterministic in tests.
|
|
10
|
+
*/
|
|
11
|
+
export declare class DyFM_GuidedOnboarding_ControlModel<TContext> {
|
|
12
|
+
private readonly catalog;
|
|
13
|
+
private readonly capabilities;
|
|
14
|
+
private readonly now;
|
|
15
|
+
private readonly steps;
|
|
16
|
+
private readonly chapterCount;
|
|
17
|
+
private status;
|
|
18
|
+
private currentIndex;
|
|
19
|
+
private enteredAtMs;
|
|
20
|
+
private readonly completed;
|
|
21
|
+
private readonly skipped;
|
|
22
|
+
private readonly listeners;
|
|
23
|
+
/** `importState` restored an active step, but it was not entered yet (`resume`). */
|
|
24
|
+
private pendingResume;
|
|
25
|
+
/** A transition (with a possibly async command) is running — a re-entrant `evaluate` waits it out. */
|
|
26
|
+
private transitioning;
|
|
27
|
+
/** Validates the catalog against the capabilities; any finding throws `DYFM-ONBOARDING-CATALOG` listing them all. */
|
|
28
|
+
constructor(catalog: DyFM_OnboardingCatalog, capabilities: DyFM_OnboardingCapabilities<TContext>, now?: () => number);
|
|
29
|
+
/**
|
|
30
|
+
* Every problem of a catalog, as data (never throws). With `capabilities`, each ref must name a registered command /
|
|
31
|
+
* predicate — a typo is caught when the game boots, not when a player reaches that step.
|
|
32
|
+
*/
|
|
33
|
+
static validateCatalog<TContext>(catalog: DyFM_OnboardingCatalog, capabilities?: DyFM_OnboardingCapabilities<TContext>): DyFM_OnboardingFinding[];
|
|
34
|
+
/** Subscribe; returns the unsubscribe. A throwing listener is logged and isolated — it never breaks the runtime. */
|
|
35
|
+
onEvent(listener: (event: DyFM_OnboardingEvent) => void): () => void;
|
|
36
|
+
getStatus(): DyFM_OnboardingStatus_Type;
|
|
37
|
+
/** Where the player is; `null` unless active. */
|
|
38
|
+
position(): DyFM_OnboardingPosition | null;
|
|
39
|
+
/** Starts a not-yet-started onboarding at its first step (no-op otherwise — a finished one is `restart`ed). */
|
|
40
|
+
start(context: TContext): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* One check of the active step: state regression → fallback · completion → next step · timeout → recorded and left.
|
|
43
|
+
* Call it from whatever drives the game (tick, state event); it never waits by itself.
|
|
44
|
+
*/
|
|
45
|
+
evaluate(context: TContext): Promise<DyFM_OnboardingEvaluation_Type>;
|
|
46
|
+
/** The player acknowledges a MANUAL step (no completion predicate); a gameplay step throws `DYFM-ONBOARDING-NOT-MANUAL`. */
|
|
47
|
+
next(context: TContext): Promise<void>;
|
|
48
|
+
/** Skip the active step, the rest of its chapter, or the whole onboarding. */
|
|
49
|
+
skip(context: TContext, scope: 'step' | 'chapter' | 'all'): Promise<void>;
|
|
50
|
+
/** Start over — all of it, or from a chapter on (earlier chapters keep their progress). */
|
|
51
|
+
restart(context: TContext, fromChapterId?: string): Promise<void>;
|
|
52
|
+
/** The progress as data (`dyfm-onboarding/1`); ids in catalog order, so two equal states serialise identically. */
|
|
53
|
+
exportState(): DyFM_OnboardingState;
|
|
54
|
+
/**
|
|
55
|
+
* Restores a save onto THIS catalog: ids are rewritten by the migration rules, vanished ids are dropped, and an
|
|
56
|
+
* active step that no longer exists (or is already done) continues at the first unfinished step. No command runs —
|
|
57
|
+
* an active onboarding must be `resume`d, and until then `evaluate` / `next` / `skip` throw
|
|
58
|
+
* `DYFM-ONBOARDING-NOT-RESUMED` (a forgotten resume would otherwise leave the player stuck, silently).
|
|
59
|
+
*/
|
|
60
|
+
importState(state: DyFM_OnboardingState): DyFM_OnboardingMigrationReport;
|
|
61
|
+
/**
|
|
62
|
+
* Enters the restored step after `importState`. Its command is NOT run again by default (it already ran before the
|
|
63
|
+
* save — e.g. placing a building twice would corrupt the game); pass `rerunCommand` for idempotent commands.
|
|
64
|
+
*/
|
|
65
|
+
resume(context: TContext, options?: {
|
|
66
|
+
rerunCommand?: boolean;
|
|
67
|
+
}): Promise<void>;
|
|
68
|
+
/** Leaves the active step (completed / skipped / timed out) and enters the next unfinished one — or finishes. */
|
|
69
|
+
private leave;
|
|
70
|
+
/** Makes `index` the active step; runs its command (a failing command is reported and the step stays active). */
|
|
71
|
+
private enter;
|
|
72
|
+
/** Evaluates a predicate; a throwing one becomes a debuggable `DYFM-ONBOARDING-PREDICATE` (step + predicate named). */
|
|
73
|
+
private predicate;
|
|
74
|
+
private transition;
|
|
75
|
+
private activeStep;
|
|
76
|
+
private assertResumed;
|
|
77
|
+
private isDone;
|
|
78
|
+
private indexOf;
|
|
79
|
+
private emit;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=guided-onboarding.control-model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guided-onboarding.control-model.d.ts","sourceRoot":"","sources":["../../../../src/_modules/game/_models/guided-onboarding.control-model.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,2BAA2B,EAC3B,sBAAsB,EACtB,8BAA8B,EAC9B,oBAAoB,EACpB,sBAAsB,EACtB,8BAA8B,EAC9B,uBAAuB,EAEvB,oBAAoB,EACpB,0BAA0B,EAE3B,MAAM,+BAA+B,CAAC;AAWvC;;;;;;;;GAQG;AACH,qBAAa,kCAAkC,CAAC,QAAQ;IAgBpD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG;IAjBtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IACvD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,MAAM,CAA6C;IAC3D,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkC;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiD;IAC3E,oFAAoF;IACpF,OAAO,CAAC,aAAa,CAAkB;IACvC,sGAAsG;IACtG,OAAO,CAAC,aAAa,CAAkB;IAEvC,qHAAqH;gBAElG,OAAO,EAAE,sBAAsB,EAC/B,YAAY,EAAE,2BAA2B,CAAC,QAAQ,CAAC,EACnD,GAAG,GAAE,MAAM,MAAiC;IAoB/D;;;OAGG;IACH,MAAM,CAAC,eAAe,CAAC,QAAQ,EAC7B,OAAO,EAAE,sBAAsB,EAC/B,YAAY,CAAC,EAAE,2BAA2B,CAAC,QAAQ,CAAC,GACnD,sBAAsB,EAAE;IA8F3B,oHAAoH;IACpH,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI;IAYpE,SAAS,IAAI,0BAA0B;IAIvC,iDAAiD;IACjD,QAAQ,IAAI,uBAAuB,GAAG,IAAI;IAsB1C,+GAA+G;IACzG,KAAK,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ7C;;;OAGG;IACG,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,8BAA8B,CAAC;IAmD1E,4HAA4H;IACtH,IAAI,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAa5C,8EAA8E;IACxE,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B/E,2FAA2F;IACrF,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBvE,mHAAmH;IACnH,WAAW,IAAI,oBAAoB;IAkBnC;;;;;OAKG;IACH,WAAW,CAAC,KAAK,EAAE,oBAAoB,GAAG,8BAA8B;IAgFxE;;;OAGG;IACG,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAQxF,iHAAiH;YACnG,KAAK;IAyCnB,iHAAiH;YACnG,KAAK;IAmBnB,uHAAuH;IACvH,OAAO,CAAC,SAAS;YAaH,UAAU;IASxB,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,OAAO;IAcf,OAAO,CAAC,IAAI;CASb"}
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DyFM_GuidedOnboarding_ControlModel = void 0;
|
|
4
|
+
const log_util_1 = require("../../../_collections/utils/log.util");
|
|
5
|
+
const error_control_model_1 = require("../../../_models/control-models/error.control-model");
|
|
6
|
+
const ISSUER = 'DyFM_GuidedOnboarding_ControlModel';
|
|
7
|
+
/**
|
|
8
|
+
* BFR-WARFACTORY-014 — content-agnostic guided onboarding: chapters of steps with STABLE ids, declarative commands and
|
|
9
|
+
* predicates resolved against the game's capability surface, state-regression recovery, bounded timeouts,
|
|
10
|
+
* skip / restart / resume, and a versioned, migratable progress state.
|
|
11
|
+
*
|
|
12
|
+
* It owns no timer and no DOM: the driver (a game loop tick, a state-change event, an Angular effect) calls
|
|
13
|
+
* `evaluate()`; the UI layer renders `position()` and listens to `onEvent()`. The clock is injectable, so timeouts are
|
|
14
|
+
* deterministic in tests.
|
|
15
|
+
*/
|
|
16
|
+
class DyFM_GuidedOnboarding_ControlModel {
|
|
17
|
+
catalog;
|
|
18
|
+
capabilities;
|
|
19
|
+
now;
|
|
20
|
+
steps = [];
|
|
21
|
+
chapterCount;
|
|
22
|
+
status = 'not-started';
|
|
23
|
+
currentIndex = -1;
|
|
24
|
+
enteredAtMs = 0;
|
|
25
|
+
completed = new Set();
|
|
26
|
+
skipped = new Set();
|
|
27
|
+
listeners = [];
|
|
28
|
+
/** `importState` restored an active step, but it was not entered yet (`resume`). */
|
|
29
|
+
pendingResume = false;
|
|
30
|
+
/** A transition (with a possibly async command) is running — a re-entrant `evaluate` waits it out. */
|
|
31
|
+
transitioning = false;
|
|
32
|
+
/** Validates the catalog against the capabilities; any finding throws `DYFM-ONBOARDING-CATALOG` listing them all. */
|
|
33
|
+
constructor(catalog, capabilities, now = () => Date.now()) {
|
|
34
|
+
this.catalog = catalog;
|
|
35
|
+
this.capabilities = capabilities;
|
|
36
|
+
this.now = now;
|
|
37
|
+
const findings = DyFM_GuidedOnboarding_ControlModel.validateCatalog(catalog, capabilities);
|
|
38
|
+
if (findings.length) {
|
|
39
|
+
throw new error_control_model_1.DyFM_Error({
|
|
40
|
+
message: `DyFM_GuidedOnboarding: invalid catalog — ${findings.map((f) => `[${f.code}] ${f.path}: ${f.message}`).join(' · ')}`,
|
|
41
|
+
errorCode: 'DYFM-ONBOARDING-CATALOG',
|
|
42
|
+
issuerService: ISSUER,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catalog.chapters.forEach((chapter, chapterIndex) => {
|
|
46
|
+
chapter.steps.forEach((step) => {
|
|
47
|
+
this.steps.push({ chapterId: chapter.chapterId, chapterIndex: chapterIndex, step: step });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
this.chapterCount = catalog.chapters.length;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Every problem of a catalog, as data (never throws). With `capabilities`, each ref must name a registered command /
|
|
54
|
+
* predicate — a typo is caught when the game boots, not when a player reaches that step.
|
|
55
|
+
*/
|
|
56
|
+
static validateCatalog(catalog, capabilities) {
|
|
57
|
+
const findings = [];
|
|
58
|
+
const add = (code, path, message) => findings.push({ code: code, path: path, message: message });
|
|
59
|
+
if (!catalog || !Array.isArray(catalog.chapters) || !catalog.chapters.length) {
|
|
60
|
+
add('ONB-EMPTY', 'chapters', 'a catalog needs at least one chapter');
|
|
61
|
+
return findings;
|
|
62
|
+
}
|
|
63
|
+
if (catalog.catalogVersion === undefined || catalog.catalogVersion === null || catalog.catalogVersion === '') {
|
|
64
|
+
add('ONB-VERSION', 'catalogVersion', 'required — saves are migrated by it');
|
|
65
|
+
}
|
|
66
|
+
const chapterIds = new Set();
|
|
67
|
+
const stepIds = new Set();
|
|
68
|
+
catalog.chapters.forEach((chapter, c) => {
|
|
69
|
+
if (!chapter?.chapterId || chapterIds.has(chapter.chapterId)) {
|
|
70
|
+
add('ONB-CHAPTER-ID', `chapters[${c}]`, `chapter id must be non-empty and unique (${chapter?.chapterId})`);
|
|
71
|
+
}
|
|
72
|
+
chapterIds.add(chapter?.chapterId);
|
|
73
|
+
if (!Array.isArray(chapter?.steps) || !chapter.steps.length) {
|
|
74
|
+
add('ONB-CHAPTER-EMPTY', `chapters[${c}]`, `chapter '${chapter?.chapterId}' has no steps`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
chapter.steps.forEach((step, s) => {
|
|
78
|
+
if (!step?.stepId || stepIds.has(step.stepId)) {
|
|
79
|
+
add('ONB-STEP-ID', `chapters[${c}].steps[${s}]`, `step id must be non-empty and unique across the catalog (${step?.stepId})`);
|
|
80
|
+
}
|
|
81
|
+
stepIds.add(step?.stepId);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
const checkRef = (ref, kind, path) => {
|
|
85
|
+
if (ref === undefined) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!ref || typeof ref.name !== 'string' || !ref.name) {
|
|
89
|
+
add('ONB-REF', path, 'a ref needs a non-empty name');
|
|
90
|
+
}
|
|
91
|
+
else if (capabilities && typeof capabilities[kind]?.[ref.name] !== 'function') {
|
|
92
|
+
add('ONB-REF-UNKNOWN', path, `'${ref.name}' is not a registered ${kind === 'commands' ? 'command' : 'predicate'}`);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
catalog.chapters.forEach((chapter, c) => {
|
|
96
|
+
(chapter?.steps ?? []).forEach((step, s) => {
|
|
97
|
+
const path = `chapters[${c}].steps[${s}]`;
|
|
98
|
+
checkRef(step?.command, 'commands', `${path}.command`);
|
|
99
|
+
checkRef(step?.completion, 'predicates', `${path}.completion`);
|
|
100
|
+
checkRef(step?.stateValidator, 'predicates', `${path}.stateValidator`);
|
|
101
|
+
(step?.fallbackStepIds ?? []).forEach((fallback) => {
|
|
102
|
+
if (fallback === step.stepId || !stepIds.has(fallback)) {
|
|
103
|
+
add('ONB-FALLBACK', `${path}.fallbackStepIds`, `'${fallback}' is not another step of the catalog`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if (step?.timeoutMs !== undefined && !(Number.isFinite(step.timeoutMs) && step.timeoutMs > 0)) {
|
|
107
|
+
add('ONB-TIMEOUT', `${path}.timeoutMs`, `must be a positive number (${step.timeoutMs})`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
for (const source of Object.keys(catalog.migrations ?? {})) {
|
|
112
|
+
if (stepIds.has(source)) {
|
|
113
|
+
add('ONB-MIGRATION-SOURCE-LIVE', `migrations.${source}`, 'the source id still exists — a rule for a live step would hijack it');
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const seen = new Set([source]);
|
|
117
|
+
let target = (catalog.migrations)[source];
|
|
118
|
+
while (target !== undefined && !stepIds.has(target) && (catalog.migrations)[target] !== undefined) {
|
|
119
|
+
if (seen.has(target)) {
|
|
120
|
+
add('ONB-MIGRATION-CYCLE', `migrations.${source}`, `cycle through '${target}'`);
|
|
121
|
+
target = undefined;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
seen.add(target);
|
|
125
|
+
target = (catalog.migrations)[target];
|
|
126
|
+
}
|
|
127
|
+
if (target !== undefined && !stepIds.has(target)) {
|
|
128
|
+
add('ONB-MIGRATION-TARGET', `migrations.${source}`, `resolves to '${target}', which is not a step of the catalog`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return findings;
|
|
132
|
+
}
|
|
133
|
+
/** Subscribe; returns the unsubscribe. A throwing listener is logged and isolated — it never breaks the runtime. */
|
|
134
|
+
onEvent(listener) {
|
|
135
|
+
this.listeners.push(listener);
|
|
136
|
+
return () => {
|
|
137
|
+
const index = this.listeners.indexOf(listener);
|
|
138
|
+
if (index >= 0) {
|
|
139
|
+
this.listeners.splice(index, 1);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
getStatus() {
|
|
144
|
+
return this.status;
|
|
145
|
+
}
|
|
146
|
+
/** Where the player is; `null` unless active. */
|
|
147
|
+
position() {
|
|
148
|
+
if (this.status !== 'active' || this.currentIndex < 0) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const current = this.steps[this.currentIndex];
|
|
152
|
+
const chapterSteps = this.steps
|
|
153
|
+
.filter((s) => s.chapterIndex === current.chapterIndex);
|
|
154
|
+
return {
|
|
155
|
+
chapterId: current.chapterId,
|
|
156
|
+
chapterIndex: current.chapterIndex,
|
|
157
|
+
chapterCount: this.chapterCount,
|
|
158
|
+
step: current.step,
|
|
159
|
+
stepIndexInChapter: chapterSteps.indexOf(current),
|
|
160
|
+
chapterStepCount: chapterSteps.length,
|
|
161
|
+
doneCount: this.completed.size + this.skipped.size,
|
|
162
|
+
totalCount: this.steps.length,
|
|
163
|
+
manual: !current.step.completion,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Starts a not-yet-started onboarding at its first step (no-op otherwise — a finished one is `restart`ed). */
|
|
167
|
+
async start(context) {
|
|
168
|
+
if (this.status !== 'not-started') {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.status = 'active';
|
|
172
|
+
await this.transition(async () => this.enter(0, context, true, false));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* One check of the active step: state regression → fallback · completion → next step · timeout → recorded and left.
|
|
176
|
+
* Call it from whatever drives the game (tick, state event); it never waits by itself.
|
|
177
|
+
*/
|
|
178
|
+
async evaluate(context) {
|
|
179
|
+
if (this.status !== 'active' || this.transitioning) {
|
|
180
|
+
return this.status === 'active' ? 'waiting' : 'inactive';
|
|
181
|
+
}
|
|
182
|
+
this.assertResumed();
|
|
183
|
+
const step = this.steps[this.currentIndex].step;
|
|
184
|
+
if (step.stateValidator && !this.predicate(step.stateValidator, context, step.stepId)) {
|
|
185
|
+
const visited = new Set([step.stepId]);
|
|
186
|
+
for (const fallbackId of step.fallbackStepIds ?? []) {
|
|
187
|
+
if (visited.has(fallbackId)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
visited.add(fallbackId);
|
|
191
|
+
const index = this.indexOf(fallbackId);
|
|
192
|
+
const fallback = this.steps[index].step;
|
|
193
|
+
if (fallback.stateValidator && !this.predicate(fallback.stateValidator, context, fallback.stepId)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// Everything from the fallback on has to be done again.
|
|
197
|
+
for (let i = index; i <= Math.max(index, this.currentIndex); i++) {
|
|
198
|
+
this.completed.delete(this.steps[i].step.stepId);
|
|
199
|
+
this.skipped.delete(this.steps[i].step.stepId);
|
|
200
|
+
}
|
|
201
|
+
this.emit({ type: 'regressed', stepId: fallbackId, fromStepId: step.stepId, chapterId: this.steps[index].chapterId });
|
|
202
|
+
await this.transition(async () => this.enter(index, context, true, false));
|
|
203
|
+
return 'regressed';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (step.completion && this.predicate(step.completion, context, step.stepId)) {
|
|
207
|
+
await this.transition(async () => this.leave('completed', context));
|
|
208
|
+
return 'advanced';
|
|
209
|
+
}
|
|
210
|
+
if (step.timeoutMs !== undefined && this.now() - this.enteredAtMs >= step.timeoutMs) {
|
|
211
|
+
await this.transition(async () => this.leave('timed-out', context));
|
|
212
|
+
return 'timed-out';
|
|
213
|
+
}
|
|
214
|
+
return 'waiting';
|
|
215
|
+
}
|
|
216
|
+
/** The player acknowledges a MANUAL step (no completion predicate); a gameplay step throws `DYFM-ONBOARDING-NOT-MANUAL`. */
|
|
217
|
+
async next(context) {
|
|
218
|
+
const step = this.activeStep('next');
|
|
219
|
+
if (step.completion) {
|
|
220
|
+
throw new error_control_model_1.DyFM_Error({
|
|
221
|
+
message: `DyFM_GuidedOnboarding: step '${step.stepId}' completes by '${step.completion.name}', not by "Next" — use skip('step') to leave it`,
|
|
222
|
+
errorCode: 'DYFM-ONBOARDING-NOT-MANUAL',
|
|
223
|
+
issuerService: ISSUER,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
await this.transition(async () => this.leave('completed', context));
|
|
227
|
+
}
|
|
228
|
+
/** Skip the active step, the rest of its chapter, or the whole onboarding. */
|
|
229
|
+
async skip(context, scope) {
|
|
230
|
+
const step = this.activeStep('skip');
|
|
231
|
+
if (scope === 'all') {
|
|
232
|
+
this.status = 'skipped';
|
|
233
|
+
this.currentIndex = -1;
|
|
234
|
+
this.emit({ type: 'skipped', stepId: step.stepId });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (scope === 'chapter') {
|
|
238
|
+
const chapterIndex = this.steps[this.currentIndex].chapterIndex;
|
|
239
|
+
for (let i = this.currentIndex + 1; i < this.steps.length && this.steps[i].chapterIndex === chapterIndex; i++) {
|
|
240
|
+
const id = this.steps[i].step.stepId;
|
|
241
|
+
if (!this.completed.has(id) && !this.skipped.has(id)) {
|
|
242
|
+
this.skipped.add(id);
|
|
243
|
+
this.emit({ type: 'step-skipped', stepId: id, chapterId: this.steps[i].chapterId });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
await this.transition(async () => this.leave('skipped', context));
|
|
248
|
+
}
|
|
249
|
+
/** Start over — all of it, or from a chapter on (earlier chapters keep their progress). */
|
|
250
|
+
async restart(context, fromChapterId) {
|
|
251
|
+
const from = fromChapterId === undefined
|
|
252
|
+
? 0
|
|
253
|
+
: this.steps.findIndex((s) => s.chapterId === fromChapterId);
|
|
254
|
+
if (from < 0) {
|
|
255
|
+
throw new error_control_model_1.DyFM_Error({
|
|
256
|
+
message: `DyFM_GuidedOnboarding: restart from unknown chapter '${fromChapterId}'`,
|
|
257
|
+
errorCode: 'DYFM-ONBOARDING-UNKNOWN-CHAPTER',
|
|
258
|
+
issuerService: ISSUER,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
for (let i = from; i < this.steps.length; i++) {
|
|
262
|
+
this.completed.delete(this.steps[i].step.stepId);
|
|
263
|
+
this.skipped.delete(this.steps[i].step.stepId);
|
|
264
|
+
}
|
|
265
|
+
this.status = 'active';
|
|
266
|
+
this.pendingResume = false;
|
|
267
|
+
this.emit({ type: 'restarted', chapterId: this.steps[from].chapterId });
|
|
268
|
+
await this.transition(async () => this.enter(from, context, true, false));
|
|
269
|
+
}
|
|
270
|
+
/** The progress as data (`dyfm-onboarding/1`); ids in catalog order, so two equal states serialise identically. */
|
|
271
|
+
exportState() {
|
|
272
|
+
const inOrder = (set) => this.steps
|
|
273
|
+
.map((s) => s.step.stepId).filter((id) => set.has(id));
|
|
274
|
+
const state = {
|
|
275
|
+
schema: 'dyfm-onboarding/1',
|
|
276
|
+
catalogVersion: this.catalog.catalogVersion,
|
|
277
|
+
status: this.status,
|
|
278
|
+
completedStepIds: inOrder(this.completed),
|
|
279
|
+
skippedStepIds: inOrder(this.skipped),
|
|
280
|
+
};
|
|
281
|
+
if (this.status === 'active' && this.currentIndex >= 0) {
|
|
282
|
+
state.stepId = this.steps[this.currentIndex].step.stepId;
|
|
283
|
+
}
|
|
284
|
+
return state;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Restores a save onto THIS catalog: ids are rewritten by the migration rules, vanished ids are dropped, and an
|
|
288
|
+
* active step that no longer exists (or is already done) continues at the first unfinished step. No command runs —
|
|
289
|
+
* an active onboarding must be `resume`d, and until then `evaluate` / `next` / `skip` throw
|
|
290
|
+
* `DYFM-ONBOARDING-NOT-RESUMED` (a forgotten resume would otherwise leave the player stuck, silently).
|
|
291
|
+
*/
|
|
292
|
+
importState(state) {
|
|
293
|
+
if (!state || state.schema !== 'dyfm-onboarding/1'
|
|
294
|
+
|| !['not-started', 'active', 'completed', 'skipped'].includes(state.status)
|
|
295
|
+
|| !Array.isArray(state.completedStepIds) || !Array.isArray(state.skippedStepIds)) {
|
|
296
|
+
throw new error_control_model_1.DyFM_Error({
|
|
297
|
+
message: `DyFM_GuidedOnboarding: not a 'dyfm-onboarding/1' state (schema: ${String(state?.schema)}, status: ${String(state?.status)})`,
|
|
298
|
+
errorCode: 'DYFM-ONBOARDING-STATE',
|
|
299
|
+
issuerService: ISSUER,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const report = { migrated: state.catalogVersion !== this.catalog.catalogVersion, renamed: {}, dropped: [] };
|
|
303
|
+
const resolve = (id) => {
|
|
304
|
+
let current = id;
|
|
305
|
+
for (let hops = 0; this.indexOf(current, false) < 0; hops++) {
|
|
306
|
+
const next = this.catalog.migrations?.[current];
|
|
307
|
+
if (next === undefined || hops > this.steps.length) {
|
|
308
|
+
if (!report.dropped.includes(id)) {
|
|
309
|
+
report.dropped.push(id);
|
|
310
|
+
}
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
current = next;
|
|
314
|
+
}
|
|
315
|
+
if (current !== id) {
|
|
316
|
+
report.renamed[id] = current;
|
|
317
|
+
}
|
|
318
|
+
return current;
|
|
319
|
+
};
|
|
320
|
+
this.completed.clear();
|
|
321
|
+
this.skipped.clear();
|
|
322
|
+
state.completedStepIds.forEach((id) => {
|
|
323
|
+
const resolved = resolve(id);
|
|
324
|
+
if (resolved) {
|
|
325
|
+
this.completed.add(resolved);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
state.skippedStepIds.forEach((id) => {
|
|
329
|
+
const resolved = resolve(id);
|
|
330
|
+
if (resolved && !this.completed.has(resolved)) {
|
|
331
|
+
this.skipped.add(resolved);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
this.status = state.status;
|
|
335
|
+
this.currentIndex = -1;
|
|
336
|
+
this.pendingResume = false;
|
|
337
|
+
if (state.status === 'active') {
|
|
338
|
+
const resolved = state.stepId === undefined ? undefined : resolve(state.stepId);
|
|
339
|
+
let index = resolved === undefined ? -1 : this.indexOf(resolved);
|
|
340
|
+
if (index < 0 || this.isDone(index)) {
|
|
341
|
+
index = this.steps.findIndex((s, i) => !this.isDone(i));
|
|
342
|
+
if (index >= 0) {
|
|
343
|
+
report.resumedAt = this.steps[index].step.stepId;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (index < 0) {
|
|
347
|
+
this.status = 'completed';
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
this.currentIndex = index;
|
|
351
|
+
this.pendingResume = true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
report.migrated = report.migrated || Object.keys(report.renamed).length > 0 || report.dropped.length > 0
|
|
355
|
+
|| report.resumedAt !== undefined;
|
|
356
|
+
return report;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Enters the restored step after `importState`. Its command is NOT run again by default (it already ran before the
|
|
360
|
+
* save — e.g. placing a building twice would corrupt the game); pass `rerunCommand` for idempotent commands.
|
|
361
|
+
*/
|
|
362
|
+
async resume(context, options = {}) {
|
|
363
|
+
if (!this.pendingResume) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this.pendingResume = false;
|
|
367
|
+
await this.transition(async () => this.enter(this.currentIndex, context, options.rerunCommand === true, true));
|
|
368
|
+
}
|
|
369
|
+
/** Leaves the active step (completed / skipped / timed out) and enters the next unfinished one — or finishes. */
|
|
370
|
+
async leave(how, context) {
|
|
371
|
+
const from = this.steps[this.currentIndex];
|
|
372
|
+
if (how === 'completed') {
|
|
373
|
+
this.completed.add(from.step.stepId);
|
|
374
|
+
this.emit({ type: 'step-completed', stepId: from.step.stepId, chapterId: from.chapterId });
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// A timed-out step is recorded as skipped: the player is not sent back into it on resume.
|
|
378
|
+
this.skipped.add(from.step.stepId);
|
|
379
|
+
this.emit({ type: how === 'skipped' ? 'step-skipped' : 'step-timed-out', stepId: from.step.stepId, chapterId: from.chapterId });
|
|
380
|
+
}
|
|
381
|
+
let next = -1;
|
|
382
|
+
for (let i = this.currentIndex + 1; i < this.steps.length; i++) {
|
|
383
|
+
if (!this.isDone(i)) {
|
|
384
|
+
next = i;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (next < 0 || this.steps[next].chapterIndex !== from.chapterIndex) {
|
|
389
|
+
const chapterDone = this.steps
|
|
390
|
+
.filter((s) => s.chapterIndex === from.chapterIndex)
|
|
391
|
+
.every((s) => this.completed.has(s.step.stepId));
|
|
392
|
+
if (chapterDone) {
|
|
393
|
+
this.emit({ type: 'chapter-completed', chapterId: from.chapterId });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (next < 0) {
|
|
397
|
+
this.status = 'completed';
|
|
398
|
+
this.currentIndex = -1;
|
|
399
|
+
this.emit({ type: 'completed' });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
await this.enter(next, context, true, false);
|
|
403
|
+
}
|
|
404
|
+
/** Makes `index` the active step; runs its command (a failing command is reported and the step stays active). */
|
|
405
|
+
async enter(index, context, runCommand, resumed) {
|
|
406
|
+
const entry = this.steps[index];
|
|
407
|
+
this.currentIndex = index;
|
|
408
|
+
this.enteredAtMs = this.now();
|
|
409
|
+
this.emit(resumed
|
|
410
|
+
? { type: 'step-entered', stepId: entry.step.stepId, chapterId: entry.chapterId, resumed: true }
|
|
411
|
+
: { type: 'step-entered', stepId: entry.step.stepId, chapterId: entry.chapterId });
|
|
412
|
+
if (runCommand && entry.step.command) {
|
|
413
|
+
try {
|
|
414
|
+
await this.capabilities.commands[entry.step.command.name](context, entry.step.command.args);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
this.emit({ type: 'command-failed', stepId: entry.step.stepId, chapterId: entry.chapterId, error: error });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/** Evaluates a predicate; a throwing one becomes a debuggable `DYFM-ONBOARDING-PREDICATE` (step + predicate named). */
|
|
422
|
+
predicate(ref, context, stepId) {
|
|
423
|
+
try {
|
|
424
|
+
return this.capabilities.predicates[ref.name](context, ref.args) === true;
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
throw new error_control_model_1.DyFM_Error({
|
|
428
|
+
message: `DyFM_GuidedOnboarding: predicate '${ref.name}' of step '${stepId}' threw`,
|
|
429
|
+
errorCode: 'DYFM-ONBOARDING-PREDICATE',
|
|
430
|
+
issuerService: ISSUER,
|
|
431
|
+
error: error,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async transition(run) {
|
|
436
|
+
this.transitioning = true;
|
|
437
|
+
try {
|
|
438
|
+
await run();
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
this.transitioning = false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
activeStep(operation) {
|
|
445
|
+
if (this.status !== 'active' || this.currentIndex < 0) {
|
|
446
|
+
throw new error_control_model_1.DyFM_Error({
|
|
447
|
+
message: `DyFM_GuidedOnboarding: '${operation}' needs an active onboarding (status: ${this.status})`,
|
|
448
|
+
errorCode: 'DYFM-ONBOARDING-INACTIVE',
|
|
449
|
+
issuerService: ISSUER,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
this.assertResumed();
|
|
453
|
+
return this.steps[this.currentIndex].step;
|
|
454
|
+
}
|
|
455
|
+
assertResumed() {
|
|
456
|
+
if (this.pendingResume) {
|
|
457
|
+
throw new error_control_model_1.DyFM_Error({
|
|
458
|
+
message: 'DyFM_GuidedOnboarding: the imported onboarding is active but not resumed — call resume(context) first',
|
|
459
|
+
errorCode: 'DYFM-ONBOARDING-NOT-RESUMED',
|
|
460
|
+
issuerService: ISSUER,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
isDone(index) {
|
|
465
|
+
const id = this.steps[index].step.stepId;
|
|
466
|
+
return this.completed.has(id) || this.skipped.has(id);
|
|
467
|
+
}
|
|
468
|
+
indexOf(stepId, required = true) {
|
|
469
|
+
const index = this.steps.findIndex((s) => s.step.stepId === stepId);
|
|
470
|
+
if (index < 0 && required) {
|
|
471
|
+
throw new error_control_model_1.DyFM_Error({
|
|
472
|
+
message: `DyFM_GuidedOnboarding: unknown step '${stepId}'`,
|
|
473
|
+
errorCode: 'DYFM-ONBOARDING-UNKNOWN-STEP',
|
|
474
|
+
issuerService: ISSUER,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return index;
|
|
478
|
+
}
|
|
479
|
+
emit(event) {
|
|
480
|
+
for (const listener of [...this.listeners]) {
|
|
481
|
+
try {
|
|
482
|
+
listener(event);
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
log_util_1.DyFM_Log.error(`${ISSUER}: an onEvent listener threw on '${event.type}'`, error);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
exports.DyFM_GuidedOnboarding_ControlModel = DyFM_GuidedOnboarding_ControlModel;
|
|
491
|
+
//# sourceMappingURL=guided-onboarding.control-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guided-onboarding.control-model.js","sourceRoot":"","sources":["../../../../src/_modules/game/_models/guided-onboarding.control-model.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BFR-WARFACTORY-014 — a DECLARATIVE reference to a game-supplied command or predicate. It is plain data (a name and
|
|
3
|
+
* one JSON argument), so a catalog can cross a package boundary — a closure over the game's services cannot.
|
|
4
|
+
*/
|
|
5
|
+
export interface DyFM_OnboardingRef {
|
|
6
|
+
name: string;
|
|
7
|
+
args?: unknown;
|
|
8
|
+
}
|
|
9
|
+
/** One onboarding step. `stepId` is stable and unique across the WHOLE catalog (saves point at it, not at an index). */
|
|
10
|
+
export interface DyFM_OnboardingStep_Definition {
|
|
11
|
+
stepId: string;
|
|
12
|
+
/** Run once when the step is entered (e.g. open a panel, pre-select a building). */
|
|
13
|
+
command?: DyFM_OnboardingRef;
|
|
14
|
+
/** The step is done when this predicate holds. Without it the step is MANUAL — the player acknowledges it (`next`). */
|
|
15
|
+
completion?: DyFM_OnboardingRef;
|
|
16
|
+
/** Must keep holding while the step is active; when it breaks, the runtime regresses to a `fallbackStepIds` step. */
|
|
17
|
+
stateValidator?: DyFM_OnboardingRef;
|
|
18
|
+
/** Recovery steps in priority order; the first one whose own `stateValidator` holds is entered. */
|
|
19
|
+
fallbackStepIds?: string[];
|
|
20
|
+
/** Bounded recovery: after this long without completion the step is recorded as timed out and left. */
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
/** Highlight target for the UI layer (the runtime never touches the DOM); alternates are tried in order. */
|
|
23
|
+
highlight?: {
|
|
24
|
+
targetId: string;
|
|
25
|
+
alternateTargetIds?: string[];
|
|
26
|
+
anchorId?: string;
|
|
27
|
+
};
|
|
28
|
+
/** Opaque, content-side data (text key, narration asset id, world position …) — carried, never interpreted. */
|
|
29
|
+
payload?: unknown;
|
|
30
|
+
}
|
|
31
|
+
export interface DyFM_OnboardingChapter_Definition {
|
|
32
|
+
chapterId: string;
|
|
33
|
+
steps: DyFM_OnboardingStep_Definition[];
|
|
34
|
+
payload?: unknown;
|
|
35
|
+
}
|
|
36
|
+
export interface DyFM_OnboardingCatalog {
|
|
37
|
+
/** Bump it when steps change — a save made with another version is migrated on import. */
|
|
38
|
+
catalogVersion: string | number;
|
|
39
|
+
chapters: DyFM_OnboardingChapter_Definition[];
|
|
40
|
+
/** Removed / renamed step id → its replacement (chains are followed; cycles are a catalog error). */
|
|
41
|
+
migrations?: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
/** The game's capability surface the declarative refs resolve against. */
|
|
44
|
+
export interface DyFM_OnboardingCapabilities<TContext> {
|
|
45
|
+
commands?: Record<string, (context: TContext, args: unknown) => void | Promise<void>>;
|
|
46
|
+
predicates?: Record<string, (context: TContext, args: unknown) => boolean>;
|
|
47
|
+
}
|
|
48
|
+
/** Lifecycle of an onboarding run. */
|
|
49
|
+
export type DyFM_OnboardingStatus_Type = 'not-started' | 'active' | 'completed' | 'skipped';
|
|
50
|
+
/** The serialisable progress — `dyfm-onboarding/1`. */
|
|
51
|
+
export interface DyFM_OnboardingState {
|
|
52
|
+
schema: 'dyfm-onboarding/1';
|
|
53
|
+
catalogVersion: string | number;
|
|
54
|
+
status: DyFM_OnboardingStatus_Type;
|
|
55
|
+
/** The active step (only while `active`). */
|
|
56
|
+
stepId?: string;
|
|
57
|
+
completedStepIds: string[];
|
|
58
|
+
skippedStepIds: string[];
|
|
59
|
+
}
|
|
60
|
+
/** What `importState` did to fit an older save onto the current catalog. */
|
|
61
|
+
export interface DyFM_OnboardingMigrationReport {
|
|
62
|
+
migrated: boolean;
|
|
63
|
+
/** Old id → new id, for every id a migration rule rewrote. */
|
|
64
|
+
renamed: Record<string, string>;
|
|
65
|
+
/** Ids that no longer exist and had no migration rule (dropped from the progress). */
|
|
66
|
+
dropped: string[];
|
|
67
|
+
/** The active step could not be kept: where the player continues instead. */
|
|
68
|
+
resumedAt?: string;
|
|
69
|
+
}
|
|
70
|
+
/** What one `evaluate()` did. */
|
|
71
|
+
export type DyFM_OnboardingEvaluation_Type = 'inactive' | 'waiting' | 'advanced' | 'regressed' | 'timed-out';
|
|
72
|
+
export interface DyFM_OnboardingEvent {
|
|
73
|
+
type: 'step-entered' | 'step-completed' | 'step-skipped' | 'step-timed-out' | 'regressed' | 'chapter-completed' | 'completed' | 'skipped' | 'restarted' | 'command-failed';
|
|
74
|
+
stepId?: string;
|
|
75
|
+
chapterId?: string;
|
|
76
|
+
/** `regressed`: the step that lost its state. */
|
|
77
|
+
fromStepId?: string;
|
|
78
|
+
/** `step-entered` after `importState` + `resume`. */
|
|
79
|
+
resumed?: boolean;
|
|
80
|
+
/** `command-failed`. */
|
|
81
|
+
error?: unknown;
|
|
82
|
+
}
|
|
83
|
+
/** Where the player is — everything a UI needs to render "Chapter 2 · step 3 of 5". */
|
|
84
|
+
export interface DyFM_OnboardingPosition {
|
|
85
|
+
chapterId: string;
|
|
86
|
+
chapterIndex: number;
|
|
87
|
+
chapterCount: number;
|
|
88
|
+
step: DyFM_OnboardingStep_Definition;
|
|
89
|
+
stepIndexInChapter: number;
|
|
90
|
+
chapterStepCount: number;
|
|
91
|
+
/** Completed + skipped steps over all steps of the catalog. */
|
|
92
|
+
doneCount: number;
|
|
93
|
+
totalCount: number;
|
|
94
|
+
/** True when the step has no completion predicate — the UI shows a "Next" button. */
|
|
95
|
+
manual: boolean;
|
|
96
|
+
}
|
|
97
|
+
export interface DyFM_OnboardingFinding {
|
|
98
|
+
code: string;
|
|
99
|
+
path: string;
|
|
100
|
+
message: string;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=guided-onboarding.interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guided-onboarding.interface.d.ts","sourceRoot":"","sources":["../../../../src/_modules/game/_models/guided-onboarding.interface.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,wHAAwH;AACxH,MAAM,WAAW,8BAA8B;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,oFAAoF;IACpF,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,uHAAuH;IACvH,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,qHAAqH;IACrH,cAAc,CAAC,EAAE,kBAAkB,CAAC;IACpC,mGAAmG;IACnG,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,uGAAuG;IACvG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4GAA4G;IAC5G,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnF,+GAA+G;IAC/G,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iCAAiC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,8BAA8B,EAAE,CAAC;IACxC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,0FAA0F;IAC1F,cAAc,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,QAAQ,EAAE,iCAAiC,EAAE,CAAC;IAC9C,qGAAqG;IACrG,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED,0EAA0E;AAC1E,MAAM,WAAW,2BAA2B,CAAC,QAAQ;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC;CAC5E;AAED,sCAAsC;AACtC,MAAM,MAAM,0BAA0B,GAAG,aAAa,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;AAE5F,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,cAAc,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,MAAM,EAAE,0BAA0B,CAAC;IACnC,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,4EAA4E;AAC5E,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,OAAO,CAAC;IAClB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,sFAAsF;IACtF,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,iCAAiC;AACjC,MAAM,MAAM,8BAA8B,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC;AAE7G,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,cAAc,GAAG,gBAAgB,GAAG,cAAc,GAAG,gBAAgB,GAAG,WAAW,GAAG,mBAAmB,GAC3G,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,gBAAgB,CAAC;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wBAAwB;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,uFAAuF;AACvF,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,8BAA8B,CAAC;IACrC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,qFAAqF;IACrF,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guided-onboarding.interface.js","sourceRoot":"","sources":["../../../../src/_modules/game/_models/guided-onboarding.interface.ts"],"names":[],"mappings":""}
|
|
@@ -20,6 +20,8 @@ export * from './_models/viewport.interface';
|
|
|
20
20
|
export * from './_models/viewport.control-model';
|
|
21
21
|
export * from './_models/input-action.interface';
|
|
22
22
|
export * from './_models/input-action-registry.control-model';
|
|
23
|
+
export * from './_models/guided-onboarding.interface';
|
|
24
|
+
export * from './_models/guided-onboarding.control-model';
|
|
23
25
|
export * from './_collections/audio-mixer.util';
|
|
24
26
|
export * from './_collections/audio-scale.util';
|
|
25
27
|
export * from './_collections/game-save.util';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/_modules/game/index.ts"],"names":[],"mappings":"AACA,cAAc,6BAA6B,CAAC;AAG5C,cAAc,wCAAwC,CAAC;AACvD,cAAc,0CAA0C,CAAC;AACzD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,uCAAuC,CAAC;AACtD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,gDAAgD,CAAC;AAC/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,uCAAuC,CAAC;AACtD,cAAc,sCAAsC,CAAC;AACrD,cAAc,0CAA0C,CAAC;AACzD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0CAA0C,CAAC;AACzD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,uCAAuC,CAAC;AACtD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,kCAAkC,CAAC;AACjD,cAAc,+CAA+C,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/_modules/game/index.ts"],"names":[],"mappings":"AACA,cAAc,6BAA6B,CAAC;AAG5C,cAAc,wCAAwC,CAAC;AACvD,cAAc,0CAA0C,CAAC;AACzD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,uCAAuC,CAAC;AACtD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,gDAAgD,CAAC;AAC/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,uCAAuC,CAAC;AACtD,cAAc,sCAAsC,CAAC;AACrD,cAAc,0CAA0C,CAAC;AACzD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0CAA0C,CAAC;AACzD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,uCAAuC,CAAC;AACtD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,kCAAkC,CAAC;AACjD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,uCAAuC,CAAC;AACtD,cAAc,2CAA2C,CAAC;AAG1D,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uCAAuC,CAAC"}
|
|
@@ -25,6 +25,8 @@ tslib_1.__exportStar(require("./_models/viewport.interface"), exports);
|
|
|
25
25
|
tslib_1.__exportStar(require("./_models/viewport.control-model"), exports);
|
|
26
26
|
tslib_1.__exportStar(require("./_models/input-action.interface"), exports);
|
|
27
27
|
tslib_1.__exportStar(require("./_models/input-action-registry.control-model"), exports);
|
|
28
|
+
tslib_1.__exportStar(require("./_models/guided-onboarding.interface"), exports);
|
|
29
|
+
tslib_1.__exportStar(require("./_models/guided-onboarding.control-model"), exports);
|
|
28
30
|
// COLLECTIONS
|
|
29
31
|
tslib_1.__exportStar(require("./_collections/audio-mixer.util"), exports);
|
|
30
32
|
tslib_1.__exportStar(require("./_collections/audio-scale.util"), exports);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/_modules/game/index.ts"],"names":[],"mappings":";;;AAAA,QAAQ;AACR,sEAA4C;AAE5C,SAAS;AACT,iFAAuD;AACvD,mFAAyD;AACzD,gFAAsD;AACtD,8EAAoD;AACpD,0EAAgD;AAChD,kFAAwD;AACxD,gFAAsD;AACtD,qFAA2D;AAC3D,yFAA+D;AAC/D,4EAAkD;AAClD,gFAAsD;AACtD,+EAAqD;AACrD,mFAAyD;AACzD,wEAA8C;AAC9C,mFAAyD;AACzD,uFAA6D;AAC7D,gFAAsD;AACtD,uEAA6C;AAC7C,2EAAiD;AACjD,2EAAiD;AACjD,wFAA8D;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/_modules/game/index.ts"],"names":[],"mappings":";;;AAAA,QAAQ;AACR,sEAA4C;AAE5C,SAAS;AACT,iFAAuD;AACvD,mFAAyD;AACzD,gFAAsD;AACtD,8EAAoD;AACpD,0EAAgD;AAChD,kFAAwD;AACxD,gFAAsD;AACtD,qFAA2D;AAC3D,yFAA+D;AAC/D,4EAAkD;AAClD,gFAAsD;AACtD,+EAAqD;AACrD,mFAAyD;AACzD,wEAA8C;AAC9C,mFAAyD;AACzD,uFAA6D;AAC7D,gFAAsD;AACtD,uEAA6C;AAC7C,2EAAiD;AACjD,2EAAiD;AACjD,wFAA8D;AAC9D,gFAAsD;AACtD,oFAA0D;AAE1D,cAAc;AACd,0EAAgD;AAChD,0EAAgD;AAChD,wEAA8C;AAC9C,gFAAsD"}
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { DyFM_Log } from '../../../_collections/utils/log.util';
|
|
2
|
+
import { DyFM_Error } from '../../../_models/control-models/error.control-model';
|
|
3
|
+
const ISSUER = 'DyFM_GuidedOnboarding_ControlModel';
|
|
4
|
+
/**
|
|
5
|
+
* BFR-WARFACTORY-014 — content-agnostic guided onboarding: chapters of steps with STABLE ids, declarative commands and
|
|
6
|
+
* predicates resolved against the game's capability surface, state-regression recovery, bounded timeouts,
|
|
7
|
+
* skip / restart / resume, and a versioned, migratable progress state.
|
|
8
|
+
*
|
|
9
|
+
* It owns no timer and no DOM: the driver (a game loop tick, a state-change event, an Angular effect) calls
|
|
10
|
+
* `evaluate()`; the UI layer renders `position()` and listens to `onEvent()`. The clock is injectable, so timeouts are
|
|
11
|
+
* deterministic in tests.
|
|
12
|
+
*/
|
|
13
|
+
export class DyFM_GuidedOnboarding_ControlModel {
|
|
14
|
+
catalog;
|
|
15
|
+
capabilities;
|
|
16
|
+
now;
|
|
17
|
+
steps = [];
|
|
18
|
+
chapterCount;
|
|
19
|
+
status = 'not-started';
|
|
20
|
+
currentIndex = -1;
|
|
21
|
+
enteredAtMs = 0;
|
|
22
|
+
completed = new Set();
|
|
23
|
+
skipped = new Set();
|
|
24
|
+
listeners = [];
|
|
25
|
+
/** `importState` restored an active step, but it was not entered yet (`resume`). */
|
|
26
|
+
pendingResume = false;
|
|
27
|
+
/** A transition (with a possibly async command) is running — a re-entrant `evaluate` waits it out. */
|
|
28
|
+
transitioning = false;
|
|
29
|
+
/** Validates the catalog against the capabilities; any finding throws `DYFM-ONBOARDING-CATALOG` listing them all. */
|
|
30
|
+
constructor(catalog, capabilities, now = () => Date.now()) {
|
|
31
|
+
this.catalog = catalog;
|
|
32
|
+
this.capabilities = capabilities;
|
|
33
|
+
this.now = now;
|
|
34
|
+
const findings = DyFM_GuidedOnboarding_ControlModel.validateCatalog(catalog, capabilities);
|
|
35
|
+
if (findings.length) {
|
|
36
|
+
throw new DyFM_Error({
|
|
37
|
+
message: `DyFM_GuidedOnboarding: invalid catalog — ${findings.map((f) => `[${f.code}] ${f.path}: ${f.message}`).join(' · ')}`,
|
|
38
|
+
errorCode: 'DYFM-ONBOARDING-CATALOG',
|
|
39
|
+
issuerService: ISSUER,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catalog.chapters.forEach((chapter, chapterIndex) => {
|
|
43
|
+
chapter.steps.forEach((step) => {
|
|
44
|
+
this.steps.push({ chapterId: chapter.chapterId, chapterIndex: chapterIndex, step: step });
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
this.chapterCount = catalog.chapters.length;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Every problem of a catalog, as data (never throws). With `capabilities`, each ref must name a registered command /
|
|
51
|
+
* predicate — a typo is caught when the game boots, not when a player reaches that step.
|
|
52
|
+
*/
|
|
53
|
+
static validateCatalog(catalog, capabilities) {
|
|
54
|
+
const findings = [];
|
|
55
|
+
const add = (code, path, message) => findings.push({ code: code, path: path, message: message });
|
|
56
|
+
if (!catalog || !Array.isArray(catalog.chapters) || !catalog.chapters.length) {
|
|
57
|
+
add('ONB-EMPTY', 'chapters', 'a catalog needs at least one chapter');
|
|
58
|
+
return findings;
|
|
59
|
+
}
|
|
60
|
+
if (catalog.catalogVersion === undefined || catalog.catalogVersion === null || catalog.catalogVersion === '') {
|
|
61
|
+
add('ONB-VERSION', 'catalogVersion', 'required — saves are migrated by it');
|
|
62
|
+
}
|
|
63
|
+
const chapterIds = new Set();
|
|
64
|
+
const stepIds = new Set();
|
|
65
|
+
catalog.chapters.forEach((chapter, c) => {
|
|
66
|
+
if (!chapter?.chapterId || chapterIds.has(chapter.chapterId)) {
|
|
67
|
+
add('ONB-CHAPTER-ID', `chapters[${c}]`, `chapter id must be non-empty and unique (${chapter?.chapterId})`);
|
|
68
|
+
}
|
|
69
|
+
chapterIds.add(chapter?.chapterId);
|
|
70
|
+
if (!Array.isArray(chapter?.steps) || !chapter.steps.length) {
|
|
71
|
+
add('ONB-CHAPTER-EMPTY', `chapters[${c}]`, `chapter '${chapter?.chapterId}' has no steps`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
chapter.steps.forEach((step, s) => {
|
|
75
|
+
if (!step?.stepId || stepIds.has(step.stepId)) {
|
|
76
|
+
add('ONB-STEP-ID', `chapters[${c}].steps[${s}]`, `step id must be non-empty and unique across the catalog (${step?.stepId})`);
|
|
77
|
+
}
|
|
78
|
+
stepIds.add(step?.stepId);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
const checkRef = (ref, kind, path) => {
|
|
82
|
+
if (ref === undefined) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!ref || typeof ref.name !== 'string' || !ref.name) {
|
|
86
|
+
add('ONB-REF', path, 'a ref needs a non-empty name');
|
|
87
|
+
}
|
|
88
|
+
else if (capabilities && typeof capabilities[kind]?.[ref.name] !== 'function') {
|
|
89
|
+
add('ONB-REF-UNKNOWN', path, `'${ref.name}' is not a registered ${kind === 'commands' ? 'command' : 'predicate'}`);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
catalog.chapters.forEach((chapter, c) => {
|
|
93
|
+
(chapter?.steps ?? []).forEach((step, s) => {
|
|
94
|
+
const path = `chapters[${c}].steps[${s}]`;
|
|
95
|
+
checkRef(step?.command, 'commands', `${path}.command`);
|
|
96
|
+
checkRef(step?.completion, 'predicates', `${path}.completion`);
|
|
97
|
+
checkRef(step?.stateValidator, 'predicates', `${path}.stateValidator`);
|
|
98
|
+
(step?.fallbackStepIds ?? []).forEach((fallback) => {
|
|
99
|
+
if (fallback === step.stepId || !stepIds.has(fallback)) {
|
|
100
|
+
add('ONB-FALLBACK', `${path}.fallbackStepIds`, `'${fallback}' is not another step of the catalog`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
if (step?.timeoutMs !== undefined && !(Number.isFinite(step.timeoutMs) && step.timeoutMs > 0)) {
|
|
104
|
+
add('ONB-TIMEOUT', `${path}.timeoutMs`, `must be a positive number (${step.timeoutMs})`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
for (const source of Object.keys(catalog.migrations ?? {})) {
|
|
109
|
+
if (stepIds.has(source)) {
|
|
110
|
+
add('ONB-MIGRATION-SOURCE-LIVE', `migrations.${source}`, 'the source id still exists — a rule for a live step would hijack it');
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const seen = new Set([source]);
|
|
114
|
+
let target = (catalog.migrations)[source];
|
|
115
|
+
while (target !== undefined && !stepIds.has(target) && (catalog.migrations)[target] !== undefined) {
|
|
116
|
+
if (seen.has(target)) {
|
|
117
|
+
add('ONB-MIGRATION-CYCLE', `migrations.${source}`, `cycle through '${target}'`);
|
|
118
|
+
target = undefined;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
seen.add(target);
|
|
122
|
+
target = (catalog.migrations)[target];
|
|
123
|
+
}
|
|
124
|
+
if (target !== undefined && !stepIds.has(target)) {
|
|
125
|
+
add('ONB-MIGRATION-TARGET', `migrations.${source}`, `resolves to '${target}', which is not a step of the catalog`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return findings;
|
|
129
|
+
}
|
|
130
|
+
/** Subscribe; returns the unsubscribe. A throwing listener is logged and isolated — it never breaks the runtime. */
|
|
131
|
+
onEvent(listener) {
|
|
132
|
+
this.listeners.push(listener);
|
|
133
|
+
return () => {
|
|
134
|
+
const index = this.listeners.indexOf(listener);
|
|
135
|
+
if (index >= 0) {
|
|
136
|
+
this.listeners.splice(index, 1);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
getStatus() {
|
|
141
|
+
return this.status;
|
|
142
|
+
}
|
|
143
|
+
/** Where the player is; `null` unless active. */
|
|
144
|
+
position() {
|
|
145
|
+
if (this.status !== 'active' || this.currentIndex < 0) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const current = this.steps[this.currentIndex];
|
|
149
|
+
const chapterSteps = this.steps
|
|
150
|
+
.filter((s) => s.chapterIndex === current.chapterIndex);
|
|
151
|
+
return {
|
|
152
|
+
chapterId: current.chapterId,
|
|
153
|
+
chapterIndex: current.chapterIndex,
|
|
154
|
+
chapterCount: this.chapterCount,
|
|
155
|
+
step: current.step,
|
|
156
|
+
stepIndexInChapter: chapterSteps.indexOf(current),
|
|
157
|
+
chapterStepCount: chapterSteps.length,
|
|
158
|
+
doneCount: this.completed.size + this.skipped.size,
|
|
159
|
+
totalCount: this.steps.length,
|
|
160
|
+
manual: !current.step.completion,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Starts a not-yet-started onboarding at its first step (no-op otherwise — a finished one is `restart`ed). */
|
|
164
|
+
async start(context) {
|
|
165
|
+
if (this.status !== 'not-started') {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.status = 'active';
|
|
169
|
+
await this.transition(async () => this.enter(0, context, true, false));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* One check of the active step: state regression → fallback · completion → next step · timeout → recorded and left.
|
|
173
|
+
* Call it from whatever drives the game (tick, state event); it never waits by itself.
|
|
174
|
+
*/
|
|
175
|
+
async evaluate(context) {
|
|
176
|
+
if (this.status !== 'active' || this.transitioning) {
|
|
177
|
+
return this.status === 'active' ? 'waiting' : 'inactive';
|
|
178
|
+
}
|
|
179
|
+
this.assertResumed();
|
|
180
|
+
const step = this.steps[this.currentIndex].step;
|
|
181
|
+
if (step.stateValidator && !this.predicate(step.stateValidator, context, step.stepId)) {
|
|
182
|
+
const visited = new Set([step.stepId]);
|
|
183
|
+
for (const fallbackId of step.fallbackStepIds ?? []) {
|
|
184
|
+
if (visited.has(fallbackId)) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
visited.add(fallbackId);
|
|
188
|
+
const index = this.indexOf(fallbackId);
|
|
189
|
+
const fallback = this.steps[index].step;
|
|
190
|
+
if (fallback.stateValidator && !this.predicate(fallback.stateValidator, context, fallback.stepId)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Everything from the fallback on has to be done again.
|
|
194
|
+
for (let i = index; i <= Math.max(index, this.currentIndex); i++) {
|
|
195
|
+
this.completed.delete(this.steps[i].step.stepId);
|
|
196
|
+
this.skipped.delete(this.steps[i].step.stepId);
|
|
197
|
+
}
|
|
198
|
+
this.emit({ type: 'regressed', stepId: fallbackId, fromStepId: step.stepId, chapterId: this.steps[index].chapterId });
|
|
199
|
+
await this.transition(async () => this.enter(index, context, true, false));
|
|
200
|
+
return 'regressed';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (step.completion && this.predicate(step.completion, context, step.stepId)) {
|
|
204
|
+
await this.transition(async () => this.leave('completed', context));
|
|
205
|
+
return 'advanced';
|
|
206
|
+
}
|
|
207
|
+
if (step.timeoutMs !== undefined && this.now() - this.enteredAtMs >= step.timeoutMs) {
|
|
208
|
+
await this.transition(async () => this.leave('timed-out', context));
|
|
209
|
+
return 'timed-out';
|
|
210
|
+
}
|
|
211
|
+
return 'waiting';
|
|
212
|
+
}
|
|
213
|
+
/** The player acknowledges a MANUAL step (no completion predicate); a gameplay step throws `DYFM-ONBOARDING-NOT-MANUAL`. */
|
|
214
|
+
async next(context) {
|
|
215
|
+
const step = this.activeStep('next');
|
|
216
|
+
if (step.completion) {
|
|
217
|
+
throw new DyFM_Error({
|
|
218
|
+
message: `DyFM_GuidedOnboarding: step '${step.stepId}' completes by '${step.completion.name}', not by "Next" — use skip('step') to leave it`,
|
|
219
|
+
errorCode: 'DYFM-ONBOARDING-NOT-MANUAL',
|
|
220
|
+
issuerService: ISSUER,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
await this.transition(async () => this.leave('completed', context));
|
|
224
|
+
}
|
|
225
|
+
/** Skip the active step, the rest of its chapter, or the whole onboarding. */
|
|
226
|
+
async skip(context, scope) {
|
|
227
|
+
const step = this.activeStep('skip');
|
|
228
|
+
if (scope === 'all') {
|
|
229
|
+
this.status = 'skipped';
|
|
230
|
+
this.currentIndex = -1;
|
|
231
|
+
this.emit({ type: 'skipped', stepId: step.stepId });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (scope === 'chapter') {
|
|
235
|
+
const chapterIndex = this.steps[this.currentIndex].chapterIndex;
|
|
236
|
+
for (let i = this.currentIndex + 1; i < this.steps.length && this.steps[i].chapterIndex === chapterIndex; i++) {
|
|
237
|
+
const id = this.steps[i].step.stepId;
|
|
238
|
+
if (!this.completed.has(id) && !this.skipped.has(id)) {
|
|
239
|
+
this.skipped.add(id);
|
|
240
|
+
this.emit({ type: 'step-skipped', stepId: id, chapterId: this.steps[i].chapterId });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
await this.transition(async () => this.leave('skipped', context));
|
|
245
|
+
}
|
|
246
|
+
/** Start over — all of it, or from a chapter on (earlier chapters keep their progress). */
|
|
247
|
+
async restart(context, fromChapterId) {
|
|
248
|
+
const from = fromChapterId === undefined
|
|
249
|
+
? 0
|
|
250
|
+
: this.steps.findIndex((s) => s.chapterId === fromChapterId);
|
|
251
|
+
if (from < 0) {
|
|
252
|
+
throw new DyFM_Error({
|
|
253
|
+
message: `DyFM_GuidedOnboarding: restart from unknown chapter '${fromChapterId}'`,
|
|
254
|
+
errorCode: 'DYFM-ONBOARDING-UNKNOWN-CHAPTER',
|
|
255
|
+
issuerService: ISSUER,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
for (let i = from; i < this.steps.length; i++) {
|
|
259
|
+
this.completed.delete(this.steps[i].step.stepId);
|
|
260
|
+
this.skipped.delete(this.steps[i].step.stepId);
|
|
261
|
+
}
|
|
262
|
+
this.status = 'active';
|
|
263
|
+
this.pendingResume = false;
|
|
264
|
+
this.emit({ type: 'restarted', chapterId: this.steps[from].chapterId });
|
|
265
|
+
await this.transition(async () => this.enter(from, context, true, false));
|
|
266
|
+
}
|
|
267
|
+
/** The progress as data (`dyfm-onboarding/1`); ids in catalog order, so two equal states serialise identically. */
|
|
268
|
+
exportState() {
|
|
269
|
+
const inOrder = (set) => this.steps
|
|
270
|
+
.map((s) => s.step.stepId).filter((id) => set.has(id));
|
|
271
|
+
const state = {
|
|
272
|
+
schema: 'dyfm-onboarding/1',
|
|
273
|
+
catalogVersion: this.catalog.catalogVersion,
|
|
274
|
+
status: this.status,
|
|
275
|
+
completedStepIds: inOrder(this.completed),
|
|
276
|
+
skippedStepIds: inOrder(this.skipped),
|
|
277
|
+
};
|
|
278
|
+
if (this.status === 'active' && this.currentIndex >= 0) {
|
|
279
|
+
state.stepId = this.steps[this.currentIndex].step.stepId;
|
|
280
|
+
}
|
|
281
|
+
return state;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Restores a save onto THIS catalog: ids are rewritten by the migration rules, vanished ids are dropped, and an
|
|
285
|
+
* active step that no longer exists (or is already done) continues at the first unfinished step. No command runs —
|
|
286
|
+
* an active onboarding must be `resume`d, and until then `evaluate` / `next` / `skip` throw
|
|
287
|
+
* `DYFM-ONBOARDING-NOT-RESUMED` (a forgotten resume would otherwise leave the player stuck, silently).
|
|
288
|
+
*/
|
|
289
|
+
importState(state) {
|
|
290
|
+
if (!state || state.schema !== 'dyfm-onboarding/1'
|
|
291
|
+
|| !['not-started', 'active', 'completed', 'skipped'].includes(state.status)
|
|
292
|
+
|| !Array.isArray(state.completedStepIds) || !Array.isArray(state.skippedStepIds)) {
|
|
293
|
+
throw new DyFM_Error({
|
|
294
|
+
message: `DyFM_GuidedOnboarding: not a 'dyfm-onboarding/1' state (schema: ${String(state?.schema)}, status: ${String(state?.status)})`,
|
|
295
|
+
errorCode: 'DYFM-ONBOARDING-STATE',
|
|
296
|
+
issuerService: ISSUER,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const report = { migrated: state.catalogVersion !== this.catalog.catalogVersion, renamed: {}, dropped: [] };
|
|
300
|
+
const resolve = (id) => {
|
|
301
|
+
let current = id;
|
|
302
|
+
for (let hops = 0; this.indexOf(current, false) < 0; hops++) {
|
|
303
|
+
const next = this.catalog.migrations?.[current];
|
|
304
|
+
if (next === undefined || hops > this.steps.length) {
|
|
305
|
+
if (!report.dropped.includes(id)) {
|
|
306
|
+
report.dropped.push(id);
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
current = next;
|
|
311
|
+
}
|
|
312
|
+
if (current !== id) {
|
|
313
|
+
report.renamed[id] = current;
|
|
314
|
+
}
|
|
315
|
+
return current;
|
|
316
|
+
};
|
|
317
|
+
this.completed.clear();
|
|
318
|
+
this.skipped.clear();
|
|
319
|
+
state.completedStepIds.forEach((id) => {
|
|
320
|
+
const resolved = resolve(id);
|
|
321
|
+
if (resolved) {
|
|
322
|
+
this.completed.add(resolved);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
state.skippedStepIds.forEach((id) => {
|
|
326
|
+
const resolved = resolve(id);
|
|
327
|
+
if (resolved && !this.completed.has(resolved)) {
|
|
328
|
+
this.skipped.add(resolved);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
this.status = state.status;
|
|
332
|
+
this.currentIndex = -1;
|
|
333
|
+
this.pendingResume = false;
|
|
334
|
+
if (state.status === 'active') {
|
|
335
|
+
const resolved = state.stepId === undefined ? undefined : resolve(state.stepId);
|
|
336
|
+
let index = resolved === undefined ? -1 : this.indexOf(resolved);
|
|
337
|
+
if (index < 0 || this.isDone(index)) {
|
|
338
|
+
index = this.steps.findIndex((s, i) => !this.isDone(i));
|
|
339
|
+
if (index >= 0) {
|
|
340
|
+
report.resumedAt = this.steps[index].step.stepId;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (index < 0) {
|
|
344
|
+
this.status = 'completed';
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
this.currentIndex = index;
|
|
348
|
+
this.pendingResume = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
report.migrated = report.migrated || Object.keys(report.renamed).length > 0 || report.dropped.length > 0
|
|
352
|
+
|| report.resumedAt !== undefined;
|
|
353
|
+
return report;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Enters the restored step after `importState`. Its command is NOT run again by default (it already ran before the
|
|
357
|
+
* save — e.g. placing a building twice would corrupt the game); pass `rerunCommand` for idempotent commands.
|
|
358
|
+
*/
|
|
359
|
+
async resume(context, options = {}) {
|
|
360
|
+
if (!this.pendingResume) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
this.pendingResume = false;
|
|
364
|
+
await this.transition(async () => this.enter(this.currentIndex, context, options.rerunCommand === true, true));
|
|
365
|
+
}
|
|
366
|
+
/** Leaves the active step (completed / skipped / timed out) and enters the next unfinished one — or finishes. */
|
|
367
|
+
async leave(how, context) {
|
|
368
|
+
const from = this.steps[this.currentIndex];
|
|
369
|
+
if (how === 'completed') {
|
|
370
|
+
this.completed.add(from.step.stepId);
|
|
371
|
+
this.emit({ type: 'step-completed', stepId: from.step.stepId, chapterId: from.chapterId });
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
// A timed-out step is recorded as skipped: the player is not sent back into it on resume.
|
|
375
|
+
this.skipped.add(from.step.stepId);
|
|
376
|
+
this.emit({ type: how === 'skipped' ? 'step-skipped' : 'step-timed-out', stepId: from.step.stepId, chapterId: from.chapterId });
|
|
377
|
+
}
|
|
378
|
+
let next = -1;
|
|
379
|
+
for (let i = this.currentIndex + 1; i < this.steps.length; i++) {
|
|
380
|
+
if (!this.isDone(i)) {
|
|
381
|
+
next = i;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (next < 0 || this.steps[next].chapterIndex !== from.chapterIndex) {
|
|
386
|
+
const chapterDone = this.steps
|
|
387
|
+
.filter((s) => s.chapterIndex === from.chapterIndex)
|
|
388
|
+
.every((s) => this.completed.has(s.step.stepId));
|
|
389
|
+
if (chapterDone) {
|
|
390
|
+
this.emit({ type: 'chapter-completed', chapterId: from.chapterId });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (next < 0) {
|
|
394
|
+
this.status = 'completed';
|
|
395
|
+
this.currentIndex = -1;
|
|
396
|
+
this.emit({ type: 'completed' });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
await this.enter(next, context, true, false);
|
|
400
|
+
}
|
|
401
|
+
/** Makes `index` the active step; runs its command (a failing command is reported and the step stays active). */
|
|
402
|
+
async enter(index, context, runCommand, resumed) {
|
|
403
|
+
const entry = this.steps[index];
|
|
404
|
+
this.currentIndex = index;
|
|
405
|
+
this.enteredAtMs = this.now();
|
|
406
|
+
this.emit(resumed
|
|
407
|
+
? { type: 'step-entered', stepId: entry.step.stepId, chapterId: entry.chapterId, resumed: true }
|
|
408
|
+
: { type: 'step-entered', stepId: entry.step.stepId, chapterId: entry.chapterId });
|
|
409
|
+
if (runCommand && entry.step.command) {
|
|
410
|
+
try {
|
|
411
|
+
await this.capabilities.commands[entry.step.command.name](context, entry.step.command.args);
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
this.emit({ type: 'command-failed', stepId: entry.step.stepId, chapterId: entry.chapterId, error: error });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/** Evaluates a predicate; a throwing one becomes a debuggable `DYFM-ONBOARDING-PREDICATE` (step + predicate named). */
|
|
419
|
+
predicate(ref, context, stepId) {
|
|
420
|
+
try {
|
|
421
|
+
return this.capabilities.predicates[ref.name](context, ref.args) === true;
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
throw new DyFM_Error({
|
|
425
|
+
message: `DyFM_GuidedOnboarding: predicate '${ref.name}' of step '${stepId}' threw`,
|
|
426
|
+
errorCode: 'DYFM-ONBOARDING-PREDICATE',
|
|
427
|
+
issuerService: ISSUER,
|
|
428
|
+
error: error,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async transition(run) {
|
|
433
|
+
this.transitioning = true;
|
|
434
|
+
try {
|
|
435
|
+
await run();
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
this.transitioning = false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
activeStep(operation) {
|
|
442
|
+
if (this.status !== 'active' || this.currentIndex < 0) {
|
|
443
|
+
throw new DyFM_Error({
|
|
444
|
+
message: `DyFM_GuidedOnboarding: '${operation}' needs an active onboarding (status: ${this.status})`,
|
|
445
|
+
errorCode: 'DYFM-ONBOARDING-INACTIVE',
|
|
446
|
+
issuerService: ISSUER,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
this.assertResumed();
|
|
450
|
+
return this.steps[this.currentIndex].step;
|
|
451
|
+
}
|
|
452
|
+
assertResumed() {
|
|
453
|
+
if (this.pendingResume) {
|
|
454
|
+
throw new DyFM_Error({
|
|
455
|
+
message: 'DyFM_GuidedOnboarding: the imported onboarding is active but not resumed — call resume(context) first',
|
|
456
|
+
errorCode: 'DYFM-ONBOARDING-NOT-RESUMED',
|
|
457
|
+
issuerService: ISSUER,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
isDone(index) {
|
|
462
|
+
const id = this.steps[index].step.stepId;
|
|
463
|
+
return this.completed.has(id) || this.skipped.has(id);
|
|
464
|
+
}
|
|
465
|
+
indexOf(stepId, required = true) {
|
|
466
|
+
const index = this.steps.findIndex((s) => s.step.stepId === stepId);
|
|
467
|
+
if (index < 0 && required) {
|
|
468
|
+
throw new DyFM_Error({
|
|
469
|
+
message: `DyFM_GuidedOnboarding: unknown step '${stepId}'`,
|
|
470
|
+
errorCode: 'DYFM-ONBOARDING-UNKNOWN-STEP',
|
|
471
|
+
issuerService: ISSUER,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return index;
|
|
475
|
+
}
|
|
476
|
+
emit(event) {
|
|
477
|
+
for (const listener of [...this.listeners]) {
|
|
478
|
+
try {
|
|
479
|
+
listener(event);
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
DyFM_Log.error(`${ISSUER}: an onEvent listener threw on '${event.type}'`, error);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -22,6 +22,8 @@ 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';
|
|
25
27
|
// COLLECTIONS
|
|
26
28
|
export * from './_collections/audio-mixer.util';
|
|
27
29
|
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.103",
|
|
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",
|