@jgengine/core 0.17.0 → 0.18.1
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 +130 -2
- package/dist/ai/decisionGraph.d.ts +52 -0
- package/dist/ai/decisionGraph.js +82 -0
- package/dist/ai/mobBrainGraph.d.ts +3 -0
- package/dist/ai/mobBrainGraph.js +25 -0
- package/dist/ai/tacticalQueries.d.ts +30 -0
- package/dist/ai/tacticalQueries.js +45 -0
- package/dist/anim/animGraph.d.ts +144 -0
- package/dist/anim/animGraph.js +286 -0
- package/dist/anim/ikSolver.d.ts +51 -0
- package/dist/anim/ikSolver.js +124 -0
- package/dist/anim/locomotionGraph.d.ts +26 -0
- package/dist/anim/locomotionGraph.js +33 -0
- package/dist/assets/spriteAtlas.d.ts +21 -0
- package/dist/assets/spriteAtlas.js +103 -0
- package/dist/audio/audioFalloff.d.ts +10 -0
- package/dist/economy/currency.d.ts +16 -2
- package/dist/economy/currency.js +55 -14
- package/dist/economy/wallet.d.ts +5 -4
- package/dist/economy/wallet.js +8 -4
- package/dist/editor/commandApply.js +1 -0
- package/dist/editor/commands.d.ts +4 -1
- package/dist/editor/document.d.ts +2 -1
- package/dist/editor/document.js +89 -0
- package/dist/editor/environment.d.ts +3 -0
- package/dist/editor/environment.js +16 -0
- package/dist/editor/index.d.ts +4 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +16 -0
- package/dist/game/cameraConfig.d.ts +8 -0
- package/dist/game/chat.d.ts +22 -1
- package/dist/game/chat.js +48 -19
- package/dist/game/controlGate.d.ts +6 -0
- package/dist/game/controlGate.js +30 -1
- package/dist/game/defineGame.d.ts +10 -1
- package/dist/game/events.d.ts +8 -1
- package/dist/game/pixelPerfect.d.ts +25 -0
- package/dist/game/pixelPerfect.js +29 -0
- package/dist/game/playableGame.d.ts +97 -3
- package/dist/game/systemRuntime.js +6 -1
- package/dist/input/actionBindings.d.ts +2 -1
- package/dist/input/actionBindings.js +10 -8
- package/dist/input/actionContexts.d.ts +21 -0
- package/dist/input/actionContexts.js +56 -0
- package/dist/input/gamepadModel.d.ts +45 -0
- package/dist/input/gamepadModel.js +69 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +103 -1
- package/dist/movement/characterController.d.ts +76 -0
- package/dist/movement/characterController.js +216 -0
- package/dist/movement/freeFlight.d.ts +119 -0
- package/dist/movement/freeFlight.js +294 -0
- package/dist/movement/playerMovement.d.ts +10 -2
- package/dist/movement/playerMovement.js +218 -4
- package/dist/movement/serverStep.d.ts +5 -0
- package/dist/movement/serverStep.js +6 -0
- package/dist/movement/solidObstacles.d.ts +30 -7
- package/dist/movement/solidObstacles.js +27 -4
- package/dist/multiplayer/poseSyncGate.d.ts +3 -1
- package/dist/multiplayer/poseSyncGate.js +9 -1
- package/dist/multiplayer/presenceContract.d.ts +6 -0
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +11 -0
- package/dist/nav/navMesh.d.ts +33 -0
- package/dist/nav/navMesh.js +164 -0
- package/dist/physics/flightDynamics.d.ts +8 -0
- package/dist/physics/kinematicVehicle.d.ts +6 -1
- package/dist/physics/physicsBackend.d.ts +191 -0
- package/dist/physics/physicsBackend.js +49 -0
- package/dist/physics/physicsBackendConformance.d.ts +27 -0
- package/dist/physics/physicsBackendConformance.js +117 -0
- package/dist/physics/physicsWorldBackend.d.ts +44 -0
- package/dist/physics/physicsWorldBackend.js +485 -0
- package/dist/physics/worldColliders.d.ts +13 -0
- package/dist/physics/worldColliders.js +96 -0
- package/dist/random/rng.d.ts +12 -1
- package/dist/random/rng.js +20 -3
- package/dist/render/environment.d.ts +23 -0
- package/dist/render/environment.js +1 -0
- package/dist/render/lookPreset.d.ts +11 -5
- package/dist/render/lookPreset.js +29 -4
- package/dist/render/postProcessing.d.ts +10 -1
- package/dist/render/sprite2d.d.ts +38 -0
- package/dist/render/sprite2d.js +40 -0
- package/dist/runtime/adapter.d.ts +20 -1
- package/dist/runtime/adapter.js +8 -1
- package/dist/runtime/commandInput.d.ts +15 -0
- package/dist/runtime/commandInput.js +13 -0
- package/dist/runtime/commandRunner.d.ts +44 -3
- package/dist/runtime/commandRunner.js +41 -3
- package/dist/runtime/gameContext.js +11 -1
- package/dist/runtime/gameContextTypes.d.ts +14 -4
- package/dist/runtime/gameRuntime.d.ts +33 -4
- package/dist/runtime/gameRuntime.js +26 -5
- package/dist/runtime/headlessRunner.d.ts +10 -0
- package/dist/runtime/headlessRunner.js +26 -8
- package/dist/runtime/hostPolicy.d.ts +3 -2
- package/dist/runtime/hostPolicy.js +8 -10
- package/dist/runtime/hostedGameRunner.js +34 -6
- package/dist/runtime/hostedWorldSession.d.ts +12 -16
- package/dist/runtime/hostedWorldSession.js +29 -6
- package/dist/runtime/hostedWorldStore.d.ts +20 -0
- package/dist/runtime/hostedWorldStore.js +1 -0
- package/dist/runtime/inputRecorder.d.ts +30 -0
- package/dist/runtime/inputRecorder.js +59 -0
- package/dist/runtime/inputSnapshot.d.ts +8 -0
- package/dist/runtime/inputSnapshot.js +1 -0
- package/dist/runtime/poseInterpolation.d.ts +36 -0
- package/dist/runtime/poseInterpolation.js +91 -0
- package/dist/runtime/prediction.d.ts +45 -0
- package/dist/runtime/prediction.js +62 -0
- package/dist/runtime/simContext.d.ts +45 -0
- package/dist/runtime/simContext.js +65 -0
- package/dist/runtime/simLoop.d.ts +52 -0
- package/dist/runtime/simLoop.js +72 -0
- package/dist/runtime/simSnapshot.d.ts +32 -0
- package/dist/runtime/simSnapshot.js +60 -0
- package/dist/runtime/snapshot.d.ts +8 -0
- package/dist/runtime/snapshotBuffer.d.ts +33 -0
- package/dist/runtime/snapshotBuffer.js +58 -0
- package/dist/runtime/transport.d.ts +38 -2
- package/dist/runtime/worldChunks.d.ts +2 -0
- package/dist/runtime/worldChunks.js +14 -0
- package/dist/runtime/worldProjection.d.ts +9 -0
- package/dist/runtime/worldProjection.js +17 -1
- package/dist/runtime/worldSnapshot.d.ts +6 -0
- package/dist/runtime/worldSnapshot.js +3 -0
- package/dist/scene/behaviorRuntime.d.ts +19 -1
- package/dist/scene/behaviorRuntime.js +55 -4
- package/dist/scene/behaviors.d.ts +8 -1
- package/dist/scene/builtinSceneKinds.js +10 -0
- package/dist/sensor/perception.d.ts +60 -0
- package/dist/sensor/perception.js +142 -0
- package/dist/settings/graphicsProfile.d.ts +19 -0
- package/dist/settings/graphicsProfile.js +36 -0
- package/dist/time/accrueSince.d.ts +10 -0
- package/dist/time/accrueSince.js +9 -0
- package/dist/time/rateWindow.d.ts +16 -0
- package/dist/time/rateWindow.js +14 -0
- package/dist/time/serverTick.d.ts +21 -3
- package/dist/time/serverTick.js +14 -13
- package/dist/vfx/particleDirector.d.ts +68 -0
- package/dist/vfx/particleDirector.js +81 -0
- package/dist/world/buildings.d.ts +12 -0
- package/dist/world/buildings.js +43 -3
- package/dist/world/cityContent.js +103 -25
- package/dist/world/cityKind.d.ts +132 -0
- package/dist/world/cityKind.js +315 -15
- package/dist/world/features.d.ts +27 -1
- package/dist/world/features.js +7 -2
- package/dist/world/placement.d.ts +7 -0
- package/dist/world/placement.js +2 -0
- package/dist/world/placementController.d.ts +1 -1
- package/dist/world/placementController.js +7 -0
- package/dist/world/raceCircuit.d.ts +168 -0
- package/dist/world/raceCircuit.js +797 -0
- package/dist/world/terrain.d.ts +5 -1
- package/dist/world/terrain.js +5 -0
- package/dist/world/territory.d.ts +97 -0
- package/dist/world/territory.js +182 -0
- package/dist/world.d.ts +5 -3
- package/dist/world.js +4 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +2 -0
- package/skills/jgengine/api.md +98 -22
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine-gameplay/SKILL.md +10 -0
- package/skills/jgengine-gameplay/api.md +40 -18
- package/skills/jgengine-gameplay/capabilities.md +12 -0
- package/skills/jgengine-world/SKILL.md +6 -0
- package/skills/jgengine-world/api.md +312 -12
- package/skills/jgengine-world/capabilities.md +102 -0
package/dist/game/chat.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { decideRateWindow } from "../time/rateWindow.js";
|
|
2
|
+
export const DEFAULT_CHAT_RATE_LIMIT = { count: 1, perMs: 2_000 };
|
|
2
3
|
export const DEFAULT_CHAT_HISTORY_LIMIT = 100;
|
|
3
|
-
export const DEFAULT_CHAT_BODY_LENGTH =
|
|
4
|
+
export const DEFAULT_CHAT_BODY_LENGTH = 240;
|
|
4
5
|
export const DEFAULT_PROXIMITY_CHAT_RADIUS = 20;
|
|
5
6
|
export const WHISPER_CHANNEL_PREFIX = "whisper:";
|
|
6
7
|
export function whisperChannelId(a, b) {
|
|
@@ -11,15 +12,17 @@ export function createChatRateLimiter(limit) {
|
|
|
11
12
|
const windows = new Map();
|
|
12
13
|
return {
|
|
13
14
|
allow(key, atMs) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
const result = decideRateWindow(windows.get(key) ?? [], atMs, { windowMs: limit.perMs, max: limit.count });
|
|
16
|
+
windows.set(key, result.timestamps);
|
|
17
|
+
return result.allowed;
|
|
18
|
+
},
|
|
19
|
+
snapshot() {
|
|
20
|
+
return Object.fromEntries(Array.from(windows, ([key, stamps]) => [key, [...stamps]]));
|
|
21
|
+
},
|
|
22
|
+
restore(state) {
|
|
23
|
+
windows.clear();
|
|
24
|
+
for (const [key, stamps] of Object.entries(state))
|
|
25
|
+
windows.set(key, [...stamps]);
|
|
23
26
|
},
|
|
24
27
|
};
|
|
25
28
|
}
|
|
@@ -93,11 +96,10 @@ export function createChat(deps) {
|
|
|
93
96
|
messagesByChannel.set(channelId, ring);
|
|
94
97
|
}
|
|
95
98
|
function sendResolved(fromUserId, channelId, body, def) {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
return { reason:
|
|
99
|
-
|
|
100
|
-
return { reason: "message too long" };
|
|
99
|
+
const validation = validateChatMessage(body, { maxLength: maxBodyLength });
|
|
100
|
+
if (!validation.ok)
|
|
101
|
+
return { reason: validation.reason };
|
|
102
|
+
const trimmed = validation.text;
|
|
101
103
|
let filteredBody = trimmed;
|
|
102
104
|
if (filter !== undefined) {
|
|
103
105
|
const filtered = filter.apply(trimmed);
|
|
@@ -136,10 +138,18 @@ export function createChat(deps) {
|
|
|
136
138
|
channels() {
|
|
137
139
|
return Array.from(channelDefs.values(), (def) => ({ ...def }));
|
|
138
140
|
},
|
|
139
|
-
send(fromUserId,
|
|
141
|
+
send(fromUserId, channelOrBody, suppliedBody) {
|
|
142
|
+
const channelId = suppliedBody === undefined ? "global" : channelOrBody;
|
|
143
|
+
const body = suppliedBody ?? channelOrBody;
|
|
140
144
|
const def = channelDefs.get(channelId) ?? null;
|
|
141
145
|
return sendResolved(fromUserId, channelId, body, def);
|
|
142
146
|
},
|
|
147
|
+
recent(options) {
|
|
148
|
+
return this.history(options?.channelId ?? "global", {
|
|
149
|
+
limit: Math.max(0, Math.min(100, Math.floor(options?.limit ?? 50) || 0)),
|
|
150
|
+
viewerUserId: options?.viewerUserId,
|
|
151
|
+
});
|
|
152
|
+
},
|
|
143
153
|
whisper(fromUserId, toUserId, body) {
|
|
144
154
|
if (fromUserId === toUserId)
|
|
145
155
|
return { reason: "cannot whisper yourself" };
|
|
@@ -153,14 +163,15 @@ export function createChat(deps) {
|
|
|
153
163
|
? ring
|
|
154
164
|
: ring.filter((message) => !hasBlocked(viewerUserId, message.fromUserId));
|
|
155
165
|
const limit = options?.limit;
|
|
156
|
-
return limit === undefined ? visible.slice() : visible.slice(-limit);
|
|
166
|
+
return limit === undefined ? visible.slice() : limit <= 0 ? [] : visible.slice(-limit);
|
|
157
167
|
},
|
|
158
168
|
snapshot() {
|
|
159
169
|
const messages = {};
|
|
160
170
|
for (const [channelId, ring] of messagesByChannel) {
|
|
161
171
|
messages[channelId] = ring.map((message) => ({ ...message }));
|
|
162
172
|
}
|
|
163
|
-
|
|
173
|
+
const rateWindows = Object.fromEntries(Array.from(limiters, ([id, limiter]) => [id, limiter.snapshot()]));
|
|
174
|
+
return { messages, counter, rateWindows };
|
|
164
175
|
},
|
|
165
176
|
hydrate(data) {
|
|
166
177
|
messagesByChannel.clear();
|
|
@@ -168,6 +179,24 @@ export function createChat(deps) {
|
|
|
168
179
|
messagesByChannel.set(channelId, ring.map((message) => ({ ...message })));
|
|
169
180
|
}
|
|
170
181
|
counter = data.counter;
|
|
182
|
+
limiters.clear();
|
|
183
|
+
for (const [channelId, state] of Object.entries(data.rateWindows ?? {})) {
|
|
184
|
+
limiterFor(channelId, channelDefs.get(channelId)?.rateLimit ?? defaultRateLimit).restore(state);
|
|
185
|
+
}
|
|
171
186
|
},
|
|
172
187
|
};
|
|
173
188
|
}
|
|
189
|
+
/** Strip control characters and enforce the shared chat message policy before storage or broadcast. */
|
|
190
|
+
export function validateChatMessage(value, options = {}) {
|
|
191
|
+
const maxLength = options.maxLength ?? DEFAULT_CHAT_BODY_LENGTH;
|
|
192
|
+
if (!Number.isSafeInteger(maxLength) || maxLength < 1)
|
|
193
|
+
throw new RangeError("Invalid chat length limit");
|
|
194
|
+
if (typeof value !== "string")
|
|
195
|
+
return { ok: false, reason: "invalid message" };
|
|
196
|
+
const text = value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "").trim();
|
|
197
|
+
if (!text.length)
|
|
198
|
+
return { ok: false, reason: "empty message" };
|
|
199
|
+
if (text.length > maxLength)
|
|
200
|
+
return { ok: false, reason: "message too long" };
|
|
201
|
+
return { ok: true, text };
|
|
202
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { GameContext } from "../runtime/gameContext.js";
|
|
2
|
+
import { type ActionContextStack } from "../input/actionContexts.js";
|
|
3
|
+
import type { ActionCodesMap } from "../input/actionBindings.js";
|
|
2
4
|
export declare const PLAY_CONTROLS_STORE_KEY = "jg.playControls";
|
|
5
|
+
/** Returns the context stack associated with a game context. */
|
|
6
|
+
export declare function actionContextStack(ctx: GameContext): ActionContextStack;
|
|
7
|
+
/** Applies active contexts to a base action map for shell input tracking. */
|
|
8
|
+
export declare function activeActionCodes(ctx: GameContext, base: ActionCodesMap): ActionCodesMap;
|
|
3
9
|
export declare function setPlayControlsActive(ctx: GameContext, active: boolean): void;
|
|
4
10
|
export declare function playControlsActive(ctx: GameContext): boolean;
|
package/dist/game/controlGate.js
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
|
+
import { createActionContextStack } from "../input/actionContexts.js";
|
|
1
2
|
export const PLAY_CONTROLS_STORE_KEY = "jg.playControls";
|
|
3
|
+
const ACTION_CONTEXTS = new WeakMap();
|
|
4
|
+
function contextsFor(ctx) {
|
|
5
|
+
let stack = ACTION_CONTEXTS.get(ctx);
|
|
6
|
+
if (stack === undefined) {
|
|
7
|
+
stack = createActionContextStack();
|
|
8
|
+
ACTION_CONTEXTS.set(ctx, stack);
|
|
9
|
+
}
|
|
10
|
+
return stack;
|
|
11
|
+
}
|
|
12
|
+
/** Returns the context stack associated with a game context. */
|
|
13
|
+
export function actionContextStack(ctx) {
|
|
14
|
+
return contextsFor(ctx);
|
|
15
|
+
}
|
|
16
|
+
/** Applies active contexts to a base action map for shell input tracking. */
|
|
17
|
+
export function activeActionCodes(ctx, base) {
|
|
18
|
+
const stack = contextsFor(ctx);
|
|
19
|
+
const layered = createActionContextStack();
|
|
20
|
+
layered.push({ id: "__jg_base_actions", codes: base, passthrough: true });
|
|
21
|
+
for (const context of stack.snapshot().contexts)
|
|
22
|
+
layered.push(context);
|
|
23
|
+
return layered.active();
|
|
24
|
+
}
|
|
2
25
|
export function setPlayControlsActive(ctx, active) {
|
|
26
|
+
const stack = contextsFor(ctx);
|
|
27
|
+
if (active)
|
|
28
|
+
stack.pop("menu");
|
|
29
|
+
else
|
|
30
|
+
stack.push({ id: "menu", codes: {}, passthrough: false });
|
|
3
31
|
ctx.game.store.set(PLAY_CONTROLS_STORE_KEY, active);
|
|
4
32
|
}
|
|
5
33
|
export function playControlsActive(ctx) {
|
|
6
|
-
return ctx.game.store.get(PLAY_CONTROLS_STORE_KEY) !== false
|
|
34
|
+
return ctx.game.store.get(PLAY_CONTROLS_STORE_KEY) !== false &&
|
|
35
|
+
!actionContextStack(ctx).snapshot().contexts.some((context) => context.id === "menu");
|
|
7
36
|
}
|
|
@@ -12,7 +12,10 @@ import { type AssetCatalog, type ModelAssetRef } from "../scene/assetCatalog.js"
|
|
|
12
12
|
import { type EntityStore } from "../scene/entityStore.js";
|
|
13
13
|
import type { StoreHandle } from "../store/defineStore.js";
|
|
14
14
|
import type { TimeConfig } from "../time/simClock.js";
|
|
15
|
+
import type { SimulationConfig } from "../runtime/simLoop.js";
|
|
15
16
|
import type { WorldFeature } from "../world/features.js";
|
|
17
|
+
import type { PhysicsBackend } from "../physics/physicsBackend.js";
|
|
18
|
+
import type { CharacterControllerConfig } from "../movement/characterController.js";
|
|
16
19
|
/** Tunes offline whole-world save (`defineGameDefinition({ persist })`). Offline games already autosave to `localStorage` by default — pass this only to tune it (or `persist: false` to opt out). Defaults: continuous `autosave` to `localStorage`, one slot, no version. */
|
|
17
20
|
export interface PersistConfig {
|
|
18
21
|
/** `"autosave"` (default) writes on a trailing timer while the world changes — at most once per `autosaveMs`, so a never-idle world (AI, physics, day/night) still persists; `"manual"` writes only on `ctx.game.save.checkpoint()` (save points / quest triggers). */
|
|
@@ -24,10 +27,14 @@ export interface PersistConfig {
|
|
|
24
27
|
/** Autosave debounce in ms (autosave mode). Default 3000. */
|
|
25
28
|
autosaveMs?: number;
|
|
26
29
|
}
|
|
27
|
-
/** World gravity and jump tuning, plus
|
|
30
|
+
/** World gravity and jump tuning, plus the optional rigid-body backend and character capsule. */
|
|
28
31
|
export interface PhysicsConfig {
|
|
29
32
|
gravity?: number;
|
|
30
33
|
jumpVelocity?: number;
|
|
34
|
+
/** Physics solver used by the shell movement controller and the context's physics stage. */
|
|
35
|
+
backend?: PhysicsBackend;
|
|
36
|
+
/** Capsule dimensions for players using the configured physics backend. */
|
|
37
|
+
controller?: CharacterControllerConfig;
|
|
31
38
|
/** Opt placed scene objects into projectile raycasts as blocking cover AABBs. Default false — enabling changes where shots land. */
|
|
32
39
|
projectileObstacles?: boolean;
|
|
33
40
|
}
|
|
@@ -180,6 +187,8 @@ export interface GameDefinition<TAssetRef extends ModelAssetRef = ModelAssetRef,
|
|
|
180
187
|
physics?: PhysicsConfig;
|
|
181
188
|
/** Simulation clock: real→game time scale, selectable speeds, calendar. Exposed as `ctx.time`; the shell feeds its scaled dt to `loop.onTick`. */
|
|
182
189
|
time?: TimeConfig;
|
|
190
|
+
/** How the simulation is stepped — a fixed `hz` with render interpolation, or `"variable"` (default) once per frame. Exposed as `ctx.sim`. */
|
|
191
|
+
simulation?: SimulationConfig;
|
|
183
192
|
/** Per-action ring-buffer capacity for `ctx.game.feed`. Default 20. */
|
|
184
193
|
feed?: GameFeedOptions;
|
|
185
194
|
inventories?: Record<string, InventoryDeclaration>;
|
package/dist/game/events.d.ts
CHANGED
|
@@ -12,11 +12,17 @@ export interface EntityDiedEvent {
|
|
|
12
12
|
position: [number, number, number];
|
|
13
13
|
serverId?: string;
|
|
14
14
|
}
|
|
15
|
-
/** Request that an entity's rig play a one-shot animation clip bound to `event` in its `animation.oneShots` (e.g. an "attack" swing); the shell resolves the clip and plays it once over the locomotion state. */
|
|
15
|
+
/** Request that an entity's rig play a one-shot animation clip bound to `event` in its `animation.oneShots` (e.g. an "attack" swing); the shell resolves the clip and plays it once over the locomotion state. With an `animation.graph`, `event` arms the trigger of that name. */
|
|
16
16
|
export interface EntityAnimationEvent {
|
|
17
17
|
instanceId: string;
|
|
18
18
|
event: string;
|
|
19
19
|
}
|
|
20
|
+
/** A named clip moment (`AnimGraph.events`) the rig just crossed — foot plant, hit frame, reload point. Audio and combat listen by `name`. */
|
|
21
|
+
export interface AnimationClipEvent {
|
|
22
|
+
instanceId: string;
|
|
23
|
+
name: string;
|
|
24
|
+
clip: string;
|
|
25
|
+
}
|
|
20
26
|
/**
|
|
21
27
|
* One granted reward: an inventory `item` id or a `currency` id, plus how many. Named because every
|
|
22
28
|
* consumer of `loot.granted` needs it — a toast renderer, a loot log, a session summary — and each
|
|
@@ -269,6 +275,7 @@ export interface GameEventMap {
|
|
|
269
275
|
"audio.loopSet": AudioLoopSetEvent;
|
|
270
276
|
"audio.loopStop": AudioLoopStopEvent;
|
|
271
277
|
"entity.animation": EntityAnimationEvent;
|
|
278
|
+
"animation.event": AnimationClipEvent;
|
|
272
279
|
}
|
|
273
280
|
export type GameEventHandler<TPayload> = (payload: TPayload) => void;
|
|
274
281
|
export interface GameEvents<TMap extends GameEventMap = GameEventMap> {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Viewport dimensions in device-independent canvas pixels. */
|
|
2
|
+
export interface PixelPerfectViewport {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
}
|
|
6
|
+
/** The orthographic bounds and scale derived from a pixel-perfect camera config. */
|
|
7
|
+
export interface PixelPerfectFrustum {
|
|
8
|
+
left: number;
|
|
9
|
+
right: number;
|
|
10
|
+
top: number;
|
|
11
|
+
bottom: number;
|
|
12
|
+
zoom: number;
|
|
13
|
+
scale: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Computes an orthographic frustum whose world units map to the requested pixel density.
|
|
17
|
+
* Integer scaling chooses the largest whole-number density multiplier available in the viewport.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pixelPerfectFrustum(viewport: PixelPerfectViewport, pixelsPerUnit: number, integerScale?: boolean): PixelPerfectFrustum;
|
|
20
|
+
/** Snaps a camera position to the authored pixel grid without changing its orientation. */
|
|
21
|
+
export declare function snapPixelPerfectPosition<T extends {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
z: number;
|
|
25
|
+
}>(position: T, pixelsPerUnit: number): T;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes an orthographic frustum whose world units map to the requested pixel density.
|
|
3
|
+
* Integer scaling chooses the largest whole-number density multiplier available in the viewport.
|
|
4
|
+
*/
|
|
5
|
+
export function pixelPerfectFrustum(viewport, pixelsPerUnit, integerScale = false) {
|
|
6
|
+
const width = Math.max(1, viewport.width);
|
|
7
|
+
const height = Math.max(1, viewport.height);
|
|
8
|
+
const density = Math.max(1e-6, pixelsPerUnit);
|
|
9
|
+
const scale = integerScale ? Math.max(1, Math.floor(Math.min(width, height) / density)) : 1;
|
|
10
|
+
const zoom = density * scale;
|
|
11
|
+
return {
|
|
12
|
+
left: -width / (2 * zoom),
|
|
13
|
+
right: width / (2 * zoom),
|
|
14
|
+
top: height / (2 * zoom),
|
|
15
|
+
bottom: -height / (2 * zoom),
|
|
16
|
+
zoom,
|
|
17
|
+
scale,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Snaps a camera position to the authored pixel grid without changing its orientation. */
|
|
21
|
+
export function snapPixelPerfectPosition(position, pixelsPerUnit) {
|
|
22
|
+
const density = Math.max(1e-6, pixelsPerUnit);
|
|
23
|
+
return {
|
|
24
|
+
...position,
|
|
25
|
+
x: Math.round(position.x * density) / density,
|
|
26
|
+
y: Math.round(position.y * density) / density,
|
|
27
|
+
z: Math.round(position.z * density) / density,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import type { AnimGraph } from "../anim/animGraph.js";
|
|
1
2
|
import type { AudioBusDef, SoundDef } from "../audio/audioFalloff.js";
|
|
2
3
|
import type { MusicTheme } from "../audio/music.js";
|
|
3
4
|
import type { EditorCatalogDefinition, EditorDocument } from "../editor/types.js";
|
|
4
5
|
import type { PostProcessingConfig } from "../render/postProcessing.js";
|
|
6
|
+
import type { EnvironmentSource } from "../render/environment.js";
|
|
7
|
+
import type { SpriteAtlas } from "../assets/spriteAtlas.js";
|
|
5
8
|
import type { LookPreset } from "../render/lookPreset.js";
|
|
6
9
|
import type { TouchControlsConfig } from "../input/touchScheme.js";
|
|
7
10
|
import type { GameSettingsConfig } from "../settings/settingsModel.js";
|
|
11
|
+
import type { GraphicsProfile } from "../settings/graphicsProfile.js";
|
|
8
12
|
import type { GameOrientation } from "../ui/orientation.js";
|
|
9
13
|
import type { HudPlatform, HudViewportConfig } from "../ui/hudScale.js";
|
|
10
14
|
import type { PositionedPrompt } from "../interaction/proximityPrompt.js";
|
|
15
|
+
import type { ChaseCameraTuning } from "../runtime/cameraDirector.js";
|
|
11
16
|
import type { CatalogEntityRole, GameContext, GameContextContent } from "../runtime/gameContext.js";
|
|
17
|
+
import type { FreeFlightBindings } from "../movement/freeFlight.js";
|
|
12
18
|
import type { ModelDims } from "../scene/assetCatalog.js";
|
|
13
19
|
import type { PartMotionParams, PartRole } from "./partAnimation.js";
|
|
14
20
|
import type { CollisionMeshData } from "../scene/collisionMesh.js";
|
|
@@ -52,6 +58,11 @@ export interface EntitySpriteConfig {
|
|
|
52
58
|
width: number;
|
|
53
59
|
height: number;
|
|
54
60
|
y: number;
|
|
61
|
+
/** Optional atlas-backed animation. The atlas image supersedes `url` when present. */
|
|
62
|
+
clip?: {
|
|
63
|
+
atlas: SpriteAtlas;
|
|
64
|
+
animation: string;
|
|
65
|
+
};
|
|
55
66
|
}
|
|
56
67
|
export interface WorldItemRenderConfig {
|
|
57
68
|
/** Baseline rarity→color/beam/label render binding (#32); the game's rarity palette. */
|
|
@@ -119,6 +130,14 @@ export interface ModelAnimationConfig {
|
|
|
119
130
|
* `string[]` picks a random variant per trigger. `death` clamps on its final frame instead of returning.
|
|
120
131
|
*/
|
|
121
132
|
oneShots?: Record<string, string | readonly string[]>;
|
|
133
|
+
/**
|
|
134
|
+
* A full animation graph (`@jgengine/core/anim/animGraph`) — layered state machine with blend trees, data
|
|
135
|
+
* transitions, triggers, and clip events. Takes over from `states`/`oneShots` when set: the shell feeds the
|
|
136
|
+
* entity's smoothed ground speed as the `speed` parameter plus anything under the entity blackboard key
|
|
137
|
+
* `ANIM_PARAMS_KEY`, arms triggers from `entity.animation` (`hit` and `death` from their combat events), and
|
|
138
|
+
* emits `animation.event` for clip events. `locomotionGraph()` builds the default idle/walk/run graph.
|
|
139
|
+
*/
|
|
140
|
+
graph?: AnimGraph;
|
|
122
141
|
}
|
|
123
142
|
/**
|
|
124
143
|
* Real PBR map URLs (e.g. `buildMaterialCatalog(...).resolve(id)!.maps` from `@jgengine/assets`)
|
|
@@ -130,8 +149,11 @@ export interface ModelMaterialMaps {
|
|
|
130
149
|
normal?: string;
|
|
131
150
|
roughness?: string;
|
|
132
151
|
ao?: string;
|
|
152
|
+
metalness?: string;
|
|
153
|
+
emissive?: string;
|
|
154
|
+
height?: string;
|
|
133
155
|
}
|
|
134
|
-
/** Per-entity PBR material override (#151.3) applied to every
|
|
156
|
+
/** Per-entity PBR material override (#151.3) applied to every standard or physical material in the model's cloned scene graph. */
|
|
135
157
|
export interface ModelMaterialOverride {
|
|
136
158
|
color?: string;
|
|
137
159
|
metalness?: number;
|
|
@@ -203,6 +225,8 @@ export interface ModelConfig {
|
|
|
203
225
|
collisionMesh?: CollisionMeshData;
|
|
204
226
|
/** Per-entity PBR tint/finish override (#151.3); cloned onto each `MeshStandardMaterial` in the model so shared GLTF caches stay untouched. */
|
|
205
227
|
material?: ModelMaterialOverride;
|
|
228
|
+
/** Shadow participation of every mesh in the model. Default `"both"` (casts and receives); `"none"` for foliage cards, decals, or viewmodels that must never shadow the world. */
|
|
229
|
+
shadows?: "cast" | "receive" | "both" | "none";
|
|
206
230
|
/**
|
|
207
231
|
* Plays a GLTF animation clip on the model when the source has any (skinned or not).
|
|
208
232
|
* A model resolved from a catalog id whose index carries `clips` animates automatically —
|
|
@@ -212,6 +236,17 @@ export interface ModelConfig {
|
|
|
212
236
|
* rig's bind pose.
|
|
213
237
|
*/
|
|
214
238
|
animation?: ModelAnimationConfig | "auto" | "none";
|
|
239
|
+
/** Optional runtime inverse-kinematics targets for a rigged model. Bone names are resolved on the loaded scene. */
|
|
240
|
+
ik?: {
|
|
241
|
+
feet: readonly {
|
|
242
|
+
root: string;
|
|
243
|
+
mid: string;
|
|
244
|
+
tip: string;
|
|
245
|
+
}[];
|
|
246
|
+
lookAt?: {
|
|
247
|
+
bone: string;
|
|
248
|
+
};
|
|
249
|
+
};
|
|
215
250
|
/** Props/weapons parented to named bones on this model's rig; each follows its bone through animation. */
|
|
216
251
|
attachments?: readonly ModelAttachment[];
|
|
217
252
|
/** Static kit-of-parts pieces stacked at fixed local offsets — no bone/rig involved. Use this for a compound entity assembled from several modular meshes (a castle keep from base + mid + roof pieces); use `attachments` for props parented to an animated rig's bones. Tag parts with a `role` to procedurally animate a rig-less character composition. */
|
|
@@ -255,10 +290,29 @@ export interface HemisphereLightingConfig {
|
|
|
255
290
|
groundColor?: string;
|
|
256
291
|
intensity?: number;
|
|
257
292
|
}
|
|
293
|
+
/** A bounded dynamic point light in world space. */
|
|
294
|
+
export interface PointLightingConfig {
|
|
295
|
+
color?: string;
|
|
296
|
+
intensity?: number;
|
|
297
|
+
position: readonly [number, number, number];
|
|
298
|
+
distance?: number;
|
|
299
|
+
decay?: number;
|
|
300
|
+
castShadow?: boolean;
|
|
301
|
+
}
|
|
302
|
+
/** A bounded dynamic spot light in world space. */
|
|
303
|
+
export interface SpotLightingConfig extends PointLightingConfig {
|
|
304
|
+
target?: readonly [number, number, number];
|
|
305
|
+
angle?: number;
|
|
306
|
+
penumbra?: number;
|
|
307
|
+
}
|
|
258
308
|
/** Declarative lighting replacing the shell's hardcoded ambient/directional default (#207.5); mounts regardless of world kind, only when supplied. */
|
|
259
309
|
export interface LightingConfig {
|
|
260
310
|
ambient?: AmbientLightingConfig;
|
|
261
311
|
directional?: readonly DirectionalLightingConfig[];
|
|
312
|
+
point?: readonly PointLightingConfig[];
|
|
313
|
+
spot?: readonly SpotLightingConfig[];
|
|
314
|
+
/** Maximum dynamic point + spot lights mounted by the shell. Default 8. */
|
|
315
|
+
maxDynamicLights?: number;
|
|
262
316
|
hemisphere?: HemisphereLightingConfig;
|
|
263
317
|
}
|
|
264
318
|
export interface BackdropFogConfig {
|
|
@@ -274,6 +328,33 @@ export interface BackdropConfig {
|
|
|
274
328
|
sky?: SkyEnvironmentConfig;
|
|
275
329
|
fog?: BackdropFogConfig;
|
|
276
330
|
}
|
|
331
|
+
/** Free-flight families moved into the walk controller so one seam covers ground + air — creative/spectator/noclip/hover are weightless, aircraft/rotorcraft stay on `flightDynamics`. */
|
|
332
|
+
export interface FlightConfig {
|
|
333
|
+
/** Which free-flight family to use when flight is active. Default "creative". */
|
|
334
|
+
mode?: "creative" | "spectator" | "noclip" | "hover";
|
|
335
|
+
/** Horizontal base speed (units/s). Default 8. */
|
|
336
|
+
speed?: number;
|
|
337
|
+
/** Vertical base speed for ascend/descend. Default = `speed`. */
|
|
338
|
+
verticalSpeed?: number;
|
|
339
|
+
/** Sprint multiplier while shift is held. Default 1.8. */
|
|
340
|
+
sprintMultiplier?: number;
|
|
341
|
+
/** Lerp rate toward target velocity. Default 20. */
|
|
342
|
+
acceleration?: number;
|
|
343
|
+
/** Downward acceleration for hover mode. Default 0 (weightless). */
|
|
344
|
+
gravity?: number;
|
|
345
|
+
/** Upward thrust while ascending in hover. Default 32. */
|
|
346
|
+
thrust?: number;
|
|
347
|
+
/** When true, W moves along the look pitch (6DOF spectator). Default false. */
|
|
348
|
+
alignWithLook?: boolean;
|
|
349
|
+
/** Whether flight slides against world solids. Default true for creative/hover, false for spectator/noclip. */
|
|
350
|
+
collide?: boolean;
|
|
351
|
+
/** Axis bindings for this flight — which actions drive each axis. Unlisted axes keep the defaults above. */
|
|
352
|
+
bindings?: FreeFlightBindings;
|
|
353
|
+
/** Camera POV while this flight is active — chase tuning overlay applied on mount, cleared on dismount. */
|
|
354
|
+
camera?: ChaseCameraTuning | null;
|
|
355
|
+
/** Gate flight behind live game state — creative toggle, stamina check, etc. `false` stays walking. Default true when `flight` is present. */
|
|
356
|
+
canFly?: (ctx: GameContext) => boolean;
|
|
357
|
+
}
|
|
277
358
|
/** Movement-control levers for the shell-driven local player walk controller. */
|
|
278
359
|
export interface PlayerMovementConfig {
|
|
279
360
|
/** "free" (default) moves camera-relative across the plane; "axis" locks travel to one world axis; "grid" snaps each committed position to cell centers. */
|
|
@@ -308,6 +389,15 @@ export interface PlayerMovementConfig {
|
|
|
308
389
|
slopeSlide?: {
|
|
309
390
|
maxClimbSlope?: number;
|
|
310
391
|
} | boolean;
|
|
392
|
+
/**
|
|
393
|
+
* Free-flight — creative/spectator/noclip/hover — folded into the same walk controller so a game
|
|
394
|
+
* doesn't hand-roll a second movement loop to get Minecraft-like flight. `true` uses creative defaults;
|
|
395
|
+
* an object tunes speed/vertical/sprint/collide/canFly. Horizontal is always yaw-relative strafe
|
|
396
|
+
* (A = left, D = right — never roll/bank), vertical is Space/Ctrl, and aircraft/rotorcraft remain
|
|
397
|
+
* on `physics/flightDynamics`. The shell drives this automatically when present; `canFly` toggles
|
|
398
|
+
* between walking and flying per tick without rebuilding the config.
|
|
399
|
+
*/
|
|
400
|
+
flight?: FlightConfig | boolean;
|
|
311
401
|
}
|
|
312
402
|
/** One frame's movement resolution handed to `PlayerMovementConfig.beforeCommit`. */
|
|
313
403
|
export interface MovementCommitFrame {
|
|
@@ -412,7 +502,7 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEnt
|
|
|
412
502
|
/** Optional canvas-layer VFX component (e.g. traveling projectiles); receives `{ ctx }` (#542) so overlay VFX read live engine state without a separate hook or a module-global workaround. */
|
|
413
503
|
WorldOverlay?: TOverlay;
|
|
414
504
|
/** Replaces the default demo backdrop (ground + grid + rocks) with the game's own scene — ground, sky, structures. Camera, input, HUD, entity rendering, and the loop stay shell-provided; supply your world without forking the shell. When unset and `game.world` is an `environment()` descriptor, the shell auto-renders that world here — no manual wiring needed. */
|
|
415
|
-
environment?: TWorldOverlay;
|
|
505
|
+
environment?: TWorldOverlay | EnvironmentSource;
|
|
416
506
|
/** The game's authored scene document (`editor.scene.json`, normalized). When set, `defineGame` always mounts `AuthoredScene` over it (draped paths, scatter, studios, placed catalog props) — any `WorldOverlay` renders alongside it as VFX only — and the embedded editor opens it as its default layers: the zero-wiring path from document to play and edit modes. */
|
|
417
507
|
editorLayers?: EditorDocument;
|
|
418
508
|
/** Game-exported gameplay catalog definitions (schemas + defaults). `GameHost` forwards them to the summoned editor's Data panel; no per-game editor bootstrap needed. */
|
|
@@ -441,6 +531,8 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEnt
|
|
|
441
531
|
capture?: GameCaptureConfig;
|
|
442
532
|
/** Cast/receive shadows across the scene (R3F Canvas shadow pass). Default true. */
|
|
443
533
|
shadows?: boolean;
|
|
534
|
+
/** Per-quality renderer budgets; omitted tiers use {@link DEFAULT_GRAPHICS_PROFILES}. */
|
|
535
|
+
graphics?: Partial<Record<"low" | "medium" | "high", Partial<GraphicsProfile>>>;
|
|
444
536
|
/** Pointer-driven input: click-to-move, box-select, right-click verbs, cursor aim (#22/#30/#31). */
|
|
445
537
|
pointer?: PointerConfig;
|
|
446
538
|
/** Touch controls on coarse-pointer devices. Unset derives a scheme from `input` (virtual joystick for movement actions, on-screen buttons for the rest); a config refines it with gestures and curated buttons; `false` opts out. */
|
|
@@ -499,10 +591,12 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEnt
|
|
|
499
591
|
collision?: VoxelCollisionConfig;
|
|
500
592
|
/** Movement-control levers (axis/grid constraints, object collision, pre-commit hook) for the shell-driven walk controller. */
|
|
501
593
|
movement?: PlayerMovementConfig;
|
|
502
|
-
/**
|
|
594
|
+
/** Graphics look profile (#1680). Unset/`"neutral"` keeps the output unstyled; `"photoreal"` and `"toon"` select authored presentation profiles, while `"cinematic"` and `"flat"` remain compatibility aliases. Explicit `lighting`/`backdrop`/`postProcessing` always wins. */
|
|
503
595
|
look?: LookPreset;
|
|
504
596
|
/** Declarative ambient/directional/hemisphere lighting (#207.5); replaces the shell's hardcoded default lights when present, regardless of world kind. */
|
|
505
597
|
lighting?: LightingConfig;
|
|
598
|
+
/** Image-based environment source; omit to use the procedural gradient probe. */
|
|
599
|
+
environmentSource?: EnvironmentSource;
|
|
506
600
|
/** Generic background/sky/fog (#207.6), applied for any world kind including a custom `environment` component. */
|
|
507
601
|
backdrop?: BackdropConfig;
|
|
508
602
|
/** Declarative post-processing chain (AO/bloom/tone-map/grade). When set, the shell mounts an EffectComposer and owns the render; absent leaves the renderer drawing directly (unchanged). */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compileSystemSchedule, } from "./systemSchedule.js";
|
|
2
|
+
const MAX_FIXED_SUBSTEPS = 8;
|
|
2
3
|
function resolveModule(spec, ctx) {
|
|
3
4
|
if (spec === undefined)
|
|
4
5
|
return undefined;
|
|
@@ -56,10 +57,14 @@ export function installSystems(ctx, systems, options) {
|
|
|
56
57
|
for (const group of schedule.fixed) {
|
|
57
58
|
const step = 1 / group.rate;
|
|
58
59
|
let acc = (fixedAcc.get(group.rate) ?? 0) + dt;
|
|
59
|
-
|
|
60
|
+
let substeps = 0;
|
|
61
|
+
while (acc >= step && substeps < MAX_FIXED_SUBSTEPS) {
|
|
60
62
|
runIds(ctx, group.order, step);
|
|
61
63
|
acc -= step;
|
|
64
|
+
substeps += 1;
|
|
62
65
|
}
|
|
66
|
+
if (acc >= step)
|
|
67
|
+
acc %= step;
|
|
63
68
|
fixedAcc.set(group.rate, acc);
|
|
64
69
|
}
|
|
65
70
|
runIds(ctx, schedule.frameOrder, dt);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type GamepadGlyphName, type GamepadGlyphSet } from "./gamepadModel.js";
|
|
1
2
|
/**
|
|
2
3
|
* Action-binding model: games bind semantic actions ("jump", "interact") to
|
|
3
4
|
* physical control codes; capture layers resolve raw events through this map
|
|
@@ -69,7 +70,7 @@ export declare function resolveActionCommand(action: string, has: (command: stri
|
|
|
69
70
|
/** Short display label for a raw key/button code (e.g. `"KeyW"` → `"W"`).
|
|
70
71
|
* @internal
|
|
71
72
|
*/
|
|
72
|
-
export declare function bindingLabel(code: string): string;
|
|
73
|
+
export declare function bindingLabel(code: string, glyphSet?: GamepadGlyphName | GamepadGlyphSet): string;
|
|
73
74
|
/** Short display label for an action's first bound code, or null when unbound.
|
|
74
75
|
* @internal
|
|
75
76
|
*/
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Action-binding model: games bind semantic actions ("jump", "interact") to
|
|
3
|
-
* physical control codes; capture layers resolve raw events through this map
|
|
4
|
-
* so gameplay code never sees keycodes. Control codes are plain strings, so
|
|
5
|
-
* the same model serves keyboard codes, mouse buttons, touch controls, or
|
|
6
|
-
* gamepad inputs.
|
|
7
|
-
*/
|
|
1
|
+
import { GAMEPAD_GLYPH_SETS } from "./gamepadModel.js";
|
|
8
2
|
/** @internal */
|
|
9
3
|
export function bindingMatches(code, binding) {
|
|
10
4
|
return code === binding.primary || code === binding.secondary;
|
|
@@ -142,7 +136,15 @@ const CODE_LABELS = {
|
|
|
142
136
|
/** Short display label for a raw key/button code (e.g. `"KeyW"` → `"W"`).
|
|
143
137
|
* @internal
|
|
144
138
|
*/
|
|
145
|
-
export function bindingLabel(code) {
|
|
139
|
+
export function bindingLabel(code, glyphSet = "generic") {
|
|
140
|
+
const padButton = /^pad:(\d+)$/.exec(code);
|
|
141
|
+
if (padButton !== null) {
|
|
142
|
+
const set = typeof glyphSet === "string" ? GAMEPAD_GLYPH_SETS[glyphSet] : glyphSet;
|
|
143
|
+
return set.buttons[Number(padButton[1])] ?? `Button ${padButton[1]}`;
|
|
144
|
+
}
|
|
145
|
+
const padAxis = /^padaxis:(\d+)([+-])$/.exec(code);
|
|
146
|
+
if (padAxis !== null)
|
|
147
|
+
return `Axis ${padAxis[1]}${padAxis[2]}`;
|
|
146
148
|
const direct = CODE_LABELS[code];
|
|
147
149
|
if (direct !== undefined)
|
|
148
150
|
return direct;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ActionCodesMap } from "./actionBindings.js";
|
|
2
|
+
/** Named action-binding layer that may optionally expose lower layers. */
|
|
3
|
+
export interface ActionContext {
|
|
4
|
+
id: string;
|
|
5
|
+
codes: ActionCodesMap;
|
|
6
|
+
passthrough: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Serializable state for an action-context stack. */
|
|
9
|
+
export interface ActionContextStackSnapshot {
|
|
10
|
+
contexts: ActionContext[];
|
|
11
|
+
}
|
|
12
|
+
/** Mutable layered action-map stack with snapshot and restore support. */
|
|
13
|
+
export interface ActionContextStack {
|
|
14
|
+
push(context: ActionContext): void;
|
|
15
|
+
pop(id: string): boolean;
|
|
16
|
+
active(): ActionCodesMap;
|
|
17
|
+
snapshot(): ActionContextStackSnapshot;
|
|
18
|
+
restore(snapshot: ActionContextStackSnapshot): void;
|
|
19
|
+
}
|
|
20
|
+
/** Creates a serializable stack of layered action maps for menus and gameplay modes. */
|
|
21
|
+
export declare function createActionContextStack(): ActionContextStack;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
function cloneCodes(codes) {
|
|
2
|
+
const result = {};
|
|
3
|
+
for (const [action, value] of Object.entries(codes)) {
|
|
4
|
+
const modes = value;
|
|
5
|
+
result[action] = Array.isArray(value)
|
|
6
|
+
? [...value]
|
|
7
|
+
: {
|
|
8
|
+
...(modes.hold === undefined ? {} : { hold: [...modes.hold] }),
|
|
9
|
+
...(modes.toggle === undefined ? {} : { toggle: [...modes.toggle] }),
|
|
10
|
+
...(modes.repeatMs === undefined ? {} : { repeatMs: modes.repeatMs }),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
/** Creates a serializable stack of layered action maps for menus and gameplay modes. */
|
|
16
|
+
export function createActionContextStack() {
|
|
17
|
+
let contexts = [];
|
|
18
|
+
return {
|
|
19
|
+
push(context) {
|
|
20
|
+
contexts = [...contexts.filter((current) => current.id !== context.id), {
|
|
21
|
+
id: context.id,
|
|
22
|
+
codes: cloneCodes(context.codes),
|
|
23
|
+
passthrough: context.passthrough,
|
|
24
|
+
}];
|
|
25
|
+
},
|
|
26
|
+
pop(id) {
|
|
27
|
+
const next = contexts.filter((context) => context.id !== id);
|
|
28
|
+
const changed = next.length !== contexts.length;
|
|
29
|
+
contexts = next;
|
|
30
|
+
return changed;
|
|
31
|
+
},
|
|
32
|
+
active() {
|
|
33
|
+
const merged = {};
|
|
34
|
+
for (let index = contexts.length - 1; index >= 0; index -= 1) {
|
|
35
|
+
const context = contexts[index];
|
|
36
|
+
for (const [action, codes] of Object.entries(context.codes)) {
|
|
37
|
+
if (merged[action] === undefined)
|
|
38
|
+
merged[action] = codes;
|
|
39
|
+
}
|
|
40
|
+
if (!context.passthrough)
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
return merged;
|
|
44
|
+
},
|
|
45
|
+
snapshot() {
|
|
46
|
+
return { contexts: contexts.map((context) => ({ ...context, codes: cloneCodes(context.codes) })) };
|
|
47
|
+
},
|
|
48
|
+
restore(snapshot) {
|
|
49
|
+
contexts = snapshot.contexts.map((context) => ({
|
|
50
|
+
id: context.id,
|
|
51
|
+
codes: cloneCodes(context.codes),
|
|
52
|
+
passthrough: context.passthrough,
|
|
53
|
+
}));
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|