@futdevpro/fsm-dynamo 1.20.100 → 1.20.101

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.
@@ -0,0 +1,56 @@
1
+ import { DyFM_InputAction_Definition, DyFM_InputApplyReport, DyFM_InputChord, DyFM_InputConflict, DyFM_InputContext, DyFM_InputFlatMap, DyFM_InputKeyEvent, DyFM_InputResolution } from './input-action.interface';
2
+ /**
3
+ * BFR-WARFACTORY-017 — typed input-action registry: default bindings, per-binding `code` / `key` matching, three
4
+ * orthogonal guards, ONE resolution per event, conflict detection, reset, and a flat `slotId → chord` map for disk.
5
+ *
6
+ * Slot ids are `<actionId>#<index>` (the index in the definition's `bindings`) — stable across remaps; a remap only
7
+ * changes a slot's chord, never its payload.
8
+ */
9
+ export declare class DyFM_InputActionRegistry_ControlModel {
10
+ private readonly actions;
11
+ private readonly slots;
12
+ /** Registers the actions; duplicate ids or an action without bindings throw `DYFM-INPUT-DEFINITION`. */
13
+ constructor(actions: DyFM_InputAction_Definition[]);
14
+ /** The chord string form: `ctrl+shift+code:KeyS` / `key:?` (modifiers in a fixed order). */
15
+ static formatChord(chord: DyFM_InputChord): string;
16
+ /** Parses a chord string; `null` when it is not one. */
17
+ static parseChord(text: string): DyFM_InputChord | null;
18
+ /**
19
+ * Resolves a key event to AT MOST ONE action: the matching slots whose guard is live in `context`, highest
20
+ * `priority` first, then definition order.
21
+ */
22
+ resolve(event: DyFM_InputKeyEvent, context: DyFM_InputContext): DyFM_InputResolution | null;
23
+ /** The current chord of a slot. */
24
+ chordOf(slotId: string): DyFM_InputChord | undefined;
25
+ /** Slots whose chord equals `chord` and whose guards can be live together with `slotId`'s action. */
26
+ findConflicts(slotId: string, chord: DyFM_InputChord): DyFM_InputConflict[];
27
+ /**
28
+ * Remaps a slot. A conflicting chord is REFUSED (the conflicts are returned) unless `force` — then the conflicting
29
+ * slots SWAP: they take this slot's previous chord (resetting them to their defaults would not help when a default
30
+ * is the contested chord itself).
31
+ */
32
+ rebind(slotId: string, chord: DyFM_InputChord, force?: boolean): {
33
+ ok: boolean;
34
+ conflicts: DyFM_InputConflict[];
35
+ };
36
+ /** Resets one slot, one action's slots, or (no argument) everything to the defaults. */
37
+ resetToDefault(slotOrActionId?: string): void;
38
+ /** ③ The flat on-disk form — ONLY the slots that differ from their defaults. */
39
+ toFlatMap(): DyFM_InputFlatMap;
40
+ /**
41
+ * ③ Applies a stored flat map on top of the defaults: unknown slots (an action the game removed) and unparsable
42
+ * chords are skipped and reported; a stored chord that now conflicts is NOT applied (reported) — a stale save
43
+ * cannot silently steal a key.
44
+ */
45
+ applyFlatMap(map: DyFM_InputFlatMap): DyFM_InputApplyReport;
46
+ private action;
47
+ private slot;
48
+ private static validChord;
49
+ /** ① code vs key, and the modifiers must match exactly (an unset modifier must not be held). */
50
+ private static matches;
51
+ /** ② The three predicates. */
52
+ private static guardAllows;
53
+ /** Can two actions be live in the same context? (phases intersect AND overlay conditions can hold together) */
54
+ private static guardsOverlap;
55
+ }
56
+ //# sourceMappingURL=input-action-registry.control-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-action-registry.control-model.d.ts","sourceRoot":"","sources":["../../../../src/_modules/game/_models/input-action-registry.control-model.ts"],"names":[],"mappings":"AACA,OAAO,EACL,2BAA2B,EAE3B,qBAAqB,EACrB,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAElB,oBAAoB,EACrB,MAAM,0BAA0B,CAAC;AAWlC;;;;;;GAMG;AACH,qBAAa,qCAAqC;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IAEtD,wGAAwG;gBAC5F,OAAO,EAAE,2BAA2B,EAAE;IA4BlD,4FAA4F;IAC5F,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM;IAOlD,wDAAwD;IACxD,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI;IAqBvD;;;OAGG;IACH,OAAO,CAAC,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,iBAAiB,GAAG,oBAAoB,GAAG,IAAI;IA4B3F,mCAAmC;IACnC,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAMpD,qGAAqG;IACrG,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,kBAAkB,EAAE;IAiB3E;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,GAAE,OAAe,GAAG;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,kBAAkB,EAAE,CAAA;KAAE;IAwBxH,wFAAwF;IACxF,cAAc,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI;IAS7C,gFAAgF;IAChF,SAAS,IAAI,iBAAiB;IAc9B;;;;OAIG;IACH,YAAY,CAAC,GAAG,EAAE,iBAAiB,GAAG,qBAAqB;IAyB3D,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,MAAM,CAAC,UAAU;IAIzB,gGAAgG;IAChG,OAAO,CAAC,MAAM,CAAC,OAAO;IAQtB,8BAA8B;IAC9B,OAAO,CAAC,MAAM,CAAC,WAAW;IAoB1B,+GAA+G;IAC/G,OAAO,CAAC,MAAM,CAAC,aAAa;CAoB7B"}
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DyFM_InputActionRegistry_ControlModel = void 0;
4
+ const error_control_model_1 = require("../../../_models/control-models/error.control-model");
5
+ /**
6
+ * BFR-WARFACTORY-017 — typed input-action registry: default bindings, per-binding `code` / `key` matching, three
7
+ * orthogonal guards, ONE resolution per event, conflict detection, reset, and a flat `slotId → chord` map for disk.
8
+ *
9
+ * Slot ids are `<actionId>#<index>` (the index in the definition's `bindings`) — stable across remaps; a remap only
10
+ * changes a slot's chord, never its payload.
11
+ */
12
+ class DyFM_InputActionRegistry_ControlModel {
13
+ actions;
14
+ slots = [];
15
+ /** Registers the actions; duplicate ids or an action without bindings throw `DYFM-INPUT-DEFINITION`. */
16
+ constructor(actions) {
17
+ const seen = new Set();
18
+ for (const action of actions) {
19
+ if (!action?.actionId || seen.has(action.actionId) || !action.bindings?.length
20
+ || action.bindings.some((slot) => !DyFM_InputActionRegistry_ControlModel.validChord(slot?.chord))) {
21
+ throw new error_control_model_1.DyFM_Error({
22
+ message: `DyFM_InputActionRegistry: action ids must be unique, with ≥ 1 valid binding (${action?.actionId})`,
23
+ errorCode: 'DYFM-INPUT-DEFINITION',
24
+ issuerService: 'DyFM_InputActionRegistry_ControlModel',
25
+ });
26
+ }
27
+ seen.add(action.actionId);
28
+ }
29
+ this.actions = actions.map((a) => ({ ...a }));
30
+ for (const action of this.actions) {
31
+ action.bindings.forEach((slot, index) => {
32
+ this.slots.push({
33
+ slotId: `${action.actionId}#${index}`,
34
+ actionId: action.actionId,
35
+ defaultChord: { ...slot.chord },
36
+ chord: { ...slot.chord },
37
+ payload: slot.payload,
38
+ });
39
+ });
40
+ }
41
+ }
42
+ /** The chord string form: `ctrl+shift+code:KeyS` / `key:?` (modifiers in a fixed order). */
43
+ static formatChord(chord) {
44
+ const modifiers = ['ctrl', 'alt', 'shift', 'meta']
45
+ .filter((m) => chord[m] === true);
46
+ return [...modifiers, `${chord.match}:${chord.value}`].join('+');
47
+ }
48
+ /** Parses a chord string; `null` when it is not one. */
49
+ static parseChord(text) {
50
+ if (typeof text !== 'string' || !text) {
51
+ return null;
52
+ }
53
+ const parts = text.split('+');
54
+ const last = parts.pop();
55
+ const colon = last.indexOf(':');
56
+ const match = last.slice(0, colon);
57
+ const chord = { match: match, value: last.slice(colon + 1) };
58
+ for (const modifier of parts) {
59
+ if (!['ctrl', 'alt', 'shift', 'meta'].includes(modifier)) {
60
+ return null;
61
+ }
62
+ chord[modifier] = true;
63
+ }
64
+ return colon > 0 && DyFM_InputActionRegistry_ControlModel.validChord(chord) ? chord : null;
65
+ }
66
+ /**
67
+ * Resolves a key event to AT MOST ONE action: the matching slots whose guard is live in `context`, highest
68
+ * `priority` first, then definition order.
69
+ */
70
+ resolve(event, context) {
71
+ let best = null;
72
+ this.slots.forEach((slot) => {
73
+ const action = this.action(slot.actionId);
74
+ if (!DyFM_InputActionRegistry_ControlModel.matches(slot.chord, event)
75
+ || !DyFM_InputActionRegistry_ControlModel.guardAllows(action.guard, context)) {
76
+ return;
77
+ }
78
+ const priority = action.priority ?? 0;
79
+ const order = this.actions.indexOf(action);
80
+ if (!best || priority > best.priority || (priority === best.priority && order < best.order)) {
81
+ best = { slot: slot, priority: priority, order: order };
82
+ }
83
+ });
84
+ if (!best) {
85
+ return null;
86
+ }
87
+ const chosen = best.slot;
88
+ return { actionId: chosen.actionId, slotId: chosen.slotId, payload: chosen.payload };
89
+ }
90
+ /** The current chord of a slot. */
91
+ chordOf(slotId) {
92
+ const slot = this.slot(slotId);
93
+ return slot ? { ...slot.chord } : undefined;
94
+ }
95
+ /** Slots whose chord equals `chord` and whose guards can be live together with `slotId`'s action. */
96
+ findConflicts(slotId, chord) {
97
+ const own = this.slot(slotId);
98
+ if (!own) {
99
+ return [];
100
+ }
101
+ const text = DyFM_InputActionRegistry_ControlModel.formatChord(chord);
102
+ const ownGuard = this.action(own.actionId).guard;
103
+ return this.slots
104
+ .filter((other) => other.slotId !== slotId
105
+ && DyFM_InputActionRegistry_ControlModel.formatChord(other.chord) === text
106
+ && DyFM_InputActionRegistry_ControlModel.guardsOverlap(ownGuard, this.action(other.actionId).guard))
107
+ .map((other) => ({ slotId: slotId, otherSlotId: other.slotId, chord: text }));
108
+ }
109
+ /**
110
+ * Remaps a slot. A conflicting chord is REFUSED (the conflicts are returned) unless `force` — then the conflicting
111
+ * slots SWAP: they take this slot's previous chord (resetting them to their defaults would not help when a default
112
+ * is the contested chord itself).
113
+ */
114
+ rebind(slotId, chord, force = false) {
115
+ const slot = this.slot(slotId);
116
+ if (!slot || !DyFM_InputActionRegistry_ControlModel.validChord(chord)) {
117
+ return { ok: false, conflicts: [] };
118
+ }
119
+ const conflicts = this.findConflicts(slotId, chord);
120
+ if (conflicts.length && !force) {
121
+ return { ok: false, conflicts: conflicts };
122
+ }
123
+ const previous = { ...slot.chord };
124
+ for (const conflict of conflicts) {
125
+ const other = this.slot(conflict.otherSlotId);
126
+ other.chord = { ...previous };
127
+ }
128
+ slot.chord = { ...chord };
129
+ return { ok: true, conflicts: conflicts };
130
+ }
131
+ /** Resets one slot, one action's slots, or (no argument) everything to the defaults. */
132
+ resetToDefault(slotOrActionId) {
133
+ this.slots
134
+ .filter((slot) => !slotOrActionId || slot.slotId === slotOrActionId || slot.actionId === slotOrActionId)
135
+ .forEach((slot) => {
136
+ slot.chord = { ...slot.defaultChord };
137
+ });
138
+ }
139
+ /** ③ The flat on-disk form — ONLY the slots that differ from their defaults. */
140
+ toFlatMap() {
141
+ const map = {};
142
+ for (const slot of this.slots) {
143
+ const current = DyFM_InputActionRegistry_ControlModel.formatChord(slot.chord);
144
+ if (current !== DyFM_InputActionRegistry_ControlModel.formatChord(slot.defaultChord)) {
145
+ map[slot.slotId] = current;
146
+ }
147
+ }
148
+ return map;
149
+ }
150
+ /**
151
+ * ③ Applies a stored flat map on top of the defaults: unknown slots (an action the game removed) and unparsable
152
+ * chords are skipped and reported; a stored chord that now conflicts is NOT applied (reported) — a stale save
153
+ * cannot silently steal a key.
154
+ */
155
+ applyFlatMap(map) {
156
+ const report = { applied: [], unknownSlots: [], invalidChords: [], conflicts: [] };
157
+ this.resetToDefault();
158
+ for (const [slotId, text] of Object.entries(map ?? {})) {
159
+ const chord = DyFM_InputActionRegistry_ControlModel.parseChord(text);
160
+ if (!this.slot(slotId)) {
161
+ report.unknownSlots.push(slotId);
162
+ }
163
+ else if (!chord) {
164
+ report.invalidChords.push(slotId);
165
+ }
166
+ else {
167
+ const result = this.rebind(slotId, chord);
168
+ if (result.ok) {
169
+ report.applied.push(slotId);
170
+ }
171
+ else {
172
+ report.conflicts.push(...result.conflicts);
173
+ }
174
+ }
175
+ }
176
+ return report;
177
+ }
178
+ action(actionId) {
179
+ return this.actions.find((a) => a.actionId === actionId);
180
+ }
181
+ slot(slotId) {
182
+ return this.slots.find((s) => s.slotId === slotId);
183
+ }
184
+ static validChord(chord) {
185
+ return !!chord && (chord.match === 'code' || chord.match === 'key') && typeof chord.value === 'string' && chord.value.length > 0;
186
+ }
187
+ /** ① code vs key, and the modifiers must match exactly (an unset modifier must not be held). */
188
+ static matches(chord, event) {
189
+ const value = chord.match === 'code' ? event.code : event.key;
190
+ return value === chord.value
191
+ && !!chord.ctrl === !!event.ctrlKey && !!chord.alt === !!event.altKey
192
+ && !!chord.shift === !!event.shiftKey && !!chord.meta === !!event.metaKey;
193
+ }
194
+ /** ② The three predicates. */
195
+ static guardAllows(guard, context) {
196
+ if (context.interactiveTarget && !guard?.allowInInteractiveTarget) {
197
+ return false;
198
+ }
199
+ const overlays = guard?.overlays ?? 'none';
200
+ if (overlays === 'none' && context.openOverlay !== null) {
201
+ return false;
202
+ }
203
+ if (overlays === 'open' && context.openOverlay === null) {
204
+ return false;
205
+ }
206
+ if (Array.isArray(overlays) && (context.openOverlay === null || !overlays.includes(context.openOverlay))) {
207
+ return false;
208
+ }
209
+ return !guard?.phases || guard.phases.includes(context.phase);
210
+ }
211
+ /** Can two actions be live in the same context? (phases intersect AND overlay conditions can hold together) */
212
+ static guardsOverlap(a, b) {
213
+ const phasesOverlap = !a?.phases || !b?.phases || a.phases.some((p) => b.phases.includes(p));
214
+ const overlayA = a?.overlays ?? 'none';
215
+ const overlayB = b?.overlays ?? 'none';
216
+ let overlaysOverlap;
217
+ if (overlayA === 'any' || overlayB === 'any') {
218
+ overlaysOverlap = true;
219
+ }
220
+ else if (overlayA === 'none' || overlayB === 'none') {
221
+ // 'none' only overlaps 'none' (an open overlay excludes it)
222
+ overlaysOverlap = overlayA === overlayB;
223
+ }
224
+ else if (overlayA === 'open' || overlayB === 'open') {
225
+ // 'open' overlaps 'open' and any overlay list (both need an overlay on top)
226
+ overlaysOverlap = true;
227
+ }
228
+ else {
229
+ overlaysOverlap = overlayA.some((o) => overlayB.includes(o));
230
+ }
231
+ return phasesOverlap && overlaysOverlap;
232
+ }
233
+ }
234
+ exports.DyFM_InputActionRegistry_ControlModel = DyFM_InputActionRegistry_ControlModel;
235
+ //# sourceMappingURL=input-action-registry.control-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-action-registry.control-model.js","sourceRoot":"","sources":["../../../../src/_modules/game/_models/input-action-registry.control-model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * BFR-WARFACTORY-017 — game input actions. The common layer owns the MECHANICS (bindings, guards, conflicts, flat
3
+ * persistence); the action ids and labels are the game's.
4
+ */
5
+ /** ① What a binding matches on: the PHYSICAL key position (`code`, e.g. `KeyW` — layout-independent) or the produced
6
+ * character (`key`, e.g. `?`). Declared per binding. */
7
+ export type DyFM_InputMatch_Type = 'code' | 'key';
8
+ /** One physical input: the matched value + the required modifiers (unset = must NOT be held). */
9
+ export interface DyFM_InputChord {
10
+ match: DyFM_InputMatch_Type;
11
+ value: string;
12
+ ctrl?: boolean;
13
+ alt?: boolean;
14
+ shift?: boolean;
15
+ meta?: boolean;
16
+ }
17
+ /** ④ A binding slot of an action: the default chord + an optional PAYLOAD (e.g. speed hotkeys: `Digit3` → 3). */
18
+ export interface DyFM_InputBindingSlot {
19
+ chord: DyFM_InputChord;
20
+ payload?: unknown;
21
+ }
22
+ /** ② The overlay condition of a guard. */
23
+ export type DyFM_InputOverlayCondition_Type = 'none' | 'open' | 'any' | string[];
24
+ /** ② When an action may fire — three ORTHOGONAL predicates instead of one `enabled` flag. */
25
+ export interface DyFM_InputActionGuard {
26
+ /** Fire while an interactive element (input / textarea / button …) has focus? Default: `false` (typing wins). */
27
+ allowInInteractiveTarget?: boolean;
28
+ /**
29
+ * Overlay condition: `'none'` (no overlay open — default) · `'open'` (SOME overlay is open — e.g. close-overlay) ·
30
+ * `'any'` (open or not) · a list of overlay ids that must be on top.
31
+ */
32
+ overlays?: DyFM_InputOverlayCondition_Type;
33
+ /** Game phases the action is live in; unset = every phase. */
34
+ phases?: string[];
35
+ }
36
+ /** One action definition. */
37
+ export interface DyFM_InputAction_Definition {
38
+ actionId: string;
39
+ label: string;
40
+ bindings: DyFM_InputBindingSlot[];
41
+ guard?: DyFM_InputActionGuard;
42
+ /** Higher wins when several actions match one event (tie → definition order). Default: `0`. */
43
+ priority?: number;
44
+ }
45
+ /** The runtime context a key event is resolved in. */
46
+ export interface DyFM_InputContext {
47
+ /** An interactive element has focus. */
48
+ interactiveTarget: boolean;
49
+ /** The TOP open overlay id (`null` = none). */
50
+ openOverlay: string | null;
51
+ /** The current game phase. */
52
+ phase: string;
53
+ }
54
+ /** The subset of a `KeyboardEvent` the registry reads. */
55
+ export interface DyFM_InputKeyEvent {
56
+ code: string;
57
+ key: string;
58
+ ctrlKey?: boolean;
59
+ altKey?: boolean;
60
+ shiftKey?: boolean;
61
+ metaKey?: boolean;
62
+ }
63
+ /** A resolved action — ONE per event (Escape never closes two layers). */
64
+ export interface DyFM_InputResolution {
65
+ actionId: string;
66
+ slotId: string;
67
+ payload?: unknown;
68
+ }
69
+ /** A binding conflict: another slot has the same chord and their guards can be live at the same time. */
70
+ export interface DyFM_InputConflict {
71
+ slotId: string;
72
+ otherSlotId: string;
73
+ chord: string;
74
+ }
75
+ /** ③ The flat, on-disk form: `slotId → chord string` (only the remapped chords; payloads stay in the definitions). */
76
+ export type DyFM_InputFlatMap = Record<string, string>;
77
+ /** What `applyFlatMap` did. */
78
+ export interface DyFM_InputApplyReport {
79
+ applied: string[];
80
+ unknownSlots: string[];
81
+ invalidChords: string[];
82
+ conflicts: DyFM_InputConflict[];
83
+ }
84
+ //# sourceMappingURL=input-action.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-action.interface.d.ts","sourceRoot":"","sources":["../../../../src/_modules/game/_models/input-action.interface.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;wDACwD;AACxD,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,KAAK,CAAC;AAElD,iGAAiG;AACjG,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,oBAAoB,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,iHAAiH;AACjH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,0CAA0C;AAC1C,MAAM,MAAM,+BAA+B,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,EAAE,CAAC;AAEjF,6FAA6F;AAC7F,MAAM,WAAW,qBAAqB;IACpC,iHAAiH;IACjH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,EAAE,+BAA+B,CAAC;IAC3C,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,6BAA6B;AAC7B,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,qBAAqB,EAAE,CAAC;IAClC,KAAK,CAAC,EAAE,qBAAqB,CAAC;IAC9B,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,sDAAsD;AACtD,MAAM,WAAW,iBAAiB;IAChC,wCAAwC;IACxC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,+CAA+C;IAC/C,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,yGAAyG;AACzG,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,sHAAsH;AACtH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEvD,+BAA+B;AAC/B,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,EAAE,kBAAkB,EAAE,CAAC;CACjC"}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /**
3
+ * BFR-WARFACTORY-017 — game input actions. The common layer owns the MECHANICS (bindings, guards, conflicts, flat
4
+ * persistence); the action ids and labels are the game's.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ //# sourceMappingURL=input-action.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-action.interface.js","sourceRoot":"","sources":["../../../../src/_modules/game/_models/input-action.interface.ts"],"names":[],"mappings":";AAAA;;;GAGG"}
@@ -18,6 +18,8 @@ export * from './_models/work-order-scheduler.control-model';
18
18
  export * from './_models/build-reservation.interface';
19
19
  export * from './_models/viewport.interface';
20
20
  export * from './_models/viewport.control-model';
21
+ export * from './_models/input-action.interface';
22
+ export * from './_models/input-action-registry.control-model';
21
23
  export * from './_collections/audio-mixer.util';
22
24
  export * from './_collections/audio-scale.util';
23
25
  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;AAGjD,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uCAAuC,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;AAG9D,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uCAAuC,CAAC"}
@@ -23,6 +23,8 @@ tslib_1.__exportStar(require("./_models/work-order-scheduler.control-model"), ex
23
23
  tslib_1.__exportStar(require("./_models/build-reservation.interface"), exports);
24
24
  tslib_1.__exportStar(require("./_models/viewport.interface"), exports);
25
25
  tslib_1.__exportStar(require("./_models/viewport.control-model"), exports);
26
+ tslib_1.__exportStar(require("./_models/input-action.interface"), exports);
27
+ tslib_1.__exportStar(require("./_models/input-action-registry.control-model"), exports);
26
28
  // COLLECTIONS
27
29
  tslib_1.__exportStar(require("./_collections/audio-mixer.util"), exports);
28
30
  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;AAEjD,cAAc;AACd,0EAAgD;AAChD,0EAAgD;AAChD,wEAA8C;AAC9C,gFAAsD"}
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;AAE9D,cAAc;AACd,0EAAgD;AAChD,0EAAgD;AAChD,wEAA8C;AAC9C,gFAAsD"}
@@ -0,0 +1,230 @@
1
+ import { DyFM_Error } from '../../../_models/control-models/error.control-model';
2
+ /**
3
+ * BFR-WARFACTORY-017 — typed input-action registry: default bindings, per-binding `code` / `key` matching, three
4
+ * orthogonal guards, ONE resolution per event, conflict detection, reset, and a flat `slotId → chord` map for disk.
5
+ *
6
+ * Slot ids are `<actionId>#<index>` (the index in the definition's `bindings`) — stable across remaps; a remap only
7
+ * changes a slot's chord, never its payload.
8
+ */
9
+ export class DyFM_InputActionRegistry_ControlModel {
10
+ actions;
11
+ slots = [];
12
+ /** Registers the actions; duplicate ids or an action without bindings throw `DYFM-INPUT-DEFINITION`. */
13
+ constructor(actions) {
14
+ const seen = new Set();
15
+ for (const action of actions) {
16
+ if (!action?.actionId || seen.has(action.actionId) || !action.bindings?.length
17
+ || action.bindings.some((slot) => !DyFM_InputActionRegistry_ControlModel.validChord(slot?.chord))) {
18
+ throw new DyFM_Error({
19
+ message: `DyFM_InputActionRegistry: action ids must be unique, with ≥ 1 valid binding (${action?.actionId})`,
20
+ errorCode: 'DYFM-INPUT-DEFINITION',
21
+ issuerService: 'DyFM_InputActionRegistry_ControlModel',
22
+ });
23
+ }
24
+ seen.add(action.actionId);
25
+ }
26
+ this.actions = actions.map((a) => ({ ...a }));
27
+ for (const action of this.actions) {
28
+ action.bindings.forEach((slot, index) => {
29
+ this.slots.push({
30
+ slotId: `${action.actionId}#${index}`,
31
+ actionId: action.actionId,
32
+ defaultChord: { ...slot.chord },
33
+ chord: { ...slot.chord },
34
+ payload: slot.payload,
35
+ });
36
+ });
37
+ }
38
+ }
39
+ /** The chord string form: `ctrl+shift+code:KeyS` / `key:?` (modifiers in a fixed order). */
40
+ static formatChord(chord) {
41
+ const modifiers = ['ctrl', 'alt', 'shift', 'meta']
42
+ .filter((m) => chord[m] === true);
43
+ return [...modifiers, `${chord.match}:${chord.value}`].join('+');
44
+ }
45
+ /** Parses a chord string; `null` when it is not one. */
46
+ static parseChord(text) {
47
+ if (typeof text !== 'string' || !text) {
48
+ return null;
49
+ }
50
+ const parts = text.split('+');
51
+ const last = parts.pop();
52
+ const colon = last.indexOf(':');
53
+ const match = last.slice(0, colon);
54
+ const chord = { match: match, value: last.slice(colon + 1) };
55
+ for (const modifier of parts) {
56
+ if (!['ctrl', 'alt', 'shift', 'meta'].includes(modifier)) {
57
+ return null;
58
+ }
59
+ chord[modifier] = true;
60
+ }
61
+ return colon > 0 && DyFM_InputActionRegistry_ControlModel.validChord(chord) ? chord : null;
62
+ }
63
+ /**
64
+ * Resolves a key event to AT MOST ONE action: the matching slots whose guard is live in `context`, highest
65
+ * `priority` first, then definition order.
66
+ */
67
+ resolve(event, context) {
68
+ let best = null;
69
+ this.slots.forEach((slot) => {
70
+ const action = this.action(slot.actionId);
71
+ if (!DyFM_InputActionRegistry_ControlModel.matches(slot.chord, event)
72
+ || !DyFM_InputActionRegistry_ControlModel.guardAllows(action.guard, context)) {
73
+ return;
74
+ }
75
+ const priority = action.priority ?? 0;
76
+ const order = this.actions.indexOf(action);
77
+ if (!best || priority > best.priority || (priority === best.priority && order < best.order)) {
78
+ best = { slot: slot, priority: priority, order: order };
79
+ }
80
+ });
81
+ if (!best) {
82
+ return null;
83
+ }
84
+ const chosen = best.slot;
85
+ return { actionId: chosen.actionId, slotId: chosen.slotId, payload: chosen.payload };
86
+ }
87
+ /** The current chord of a slot. */
88
+ chordOf(slotId) {
89
+ const slot = this.slot(slotId);
90
+ return slot ? { ...slot.chord } : undefined;
91
+ }
92
+ /** Slots whose chord equals `chord` and whose guards can be live together with `slotId`'s action. */
93
+ findConflicts(slotId, chord) {
94
+ const own = this.slot(slotId);
95
+ if (!own) {
96
+ return [];
97
+ }
98
+ const text = DyFM_InputActionRegistry_ControlModel.formatChord(chord);
99
+ const ownGuard = this.action(own.actionId).guard;
100
+ return this.slots
101
+ .filter((other) => other.slotId !== slotId
102
+ && DyFM_InputActionRegistry_ControlModel.formatChord(other.chord) === text
103
+ && DyFM_InputActionRegistry_ControlModel.guardsOverlap(ownGuard, this.action(other.actionId).guard))
104
+ .map((other) => ({ slotId: slotId, otherSlotId: other.slotId, chord: text }));
105
+ }
106
+ /**
107
+ * Remaps a slot. A conflicting chord is REFUSED (the conflicts are returned) unless `force` — then the conflicting
108
+ * slots SWAP: they take this slot's previous chord (resetting them to their defaults would not help when a default
109
+ * is the contested chord itself).
110
+ */
111
+ rebind(slotId, chord, force = false) {
112
+ const slot = this.slot(slotId);
113
+ if (!slot || !DyFM_InputActionRegistry_ControlModel.validChord(chord)) {
114
+ return { ok: false, conflicts: [] };
115
+ }
116
+ const conflicts = this.findConflicts(slotId, chord);
117
+ if (conflicts.length && !force) {
118
+ return { ok: false, conflicts: conflicts };
119
+ }
120
+ const previous = { ...slot.chord };
121
+ for (const conflict of conflicts) {
122
+ const other = this.slot(conflict.otherSlotId);
123
+ other.chord = { ...previous };
124
+ }
125
+ slot.chord = { ...chord };
126
+ return { ok: true, conflicts: conflicts };
127
+ }
128
+ /** Resets one slot, one action's slots, or (no argument) everything to the defaults. */
129
+ resetToDefault(slotOrActionId) {
130
+ this.slots
131
+ .filter((slot) => !slotOrActionId || slot.slotId === slotOrActionId || slot.actionId === slotOrActionId)
132
+ .forEach((slot) => {
133
+ slot.chord = { ...slot.defaultChord };
134
+ });
135
+ }
136
+ /** ③ The flat on-disk form — ONLY the slots that differ from their defaults. */
137
+ toFlatMap() {
138
+ const map = {};
139
+ for (const slot of this.slots) {
140
+ const current = DyFM_InputActionRegistry_ControlModel.formatChord(slot.chord);
141
+ if (current !== DyFM_InputActionRegistry_ControlModel.formatChord(slot.defaultChord)) {
142
+ map[slot.slotId] = current;
143
+ }
144
+ }
145
+ return map;
146
+ }
147
+ /**
148
+ * ③ Applies a stored flat map on top of the defaults: unknown slots (an action the game removed) and unparsable
149
+ * chords are skipped and reported; a stored chord that now conflicts is NOT applied (reported) — a stale save
150
+ * cannot silently steal a key.
151
+ */
152
+ applyFlatMap(map) {
153
+ const report = { applied: [], unknownSlots: [], invalidChords: [], conflicts: [] };
154
+ this.resetToDefault();
155
+ for (const [slotId, text] of Object.entries(map ?? {})) {
156
+ const chord = DyFM_InputActionRegistry_ControlModel.parseChord(text);
157
+ if (!this.slot(slotId)) {
158
+ report.unknownSlots.push(slotId);
159
+ }
160
+ else if (!chord) {
161
+ report.invalidChords.push(slotId);
162
+ }
163
+ else {
164
+ const result = this.rebind(slotId, chord);
165
+ if (result.ok) {
166
+ report.applied.push(slotId);
167
+ }
168
+ else {
169
+ report.conflicts.push(...result.conflicts);
170
+ }
171
+ }
172
+ }
173
+ return report;
174
+ }
175
+ action(actionId) {
176
+ return this.actions.find((a) => a.actionId === actionId);
177
+ }
178
+ slot(slotId) {
179
+ return this.slots.find((s) => s.slotId === slotId);
180
+ }
181
+ static validChord(chord) {
182
+ return !!chord && (chord.match === 'code' || chord.match === 'key') && typeof chord.value === 'string' && chord.value.length > 0;
183
+ }
184
+ /** ① code vs key, and the modifiers must match exactly (an unset modifier must not be held). */
185
+ static matches(chord, event) {
186
+ const value = chord.match === 'code' ? event.code : event.key;
187
+ return value === chord.value
188
+ && !!chord.ctrl === !!event.ctrlKey && !!chord.alt === !!event.altKey
189
+ && !!chord.shift === !!event.shiftKey && !!chord.meta === !!event.metaKey;
190
+ }
191
+ /** ② The three predicates. */
192
+ static guardAllows(guard, context) {
193
+ if (context.interactiveTarget && !guard?.allowInInteractiveTarget) {
194
+ return false;
195
+ }
196
+ const overlays = guard?.overlays ?? 'none';
197
+ if (overlays === 'none' && context.openOverlay !== null) {
198
+ return false;
199
+ }
200
+ if (overlays === 'open' && context.openOverlay === null) {
201
+ return false;
202
+ }
203
+ if (Array.isArray(overlays) && (context.openOverlay === null || !overlays.includes(context.openOverlay))) {
204
+ return false;
205
+ }
206
+ return !guard?.phases || guard.phases.includes(context.phase);
207
+ }
208
+ /** Can two actions be live in the same context? (phases intersect AND overlay conditions can hold together) */
209
+ static guardsOverlap(a, b) {
210
+ const phasesOverlap = !a?.phases || !b?.phases || a.phases.some((p) => b.phases.includes(p));
211
+ const overlayA = a?.overlays ?? 'none';
212
+ const overlayB = b?.overlays ?? 'none';
213
+ let overlaysOverlap;
214
+ if (overlayA === 'any' || overlayB === 'any') {
215
+ overlaysOverlap = true;
216
+ }
217
+ else if (overlayA === 'none' || overlayB === 'none') {
218
+ // 'none' only overlaps 'none' (an open overlay excludes it)
219
+ overlaysOverlap = overlayA === overlayB;
220
+ }
221
+ else if (overlayA === 'open' || overlayB === 'open') {
222
+ // 'open' overlaps 'open' and any overlay list (both need an overlay on top)
223
+ overlaysOverlap = true;
224
+ }
225
+ else {
226
+ overlaysOverlap = overlayA.some((o) => overlayB.includes(o));
227
+ }
228
+ return phasesOverlap && overlaysOverlap;
229
+ }
230
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * BFR-WARFACTORY-017 — game input actions. The common layer owns the MECHANICS (bindings, guards, conflicts, flat
3
+ * persistence); the action ids and labels are the game's.
4
+ */
5
+ export {};
@@ -20,6 +20,8 @@ export * from './_models/work-order-scheduler.control-model';
20
20
  export * from './_models/build-reservation.interface';
21
21
  export * from './_models/viewport.interface';
22
22
  export * from './_models/viewport.control-model';
23
+ export * from './_models/input-action.interface';
24
+ export * from './_models/input-action-registry.control-model';
23
25
  // COLLECTIONS
24
26
  export * from './_collections/audio-mixer.util';
25
27
  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.100",
3
+ "version": "1.20.101",
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",