@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
|
@@ -4,46 +4,94 @@
|
|
|
4
4
|
* @capability regen-shield a rechargeable overshield that absorbs damage and refills after a lull
|
|
5
5
|
*/
|
|
6
6
|
export function createRegenShield(config) {
|
|
7
|
-
|
|
7
|
+
const pool = config.pool;
|
|
8
8
|
const regenPerSecond = Math.max(0, config.regenPerSecond);
|
|
9
9
|
const regenDelayMs = Math.max(0, config.regenDelayMs);
|
|
10
10
|
function clamp(value, ceiling) {
|
|
11
11
|
return Math.max(0, Math.min(ceiling, value));
|
|
12
12
|
}
|
|
13
|
-
let
|
|
13
|
+
let ownMax = Math.max(0, config.max);
|
|
14
|
+
let ownCurrent = clamp(config.current ?? ownMax, ownMax);
|
|
14
15
|
let sinceDamageMs = regenDelayMs;
|
|
16
|
+
/** Grace period currently in force — the configured one until a `tick` overrides it. */
|
|
17
|
+
let activeDelayMs = regenDelayMs;
|
|
18
|
+
const readMax = () => (pool === undefined ? ownMax : Math.max(0, pool.max()));
|
|
19
|
+
const readCurrent = () => (pool === undefined ? ownCurrent : clamp(pool.current(), readMax()));
|
|
20
|
+
function write(value) {
|
|
21
|
+
const next = clamp(value, readMax());
|
|
22
|
+
if (pool === undefined)
|
|
23
|
+
ownCurrent = next;
|
|
24
|
+
else
|
|
25
|
+
pool.set(next);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Last value this shield knows about, so a pool-backed shield can tell its own writes apart from
|
|
29
|
+
* damage applied elsewhere. `null` until the first observation, so the first tick never reads a
|
|
30
|
+
* cold start as a hit.
|
|
31
|
+
*/
|
|
32
|
+
let observed = pool === undefined ? null : readCurrent();
|
|
33
|
+
/** Detect an external decrease in a pool-backed value and restart the grace period if one happened. */
|
|
34
|
+
function syncExternal() {
|
|
35
|
+
if (pool === undefined)
|
|
36
|
+
return;
|
|
37
|
+
const live = readCurrent();
|
|
38
|
+
if (observed !== null && live < observed)
|
|
39
|
+
sinceDamageMs = 0;
|
|
40
|
+
observed = live;
|
|
41
|
+
}
|
|
15
42
|
return {
|
|
16
|
-
current:
|
|
17
|
-
max:
|
|
43
|
+
current: readCurrent,
|
|
44
|
+
max: readMax,
|
|
18
45
|
setMax(value) {
|
|
19
|
-
|
|
20
|
-
|
|
46
|
+
// A pool owns its own bounds (an entity stat's max, a save record's cap); resizing it here
|
|
47
|
+
// would silently disagree with whatever else writes that storage.
|
|
48
|
+
if (pool !== undefined)
|
|
49
|
+
return;
|
|
50
|
+
ownMax = Math.max(0, value);
|
|
51
|
+
ownCurrent = clamp(ownCurrent, ownMax);
|
|
21
52
|
},
|
|
22
|
-
fraction: () => (
|
|
23
|
-
isFull: () =>
|
|
24
|
-
isBroken: () =>
|
|
25
|
-
suppressed: () => sinceDamageMs <
|
|
53
|
+
fraction: () => (readMax() <= 0 ? 0 : readCurrent() / readMax()),
|
|
54
|
+
isFull: () => readCurrent() >= readMax(),
|
|
55
|
+
isBroken: () => readCurrent() <= 0,
|
|
56
|
+
suppressed: () => sinceDamageMs < activeDelayMs,
|
|
26
57
|
damage(amount) {
|
|
27
58
|
if (amount <= 0)
|
|
28
59
|
return 0;
|
|
60
|
+
syncExternal();
|
|
29
61
|
sinceDamageMs = 0;
|
|
30
|
-
const
|
|
31
|
-
|
|
62
|
+
const live = readCurrent();
|
|
63
|
+
const overflow = Math.max(0, amount - live);
|
|
64
|
+
write(live - amount);
|
|
65
|
+
observed = pool === undefined ? observed : readCurrent();
|
|
32
66
|
return overflow;
|
|
33
67
|
},
|
|
34
68
|
restore(amount) {
|
|
35
|
-
if (amount
|
|
36
|
-
|
|
69
|
+
if (amount <= 0)
|
|
70
|
+
return;
|
|
71
|
+
syncExternal();
|
|
72
|
+
write(readCurrent() + amount);
|
|
73
|
+
observed = pool === undefined ? observed : readCurrent();
|
|
37
74
|
},
|
|
38
75
|
set(value) {
|
|
39
|
-
|
|
76
|
+
write(value);
|
|
77
|
+
observed = pool === undefined ? observed : readCurrent();
|
|
78
|
+
},
|
|
79
|
+
noteDamage() {
|
|
80
|
+
syncExternal();
|
|
81
|
+
sinceDamageMs = 0;
|
|
40
82
|
},
|
|
41
|
-
tick(dtSeconds) {
|
|
83
|
+
tick(dtSeconds, rates) {
|
|
84
|
+
activeDelayMs = Math.max(0, rates?.regenDelayMs ?? regenDelayMs);
|
|
42
85
|
if (dtSeconds <= 0)
|
|
43
86
|
return;
|
|
87
|
+
// Advance time first, THEN observe: damage that landed since the last tick resets the timer to
|
|
88
|
+
// zero for this tick, so the tick that sees a hit never also regenerates over it.
|
|
44
89
|
sinceDamageMs += dtSeconds * 1000;
|
|
45
|
-
|
|
46
|
-
|
|
90
|
+
syncExternal();
|
|
91
|
+
if (sinceDamageMs < activeDelayMs)
|
|
92
|
+
return;
|
|
93
|
+
write(readCurrent() + Math.max(0, rates?.regenPerSecond ?? regenPerSecond) * dtSeconds);
|
|
94
|
+
observed = pool === undefined ? observed : readCurrent();
|
|
47
95
|
},
|
|
48
96
|
};
|
|
49
97
|
}
|
|
@@ -4,6 +4,19 @@ export interface ResourcePoolConfig {
|
|
|
4
4
|
regenPerSecond?: number;
|
|
5
5
|
decayPerSecond?: number;
|
|
6
6
|
}
|
|
7
|
+
/** Complete serializable state for a {@link ResourcePool}. */
|
|
8
|
+
export interface ResourcePoolSnapshot {
|
|
9
|
+
current: number;
|
|
10
|
+
max: number;
|
|
11
|
+
regenPerSecond: number;
|
|
12
|
+
decayPerSecond: number;
|
|
13
|
+
}
|
|
14
|
+
/** Runtime tuning patch for {@link ResourcePool.retune} — omitted fields keep their current value. */
|
|
15
|
+
export interface ResourcePoolRetune {
|
|
16
|
+
max?: number;
|
|
17
|
+
regenPerSecond?: number;
|
|
18
|
+
decayPerSecond?: number;
|
|
19
|
+
}
|
|
7
20
|
export interface ResourcePool {
|
|
8
21
|
current(): number;
|
|
9
22
|
max(): number;
|
|
@@ -16,6 +29,12 @@ export interface ResourcePool {
|
|
|
16
29
|
tick(dtSeconds: number): void;
|
|
17
30
|
isFull(): boolean;
|
|
18
31
|
isEmpty(): boolean;
|
|
32
|
+
/** The whole pool as plain data — save it, ship it to a host, diff it in a test. */
|
|
33
|
+
snapshot(): ResourcePoolSnapshot;
|
|
34
|
+
/** Adopt a snapshot, replacing current value and tuning. */
|
|
35
|
+
restore(state: ResourcePoolSnapshot): void;
|
|
36
|
+
/** Change regen/decay/max mid-run (a buff, a difficulty change) without rebuilding the pool. */
|
|
37
|
+
retune(patch: ResourcePoolRetune): void;
|
|
19
38
|
}
|
|
20
39
|
/**
|
|
21
40
|
* A regenerating resource pool — mana, stamina, energy — that actions spend from and that refills over time.
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export function createResourcePool(config) {
|
|
7
7
|
let max = Math.max(0, config.max);
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
let regenPerSecond = Math.max(0, config.regenPerSecond ?? 0);
|
|
9
|
+
let decayPerSecond = Math.max(0, config.decayPerSecond ?? 0);
|
|
10
10
|
let current = clamp(config.initial ?? max, max);
|
|
11
11
|
function clamp(value, ceiling) {
|
|
12
12
|
return Math.max(0, Math.min(ceiling, value));
|
|
@@ -56,5 +56,23 @@ export function createResourcePool(config) {
|
|
|
56
56
|
isEmpty() {
|
|
57
57
|
return current <= 0;
|
|
58
58
|
},
|
|
59
|
+
snapshot() {
|
|
60
|
+
return { current, max, regenPerSecond, decayPerSecond };
|
|
61
|
+
},
|
|
62
|
+
restore(state) {
|
|
63
|
+
max = Math.max(0, state.max);
|
|
64
|
+
regenPerSecond = Math.max(0, state.regenPerSecond);
|
|
65
|
+
decayPerSecond = Math.max(0, state.decayPerSecond);
|
|
66
|
+
current = clamp(state.current, max);
|
|
67
|
+
},
|
|
68
|
+
retune(patch) {
|
|
69
|
+
if (patch.max !== undefined)
|
|
70
|
+
max = Math.max(0, patch.max);
|
|
71
|
+
if (patch.regenPerSecond !== undefined)
|
|
72
|
+
regenPerSecond = Math.max(0, patch.regenPerSecond);
|
|
73
|
+
if (patch.decayPerSecond !== undefined)
|
|
74
|
+
decayPerSecond = Math.max(0, patch.decayPerSecond);
|
|
75
|
+
current = clamp(current, max);
|
|
76
|
+
},
|
|
59
77
|
};
|
|
60
78
|
}
|
|
@@ -43,6 +43,11 @@ export type ShotOriginPolicy = {
|
|
|
43
43
|
origin: EntityPosition;
|
|
44
44
|
direction?: EntityPosition;
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* A shot's resolved firing geometry in world space: `origin` is the point the projectile/ray starts
|
|
48
|
+
* from and `direction` is its normalized aim vector, both computed by {@link resolveShot} from the
|
|
49
|
+
* shooter's position, facing, and the chosen origin policy.
|
|
50
|
+
*/
|
|
46
51
|
export interface ResolvedShot {
|
|
47
52
|
origin: EntityPosition;
|
|
48
53
|
direction: EntityPosition;
|
|
@@ -70,7 +70,7 @@ export interface StatusApplicationInput {
|
|
|
70
70
|
immunity?: StatusImmunity | null;
|
|
71
71
|
/** Attribution written onto the resulting instance. */
|
|
72
72
|
source?: string;
|
|
73
|
-
/** Deterministic RNG returning `[0,1)
|
|
73
|
+
/** Deterministic RNG returning `[0,1)`. Omitting it falls back to `Math.random` and warns once — pass `ctx.rng` or a seeded stream. */
|
|
74
74
|
rng?: () => number;
|
|
75
75
|
}
|
|
76
76
|
/** Why an application did or did not take, plus the roll for provenance. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveRng } from "../random/resolveRng.js";
|
|
1
2
|
function clamp01(value) {
|
|
2
3
|
if (value < 0)
|
|
3
4
|
return 0;
|
|
@@ -15,7 +16,7 @@ function clamp01(value) {
|
|
|
15
16
|
*/
|
|
16
17
|
export function resolveStatusApplication(input) {
|
|
17
18
|
const { spec, scale, current, immunity, source } = input;
|
|
18
|
-
const rng = input.rng
|
|
19
|
+
const rng = resolveRng(input.rng, "applyStatus");
|
|
19
20
|
if (immunity?.statuses?.includes(spec.status) === true) {
|
|
20
21
|
return { kind: "immune", instance: current ?? null, chance: 0, roll: null };
|
|
21
22
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { StatusInstance } from "./statusApplication.js";
|
|
2
|
+
/**
|
|
3
|
+
* A serializable, presentation-ready snapshot of one active status for a timeline HUD row:
|
|
4
|
+
* a stable list `id`, the free-string `kind` the game styles (color / label / icon), the
|
|
5
|
+
* countdown-ring inputs `remainingMs` / `durationMs`, the `stacks` badge count, and the optional
|
|
6
|
+
* `magnitude` in force. Derived from a live {@link StatusInstance} via {@link toStatusEffectView}.
|
|
7
|
+
* The engine never interprets `kind` — it is a reference the game skins, not a built-in condition.
|
|
8
|
+
*/
|
|
9
|
+
export interface StatusEffectView {
|
|
10
|
+
/** Stable list key. Defaults to the status id; override when several instances share a status. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** The free-string status id (`"poison"`, `"haste"`, `"shield"`) — the game styles it. */
|
|
13
|
+
kind: string;
|
|
14
|
+
/** Milliseconds of life remaining (the numerator of the ring fraction). */
|
|
15
|
+
remainingMs: number;
|
|
16
|
+
/** The status's full (unelapsed) duration in ms (the ring denominator). */
|
|
17
|
+
durationMs: number;
|
|
18
|
+
/** Current stack count (>= 1 while active). */
|
|
19
|
+
stacks: number;
|
|
20
|
+
/** Per-application magnitude in force, when the source status tracks one. */
|
|
21
|
+
magnitude?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Options for {@link toStatusEffectView} / {@link toStatusEffectViews}. */
|
|
24
|
+
export interface StatusEffectViewOptions {
|
|
25
|
+
/**
|
|
26
|
+
* The status's full (unelapsed) duration in ms — the countdown-ring denominator. A fixed number,
|
|
27
|
+
* or a resolver keyed off the instance (base durations usually live in the game's spec table, not
|
|
28
|
+
* on the live {@link StatusInstance}, which only carries `remainingMs`). When omitted or
|
|
29
|
+
* non-positive for an instance, the view reports `durationMs === remainingMs` so the ring shows
|
|
30
|
+
* full — the elapsed fraction is simply unknown, never negative.
|
|
31
|
+
*/
|
|
32
|
+
durationMs?: number | ((instance: StatusInstance) => number);
|
|
33
|
+
/** Derive the stable list key; defaults to the status id. */
|
|
34
|
+
id?: (instance: StatusInstance) => string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Map one live {@link StatusInstance} to a serializable {@link StatusEffectView} for a HUD row.
|
|
38
|
+
* Pure: no clock, no allocation beyond the returned view. The `durationMs` option supplies the ring
|
|
39
|
+
* denominator the instance does not carry; without it the ring reads full.
|
|
40
|
+
*
|
|
41
|
+
* @capability status-effect-view adapt one live status instance into a serializable timeline-HUD view (id, kind, remaining/duration, stacks)
|
|
42
|
+
*/
|
|
43
|
+
export declare function toStatusEffectView(instance: StatusInstance, options?: StatusEffectViewOptions): StatusEffectView;
|
|
44
|
+
/**
|
|
45
|
+
* Map a list of live statuses (a target's carried {@link StatusInstance}s) to ordered HUD views.
|
|
46
|
+
* Pure and allocation-bounded — one pass, one view per input — so a status bar can render straight
|
|
47
|
+
* off the existing status model's snapshot without re-deriving ring math or timers.
|
|
48
|
+
*
|
|
49
|
+
* @capability status-effect-timeline adapt live status instances into serializable timeline-HUD views (id, kind, remaining/duration, stacks) for a countdown-ring effect bar
|
|
50
|
+
*/
|
|
51
|
+
export declare function toStatusEffectViews(instances: readonly StatusInstance[], options?: StatusEffectViewOptions): StatusEffectView[];
|
|
52
|
+
/**
|
|
53
|
+
* Fraction of the countdown still remaining, clamped to `[0,1]` — the value a countdown ring fills
|
|
54
|
+
* to. Returns `0` when the duration is non-positive. Pure; safe to call every frame.
|
|
55
|
+
*/
|
|
56
|
+
export declare function statusEffectRemainingFraction(view: StatusEffectView): number;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function resolveDurationMs(instance, options) {
|
|
2
|
+
const raw = typeof options?.durationMs === "function" ? options.durationMs(instance) : options?.durationMs;
|
|
3
|
+
if (raw === undefined || !Number.isFinite(raw) || raw <= 0)
|
|
4
|
+
return instance.remainingMs;
|
|
5
|
+
return Math.max(raw, instance.remainingMs);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Map one live {@link StatusInstance} to a serializable {@link StatusEffectView} for a HUD row.
|
|
9
|
+
* Pure: no clock, no allocation beyond the returned view. The `durationMs` option supplies the ring
|
|
10
|
+
* denominator the instance does not carry; without it the ring reads full.
|
|
11
|
+
*
|
|
12
|
+
* @capability status-effect-view adapt one live status instance into a serializable timeline-HUD view (id, kind, remaining/duration, stacks)
|
|
13
|
+
*/
|
|
14
|
+
export function toStatusEffectView(instance, options) {
|
|
15
|
+
const view = {
|
|
16
|
+
id: options?.id?.(instance) ?? instance.status,
|
|
17
|
+
kind: instance.status,
|
|
18
|
+
remainingMs: Math.max(0, instance.remainingMs),
|
|
19
|
+
durationMs: resolveDurationMs(instance, options),
|
|
20
|
+
stacks: instance.stacks,
|
|
21
|
+
};
|
|
22
|
+
if (instance.magnitude !== 0)
|
|
23
|
+
view.magnitude = instance.magnitude;
|
|
24
|
+
return view;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Map a list of live statuses (a target's carried {@link StatusInstance}s) to ordered HUD views.
|
|
28
|
+
* Pure and allocation-bounded — one pass, one view per input — so a status bar can render straight
|
|
29
|
+
* off the existing status model's snapshot without re-deriving ring math or timers.
|
|
30
|
+
*
|
|
31
|
+
* @capability status-effect-timeline adapt live status instances into serializable timeline-HUD views (id, kind, remaining/duration, stacks) for a countdown-ring effect bar
|
|
32
|
+
*/
|
|
33
|
+
export function toStatusEffectViews(instances, options) {
|
|
34
|
+
const views = [];
|
|
35
|
+
for (const instance of instances)
|
|
36
|
+
views.push(toStatusEffectView(instance, options));
|
|
37
|
+
return views;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fraction of the countdown still remaining, clamped to `[0,1]` — the value a countdown ring fills
|
|
41
|
+
* to. Returns `0` when the duration is non-positive. Pure; safe to call every frame.
|
|
42
|
+
*/
|
|
43
|
+
export function statusEffectRemainingFraction(view) {
|
|
44
|
+
if (view.durationMs <= 0)
|
|
45
|
+
return 0;
|
|
46
|
+
const fraction = view.remainingMs / view.durationMs;
|
|
47
|
+
if (fraction < 0)
|
|
48
|
+
return 0;
|
|
49
|
+
if (fraction > 1)
|
|
50
|
+
return 1;
|
|
51
|
+
return fraction;
|
|
52
|
+
}
|
package/dist/combat.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { type AnimationClip, type FrameRange } from "./combat/animationState.js"
|
|
|
3
3
|
export { attackMeta, counters, isBlockable, isDodgeable, isParryable } from "./combat/attackTags.js";
|
|
4
4
|
export { createBuildupMeter, createStaggerMeter, type BuildupProc } from "./combat/breakMeters.js";
|
|
5
5
|
export { createCastRunner } from "./combat/castRunner.js";
|
|
6
|
+
export { createComboMeter, type ComboMeter, type ComboMeterConfig, type ComboMeterSnapshot, type ComboMeterView, type ComboTier, } from "./combat/comboMeter.js";
|
|
6
7
|
export { createComboPoints } from "./combat/comboPoints.js";
|
|
7
8
|
export { advanceCombo, createComboRunner, type ComboStep } from "./combat/comboString.js";
|
|
8
9
|
export { MAX_INTERCEPT_STEPS, createAntiOneShotPolicy, createDamageClamp, createDamagePipeline, createImmunityWindow, resolveDamage, type AntiOneShotConfig, type AntiOneShotPolicy, type DamageClampConfig, type DamageInterceptor, type DamagePipeline, type DamageResolution, type ImmunityWindow, type InterceptContext, type InterceptDecision, type InterceptRecord, type PendingDamage, } from "./combat/damageInterceptors.js";
|
|
@@ -10,7 +11,7 @@ export { resolveMatchup, type DamageChannelId, type DamageMatchup, type MatchupO
|
|
|
10
11
|
export { resolveDamageHit, type DamageHitInput, type DamageHitResolution } from "./combat/damageResolution.js";
|
|
11
12
|
export { createDeathSystem, type OnDeathSpec } from "./combat/death.js";
|
|
12
13
|
export { type DeathReason } from "./combat/deathReason.js";
|
|
13
|
-
export { createDefensiveWindow, resolveDefense } from "./combat/defensiveWindow.js";
|
|
14
|
+
export { createDefensiveWindow, resolveDefense, type DefenseResolution } from "./combat/defensiveWindow.js";
|
|
14
15
|
export { createDotField } from "./combat/dotField.js";
|
|
15
16
|
export { createDownedState } from "./combat/downed.js";
|
|
16
17
|
export { createEffectSystem, type ReceiveMap } from "./combat/effects.js";
|
|
@@ -24,8 +25,9 @@ export { DEFAULT_FIRE_PULSE_SECONDS, DEFAULT_HIT_PULSE_SECONDS, DEFAULT_RENDER_C
|
|
|
24
25
|
export { resistanceScale, resolveResistance } from "./combat/resistance.js";
|
|
25
26
|
export { createResourcePool, type ResourcePool } from "./combat/resourcePool.js";
|
|
26
27
|
export { allocateRewards, filterOutcomeFor, resolveClaim, type AllocationOutcome, type AllocationRequest, type ClaimOutcome, type ClaimablePool, type RewardClaimSpec, type RewardGrant, type RewardPolicyKind, type RewardRecipient, type RewardResult, type RewardShare, } from "./combat/rewardAllocation.js";
|
|
27
|
-
export { DEFAULT_EYE_HEIGHT, convergeShot, eyeHeightFromColliders, resolveShot, type ShotOriginPolicy, } from "./combat/shotOrigin.js";
|
|
28
|
+
export { DEFAULT_EYE_HEIGHT, convergeShot, eyeHeightFromColliders, resolveShot, type ResolvedShot, type ShotOriginPolicy, } from "./combat/shotOrigin.js";
|
|
28
29
|
export { resolveStatusApplication, type StatusApplicationOutcome, type StatusApplicationSpec, type StatusImmunity, type StatusInstance, type StatusStackPolicy, } from "./combat/statusApplication.js";
|
|
30
|
+
export { statusEffectRemainingFraction, toStatusEffectView, toStatusEffectViews, type StatusEffectView, type StatusEffectViewOptions, } from "./combat/statusEffectView.js";
|
|
29
31
|
export { type TelegraphConfig, type TelegraphShape } from "./combat/telegraph.js";
|
|
30
32
|
export { createThresholdTracker, type CrossingDirection, type CrossingPolicy, type CrossingTrigger, type ThresholdCrossing, type ThresholdCrossingConfig, type ThresholdMark, type ThresholdTracker, type ThresholdTrackerSnapshot, } from "./combat/thresholdCrossings.js";
|
|
31
33
|
export { DEFAULT_VFX_PRESET, resolveVfxPreset, vfxPresets, type VfxPreset, type VfxPresetName, } from "./combat/vfxPresets.js";
|
package/dist/combat.js
CHANGED
|
@@ -3,6 +3,7 @@ export {} from "./combat/animationState.js";
|
|
|
3
3
|
export { attackMeta, counters, isBlockable, isDodgeable, isParryable } from "./combat/attackTags.js";
|
|
4
4
|
export { createBuildupMeter, createStaggerMeter } from "./combat/breakMeters.js";
|
|
5
5
|
export { createCastRunner } from "./combat/castRunner.js";
|
|
6
|
+
export { createComboMeter, } from "./combat/comboMeter.js";
|
|
6
7
|
export { createComboPoints } from "./combat/comboPoints.js";
|
|
7
8
|
export { advanceCombo, createComboRunner } from "./combat/comboString.js";
|
|
8
9
|
export { MAX_INTERCEPT_STEPS, createAntiOneShotPolicy, createDamageClamp, createDamagePipeline, createImmunityWindow, resolveDamage, } from "./combat/damageInterceptors.js";
|
|
@@ -26,6 +27,7 @@ export { createResourcePool } from "./combat/resourcePool.js";
|
|
|
26
27
|
export { allocateRewards, filterOutcomeFor, resolveClaim, } from "./combat/rewardAllocation.js";
|
|
27
28
|
export { DEFAULT_EYE_HEIGHT, convergeShot, eyeHeightFromColliders, resolveShot, } from "./combat/shotOrigin.js";
|
|
28
29
|
export { resolveStatusApplication, } from "./combat/statusApplication.js";
|
|
30
|
+
export { statusEffectRemainingFraction, toStatusEffectView, toStatusEffectViews, } from "./combat/statusEffectView.js";
|
|
29
31
|
export {} from "./combat/telegraph.js";
|
|
30
32
|
export { createThresholdTracker, } from "./combat/thresholdCrossings.js";
|
|
31
33
|
export { DEFAULT_VFX_PRESET, resolveVfxPreset, vfxPresets, } from "./combat/vfxPresets.js";
|
|
@@ -7,9 +7,10 @@ export type FallbackSeam = "ground" | "entity" | "object" | "scatter";
|
|
|
7
7
|
/**
|
|
8
8
|
* Why a seam fell back: `omittedMapping` (no mapping supplied — often intended),
|
|
9
9
|
* `unpulledPack` (a mapping was supplied but its asset pack is not pulled/indexed),
|
|
10
|
-
* `noScene` (nothing authored — e.g. no environment component)
|
|
10
|
+
* `noScene` (nothing authored — e.g. no environment component),
|
|
11
|
+
* `renderError` (authored content was found but threw while rendering — always a defect).
|
|
11
12
|
*/
|
|
12
|
-
export type FallbackCause = "omittedMapping" | "unpulledPack" | "noScene";
|
|
13
|
+
export type FallbackCause = "omittedMapping" | "unpulledPack" | "noScene" | "renderError";
|
|
13
14
|
/** A per-seam, per-cause count table. Serializable, allocation-stable (keys never change). */
|
|
14
15
|
export type FallbackSeamCounts = Record<FallbackSeam, Record<FallbackCause, number>>;
|
|
15
16
|
/** Non-zero-only view of {@link FallbackSeamCounts} returned by {@link fallbackSeamsSnapshot}. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { devtools } from "./devtools.js";
|
|
2
2
|
const SEAMS = ["ground", "entity", "object", "scatter"];
|
|
3
|
-
const CAUSES = ["omittedMapping", "unpulledPack", "noScene"];
|
|
3
|
+
const CAUSES = ["omittedMapping", "unpulledPack", "noScene", "renderError"];
|
|
4
4
|
function freshCounts() {
|
|
5
5
|
const out = {};
|
|
6
6
|
for (const seam of SEAMS) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One texture (or other GLB sub-resource) that failed to load, plus how many times
|
|
3
|
+
* a load for that URL errored. Serializable so it flows through the `debug_snapshot`
|
|
4
|
+
* RPC exactly like {@link import("./fallbackSeams.js").FallbackSeamsReport}.
|
|
5
|
+
*/
|
|
6
|
+
export interface TextureLoadError {
|
|
7
|
+
/** The URL the loader tried and failed to fetch (e.g. a `THREE.GLTFLoader` texture image). */
|
|
8
|
+
url: string;
|
|
9
|
+
/** How many load attempts for this URL have errored since the probe was armed. */
|
|
10
|
+
count: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Arm or disarm the texture-load-error diagnostic. Off by default; the shell arms it only when devtools
|
|
14
|
+
* is enabled (dev builds), so production is a pure no-op. Toggling clears the tally so a fresh observation
|
|
15
|
+
* starts empty and the opt-out path reports nothing.
|
|
16
|
+
* @capability render-fallback-diagnostics Enable/disable the dev-only texture-load-error probe.
|
|
17
|
+
*/
|
|
18
|
+
export declare function armTextureErrors(on: boolean): void;
|
|
19
|
+
/**
|
|
20
|
+
* Clear the texture-error tally without changing the armed state. Used to bracket a fresh observation
|
|
21
|
+
* (e.g. between scenes or in tests). No-op when disarmed.
|
|
22
|
+
* @capability render-fallback-diagnostics Reset the texture-load-error tally.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resetTextureErrors(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Record that a sub-resource load ERRORED for `url` (called from the shared GLB loading manager's
|
|
27
|
+
* `onError`, which fires for GLTF textures/images whose fetch 404s or otherwise fails — the failure the
|
|
28
|
+
* whole-model fallback probe never sees because the model itself still resolves). A pure, allocation-free
|
|
29
|
+
* no-op when disarmed. On the armed path it also emits ONE deduped devtools warn line per newly-seen
|
|
30
|
+
* failure set so a texture-404'd scene is loud, not silent.
|
|
31
|
+
* @capability render-fallback-diagnostics Report a texture/sub-resource load failure.
|
|
32
|
+
*/
|
|
33
|
+
export declare function reportTextureLoadError(url: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Snapshot the current texture-load-error tally as a compact, serializable list (empty when nothing
|
|
36
|
+
* failed). Flows into the devtools `textureErrors` probe and the `debug_snapshot` RPC so `jgengine-verify`
|
|
37
|
+
* can treat a non-empty set exactly like a model fallback: content that resolved but is visibly broken.
|
|
38
|
+
* @capability render-fallback-diagnostics Read per-URL texture/sub-resource load-failure counts.
|
|
39
|
+
*/
|
|
40
|
+
export declare function textureErrorsSnapshot(): TextureLoadError[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { devtools } from "./devtools.js";
|
|
2
|
+
// Module-private state. `enabled` gates every mutation so production (devtools off) is a pure no-op;
|
|
3
|
+
// `counts` is the authoritative per-URL error tally. A Map keeps report O(1) and allocation-free on
|
|
4
|
+
// repeat errors (only a first-seen URL allocates a key), matching the allocation-aware fallback probe.
|
|
5
|
+
let enabled = false;
|
|
6
|
+
const counts = new Map();
|
|
7
|
+
let lastSignature = "";
|
|
8
|
+
/**
|
|
9
|
+
* Arm or disarm the texture-load-error diagnostic. Off by default; the shell arms it only when devtools
|
|
10
|
+
* is enabled (dev builds), so production is a pure no-op. Toggling clears the tally so a fresh observation
|
|
11
|
+
* starts empty and the opt-out path reports nothing.
|
|
12
|
+
* @capability render-fallback-diagnostics Enable/disable the dev-only texture-load-error probe.
|
|
13
|
+
*/
|
|
14
|
+
export function armTextureErrors(on) {
|
|
15
|
+
enabled = on;
|
|
16
|
+
counts.clear();
|
|
17
|
+
lastSignature = "";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Clear the texture-error tally without changing the armed state. Used to bracket a fresh observation
|
|
21
|
+
* (e.g. between scenes or in tests). No-op when disarmed.
|
|
22
|
+
* @capability render-fallback-diagnostics Reset the texture-load-error tally.
|
|
23
|
+
*/
|
|
24
|
+
export function resetTextureErrors() {
|
|
25
|
+
if (!enabled)
|
|
26
|
+
return;
|
|
27
|
+
counts.clear();
|
|
28
|
+
lastSignature = "";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Record that a sub-resource load ERRORED for `url` (called from the shared GLB loading manager's
|
|
32
|
+
* `onError`, which fires for GLTF textures/images whose fetch 404s or otherwise fails — the failure the
|
|
33
|
+
* whole-model fallback probe never sees because the model itself still resolves). A pure, allocation-free
|
|
34
|
+
* no-op when disarmed. On the armed path it also emits ONE deduped devtools warn line per newly-seen
|
|
35
|
+
* failure set so a texture-404'd scene is loud, not silent.
|
|
36
|
+
* @capability render-fallback-diagnostics Report a texture/sub-resource load failure.
|
|
37
|
+
*/
|
|
38
|
+
export function reportTextureLoadError(url) {
|
|
39
|
+
if (!enabled)
|
|
40
|
+
return;
|
|
41
|
+
counts.set(url, (counts.get(url) ?? 0) + 1);
|
|
42
|
+
const sig = signature();
|
|
43
|
+
if (sig === lastSignature)
|
|
44
|
+
return;
|
|
45
|
+
lastSignature = sig;
|
|
46
|
+
devtools.logs.push("warn", `[jgengine] texture/sub-resource loads failed — ${sig.replaceAll("|", ", ")}. ` +
|
|
47
|
+
`The model resolved but its textures 404'd; fix the asset path/pack or the scene renders untextured.`);
|
|
48
|
+
}
|
|
49
|
+
// Keys on the SET of failed URLs, not their counts, so re-erroring the same texture (counts keep
|
|
50
|
+
// growing as the loader retries) does not re-warn — only a newly-seen failing URL widens the line.
|
|
51
|
+
function signature() {
|
|
52
|
+
let sig = "";
|
|
53
|
+
for (const url of counts.keys())
|
|
54
|
+
sig += `${url}|`;
|
|
55
|
+
return sig.replace(/\|$/, "");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Snapshot the current texture-load-error tally as a compact, serializable list (empty when nothing
|
|
59
|
+
* failed). Flows into the devtools `textureErrors` probe and the `debug_snapshot` RPC so `jgengine-verify`
|
|
60
|
+
* can treat a non-empty set exactly like a model fallback: content that resolved but is visibly broken.
|
|
61
|
+
* @capability render-fallback-diagnostics Read per-URL texture/sub-resource load-failure counts.
|
|
62
|
+
*/
|
|
63
|
+
export function textureErrorsSnapshot() {
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const [url, count] of counts)
|
|
66
|
+
out.push({ url, count });
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type WalletState } from "./wallet.js";
|
|
2
|
+
/**
|
|
3
|
+
* A price tag: how much of a free-string `currency` an entry costs (buy) or returns (sell).
|
|
4
|
+
* `amount` is a non-negative finite number; `0` means free (no wallet charge/grant happens).
|
|
5
|
+
*/
|
|
6
|
+
export interface ShopPrice {
|
|
7
|
+
/** Free-string currency key the game owns — matches a {@link WalletState} balance key. Never interpreted. */
|
|
8
|
+
currency: string;
|
|
9
|
+
/** Non-negative finite quantity of `currency`. */
|
|
10
|
+
amount: number;
|
|
11
|
+
}
|
|
12
|
+
/** One item a vendor stocks. Fully serializable; `kind` and `currency` are free strings the model never branches on. */
|
|
13
|
+
export interface ShopStockEntry {
|
|
14
|
+
/** Stable unique id within the shop. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Free-string label the game owns and styles ("potion", "blade", "charm", …). The model never interprets it. */
|
|
17
|
+
kind: string;
|
|
18
|
+
/** What it costs to buy one. */
|
|
19
|
+
price: ShopPrice;
|
|
20
|
+
/** Units on hand: a non-negative integer, or `null` for unlimited (never decrements, never sells out). */
|
|
21
|
+
qty: number | null;
|
|
22
|
+
/** Optional buy-back price paid to the wallet on {@link ShopStock.sell}. Omitted means the item is not sellable. */
|
|
23
|
+
sellPrice?: ShopPrice;
|
|
24
|
+
}
|
|
25
|
+
/** Config for {@link createShopStock}: the initial set of entries. */
|
|
26
|
+
export interface ShopStockConfig {
|
|
27
|
+
/** Initial stock. Ids must be unique; a duplicate id throws. */
|
|
28
|
+
entries?: readonly ShopStockEntry[];
|
|
29
|
+
}
|
|
30
|
+
/** Why a {@link ShopStock.buy} was refused. */
|
|
31
|
+
export type ShopBuyRejection = "unknown-item" | "out-of-stock" | "insufficient-funds";
|
|
32
|
+
/** Why a {@link ShopStock.sell} was refused. */
|
|
33
|
+
export type ShopSellRejection = "unknown-item" | "not-sellable";
|
|
34
|
+
/**
|
|
35
|
+
* Result of {@link ShopStock.buy}: on success the caller receives the debited `wallet` to adopt and a
|
|
36
|
+
* snapshot of the purchased `entry` (post-decrement); on failure a `reason` and the wallet is untouched.
|
|
37
|
+
*/
|
|
38
|
+
export type ShopBuyResult = {
|
|
39
|
+
ok: true;
|
|
40
|
+
wallet: WalletState;
|
|
41
|
+
entry: ShopStockEntry;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
reason: ShopBuyRejection;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Result of {@link ShopStock.sell}: on success the caller receives the credited `wallet` and a snapshot
|
|
48
|
+
* of the sold `entry` (post-restock); on failure a `reason` and the wallet is untouched.
|
|
49
|
+
*/
|
|
50
|
+
export type ShopSellResult = {
|
|
51
|
+
ok: true;
|
|
52
|
+
wallet: WalletState;
|
|
53
|
+
entry: ShopStockEntry;
|
|
54
|
+
} | {
|
|
55
|
+
ok: false;
|
|
56
|
+
reason: ShopSellRejection;
|
|
57
|
+
};
|
|
58
|
+
/** Serializable state of a shop's stock, for save/restore. */
|
|
59
|
+
export interface ShopStockSnapshot {
|
|
60
|
+
entries: readonly ShopStockEntry[];
|
|
61
|
+
}
|
|
62
|
+
/** A live, observable vendor/shop stock operating over a caller-owned wallet. */
|
|
63
|
+
export interface ShopStock {
|
|
64
|
+
/**
|
|
65
|
+
* Buy one unit of `id` against `wallet` (the shop never owns the wallet). Debits the entry's `price`
|
|
66
|
+
* via {@link charge}, decrements a finite `qty`, and returns the new wallet to adopt plus the entry
|
|
67
|
+
* snapshot. Rejects `"unknown-item"`, `"out-of-stock"` (finite qty at 0), or `"insufficient-funds"`
|
|
68
|
+
* — the passed wallet is never mutated.
|
|
69
|
+
*/
|
|
70
|
+
buy(id: string, wallet: WalletState): ShopBuyResult;
|
|
71
|
+
/**
|
|
72
|
+
* Sell one unit of `id` back to the vendor, crediting the entry's `sellPrice` to `wallet` via
|
|
73
|
+
* {@link grant} and incrementing a finite `qty`. Rejects `"unknown-item"` or `"not-sellable"`
|
|
74
|
+
* (no `sellPrice`). The passed wallet is never mutated.
|
|
75
|
+
*/
|
|
76
|
+
sell(id: string, wallet: WalletState): ShopSellResult;
|
|
77
|
+
/** Whether `wallet` can currently afford one unit of `id` (false for unknown or sold-out items). */
|
|
78
|
+
canAfford(id: string, wallet: WalletState): boolean;
|
|
79
|
+
/** Add `n` units to a finite-`qty` entry (no-op for unlimited stock). Notifies subscribers. */
|
|
80
|
+
restock(id: string, n: number): void;
|
|
81
|
+
/** Replace an entry's buy `price`. Notifies subscribers. */
|
|
82
|
+
setPrice(id: string, price: ShopPrice): void;
|
|
83
|
+
/** Insert a new entry. Throws if its id already exists. Notifies subscribers. */
|
|
84
|
+
add(entry: ShopStockEntry): void;
|
|
85
|
+
/** Remove an entry by id (no-op if absent). Notifies subscribers. */
|
|
86
|
+
remove(id: string): void;
|
|
87
|
+
/** A snapshot array of every entry, in insertion order (safe to retain — copies, not live records). */
|
|
88
|
+
list(): ShopStockEntry[];
|
|
89
|
+
/** A snapshot of one entry, or `null` if unknown (safe to retain). */
|
|
90
|
+
get(id: string): ShopStockEntry | null;
|
|
91
|
+
/** Observe changes (buy, sell, restock, setPrice, add, remove, restore). Returns an unsubscribe fn. */
|
|
92
|
+
subscribe(listener: () => void): () => void;
|
|
93
|
+
/** Serializable state for a save. */
|
|
94
|
+
snapshot(): ShopStockSnapshot;
|
|
95
|
+
/** Restore from a {@link ShopStockSnapshot}, replacing all entries. Notifies subscribers. */
|
|
96
|
+
restore(snapshot: ShopStockSnapshot): void;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A serializable, observable vendor/shop stock. Each entry carries a game-owned free-string `kind`, a
|
|
100
|
+
* `price` in a free-string `currency`, a finite or unlimited (`qty: null`) count, and an optional
|
|
101
|
+
* `sellPrice` buy-back. All currency math is delegated to the existing pure `wallet` model — `buy`
|
|
102
|
+
* charges and `sell` grants a **caller-owned** {@link WalletState} (the shop never holds the wallet, it
|
|
103
|
+
* returns the new wallet for the caller to adopt), so the same stock composes with any wallet the game
|
|
104
|
+
* owns. Nothing here is genre-specific: `kind` and `currency` are free labels the game styles and the
|
|
105
|
+
* model never branches on. `restock`/`setPrice`/`add`/`remove` mutate the catalog, `subscribe` observes
|
|
106
|
+
* every change, and `snapshot`/`restore` round-trips the whole stock through a save.
|
|
107
|
+
*
|
|
108
|
+
* @capability shop-grid serializable vendor/shop stock with buy/sell over a caller-owned wallet — finite/unlimited qty, sell-back, restock/setPrice, snapshot/restore
|
|
109
|
+
*/
|
|
110
|
+
export declare function createShopStock(config?: ShopStockConfig): ShopStock;
|