@jgengine/core 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +500 -1
- package/dist/ai/difficulty.d.ts +154 -0
- package/dist/ai/difficulty.js +194 -0
- package/dist/ai/driver.d.ts +107 -0
- package/dist/ai/driver.js +175 -0
- package/dist/ai/flock.js +13 -23
- package/dist/ai/laneSelect.js +2 -1
- package/dist/ai/mobBrain.js +2 -1
- package/dist/ai/waveRunner.d.ts +81 -0
- package/dist/ai/waveRunner.js +104 -0
- package/dist/audio/musicState.d.ts +47 -0
- package/dist/audio/musicState.js +38 -0
- package/dist/audio/stepCadence.d.ts +47 -0
- package/dist/audio/stepCadence.js +29 -0
- package/dist/audio/synth.d.ts +12 -0
- package/dist/authoring.d.ts +6 -4
- package/dist/authoring.js +6 -4
- package/dist/combat/castRunner.d.ts +13 -1
- package/dist/combat/castRunner.js +5 -1
- package/dist/combat/comboMeter.d.ts +140 -0
- package/dist/combat/comboMeter.js +215 -0
- package/dist/combat/comboPoints.d.ts +11 -0
- package/dist/combat/comboPoints.js +11 -2
- package/dist/combat/damageResolution.d.ts +1 -1
- package/dist/combat/defensiveWindow.d.ts +5 -0
- package/dist/combat/effects.d.ts +24 -0
- package/dist/combat/effects.js +6 -1
- package/dist/combat/projectiles.d.ts +6 -0
- package/dist/combat/projectiles.js +2 -2
- package/dist/combat/regenShield.d.ts +40 -2
- package/dist/combat/regenShield.js +66 -18
- package/dist/combat/resourcePool.d.ts +19 -0
- package/dist/combat/resourcePool.js +20 -2
- package/dist/combat/shotOrigin.d.ts +5 -0
- package/dist/combat/statusApplication.d.ts +1 -1
- package/dist/combat/statusApplication.js +2 -1
- package/dist/combat/statusEffectView.d.ts +56 -0
- package/dist/combat/statusEffectView.js +52 -0
- package/dist/combat.d.ts +4 -2
- package/dist/combat.js +2 -0
- package/dist/devtools/fallbackSeams.d.ts +3 -2
- package/dist/devtools/fallbackSeams.js +1 -1
- package/dist/devtools/textureErrors.d.ts +40 -0
- package/dist/devtools/textureErrors.js +68 -0
- package/dist/economy/shopStock.d.ts +110 -0
- package/dist/economy/shopStock.js +157 -0
- package/dist/economy/wallet.d.ts +6 -0
- package/dist/economy/wallet.js +1 -0
- package/dist/game/achievements.d.ts +80 -0
- package/dist/game/achievements.js +124 -0
- package/dist/game/cameraConfig.d.ts +7 -0
- package/dist/game/codex.d.ts +60 -0
- package/dist/game/codex.js +89 -0
- package/dist/game/credits.d.ts +36 -0
- package/dist/game/credits.js +0 -0
- package/dist/game/defineGame.d.ts +28 -3
- package/dist/game/defineGame.js +5 -0
- package/dist/game/dialogueGraph.d.ts +116 -0
- package/dist/game/dialogueGraph.js +103 -0
- package/dist/game/eventTicker.d.ts +95 -0
- package/dist/game/eventTicker.js +91 -0
- package/dist/game/events.d.ts +24 -5
- package/dist/game/events.js +18 -0
- package/dist/game/gamePhase.d.ts +1 -1
- package/dist/game/gamePhase.js +1 -1
- package/dist/game/leaderboardRank.d.ts +71 -0
- package/dist/game/leaderboardRank.js +76 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +4 -2
- package/dist/game/notifications.d.ts +57 -0
- package/dist/game/notifications.js +107 -0
- package/dist/game/playableGame.d.ts +39 -2
- package/dist/game/quest.d.ts +26 -0
- package/dist/game/quest.js +31 -0
- package/dist/game/runDraft.js +2 -1
- package/dist/game/saveSlots.d.ts +102 -0
- package/dist/game/saveSlots.js +136 -0
- package/dist/game/talentTreeView.d.ts +106 -0
- package/dist/game/talentTreeView.js +114 -0
- package/dist/game/worldItem.js +2 -1
- package/dist/gameplay.d.ts +15 -4
- package/dist/gameplay.js +13 -2
- package/dist/i18n/i18n.d.ts +51 -0
- package/dist/i18n/i18n.js +82 -0
- package/dist/input/axisInput.d.ts +1 -1
- package/dist/input/axisInput.js +18 -2
- package/dist/input/rebindSession.d.ts +141 -0
- package/dist/input/rebindSession.js +235 -0
- package/dist/input/touchControlsMode.d.ts +16 -0
- package/dist/input/touchControlsMode.js +23 -0
- package/dist/input/touchScheme.d.ts +30 -1
- package/dist/input/touchScheme.js +31 -1
- package/dist/interaction/promptRegistry.d.ts +78 -0
- package/dist/interaction/promptRegistry.js +130 -0
- package/dist/inventory/storageTier.js +3 -2
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +145 -1
- package/dist/movement/movementModel.d.ts +26 -3
- package/dist/movement/movementModel.js +121 -15
- package/dist/movement/playerMovement.js +161 -25
- package/dist/physics/drivableVehicle.js +3 -1
- package/dist/physics/kinematicVehicle.d.ts +56 -1
- package/dist/physics/kinematicVehicle.js +99 -10
- package/dist/physics/vehicleObstacles.js +19 -0
- package/dist/random/resolveRng.d.ts +14 -0
- package/dist/random/resolveRng.js +31 -0
- package/dist/render/postProcessing.d.ts +12 -1
- package/dist/runtime/cameraDirector.d.ts +2 -2
- package/dist/runtime/context/combat.js +8 -0
- package/dist/runtime/gameContext.js +6 -1
- package/dist/runtime/gameContextTypes.d.ts +18 -0
- package/dist/runtime/inputSnapshot.d.ts +10 -1
- package/dist/runtime/inputSnapshot.js +13 -1
- package/dist/runtime/runtimeSave.d.ts +3 -3
- package/dist/runtime/runtimeSave.js +11 -2
- package/dist/scene/assetDiagnostics.js +2 -2
- package/dist/scene/autoTarget.js +2 -1
- package/dist/scene/captureCheck.js +3 -2
- package/dist/scene/entityStore.d.ts +4 -1
- package/dist/scene/entityStore.js +3 -0
- package/dist/scene/objectStore.d.ts +10 -0
- package/dist/scene/objectStore.js +29 -6
- package/dist/scene/sceneKinds.d.ts +18 -0
- package/dist/scene/sceneKinds.js +19 -0
- package/dist/scene/sequenceDirector.d.ts +110 -0
- package/dist/scene/sequenceDirector.js +146 -0
- package/dist/session/roles.js +3 -2
- package/dist/settings/settingsModel.d.ts +1 -0
- package/dist/settings/settingsModel.js +1 -0
- package/dist/stats/rollCheck.js +4 -2
- package/dist/time/dayNightCycle.d.ts +114 -0
- package/dist/time/dayNightCycle.js +208 -0
- package/dist/time/timerSet.d.ts +132 -0
- package/dist/time/timerSet.js +243 -0
- package/dist/ui/accessibility.d.ts +51 -0
- package/dist/ui/accessibility.js +83 -0
- package/dist/ui/coachMarks.d.ts +99 -0
- package/dist/ui/coachMarks.js +126 -0
- package/dist/ui/floatingText.d.ts +114 -0
- package/dist/ui/floatingText.js +203 -0
- package/dist/ui/modalStack.d.ts +143 -0
- package/dist/ui/modalStack.js +160 -0
- package/dist/ui/objectiveBanner.d.ts +123 -0
- package/dist/ui/objectiveBanner.js +162 -0
- package/dist/ui/photoMode.d.ts +32 -0
- package/dist/ui/photoMode.js +51 -0
- package/dist/ui/radialMenu.d.ts +47 -0
- package/dist/ui/radialMenu.js +74 -0
- package/dist/ui/screenMarkers.d.ts +128 -0
- package/dist/ui/screenMarkers.js +118 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +8 -0
- package/dist/vfx/cameraShake.d.ts +97 -0
- package/dist/vfx/cameraShake.js +136 -0
- package/dist/vfx/damageDirection.d.ts +102 -0
- package/dist/vfx/damageDirection.js +188 -0
- package/dist/vfx/particles.d.ts +103 -0
- package/dist/vfx/particles.js +256 -0
- package/dist/vfx/screenEffects.d.ts +149 -0
- package/dist/vfx/screenEffects.js +191 -0
- package/dist/world/authoredObjects.d.ts +18 -0
- package/dist/world/authoredObjects.js +21 -0
- package/dist/world/authoredSpawn.js +19 -0
- package/dist/world/buildingLots.d.ts +54 -11
- package/dist/world/buildingLots.js +116 -30
- package/dist/world/cityBlocks.d.ts +45 -0
- package/dist/world/cityBlocks.js +159 -0
- package/dist/world/cityContent.d.ts +46 -2
- package/dist/world/cityContent.js +160 -2
- package/dist/world/cityGenerator.d.ts +261 -23
- package/dist/world/cityGenerator.js +643 -33
- package/dist/world/cityGeometry.d.ts +20 -0
- package/dist/world/cityGeometry.js +91 -0
- package/dist/world/cityKind.js +1 -44
- package/dist/world/fastTravel.d.ts +64 -0
- package/dist/world/fastTravel.js +108 -0
- package/dist/world/features.d.ts +16 -0
- package/dist/world/features.js +2 -0
- package/dist/world/grounded.d.ts +11 -0
- package/dist/world/grounded.js +12 -0
- package/dist/world/mapAnnotations.d.ts +75 -0
- package/dist/world/mapAnnotations.js +139 -0
- package/dist/world/markers.js +1 -0
- package/dist/world/resourceNode.d.ts +1 -1
- package/dist/world/resourceNode.js +2 -1
- package/dist/world/roads.d.ts +294 -5
- package/dist/world/roads.js +1262 -21
- package/dist/world/sharedAnnotations.d.ts +84 -0
- package/dist/world/sharedAnnotations.js +76 -0
- package/dist/world/spawnOverride.d.ts +37 -0
- package/dist/world/spawnOverride.js +50 -0
- package/dist/world/streetGenerator.d.ts +60 -11
- package/dist/world/streetGenerator.js +1786 -129
- package/dist/world/terrain.d.ts +1 -0
- package/dist/world/terrain.js +2 -0
- package/dist/world/waypoints.d.ts +63 -0
- package/dist/world/waypoints.js +137 -0
- package/dist/world.d.ts +17 -2
- package/dist/world.js +17 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +3 -1
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-combat/api.md +40 -4
- package/skills/jgengine-combat/capabilities.md +12 -0
- package/skills/jgengine-combat/reference.md +2 -0
- package/skills/jgengine-gameplay/api.md +212 -11
- package/skills/jgengine-gameplay/capabilities.md +66 -1
- package/skills/jgengine-world/api.md +397 -11
- package/skills/jgengine-world/capabilities.md +123 -2
- package/skills/jgengine-world/reference.md +15 -1
package/dist/game/lootTable.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveRng } from "../random/resolveRng.js";
|
|
1
2
|
function assertValidEntry(entry, mode) {
|
|
2
3
|
const kinds = [entry.item !== undefined, entry.currency !== undefined, entry.generate !== undefined].filter(Boolean).length;
|
|
3
4
|
if (kinds !== 1) {
|
|
@@ -53,8 +54,9 @@ function rollEntry(entry, rng) {
|
|
|
53
54
|
*
|
|
54
55
|
* @capability loot-table register loot tables and roll weighted randomized drops
|
|
55
56
|
*/
|
|
56
|
-
export function createLootRegistry() {
|
|
57
|
+
export function createLootRegistry(options = {}) {
|
|
57
58
|
const tables = new Map();
|
|
59
|
+
const defaultRng = resolveRng(options.rng, "createLootRegistry");
|
|
58
60
|
return {
|
|
59
61
|
register(def) {
|
|
60
62
|
assertValidDef(def);
|
|
@@ -66,7 +68,7 @@ export function createLootRegistry() {
|
|
|
66
68
|
has(id) {
|
|
67
69
|
return tables.has(id);
|
|
68
70
|
},
|
|
69
|
-
roll(id, rng =
|
|
71
|
+
roll(id, rng = defaultRng) {
|
|
70
72
|
const table = tables.get(id);
|
|
71
73
|
if (!table) {
|
|
72
74
|
throw new Error(`unknown loot table: ${id}`);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** One persistent notification — unlike a transient toast, it stays in the log until dismissed. */
|
|
2
|
+
export interface NotificationEntry<TMeta = unknown> {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Severity/category (`"info"`, `"success"`, `"warning"`, `"danger"`, or a game kind). */
|
|
5
|
+
kind: string;
|
|
6
|
+
title: string;
|
|
7
|
+
body?: string;
|
|
8
|
+
/** Creation time from the injected clock. */
|
|
9
|
+
at: number;
|
|
10
|
+
read: boolean;
|
|
11
|
+
meta?: TMeta;
|
|
12
|
+
}
|
|
13
|
+
/** Fields accepted by {@link NotificationStore.push}. */
|
|
14
|
+
export interface NotificationInput<TMeta = unknown> {
|
|
15
|
+
id?: string;
|
|
16
|
+
kind?: string;
|
|
17
|
+
title: string;
|
|
18
|
+
body?: string;
|
|
19
|
+
at?: number;
|
|
20
|
+
meta?: TMeta;
|
|
21
|
+
}
|
|
22
|
+
/** Filter for {@link NotificationStore.list}. */
|
|
23
|
+
export interface NotificationFilter {
|
|
24
|
+
kind?: string;
|
|
25
|
+
unreadOnly?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** Serializable, observable log of persistent notifications with read tracking. */
|
|
28
|
+
export interface NotificationStore<TMeta = unknown> {
|
|
29
|
+
push(input: NotificationInput<TMeta>): NotificationEntry<TMeta>;
|
|
30
|
+
/** Newest-first; filter by kind and/or unread. Stable identity between changes for the unfiltered read. */
|
|
31
|
+
list(filter?: NotificationFilter): readonly NotificationEntry<TMeta>[];
|
|
32
|
+
unreadCount(): number;
|
|
33
|
+
markRead(id: string): void;
|
|
34
|
+
markAllRead(): void;
|
|
35
|
+
remove(id: string): boolean;
|
|
36
|
+
clear(): void;
|
|
37
|
+
subscribe(listener: () => void): () => void;
|
|
38
|
+
snapshot(): readonly NotificationEntry<TMeta>[];
|
|
39
|
+
restore(snapshot: readonly NotificationEntry<TMeta>[]): void;
|
|
40
|
+
}
|
|
41
|
+
/** Options for {@link createNotificationCenter}. */
|
|
42
|
+
export interface NotificationCenterOptions {
|
|
43
|
+
/** Injected clock (ms). Default `Date.now`. */
|
|
44
|
+
now?: () => number;
|
|
45
|
+
/** Keep only the newest `cap` notifications. Default 100; 0 = unbounded. */
|
|
46
|
+
cap?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A persistent notification log with read/unread tracking — the "notification
|
|
50
|
+
* center" surface toasts don't cover. Newest-first, capped, serializable, and
|
|
51
|
+
* observable; a game pushes durable events (quest updates, trades, party news)
|
|
52
|
+
* here and a HUD renders the list + an unread badge. `snapshot`/`restore`
|
|
53
|
+
* round-trip through a save.
|
|
54
|
+
*
|
|
55
|
+
* @capability notification-center persistent, read-tracked notification log (newest-first, capped, serializable) behind an unread badge — the durable counterpart to transient toasts
|
|
56
|
+
*/
|
|
57
|
+
export declare function createNotificationCenter<TMeta = unknown>(options?: NotificationCenterOptions): NotificationStore<TMeta>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A persistent notification log with read/unread tracking — the "notification
|
|
3
|
+
* center" surface toasts don't cover. Newest-first, capped, serializable, and
|
|
4
|
+
* observable; a game pushes durable events (quest updates, trades, party news)
|
|
5
|
+
* here and a HUD renders the list + an unread badge. `snapshot`/`restore`
|
|
6
|
+
* round-trip through a save.
|
|
7
|
+
*
|
|
8
|
+
* @capability notification-center persistent, read-tracked notification log (newest-first, capped, serializable) behind an unread badge — the durable counterpart to transient toasts
|
|
9
|
+
*/
|
|
10
|
+
export function createNotificationCenter(options = {}) {
|
|
11
|
+
const now = options.now ?? Date.now;
|
|
12
|
+
const cap = options.cap ?? 100;
|
|
13
|
+
let entries = []; // newest-first
|
|
14
|
+
const listeners = new Set();
|
|
15
|
+
let cache = null;
|
|
16
|
+
let counter = 0;
|
|
17
|
+
function notify() {
|
|
18
|
+
cache = null;
|
|
19
|
+
for (const listener of listeners)
|
|
20
|
+
listener();
|
|
21
|
+
}
|
|
22
|
+
function generateId() {
|
|
23
|
+
counter += 1;
|
|
24
|
+
return `notice-${counter}`;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
push(input) {
|
|
28
|
+
const entry = {
|
|
29
|
+
id: input.id ?? generateId(),
|
|
30
|
+
kind: input.kind ?? "info",
|
|
31
|
+
title: input.title,
|
|
32
|
+
at: input.at ?? now(),
|
|
33
|
+
read: false,
|
|
34
|
+
...(input.body !== undefined ? { body: input.body } : {}),
|
|
35
|
+
...(input.meta !== undefined ? { meta: input.meta } : {}),
|
|
36
|
+
};
|
|
37
|
+
entries = [entry, ...entries.filter((existing) => existing.id !== entry.id)];
|
|
38
|
+
if (cap > 0 && entries.length > cap)
|
|
39
|
+
entries = entries.slice(0, cap);
|
|
40
|
+
notify();
|
|
41
|
+
return entry;
|
|
42
|
+
},
|
|
43
|
+
list(filter) {
|
|
44
|
+
if (filter === undefined) {
|
|
45
|
+
if (cache === null)
|
|
46
|
+
cache = [...entries];
|
|
47
|
+
return cache;
|
|
48
|
+
}
|
|
49
|
+
return entries.filter((entry) => (filter.kind === undefined || entry.kind === filter.kind) &&
|
|
50
|
+
(filter.unreadOnly !== true || !entry.read));
|
|
51
|
+
},
|
|
52
|
+
unreadCount() {
|
|
53
|
+
let count = 0;
|
|
54
|
+
for (const entry of entries)
|
|
55
|
+
if (!entry.read)
|
|
56
|
+
count += 1;
|
|
57
|
+
return count;
|
|
58
|
+
},
|
|
59
|
+
markRead(id) {
|
|
60
|
+
const entry = entries.find((candidate) => candidate.id === id);
|
|
61
|
+
if (entry === undefined || entry.read)
|
|
62
|
+
return;
|
|
63
|
+
entry.read = true;
|
|
64
|
+
notify();
|
|
65
|
+
},
|
|
66
|
+
markAllRead() {
|
|
67
|
+
let changed = false;
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (!entry.read) {
|
|
70
|
+
entry.read = true;
|
|
71
|
+
changed = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (changed)
|
|
75
|
+
notify();
|
|
76
|
+
},
|
|
77
|
+
remove(id) {
|
|
78
|
+
const next = entries.filter((entry) => entry.id !== id);
|
|
79
|
+
if (next.length === entries.length)
|
|
80
|
+
return false;
|
|
81
|
+
entries = next;
|
|
82
|
+
notify();
|
|
83
|
+
return true;
|
|
84
|
+
},
|
|
85
|
+
clear() {
|
|
86
|
+
if (entries.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
entries = [];
|
|
89
|
+
notify();
|
|
90
|
+
},
|
|
91
|
+
subscribe(listener) {
|
|
92
|
+
listeners.add(listener);
|
|
93
|
+
return () => listeners.delete(listener);
|
|
94
|
+
},
|
|
95
|
+
snapshot() {
|
|
96
|
+
if (cache === null)
|
|
97
|
+
cache = [...entries];
|
|
98
|
+
return cache;
|
|
99
|
+
},
|
|
100
|
+
restore(snapshot) {
|
|
101
|
+
entries = snapshot.map((entry) => ({ ...entry }));
|
|
102
|
+
if (cap > 0 && entries.length > cap)
|
|
103
|
+
entries = entries.slice(0, cap);
|
|
104
|
+
notify();
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -140,6 +140,22 @@ export interface ModelMaterialOverride {
|
|
|
140
140
|
emissiveIntensity?: number;
|
|
141
141
|
/** Real PBR texture maps applied over the model's material — see {@link ModelMaterialMaps}. */
|
|
142
142
|
maps?: ModelMaterialMaps;
|
|
143
|
+
/**
|
|
144
|
+
* Fresnel rim light: a bright edge where the surface turns away from the viewer. This is what
|
|
145
|
+
* separates an actor from ground of the same value — a body lit only by the scene's own key sits
|
|
146
|
+
* in the terrain's tonal band at range and reads as a smudge, however well its albedo is chosen.
|
|
147
|
+
* Omit for no rim.
|
|
148
|
+
*/
|
|
149
|
+
rim?: ModelRimLight;
|
|
150
|
+
}
|
|
151
|
+
/** Fresnel rim-light term added on top of a model's lighting — see {@link ModelMaterialOverride.rim}. */
|
|
152
|
+
export interface ModelRimLight {
|
|
153
|
+
/** Rim colour. Default "#ffffff". */
|
|
154
|
+
color?: string;
|
|
155
|
+
/** Rim brightness. 0 disables it; ~0.3 is a subtle read, ~1.5 is a hard sci-fi edge. Default 0.6. */
|
|
156
|
+
strength?: number;
|
|
157
|
+
/** Falloff exponent: higher confines the rim to the extreme edge. Default 2.5. */
|
|
158
|
+
power?: number;
|
|
143
159
|
}
|
|
144
160
|
/** Parents a prop/weapon model to a named bone or node on the host model's rig — a sword on `handslot.r`, a spellbook offhand — following the bone's animated transform each frame. */
|
|
145
161
|
export interface ModelAttachment {
|
|
@@ -221,6 +237,14 @@ export interface DirectionalLightingConfig {
|
|
|
221
237
|
shadowMapSize?: number;
|
|
222
238
|
/** Half-extent of the orthographic shadow camera in world units — sized to the shadowed area. Default 40. */
|
|
223
239
|
shadowCameraSize?: number;
|
|
240
|
+
/**
|
|
241
|
+
* Cascade count for cascaded shadow maps (CSM). When `> 1` and `castShadow`, the shell mounts
|
|
242
|
+
* three.js CSM so outdoor scenes keep shadows past a single ortho frustum. Default `1` (legacy
|
|
243
|
+
* single map). Typical outdoor value: `3` or `4`.
|
|
244
|
+
*/
|
|
245
|
+
cascades?: number;
|
|
246
|
+
/** Max far plane for CSM cascades in world units. Default 200. */
|
|
247
|
+
shadowMaxFar?: number;
|
|
224
248
|
/** Shadow depth bias to fight acne. Default -0.0004. */
|
|
225
249
|
shadowBias?: number;
|
|
226
250
|
/** Shadow normal bias to fight peter-panning. Default 0.02. */
|
|
@@ -258,8 +282,16 @@ export interface PlayerMovementConfig {
|
|
|
258
282
|
axis?: "x" | "z";
|
|
259
283
|
/** Cell size for mode "grid". Default 1. */
|
|
260
284
|
cellSize?: number;
|
|
261
|
-
/**
|
|
285
|
+
/**
|
|
286
|
+
* Collide the walking player against placed scene objects' blocking colliders even without
|
|
287
|
+
* `collision.voxel`. Default **true** — a placed object with a blocking physical collider stops,
|
|
288
|
+
* supports, and is stood upon by the player; set `false` to walk through everything (ghost/spectator
|
|
289
|
+
* modes, pure-board presentations). Objects that should never block should carry non-blocking
|
|
290
|
+
* colliders instead of relying on this switch.
|
|
291
|
+
*/
|
|
262
292
|
collideObjects?: boolean;
|
|
293
|
+
/** Tallest object ledge stepped up while walking (and largest drop still snapped down instead of falling). Default 0.4. */
|
|
294
|
+
stepHeight?: number;
|
|
263
295
|
/** Intercepts each frame's resolved position before the pose commits (and before onTick): return a replacement [x,y,z] to constrain or redirect the step, or nothing to accept it. */
|
|
264
296
|
beforeCommit?: (frame: MovementCommitFrame) => readonly [number, number, number] | undefined | void;
|
|
265
297
|
/** Gates the built-in sprint (`runSpeedMultiplier`) behind live game state — a stamina stat, an encumbrance check (#282.3). Called each frame while sprint is held; `false` walks. */
|
|
@@ -297,7 +329,12 @@ export interface PresentationEffectsConfig {
|
|
|
297
329
|
vfx?: boolean;
|
|
298
330
|
/** Floating combat text (damage numbers, etc.). Default true. */
|
|
299
331
|
floatText?: boolean;
|
|
300
|
-
/**
|
|
332
|
+
/**
|
|
333
|
+
* Straight muzzle→impact tracer lines for direct-fire shots. Opt-in (default false): unlike the
|
|
334
|
+
* other channels, a missing `tracers` key leaves them OFF, because `fireProjectile` is a generic
|
|
335
|
+
* seam and a tracer is only meaningful for a game that fires direct, bullet-like projectiles.
|
|
336
|
+
* Arced/exploding shots (grenades, launchers) never draw a tracer even when this is on.
|
|
337
|
+
*/
|
|
301
338
|
tracers?: boolean;
|
|
302
339
|
/** Trauma-driven combat camera shake. Default true. */
|
|
303
340
|
shake?: boolean;
|
package/dist/game/quest.d.ts
CHANGED
|
@@ -99,6 +99,32 @@ export interface QuestJournal {
|
|
|
99
99
|
* @capability quest-log track accepted quests and their per-objective progress
|
|
100
100
|
*/
|
|
101
101
|
export declare function createQuestJournal(deps: QuestJournalDeps): QuestJournal;
|
|
102
|
+
/** One objective as a tracker/HUD reads it — label + progress toward its count. */
|
|
103
|
+
export interface TrackedObjectiveView {
|
|
104
|
+
id: string;
|
|
105
|
+
/** Human label (e.g. "Defeat 3 wraiths"); falls back to the objective id. */
|
|
106
|
+
label: string;
|
|
107
|
+
count: number;
|
|
108
|
+
progress: number;
|
|
109
|
+
complete: boolean;
|
|
110
|
+
}
|
|
111
|
+
/** A quest as a tracker/HUD reads it — title, status, and per-objective progress. */
|
|
112
|
+
export interface TrackedQuestView {
|
|
113
|
+
id: string;
|
|
114
|
+
title: string;
|
|
115
|
+
status: QuestStatus;
|
|
116
|
+
objectives: TrackedObjectiveView[];
|
|
117
|
+
}
|
|
118
|
+
/** Default objective label: a readable "verb count noun" from a {@link QuestObjective}. */
|
|
119
|
+
export declare function defaultObjectiveLabel(objective: QuestObjective): string;
|
|
120
|
+
/**
|
|
121
|
+
* Join a quest's static {@link QuestDef} with a player's live {@link QuestInstance}
|
|
122
|
+
* into a flat, renderer-free view a HUD tracker draws (title, status, labelled
|
|
123
|
+
* objective progress). Pass `label` to override the derived objective text.
|
|
124
|
+
*
|
|
125
|
+
* @capability quest-tracker-view join a QuestDef + live QuestInstance into a flat labelled view for a quest/objective HUD tracker
|
|
126
|
+
*/
|
|
127
|
+
export declare function describeTrackedQuest(def: QuestDef, instance: QuestInstance, label?: (objective: QuestObjective) => string): TrackedQuestView;
|
|
102
128
|
export interface QuestAcceptOptions {
|
|
103
129
|
hasUnlock?(id: string): boolean;
|
|
104
130
|
}
|
package/dist/game/quest.js
CHANGED
|
@@ -259,6 +259,37 @@ export function createQuestJournal(deps) {
|
|
|
259
259
|
},
|
|
260
260
|
};
|
|
261
261
|
}
|
|
262
|
+
/** Default objective label: a readable "verb count noun" from a {@link QuestObjective}. */
|
|
263
|
+
export function defaultObjectiveLabel(objective) {
|
|
264
|
+
const noun = objective.target ?? objective.item ?? objective.id;
|
|
265
|
+
const verb = objective.kind === "kill" ? "Defeat" : objective.kind === "collect" ? "Collect" : objective.kind;
|
|
266
|
+
return `${verb} ${objective.count} ${noun}`;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Join a quest's static {@link QuestDef} with a player's live {@link QuestInstance}
|
|
270
|
+
* into a flat, renderer-free view a HUD tracker draws (title, status, labelled
|
|
271
|
+
* objective progress). Pass `label` to override the derived objective text.
|
|
272
|
+
*
|
|
273
|
+
* @capability quest-tracker-view join a QuestDef + live QuestInstance into a flat labelled view for a quest/objective HUD tracker
|
|
274
|
+
*/
|
|
275
|
+
export function describeTrackedQuest(def, instance, label = defaultObjectiveLabel) {
|
|
276
|
+
const defs = new Map(def.objectives.map((objective) => [objective.id, objective]));
|
|
277
|
+
return {
|
|
278
|
+
id: def.id,
|
|
279
|
+
title: def.title,
|
|
280
|
+
status: instance.status,
|
|
281
|
+
objectives: instance.objectives.map((progress) => {
|
|
282
|
+
const source = defs.get(progress.id);
|
|
283
|
+
return {
|
|
284
|
+
id: progress.id,
|
|
285
|
+
label: source === undefined ? progress.id : label(source),
|
|
286
|
+
count: progress.count,
|
|
287
|
+
progress: progress.progress,
|
|
288
|
+
complete: progress.complete,
|
|
289
|
+
};
|
|
290
|
+
}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
262
293
|
export function applyQuestRewards(rewards, appliers) {
|
|
263
294
|
for (const entry of rewards.items ?? []) {
|
|
264
295
|
const fail = appliers.grantItem?.(entry.inventory, entry.item, entry.count);
|
package/dist/game/runDraft.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pickWeighted } from "../world/scatterItems.js";
|
|
2
|
+
import { resolveRng } from "../random/resolveRng.js";
|
|
2
3
|
export function createRunModifierStack(offers) {
|
|
3
4
|
const byId = new Map();
|
|
4
5
|
for (const offer of offers) {
|
|
@@ -75,7 +76,7 @@ export function createRunModifierStack(offers) {
|
|
|
75
76
|
*/
|
|
76
77
|
export function createRunDraft(config) {
|
|
77
78
|
const stack = createRunModifierStack(config.offers);
|
|
78
|
-
const defaultRng = config.rng
|
|
79
|
+
const defaultRng = resolveRng(config.rng, "createRunDraft");
|
|
79
80
|
return {
|
|
80
81
|
present(count, rng = defaultRng) {
|
|
81
82
|
const pool = config.offers.filter((offer) => !stack.atMax(offer.id));
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One entry in a {@link SaveSlots} index: a save/profile slot's *display*
|
|
3
|
+
* metadata, not its payload. The real save data lives in a `createSaveStore`
|
|
4
|
+
* slot; this record is only what a save-select menu renders — an id, an optional
|
|
5
|
+
* player-facing `name`, whether the slot is `empty`, when it was last written
|
|
6
|
+
* (`savedAt`), and a free-string `meta` bag the game fills with whatever the
|
|
7
|
+
* menu should show (level, playtime, location, chapter, a thumbnail ref, …).
|
|
8
|
+
* `meta` keys are opaque to this model — it never reads or branches on them.
|
|
9
|
+
*/
|
|
10
|
+
export interface SaveSlotMeta {
|
|
11
|
+
/** Stable slot id — pair it with the same slot in the game's `createSaveStore`. */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Player-facing label (character name, save title). `undefined` until named. */
|
|
14
|
+
name?: string;
|
|
15
|
+
/** `true` when no save occupies this slot — the menu shows a "New Game" affordance. */
|
|
16
|
+
empty: boolean;
|
|
17
|
+
/** Clock time (ms) of the last {@link SaveSlots.write}, or `undefined` while empty. */
|
|
18
|
+
savedAt?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Free-string display fields the game owns and the menu renders as chips —
|
|
21
|
+
* e.g. `{ chapter: "The Drowned Keep", playtime: "4h 12m", level: 8 }`. The
|
|
22
|
+
* model never interprets these keys; they are pure passthrough to the renderer.
|
|
23
|
+
*/
|
|
24
|
+
meta: Record<string, string | number>;
|
|
25
|
+
}
|
|
26
|
+
/** What a {@link SaveSlots.write} stamps onto a slot — a name and/or a fresh `meta` bag. */
|
|
27
|
+
export interface SaveSlotWrite {
|
|
28
|
+
/** Player-facing label for the slot. Omit to keep the existing name. */
|
|
29
|
+
name?: string;
|
|
30
|
+
/** Replacement free-string display fields. Omit to keep the existing `meta`. */
|
|
31
|
+
meta?: Record<string, string | number>;
|
|
32
|
+
}
|
|
33
|
+
/** How a {@link createSaveSlots} index is wired. */
|
|
34
|
+
export interface SaveSlotsConfig {
|
|
35
|
+
/** Injected clock (ms) used to stamp `savedAt`. Default `Date.now`. */
|
|
36
|
+
now?: () => number;
|
|
37
|
+
/**
|
|
38
|
+
* Initial slots, in menu order. Any slot omitting `empty` is treated as empty.
|
|
39
|
+
* When `capacity` is set and this is shorter, the list is padded with empty
|
|
40
|
+
* slots (`slot-1`, `slot-2`, …) up to `capacity`.
|
|
41
|
+
*/
|
|
42
|
+
slots?: SaveSlotMeta[];
|
|
43
|
+
/**
|
|
44
|
+
* Fixed number of slots the menu shows. When set, the index is padded with
|
|
45
|
+
* empty slots up to this count and never grows past it. Omit for an
|
|
46
|
+
* open-ended list that only holds the slots you seed.
|
|
47
|
+
*/
|
|
48
|
+
capacity?: number;
|
|
49
|
+
}
|
|
50
|
+
/** Serializable state of a {@link SaveSlots} index, for save/restore. */
|
|
51
|
+
export interface SaveSlotsSnapshot {
|
|
52
|
+
/** The slots in menu order. */
|
|
53
|
+
slots: SaveSlotMeta[];
|
|
54
|
+
/** The fixed capacity, or `undefined` for an open-ended list. */
|
|
55
|
+
capacity: number | undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A serializable, observable index of save/profile slot *metadata* that a
|
|
59
|
+
* save-select menu renders. It complements `createSaveStore` (which owns the
|
|
60
|
+
* actual save payload and named slots) by carrying the per-slot display fields
|
|
61
|
+
* the store does not: name, empty flag, last-saved time, and a free-string
|
|
62
|
+
* `meta` bag the game fills. Nothing here is genre-specific.
|
|
63
|
+
*/
|
|
64
|
+
export interface SaveSlots {
|
|
65
|
+
/**
|
|
66
|
+
* Stamp a slot as written: sets `empty=false`, records `savedAt` via the
|
|
67
|
+
* injected clock, and applies the given `name`/`meta`. Creates the slot if it
|
|
68
|
+
* does not exist (subject to `capacity`). Returns the resulting slot record.
|
|
69
|
+
*/
|
|
70
|
+
write(id: string, write?: SaveSlotWrite): SaveSlotMeta;
|
|
71
|
+
/** Mark a slot empty, dropping its `name`, `savedAt`, and `meta`. Returns the cleared record, or `null` if the id is unknown. */
|
|
72
|
+
clear(id: string): SaveSlotMeta | null;
|
|
73
|
+
/** Rename a slot without touching its `savedAt`/`meta`. Returns the updated record, or `null` if the id is unknown. */
|
|
74
|
+
rename(id: string, name: string): SaveSlotMeta | null;
|
|
75
|
+
/** The slot with this id, or `null`. The returned record is a copy — mutating it does not affect the index. */
|
|
76
|
+
get(id: string): SaveSlotMeta | null;
|
|
77
|
+
/** All slots in menu order (copies). Powers the save-select grid. */
|
|
78
|
+
list(): SaveSlotMeta[];
|
|
79
|
+
/** The newest non-empty slot (highest `savedAt`), or `null` when every slot is empty — powers "Continue". */
|
|
80
|
+
mostRecent(): SaveSlotMeta | null;
|
|
81
|
+
/** Observe any change (write/clear/rename/restore). Returns an unsubscribe fn. */
|
|
82
|
+
subscribe(listener: () => void): () => void;
|
|
83
|
+
/** Serializable state for a save. */
|
|
84
|
+
snapshot(): SaveSlotsSnapshot;
|
|
85
|
+
/** Restore from a {@link SaveSlotsSnapshot}, replacing all slots. */
|
|
86
|
+
restore(snapshot: SaveSlotsSnapshot): void;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a {@link SaveSlots} index — the serializable, observable list of
|
|
90
|
+
* save/profile slot metadata a save-select menu renders. It is a thin companion
|
|
91
|
+
* to `createSaveStore`: the store owns the real save payload keyed by slot, this
|
|
92
|
+
* owns only the display fields (name, empty, last-saved, and a free-string
|
|
93
|
+
* `meta` bag) the menu shows. `write` stamps a slot saved on the injected clock,
|
|
94
|
+
* `clear` empties one, `rename` relabels one, `mostRecent()` returns the newest
|
|
95
|
+
* non-empty slot to power a "Continue" button, and `list()` feeds the New /
|
|
96
|
+
* Continue / Load / Delete grid. `meta` keys are free strings the game owns —
|
|
97
|
+
* the model never interprets them — and `snapshot`/`restore` round-trips the
|
|
98
|
+
* whole index through a save.
|
|
99
|
+
*
|
|
100
|
+
* @capability save-slots serializable save-slot / profile metadata index — write/clear/rename/mostRecent over free-string per-slot meta, powering New/Continue/Load/Delete menus, with snapshot/restore
|
|
101
|
+
*/
|
|
102
|
+
export declare function createSaveSlots(config?: SaveSlotsConfig): SaveSlots;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
function emptySlot(id) {
|
|
2
|
+
return { id, empty: true, meta: {} };
|
|
3
|
+
}
|
|
4
|
+
function cloneSlot(slot) {
|
|
5
|
+
return {
|
|
6
|
+
id: slot.id,
|
|
7
|
+
name: slot.name,
|
|
8
|
+
empty: slot.empty,
|
|
9
|
+
savedAt: slot.savedAt,
|
|
10
|
+
meta: { ...slot.meta },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function normalizeSlot(slot) {
|
|
14
|
+
const empty = slot.empty ?? true;
|
|
15
|
+
return {
|
|
16
|
+
id: slot.id,
|
|
17
|
+
name: slot.name,
|
|
18
|
+
empty,
|
|
19
|
+
savedAt: empty ? undefined : slot.savedAt,
|
|
20
|
+
meta: empty ? {} : { ...(slot.meta ?? {}) },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a {@link SaveSlots} index — the serializable, observable list of
|
|
25
|
+
* save/profile slot metadata a save-select menu renders. It is a thin companion
|
|
26
|
+
* to `createSaveStore`: the store owns the real save payload keyed by slot, this
|
|
27
|
+
* owns only the display fields (name, empty, last-saved, and a free-string
|
|
28
|
+
* `meta` bag) the menu shows. `write` stamps a slot saved on the injected clock,
|
|
29
|
+
* `clear` empties one, `rename` relabels one, `mostRecent()` returns the newest
|
|
30
|
+
* non-empty slot to power a "Continue" button, and `list()` feeds the New /
|
|
31
|
+
* Continue / Load / Delete grid. `meta` keys are free strings the game owns —
|
|
32
|
+
* the model never interprets them — and `snapshot`/`restore` round-trips the
|
|
33
|
+
* whole index through a save.
|
|
34
|
+
*
|
|
35
|
+
* @capability save-slots serializable save-slot / profile metadata index — write/clear/rename/mostRecent over free-string per-slot meta, powering New/Continue/Load/Delete menus, with snapshot/restore
|
|
36
|
+
*/
|
|
37
|
+
export function createSaveSlots(config = {}) {
|
|
38
|
+
const now = config.now ?? Date.now;
|
|
39
|
+
let capacity = config.capacity;
|
|
40
|
+
const slots = (config.slots ?? []).map(normalizeSlot);
|
|
41
|
+
const seen = new Set(slots.map((slot) => slot.id));
|
|
42
|
+
if (capacity !== undefined) {
|
|
43
|
+
let n = 1;
|
|
44
|
+
while (slots.length < capacity) {
|
|
45
|
+
let id = `slot-${n}`;
|
|
46
|
+
while (seen.has(id)) {
|
|
47
|
+
n += 1;
|
|
48
|
+
id = `slot-${n}`;
|
|
49
|
+
}
|
|
50
|
+
seen.add(id);
|
|
51
|
+
slots.push(emptySlot(id));
|
|
52
|
+
n += 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const listeners = new Set();
|
|
56
|
+
function notify() {
|
|
57
|
+
for (const listener of listeners)
|
|
58
|
+
listener();
|
|
59
|
+
}
|
|
60
|
+
function indexOf(id) {
|
|
61
|
+
return slots.findIndex((slot) => slot.id === id);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
write(id, write) {
|
|
65
|
+
let i = indexOf(id);
|
|
66
|
+
if (i === -1) {
|
|
67
|
+
if (capacity !== undefined && slots.length >= capacity) {
|
|
68
|
+
throw new Error(`save-slots: cannot create slot "${id}" — capacity ${capacity} reached`);
|
|
69
|
+
}
|
|
70
|
+
slots.push(emptySlot(id));
|
|
71
|
+
i = slots.length - 1;
|
|
72
|
+
}
|
|
73
|
+
const slot = slots[i];
|
|
74
|
+
slot.empty = false;
|
|
75
|
+
slot.savedAt = now();
|
|
76
|
+
if (write?.name !== undefined)
|
|
77
|
+
slot.name = write.name;
|
|
78
|
+
if (write?.meta !== undefined)
|
|
79
|
+
slot.meta = { ...write.meta };
|
|
80
|
+
notify();
|
|
81
|
+
return cloneSlot(slot);
|
|
82
|
+
},
|
|
83
|
+
clear(id) {
|
|
84
|
+
const i = indexOf(id);
|
|
85
|
+
if (i === -1)
|
|
86
|
+
return null;
|
|
87
|
+
const slot = slots[i];
|
|
88
|
+
slot.empty = true;
|
|
89
|
+
slot.name = undefined;
|
|
90
|
+
slot.savedAt = undefined;
|
|
91
|
+
slot.meta = {};
|
|
92
|
+
notify();
|
|
93
|
+
return cloneSlot(slot);
|
|
94
|
+
},
|
|
95
|
+
rename(id, name) {
|
|
96
|
+
const i = indexOf(id);
|
|
97
|
+
if (i === -1)
|
|
98
|
+
return null;
|
|
99
|
+
const slot = slots[i];
|
|
100
|
+
slot.name = name;
|
|
101
|
+
notify();
|
|
102
|
+
return cloneSlot(slot);
|
|
103
|
+
},
|
|
104
|
+
get(id) {
|
|
105
|
+
const i = indexOf(id);
|
|
106
|
+
return i === -1 ? null : cloneSlot(slots[i]);
|
|
107
|
+
},
|
|
108
|
+
list() {
|
|
109
|
+
return slots.map(cloneSlot);
|
|
110
|
+
},
|
|
111
|
+
mostRecent() {
|
|
112
|
+
let best = null;
|
|
113
|
+
for (const slot of slots) {
|
|
114
|
+
if (slot.empty)
|
|
115
|
+
continue;
|
|
116
|
+
if (best === null || (slot.savedAt ?? 0) > (best.savedAt ?? 0))
|
|
117
|
+
best = slot;
|
|
118
|
+
}
|
|
119
|
+
return best === null ? null : cloneSlot(best);
|
|
120
|
+
},
|
|
121
|
+
subscribe(listener) {
|
|
122
|
+
listeners.add(listener);
|
|
123
|
+
return () => listeners.delete(listener);
|
|
124
|
+
},
|
|
125
|
+
snapshot() {
|
|
126
|
+
return { slots: slots.map(cloneSlot), capacity };
|
|
127
|
+
},
|
|
128
|
+
restore(snapshot) {
|
|
129
|
+
capacity = snapshot.capacity;
|
|
130
|
+
slots.length = 0;
|
|
131
|
+
for (const slot of snapshot.slots)
|
|
132
|
+
slots.push(normalizeSlot(slot));
|
|
133
|
+
notify();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|