@jgengine/core 0.13.0 → 0.15.0-next.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 +529 -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/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 +21 -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/context/scene.d.ts +2 -0
- package/dist/runtime/context/scene.js +48 -1
- package/dist/runtime/gameContext.js +8 -2
- package/dist/runtime/gameContextTypes.d.ts +39 -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/builtinSceneKinds.d.ts +6 -1
- package/dist/scene/builtinSceneKinds.js +10 -3
- package/dist/scene/captureCheck.js +3 -2
- package/dist/scene/colliders.d.ts +20 -0
- package/dist/scene/colliders.js +78 -0
- package/dist/scene/collisionMesh.d.ts +8 -0
- package/dist/scene/collisionMesh.js +48 -0
- 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 +37 -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/hudScale.d.ts +16 -1
- package/dist/ui/hudScale.js +22 -1
- 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 +9 -46
- 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/grassKind.js +4 -1
- 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/scatterCoverage.d.ts +98 -0
- package/dist/world/scatterCoverage.js +101 -0
- package/dist/world/scatterRegion.js +6 -0
- 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 +18 -2
- package/dist/world.js +18 -2
- package/package.json +2 -2
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +7 -3
- 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 +408 -12
- package/skills/jgengine-world/capabilities.md +127 -2
- package/skills/jgengine-world/reference.md +15 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { TalentNodeDef, TalentTree } from "./talents.js";
|
|
2
|
+
/**
|
|
3
|
+
* Render state of a talent node, derived from its rank and prerequisite satisfaction.
|
|
4
|
+
* - `locked` — rank 0 and at least one prerequisite unmet; cannot be trained yet.
|
|
5
|
+
* - `available` — rank 0, every prerequisite met; ready to take a first point.
|
|
6
|
+
* - `learned` — at least one rank invested, but below `maxRank`.
|
|
7
|
+
* - `maxed` — fully invested (`rank === maxRank`).
|
|
8
|
+
*/
|
|
9
|
+
export type TalentNodeState = "locked" | "available" | "learned" | "maxed";
|
|
10
|
+
/** One prerequisite edge into a node — the source node, the rank it demands, and whether that is met. */
|
|
11
|
+
export interface TalentEdgeView {
|
|
12
|
+
/** Prerequisite node id (edge source). */
|
|
13
|
+
from: string;
|
|
14
|
+
/** Rank the prerequisite must reach for this edge to be satisfied. */
|
|
15
|
+
rank: number;
|
|
16
|
+
/** Whether the prerequisite currently sits at or above the required rank. */
|
|
17
|
+
met: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** A per-node view for rendering: grid placement (tier/branch), rank, state, and inbound prerequisite edges. */
|
|
20
|
+
export interface TalentNodeView {
|
|
21
|
+
/** Stable node id (matches the {@link TalentNodeDef}). */
|
|
22
|
+
id: string;
|
|
23
|
+
/** Branch key the node belongs to (`""` when the def has none) — a free string the game styles/columns by. */
|
|
24
|
+
branch: string;
|
|
25
|
+
/** 0-based prerequisite depth: `0` for a root, else `1 + max(tier of prerequisites)`. Drives row/tier layout. */
|
|
26
|
+
tier: number;
|
|
27
|
+
/** Points currently invested in the node. */
|
|
28
|
+
rank: number;
|
|
29
|
+
/** Maximum points the node accepts. */
|
|
30
|
+
maxRank: number;
|
|
31
|
+
/** Derived render state (see {@link TalentNodeState}). */
|
|
32
|
+
state: TalentNodeState;
|
|
33
|
+
/** Whether a point can be spent on the node right now (mirrors `tree.canAllocate(id).ok`). */
|
|
34
|
+
allocatable: boolean;
|
|
35
|
+
/** Inbound prerequisite edges, in definition order. */
|
|
36
|
+
requires: readonly TalentEdgeView[];
|
|
37
|
+
}
|
|
38
|
+
/** A whole-tree render view: placed nodes plus branch/tier extents and point totals. */
|
|
39
|
+
export interface TalentTreeView {
|
|
40
|
+
/** Every node, in definition order, with its render placement and state. */
|
|
41
|
+
nodes: readonly TalentNodeView[];
|
|
42
|
+
/** Distinct branch keys, in first-seen (definition) order — the natural column order. */
|
|
43
|
+
branches: readonly string[];
|
|
44
|
+
/** Number of tiers, i.e. `max(tier) + 1` (0 when the tree is empty). */
|
|
45
|
+
tiers: number;
|
|
46
|
+
/** Points the player still has to spend. */
|
|
47
|
+
pointsAvailable: number;
|
|
48
|
+
/** Points already invested across the tree. */
|
|
49
|
+
pointsSpent: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Per-node status a game supplies to build a view from *any* unlock rule — not just point-spend.
|
|
53
|
+
* A game computes these from whatever it wants (a currency threshold, a level, a quest flag, a
|
|
54
|
+
* skill-point pool, nothing at all) so the same widget renders a buy-with-points talent tree, a
|
|
55
|
+
* money-gated upgrade tree, or a condition-unlocked ability web without changing the renderer.
|
|
56
|
+
*/
|
|
57
|
+
export interface TalentNodeStatus {
|
|
58
|
+
/** Points/levels invested in the node — `0` means "not taken yet"; a binary unlock uses `0`/`1`. */
|
|
59
|
+
rank: number;
|
|
60
|
+
/**
|
|
61
|
+
* Whether the node can be taken right now under the game's own rule (enough points, enough cash,
|
|
62
|
+
* a level reached, a flag set, …). Drives the `available` state and whether the widget fires
|
|
63
|
+
* `onLearn` for it. The view never interprets *why* — that stays the game's decision.
|
|
64
|
+
*/
|
|
65
|
+
allocatable: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** Optional overall point totals for the view header; omit (or 0) for trees that spend no point currency. */
|
|
68
|
+
export interface TalentPointTotals {
|
|
69
|
+
/** Points the player still has to spend. Default `0`. */
|
|
70
|
+
pointsAvailable?: number;
|
|
71
|
+
/** Points already invested across the tree. Default `0`. */
|
|
72
|
+
pointsSpent?: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build a flat, serializable render view of a node graph from a caller-supplied per-node status,
|
|
76
|
+
* decoupled from any particular progression model. For each node the game returns its `rank` and
|
|
77
|
+
* whether it is `allocatable` *right now* under whatever rule it likes — a skill-point pool, a cash
|
|
78
|
+
* threshold, a level gate, a quest flag, or nothing. The builder places every node by branch +
|
|
79
|
+
* prerequisite-depth tier, tags it learned/available/locked/maxed (`available` when rank 0 and every
|
|
80
|
+
* prerequisite is met — see below), and resolves inbound prerequisite edges (each `met` when the
|
|
81
|
+
* source node's supplied rank meets the demand). This is the general escape hatch behind
|
|
82
|
+
* {@link talentTreeView}: it lets the same widget render a buy-with-points talent tree, a money-gated
|
|
83
|
+
* upgrade tree, or a condition-unlocked ability web without the renderer changing. It never interprets
|
|
84
|
+
* what a node *means* — ids, branches, and ranks stay opaque game data.
|
|
85
|
+
*
|
|
86
|
+
* `state` is derived purely from the supplied `rank` vs `maxRank` and prerequisite satisfaction, so a
|
|
87
|
+
* node the game considers takeable-by-condition surfaces as `available` (rank 0, prerequisites met)
|
|
88
|
+
* and the widget only fires `onLearn` for nodes whose `allocatable` the game returned `true`.
|
|
89
|
+
*
|
|
90
|
+
* @capability talent-tree-view-from build a placed talent/upgrade-tree render view from any per-node unlock rule (points, currency, level, flag) — branch/tier layout, learned/available/locked/maxed state, prerequisite edges
|
|
91
|
+
*/
|
|
92
|
+
export declare function talentTreeViewFrom<TStat extends string = string>(nodes: readonly TalentNodeDef<TStat>[], status: (node: TalentNodeDef<TStat>) => TalentNodeStatus, totals?: TalentPointTotals): TalentTreeView;
|
|
93
|
+
/**
|
|
94
|
+
* Project the existing talent model (`createTalentTree`) into a flat, serializable render view: every
|
|
95
|
+
* node placed by branch + prerequisite-depth tier, tagged learned/available/locked/maxed, with its
|
|
96
|
+
* inbound prerequisite edges (and whether each is met) and whether a point can be spent right now. It is
|
|
97
|
+
* a pure read over the node defs plus a live {@link TalentTree}, so a React/canvas widget can lay out
|
|
98
|
+
* nodes, draw edges, and gate clicks without re-deriving topology or re-interpreting eligibility. The
|
|
99
|
+
* model still owns allocation, requirements, and point rules — this only reshapes them for drawing, and
|
|
100
|
+
* never interprets what a node *means* (ids, branches, and ranks stay opaque game data). This is the
|
|
101
|
+
* point-spend adapter over {@link talentTreeViewFrom}; pass a custom status to that builder instead to
|
|
102
|
+
* drive the tree from a currency, level, or any other unlock rule.
|
|
103
|
+
*
|
|
104
|
+
* @capability talent-tree-view flatten a talent tree into a placed, per-node render view — branch/tier layout, learned/available/locked/maxed state, prerequisite edges
|
|
105
|
+
*/
|
|
106
|
+
export declare function talentTreeView<TStat extends string = string>(nodes: readonly TalentNodeDef<TStat>[], tree: TalentTree<TStat>): TalentTreeView;
|