@jgengine/core 0.26.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.
Files changed (143) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/dist/ai/policeResponse.d.ts +253 -0
  3. package/dist/ai/policeResponse.js +421 -0
  4. package/dist/anim/actionLayer.d.ts +47 -0
  5. package/dist/anim/actionLayer.js +63 -0
  6. package/dist/anim/ikSolver.d.ts +14 -0
  7. package/dist/anim/ikSolver.js +37 -0
  8. package/dist/anim/locomotionGraph.d.ts +58 -3
  9. package/dist/anim/locomotionGraph.js +142 -15
  10. package/dist/anim/ragdollRig.d.ts +53 -0
  11. package/dist/anim/ragdollRig.js +132 -0
  12. package/dist/anim/skeletonStandard.d.ts +140 -0
  13. package/dist/anim/skeletonStandard.js +326 -0
  14. package/dist/combat/damageRuntime.d.ts +14 -1
  15. package/dist/combat/damageRuntime.js +62 -4
  16. package/dist/combat/death.js +7 -3
  17. package/dist/combat/deathReason.d.ts +7 -1
  18. package/dist/combat/effects.d.ts +25 -0
  19. package/dist/combat/effects.js +16 -0
  20. package/dist/combat/statusRuntime.d.ts +2 -0
  21. package/dist/combat/statusRuntime.js +1 -0
  22. package/dist/economy/loanBook.d.ts +182 -0
  23. package/dist/economy/loanBook.js +303 -0
  24. package/dist/economy/market.d.ts +161 -0
  25. package/dist/economy/market.js +248 -0
  26. package/dist/economy/transactionLedger.d.ts +162 -0
  27. package/dist/economy/transactionLedger.js +301 -0
  28. package/dist/editor/creatorStorage.js +3 -1
  29. package/dist/editor/simulation.d.ts +5 -2
  30. package/dist/editor/simulation.js +29 -19
  31. package/dist/game/cameraConfig.d.ts +14 -0
  32. package/dist/game/clipRoles.d.ts +5 -4
  33. package/dist/game/clipRoles.js +34 -4
  34. package/dist/game/events.d.ts +27 -0
  35. package/dist/game/gameSave.d.ts +142 -0
  36. package/dist/game/gameSave.js +174 -0
  37. package/dist/game/playableGame.d.ts +74 -6
  38. package/dist/game/saveSlotIndex.d.ts +54 -0
  39. package/dist/game/saveSlotIndex.js +71 -0
  40. package/dist/game/saveSlots.d.ts +2 -0
  41. package/dist/game/saveSlots.js +1 -1
  42. package/dist/game/saveStore.d.ts +8 -0
  43. package/dist/game/saveStore.js +16 -0
  44. package/dist/gameplay.d.ts +17 -2
  45. package/dist/gameplay.js +15 -0
  46. package/dist/inventory/carryLoad.d.ts +48 -0
  47. package/dist/inventory/carryLoad.js +44 -0
  48. package/dist/inventory/equipment.d.ts +95 -0
  49. package/dist/inventory/equipment.js +169 -0
  50. package/dist/inventory/inventoryModel.d.ts +15 -6
  51. package/dist/inventory/inventoryModel.js +54 -5
  52. package/dist/inventory/shapedGrid.d.ts +26 -0
  53. package/dist/inventory/shapedGrid.js +47 -0
  54. package/dist/loot/lootGen.d.ts +186 -0
  55. package/dist/loot/lootGen.js +437 -0
  56. package/dist/loot/lootWeapon.d.ts +35 -0
  57. package/dist/loot/lootWeapon.js +70 -0
  58. package/dist/meta/changelog.d.ts +1 -1
  59. package/dist/meta/changelog.js +156 -1
  60. package/dist/mission/compositions.d.ts +85 -0
  61. package/dist/mission/compositions.js +130 -0
  62. package/dist/mission/mission.d.ts +435 -0
  63. package/dist/mission/mission.js +770 -0
  64. package/dist/mission/triggers.d.ts +61 -0
  65. package/dist/mission/triggers.js +108 -0
  66. package/dist/nav/laneGraph.d.ts +178 -0
  67. package/dist/nav/laneGraph.js +671 -0
  68. package/dist/nav/traffic.d.ts +222 -0
  69. package/dist/nav/traffic.js +918 -0
  70. package/dist/physics/kinematicVehicle.d.ts +36 -0
  71. package/dist/physics/kinematicVehicle.js +15 -1
  72. package/dist/physics/vehicleDamage.d.ts +136 -0
  73. package/dist/physics/vehicleDamage.js +214 -0
  74. package/dist/physics/vehicleParts.d.ts +62 -0
  75. package/dist/physics/vehicleParts.js +65 -0
  76. package/dist/progression/gearScore.d.ts +56 -0
  77. package/dist/progression/gearScore.js +59 -0
  78. package/dist/progression/skillTree.d.ts +129 -0
  79. package/dist/progression/skillTree.js +265 -0
  80. package/dist/progression/xpCurve.d.ts +60 -0
  81. package/dist/progression/xpCurve.js +141 -0
  82. package/dist/runtime/context/combat.js +11 -0
  83. package/dist/scene/builtinSceneKinds.js +2 -0
  84. package/dist/settings/accessibilitySettings.d.ts +81 -0
  85. package/dist/settings/accessibilitySettings.js +152 -0
  86. package/dist/settings/graphicsProfile.d.ts +3 -0
  87. package/dist/settings/graphicsProfile.js +4 -0
  88. package/dist/settings/settingsModel.d.ts +1 -1
  89. package/dist/settings/settingsModel.js +1 -0
  90. package/dist/sim/nearAgents.d.ts +8 -1
  91. package/dist/sim/nearAgents.js +34 -17
  92. package/dist/sim/offScreen.d.ts +2 -1
  93. package/dist/sim/offScreen.js +14 -3
  94. package/dist/sim/population.d.ts +147 -0
  95. package/dist/sim/population.js +182 -0
  96. package/dist/sim/queue.d.ts +90 -0
  97. package/dist/sim/queue.js +171 -0
  98. package/dist/sim/rides.d.ts +287 -0
  99. package/dist/sim/rides.js +471 -0
  100. package/dist/sim/routine.js +19 -11
  101. package/dist/ui/loadingTips.d.ts +41 -0
  102. package/dist/ui/loadingTips.js +64 -0
  103. package/dist/ui/menuNav.d.ts +79 -0
  104. package/dist/ui/menuNav.js +147 -0
  105. package/dist/ui/menuTransition.d.ts +50 -0
  106. package/dist/ui/menuTransition.js +67 -0
  107. package/dist/ui.d.ts +2 -0
  108. package/dist/ui.js +2 -0
  109. package/dist/vfx/effectLibrary.d.ts +98 -0
  110. package/dist/vfx/effectLibrary.js +181 -0
  111. package/dist/vfx/particleBudget.d.ts +40 -0
  112. package/dist/vfx/particleBudget.js +39 -0
  113. package/dist/vfx/particleDirector.d.ts +16 -0
  114. package/dist/vfx/particleDirector.js +18 -0
  115. package/dist/world/authoredSimulation.js +7 -1
  116. package/dist/world/captureCamera.d.ts +30 -0
  117. package/dist/world/captureCamera.js +19 -0
  118. package/dist/world/decalKind.d.ts +48 -0
  119. package/dist/world/decalKind.js +86 -0
  120. package/dist/world/features.d.ts +18 -5
  121. package/dist/world/features.js +1 -0
  122. package/dist/world/grassKind.d.ts +5 -0
  123. package/dist/world/grassKind.js +3 -0
  124. package/dist/world/terraform.d.ts +8 -0
  125. package/dist/world/terrain.d.ts +1 -0
  126. package/dist/world/terrain.js +2 -0
  127. package/dist/world.d.ts +7 -2
  128. package/dist/world.js +5 -0
  129. package/package.json +1 -1
  130. package/skills/jgengine/api.md +2 -2
  131. package/skills/jgengine-combat/SKILL.md +9 -1
  132. package/skills/jgengine-combat/api.md +5 -4
  133. package/skills/jgengine-combat/reference.md +4 -0
  134. package/skills/jgengine-gameplay/SKILL.md +86 -0
  135. package/skills/jgengine-gameplay/api.md +485 -16
  136. package/skills/jgengine-gameplay/capabilities.md +131 -4
  137. package/skills/jgengine-gameplay/reference-systems.md +118 -0
  138. package/skills/jgengine-world/SKILL.md +8 -0
  139. package/skills/jgengine-world/api.md +243 -11
  140. package/skills/jgengine-world/capabilities.md +61 -1
  141. package/skills/jgengine-world/recipes/character-animation.md +25 -2
  142. package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
  143. package/skills/jgengine-world/reference.md +1 -1
@@ -13,6 +13,7 @@ import { registerWaterKind } from "../world/waterKind.js";
13
13
  import { registerGrassKind } from "../world/grassKind.js";
14
14
  import { registerSoilKind } from "../world/soilKind.js";
15
15
  import { registerPoleLineKind } from "../world/poleLineKind.js";
16
+ import { registerDecalKind } from "../world/decalKind.js";
16
17
  import { registerBuildingGenerator } from "../world/buildingGenerator.js";
17
18
  import { registerCityKind } from "../world/cityKind.js";
18
19
  import { describeScatterCoverage, densityCoverage } from "../world/scatterCoverage.js";
@@ -80,5 +81,6 @@ export function registerBuiltinSceneKinds() {
80
81
  registerSoilKind();
81
82
  registerCityKind();
82
83
  registerPoleLineKind();
84
+ registerDecalKind();
83
85
  registerBuildingGenerator();
84
86
  }
@@ -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[];
@@ -3,6 +3,7 @@ export const BUILT_IN_SETTING_CATEGORIES = [
3
3
  "sound",
4
4
  "graphics",
5
5
  "gameplay",
6
+ "accessibility",
6
7
  "controls",
7
8
  ];
8
9
  export const SETTINGS_STORAGE_PREFIX = "jgengine:setting:";
@@ -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
  /**
@@ -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 = createUtilityBrain({
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 id of [...agents.keys()]) {
127
- crowd.remove(id);
128
- agents.get(id)?.brain.dispose();
129
- agents.delete(id);
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
- mount({
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
- position: saved.position,
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
- }, saved);
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() {
@@ -119,7 +119,8 @@ export interface OffScreenSnapshot {
119
119
  travelling: boolean;
120
120
  pinned: boolean;
121
121
  updatedAt: number;
122
- staleAt: number;
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
  }
@@ -89,7 +89,18 @@ export function createOffScreenWorld(options) {
89
89
  }
90
90
  }
91
91
  }
92
- resident.staleAt = resident.routine.nextChangeAfter(resident.updatedAt);
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
  };
@@ -0,0 +1,147 @@
1
+ import type { Vec3 } from "../world/geometry.js";
2
+ /** One visitor on site: the id its body, its needs and its money are keyed by. */
3
+ export interface Visitor {
4
+ readonly id: string;
5
+ /** Game-second it arrived; a restored visitor keeps the original. */
6
+ readonly admittedAt: number;
7
+ }
8
+ /** What a flow policy reads to decide how many arrive and when one has had enough. */
9
+ export interface VisitorFlow {
10
+ /** Visitors on site this tick — the member count, never a tally kept beside it. */
11
+ readonly onSite: number;
12
+ /** Most visitors the gates hold this tick, after resolving {@link VisitorPolicy.capacity}. */
13
+ readonly capacity: number;
14
+ /** Arrivals since {@link VisitorPopulation.rollDay}. */
15
+ readonly arrivalsToday: number;
16
+ readonly departuresToday: number;
17
+ readonly now: number;
18
+ }
19
+ /** The game's world behind every counted visitor: it creates and removes the body. */
20
+ export interface VisitorWorld {
21
+ /**
22
+ * Create one visitor's body and its game-side record, and return where the body now stands.
23
+ * `null` means the world refused it — no room, no walkable spot — and a refused body is never
24
+ * counted as on site or as an arrival.
25
+ */
26
+ readonly admit: (visitor: Visitor) => Vec3 | null;
27
+ /** Remove one visitor's body and record. Called for every departure, refusal and reset. */
28
+ readonly release: (visitor: Visitor) => void;
29
+ /**
30
+ * True when the world currently holds a body for this id. `adopt` and `restore` refuse an id the
31
+ * world does not hold, so the count can never name a body that is not on the ground.
32
+ */
33
+ readonly has: (id: string) => boolean;
34
+ }
35
+ /** The game's flow policy: what draws visitors in, how many fit, and when one has had enough. */
36
+ export interface VisitorPolicy {
37
+ /** Most visitors on site at once; a function lets the cap follow a rating, a level or a build. */
38
+ readonly capacity: number | ((now: number) => number);
39
+ /** Visitors arriving per second at this instant; `0` with the gates shut. */
40
+ readonly arrivalRate: (flow: VisitorFlow) => number;
41
+ /** True when this visitor should leave now. Omit for a population that only leaves when told. */
42
+ readonly shouldLeave?: (visitor: Visitor, flow: VisitorFlow) => boolean;
43
+ }
44
+ /** Flow policy fields a scenario can change in play. */
45
+ export interface VisitorPolicyPatch {
46
+ readonly capacity?: number | ((now: number) => number);
47
+ readonly arrivalRate?: (flow: VisitorFlow) => number;
48
+ /** `null` removes the leaving rule, so visitors stay until {@link VisitorPopulation.leave}. */
49
+ readonly shouldLeave?: ((visitor: Visitor, flow: VisitorFlow) => boolean) | null;
50
+ readonly arrivalBudget?: number;
51
+ /** `null` counts every admitted body as on site. */
52
+ readonly inside?: ((position: Vec3) => boolean) | null;
53
+ }
54
+ /** Counters a HUD, a probe or a test reads. The on-site figure is the visitor list's length. */
55
+ export interface VisitorPopulationStats {
56
+ onSite: number;
57
+ capacity: number;
58
+ arrivalsToday: number;
59
+ departuresToday: number;
60
+ /** Highest on-site count since the last {@link VisitorPopulation.rollDay}. */
61
+ peakToday: number;
62
+ /** Bodies the world or the bounds check refused. Never counted as arrivals. */
63
+ refused: number;
64
+ /** Arrival credit not yet spent. */
65
+ pending: number;
66
+ }
67
+ /** How a body the game made itself joins the population. */
68
+ export interface VisitorAdoption {
69
+ /** Game-second it arrived; defaults to the current tick's clock. */
70
+ readonly admittedAt?: number;
71
+ /** True for a body that is a new arrival today; false for one a save or a seed restored. */
72
+ readonly arrival?: boolean;
73
+ }
74
+ /** Serializable state. No on-site count is stored: it is the restored visitor list. */
75
+ export interface VisitorPopulationSnapshot {
76
+ visitors: {
77
+ id: string;
78
+ admittedAt: number;
79
+ }[];
80
+ arrivalsToday: number;
81
+ departuresToday: number;
82
+ peakToday: number;
83
+ pending: number;
84
+ /** Next id suffix, so a restored save does not reuse an id. */
85
+ sequence: number;
86
+ }
87
+ /** Options for {@link createVisitorPopulation}. */
88
+ export interface VisitorPopulationOptions {
89
+ readonly world: VisitorWorld;
90
+ readonly policy: VisitorPolicy;
91
+ /** First half of every generated id. Default `"visitor#"`. */
92
+ readonly idPrefix?: string;
93
+ /** First suffix used after `idPrefix`. Default 1. */
94
+ readonly sequenceStart?: number;
95
+ /** Most bodies one tick may admit, so a long frame cannot flood the park. Default 4. */
96
+ readonly arrivalBudget?: number;
97
+ /** A body must stand here to count as on site; omit to count every admitted body. */
98
+ readonly inside?: (position: Vec3) => boolean;
99
+ /** Register a devtools probe under this name so the overlay shows the crowd and the day. */
100
+ readonly devtoolsName?: string;
101
+ }
102
+ /**
103
+ * The crowd a scene reports. Every visitor is a member with a body the game's world made, so the
104
+ * on-site figure is the member count and the daily figure is the sum of admissions the world
105
+ * accepted — there is no second tally to drift from what is on the ground. Arrivals, capacity and
106
+ * leaving are the game's policy; ids, day rollover and counting are this seam's.
107
+ */
108
+ export interface VisitorPopulation {
109
+ /** Run the flow for `dt` seconds: leaving first, then arrivals within the budget and the cap. */
110
+ tick(dt: number, now: number): void;
111
+ /** Visitors on site. Reads the member count — no scan of the world, safe for a per-frame HUD. */
112
+ onSite(): number;
113
+ arrivalsToday(): number;
114
+ departuresToday(): number;
115
+ peakToday(): number;
116
+ /** Ids on site, arrival order. Allocates — HUD lists and tests. */
117
+ ids(): readonly string[];
118
+ has(id: string): boolean;
119
+ visitor(id: string): Visitor | null;
120
+ /** Send one visitor home now (it reached the exit, or the game evicted it). */
121
+ leave(id: string): boolean;
122
+ /**
123
+ * Take over a body the game created itself — one restored from a save, seeded at park open, or
124
+ * scripted. It is on site from now on; pass `arrival: true` when it is also a new arrival today.
125
+ */
126
+ adopt(id: string, adoption?: VisitorAdoption): boolean;
127
+ /** Close the day: arrivals, departures and peak reset. The crowd stays where it stands. */
128
+ rollDay(): void;
129
+ retune(patch: VisitorPolicyPatch): void;
130
+ stats(): VisitorPopulationStats;
131
+ snapshot(): VisitorPopulationSnapshot;
132
+ /**
133
+ * Replace the members with the saved ones. Only visitors the world already holds come back, so
134
+ * restore the bodies first (or `adopt` each one as it spawns); the rest count as refused.
135
+ */
136
+ restore(next: VisitorPopulationSnapshot): void;
137
+ /** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */
138
+ dispose(): void;
139
+ }
140
+ /**
141
+ * Create the population a scene reports, with the game owning arrival and leaving policy. A
142
+ * visitor only exists once the world has made its body, so the HUD's count and the bodies on
143
+ * screen are the same list: `admit` returning `null` is a refused arrival, not a number.
144
+ *
145
+ * @capability visitor-population a crowd whose on-site and daily counts are the bodies the game made
146
+ */
147
+ export declare function createVisitorPopulation(options: VisitorPopulationOptions): VisitorPopulation;