@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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One tier threshold in a {@link ComboMeter}. Tiers gate on the integer combo
|
|
3
|
+
* `count` (not a filled fraction), and `id` is a free string the game owns and
|
|
4
|
+
* styles ("good", "great", "savage", …) — the model never interprets it.
|
|
5
|
+
*/
|
|
6
|
+
export interface ComboTier {
|
|
7
|
+
/** Combo count at or above which this tier becomes active. Tiers sort ascending. */
|
|
8
|
+
threshold: number;
|
|
9
|
+
/** Free-string tier id the game styles; the model never branches on it. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Explicit multiplier while this tier is active. Overrides any fallback curve. */
|
|
12
|
+
multiplier?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Options for {@link createComboMeter}. */
|
|
15
|
+
export interface ComboMeterConfig {
|
|
16
|
+
/**
|
|
17
|
+
* Decay window in ms: how long after the last {@link ComboMeter.hit} the combo
|
|
18
|
+
* survives before it drops. Each `hit` resets this window; when it elapses the
|
|
19
|
+
* combo drops (to 0, or by {@link ComboMeterConfig.dropStep}).
|
|
20
|
+
*/
|
|
21
|
+
windowMs: number;
|
|
22
|
+
/** Tier thresholds on the integer count. Sorted ascending internally; ids are free strings. */
|
|
23
|
+
tiers?: readonly ComboTier[];
|
|
24
|
+
/**
|
|
25
|
+
* When the window elapses, drop the count by this many instead of clearing to 0,
|
|
26
|
+
* re-arming the window so a long combo bleeds down step by step. Omit → drop to 0.
|
|
27
|
+
*/
|
|
28
|
+
dropStep?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Fallback multiplier growth when the active tier has no explicit `multiplier`:
|
|
31
|
+
* `1 + (tierIndex + 1) * multiplierPerTier`. Below the first tier the multiplier
|
|
32
|
+
* is always 1. Ignored where a tier sets its own `multiplier`.
|
|
33
|
+
*/
|
|
34
|
+
multiplierPerTier?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Last-resort multiplier curve when neither the active tier's `multiplier` nor
|
|
37
|
+
* `multiplierPerTier` applies. Receives the current count and active tier index
|
|
38
|
+
* (`-1` below the first tier). Not serialized — snapshot/restore carry state, not config.
|
|
39
|
+
*/
|
|
40
|
+
multiplierCurve?: (count: number, tierIndex: number) => number;
|
|
41
|
+
/** Injected clock (ms). Default `Date.now`. Ignored once {@link ComboMeter.update} drives time. */
|
|
42
|
+
now?: () => number;
|
|
43
|
+
/** Initial combo count (pre-seed a demo/save at a high chain). Default 0. */
|
|
44
|
+
count?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A pooled, read-only snapshot of the live meter for a renderer to draw each
|
|
48
|
+
* frame. Reused across {@link ComboMeter.view} calls and overwritten on the next
|
|
49
|
+
* one — read it, do not retain it. For a save, use {@link ComboMeter.snapshot}.
|
|
50
|
+
*/
|
|
51
|
+
export interface ComboMeterView {
|
|
52
|
+
/** Current combo count. */
|
|
53
|
+
count: number;
|
|
54
|
+
/** Highest count reached since the last {@link ComboMeter.reset}. */
|
|
55
|
+
peak: number;
|
|
56
|
+
/** Active tier id, or `null` below the first tier. */
|
|
57
|
+
tier: string | null;
|
|
58
|
+
/** Active tier index into the sorted tier list, or `-1` below the first tier. */
|
|
59
|
+
tierIndex: number;
|
|
60
|
+
/** Derived multiplier for the active tier (default 1). */
|
|
61
|
+
multiplier: number;
|
|
62
|
+
/** Milliseconds left in the decay window before the next drop (0 when idle). */
|
|
63
|
+
remainingMs: number;
|
|
64
|
+
/** Window remaining as `0..1` — for a draining ring/bar. */
|
|
65
|
+
fraction: number;
|
|
66
|
+
/** Free-string `kind` of the most recent hit, or `""` — carried through, never interpreted. */
|
|
67
|
+
kind: string;
|
|
68
|
+
}
|
|
69
|
+
/** Serializable state for save/restore — clock-agnostic (stores time left, not an absolute deadline). */
|
|
70
|
+
export interface ComboMeterSnapshot {
|
|
71
|
+
/** Combo count. */
|
|
72
|
+
count: number;
|
|
73
|
+
/** Peak count reached. */
|
|
74
|
+
peak: number;
|
|
75
|
+
/** Milliseconds remaining in the decay window at snapshot time. */
|
|
76
|
+
remainingMs: number;
|
|
77
|
+
/** Active tier id at snapshot time (recomputed on restore; kept for readability). */
|
|
78
|
+
tier: string | null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A live, observable combo/multiplier meter — an integer hit chain with a decay
|
|
82
|
+
* window, free-string tier thresholds, a derived multiplier, and peak tracking.
|
|
83
|
+
*/
|
|
84
|
+
export interface ComboMeter {
|
|
85
|
+
/**
|
|
86
|
+
* Register a landed hit: bump the count by one and reset the decay window. `kind`
|
|
87
|
+
* is a free string carried through to {@link ComboMeter.view} for the renderer to
|
|
88
|
+
* color/skin; the model never interprets it. Returns the new count.
|
|
89
|
+
*/
|
|
90
|
+
hit(kind?: string): number;
|
|
91
|
+
/**
|
|
92
|
+
* Advance the decay window by `dtSeconds`, dropping the combo when the window
|
|
93
|
+
* elapses. Calling this once switches the meter to delta-driven time (the injected
|
|
94
|
+
* `now` clock is then ignored) — drive it each frame, or omit it and let reads read
|
|
95
|
+
* `now`. Notifies subscribers when the count changes.
|
|
96
|
+
*/
|
|
97
|
+
update(dtSeconds: number): void;
|
|
98
|
+
/** Current combo count (processes any pending window expiry against the clock first). */
|
|
99
|
+
count(): number;
|
|
100
|
+
/** Highest count reached since the last {@link ComboMeter.reset}. */
|
|
101
|
+
peak(): number;
|
|
102
|
+
/** Active tier id, or `null` below the first tier. */
|
|
103
|
+
tier(): string | null;
|
|
104
|
+
/** Derived multiplier for the active tier (default 1). */
|
|
105
|
+
multiplier(): number;
|
|
106
|
+
/** Milliseconds left in the decay window before the next drop (0 when idle). */
|
|
107
|
+
remainingMs(): number;
|
|
108
|
+
/** Window remaining as `0..1`, for a draining ring/bar. */
|
|
109
|
+
fraction(): number;
|
|
110
|
+
/** The pooled view to draw this frame (do not retain). */
|
|
111
|
+
view(): ComboMeterView;
|
|
112
|
+
/** Clear the chain, peak, and window to empty. Notifies subscribers. */
|
|
113
|
+
reset(): void;
|
|
114
|
+
/** Patch config (window, tiers, drop step, multiplier curve) in place. Notifies subscribers. */
|
|
115
|
+
configure(patch: Partial<ComboMeterConfig>): void;
|
|
116
|
+
/** Observe changes (hit, drop, reset, configure, restore). Returns an unsubscribe fn. */
|
|
117
|
+
subscribe(listener: () => void): () => void;
|
|
118
|
+
/** Serializable state for a save. */
|
|
119
|
+
snapshot(): ComboMeterSnapshot;
|
|
120
|
+
/** Restore from a {@link ComboMeterSnapshot}, re-anchoring the window to the current clock. */
|
|
121
|
+
restore(snapshot: ComboMeterSnapshot): void;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A count-based combo / multiplier meter: an integer hit chain that climbs with
|
|
125
|
+
* every `hit()` and drops when a decay window elapses, crossing free-string tier
|
|
126
|
+
* thresholds ("good" → "great" → "savage") that derive a score multiplier, while
|
|
127
|
+
* tracking the peak reached. Time is injected (`now`, default `Date.now`) and/or
|
|
128
|
+
* driven by `update(dt)`; `view()` returns a pooled draw-state and
|
|
129
|
+
* `snapshot`/`restore` round-trips the chain through a save.
|
|
130
|
+
*
|
|
131
|
+
* Distinct from the existing meters — reused none of them because none is this
|
|
132
|
+
* shape: `stats/eventMeter` is a value/heat gauge that rises by tagged gains and
|
|
133
|
+
* decays continuously (not an integer chain count with a reset-on-hit window);
|
|
134
|
+
* `combat/comboPoints` is a finisher-point pool that hard-expires with no tiers,
|
|
135
|
+
* multiplier, or subscribe; `stats/accumulatorMeter` is a float gauge crossing
|
|
136
|
+
* `at`-value tiers, not a count with a draining window and derived multiplier.
|
|
137
|
+
*
|
|
138
|
+
* @capability combo-meter count-based combo/multiplier meter with a decay window, free-string tier thresholds, derived multiplier, peak tracking and snapshot/restore
|
|
139
|
+
*/
|
|
140
|
+
export declare function createComboMeter(config: ComboMeterConfig): ComboMeter;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
function sortTiers(tiers) {
|
|
2
|
+
return tiers === undefined ? [] : [...tiers].sort((a, b) => a.threshold - b.threshold);
|
|
3
|
+
}
|
|
4
|
+
/** Index of the highest tier whose `threshold` `count` has reached, or `-1` below the first. */
|
|
5
|
+
function activeIndex(count, tiers) {
|
|
6
|
+
let idx = -1;
|
|
7
|
+
for (let i = 0; i < tiers.length; i += 1) {
|
|
8
|
+
if (count >= tiers[i].threshold)
|
|
9
|
+
idx = i;
|
|
10
|
+
else
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
return idx;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A count-based combo / multiplier meter: an integer hit chain that climbs with
|
|
17
|
+
* every `hit()` and drops when a decay window elapses, crossing free-string tier
|
|
18
|
+
* thresholds ("good" → "great" → "savage") that derive a score multiplier, while
|
|
19
|
+
* tracking the peak reached. Time is injected (`now`, default `Date.now`) and/or
|
|
20
|
+
* driven by `update(dt)`; `view()` returns a pooled draw-state and
|
|
21
|
+
* `snapshot`/`restore` round-trips the chain through a save.
|
|
22
|
+
*
|
|
23
|
+
* Distinct from the existing meters — reused none of them because none is this
|
|
24
|
+
* shape: `stats/eventMeter` is a value/heat gauge that rises by tagged gains and
|
|
25
|
+
* decays continuously (not an integer chain count with a reset-on-hit window);
|
|
26
|
+
* `combat/comboPoints` is a finisher-point pool that hard-expires with no tiers,
|
|
27
|
+
* multiplier, or subscribe; `stats/accumulatorMeter` is a float gauge crossing
|
|
28
|
+
* `at`-value tiers, not a count with a draining window and derived multiplier.
|
|
29
|
+
*
|
|
30
|
+
* @capability combo-meter count-based combo/multiplier meter with a decay window, free-string tier thresholds, derived multiplier, peak tracking and snapshot/restore
|
|
31
|
+
*/
|
|
32
|
+
export function createComboMeter(config) {
|
|
33
|
+
const now = config.now ?? Date.now;
|
|
34
|
+
let windowMs = Math.max(0, config.windowMs);
|
|
35
|
+
let tiers = sortTiers(config.tiers);
|
|
36
|
+
let dropStep = config.dropStep;
|
|
37
|
+
let multiplierPerTier = config.multiplierPerTier;
|
|
38
|
+
let multiplierCurve = config.multiplierCurve;
|
|
39
|
+
let count = Math.max(0, Math.floor(config.count ?? 0));
|
|
40
|
+
let peak = count;
|
|
41
|
+
let kind = "";
|
|
42
|
+
// Logical clock (ms). `manual` flips true on the first update(dt) call, after
|
|
43
|
+
// which the injected `now` is ignored and only update(dt) advances time.
|
|
44
|
+
let manual = false;
|
|
45
|
+
let time = now();
|
|
46
|
+
let lastNow = time;
|
|
47
|
+
// Absolute logical time the current window ends; null when the chain is empty.
|
|
48
|
+
let deadline = count > 0 && windowMs > 0 ? time + windowMs : null;
|
|
49
|
+
const listeners = new Set();
|
|
50
|
+
const pooledView = {
|
|
51
|
+
count: 0,
|
|
52
|
+
peak: 0,
|
|
53
|
+
tier: null,
|
|
54
|
+
tierIndex: -1,
|
|
55
|
+
multiplier: 1,
|
|
56
|
+
remainingMs: 0,
|
|
57
|
+
fraction: 0,
|
|
58
|
+
kind: "",
|
|
59
|
+
};
|
|
60
|
+
function notify() {
|
|
61
|
+
for (const listener of listeners)
|
|
62
|
+
listener();
|
|
63
|
+
}
|
|
64
|
+
/** Sync the logical clock from wall time unless update(dt) has taken over. */
|
|
65
|
+
function syncWall() {
|
|
66
|
+
if (manual)
|
|
67
|
+
return;
|
|
68
|
+
const t = now();
|
|
69
|
+
time += t - lastNow;
|
|
70
|
+
lastNow = t;
|
|
71
|
+
}
|
|
72
|
+
/** Drop the chain once for each elapsed window; returns whether anything changed. */
|
|
73
|
+
function processExpiry() {
|
|
74
|
+
if (deadline === null || windowMs <= 0)
|
|
75
|
+
return false;
|
|
76
|
+
let changed = false;
|
|
77
|
+
while (deadline !== null && time >= deadline) {
|
|
78
|
+
if (dropStep !== undefined && dropStep > 0 && count - dropStep > 0) {
|
|
79
|
+
count -= dropStep;
|
|
80
|
+
deadline += windowMs;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
count = 0;
|
|
84
|
+
deadline = null;
|
|
85
|
+
}
|
|
86
|
+
changed = true;
|
|
87
|
+
}
|
|
88
|
+
return changed;
|
|
89
|
+
}
|
|
90
|
+
/** Bring the clock and expiry state up to date before any read. */
|
|
91
|
+
function pull() {
|
|
92
|
+
syncWall();
|
|
93
|
+
if (processExpiry())
|
|
94
|
+
notify();
|
|
95
|
+
}
|
|
96
|
+
function multiplierAt(c) {
|
|
97
|
+
const idx = activeIndex(c, tiers);
|
|
98
|
+
if (idx >= 0) {
|
|
99
|
+
const explicit = tiers[idx].multiplier;
|
|
100
|
+
if (explicit !== undefined)
|
|
101
|
+
return explicit;
|
|
102
|
+
}
|
|
103
|
+
if (multiplierPerTier !== undefined)
|
|
104
|
+
return 1 + (idx + 1) * multiplierPerTier;
|
|
105
|
+
if (multiplierCurve !== undefined)
|
|
106
|
+
return multiplierCurve(c, idx);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
function remaining() {
|
|
110
|
+
if (count <= 0 || deadline === null)
|
|
111
|
+
return 0;
|
|
112
|
+
return Math.max(0, deadline - time);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
hit(hitKind = "") {
|
|
116
|
+
syncWall();
|
|
117
|
+
processExpiry();
|
|
118
|
+
count += 1;
|
|
119
|
+
if (count > peak)
|
|
120
|
+
peak = count;
|
|
121
|
+
kind = hitKind;
|
|
122
|
+
deadline = windowMs > 0 ? time + windowMs : null;
|
|
123
|
+
notify();
|
|
124
|
+
return count;
|
|
125
|
+
},
|
|
126
|
+
update(dtSeconds) {
|
|
127
|
+
manual = true;
|
|
128
|
+
if (dtSeconds > 0)
|
|
129
|
+
time += dtSeconds * 1000;
|
|
130
|
+
if (processExpiry())
|
|
131
|
+
notify();
|
|
132
|
+
},
|
|
133
|
+
count() {
|
|
134
|
+
pull();
|
|
135
|
+
return count;
|
|
136
|
+
},
|
|
137
|
+
peak() {
|
|
138
|
+
pull();
|
|
139
|
+
return peak;
|
|
140
|
+
},
|
|
141
|
+
tier() {
|
|
142
|
+
pull();
|
|
143
|
+
const idx = activeIndex(count, tiers);
|
|
144
|
+
return idx >= 0 ? tiers[idx].id : null;
|
|
145
|
+
},
|
|
146
|
+
multiplier() {
|
|
147
|
+
pull();
|
|
148
|
+
return multiplierAt(count);
|
|
149
|
+
},
|
|
150
|
+
remainingMs() {
|
|
151
|
+
pull();
|
|
152
|
+
return remaining();
|
|
153
|
+
},
|
|
154
|
+
fraction() {
|
|
155
|
+
pull();
|
|
156
|
+
return windowMs <= 0 ? 0 : Math.max(0, Math.min(1, remaining() / windowMs));
|
|
157
|
+
},
|
|
158
|
+
view() {
|
|
159
|
+
pull();
|
|
160
|
+
const idx = activeIndex(count, tiers);
|
|
161
|
+
const rem = remaining();
|
|
162
|
+
pooledView.count = count;
|
|
163
|
+
pooledView.peak = peak;
|
|
164
|
+
pooledView.tier = idx >= 0 ? tiers[idx].id : null;
|
|
165
|
+
pooledView.tierIndex = idx;
|
|
166
|
+
pooledView.multiplier = multiplierAt(count);
|
|
167
|
+
pooledView.remainingMs = rem;
|
|
168
|
+
pooledView.fraction = windowMs <= 0 ? 0 : Math.max(0, Math.min(1, rem / windowMs));
|
|
169
|
+
pooledView.kind = kind;
|
|
170
|
+
return pooledView;
|
|
171
|
+
},
|
|
172
|
+
reset() {
|
|
173
|
+
count = 0;
|
|
174
|
+
peak = 0;
|
|
175
|
+
kind = "";
|
|
176
|
+
deadline = null;
|
|
177
|
+
notify();
|
|
178
|
+
},
|
|
179
|
+
configure(patch) {
|
|
180
|
+
if (patch.windowMs !== undefined)
|
|
181
|
+
windowMs = Math.max(0, patch.windowMs);
|
|
182
|
+
if (patch.tiers !== undefined)
|
|
183
|
+
tiers = sortTiers(patch.tiers);
|
|
184
|
+
if ("dropStep" in patch)
|
|
185
|
+
dropStep = patch.dropStep;
|
|
186
|
+
if ("multiplierPerTier" in patch)
|
|
187
|
+
multiplierPerTier = patch.multiplierPerTier;
|
|
188
|
+
if ("multiplierCurve" in patch)
|
|
189
|
+
multiplierCurve = patch.multiplierCurve;
|
|
190
|
+
// Re-arm the window if it changed while a chain is live.
|
|
191
|
+
if (patch.windowMs !== undefined && count > 0) {
|
|
192
|
+
deadline = windowMs > 0 ? time + windowMs : null;
|
|
193
|
+
}
|
|
194
|
+
notify();
|
|
195
|
+
},
|
|
196
|
+
subscribe(listener) {
|
|
197
|
+
listeners.add(listener);
|
|
198
|
+
return () => listeners.delete(listener);
|
|
199
|
+
},
|
|
200
|
+
snapshot() {
|
|
201
|
+
syncWall();
|
|
202
|
+
processExpiry();
|
|
203
|
+
const idx = activeIndex(count, tiers);
|
|
204
|
+
return { count, peak, remainingMs: remaining(), tier: idx >= 0 ? tiers[idx].id : null };
|
|
205
|
+
},
|
|
206
|
+
restore(snapshot) {
|
|
207
|
+
syncWall();
|
|
208
|
+
count = Math.max(0, Math.floor(snapshot.count));
|
|
209
|
+
peak = Math.max(count, Math.floor(snapshot.peak));
|
|
210
|
+
kind = "";
|
|
211
|
+
deadline = count > 0 && windowMs > 0 && snapshot.remainingMs > 0 ? time + snapshot.remainingMs : null;
|
|
212
|
+
notify();
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -2,6 +2,13 @@ export interface ComboPointsConfig {
|
|
|
2
2
|
max: number;
|
|
3
3
|
expireAfterSeconds?: number;
|
|
4
4
|
}
|
|
5
|
+
/** Complete serializable state for {@link ComboPoints}; `remaining: null` means no expiry is running. */
|
|
6
|
+
export interface ComboPointsSnapshot {
|
|
7
|
+
points: number;
|
|
8
|
+
remaining: number | null;
|
|
9
|
+
max: number;
|
|
10
|
+
expireAfterSeconds: number | null;
|
|
11
|
+
}
|
|
5
12
|
export interface ComboPoints {
|
|
6
13
|
points(): number;
|
|
7
14
|
gain(amount?: number): number;
|
|
@@ -10,6 +17,10 @@ export interface ComboPoints {
|
|
|
10
17
|
tick(dtSeconds: number): void;
|
|
11
18
|
expiresIn(): number | null;
|
|
12
19
|
clear(): void;
|
|
20
|
+
/** The whole builder as plain data — save it, ship it to a host, diff it in a test. */
|
|
21
|
+
snapshot(): ComboPointsSnapshot;
|
|
22
|
+
/** Adopt a snapshot, replacing points, expiry, and tuning. */
|
|
23
|
+
restore(state: ComboPointsSnapshot): void;
|
|
13
24
|
}
|
|
14
25
|
/**
|
|
15
26
|
* Build and spend finisher points — the combo-point economy behind rogue-style builders and spenders.
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* @capability combo-points build up and spend finisher/combo points
|
|
5
5
|
*/
|
|
6
6
|
export function createComboPoints(config) {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
let max = config.max;
|
|
8
|
+
let expireAfterSeconds = config.expireAfterSeconds;
|
|
9
9
|
let current = 0;
|
|
10
10
|
let remaining = null;
|
|
11
11
|
return {
|
|
@@ -49,6 +49,15 @@ export function createComboPoints(config) {
|
|
|
49
49
|
return null;
|
|
50
50
|
return remaining;
|
|
51
51
|
},
|
|
52
|
+
snapshot() {
|
|
53
|
+
return { points: current, remaining, max, expireAfterSeconds: expireAfterSeconds ?? null };
|
|
54
|
+
},
|
|
55
|
+
restore(state) {
|
|
56
|
+
max = state.max;
|
|
57
|
+
expireAfterSeconds = state.expireAfterSeconds ?? undefined;
|
|
58
|
+
current = Math.max(0, Math.min(max, state.points));
|
|
59
|
+
remaining = current <= 0 ? null : state.remaining;
|
|
60
|
+
},
|
|
52
61
|
clear() {
|
|
53
62
|
current = 0;
|
|
54
63
|
remaining = null;
|
|
@@ -29,7 +29,7 @@ export interface DamageHitInput {
|
|
|
29
29
|
currentStatus?: StatusInstance | null;
|
|
30
30
|
/** Statuses the target is immune to. */
|
|
31
31
|
immunity?: StatusImmunity | null;
|
|
32
|
-
/** Deterministic RNG for the status roll
|
|
32
|
+
/** Deterministic RNG for the status roll. Omitting it falls back to `Math.random` and warns once — pass `ctx.rng`. */
|
|
33
33
|
rng?: () => number;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
@@ -11,6 +11,11 @@ export interface DefensiveWindowConfig {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
export type DefenseOutcome = "parry" | "block" | "iframe" | "hit";
|
|
14
|
+
/**
|
|
15
|
+
* Result of resolving an incoming hit against a defensive window: `outcome` is the graded defense
|
|
16
|
+
* (`"parry"`/`"block"`/`"iframe"`/`"hit"`) and `perfect` is true when the input landed inside the
|
|
17
|
+
* tight just-frame at the window's start rather than its wider active span.
|
|
18
|
+
*/
|
|
14
19
|
export interface DefenseResolution {
|
|
15
20
|
outcome: DefenseOutcome;
|
|
16
21
|
perfect: boolean;
|
package/dist/combat/effects.d.ts
CHANGED
|
@@ -29,11 +29,29 @@ export interface AppliedPoolDelta {
|
|
|
29
29
|
statId: string;
|
|
30
30
|
delta: number;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Identity of an entity slain by a lethal effect, captured before the death system despawns it.
|
|
34
|
+
* Serializable and genre-agnostic: `catalogId` is the slain entity's spawn kind/name (the same
|
|
35
|
+
* value the `entity.died` event carries), `name` an optional human-readable label for kill feeds,
|
|
36
|
+
* and `userId` the owning player when the slain unit was player-controlled. The slain entity's
|
|
37
|
+
* instance id is already {@link EffectResult.instanceId}.
|
|
38
|
+
*/
|
|
39
|
+
export interface SlainIdentity {
|
|
40
|
+
catalogId: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
userId?: string;
|
|
43
|
+
}
|
|
32
44
|
export interface EffectResult {
|
|
33
45
|
instanceId: string;
|
|
34
46
|
effect: string;
|
|
35
47
|
applied: AppliedPoolDelta[];
|
|
36
48
|
lethal: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Present only on a lethal hit: the slain entity's identity, captured before despawn so
|
|
51
|
+
* kill-credit / XP can read `catalogId`/`name` without a game-side spawn-time registry.
|
|
52
|
+
* A non-lethal hit omits this field.
|
|
53
|
+
*/
|
|
54
|
+
slain?: SlainIdentity;
|
|
37
55
|
}
|
|
38
56
|
export interface LethalContext {
|
|
39
57
|
from: string;
|
|
@@ -68,6 +86,12 @@ export interface EffectSystemDeps {
|
|
|
68
86
|
getStat(itemId: string, stat: string): number | null;
|
|
69
87
|
spatial: CombatSpatialDeps;
|
|
70
88
|
drainStatByEffect?: Record<string, string>;
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the target's identity, called on a lethal hit *before* {@link onLethal} despawns it.
|
|
91
|
+
* The returned {@link SlainIdentity} is attached to the lethal {@link EffectResult.slain} so kill
|
|
92
|
+
* credit works without a parallel registry. Return `null`/`undefined` to omit identity.
|
|
93
|
+
*/
|
|
94
|
+
resolveSlainIdentity?(instanceId: string): SlainIdentity | null | undefined;
|
|
71
95
|
onLethal?(instanceId: string, ctx: LethalContext): void;
|
|
72
96
|
}
|
|
73
97
|
export interface EffectSystem {
|
package/dist/combat/effects.js
CHANGED
|
@@ -103,8 +103,13 @@ export function createEffectSystem(deps) {
|
|
|
103
103
|
if (canReceive(instanceId, effect, drainMagnitude) !== null)
|
|
104
104
|
return null;
|
|
105
105
|
const result = drainPools(instanceId, effect, rule, drainMagnitude);
|
|
106
|
-
if (result.lethal)
|
|
106
|
+
if (result.lethal) {
|
|
107
|
+
// Capture identity before onLethal, which runs the death system and despawns the target.
|
|
108
|
+
const slain = deps.resolveSlainIdentity?.(instanceId);
|
|
109
|
+
if (slain !== null && slain !== undefined)
|
|
110
|
+
result.slain = slain;
|
|
107
111
|
deps.onLethal?.(instanceId, { from, via, effect });
|
|
112
|
+
}
|
|
108
113
|
return result;
|
|
109
114
|
}
|
|
110
115
|
return {
|
|
@@ -40,6 +40,12 @@ export interface ProjectileSettleReport {
|
|
|
40
40
|
at: EntityPosition;
|
|
41
41
|
effect: string;
|
|
42
42
|
hit: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* True for lobbed/exploding shots (grenades, launchers, rockets) whose real path is an arc,
|
|
45
|
+
* false for direct-fire shots (bullets, bolts) that travel muzzle→impact in a straight line.
|
|
46
|
+
* Presentation uses it to keep straight-line tracers off arced projectiles.
|
|
47
|
+
*/
|
|
48
|
+
ballistic: boolean;
|
|
43
49
|
}
|
|
44
50
|
export interface ProjectileObjectsDeps {
|
|
45
51
|
list(): readonly {
|
|
@@ -308,7 +308,7 @@ export function createProjectileSystem(deps) {
|
|
|
308
308
|
radius,
|
|
309
309
|
falloff: splashRadius === null ? "none" : "linear",
|
|
310
310
|
});
|
|
311
|
-
deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: hits.length > 0 });
|
|
311
|
+
deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: hits.length > 0, ballistic: true });
|
|
312
312
|
return { status: "settled", shotId, at, hits, origin: originTuple };
|
|
313
313
|
}
|
|
314
314
|
const { visible, rawHits } = predictHits(input);
|
|
@@ -343,7 +343,7 @@ export function createProjectileSystem(deps) {
|
|
|
343
343
|
else {
|
|
344
344
|
at = missPoint(input);
|
|
345
345
|
}
|
|
346
|
-
deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: hits.length > 0 });
|
|
346
|
+
deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: hits.length > 0, ballistic: false });
|
|
347
347
|
return { status: "settled", shotId, at, hits, origin: originTuple };
|
|
348
348
|
},
|
|
349
349
|
};
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External storage a {@link RegenShield} reads and writes instead of keeping the value itself —
|
|
3
|
+
* an entity stat, a replicated store, a save record. This is the same port `Magazine` exposes as
|
|
4
|
+
* `MagazineReserve`, and it exists for the same reason: in most games the shield value is already
|
|
5
|
+
* owned by the stat system that damage, the HUD, and replication all read, so a primitive that
|
|
6
|
+
* insists on owning the number privately cannot be adopted without creating a second source of truth.
|
|
7
|
+
*
|
|
8
|
+
* A pool-backed shield also watches for *external* decreases — damage applied through the game's own
|
|
9
|
+
* effect pipeline rather than through `damage()` — and restarts the regen grace period when it sees
|
|
10
|
+
* one, which is the per-entity snapshot bookkeeping games otherwise hand-roll.
|
|
11
|
+
*/
|
|
12
|
+
export interface RegenShieldPool {
|
|
13
|
+
current(): number;
|
|
14
|
+
max(): number;
|
|
15
|
+
/** Write the value back; implementations clamp to their own bounds. */
|
|
16
|
+
set(value: number): void;
|
|
17
|
+
}
|
|
1
18
|
/** Tuning for `createRegenShield`: pool size, refill rate, and the post-damage grace period. */
|
|
2
19
|
export interface RegenShieldConfig {
|
|
3
20
|
max: number;
|
|
@@ -5,8 +22,14 @@ export interface RegenShieldConfig {
|
|
|
5
22
|
regenPerSecond: number;
|
|
6
23
|
/** Grace period after the last damage before regen resumes. */
|
|
7
24
|
regenDelayMs: number;
|
|
8
|
-
/** Starting value; defaults to `max` (full). */
|
|
25
|
+
/** Starting value; defaults to `max` (full). Ignored when `pool` is set — the pool owns the value. */
|
|
9
26
|
current?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Back this shield with external storage instead of internal state. `max` and `current` then come
|
|
29
|
+
* from the pool, `setMax` is a no-op (the pool owns its bounds), and a drop in the pool's value
|
|
30
|
+
* between ticks is treated as damage for the purposes of the regen grace period.
|
|
31
|
+
*/
|
|
32
|
+
pool?: RegenShieldPool;
|
|
10
33
|
}
|
|
11
34
|
/**
|
|
12
35
|
* A shield pool that stops regenerating for `regenDelayMs` after every hit, then refills at
|
|
@@ -28,8 +51,23 @@ export interface RegenShield {
|
|
|
28
51
|
/** Add `amount` without touching the regen timer (a pickup or scripted refill). */
|
|
29
52
|
restore(amount: number): void;
|
|
30
53
|
set(value: number): void;
|
|
54
|
+
/**
|
|
55
|
+
* Restart the regen grace period without changing the value — for damage the shield cannot see,
|
|
56
|
+
* such as a hit that landed on health after the shield was already down, or damage routed through
|
|
57
|
+
* a game's own effect pipeline.
|
|
58
|
+
*/
|
|
59
|
+
noteDamage(): void;
|
|
31
60
|
/** Advance time: count down the grace period, then regenerate once it has elapsed. */
|
|
32
|
-
tick(dtSeconds: number): void;
|
|
61
|
+
tick(dtSeconds: number, rates?: RegenShieldRates): void;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Per-tick rate overrides, for games that derive regen from live state — a talent tree, a buff, an
|
|
65
|
+
* equipped mod — rather than a constant fixed when the shield was built. Omit to use the configured
|
|
66
|
+
* values; the grace period in effect is whichever `regenDelayMs` was last passed to `tick`.
|
|
67
|
+
*/
|
|
68
|
+
export interface RegenShieldRates {
|
|
69
|
+
regenPerSecond?: number;
|
|
70
|
+
regenDelayMs?: number;
|
|
33
71
|
}
|
|
34
72
|
/**
|
|
35
73
|
* Builds a {@link RegenShield} that suppresses regen for `regenDelayMs` after each hit.
|