@jgengine/core 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +500 -1
- package/dist/ai/difficulty.d.ts +154 -0
- package/dist/ai/difficulty.js +194 -0
- package/dist/ai/driver.d.ts +107 -0
- package/dist/ai/driver.js +175 -0
- package/dist/ai/flock.js +13 -23
- package/dist/ai/laneSelect.js +2 -1
- package/dist/ai/mobBrain.js +2 -1
- package/dist/ai/waveRunner.d.ts +81 -0
- package/dist/ai/waveRunner.js +104 -0
- package/dist/audio/musicState.d.ts +47 -0
- package/dist/audio/musicState.js +38 -0
- package/dist/audio/stepCadence.d.ts +47 -0
- package/dist/audio/stepCadence.js +29 -0
- package/dist/audio/synth.d.ts +12 -0
- package/dist/authoring.d.ts +6 -4
- package/dist/authoring.js +6 -4
- package/dist/combat/castRunner.d.ts +13 -1
- package/dist/combat/castRunner.js +5 -1
- package/dist/combat/comboMeter.d.ts +140 -0
- package/dist/combat/comboMeter.js +215 -0
- package/dist/combat/comboPoints.d.ts +11 -0
- package/dist/combat/comboPoints.js +11 -2
- package/dist/combat/damageResolution.d.ts +1 -1
- package/dist/combat/defensiveWindow.d.ts +5 -0
- package/dist/combat/effects.d.ts +24 -0
- package/dist/combat/effects.js +6 -1
- package/dist/combat/projectiles.d.ts +6 -0
- package/dist/combat/projectiles.js +2 -2
- package/dist/combat/regenShield.d.ts +40 -2
- package/dist/combat/regenShield.js +66 -18
- package/dist/combat/resourcePool.d.ts +19 -0
- package/dist/combat/resourcePool.js +20 -2
- package/dist/combat/shotOrigin.d.ts +5 -0
- package/dist/combat/statusApplication.d.ts +1 -1
- package/dist/combat/statusApplication.js +2 -1
- package/dist/combat/statusEffectView.d.ts +56 -0
- package/dist/combat/statusEffectView.js +52 -0
- package/dist/combat.d.ts +4 -2
- package/dist/combat.js +2 -0
- package/dist/devtools/fallbackSeams.d.ts +3 -2
- package/dist/devtools/fallbackSeams.js +1 -1
- package/dist/devtools/textureErrors.d.ts +40 -0
- package/dist/devtools/textureErrors.js +68 -0
- package/dist/economy/shopStock.d.ts +110 -0
- package/dist/economy/shopStock.js +157 -0
- package/dist/economy/wallet.d.ts +6 -0
- package/dist/economy/wallet.js +1 -0
- package/dist/game/achievements.d.ts +80 -0
- package/dist/game/achievements.js +124 -0
- package/dist/game/cameraConfig.d.ts +7 -0
- package/dist/game/codex.d.ts +60 -0
- package/dist/game/codex.js +89 -0
- package/dist/game/credits.d.ts +36 -0
- package/dist/game/credits.js +0 -0
- package/dist/game/defineGame.d.ts +28 -3
- package/dist/game/defineGame.js +5 -0
- package/dist/game/dialogueGraph.d.ts +116 -0
- package/dist/game/dialogueGraph.js +103 -0
- package/dist/game/eventTicker.d.ts +95 -0
- package/dist/game/eventTicker.js +91 -0
- package/dist/game/events.d.ts +24 -5
- package/dist/game/events.js +18 -0
- package/dist/game/gamePhase.d.ts +1 -1
- package/dist/game/gamePhase.js +1 -1
- package/dist/game/leaderboardRank.d.ts +71 -0
- package/dist/game/leaderboardRank.js +76 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +4 -2
- package/dist/game/notifications.d.ts +57 -0
- package/dist/game/notifications.js +107 -0
- package/dist/game/playableGame.d.ts +39 -2
- package/dist/game/quest.d.ts +26 -0
- package/dist/game/quest.js +31 -0
- package/dist/game/runDraft.js +2 -1
- package/dist/game/saveSlots.d.ts +102 -0
- package/dist/game/saveSlots.js +136 -0
- package/dist/game/talentTreeView.d.ts +106 -0
- package/dist/game/talentTreeView.js +114 -0
- package/dist/game/worldItem.js +2 -1
- package/dist/gameplay.d.ts +15 -4
- package/dist/gameplay.js +13 -2
- package/dist/i18n/i18n.d.ts +51 -0
- package/dist/i18n/i18n.js +82 -0
- package/dist/input/axisInput.d.ts +1 -1
- package/dist/input/axisInput.js +18 -2
- package/dist/input/rebindSession.d.ts +141 -0
- package/dist/input/rebindSession.js +235 -0
- package/dist/input/touchControlsMode.d.ts +16 -0
- package/dist/input/touchControlsMode.js +23 -0
- package/dist/input/touchScheme.d.ts +30 -1
- package/dist/input/touchScheme.js +31 -1
- package/dist/interaction/promptRegistry.d.ts +78 -0
- package/dist/interaction/promptRegistry.js +130 -0
- package/dist/inventory/storageTier.js +3 -2
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +145 -1
- package/dist/movement/movementModel.d.ts +26 -3
- package/dist/movement/movementModel.js +121 -15
- package/dist/movement/playerMovement.js +161 -25
- package/dist/physics/drivableVehicle.js +3 -1
- package/dist/physics/kinematicVehicle.d.ts +56 -1
- package/dist/physics/kinematicVehicle.js +99 -10
- package/dist/physics/vehicleObstacles.js +19 -0
- package/dist/random/resolveRng.d.ts +14 -0
- package/dist/random/resolveRng.js +31 -0
- package/dist/render/postProcessing.d.ts +12 -1
- package/dist/runtime/cameraDirector.d.ts +2 -2
- package/dist/runtime/context/combat.js +8 -0
- package/dist/runtime/gameContext.js +6 -1
- package/dist/runtime/gameContextTypes.d.ts +18 -0
- package/dist/runtime/inputSnapshot.d.ts +10 -1
- package/dist/runtime/inputSnapshot.js +13 -1
- package/dist/runtime/runtimeSave.d.ts +3 -3
- package/dist/runtime/runtimeSave.js +11 -2
- package/dist/scene/assetDiagnostics.js +2 -2
- package/dist/scene/autoTarget.js +2 -1
- package/dist/scene/captureCheck.js +3 -2
- package/dist/scene/entityStore.d.ts +4 -1
- package/dist/scene/entityStore.js +3 -0
- package/dist/scene/objectStore.d.ts +10 -0
- package/dist/scene/objectStore.js +29 -6
- package/dist/scene/sceneKinds.d.ts +18 -0
- package/dist/scene/sceneKinds.js +19 -0
- package/dist/scene/sequenceDirector.d.ts +110 -0
- package/dist/scene/sequenceDirector.js +146 -0
- package/dist/session/roles.js +3 -2
- package/dist/settings/settingsModel.d.ts +1 -0
- package/dist/settings/settingsModel.js +1 -0
- package/dist/stats/rollCheck.js +4 -2
- package/dist/time/dayNightCycle.d.ts +114 -0
- package/dist/time/dayNightCycle.js +208 -0
- package/dist/time/timerSet.d.ts +132 -0
- package/dist/time/timerSet.js +243 -0
- package/dist/ui/accessibility.d.ts +51 -0
- package/dist/ui/accessibility.js +83 -0
- package/dist/ui/coachMarks.d.ts +99 -0
- package/dist/ui/coachMarks.js +126 -0
- package/dist/ui/floatingText.d.ts +114 -0
- package/dist/ui/floatingText.js +203 -0
- package/dist/ui/modalStack.d.ts +143 -0
- package/dist/ui/modalStack.js +160 -0
- package/dist/ui/objectiveBanner.d.ts +123 -0
- package/dist/ui/objectiveBanner.js +162 -0
- package/dist/ui/photoMode.d.ts +32 -0
- package/dist/ui/photoMode.js +51 -0
- package/dist/ui/radialMenu.d.ts +47 -0
- package/dist/ui/radialMenu.js +74 -0
- package/dist/ui/screenMarkers.d.ts +128 -0
- package/dist/ui/screenMarkers.js +118 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +8 -0
- package/dist/vfx/cameraShake.d.ts +97 -0
- package/dist/vfx/cameraShake.js +136 -0
- package/dist/vfx/damageDirection.d.ts +102 -0
- package/dist/vfx/damageDirection.js +188 -0
- package/dist/vfx/particles.d.ts +103 -0
- package/dist/vfx/particles.js +256 -0
- package/dist/vfx/screenEffects.d.ts +149 -0
- package/dist/vfx/screenEffects.js +191 -0
- package/dist/world/authoredObjects.d.ts +18 -0
- package/dist/world/authoredObjects.js +21 -0
- package/dist/world/authoredSpawn.js +19 -0
- package/dist/world/buildingLots.d.ts +54 -11
- package/dist/world/buildingLots.js +116 -30
- package/dist/world/cityBlocks.d.ts +45 -0
- package/dist/world/cityBlocks.js +159 -0
- package/dist/world/cityContent.d.ts +46 -2
- package/dist/world/cityContent.js +160 -2
- package/dist/world/cityGenerator.d.ts +261 -23
- package/dist/world/cityGenerator.js +643 -33
- package/dist/world/cityGeometry.d.ts +20 -0
- package/dist/world/cityGeometry.js +91 -0
- package/dist/world/cityKind.js +1 -44
- package/dist/world/fastTravel.d.ts +64 -0
- package/dist/world/fastTravel.js +108 -0
- package/dist/world/features.d.ts +16 -0
- package/dist/world/features.js +2 -0
- package/dist/world/grounded.d.ts +11 -0
- package/dist/world/grounded.js +12 -0
- package/dist/world/mapAnnotations.d.ts +75 -0
- package/dist/world/mapAnnotations.js +139 -0
- package/dist/world/markers.js +1 -0
- package/dist/world/resourceNode.d.ts +1 -1
- package/dist/world/resourceNode.js +2 -1
- package/dist/world/roads.d.ts +294 -5
- package/dist/world/roads.js +1262 -21
- package/dist/world/sharedAnnotations.d.ts +84 -0
- package/dist/world/sharedAnnotations.js +76 -0
- package/dist/world/spawnOverride.d.ts +37 -0
- package/dist/world/spawnOverride.js +50 -0
- package/dist/world/streetGenerator.d.ts +60 -11
- package/dist/world/streetGenerator.js +1786 -129
- package/dist/world/terrain.d.ts +1 -0
- package/dist/world/terrain.js +2 -0
- package/dist/world/waypoints.d.ts +63 -0
- package/dist/world/waypoints.js +137 -0
- package/dist/world.d.ts +17 -2
- package/dist/world.js +17 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +3 -1
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-combat/api.md +40 -4
- package/skills/jgengine-combat/capabilities.md +12 -0
- package/skills/jgengine-combat/reference.md +2 -0
- package/skills/jgengine-gameplay/api.md +212 -11
- package/skills/jgengine-gameplay/capabilities.md +66 -1
- package/skills/jgengine-world/api.md +397 -11
- package/skills/jgengine-world/capabilities.md +123 -2
- package/skills/jgengine-world/reference.md +15 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { type ActionCodes, type ActionCodesMap } from "./actionBindings.js";
|
|
2
|
+
import { type BindingOverrides } from "./bindingOverrides.js";
|
|
3
|
+
/**
|
|
4
|
+
* One rebindable action as declared to {@link createRebindSession}. The `id` and
|
|
5
|
+
* `label` are FREE strings the session never interprets — the game owns their
|
|
6
|
+
* meaning and display text. `defaultCodes` is the authored binding the session
|
|
7
|
+
* resets back to.
|
|
8
|
+
*/
|
|
9
|
+
export interface RebindActionConfig {
|
|
10
|
+
/** Stable action id (also the {@link BindingOverrides} key). Free string. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Human label shown in the controls list ("Move Forward"). Free string. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** The game's authored binding for this action; `reset` returns here. */
|
|
15
|
+
defaultCodes: ActionCodes;
|
|
16
|
+
}
|
|
17
|
+
/** Config for {@link createRebindSession}: an explicit action list, or an {@link ActionCodesMap} + labels. */
|
|
18
|
+
export type RebindSessionConfig = RebindSessionActionsConfig | RebindSessionMapConfig;
|
|
19
|
+
interface RebindSessionCommon {
|
|
20
|
+
/** Player rebinds to start from (e.g. from {@link loadBindingOverrides}). */
|
|
21
|
+
overrides?: BindingOverrides;
|
|
22
|
+
/** Injected clock (ms) used to timestamp each rebind. Default `Date.now`. */
|
|
23
|
+
now?: () => number;
|
|
24
|
+
}
|
|
25
|
+
/** Declare actions explicitly, in list order. */
|
|
26
|
+
export interface RebindSessionActionsConfig extends RebindSessionCommon {
|
|
27
|
+
actions: readonly RebindActionConfig[];
|
|
28
|
+
}
|
|
29
|
+
/** Declare actions from an authored {@link ActionCodesMap}, labelled by `labels` (falling back to the id). */
|
|
30
|
+
export interface RebindSessionMapConfig extends RebindSessionCommon {
|
|
31
|
+
input: ActionCodesMap;
|
|
32
|
+
labels?: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
/** One controls-list row: an action, its effective binding, and any conflicts. */
|
|
35
|
+
export interface RebindRow {
|
|
36
|
+
/** The action's free-string id. */
|
|
37
|
+
actionId: string;
|
|
38
|
+
/** The action's free-string label. */
|
|
39
|
+
label: string;
|
|
40
|
+
/** Effective binding (default merged with the current override) — hold/toggle/repeat shape preserved. */
|
|
41
|
+
codes: ActionCodes;
|
|
42
|
+
/** Normalized primary code driving this action (`"KeyW"`, `"Space"`), or `null` when unbound. */
|
|
43
|
+
code: string | null;
|
|
44
|
+
/** Short display glyph for {@link RebindRow.code} (via `bindingLabel`), or `""` when unbound. */
|
|
45
|
+
bindingLabel: string;
|
|
46
|
+
/** True when no override is set — the effective binding is the authored default. */
|
|
47
|
+
isDefault: boolean;
|
|
48
|
+
/** Other action ids that share a normalized code with this row (the conflict set). */
|
|
49
|
+
conflictWith: string[];
|
|
50
|
+
/** Clock time (ms) this action was last rebound, or `null` if never rebound this session. */
|
|
51
|
+
changedAt: number | null;
|
|
52
|
+
}
|
|
53
|
+
/** A group of actions bound to the same normalized code — the conflict this session detects. */
|
|
54
|
+
export interface RebindConflict {
|
|
55
|
+
/** The shared normalized code. */
|
|
56
|
+
code: string;
|
|
57
|
+
/** Every action id bound to {@link RebindConflict.code} (always length ≥ 2). */
|
|
58
|
+
actionIds: string[];
|
|
59
|
+
}
|
|
60
|
+
/** Serializable session state for save/restore. */
|
|
61
|
+
export interface RebindSessionSnapshot {
|
|
62
|
+
/** The current player rebinds. */
|
|
63
|
+
overrides: BindingOverrides;
|
|
64
|
+
/** When each action was last rebound (ms), by action id. */
|
|
65
|
+
changedAt: Record<string, number>;
|
|
66
|
+
/** The action currently armed for capture, or `null`. */
|
|
67
|
+
capturingActionId: string | null;
|
|
68
|
+
}
|
|
69
|
+
/** A live, observable, conflict-aware key-remap editor over the action-binding model. */
|
|
70
|
+
export interface RebindSession {
|
|
71
|
+
/** The declared actions, in list order. */
|
|
72
|
+
actions(): readonly RebindActionConfig[];
|
|
73
|
+
/** One {@link RebindRow} per action, in list order, with effective binding + conflicts. */
|
|
74
|
+
rows(): RebindRow[];
|
|
75
|
+
/** The row for a single action, or `null` if the id is unknown. */
|
|
76
|
+
row(actionId: string): RebindRow | null;
|
|
77
|
+
/**
|
|
78
|
+
* Arm capture for an action: the next {@link RebindSession.capture} assigns to
|
|
79
|
+
* it. A React host shows "Press a key…" for the armed action. No-op for an
|
|
80
|
+
* unknown id.
|
|
81
|
+
*/
|
|
82
|
+
beginCapture(actionId: string): void;
|
|
83
|
+
/** The action currently armed for capture, or `null`. */
|
|
84
|
+
capturingActionId(): string | null;
|
|
85
|
+
/** Whether capture is armed (for `actionId` specifically, if given). */
|
|
86
|
+
isCapturing(actionId?: string): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Feed a raw key/button code (e.g. a `KeyboardEvent.code`) to the armed action:
|
|
89
|
+
* it is normalized (`normalizeKeyCode`) and recorded as that action's primary
|
|
90
|
+
* override, preserving the action's hold/toggle/repeat shape, then capture
|
|
91
|
+
* disarms. Returns whether an assignment happened (false when nothing is armed).
|
|
92
|
+
*/
|
|
93
|
+
capture(code: string): boolean;
|
|
94
|
+
/** Disarm capture without changing any binding. */
|
|
95
|
+
cancelCapture(): void;
|
|
96
|
+
/** Every conflict group — actions that share a normalized code. Empty when clean. */
|
|
97
|
+
conflicts(): RebindConflict[];
|
|
98
|
+
/** Whether any two actions share a normalized code. */
|
|
99
|
+
hasConflicts(): boolean;
|
|
100
|
+
/** Reset one action back to its `defaultCodes` (drops its override). */
|
|
101
|
+
reset(actionId: string): void;
|
|
102
|
+
/** Reset every action back to defaults (drops all overrides). */
|
|
103
|
+
resetAll(): void;
|
|
104
|
+
/** Whether an action has no override (its effective binding is the authored default). */
|
|
105
|
+
isDefault(actionId: string): boolean;
|
|
106
|
+
/** The current {@link BindingOverrides} to persist (the caller saves via `saveBindingOverride`). */
|
|
107
|
+
overrides(): BindingOverrides;
|
|
108
|
+
/**
|
|
109
|
+
* Hand the current overrides to a persist callback (e.g. one that calls
|
|
110
|
+
* `saveBindingOverride` per entry) and return them. With no callback it just
|
|
111
|
+
* returns the overrides — a convenient "commit" seam.
|
|
112
|
+
*/
|
|
113
|
+
apply(persistFn?: (overrides: BindingOverrides) => void): BindingOverrides;
|
|
114
|
+
/** Observe changes (capture, reset, restore, arm/disarm). Returns an unsubscribe fn. */
|
|
115
|
+
subscribe(listener: () => void): () => void;
|
|
116
|
+
/** Serializable state for a save. */
|
|
117
|
+
snapshot(): RebindSessionSnapshot;
|
|
118
|
+
/** Restore from a {@link RebindSessionSnapshot}. */
|
|
119
|
+
restore(snapshot: RebindSessionSnapshot): void;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A conflict-aware key-remap session over the action-binding model — the missing
|
|
123
|
+
* editor behind a controls-settings menu. It takes the game's authored actions
|
|
124
|
+
* ({@link RebindActionConfig}s, or an {@link ActionCodesMap} + labels) plus the
|
|
125
|
+
* player's saved {@link BindingOverrides}, and tracks the *effective* binding per
|
|
126
|
+
* action (default merged with the override via `applyBindingOverrides`). `rows()`
|
|
127
|
+
* exposes each action's label, current key glyph (via `bindingLabel`), whether it
|
|
128
|
+
* is still the default, and which other actions it collides with; `conflicts()`
|
|
129
|
+
* groups every action pair sharing a normalized code (the value-add a raw
|
|
130
|
+
* override map can't give you). `beginCapture`/`capture`/`cancelCapture` drive a
|
|
131
|
+
* "press a key" rebind (codes normalized via `normalizeKeyCode`), `reset`/
|
|
132
|
+
* `resetAll` return to defaults, and `overrides()`/`apply()` hand the caller the
|
|
133
|
+
* `BindingOverrides` to persist. Nothing here is genre-specific: action ids and
|
|
134
|
+
* labels are free strings the session never interprets, and hold/toggle/repeat
|
|
135
|
+
* binding shapes survive a rebind. `snapshot`/`restore` round-trips the editor
|
|
136
|
+
* (including the in-flight capture) through a save.
|
|
137
|
+
*
|
|
138
|
+
* @capability key-rebinding conflict-aware key-remap session over the action-binding model — capture/reassign, conflict detection, reset-to-defaults, persist overrides, snapshot/restore
|
|
139
|
+
*/
|
|
140
|
+
export declare function createRebindSession(config: RebindSessionConfig): RebindSession;
|
|
141
|
+
export {};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { bindingLabel, normalizeKeyCode, } from "./actionBindings.js";
|
|
2
|
+
import { applyBindingOverrides } from "./bindingOverrides.js";
|
|
3
|
+
/** All codes an {@link ActionCodes} entry binds (hold + toggle, or the plain array). */
|
|
4
|
+
function allCodes(codes) {
|
|
5
|
+
if (Array.isArray(codes))
|
|
6
|
+
return [...codes];
|
|
7
|
+
const modes = codes;
|
|
8
|
+
return [...(modes.hold ?? []), ...(modes.toggle ?? [])];
|
|
9
|
+
}
|
|
10
|
+
/** The first (primary) code of an {@link ActionCodes} entry, or `null` when unbound. */
|
|
11
|
+
function primaryCode(codes) {
|
|
12
|
+
return allCodes(codes)[0] ?? null;
|
|
13
|
+
}
|
|
14
|
+
/** Replace the primary code of `codes` with `code`, keeping the hold/toggle/repeat shape. */
|
|
15
|
+
function withPrimaryCode(codes, code) {
|
|
16
|
+
if (Array.isArray(codes)) {
|
|
17
|
+
return [code, ...codes.slice(1)];
|
|
18
|
+
}
|
|
19
|
+
const modes = codes;
|
|
20
|
+
if (modes.hold !== undefined && modes.hold.length > 0) {
|
|
21
|
+
return { ...modes, hold: [code, ...modes.hold.slice(1)] };
|
|
22
|
+
}
|
|
23
|
+
if (modes.toggle !== undefined && modes.toggle.length > 0) {
|
|
24
|
+
return { ...modes, toggle: [code, ...modes.toggle.slice(1)] };
|
|
25
|
+
}
|
|
26
|
+
return { ...modes, hold: [code] };
|
|
27
|
+
}
|
|
28
|
+
function normalizeConfig(config) {
|
|
29
|
+
if ("actions" in config)
|
|
30
|
+
return config.actions.map((a) => ({ ...a }));
|
|
31
|
+
const labels = config.labels ?? {};
|
|
32
|
+
return Object.keys(config.input).map((id) => ({
|
|
33
|
+
id,
|
|
34
|
+
label: labels[id] ?? id,
|
|
35
|
+
defaultCodes: config.input[id],
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A conflict-aware key-remap session over the action-binding model — the missing
|
|
40
|
+
* editor behind a controls-settings menu. It takes the game's authored actions
|
|
41
|
+
* ({@link RebindActionConfig}s, or an {@link ActionCodesMap} + labels) plus the
|
|
42
|
+
* player's saved {@link BindingOverrides}, and tracks the *effective* binding per
|
|
43
|
+
* action (default merged with the override via `applyBindingOverrides`). `rows()`
|
|
44
|
+
* exposes each action's label, current key glyph (via `bindingLabel`), whether it
|
|
45
|
+
* is still the default, and which other actions it collides with; `conflicts()`
|
|
46
|
+
* groups every action pair sharing a normalized code (the value-add a raw
|
|
47
|
+
* override map can't give you). `beginCapture`/`capture`/`cancelCapture` drive a
|
|
48
|
+
* "press a key" rebind (codes normalized via `normalizeKeyCode`), `reset`/
|
|
49
|
+
* `resetAll` return to defaults, and `overrides()`/`apply()` hand the caller the
|
|
50
|
+
* `BindingOverrides` to persist. Nothing here is genre-specific: action ids and
|
|
51
|
+
* labels are free strings the session never interprets, and hold/toggle/repeat
|
|
52
|
+
* binding shapes survive a rebind. `snapshot`/`restore` round-trips the editor
|
|
53
|
+
* (including the in-flight capture) through a save.
|
|
54
|
+
*
|
|
55
|
+
* @capability key-rebinding conflict-aware key-remap session over the action-binding model — capture/reassign, conflict detection, reset-to-defaults, persist overrides, snapshot/restore
|
|
56
|
+
*/
|
|
57
|
+
export function createRebindSession(config) {
|
|
58
|
+
const now = config.now ?? Date.now;
|
|
59
|
+
const actionList = normalizeConfig(config);
|
|
60
|
+
const defaultMap = {};
|
|
61
|
+
for (const action of actionList)
|
|
62
|
+
defaultMap[action.id] = action.defaultCodes;
|
|
63
|
+
const knownIds = new Set(actionList.map((a) => a.id));
|
|
64
|
+
const overrides = {};
|
|
65
|
+
const changedAt = new Map();
|
|
66
|
+
let capturingActionId = null;
|
|
67
|
+
const listeners = new Set();
|
|
68
|
+
function ingestOverrides(source) {
|
|
69
|
+
for (const id of Object.keys(source)) {
|
|
70
|
+
if (knownIds.has(id))
|
|
71
|
+
overrides[id] = source[id];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (config.overrides !== undefined)
|
|
75
|
+
ingestOverrides(config.overrides);
|
|
76
|
+
function notify() {
|
|
77
|
+
for (const listener of listeners)
|
|
78
|
+
listener();
|
|
79
|
+
}
|
|
80
|
+
/** Effective codes for an action: override merged over the authored default. */
|
|
81
|
+
function effectiveMap() {
|
|
82
|
+
return applyBindingOverrides(defaultMap, overrides);
|
|
83
|
+
}
|
|
84
|
+
/** Map of normalized code → action ids currently bound to it. */
|
|
85
|
+
function codeIndex() {
|
|
86
|
+
const effective = effectiveMap();
|
|
87
|
+
const index = new Map();
|
|
88
|
+
for (const action of actionList) {
|
|
89
|
+
for (const raw of allCodes(effective[action.id])) {
|
|
90
|
+
const norm = normalizeKeyCode(raw);
|
|
91
|
+
const bucket = index.get(norm);
|
|
92
|
+
if (bucket === undefined)
|
|
93
|
+
index.set(norm, [action.id]);
|
|
94
|
+
else
|
|
95
|
+
bucket.push(action.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return index;
|
|
99
|
+
}
|
|
100
|
+
function rows() {
|
|
101
|
+
const effective = effectiveMap();
|
|
102
|
+
const index = codeIndex();
|
|
103
|
+
return actionList.map((action) => {
|
|
104
|
+
const codes = effective[action.id];
|
|
105
|
+
const primaryRaw = primaryCode(codes);
|
|
106
|
+
const primary = primaryRaw === null ? null : normalizeKeyCode(primaryRaw);
|
|
107
|
+
const conflictWith = new Set();
|
|
108
|
+
for (const raw of allCodes(codes)) {
|
|
109
|
+
const bound = index.get(normalizeKeyCode(raw));
|
|
110
|
+
if (bound === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
for (const other of bound)
|
|
113
|
+
if (other !== action.id)
|
|
114
|
+
conflictWith.add(other);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
actionId: action.id,
|
|
118
|
+
label: action.label,
|
|
119
|
+
codes,
|
|
120
|
+
code: primary,
|
|
121
|
+
bindingLabel: primary === null ? "" : bindingLabel(primary),
|
|
122
|
+
isDefault: overrides[action.id] === undefined,
|
|
123
|
+
conflictWith: [...conflictWith],
|
|
124
|
+
changedAt: changedAt.get(action.id) ?? null,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
actions() {
|
|
130
|
+
return actionList;
|
|
131
|
+
},
|
|
132
|
+
rows,
|
|
133
|
+
row(actionId) {
|
|
134
|
+
return rows().find((r) => r.actionId === actionId) ?? null;
|
|
135
|
+
},
|
|
136
|
+
beginCapture(actionId) {
|
|
137
|
+
if (!knownIds.has(actionId) || capturingActionId === actionId)
|
|
138
|
+
return;
|
|
139
|
+
capturingActionId = actionId;
|
|
140
|
+
notify();
|
|
141
|
+
},
|
|
142
|
+
capturingActionId() {
|
|
143
|
+
return capturingActionId;
|
|
144
|
+
},
|
|
145
|
+
isCapturing(actionId) {
|
|
146
|
+
if (actionId === undefined)
|
|
147
|
+
return capturingActionId !== null;
|
|
148
|
+
return capturingActionId === actionId;
|
|
149
|
+
},
|
|
150
|
+
capture(code) {
|
|
151
|
+
const target = capturingActionId;
|
|
152
|
+
if (target === null)
|
|
153
|
+
return false;
|
|
154
|
+
const normalized = normalizeKeyCode(code);
|
|
155
|
+
const base = effectiveMap()[target];
|
|
156
|
+
overrides[target] = withPrimaryCode(base, normalized);
|
|
157
|
+
changedAt.set(target, now());
|
|
158
|
+
capturingActionId = null;
|
|
159
|
+
notify();
|
|
160
|
+
return true;
|
|
161
|
+
},
|
|
162
|
+
cancelCapture() {
|
|
163
|
+
if (capturingActionId === null)
|
|
164
|
+
return;
|
|
165
|
+
capturingActionId = null;
|
|
166
|
+
notify();
|
|
167
|
+
},
|
|
168
|
+
conflicts() {
|
|
169
|
+
const groups = [];
|
|
170
|
+
for (const [code, actionIds] of codeIndex()) {
|
|
171
|
+
if (actionIds.length >= 2)
|
|
172
|
+
groups.push({ code, actionIds: [...actionIds] });
|
|
173
|
+
}
|
|
174
|
+
return groups;
|
|
175
|
+
},
|
|
176
|
+
hasConflicts() {
|
|
177
|
+
for (const actionIds of codeIndex().values())
|
|
178
|
+
if (actionIds.length >= 2)
|
|
179
|
+
return true;
|
|
180
|
+
return false;
|
|
181
|
+
},
|
|
182
|
+
reset(actionId) {
|
|
183
|
+
if (overrides[actionId] === undefined)
|
|
184
|
+
return;
|
|
185
|
+
delete overrides[actionId];
|
|
186
|
+
changedAt.delete(actionId);
|
|
187
|
+
notify();
|
|
188
|
+
},
|
|
189
|
+
resetAll() {
|
|
190
|
+
if (Object.keys(overrides).length === 0 && changedAt.size === 0)
|
|
191
|
+
return;
|
|
192
|
+
for (const key of Object.keys(overrides))
|
|
193
|
+
delete overrides[key];
|
|
194
|
+
changedAt.clear();
|
|
195
|
+
notify();
|
|
196
|
+
},
|
|
197
|
+
isDefault(actionId) {
|
|
198
|
+
return overrides[actionId] === undefined;
|
|
199
|
+
},
|
|
200
|
+
overrides() {
|
|
201
|
+
return { ...overrides };
|
|
202
|
+
},
|
|
203
|
+
apply(persistFn) {
|
|
204
|
+
const snapshot = { ...overrides };
|
|
205
|
+
persistFn?.(snapshot);
|
|
206
|
+
return snapshot;
|
|
207
|
+
},
|
|
208
|
+
subscribe(listener) {
|
|
209
|
+
listeners.add(listener);
|
|
210
|
+
return () => listeners.delete(listener);
|
|
211
|
+
},
|
|
212
|
+
snapshot() {
|
|
213
|
+
return {
|
|
214
|
+
overrides: { ...overrides },
|
|
215
|
+
changedAt: Object.fromEntries(changedAt),
|
|
216
|
+
capturingActionId,
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
restore(snapshot) {
|
|
220
|
+
for (const key of Object.keys(overrides))
|
|
221
|
+
delete overrides[key];
|
|
222
|
+
changedAt.clear();
|
|
223
|
+
ingestOverrides(snapshot.overrides);
|
|
224
|
+
for (const [id, at] of Object.entries(snapshot.changedAt)) {
|
|
225
|
+
if (knownIds.has(id))
|
|
226
|
+
changedAt.set(id, at);
|
|
227
|
+
}
|
|
228
|
+
capturingActionId =
|
|
229
|
+
snapshot.capturingActionId !== null && knownIds.has(snapshot.capturingActionId)
|
|
230
|
+
? snapshot.capturingActionId
|
|
231
|
+
: null;
|
|
232
|
+
notify();
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime switch for the active touch control context (see `TouchControlsConfig.modes`): gameplay
|
|
3
|
+
* calls `setTouchControlsMode(ctx, "car")` when the player boards a vehicle and `null` on exit, and
|
|
4
|
+
* the shell re-derives the touch scheme so only that context's controls are on screen. Backed by the
|
|
5
|
+
* ctx keyed store, so writes notify `ctx.subscribe` (the shell re-renders) and the value rides along
|
|
6
|
+
* with whole-world saves exactly like the gameplay state that implies it.
|
|
7
|
+
*/
|
|
8
|
+
import type { GameContext } from "../runtime/gameContext.js";
|
|
9
|
+
/**
|
|
10
|
+
* Activate a named touch control mode, or `null` to return to the base config.
|
|
11
|
+
*
|
|
12
|
+
* @capability touch-controls switch the on-screen touch control set when gameplay context changes (enter/exit vehicle, mount, build mode)
|
|
13
|
+
*/
|
|
14
|
+
export declare function setTouchControlsMode(ctx: GameContext, mode: string | null): void;
|
|
15
|
+
/** The active touch control mode, `null` when the base config applies. */
|
|
16
|
+
export declare function activeTouchControlsMode(ctx: GameContext): string | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime switch for the active touch control context (see `TouchControlsConfig.modes`): gameplay
|
|
3
|
+
* calls `setTouchControlsMode(ctx, "car")` when the player boards a vehicle and `null` on exit, and
|
|
4
|
+
* the shell re-derives the touch scheme so only that context's controls are on screen. Backed by the
|
|
5
|
+
* ctx keyed store, so writes notify `ctx.subscribe` (the shell re-renders) and the value rides along
|
|
6
|
+
* with whole-world saves exactly like the gameplay state that implies it.
|
|
7
|
+
*/
|
|
8
|
+
import { defineStore } from "../store/defineStore.js";
|
|
9
|
+
const touchModeStore = defineStore("engine.touch.mode", null);
|
|
10
|
+
/**
|
|
11
|
+
* Activate a named touch control mode, or `null` to return to the base config.
|
|
12
|
+
*
|
|
13
|
+
* @capability touch-controls switch the on-screen touch control set when gameplay context changes (enter/exit vehicle, mount, build mode)
|
|
14
|
+
*/
|
|
15
|
+
export function setTouchControlsMode(ctx, mode) {
|
|
16
|
+
if (touchModeStore.read(ctx) === mode)
|
|
17
|
+
return;
|
|
18
|
+
touchModeStore.write(ctx, mode);
|
|
19
|
+
}
|
|
20
|
+
/** The active touch control mode, `null` when the base config applies. */
|
|
21
|
+
export function activeTouchControlsMode(ctx) {
|
|
22
|
+
return touchModeStore.read(ctx);
|
|
23
|
+
}
|
|
@@ -62,6 +62,22 @@ export declare const TOUCH_STYLE_OPTIONS: readonly {
|
|
|
62
62
|
}[];
|
|
63
63
|
/** Skin used when neither the game nor the player picks one. */
|
|
64
64
|
export declare const DEFAULT_TOUCH_STYLE: TouchStyle;
|
|
65
|
+
/**
|
|
66
|
+
* How the movement joystick behaves — the two shipped variants, player-selectable in
|
|
67
|
+
* Settings → Controls. `fixed` is the classic always-visible stick in its dock corner;
|
|
68
|
+
* `floating` keeps the corner as an empty capture zone and spawns the stick centered under
|
|
69
|
+
* wherever the thumb lands, so the hand never has to find a fixed target mid-play.
|
|
70
|
+
*/
|
|
71
|
+
export type TouchJoystickVariant = "fixed" | "floating";
|
|
72
|
+
/** Every joystick variant id, in menu order. */
|
|
73
|
+
export declare const TOUCH_JOYSTICK_VARIANTS: readonly TouchJoystickVariant[];
|
|
74
|
+
/** Joystick variants as `{ value, label }` rows for the Settings → Controls selector. */
|
|
75
|
+
export declare const TOUCH_JOYSTICK_VARIANT_OPTIONS: readonly {
|
|
76
|
+
value: TouchJoystickVariant;
|
|
77
|
+
label: string;
|
|
78
|
+
}[];
|
|
79
|
+
/** Variant used when the player has not picked one. */
|
|
80
|
+
export declare const DEFAULT_TOUCH_JOYSTICK_VARIANT: TouchJoystickVariant;
|
|
65
81
|
/** Where each touch cluster docks; unset falls back to the classic bottom layout. */
|
|
66
82
|
export interface TouchLayoutConfig {
|
|
67
83
|
/** Virtual joystick / movement zone. Default `bottom-left`. */
|
|
@@ -95,6 +111,9 @@ export interface TouchButtonSpec {
|
|
|
95
111
|
export interface TouchMovementConfig {
|
|
96
112
|
axis?: "both" | "horizontal" | "vertical";
|
|
97
113
|
}
|
|
114
|
+
/** One named control context's touch config — the same shape as the base config, minus nested modes. */
|
|
115
|
+
export type TouchControlsModeConfig = Omit<TouchControlsConfig, "modes">;
|
|
116
|
+
/** Game-authored refinement of the derived touch scheme on `defineGame({ touch })` — gestures, curated buttons, hidden actions, cluster layout, skin, and per-context `modes`. */
|
|
98
117
|
export interface TouchControlsConfig {
|
|
99
118
|
/** `false` removes the virtual joystick even when movement actions are bound; an object restricts it to one axis. */
|
|
100
119
|
movement?: false | TouchMovementConfig;
|
|
@@ -112,6 +131,14 @@ export interface TouchControlsConfig {
|
|
|
112
131
|
layout?: TouchLayoutConfig;
|
|
113
132
|
/** Suggested default skin; the player's Settings choice overrides it. Default `glass`. */
|
|
114
133
|
style?: TouchStyle;
|
|
134
|
+
/**
|
|
135
|
+
* Per-context control sets for games whose verbs change at runtime — on foot vs driving vs
|
|
136
|
+
* flying. Each key is a mode name whose fields *replace* the matching base fields while that
|
|
137
|
+
* mode is active (base config = the default mode). Gameplay switches with
|
|
138
|
+
* `setTouchControlsMode(ctx, "car")` and back with `null`; only the actions a context actually
|
|
139
|
+
* uses should be visible in it, so a runner on foot never sees flight or vehicle chrome.
|
|
140
|
+
*/
|
|
141
|
+
modes?: Record<string, TouchControlsModeConfig>;
|
|
115
142
|
}
|
|
116
143
|
export interface TouchJoystick {
|
|
117
144
|
up: string | null;
|
|
@@ -157,9 +184,11 @@ export interface DeriveTouchSchemeOptions {
|
|
|
157
184
|
reserved: ReadonlySet<string>;
|
|
158
185
|
firstPerson: boolean;
|
|
159
186
|
config?: TouchControlsConfig | false;
|
|
187
|
+
/** Active control context (see {@link TouchControlsConfig.modes}); null/unknown falls back to the base config. */
|
|
188
|
+
mode?: string | null;
|
|
160
189
|
}
|
|
161
190
|
/**
|
|
162
191
|
* Null means "render no touch controls" — either the game opted out or there
|
|
163
192
|
* is nothing to synthesize.
|
|
164
193
|
*/
|
|
165
|
-
export declare function deriveTouchScheme(input: ActionCodesMap | undefined, { reserved, firstPerson, config }: DeriveTouchSchemeOptions): TouchScheme | null;
|
|
194
|
+
export declare function deriveTouchScheme(input: ActionCodesMap | undefined, { reserved, firstPerson, config: rawConfig, mode }: DeriveTouchSchemeOptions): TouchScheme | null;
|
|
@@ -44,6 +44,15 @@ export const TOUCH_STYLE_OPTIONS = [
|
|
|
44
44
|
];
|
|
45
45
|
/** Skin used when neither the game nor the player picks one. */
|
|
46
46
|
export const DEFAULT_TOUCH_STYLE = "glass";
|
|
47
|
+
/** Every joystick variant id, in menu order. */
|
|
48
|
+
export const TOUCH_JOYSTICK_VARIANTS = ["fixed", "floating"];
|
|
49
|
+
/** Joystick variants as `{ value, label }` rows for the Settings → Controls selector. */
|
|
50
|
+
export const TOUCH_JOYSTICK_VARIANT_OPTIONS = [
|
|
51
|
+
{ value: "floating", label: "Floating (under your thumb)" },
|
|
52
|
+
{ value: "fixed", label: "Fixed" },
|
|
53
|
+
];
|
|
54
|
+
/** Variant used when the player has not picked one. */
|
|
55
|
+
export const DEFAULT_TOUCH_JOYSTICK_VARIANT = "floating";
|
|
47
56
|
const DEFAULT_LOOK_SENSITIVITY = 0.005;
|
|
48
57
|
const DEFAULT_TOUCH_LAYOUT = {
|
|
49
58
|
movement: "bottom-left",
|
|
@@ -175,11 +184,27 @@ function gestureActions(gestures) {
|
|
|
175
184
|
}
|
|
176
185
|
return used;
|
|
177
186
|
}
|
|
187
|
+
/** Resolve the effective config for a mode: mode fields replace base fields; the base is the default mode. */
|
|
188
|
+
function resolveModeConfig(config, mode) {
|
|
189
|
+
if (config === false || config === undefined || mode === null || mode === undefined)
|
|
190
|
+
return config;
|
|
191
|
+
const active = config.modes?.[mode];
|
|
192
|
+
if (active === undefined)
|
|
193
|
+
return config;
|
|
194
|
+
const { modes: _modes, ...base } = config;
|
|
195
|
+
return { ...base, ...active };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Auto-derived button count past which the dock stops being playable — two full thumb arcs. A
|
|
199
|
+
* curated `touch.buttons` list may exceed it deliberately; blind derivation may not.
|
|
200
|
+
*/
|
|
201
|
+
const DERIVED_BUTTON_BUDGET = 8;
|
|
178
202
|
/**
|
|
179
203
|
* Null means "render no touch controls" — either the game opted out or there
|
|
180
204
|
* is nothing to synthesize.
|
|
181
205
|
*/
|
|
182
|
-
export function deriveTouchScheme(input, { reserved, firstPerson, config }) {
|
|
206
|
+
export function deriveTouchScheme(input, { reserved, firstPerson, config: rawConfig, mode }) {
|
|
207
|
+
const config = resolveModeConfig(rawConfig, mode);
|
|
183
208
|
if (config === false)
|
|
184
209
|
return null;
|
|
185
210
|
const actions = Object.keys(input ?? {});
|
|
@@ -246,6 +271,11 @@ export function deriveTouchScheme(input, { reserved, firstPerson, config }) {
|
|
|
246
271
|
anchor: null,
|
|
247
272
|
image: null,
|
|
248
273
|
}));
|
|
274
|
+
if (buttons.length > DERIVED_BUTTON_BUDGET) {
|
|
275
|
+
console.warn(`[jgengine:touch] derived ${buttons.length} on-screen buttons (${buttons
|
|
276
|
+
.map((button) => button.action)
|
|
277
|
+
.join(", ")}) — more than a thumb can reach. Curate touch.buttons, hide actions with touch.hidden, or split contexts with touch.modes.`);
|
|
278
|
+
}
|
|
249
279
|
}
|
|
250
280
|
const look = config?.look ?? firstPerson;
|
|
251
281
|
if (joystick === null && buttons.length === 0 && gestures === null && !look)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type PositionedPrompt, type PromptPoint } from "./proximityPrompt.js";
|
|
2
|
+
/**
|
|
3
|
+
* A partial edit applied to an already-registered {@link PositionedPrompt} by
|
|
4
|
+
* {@link PromptRegistry.update}. Any subset of the mutable fields — everything
|
|
5
|
+
* but the stable `id` — may be supplied; omitted fields keep their prior value.
|
|
6
|
+
*/
|
|
7
|
+
export type PositionedPromptPatch = Partial<Omit<PositionedPrompt, "id">>;
|
|
8
|
+
/**
|
|
9
|
+
* The serializable shape {@link PromptRegistry.snapshot} returns and
|
|
10
|
+
* {@link PromptRegistry.restore} consumes: the full set of positioned prompts
|
|
11
|
+
* (in registration order) plus the id of the prompt that was active when the
|
|
12
|
+
* snapshot was taken. Prompt display/command payloads are free strings and ids —
|
|
13
|
+
* keep them serializable and this round-trips through a save cleanly.
|
|
14
|
+
*/
|
|
15
|
+
export interface PromptRegistrySnapshot {
|
|
16
|
+
/** Every registered prompt, in registration order (tie-break order for resolve). */
|
|
17
|
+
prompts: readonly PositionedPrompt[];
|
|
18
|
+
/** Id of the active prompt at snapshot time, or `null` if none was active. */
|
|
19
|
+
activeId: string | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A live, observable set of interaction prompts that OWNS the registered
|
|
23
|
+
* {@link PositionedPrompt}s and tracks which one is active for a player. It is a
|
|
24
|
+
* thin stateful layer over {@link resolveActivePrompt}: registration/order lives
|
|
25
|
+
* here, the nearest-within-radius / higher-priority selection stays in the pure
|
|
26
|
+
* resolver. {@link resolve} is the per-frame hot path — call it as the player
|
|
27
|
+
* moves; subscribers fire only when the active prompt actually CHANGES.
|
|
28
|
+
*/
|
|
29
|
+
export interface PromptRegistry {
|
|
30
|
+
/**
|
|
31
|
+
* Add (or replace, by id) a positioned prompt. Replacing an existing id keeps
|
|
32
|
+
* its registration slot so tie-break order is stable. Does not re-resolve on
|
|
33
|
+
* its own — the next {@link resolve} picks up the change.
|
|
34
|
+
*/
|
|
35
|
+
register(prompt: PositionedPrompt): void;
|
|
36
|
+
/**
|
|
37
|
+
* Patch a registered prompt in place. No-op if `id` is unknown. Only the fields
|
|
38
|
+
* present in `patch` change; the rest are preserved.
|
|
39
|
+
*/
|
|
40
|
+
update(id: string, patch: PositionedPromptPatch): void;
|
|
41
|
+
/** Remove the prompt with this id, if present. Returns whether one was removed. */
|
|
42
|
+
unregister(id: string): boolean;
|
|
43
|
+
/** Remove every prompt and clear the active selection. */
|
|
44
|
+
clear(): void;
|
|
45
|
+
/** All registered prompts in registration order (do not mutate the returned array). */
|
|
46
|
+
all(): readonly PositionedPrompt[];
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the active prompt for a player at `playerPosition` by delegating to
|
|
49
|
+
* {@link resolveActivePrompt}, cache its id, and notify subscribers only when
|
|
50
|
+
* the active prompt changed since the last call — so a HUD bound to this
|
|
51
|
+
* registry re-renders on transitions, not every frame. Returns the active
|
|
52
|
+
* prompt or `null`.
|
|
53
|
+
*/
|
|
54
|
+
resolve(playerPosition: PromptPoint): PositionedPrompt | null;
|
|
55
|
+
/** The prompt selected by the most recent {@link resolve}, or `null`. */
|
|
56
|
+
active(): PositionedPrompt | null;
|
|
57
|
+
/** Observe active-prompt changes (resolve transitions, clear, restore). Returns an unsubscribe fn. */
|
|
58
|
+
subscribe(listener: () => void): () => void;
|
|
59
|
+
/** Serializable state for a save. */
|
|
60
|
+
snapshot(): PromptRegistrySnapshot;
|
|
61
|
+
/** Restore prompts + active id from a {@link PromptRegistrySnapshot}. */
|
|
62
|
+
restore(snapshot: PromptRegistrySnapshot): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Create a live, observable interaction-prompt registry over the pure
|
|
66
|
+
* {@link resolveActivePrompt} resolver. Register positioned "press E" prompts,
|
|
67
|
+
* `resolve(playerPosition)` each frame as the hero moves, and read `active()` —
|
|
68
|
+
* the nearest strictly-in-range prompt (higher `priority` beats closer). The set
|
|
69
|
+
* is owned here (register/update/unregister/clear/all); selection stays pure.
|
|
70
|
+
* Subscribers are notified only when the active prompt *changes*, so a HUD does
|
|
71
|
+
* not thrash per frame, and `snapshot`/`restore` round-trip the prompts plus the
|
|
72
|
+
* active id through a save (display payloads are free strings/ids the game owns —
|
|
73
|
+
* the registry never branches on their meaning). Allocation-aware: `all()` and
|
|
74
|
+
* `resolve()` reuse a cached array instead of rebuilding per call.
|
|
75
|
+
*
|
|
76
|
+
* @capability interaction-prompt observable nearest-interactable prompt registry over the proximity-prompt resolver — register/resolve/active with priority+range and change-notified snapshot/restore
|
|
77
|
+
*/
|
|
78
|
+
export declare function createPromptRegistry(): PromptRegistry;
|