@jgengine/core 0.27.0 → 0.28.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 +130 -0
- package/dist/ai/policeResponse.d.ts +253 -0
- package/dist/ai/policeResponse.js +421 -0
- package/dist/anim/actionLayer.d.ts +47 -0
- package/dist/anim/actionLayer.js +63 -0
- package/dist/anim/ikSolver.d.ts +14 -0
- package/dist/anim/ikSolver.js +37 -0
- package/dist/anim/locomotionGraph.d.ts +58 -3
- package/dist/anim/locomotionGraph.js +142 -15
- package/dist/anim/ragdollRig.d.ts +53 -0
- package/dist/anim/ragdollRig.js +132 -0
- package/dist/anim/skeletonStandard.d.ts +140 -0
- package/dist/anim/skeletonStandard.js +326 -0
- package/dist/combat/damageRuntime.d.ts +14 -1
- package/dist/combat/damageRuntime.js +62 -4
- package/dist/combat/death.js +7 -3
- package/dist/combat/deathReason.d.ts +7 -1
- package/dist/combat/effects.d.ts +25 -0
- package/dist/combat/effects.js +16 -0
- package/dist/combat/statusRuntime.d.ts +2 -0
- package/dist/combat/statusRuntime.js +1 -0
- package/dist/economy/loanBook.d.ts +182 -0
- package/dist/economy/loanBook.js +303 -0
- package/dist/economy/market.d.ts +161 -0
- package/dist/economy/market.js +248 -0
- package/dist/economy/transactionLedger.d.ts +162 -0
- package/dist/economy/transactionLedger.js +301 -0
- package/dist/editor/creatorStorage.js +3 -1
- package/dist/editor/simulation.d.ts +5 -2
- package/dist/editor/simulation.js +29 -19
- package/dist/game/cameraConfig.d.ts +14 -0
- package/dist/game/clipRoles.d.ts +5 -4
- package/dist/game/clipRoles.js +34 -4
- package/dist/game/events.d.ts +27 -0
- package/dist/game/gameSave.d.ts +142 -0
- package/dist/game/gameSave.js +174 -0
- package/dist/game/playableGame.d.ts +55 -4
- package/dist/game/saveSlotIndex.d.ts +54 -0
- package/dist/game/saveSlotIndex.js +71 -0
- package/dist/game/saveSlots.d.ts +2 -0
- package/dist/game/saveSlots.js +1 -1
- package/dist/game/saveStore.d.ts +8 -0
- package/dist/game/saveStore.js +16 -0
- package/dist/gameplay.d.ts +12 -2
- package/dist/gameplay.js +10 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +120 -1
- package/dist/mission/compositions.d.ts +85 -0
- package/dist/mission/compositions.js +130 -0
- package/dist/mission/mission.d.ts +435 -0
- package/dist/mission/mission.js +770 -0
- package/dist/mission/triggers.d.ts +61 -0
- package/dist/mission/triggers.js +108 -0
- package/dist/nav/traffic.d.ts +0 -1
- package/dist/nav/traffic.js +1 -2
- package/dist/runtime/context/combat.js +11 -0
- package/dist/settings/accessibilitySettings.d.ts +81 -0
- package/dist/settings/accessibilitySettings.js +152 -0
- package/dist/settings/graphicsProfile.d.ts +3 -0
- package/dist/settings/graphicsProfile.js +4 -0
- package/dist/settings/settingsModel.d.ts +1 -1
- package/dist/settings/settingsModel.js +1 -0
- package/dist/sim/nearAgents.d.ts +8 -1
- package/dist/sim/nearAgents.js +34 -17
- package/dist/sim/offScreen.d.ts +2 -1
- package/dist/sim/offScreen.js +14 -3
- package/dist/sim/population.d.ts +147 -0
- package/dist/sim/population.js +182 -0
- package/dist/sim/queue.d.ts +90 -0
- package/dist/sim/queue.js +171 -0
- package/dist/sim/rides.d.ts +287 -0
- package/dist/sim/rides.js +471 -0
- package/dist/sim/routine.js +19 -11
- package/dist/ui/loadingTips.d.ts +41 -0
- package/dist/ui/loadingTips.js +64 -0
- package/dist/ui/menuNav.d.ts +79 -0
- package/dist/ui/menuNav.js +147 -0
- package/dist/ui/menuTransition.d.ts +50 -0
- package/dist/ui/menuTransition.js +67 -0
- package/dist/ui.d.ts +2 -0
- package/dist/ui.js +2 -0
- package/dist/vfx/effectLibrary.d.ts +98 -0
- package/dist/vfx/effectLibrary.js +181 -0
- package/dist/vfx/particleBudget.d.ts +40 -0
- package/dist/vfx/particleBudget.js +39 -0
- package/dist/vfx/particleDirector.d.ts +16 -0
- package/dist/vfx/particleDirector.js +18 -0
- package/dist/world/authoredSimulation.js +7 -1
- package/dist/world/captureCamera.d.ts +30 -0
- package/dist/world/captureCamera.js +19 -0
- package/dist/world/features.d.ts +15 -4
- package/dist/world/terrain.d.ts +1 -0
- package/dist/world/terrain.js +2 -0
- package/dist/world.d.ts +2 -1
- package/dist/world.js +1 -0
- package/package.json +1 -1
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-combat/SKILL.md +6 -0
- package/skills/jgengine-combat/api.md +5 -4
- package/skills/jgengine-combat/reference.md +4 -0
- package/skills/jgengine-gameplay/SKILL.md +55 -0
- package/skills/jgengine-gameplay/api.md +335 -10
- package/skills/jgengine-gameplay/capabilities.md +83 -1
- package/skills/jgengine-world/SKILL.md +4 -0
- package/skills/jgengine-world/api.md +128 -6
- package/skills/jgengine-world/capabilities.md +41 -1
- package/skills/jgengine-world/recipes/character-animation.md +25 -2
- package/skills/jgengine-world/reference.md +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Missions driven from authored scene markers. A designer places a marker (or volume) in the editor
|
|
3
|
+
* and sets one of the registered mission actions in the inspector — `mission_start`,
|
|
4
|
+
* `mission_objective` or `mission_fail` — and the runtime starts, advances or fails the mission when
|
|
5
|
+
* an actor trips it. Nothing about the marker is mission-specific code; it is the shared authored
|
|
6
|
+
* trigger vocabulary, plus the marker positions a positional objective reads.
|
|
7
|
+
*/
|
|
8
|
+
import { type AuthoredTrigger, type TriggerActionDefinition, type TriggerDispatchEvent } from "../scene/authoredTriggers.js";
|
|
9
|
+
import type { SceneDocumentLike, ScenePoint3 } from "../world/sceneShapes.js";
|
|
10
|
+
import type { MissionRuntime } from "./mission.js";
|
|
11
|
+
/**
|
|
12
|
+
* Marker positions from an authored scene, keyed by marker id — what a positional objective reads.
|
|
13
|
+
*
|
|
14
|
+
* @capability mission-triggers read authored scene marker positions for positional objectives
|
|
15
|
+
*/
|
|
16
|
+
export declare function missionMarkers(document: SceneDocumentLike): Record<string, ScenePoint3>;
|
|
17
|
+
/**
|
|
18
|
+
* The mission actions a game registers so the editor inspector can assign them to a marker or volume.
|
|
19
|
+
* Params land flat on the object's `meta` beside `on`/`action`, like any authored trigger.
|
|
20
|
+
*
|
|
21
|
+
* @capability mission-triggers start, advance and fail a mission from authored scene markers
|
|
22
|
+
*/
|
|
23
|
+
export declare const MISSION_TRIGGER_ACTIONS: readonly TriggerActionDefinition[];
|
|
24
|
+
/**
|
|
25
|
+
* Register {@link MISSION_TRIGGER_ACTIONS} so the editor lists them. Idempotent; call at module load
|
|
26
|
+
* next to the game's catalogs.
|
|
27
|
+
*
|
|
28
|
+
* @capability mission-triggers start, advance and fail a mission from authored scene markers
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerMissionTriggerActions(): void;
|
|
31
|
+
/** Watches a document's authored mission triggers against moving actors. */
|
|
32
|
+
export interface MissionTriggerBridge {
|
|
33
|
+
/** Advance one tick; fires the mission actions the actors tripped. */
|
|
34
|
+
step(input: {
|
|
35
|
+
actors: readonly {
|
|
36
|
+
id: string;
|
|
37
|
+
position: readonly [number, number, number] | {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
z: number;
|
|
41
|
+
};
|
|
42
|
+
}[];
|
|
43
|
+
interact?: readonly string[];
|
|
44
|
+
}): readonly TriggerDispatchEvent[];
|
|
45
|
+
/** The mission triggers this bridge was built from. */
|
|
46
|
+
triggers(): readonly AuthoredTrigger[];
|
|
47
|
+
/** Drop membership so the next step re-fires enters for still-inside actors. */
|
|
48
|
+
reset(): void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Wire authored markers to a {@link MissionRuntime}: an actor entering a `mission_start` marker starts
|
|
52
|
+
* the named mission, `mission_objective` completes the named objective, and `mission_fail` lands the
|
|
53
|
+
* named fail state. Pure membership math over the document, exactly like any other authored trigger.
|
|
54
|
+
*
|
|
55
|
+
* @capability mission-triggers start, advance and fail a mission from authored scene markers
|
|
56
|
+
*/
|
|
57
|
+
export declare function createMissionTriggerBridge(options: {
|
|
58
|
+
runtime: MissionRuntime;
|
|
59
|
+
document: SceneDocumentLike;
|
|
60
|
+
triggers?: readonly AuthoredTrigger[];
|
|
61
|
+
}): MissionTriggerBridge;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Missions driven from authored scene markers. A designer places a marker (or volume) in the editor
|
|
3
|
+
* and sets one of the registered mission actions in the inspector — `mission_start`,
|
|
4
|
+
* `mission_objective` or `mission_fail` — and the runtime starts, advances or fails the mission when
|
|
5
|
+
* an actor trips it. Nothing about the marker is mission-specific code; it is the shared authored
|
|
6
|
+
* trigger vocabulary, plus the marker positions a positional objective reads.
|
|
7
|
+
*/
|
|
8
|
+
import { createAuthoredTriggerRuntime, registerTriggerAction, } from "../scene/authoredTriggers.js";
|
|
9
|
+
/**
|
|
10
|
+
* Marker positions from an authored scene, keyed by marker id — what a positional objective reads.
|
|
11
|
+
*
|
|
12
|
+
* @capability mission-triggers read authored scene marker positions for positional objectives
|
|
13
|
+
*/
|
|
14
|
+
export function missionMarkers(document) {
|
|
15
|
+
const out = {};
|
|
16
|
+
for (const marker of document.markers) {
|
|
17
|
+
out[marker.id] = { x: marker.position.x, y: marker.position.y, z: marker.position.z };
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The mission actions a game registers so the editor inspector can assign them to a marker or volume.
|
|
23
|
+
* Params land flat on the object's `meta` beside `on`/`action`, like any authored trigger.
|
|
24
|
+
*
|
|
25
|
+
* @capability mission-triggers start, advance and fail a mission from authored scene markers
|
|
26
|
+
*/
|
|
27
|
+
export const MISSION_TRIGGER_ACTIONS = [
|
|
28
|
+
{
|
|
29
|
+
id: "mission_start",
|
|
30
|
+
label: "Start mission",
|
|
31
|
+
schema: { fields: [{ type: "text", key: "mission", label: "Mission", default: "" }] },
|
|
32
|
+
events: ["enter"],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "mission_objective",
|
|
36
|
+
label: "Complete mission objective",
|
|
37
|
+
schema: {
|
|
38
|
+
fields: [
|
|
39
|
+
{ type: "text", key: "mission", label: "Mission", default: "" },
|
|
40
|
+
{ type: "text", key: "objective", label: "Objective", default: "" },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
events: ["enter", "interact"],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "mission_fail",
|
|
47
|
+
label: "Fail mission",
|
|
48
|
+
schema: {
|
|
49
|
+
fields: [
|
|
50
|
+
{ type: "text", key: "mission", label: "Mission", default: "" },
|
|
51
|
+
{ type: "text", key: "state", label: "Fail state", default: "" },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
events: ["enter", "interact"],
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* Register {@link MISSION_TRIGGER_ACTIONS} so the editor lists them. Idempotent; call at module load
|
|
59
|
+
* next to the game's catalogs.
|
|
60
|
+
*
|
|
61
|
+
* @capability mission-triggers start, advance and fail a mission from authored scene markers
|
|
62
|
+
*/
|
|
63
|
+
export function registerMissionTriggerActions() {
|
|
64
|
+
for (const action of MISSION_TRIGGER_ACTIONS)
|
|
65
|
+
registerTriggerAction(action);
|
|
66
|
+
}
|
|
67
|
+
function textParam(event, key) {
|
|
68
|
+
const value = event.params[key];
|
|
69
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Wire authored markers to a {@link MissionRuntime}: an actor entering a `mission_start` marker starts
|
|
73
|
+
* the named mission, `mission_objective` completes the named objective, and `mission_fail` lands the
|
|
74
|
+
* named fail state. Pure membership math over the document, exactly like any other authored trigger.
|
|
75
|
+
*
|
|
76
|
+
* @capability mission-triggers start, advance and fail a mission from authored scene markers
|
|
77
|
+
*/
|
|
78
|
+
export function createMissionTriggerBridge(options) {
|
|
79
|
+
const handlers = {
|
|
80
|
+
mission_start: (event) => {
|
|
81
|
+
const mission = textParam(event, "mission");
|
|
82
|
+
if (mission !== undefined)
|
|
83
|
+
options.runtime.start(mission);
|
|
84
|
+
},
|
|
85
|
+
mission_objective: (event) => {
|
|
86
|
+
const mission = textParam(event, "mission");
|
|
87
|
+
const objective = textParam(event, "objective");
|
|
88
|
+
if (mission !== undefined && objective !== undefined)
|
|
89
|
+
options.runtime.signal(mission, objective);
|
|
90
|
+
},
|
|
91
|
+
mission_fail: (event) => {
|
|
92
|
+
const mission = textParam(event, "mission");
|
|
93
|
+
const state = textParam(event, "state");
|
|
94
|
+
if (mission !== undefined && state !== undefined)
|
|
95
|
+
options.runtime.fail(mission, state);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
const runtime = createAuthoredTriggerRuntime({
|
|
99
|
+
document: options.document,
|
|
100
|
+
handlers,
|
|
101
|
+
triggers: options.triggers,
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
step: (input) => runtime.step(input),
|
|
105
|
+
triggers: () => runtime.triggers(),
|
|
106
|
+
reset: () => runtime.reset(),
|
|
107
|
+
};
|
|
108
|
+
}
|
package/dist/nav/traffic.d.ts
CHANGED
|
@@ -151,7 +151,6 @@ export interface TrafficSnapshot {
|
|
|
151
151
|
ticks: number;
|
|
152
152
|
maxStillSeconds: number;
|
|
153
153
|
maxDeadlockSeconds: number;
|
|
154
|
-
slowestTickMs: number;
|
|
155
154
|
};
|
|
156
155
|
}
|
|
157
156
|
/** Everything {@link createTraffic} needs: the graph, the injected generator and the caller's policies. */
|
package/dist/nav/traffic.js
CHANGED
|
@@ -861,7 +861,6 @@ export function createTraffic(options) {
|
|
|
861
861
|
ticks,
|
|
862
862
|
maxStillSeconds,
|
|
863
863
|
maxDeadlockSeconds,
|
|
864
|
-
slowestTickMs,
|
|
865
864
|
},
|
|
866
865
|
};
|
|
867
866
|
},
|
|
@@ -883,7 +882,7 @@ export function createTraffic(options) {
|
|
|
883
882
|
ticks = next.counters.ticks;
|
|
884
883
|
maxStillSeconds = next.counters.maxStillSeconds;
|
|
885
884
|
maxDeadlockSeconds = next.counters.maxDeadlockSeconds;
|
|
886
|
-
slowestTickMs =
|
|
885
|
+
slowestTickMs = 0;
|
|
887
886
|
for (const saved of next.cars) {
|
|
888
887
|
const car = {
|
|
889
888
|
...saved,
|
|
@@ -59,6 +59,17 @@ export function createCombatSubsystem(d) {
|
|
|
59
59
|
const userId = userIdOf(instanceId);
|
|
60
60
|
return { catalogId: entity.name, name: entity.name, ...(userId === undefined ? {} : { userId }) };
|
|
61
61
|
},
|
|
62
|
+
onDamage(application) {
|
|
63
|
+
events.emit("combat.damage", {
|
|
64
|
+
target: application.target,
|
|
65
|
+
source: application.source,
|
|
66
|
+
type: application.effect,
|
|
67
|
+
amount: application.dealt,
|
|
68
|
+
absorbed: Math.max(0, application.incoming - application.dealt),
|
|
69
|
+
remaining: application.remaining,
|
|
70
|
+
lethal: application.lethal,
|
|
71
|
+
});
|
|
72
|
+
},
|
|
62
73
|
onLethal(instanceId, lethalCtx) {
|
|
63
74
|
// Capture identity + onDeath *before* resolveDeath despawns the entity.
|
|
64
75
|
const dyingEntity = entities.get(instanceId);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { AccessibilityState, ColorblindMode } from "../ui/accessibility.js";
|
|
2
|
+
import type { GameSettingDef, SettingOption, SettingValue, SettingsStore } from "./settingsModel.js";
|
|
3
|
+
/**
|
|
4
|
+
* A "hold or toggle" preference for actions that can be either (aim, sprint, crouch, block). Shared
|
|
5
|
+
* because players expect one setting to cover them all, rather than each game inventing a flag.
|
|
6
|
+
*/
|
|
7
|
+
export type HoldVsToggle = "hold" | "toggle";
|
|
8
|
+
/** The persisted settings ids behind the accessibility section. */
|
|
9
|
+
export declare const ACCESSIBILITY_SETTING_IDS: {
|
|
10
|
+
readonly textScale: "accessibility.textScale";
|
|
11
|
+
readonly colorblind: "accessibility.colorblind";
|
|
12
|
+
readonly reducedMotion: "accessibility.reducedMotion";
|
|
13
|
+
readonly highContrast: "accessibility.highContrast";
|
|
14
|
+
readonly captions: "accessibility.captions";
|
|
15
|
+
readonly holdVsToggle: "controls.holdVsToggle";
|
|
16
|
+
};
|
|
17
|
+
/** Text-scale choices, as numeric strings the settings store persists. */
|
|
18
|
+
export declare const TEXT_SCALE_OPTIONS: readonly SettingOption[];
|
|
19
|
+
/** Colorblind palette choices. `none` keeps the game's own colours. */
|
|
20
|
+
export declare const COLORBLIND_OPTIONS: readonly SettingOption[];
|
|
21
|
+
/** Hold-versus-toggle choices for actions that support both. */
|
|
22
|
+
export declare const HOLD_VS_TOGGLE_OPTIONS: readonly SettingOption[];
|
|
23
|
+
/** Read a stored text scale, clamped to the supported range. */
|
|
24
|
+
export declare function readTextScale(store: SettingsStore): number;
|
|
25
|
+
/** Read a stored colorblind mode, falling back to `none` for an unknown value. */
|
|
26
|
+
export declare function readColorblind(store: SettingsStore): ColorblindMode;
|
|
27
|
+
/** Read the hold-or-toggle preference, falling back to `hold`. */
|
|
28
|
+
export declare function readHoldVsToggle(store: SettingsStore): HoldVsToggle;
|
|
29
|
+
/**
|
|
30
|
+
* Read the whole accessibility state the presentation needs (reduced motion, high contrast, text
|
|
31
|
+
* scale, colorblind mode, captions) plus the hold/toggle preference, from the shared settings store.
|
|
32
|
+
*
|
|
33
|
+
* @capability accessibility-settings read accessibility preferences and the hold-vs-toggle choice from the shared settings store
|
|
34
|
+
*/
|
|
35
|
+
export declare function readAccessibilitySettings(store: SettingsStore): {
|
|
36
|
+
state: AccessibilityState;
|
|
37
|
+
holdVsToggle: HoldVsToggle;
|
|
38
|
+
};
|
|
39
|
+
/** One accessibility setting as the shared settings menu renders it. */
|
|
40
|
+
export interface AccessibilitySettingRow {
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
kind: "slider" | "toggle" | "select";
|
|
44
|
+
value: SettingValue;
|
|
45
|
+
min?: number;
|
|
46
|
+
max?: number;
|
|
47
|
+
step?: number;
|
|
48
|
+
options?: readonly SettingOption[];
|
|
49
|
+
format?: (value: number) => string;
|
|
50
|
+
set: (value: SettingValue) => void;
|
|
51
|
+
/** When this row drives presentation, the accessibility state key it maps to. */
|
|
52
|
+
affects?: keyof AccessibilityState;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The accessibility rows, wired to the settings store: text scale, colorblind palette, reduced
|
|
56
|
+
* motion, high contrast, captions and hold-vs-toggle. The shell adds them as its `accessibility`
|
|
57
|
+
* category and any game can render them inside its own settings layout.
|
|
58
|
+
*
|
|
59
|
+
* @capability accessibility-settings-rows accessibility setting rows (text scale, colorblind, reduced motion, high contrast, captions, hold-vs-toggle) backed by the shared settings store
|
|
60
|
+
*/
|
|
61
|
+
export declare function accessibilitySettingRows(store: SettingsStore): AccessibilitySettingRow[];
|
|
62
|
+
/**
|
|
63
|
+
* Declarative form of the accessibility rows for `defineGame({ settings })`, for a game that wants
|
|
64
|
+
* them inside its own setting categories rather than the built-in `accessibility` one.
|
|
65
|
+
*/
|
|
66
|
+
export declare function accessibilitySettingDefs(category?: string): GameSettingDef[];
|
|
67
|
+
/** One frame of an action's raw input. */
|
|
68
|
+
export interface HoldToggleInput {
|
|
69
|
+
/** The action's key/button went down this frame (the edge). */
|
|
70
|
+
pressed: boolean;
|
|
71
|
+
/** The action's key/button is down. */
|
|
72
|
+
held: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The action's active state for one frame under a {@link HoldVsToggle} preference: `hold` follows the
|
|
76
|
+
* button, `toggle` latches on each press edge. Call with the previous result so toggle mode can flip
|
|
77
|
+
* without the caller keeping a second flag.
|
|
78
|
+
*
|
|
79
|
+
* @capability hold-vs-toggle resolve one action frame under a hold-or-toggle preference
|
|
80
|
+
*/
|
|
81
|
+
export declare function holdToggleActive(previous: boolean, input: HoldToggleInput, mode: HoldVsToggle): boolean;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { DEFAULT_ACCESSIBILITY, clampTextScale } from "../ui/accessibility.js";
|
|
2
|
+
/** The persisted settings ids behind the accessibility section. */
|
|
3
|
+
export const ACCESSIBILITY_SETTING_IDS = {
|
|
4
|
+
textScale: "accessibility.textScale",
|
|
5
|
+
colorblind: "accessibility.colorblind",
|
|
6
|
+
reducedMotion: "accessibility.reducedMotion",
|
|
7
|
+
highContrast: "accessibility.highContrast",
|
|
8
|
+
captions: "accessibility.captions",
|
|
9
|
+
holdVsToggle: "controls.holdVsToggle",
|
|
10
|
+
};
|
|
11
|
+
/** Text-scale choices, as numeric strings the settings store persists. */
|
|
12
|
+
export const TEXT_SCALE_OPTIONS = [
|
|
13
|
+
{ value: "1", label: "100%" },
|
|
14
|
+
{ value: "1.15", label: "115%" },
|
|
15
|
+
{ value: "1.3", label: "130%" },
|
|
16
|
+
{ value: "1.5", label: "150%" },
|
|
17
|
+
{ value: "1.75", label: "175%" },
|
|
18
|
+
{ value: "2", label: "200%" },
|
|
19
|
+
];
|
|
20
|
+
/** Colorblind palette choices. `none` keeps the game's own colours. */
|
|
21
|
+
export const COLORBLIND_OPTIONS = [
|
|
22
|
+
{ value: "none", label: "Off" },
|
|
23
|
+
{ value: "protanopia", label: "Protanopia" },
|
|
24
|
+
{ value: "deuteranopia", label: "Deuteranopia" },
|
|
25
|
+
{ value: "tritanopia", label: "Tritanopia" },
|
|
26
|
+
{ value: "grayscale", label: "Grayscale" },
|
|
27
|
+
];
|
|
28
|
+
/** Hold-versus-toggle choices for actions that support both. */
|
|
29
|
+
export const HOLD_VS_TOGGLE_OPTIONS = [
|
|
30
|
+
{ value: "hold", label: "Hold" },
|
|
31
|
+
{ value: "toggle", label: "Toggle" },
|
|
32
|
+
];
|
|
33
|
+
/** Read a stored text scale, clamped to the supported range. */
|
|
34
|
+
export function readTextScale(store) {
|
|
35
|
+
return clampTextScale(Number(store.get(ACCESSIBILITY_SETTING_IDS.textScale, 1)));
|
|
36
|
+
}
|
|
37
|
+
/** Read a stored colorblind mode, falling back to `none` for an unknown value. */
|
|
38
|
+
export function readColorblind(store) {
|
|
39
|
+
const value = String(store.get(ACCESSIBILITY_SETTING_IDS.colorblind, "none"));
|
|
40
|
+
const known = COLORBLIND_OPTIONS.some((option) => option.value === value);
|
|
41
|
+
return known ? value : "none";
|
|
42
|
+
}
|
|
43
|
+
/** Read the hold-or-toggle preference, falling back to `hold`. */
|
|
44
|
+
export function readHoldVsToggle(store) {
|
|
45
|
+
const value = String(store.get(ACCESSIBILITY_SETTING_IDS.holdVsToggle, "hold"));
|
|
46
|
+
return value === "toggle" ? "toggle" : "hold";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read the whole accessibility state the presentation needs (reduced motion, high contrast, text
|
|
50
|
+
* scale, colorblind mode, captions) plus the hold/toggle preference, from the shared settings store.
|
|
51
|
+
*
|
|
52
|
+
* @capability accessibility-settings read accessibility preferences and the hold-vs-toggle choice from the shared settings store
|
|
53
|
+
*/
|
|
54
|
+
export function readAccessibilitySettings(store) {
|
|
55
|
+
return {
|
|
56
|
+
state: {
|
|
57
|
+
reducedMotion: Boolean(store.get(ACCESSIBILITY_SETTING_IDS.reducedMotion, DEFAULT_ACCESSIBILITY.reducedMotion)),
|
|
58
|
+
highContrast: Boolean(store.get(ACCESSIBILITY_SETTING_IDS.highContrast, DEFAULT_ACCESSIBILITY.highContrast)),
|
|
59
|
+
textScale: readTextScale(store),
|
|
60
|
+
colorblind: readColorblind(store),
|
|
61
|
+
captions: Boolean(store.get(ACCESSIBILITY_SETTING_IDS.captions, DEFAULT_ACCESSIBILITY.captions)),
|
|
62
|
+
},
|
|
63
|
+
holdVsToggle: readHoldVsToggle(store),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The accessibility rows, wired to the settings store: text scale, colorblind palette, reduced
|
|
68
|
+
* motion, high contrast, captions and hold-vs-toggle. The shell adds them as its `accessibility`
|
|
69
|
+
* category and any game can render them inside its own settings layout.
|
|
70
|
+
*
|
|
71
|
+
* @capability accessibility-settings-rows accessibility setting rows (text scale, colorblind, reduced motion, high contrast, captions, hold-vs-toggle) backed by the shared settings store
|
|
72
|
+
*/
|
|
73
|
+
export function accessibilitySettingRows(store) {
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
id: ACCESSIBILITY_SETTING_IDS.textScale,
|
|
77
|
+
label: "Text size",
|
|
78
|
+
kind: "select",
|
|
79
|
+
value: String(readTextScale(store)),
|
|
80
|
+
options: TEXT_SCALE_OPTIONS,
|
|
81
|
+
set: (value) => store.set(ACCESSIBILITY_SETTING_IDS.textScale, Number(value)),
|
|
82
|
+
affects: "textScale",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: ACCESSIBILITY_SETTING_IDS.colorblind,
|
|
86
|
+
label: "Colourblind palette",
|
|
87
|
+
kind: "select",
|
|
88
|
+
value: readColorblind(store),
|
|
89
|
+
options: COLORBLIND_OPTIONS,
|
|
90
|
+
set: (value) => store.set(ACCESSIBILITY_SETTING_IDS.colorblind, String(value)),
|
|
91
|
+
affects: "colorblind",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: ACCESSIBILITY_SETTING_IDS.reducedMotion,
|
|
95
|
+
label: "Reduce motion",
|
|
96
|
+
kind: "toggle",
|
|
97
|
+
value: Boolean(store.get(ACCESSIBILITY_SETTING_IDS.reducedMotion, DEFAULT_ACCESSIBILITY.reducedMotion)),
|
|
98
|
+
set: (value) => store.set(ACCESSIBILITY_SETTING_IDS.reducedMotion, Boolean(value)),
|
|
99
|
+
affects: "reducedMotion",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: ACCESSIBILITY_SETTING_IDS.highContrast,
|
|
103
|
+
label: "High contrast",
|
|
104
|
+
kind: "toggle",
|
|
105
|
+
value: Boolean(store.get(ACCESSIBILITY_SETTING_IDS.highContrast, DEFAULT_ACCESSIBILITY.highContrast)),
|
|
106
|
+
set: (value) => store.set(ACCESSIBILITY_SETTING_IDS.highContrast, Boolean(value)),
|
|
107
|
+
affects: "highContrast",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: ACCESSIBILITY_SETTING_IDS.captions,
|
|
111
|
+
label: "Captions",
|
|
112
|
+
kind: "toggle",
|
|
113
|
+
value: Boolean(store.get(ACCESSIBILITY_SETTING_IDS.captions, DEFAULT_ACCESSIBILITY.captions)),
|
|
114
|
+
set: (value) => store.set(ACCESSIBILITY_SETTING_IDS.captions, Boolean(value)),
|
|
115
|
+
affects: "captions",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: ACCESSIBILITY_SETTING_IDS.holdVsToggle,
|
|
119
|
+
label: "Hold or toggle aim",
|
|
120
|
+
kind: "select",
|
|
121
|
+
value: readHoldVsToggle(store),
|
|
122
|
+
options: HOLD_VS_TOGGLE_OPTIONS,
|
|
123
|
+
set: (value) => store.set(ACCESSIBILITY_SETTING_IDS.holdVsToggle, String(value)),
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Declarative form of the accessibility rows for `defineGame({ settings })`, for a game that wants
|
|
129
|
+
* them inside its own setting categories rather than the built-in `accessibility` one.
|
|
130
|
+
*/
|
|
131
|
+
export function accessibilitySettingDefs(category = "accessibility") {
|
|
132
|
+
return [
|
|
133
|
+
{ id: ACCESSIBILITY_SETTING_IDS.textScale, label: "Text size", category, kind: "select", default: "1", options: TEXT_SCALE_OPTIONS },
|
|
134
|
+
{ id: ACCESSIBILITY_SETTING_IDS.colorblind, label: "Colourblind palette", category, kind: "select", default: "none", options: COLORBLIND_OPTIONS },
|
|
135
|
+
{ id: ACCESSIBILITY_SETTING_IDS.reducedMotion, label: "Reduce motion", category, kind: "toggle", default: false },
|
|
136
|
+
{ id: ACCESSIBILITY_SETTING_IDS.highContrast, label: "High contrast", category, kind: "toggle", default: false },
|
|
137
|
+
{ id: ACCESSIBILITY_SETTING_IDS.captions, label: "Captions", category, kind: "toggle", default: false },
|
|
138
|
+
{ id: ACCESSIBILITY_SETTING_IDS.holdVsToggle, label: "Hold or toggle aim", category: "controls", kind: "select", default: "hold", options: HOLD_VS_TOGGLE_OPTIONS },
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The action's active state for one frame under a {@link HoldVsToggle} preference: `hold` follows the
|
|
143
|
+
* button, `toggle` latches on each press edge. Call with the previous result so toggle mode can flip
|
|
144
|
+
* without the caller keeping a second flag.
|
|
145
|
+
*
|
|
146
|
+
* @capability hold-vs-toggle resolve one action frame under a hold-or-toggle preference
|
|
147
|
+
*/
|
|
148
|
+
export function holdToggleActive(previous, input, mode) {
|
|
149
|
+
if (mode === "hold")
|
|
150
|
+
return input.held;
|
|
151
|
+
return input.pressed ? !previous : previous;
|
|
152
|
+
}
|
|
@@ -10,7 +10,10 @@ export interface GraphicsProfile {
|
|
|
10
10
|
shadowMapSize: 512 | 1024 | 2048 | 4096;
|
|
11
11
|
cascades: 1 | 2 | 3 | 4;
|
|
12
12
|
drawDistance: number;
|
|
13
|
+
/** Largest pool one particle effect may allocate. */
|
|
13
14
|
particleCap: number;
|
|
15
|
+
/** Live particles the whole scene may hold across every effect; the VFX library drops lowest-priority effects past it. */
|
|
16
|
+
particleBudget: number;
|
|
14
17
|
/** Anisotropic texture filtering samples; clamped to the GPU maximum. 1 is plain trilinear. */
|
|
15
18
|
anisotropy: 1 | 2 | 4 | 8 | 16;
|
|
16
19
|
postStages: {
|
|
@@ -6,6 +6,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
|
|
|
6
6
|
cascades: 1,
|
|
7
7
|
drawDistance: 120,
|
|
8
8
|
particleCap: 128,
|
|
9
|
+
particleBudget: 1024,
|
|
9
10
|
anisotropy: 2,
|
|
10
11
|
postStages: { ao: false, bloom: true, dof: false, smaa: true },
|
|
11
12
|
surfaceDetail: "lite",
|
|
@@ -16,6 +17,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
|
|
|
16
17
|
cascades: 2,
|
|
17
18
|
drawDistance: 240,
|
|
18
19
|
particleCap: 256,
|
|
20
|
+
particleBudget: 2048,
|
|
19
21
|
anisotropy: 4,
|
|
20
22
|
postStages: { ao: true, bloom: true, dof: true, smaa: true },
|
|
21
23
|
surfaceDetail: "full",
|
|
@@ -26,6 +28,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
|
|
|
26
28
|
cascades: 4,
|
|
27
29
|
drawDistance: 400,
|
|
28
30
|
particleCap: 512,
|
|
31
|
+
particleBudget: 4096,
|
|
29
32
|
anisotropy: 8,
|
|
30
33
|
postStages: { ao: true, bloom: true, dof: true, smaa: true },
|
|
31
34
|
surfaceDetail: "full",
|
|
@@ -36,6 +39,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
|
|
|
36
39
|
cascades: 4,
|
|
37
40
|
drawDistance: 800,
|
|
38
41
|
particleCap: 1024,
|
|
42
|
+
particleBudget: 8192,
|
|
39
43
|
anisotropy: 16,
|
|
40
44
|
postStages: { ao: true, bloom: true, dof: true, smaa: true },
|
|
41
45
|
surfaceDetail: "full",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { GameContext } from "../runtime/gameContext.js";
|
|
2
2
|
export type SettingValue = number | boolean | string;
|
|
3
3
|
export type SettingKind = "slider" | "toggle" | "select";
|
|
4
|
-
export type BuiltInSettingCategory = "sound" | "graphics" | "gameplay" | "controls";
|
|
4
|
+
export type BuiltInSettingCategory = "sound" | "graphics" | "gameplay" | "controls" | "accessibility";
|
|
5
5
|
/** Built-in category ids keep autocomplete; any other string makes a fresh category. */
|
|
6
6
|
export type SettingCategory = BuiltInSettingCategory | (string & {});
|
|
7
7
|
export declare const BUILT_IN_SETTING_CATEGORIES: readonly BuiltInSettingCategory[];
|
package/dist/sim/nearAgents.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Vec3 } from "../world/geometry.js";
|
|
2
|
-
import { type NavCrowd, type NavCrowdOptions } from "../nav/navCrowd.js";
|
|
2
|
+
import { type NavCrowd, type NavCrowdOptions, type NavCrowdSnapshot } from "../nav/navCrowd.js";
|
|
3
3
|
import type { NavRuntime } from "../nav/navRuntime.js";
|
|
4
4
|
import { type UtilityBrainSnapshot, type UtilityCandidate, type UtilityInterrupt, type UtilityOption } from "../ai/utilityBrain.js";
|
|
5
5
|
import { type NeedDefinition, type NeedSnapshot } from "./needs.js";
|
|
@@ -51,11 +51,18 @@ export interface NearCrowdAgents extends NearAgents {
|
|
|
51
51
|
decisions(): number;
|
|
52
52
|
view(id: string): NearAgentView | null;
|
|
53
53
|
crowd(): NavCrowd;
|
|
54
|
+
/**
|
|
55
|
+
* Serializable state: each agent's needs and held pick, plus the crowd's own routes. The crowd is
|
|
56
|
+
* part of the snapshot because a restored agent must resume the route it was walking, not re-plan a
|
|
57
|
+
* fresh one from wherever it stood.
|
|
58
|
+
*/
|
|
54
59
|
snapshot(): {
|
|
55
60
|
agents: NearAgentSnapshot[];
|
|
61
|
+
crowd: NavCrowdSnapshot;
|
|
56
62
|
};
|
|
57
63
|
restore(next: {
|
|
58
64
|
agents: readonly NearAgentSnapshot[];
|
|
65
|
+
crowd: NavCrowdSnapshot;
|
|
59
66
|
}): void;
|
|
60
67
|
}
|
|
61
68
|
/**
|
package/dist/sim/nearAgents.js
CHANGED
|
@@ -33,16 +33,19 @@ export function createNearAgents(options) {
|
|
|
33
33
|
needs: agent.needs.snapshot(),
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
function makeBrain(forNeeds) {
|
|
37
|
+
return createUtilityBrain({
|
|
38
|
+
needs: forNeeds,
|
|
39
|
+
options: options.options ?? [],
|
|
40
|
+
...(options.interrupts === undefined ? {} : { interrupts: options.interrupts }),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
36
43
|
function mount(next, saved) {
|
|
37
44
|
if (agents.has(next.id))
|
|
38
45
|
return;
|
|
39
46
|
const needs = createNeedSet(defs);
|
|
40
47
|
needs.restore(saved === undefined ? { values: { ...next.needs }, rates: {}, modifiers: {} } : saved.needs);
|
|
41
|
-
const brain =
|
|
42
|
-
needs,
|
|
43
|
-
options: options.options ?? [],
|
|
44
|
-
...(options.interrupts === undefined ? {} : { interrupts: options.interrupts }),
|
|
45
|
-
});
|
|
48
|
+
const brain = makeBrain(needs);
|
|
46
49
|
if (saved !== undefined)
|
|
47
50
|
brain.restore(saved.brain);
|
|
48
51
|
const goal = saved?.goal ?? next.goal;
|
|
@@ -120,25 +123,39 @@ export function createNearAgents(options) {
|
|
|
120
123
|
brain: agent.brain.snapshot(),
|
|
121
124
|
});
|
|
122
125
|
}
|
|
123
|
-
return { agents: out };
|
|
126
|
+
return { agents: out, crowd: crowd.snapshot() };
|
|
124
127
|
},
|
|
125
128
|
restore(next) {
|
|
126
|
-
for (const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
for (const agent of agents.values())
|
|
130
|
+
agent.brain.dispose();
|
|
131
|
+
agents.clear();
|
|
132
|
+
// The crowd's own snapshot carries each agent's route, so a restored agent walks the corridor it
|
|
133
|
+
// was on rather than a fresh plan from its current spot.
|
|
134
|
+
crowd.restore(next.crowd);
|
|
131
135
|
for (const saved of next.agents) {
|
|
132
|
-
|
|
136
|
+
const needs = createNeedSet(defs);
|
|
137
|
+
needs.restore(saved.needs);
|
|
138
|
+
const brain = makeBrain(needs);
|
|
139
|
+
brain.restore(saved.brain);
|
|
140
|
+
agents.set(saved.id, {
|
|
133
141
|
id: saved.id,
|
|
134
|
-
|
|
142
|
+
needs,
|
|
143
|
+
brain,
|
|
144
|
+
goal: saved.goal,
|
|
135
145
|
place: saved.place,
|
|
136
146
|
activity: saved.activity,
|
|
137
|
-
needs: saved.needs.values,
|
|
138
|
-
goal: saved.goal,
|
|
139
|
-
stop: 0,
|
|
140
147
|
travelling: saved.travelling,
|
|
141
|
-
|
|
148
|
+
spawned: {
|
|
149
|
+
id: saved.id,
|
|
150
|
+
position: saved.position,
|
|
151
|
+
place: saved.place,
|
|
152
|
+
activity: saved.activity,
|
|
153
|
+
needs: saved.needs.values,
|
|
154
|
+
goal: saved.goal,
|
|
155
|
+
stop: 0,
|
|
156
|
+
travelling: saved.travelling,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
142
159
|
}
|
|
143
160
|
},
|
|
144
161
|
dispose() {
|
package/dist/sim/offScreen.d.ts
CHANGED
|
@@ -119,7 +119,8 @@ export interface OffScreenSnapshot {
|
|
|
119
119
|
travelling: boolean;
|
|
120
120
|
pinned: boolean;
|
|
121
121
|
updatedAt: number;
|
|
122
|
-
|
|
122
|
+
/** Next cheap-state refresh; `null` for a resident that never goes stale (no routine). */
|
|
123
|
+
staleAt: number | null;
|
|
123
124
|
needs: Record<string, number>;
|
|
124
125
|
}[];
|
|
125
126
|
}
|
package/dist/sim/offScreen.js
CHANGED
|
@@ -89,7 +89,18 @@ export function createOffScreenWorld(options) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
const next = resident.routine.nextChangeAfter(resident.updatedAt);
|
|
93
|
+
if (next > resident.updatedAt) {
|
|
94
|
+
resident.staleAt = next;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// A schedule that cannot name a later change must not spin this catch-up loop; hold the
|
|
98
|
+
// phase, decay to `now` and try once more a cycle later.
|
|
99
|
+
resident.needs = decayMeters(resident.needs, defs, now - resident.updatedAt);
|
|
100
|
+
resident.updatedAt = now;
|
|
101
|
+
resident.staleAt = now + resident.routine.cycleSeconds;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
106
|
/** Keep a near resident's routine phase current and walk it to the next place when the clock moves it. */
|
|
@@ -380,7 +391,7 @@ export function createOffScreenWorld(options) {
|
|
|
380
391
|
travelling: resident.travelling,
|
|
381
392
|
pinned: resident.pinned,
|
|
382
393
|
updatedAt: resident.updatedAt,
|
|
383
|
-
staleAt: resident.staleAt,
|
|
394
|
+
staleAt: Number.isFinite(resident.staleAt) ? resident.staleAt : null,
|
|
384
395
|
needs: { ...(live ?? resident.needs) },
|
|
385
396
|
};
|
|
386
397
|
});
|
|
@@ -418,7 +429,7 @@ export function createOffScreenWorld(options) {
|
|
|
418
429
|
travelling: saved.travelling,
|
|
419
430
|
pinned: saved.pinned,
|
|
420
431
|
updatedAt: saved.updatedAt,
|
|
421
|
-
staleAt: saved.staleAt,
|
|
432
|
+
staleAt: saved.staleAt === null ? Infinity : saved.staleAt,
|
|
422
433
|
needs: { ...saved.needs },
|
|
423
434
|
distance: Infinity,
|
|
424
435
|
};
|