@jgengine/core 0.5.0 → 0.7.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 +191 -2
- package/dist/ai/crowd.d.ts +36 -0
- package/dist/ai/crowd.js +213 -0
- package/dist/ai/jobBoard.d.ts +49 -0
- package/dist/ai/jobBoard.js +155 -0
- package/dist/ai/spawnDirector.d.ts +56 -0
- package/dist/ai/spawnDirector.js +150 -0
- package/dist/ai/threat.d.ts +25 -0
- package/dist/ai/threat.js +67 -0
- package/dist/audio/audioFalloff.d.ts +38 -0
- package/dist/audio/audioFalloff.js +32 -0
- package/dist/board/laneBoard.d.ts +63 -0
- package/dist/board/laneBoard.js +114 -0
- package/dist/board/timelineBoard.d.ts +36 -0
- package/dist/board/timelineBoard.js +70 -0
- package/dist/cards/cardPile.d.ts +63 -0
- package/dist/cards/cardPile.js +166 -0
- package/dist/cards/modifierPipeline.d.ts +33 -0
- package/dist/cards/modifierPipeline.js +48 -0
- package/dist/combat/abilityKit.d.ts +42 -0
- package/dist/combat/abilityKit.js +129 -0
- package/dist/combat/animationState.d.ts +47 -0
- package/dist/combat/animationState.js +101 -0
- package/dist/combat/attackTags.d.ts +14 -0
- package/dist/combat/attackTags.js +26 -0
- package/dist/combat/breakMeters.d.ts +32 -0
- package/dist/combat/breakMeters.js +51 -0
- package/dist/combat/comboString.d.ts +35 -0
- package/dist/combat/comboString.js +59 -0
- package/dist/combat/death.d.ts +8 -0
- package/dist/combat/death.js +5 -2
- package/dist/combat/defensiveWindow.d.ts +34 -0
- package/dist/combat/defensiveWindow.js +66 -0
- package/dist/combat/downed.d.ts +35 -0
- package/dist/combat/downed.js +104 -0
- package/dist/combat/effects.d.ts +11 -0
- package/dist/combat/effects.js +22 -13
- package/dist/combat/hitReaction.d.ts +23 -0
- package/dist/combat/hitReaction.js +20 -0
- package/dist/combat/resistance.d.ts +13 -0
- package/dist/combat/resistance.js +46 -0
- package/dist/combat/telegraph.d.ts +27 -0
- package/dist/combat/telegraph.js +37 -0
- package/dist/crafting/crop.d.ts +59 -0
- package/dist/crafting/crop.js +181 -0
- package/dist/crafting/production.d.ts +64 -0
- package/dist/crafting/production.js +130 -0
- package/dist/crafting/recipe.d.ts +62 -0
- package/dist/crafting/recipe.js +101 -0
- package/dist/economy/sharedWallet.d.ts +28 -0
- package/dist/economy/sharedWallet.js +62 -0
- package/dist/economy/techTree.d.ts +40 -0
- package/dist/economy/techTree.js +66 -0
- package/dist/game/cosmetics.d.ts +25 -0
- package/dist/game/cosmetics.js +51 -0
- package/dist/game/defineGame.d.ts +5 -0
- package/dist/game/events.d.ts +62 -0
- package/dist/game/lootFilter.d.ts +33 -0
- package/dist/game/lootFilter.js +48 -0
- package/dist/game/ping.d.ts +67 -0
- package/dist/game/ping.js +86 -0
- package/dist/game/playableGame.d.ts +291 -5
- package/dist/game/race.d.ts +95 -0
- package/dist/game/race.js +216 -0
- package/dist/game/runDraft.d.ts +34 -0
- package/dist/game/runDraft.js +97 -0
- package/dist/game/social.d.ts +27 -0
- package/dist/game/social.js +16 -0
- package/dist/game/worldItem.d.ts +92 -0
- package/dist/game/worldItem.js +132 -0
- package/dist/input/axisInput.d.ts +43 -0
- package/dist/input/axisInput.js +70 -0
- package/dist/input/pointer.d.ts +24 -0
- package/dist/input/pointer.js +17 -0
- package/dist/interaction/contextMenu.d.ts +30 -0
- package/dist/interaction/contextMenu.js +22 -0
- package/dist/interaction/qte.d.ts +20 -0
- package/dist/interaction/qte.js +24 -0
- package/dist/interaction/skillCheck.d.ts +20 -0
- package/dist/interaction/skillCheck.js +26 -0
- package/dist/inventory/shapedGrid.d.ts +56 -0
- package/dist/inventory/shapedGrid.js +176 -0
- package/dist/inventory/storageTier.d.ts +52 -0
- package/dist/inventory/storageTier.js +77 -0
- package/dist/item/affix.d.ts +54 -0
- package/dist/item/affix.js +133 -0
- package/dist/item/durability.d.ts +51 -0
- package/dist/item/durability.js +78 -0
- package/dist/item/modularItem.d.ts +46 -0
- package/dist/item/modularItem.js +75 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +53 -1
- package/dist/movement/dash.d.ts +37 -0
- package/dist/movement/dash.js +65 -0
- package/dist/multiplayer/combatSnapshot.d.ts +45 -0
- package/dist/multiplayer/combatSnapshot.js +89 -0
- package/dist/multiplayer/lagCompensation.d.ts +43 -0
- package/dist/multiplayer/lagCompensation.js +154 -0
- package/dist/multiplayer/matchmaking.d.ts +33 -0
- package/dist/multiplayer/matchmaking.js +60 -0
- package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
- package/dist/multiplayer/simultaneousCommit.js +53 -0
- package/dist/nav/navGrid.d.ts +46 -0
- package/dist/nav/navGrid.js +248 -0
- package/dist/nav/pathFollow.d.ts +29 -0
- package/dist/nav/pathFollow.js +67 -0
- package/dist/physics/buoyancy.d.ts +49 -0
- package/dist/physics/buoyancy.js +112 -0
- package/dist/physics/carryable.d.ts +46 -0
- package/dist/physics/carryable.js +110 -0
- package/dist/physics/damageZones.d.ts +56 -0
- package/dist/physics/damageZones.js +107 -0
- package/dist/physics/forceVolume.d.ts +48 -0
- package/dist/physics/forceVolume.js +119 -0
- package/dist/physics/physicsWorld.d.ts +219 -0
- package/dist/physics/physicsWorld.js +749 -0
- package/dist/physics/ragdoll.d.ts +49 -0
- package/dist/physics/ragdoll.js +103 -0
- package/dist/physics/spatialGrid.d.ts +41 -0
- package/dist/physics/spatialGrid.js +138 -0
- package/dist/physics/structure.d.ts +83 -0
- package/dist/physics/structure.js +0 -0
- package/dist/physics/traversal.d.ts +75 -0
- package/dist/physics/traversal.js +138 -0
- package/dist/physics/vehicleBody.d.ts +93 -0
- package/dist/physics/vehicleBody.js +234 -0
- package/dist/runtime/gameContext.d.ts +66 -2
- package/dist/runtime/gameContext.js +157 -4
- package/dist/runtime/hostPersistence.d.ts +18 -0
- package/dist/runtime/hostPersistence.js +4 -0
- package/dist/runtime/persistenceScope.d.ts +32 -0
- package/dist/runtime/persistenceScope.js +64 -0
- package/dist/scene/assetCatalog.d.ts +13 -0
- package/dist/scene/autoTarget.d.ts +15 -0
- package/dist/scene/autoTarget.js +54 -0
- package/dist/scene/behaviors.d.ts +13 -1
- package/dist/scene/behaviors.js +3 -0
- package/dist/scene/captureCheck.d.ts +7 -0
- package/dist/scene/captureCheck.js +11 -0
- package/dist/scene/entityStore.d.ts +7 -1
- package/dist/scene/entityStore.js +15 -1
- package/dist/scene/form.d.ts +40 -0
- package/dist/scene/form.js +56 -0
- package/dist/scene/mount.d.ts +62 -0
- package/dist/scene/mount.js +107 -0
- package/dist/scene/possession.d.ts +30 -0
- package/dist/scene/possession.js +51 -0
- package/dist/scene/roster.d.ts +25 -0
- package/dist/scene/roster.js +57 -0
- package/dist/scene/selection.d.ts +30 -0
- package/dist/scene/selection.js +38 -0
- package/dist/scene/stationClaim.d.ts +43 -0
- package/dist/scene/stationClaim.js +109 -0
- package/dist/sensor/frustumSensor.d.ts +60 -0
- package/dist/sensor/frustumSensor.js +104 -0
- package/dist/sensor/hiddenStateProbe.d.ts +31 -0
- package/dist/sensor/hiddenStateProbe.js +40 -0
- package/dist/sensor/recordingBuffer.d.ts +29 -0
- package/dist/sensor/recordingBuffer.js +61 -0
- package/dist/sensor/revealQuery.d.ts +22 -0
- package/dist/sensor/revealQuery.js +36 -0
- package/dist/session/contestedChannel.d.ts +36 -0
- package/dist/session/contestedChannel.js +100 -0
- package/dist/session/extraction.d.ts +54 -0
- package/dist/session/extraction.js +108 -0
- package/dist/session/ring.d.ts +36 -0
- package/dist/session/ring.js +88 -0
- package/dist/session/roundState.d.ts +55 -0
- package/dist/session/roundState.js +129 -0
- package/dist/stats/accumulatorMeter.d.ts +31 -0
- package/dist/stats/accumulatorMeter.js +81 -0
- package/dist/stats/eventMeter.d.ts +34 -0
- package/dist/stats/eventMeter.js +76 -0
- package/dist/stats/rollCheck.d.ts +15 -0
- package/dist/stats/rollCheck.js +15 -0
- package/dist/survival/decayMeter.d.ts +59 -0
- package/dist/survival/decayMeter.js +95 -0
- package/dist/survival/moodle.d.ts +48 -0
- package/dist/survival/moodle.js +91 -0
- package/dist/survival/regionHealth.d.ts +72 -0
- package/dist/survival/regionHealth.js +129 -0
- package/dist/tactics/predictiveQuery.d.ts +34 -0
- package/dist/tactics/predictiveQuery.js +44 -0
- package/dist/tactics/snapshot.d.ts +18 -0
- package/dist/tactics/snapshot.js +65 -0
- package/dist/tactics/surface.d.ts +52 -0
- package/dist/tactics/surface.js +135 -0
- package/dist/tactics/tacticalGrid.d.ts +57 -0
- package/dist/tactics/tacticalGrid.js +244 -0
- package/dist/time/beatClock.d.ts +40 -0
- package/dist/time/beatClock.js +73 -0
- package/dist/time/simClock.d.ts +62 -0
- package/dist/time/simClock.js +132 -0
- package/dist/turn/commit.d.ts +27 -0
- package/dist/turn/commit.js +72 -0
- package/dist/turn/turnLoop.d.ts +58 -0
- package/dist/turn/turnLoop.js +216 -0
- package/dist/world/buildPermissions.d.ts +47 -0
- package/dist/world/buildPermissions.js +134 -0
- package/dist/world/buildingIndex.d.ts +16 -0
- package/dist/world/buildingIndex.js +63 -0
- package/dist/world/buildings.d.ts +98 -0
- package/dist/world/buildings.js +320 -0
- package/dist/world/carve.d.ts +105 -0
- package/dist/world/carve.js +175 -0
- package/dist/world/connectors.d.ts +40 -0
- package/dist/world/connectors.js +67 -0
- package/dist/world/envField.d.ts +71 -0
- package/dist/world/envField.js +96 -0
- package/dist/world/features.d.ts +105 -1
- package/dist/world/features.js +95 -0
- package/dist/world/fog.d.ts +40 -0
- package/dist/world/fog.js +137 -0
- package/dist/world/markers.d.ts +52 -0
- package/dist/world/markers.js +96 -0
- package/dist/world/minimap.d.ts +42 -0
- package/dist/world/minimap.js +72 -0
- package/dist/world/placedStructureStore.d.ts +39 -0
- package/dist/world/placedStructureStore.js +95 -0
- package/dist/world/placementController.d.ts +48 -0
- package/dist/world/placementController.js +97 -0
- package/dist/world/realm.d.ts +61 -0
- package/dist/world/realm.js +66 -0
- package/dist/world/regions.d.ts +66 -0
- package/dist/world/regions.js +158 -0
- package/dist/world/scatter.d.ts +24 -0
- package/dist/world/scatter.js +126 -0
- package/dist/world/scatterItems.d.ts +38 -0
- package/dist/world/scatterItems.js +85 -0
- package/dist/world/support.d.ts +27 -0
- package/dist/world/support.js +75 -0
- package/dist/world/terraform.d.ts +54 -0
- package/dist/world/terraform.js +186 -0
- package/dist/world/terrain.d.ts +54 -0
- package/dist/world/terrain.js +181 -0
- package/dist/world/walls.d.ts +61 -0
- package/dist/world/walls.js +221 -0
- package/dist/world/water.d.ts +27 -0
- package/dist/world/water.js +125 -0
- package/dist/world/weather.d.ts +92 -0
- package/dist/world/weather.js +169 -0
- package/dist/world/wind.d.ts +17 -0
- package/dist/world/wind.js +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Moodle, MoodleSeverity } from "./moodle.js";
|
|
2
|
+
export interface MeterThreshold {
|
|
3
|
+
/** Moodle id raised while the meter is on the wrong side of `at`. */
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** Boundary value. */
|
|
7
|
+
at: number;
|
|
8
|
+
/** Raise the moodle when the meter is `below` (hunger/thirst) or `above` (temperature, radiation) this value. */
|
|
9
|
+
when: "below" | "above";
|
|
10
|
+
severity?: MoodleSeverity;
|
|
11
|
+
icon?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DecayMeterConfig {
|
|
14
|
+
id: string;
|
|
15
|
+
max: number;
|
|
16
|
+
min?: number;
|
|
17
|
+
/** Starting value; defaults to `max` (a full stomach). */
|
|
18
|
+
start?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Units changed per game-second at rate-modifier 1. Positive drains toward `min`
|
|
21
|
+
* (hunger, oxygen), negative fills toward `max` (a warmth meter recovering by a fire).
|
|
22
|
+
*/
|
|
23
|
+
rate: number;
|
|
24
|
+
/** Status thresholds → moodles, distinct from the raw numeric bar. */
|
|
25
|
+
thresholds?: readonly MeterThreshold[];
|
|
26
|
+
}
|
|
27
|
+
export interface DecayMeterState {
|
|
28
|
+
id: string;
|
|
29
|
+
value: number;
|
|
30
|
+
max: number;
|
|
31
|
+
min: number;
|
|
32
|
+
/** 0..1 fill. */
|
|
33
|
+
fraction: number;
|
|
34
|
+
}
|
|
35
|
+
export interface DecayMeterSet {
|
|
36
|
+
/** Drain/fill every meter by `rate * rateModifier * dt`. Call once per game tick. */
|
|
37
|
+
tick(dt: number): void;
|
|
38
|
+
value(id: string): number;
|
|
39
|
+
state(id: string): DecayMeterState;
|
|
40
|
+
/** Consumable/action refill: `refill("hunger", 40)` after eating. Negative drains. */
|
|
41
|
+
refill(id: string, amount: number): void;
|
|
42
|
+
/** Overwrite the base drain rate (e.g. a game mode with harsher hunger). */
|
|
43
|
+
setRate(id: string, rate: number): void;
|
|
44
|
+
/** Multiply the drain rate this tick onward — cold biome speeds warmth loss, sprint burns stamina. */
|
|
45
|
+
setRateModifier(id: string, multiplier: number): void;
|
|
46
|
+
/** Moodles for every crossed threshold, worst-first per meter. */
|
|
47
|
+
moodles(): Moodle[];
|
|
48
|
+
/** True while any threshold with the given severity (or worse) is active — cheap game gate. */
|
|
49
|
+
snapshot(): Record<string, DecayMeterState>;
|
|
50
|
+
ids(): readonly string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Named decay meters — hunger, thirst, oxygen, sanity, warmth, stamina. Each drains
|
|
54
|
+
* (or recovers) on game-time `dt` at a configurable rate, refills from consumables or
|
|
55
|
+
* actions, and raises moodle statuses at thresholds. Rate modifiers let the environment
|
|
56
|
+
* drive them (colder → faster warmth loss; toxic biome → oxygen drops), so a game reads
|
|
57
|
+
* an environment field then calls `setRateModifier`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createDecayMeterSet(configs: readonly DecayMeterConfig[]): DecayMeterSet;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
function clamp(value, min, max) {
|
|
2
|
+
return value < min ? min : value > max ? max : value;
|
|
3
|
+
}
|
|
4
|
+
function toState(runtime) {
|
|
5
|
+
const span = runtime.max - runtime.min;
|
|
6
|
+
return {
|
|
7
|
+
id: runtime.config.id,
|
|
8
|
+
value: runtime.value,
|
|
9
|
+
max: runtime.max,
|
|
10
|
+
min: runtime.min,
|
|
11
|
+
fraction: span > 0 ? (runtime.value - runtime.min) / span : 0,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function crossed(threshold, value) {
|
|
15
|
+
return threshold.when === "below" ? value < threshold.at : value > threshold.at;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Named decay meters — hunger, thirst, oxygen, sanity, warmth, stamina. Each drains
|
|
19
|
+
* (or recovers) on game-time `dt` at a configurable rate, refills from consumables or
|
|
20
|
+
* actions, and raises moodle statuses at thresholds. Rate modifiers let the environment
|
|
21
|
+
* drive them (colder → faster warmth loss; toxic biome → oxygen drops), so a game reads
|
|
22
|
+
* an environment field then calls `setRateModifier`.
|
|
23
|
+
*/
|
|
24
|
+
export function createDecayMeterSet(configs) {
|
|
25
|
+
const meters = new Map();
|
|
26
|
+
const order = [];
|
|
27
|
+
for (const config of configs) {
|
|
28
|
+
const min = config.min ?? 0;
|
|
29
|
+
const max = config.max;
|
|
30
|
+
const value = clamp(config.start ?? max, min, max);
|
|
31
|
+
meters.set(config.id, { config, min, max, value, rate: config.rate, modifier: 1 });
|
|
32
|
+
order.push(config.id);
|
|
33
|
+
}
|
|
34
|
+
const require = (id) => {
|
|
35
|
+
const runtime = meters.get(id);
|
|
36
|
+
if (runtime === undefined)
|
|
37
|
+
throw new Error(`unknown decay meter "${id}"`);
|
|
38
|
+
return runtime;
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
tick(dt) {
|
|
42
|
+
if (dt <= 0)
|
|
43
|
+
return;
|
|
44
|
+
for (const runtime of meters.values()) {
|
|
45
|
+
runtime.value = clamp(runtime.value - runtime.rate * runtime.modifier * dt, runtime.min, runtime.max);
|
|
46
|
+
runtime.modifier = 1;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
value(id) {
|
|
50
|
+
return require(id).value;
|
|
51
|
+
},
|
|
52
|
+
state(id) {
|
|
53
|
+
return toState(require(id));
|
|
54
|
+
},
|
|
55
|
+
refill(id, amount) {
|
|
56
|
+
const runtime = require(id);
|
|
57
|
+
runtime.value = clamp(runtime.value + amount, runtime.min, runtime.max);
|
|
58
|
+
},
|
|
59
|
+
setRate(id, rate) {
|
|
60
|
+
require(id).rate = rate;
|
|
61
|
+
},
|
|
62
|
+
setRateModifier(id, multiplier) {
|
|
63
|
+
require(id).modifier = multiplier;
|
|
64
|
+
},
|
|
65
|
+
moodles() {
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const id of order) {
|
|
68
|
+
const runtime = meters.get(id);
|
|
69
|
+
for (const threshold of runtime.config.thresholds ?? []) {
|
|
70
|
+
if (!crossed(threshold, runtime.value))
|
|
71
|
+
continue;
|
|
72
|
+
out.push({
|
|
73
|
+
id: threshold.id,
|
|
74
|
+
label: threshold.label,
|
|
75
|
+
severity: threshold.severity ?? "warning",
|
|
76
|
+
source: "meter",
|
|
77
|
+
stacks: 1,
|
|
78
|
+
...(threshold.icon === undefined ? {} : { icon: threshold.icon }),
|
|
79
|
+
fraction: toState(runtime).fraction,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
},
|
|
85
|
+
snapshot() {
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const id of order)
|
|
88
|
+
out[id] = toState(meters.get(id));
|
|
89
|
+
return out;
|
|
90
|
+
},
|
|
91
|
+
ids() {
|
|
92
|
+
return order;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type MoodleSeverity = "good" | "neutral" | "warning" | "critical";
|
|
2
|
+
export type MoodleSource = "meter" | "ailment" | "buff";
|
|
3
|
+
export interface Moodle {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
severity: MoodleSeverity;
|
|
7
|
+
source: MoodleSource;
|
|
8
|
+
stacks: number;
|
|
9
|
+
icon?: string;
|
|
10
|
+
/** 0..1 progress ring — remaining buff time, wound treatment, meter fill. Absent = no ring. */
|
|
11
|
+
fraction?: number;
|
|
12
|
+
/** Free-form note surfaced under the label (e.g. "-3°C", "needs bandage"). */
|
|
13
|
+
note?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const MOODLE_SEVERITY_ORDER: Record<MoodleSeverity, number>;
|
|
16
|
+
/**
|
|
17
|
+
* Merge any number of moodle groups into one stack — meters, ailments, and buffs
|
|
18
|
+
* share this display. Same-id moodles fold together (stacks add, worst severity wins);
|
|
19
|
+
* the result is ordered worst-first so the HUD reads critical statuses at a glance.
|
|
20
|
+
*/
|
|
21
|
+
export declare function stackMoodles(...groups: readonly (readonly Moodle[])[]): Moodle[];
|
|
22
|
+
export interface TimedMoodleInput {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
severity?: MoodleSeverity;
|
|
26
|
+
icon?: string;
|
|
27
|
+
note?: string;
|
|
28
|
+
stacks?: number;
|
|
29
|
+
/** Game-seconds until the moodle drops. Omit for a moodle that stays until removed. */
|
|
30
|
+
duration?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface MoodleStack {
|
|
33
|
+
/** Add or refresh a timed moodle (Valheim food buff, a temporary shelter status). */
|
|
34
|
+
add(input: TimedMoodleInput): void;
|
|
35
|
+
remove(id: string): void;
|
|
36
|
+
has(id: string): boolean;
|
|
37
|
+
/** Advance timers by a game-time delta; drops expired moodles. */
|
|
38
|
+
tick(dt: number): void;
|
|
39
|
+
/** The live timed moodles (buffs, temporary statuses). */
|
|
40
|
+
list(): Moodle[];
|
|
41
|
+
clear(): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A stateful holder for timed status moodles (food buffs, temporary shelter, warmth).
|
|
45
|
+
* Meters and multi-region health derive their own moodles on read; combine all three
|
|
46
|
+
* through `stackMoodles(stack.list(), meterMoodles, ailmentMoodles)` for one display.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createMoodleStack(): MoodleStack;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const MOODLE_SEVERITY_ORDER = {
|
|
2
|
+
critical: 0,
|
|
3
|
+
warning: 1,
|
|
4
|
+
neutral: 2,
|
|
5
|
+
good: 3,
|
|
6
|
+
};
|
|
7
|
+
function mergeStack(into, moodle) {
|
|
8
|
+
const existing = into.get(moodle.id);
|
|
9
|
+
if (existing === undefined) {
|
|
10
|
+
into.set(moodle.id, { ...moodle });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
into.set(moodle.id, {
|
|
14
|
+
...existing,
|
|
15
|
+
stacks: existing.stacks + moodle.stacks,
|
|
16
|
+
severity: MOODLE_SEVERITY_ORDER[moodle.severity] < MOODLE_SEVERITY_ORDER[existing.severity]
|
|
17
|
+
? moodle.severity
|
|
18
|
+
: existing.severity,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Merge any number of moodle groups into one stack — meters, ailments, and buffs
|
|
23
|
+
* share this display. Same-id moodles fold together (stacks add, worst severity wins);
|
|
24
|
+
* the result is ordered worst-first so the HUD reads critical statuses at a glance.
|
|
25
|
+
*/
|
|
26
|
+
export function stackMoodles(...groups) {
|
|
27
|
+
const merged = new Map();
|
|
28
|
+
for (const group of groups)
|
|
29
|
+
for (const moodle of group)
|
|
30
|
+
mergeStack(merged, moodle);
|
|
31
|
+
return [...merged.values()].sort((a, b) => MOODLE_SEVERITY_ORDER[a.severity] - MOODLE_SEVERITY_ORDER[b.severity]);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A stateful holder for timed status moodles (food buffs, temporary shelter, warmth).
|
|
35
|
+
* Meters and multi-region health derive their own moodles on read; combine all three
|
|
36
|
+
* through `stackMoodles(stack.list(), meterMoodles, ailmentMoodles)` for one display.
|
|
37
|
+
*/
|
|
38
|
+
export function createMoodleStack() {
|
|
39
|
+
const moodles = new Map();
|
|
40
|
+
return {
|
|
41
|
+
add(input) {
|
|
42
|
+
moodles.set(input.id, {
|
|
43
|
+
label: input.label,
|
|
44
|
+
severity: input.severity ?? "good",
|
|
45
|
+
stacks: input.stacks ?? 1,
|
|
46
|
+
...(input.icon === undefined ? {} : { icon: input.icon }),
|
|
47
|
+
...(input.note === undefined ? {} : { note: input.note }),
|
|
48
|
+
remaining: input.duration ?? null,
|
|
49
|
+
total: input.duration ?? null,
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
remove(id) {
|
|
53
|
+
moodles.delete(id);
|
|
54
|
+
},
|
|
55
|
+
has(id) {
|
|
56
|
+
return moodles.has(id);
|
|
57
|
+
},
|
|
58
|
+
tick(dt) {
|
|
59
|
+
if (dt <= 0)
|
|
60
|
+
return;
|
|
61
|
+
for (const [id, state] of moodles) {
|
|
62
|
+
if (state.remaining === null)
|
|
63
|
+
continue;
|
|
64
|
+
state.remaining -= dt;
|
|
65
|
+
if (state.remaining <= 0)
|
|
66
|
+
moodles.delete(id);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
list() {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const [id, state] of moodles) {
|
|
72
|
+
out.push({
|
|
73
|
+
id,
|
|
74
|
+
label: state.label,
|
|
75
|
+
severity: state.severity,
|
|
76
|
+
source: "buff",
|
|
77
|
+
stacks: state.stacks,
|
|
78
|
+
...(state.icon === undefined ? {} : { icon: state.icon }),
|
|
79
|
+
...(state.note === undefined ? {} : { note: state.note }),
|
|
80
|
+
...(state.remaining !== null && state.total !== null && state.total > 0
|
|
81
|
+
? { fraction: Math.max(0, Math.min(1, state.remaining / state.total)) }
|
|
82
|
+
: {}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
},
|
|
87
|
+
clear() {
|
|
88
|
+
moodles.clear();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Moodle, MoodleSeverity } from "./moodle.js";
|
|
2
|
+
export interface HealthRegionConfig {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
max: number;
|
|
6
|
+
/** A vital part (head, thorax): the entity dies when it empties. Defaults to false. */
|
|
7
|
+
vital?: boolean;
|
|
8
|
+
/** Extra multiplier on incoming damage — a head takes more per hit. Defaults to 1. */
|
|
9
|
+
vulnerability?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface AilmentConfig {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
/** The part this injury belongs to; drain (bleed) applies here. */
|
|
15
|
+
region?: string;
|
|
16
|
+
severity?: MoodleSeverity;
|
|
17
|
+
icon?: string;
|
|
18
|
+
/** Health lost per game-second per stack while untreated (bleed, infection). */
|
|
19
|
+
drainPerSecond?: number;
|
|
20
|
+
/** Can queue multiple instances (three bleeds stack); otherwise re-applying refreshes. */
|
|
21
|
+
stacking?: boolean;
|
|
22
|
+
/** Treatment item ids that clear this injury — bandage, tourniquet, splint. */
|
|
23
|
+
treatedBy?: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
export interface MultiRegionHealthConfig {
|
|
26
|
+
regions: readonly HealthRegionConfig[];
|
|
27
|
+
/** Ailment catalog keyed by id — the queue references these. */
|
|
28
|
+
ailments?: Record<string, AilmentConfig>;
|
|
29
|
+
}
|
|
30
|
+
export interface RegionHealthState {
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
current: number;
|
|
34
|
+
max: number;
|
|
35
|
+
fraction: number;
|
|
36
|
+
vital: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface AilmentInstance {
|
|
39
|
+
id: string;
|
|
40
|
+
stacks: number;
|
|
41
|
+
}
|
|
42
|
+
export interface DamageResult {
|
|
43
|
+
region: RegionHealthState;
|
|
44
|
+
/** Damage actually applied after vulnerability. */
|
|
45
|
+
applied: number;
|
|
46
|
+
dead: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface TreatResult {
|
|
49
|
+
/** Ailment ids fully or partially cleared by the item. */
|
|
50
|
+
treated: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
export interface MultiRegionHealth {
|
|
53
|
+
damage(regionId: string, amount: number): DamageResult;
|
|
54
|
+
heal(regionId: string, amount: number): RegionHealthState;
|
|
55
|
+
region(regionId: string): RegionHealthState;
|
|
56
|
+
regions(): RegionHealthState[];
|
|
57
|
+
/** 0..1 aggregate across all parts (share-weighted by max). */
|
|
58
|
+
overall(): number;
|
|
59
|
+
/** Queue an ailment from the catalog; stacking ailments increment, others refresh. */
|
|
60
|
+
applyAilment(ailmentId: string): void;
|
|
61
|
+
/** Clear ailments treated by an item id; one item clears one stack of each match. */
|
|
62
|
+
treat(itemId: string): TreatResult;
|
|
63
|
+
/** Apply ailment drains over game-time `dt`; may kill via a vital part. */
|
|
64
|
+
tick(dt: number): {
|
|
65
|
+
dead: boolean;
|
|
66
|
+
};
|
|
67
|
+
ailments(): AilmentInstance[];
|
|
68
|
+
/** Wounds as moodles for the shared status stack (#78 shares #90's moodle display). */
|
|
69
|
+
ailmentMoodles(): Moodle[];
|
|
70
|
+
readonly dead: boolean;
|
|
71
|
+
}
|
|
72
|
+
export declare function createMultiRegionHealth(config: MultiRegionHealthConfig): MultiRegionHealth;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
function clamp(value, min, max) {
|
|
2
|
+
return value < min ? min : value > max ? max : value;
|
|
3
|
+
}
|
|
4
|
+
export function createMultiRegionHealth(config) {
|
|
5
|
+
if (config.regions.length === 0) {
|
|
6
|
+
throw new Error("createMultiRegionHealth: regions must be non-empty");
|
|
7
|
+
}
|
|
8
|
+
const regions = new Map();
|
|
9
|
+
const order = [];
|
|
10
|
+
for (const region of config.regions) {
|
|
11
|
+
regions.set(region.id, { config: region, current: region.max });
|
|
12
|
+
order.push(region.id);
|
|
13
|
+
}
|
|
14
|
+
const catalog = config.ailments ?? {};
|
|
15
|
+
const queue = new Map();
|
|
16
|
+
let dead = false;
|
|
17
|
+
const requireRegion = (id) => {
|
|
18
|
+
const runtime = regions.get(id);
|
|
19
|
+
if (runtime === undefined)
|
|
20
|
+
throw new Error(`unknown health region "${id}"`);
|
|
21
|
+
return runtime;
|
|
22
|
+
};
|
|
23
|
+
const toState = (runtime) => ({
|
|
24
|
+
id: runtime.config.id,
|
|
25
|
+
label: runtime.config.label,
|
|
26
|
+
current: runtime.current,
|
|
27
|
+
max: runtime.config.max,
|
|
28
|
+
fraction: runtime.config.max > 0 ? runtime.current / runtime.config.max : 0,
|
|
29
|
+
vital: runtime.config.vital ?? false,
|
|
30
|
+
});
|
|
31
|
+
const applyDamage = (runtime, amount) => {
|
|
32
|
+
const before = runtime.current;
|
|
33
|
+
runtime.current = clamp(runtime.current - amount, 0, runtime.config.max);
|
|
34
|
+
if ((runtime.config.vital ?? false) && runtime.current <= 0)
|
|
35
|
+
dead = true;
|
|
36
|
+
return before - runtime.current;
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
damage(regionId, amount) {
|
|
40
|
+
const runtime = requireRegion(regionId);
|
|
41
|
+
const scaled = Math.max(0, amount) * (runtime.config.vulnerability ?? 1);
|
|
42
|
+
const applied = applyDamage(runtime, scaled);
|
|
43
|
+
return { region: toState(runtime), applied, dead };
|
|
44
|
+
},
|
|
45
|
+
heal(regionId, amount) {
|
|
46
|
+
const runtime = requireRegion(regionId);
|
|
47
|
+
runtime.current = clamp(runtime.current + Math.max(0, amount), 0, runtime.config.max);
|
|
48
|
+
return toState(runtime);
|
|
49
|
+
},
|
|
50
|
+
region(regionId) {
|
|
51
|
+
return toState(requireRegion(regionId));
|
|
52
|
+
},
|
|
53
|
+
regions() {
|
|
54
|
+
return order.map((id) => toState(regions.get(id)));
|
|
55
|
+
},
|
|
56
|
+
overall() {
|
|
57
|
+
let current = 0;
|
|
58
|
+
let max = 0;
|
|
59
|
+
for (const runtime of regions.values()) {
|
|
60
|
+
current += runtime.current;
|
|
61
|
+
max += runtime.config.max;
|
|
62
|
+
}
|
|
63
|
+
return max > 0 ? current / max : 0;
|
|
64
|
+
},
|
|
65
|
+
applyAilment(ailmentId) {
|
|
66
|
+
const def = catalog[ailmentId];
|
|
67
|
+
if (def === undefined)
|
|
68
|
+
throw new Error(`unknown ailment "${ailmentId}"`);
|
|
69
|
+
const current = queue.get(ailmentId) ?? 0;
|
|
70
|
+
queue.set(ailmentId, def.stacking ?? false ? current + 1 : 1);
|
|
71
|
+
},
|
|
72
|
+
treat(itemId) {
|
|
73
|
+
const treated = [];
|
|
74
|
+
for (const [ailmentId, stacks] of queue) {
|
|
75
|
+
const def = catalog[ailmentId];
|
|
76
|
+
if (def === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
if (!(def.treatedBy ?? []).includes(itemId))
|
|
79
|
+
continue;
|
|
80
|
+
treated.push(ailmentId);
|
|
81
|
+
if (stacks <= 1)
|
|
82
|
+
queue.delete(ailmentId);
|
|
83
|
+
else
|
|
84
|
+
queue.set(ailmentId, stacks - 1);
|
|
85
|
+
}
|
|
86
|
+
return { treated };
|
|
87
|
+
},
|
|
88
|
+
tick(dt) {
|
|
89
|
+
if (dt > 0) {
|
|
90
|
+
for (const [ailmentId, stacks] of queue) {
|
|
91
|
+
const def = catalog[ailmentId];
|
|
92
|
+
if (def === undefined || def.drainPerSecond === undefined)
|
|
93
|
+
continue;
|
|
94
|
+
const target = def.region ?? order[0];
|
|
95
|
+
const runtime = regions.get(target);
|
|
96
|
+
if (runtime === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
applyDamage(runtime, def.drainPerSecond * stacks * dt);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { dead };
|
|
102
|
+
},
|
|
103
|
+
ailments() {
|
|
104
|
+
return [...queue].map(([id, stacks]) => ({ id, stacks }));
|
|
105
|
+
},
|
|
106
|
+
ailmentMoodles() {
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const [ailmentId, stacks] of queue) {
|
|
109
|
+
const def = catalog[ailmentId];
|
|
110
|
+
if (def === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
const untreated = (def.treatedBy ?? []).length > 0;
|
|
113
|
+
out.push({
|
|
114
|
+
id: ailmentId,
|
|
115
|
+
label: def.label,
|
|
116
|
+
severity: def.severity ?? "warning",
|
|
117
|
+
source: "ailment",
|
|
118
|
+
stacks,
|
|
119
|
+
...(def.icon === undefined ? {} : { icon: def.icon }),
|
|
120
|
+
...(untreated ? { note: `needs ${def.treatedBy[0]}` } : {}),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
},
|
|
125
|
+
get dead() {
|
|
126
|
+
return dead;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type AreaTargetInput, type CombatSpatialDeps } from "../combat/effects.js";
|
|
2
|
+
import type { EntityPosition } from "../scene/entityStore.js";
|
|
3
|
+
import type { Aim, QueryArcOptions } from "../scene/spatial.js";
|
|
4
|
+
import type { Tile } from "./tacticalGrid.js";
|
|
5
|
+
export interface PredictiveDeps {
|
|
6
|
+
spatial: CombatSpatialDeps;
|
|
7
|
+
queryArc(options: QueryArcOptions): string[];
|
|
8
|
+
canReceive?(instanceId: string, effect: string): string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface PredictedTarget {
|
|
11
|
+
instanceId: string;
|
|
12
|
+
scale: number;
|
|
13
|
+
}
|
|
14
|
+
export interface AreaPredictInput extends AreaTargetInput {
|
|
15
|
+
effect?: string;
|
|
16
|
+
requireReceive?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface ArcPredictInput {
|
|
19
|
+
from: string;
|
|
20
|
+
aim: Aim;
|
|
21
|
+
radius: number;
|
|
22
|
+
halfAngleDeg?: number;
|
|
23
|
+
effect?: string;
|
|
24
|
+
requireReceive?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function predictAreaEffect(deps: PredictiveDeps, input: AreaPredictInput): PredictedTarget[];
|
|
27
|
+
export declare function predictArcEffect(deps: PredictiveDeps, input: ArcPredictInput): PredictedTarget[];
|
|
28
|
+
export interface TilePredictInput {
|
|
29
|
+
at: EntityPosition;
|
|
30
|
+
radius: number;
|
|
31
|
+
originTile: Tile;
|
|
32
|
+
tileSize: number;
|
|
33
|
+
}
|
|
34
|
+
export declare function predictTiles(input: TilePredictInput): Tile[];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { resolveAreaTargets, } from "../combat/effects.js";
|
|
2
|
+
function receiveGate(deps, effect, requireReceive) {
|
|
3
|
+
if (!requireReceive || effect === undefined || deps.canReceive === undefined)
|
|
4
|
+
return undefined;
|
|
5
|
+
const gate = deps.canReceive;
|
|
6
|
+
return (instanceId) => gate(instanceId, effect) === null;
|
|
7
|
+
}
|
|
8
|
+
export function predictAreaEffect(deps, input) {
|
|
9
|
+
const requireReceive = input.requireReceive ?? true;
|
|
10
|
+
return resolveAreaTargets(deps.spatial, input, receiveGate(deps, input.effect, requireReceive));
|
|
11
|
+
}
|
|
12
|
+
export function predictArcEffect(deps, input) {
|
|
13
|
+
const requireReceive = input.requireReceive ?? true;
|
|
14
|
+
const gate = receiveGate(deps, input.effect, requireReceive);
|
|
15
|
+
const hit = deps.queryArc({
|
|
16
|
+
from: input.from,
|
|
17
|
+
aim: input.aim,
|
|
18
|
+
radius: input.radius,
|
|
19
|
+
...(input.halfAngleDeg === undefined ? {} : { halfAngleDeg: input.halfAngleDeg }),
|
|
20
|
+
});
|
|
21
|
+
const targets = [];
|
|
22
|
+
for (const instanceId of hit) {
|
|
23
|
+
if (gate !== undefined && !gate(instanceId))
|
|
24
|
+
continue;
|
|
25
|
+
targets.push({ instanceId, scale: 1 });
|
|
26
|
+
}
|
|
27
|
+
return targets;
|
|
28
|
+
}
|
|
29
|
+
export function predictTiles(input) {
|
|
30
|
+
const reach = Math.ceil(input.radius / input.tileSize);
|
|
31
|
+
const tiles = [];
|
|
32
|
+
for (let dc = -reach; dc <= reach; dc += 1) {
|
|
33
|
+
for (let dr = -reach; dr <= reach; dr += 1) {
|
|
34
|
+
const worldX = input.at[0] + dc * input.tileSize;
|
|
35
|
+
const worldZ = input.at[2] + dr * input.tileSize;
|
|
36
|
+
const dx = worldX - input.at[0];
|
|
37
|
+
const dz = worldZ - input.at[2];
|
|
38
|
+
if (Math.sqrt(dx * dx + dz * dz) <= input.radius) {
|
|
39
|
+
tiles.push([input.originTile[0] + dc, input.originTile[1] + dr]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return tiles;
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SnapshotSlice<T = unknown> {
|
|
2
|
+
capture(): T;
|
|
3
|
+
restore(state: T): void;
|
|
4
|
+
}
|
|
5
|
+
export type Snapshot = Record<string, unknown>;
|
|
6
|
+
export interface SnapshotStore {
|
|
7
|
+
register<T>(id: string, slice: SnapshotSlice<T>): void;
|
|
8
|
+
unregister(id: string): void;
|
|
9
|
+
capture(): Snapshot;
|
|
10
|
+
restore(snapshot: Snapshot): void;
|
|
11
|
+
push(): Snapshot;
|
|
12
|
+
pop(): boolean;
|
|
13
|
+
peek(): Snapshot | null;
|
|
14
|
+
depth(): number;
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function deepClone<T>(value: T): T;
|
|
18
|
+
export declare function createSnapshotStore(): SnapshotStore;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function deepClone(value) {
|
|
2
|
+
if (value === null || typeof value !== "object")
|
|
3
|
+
return value;
|
|
4
|
+
if (Array.isArray(value))
|
|
5
|
+
return value.map((item) => deepClone(item));
|
|
6
|
+
if (value instanceof Map) {
|
|
7
|
+
const clone = new Map();
|
|
8
|
+
for (const [k, v] of value)
|
|
9
|
+
clone.set(deepClone(k), deepClone(v));
|
|
10
|
+
return clone;
|
|
11
|
+
}
|
|
12
|
+
if (value instanceof Set) {
|
|
13
|
+
const clone = new Set();
|
|
14
|
+
for (const item of value)
|
|
15
|
+
clone.add(deepClone(item));
|
|
16
|
+
return clone;
|
|
17
|
+
}
|
|
18
|
+
const clone = {};
|
|
19
|
+
for (const [k, v] of Object.entries(value))
|
|
20
|
+
clone[k] = deepClone(v);
|
|
21
|
+
return clone;
|
|
22
|
+
}
|
|
23
|
+
export function createSnapshotStore() {
|
|
24
|
+
const slices = new Map();
|
|
25
|
+
const stack = [];
|
|
26
|
+
function capture() {
|
|
27
|
+
const snapshot = {};
|
|
28
|
+
for (const [id, slice] of slices)
|
|
29
|
+
snapshot[id] = deepClone(slice.capture());
|
|
30
|
+
return snapshot;
|
|
31
|
+
}
|
|
32
|
+
function restore(snapshot) {
|
|
33
|
+
for (const [id, slice] of slices) {
|
|
34
|
+
if (id in snapshot)
|
|
35
|
+
slice.restore(deepClone(snapshot[id]));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
register: (id, slice) => {
|
|
40
|
+
slices.set(id, slice);
|
|
41
|
+
},
|
|
42
|
+
unregister: (id) => {
|
|
43
|
+
slices.delete(id);
|
|
44
|
+
},
|
|
45
|
+
capture,
|
|
46
|
+
restore,
|
|
47
|
+
push: () => {
|
|
48
|
+
const snapshot = capture();
|
|
49
|
+
stack.push(snapshot);
|
|
50
|
+
return snapshot;
|
|
51
|
+
},
|
|
52
|
+
pop: () => {
|
|
53
|
+
const snapshot = stack.pop();
|
|
54
|
+
if (snapshot === undefined)
|
|
55
|
+
return false;
|
|
56
|
+
restore(snapshot);
|
|
57
|
+
return true;
|
|
58
|
+
},
|
|
59
|
+
peek: () => (stack.length === 0 ? null : stack[stack.length - 1]),
|
|
60
|
+
depth: () => stack.length,
|
|
61
|
+
clear: () => {
|
|
62
|
+
stack.length = 0;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|