@jgengine/core 0.8.0 → 0.9.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 +16 -0
- package/dist/ai/flock.d.ts +36 -0
- package/dist/ai/flock.js +182 -0
- package/dist/ai/groupAssist.d.ts +19 -0
- package/dist/ai/groupAssist.js +59 -0
- package/dist/ai/laneSelect.d.ts +21 -0
- package/dist/ai/laneSelect.js +37 -0
- package/dist/ai/mobBrain.d.ts +68 -0
- package/dist/ai/mobBrain.js +115 -0
- package/dist/ai/spawnDirector.js +1 -1
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/combat/abilityKit.d.ts +17 -2
- package/dist/combat/abilityKit.js +62 -13
- package/dist/combat/castRunner.d.ts +40 -0
- package/dist/combat/castRunner.js +56 -0
- package/dist/combat/comboPoints.d.ts +14 -0
- package/dist/combat/comboPoints.js +52 -0
- package/dist/combat/defensiveWindow.d.ts +1 -1
- package/dist/combat/defensiveWindow.js +5 -3
- package/dist/combat/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +201 -129
- package/dist/combat/resistance.d.ts +16 -4
- package/dist/combat/resistance.js +43 -7
- package/dist/combat/resourcePool.d.ts +20 -0
- package/dist/combat/resourcePool.js +55 -0
- package/dist/combat/shotOrigin.d.ts +40 -0
- package/dist/combat/shotOrigin.js +57 -0
- package/dist/combat/telegraph.d.ts +25 -0
- package/dist/combat/telegraph.js +47 -0
- package/dist/crafting/recipe.js +4 -1
- package/dist/devtools/devtools.d.ts +72 -24
- package/dist/devtools/devtools.js +449 -101
- package/dist/devtools/transformTunables.js +3 -3
- package/dist/devtools/tunableSchema.d.ts +140 -0
- package/dist/devtools/tunableSchema.js +476 -0
- package/dist/faction/factions.d.ts +33 -0
- package/dist/faction/factions.js +76 -0
- package/dist/faction/reputation.d.ts +32 -0
- package/dist/faction/reputation.js +82 -0
- package/dist/game/chat.d.ts +2 -0
- package/dist/game/chat.js +9 -1
- package/dist/game/chatFilter.d.ts +15 -0
- package/dist/game/chatFilter.js +55 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +2 -0
- package/dist/game/events.d.ts +10 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +50 -1
- package/dist/game/playableGame.js +1 -1
- package/dist/game/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +39 -1
- package/dist/game/race.js +96 -11
- package/dist/game/recordBook.d.ts +29 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/input/axisInput.d.ts +26 -3
- package/dist/input/axisInput.js +44 -4
- package/dist/input/bindingOverrides.d.ts +25 -0
- package/dist/input/bindingOverrides.js +95 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +6 -0
- package/dist/input/touchScheme.js +45 -7
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +23 -1
- package/dist/movement/dash.d.ts +3 -0
- package/dist/movement/dash.js +19 -5
- package/dist/movement/glideModel.d.ts +44 -0
- package/dist/movement/glideModel.js +54 -0
- package/dist/movement/grappleSwing.d.ts +34 -0
- package/dist/movement/grappleSwing.js +56 -0
- package/dist/movement/leaderTrail.d.ts +29 -0
- package/dist/movement/leaderTrail.js +67 -0
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- package/dist/multiplayer/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/presenceModel.d.ts +14 -2
- package/dist/multiplayer/presenceModel.js +15 -3
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -0
- package/dist/nav/navGrid.js +30 -4
- package/dist/nav/railGraph.d.ts +66 -0
- package/dist/nav/railGraph.js +129 -0
- package/dist/nav/timetable.d.ts +41 -0
- package/dist/nav/timetable.js +94 -0
- package/dist/physics/buoyancy.d.ts +6 -0
- package/dist/physics/buoyancy.js +14 -0
- package/dist/physics/flowTube.d.ts +32 -0
- package/dist/physics/flowTube.js +64 -0
- package/dist/physics/forceVolume.d.ts +27 -0
- package/dist/physics/forceVolume.js +38 -0
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +13 -6
- package/dist/physics/physicsWorld.js +169 -40
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/vehicleBody.js +2 -1
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/runtime/cameraDirector.d.ts +6 -1
- package/dist/runtime/cameraDirector.js +6 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +21 -0
- package/dist/runtime/gameContext.js +184 -9
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +9 -1
- package/dist/runtime/hostPersistence.js +23 -8
- package/dist/runtime/inputSnapshot.d.ts +5 -0
- package/dist/runtime/inputSnapshot.js +5 -0
- package/dist/runtime/motionIntents.d.ts +14 -6
- package/dist/runtime/motionIntents.js +20 -2
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/visibility.d.ts +74 -0
- package/dist/runtime/visibility.js +127 -0
- package/dist/scene/colliders.d.ts +44 -0
- package/dist/scene/colliders.js +77 -0
- package/dist/scene/entityStore.d.ts +22 -5
- package/dist/scene/entityStore.js +59 -12
- package/dist/scene/objectQuery.d.ts +13 -2
- package/dist/scene/objectQuery.js +34 -2
- package/dist/scene/objectStore.d.ts +2 -0
- package/dist/scene/objectStore.js +31 -12
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +11 -16
- package/dist/scene/spatial.js +99 -38
- package/dist/sensor/recordingBuffer.d.ts +6 -0
- package/dist/sensor/recordingBuffer.js +7 -0
- package/dist/sensor/replayLoop.d.ts +39 -0
- package/dist/sensor/replayLoop.js +64 -0
- package/dist/sensor/visionCone.d.ts +31 -0
- package/dist/sensor/visionCone.js +72 -0
- package/dist/session/extraction.d.ts +2 -2
- package/dist/session/extraction.js +13 -2
- package/dist/settings/settingsModel.d.ts +97 -0
- package/dist/settings/settingsModel.js +100 -0
- package/dist/stats/statModifiers.d.ts +5 -2
- package/dist/stats/statModifiers.js +32 -14
- package/dist/store/observableKeyedStore.js +8 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/time/beatClock.d.ts +18 -0
- package/dist/time/beatClock.js +24 -0
- package/dist/time/idleProgress.d.ts +29 -0
- package/dist/time/idleProgress.js +31 -0
- package/dist/time/serverTick.d.ts +7 -1
- package/dist/time/serverTick.js +27 -4
- package/dist/time/stateSchedule.d.ts +57 -0
- package/dist/time/stateSchedule.js +121 -0
- package/dist/turn/commit.d.ts +2 -2
- package/dist/turn/commit.js +2 -2
- package/dist/ui/hudLayout.d.ts +58 -0
- package/dist/ui/hudLayout.js +207 -0
- package/dist/ui/hudScale.d.ts +44 -0
- package/dist/ui/hudScale.js +55 -0
- package/dist/visibility/assetStreaming.d.ts +66 -0
- package/dist/visibility/assetStreaming.js +201 -0
- package/dist/visibility/bounds.d.ts +66 -0
- package/dist/visibility/bounds.js +113 -0
- package/dist/visibility/camera.d.ts +21 -0
- package/dist/visibility/camera.js +3 -0
- package/dist/visibility/config.d.ts +21 -0
- package/dist/visibility/config.js +1 -0
- package/dist/visibility/diagnostics.d.ts +26 -0
- package/dist/visibility/diagnostics.js +36 -0
- package/dist/visibility/distance.d.ts +11 -0
- package/dist/visibility/distance.js +30 -0
- package/dist/visibility/frustum.d.ts +47 -0
- package/dist/visibility/frustum.js +186 -0
- package/dist/visibility/occlusion.d.ts +36 -0
- package/dist/visibility/occlusion.js +45 -0
- package/dist/visibility/settings.d.ts +75 -0
- package/dist/visibility/settings.js +49 -0
- package/dist/visibility/simulationCulling.d.ts +34 -0
- package/dist/visibility/simulationCulling.js +36 -0
- package/dist/visibility/spatialIndex.d.ts +45 -0
- package/dist/visibility/spatialIndex.js +198 -0
- package/dist/visibility/visibilitySystem.d.ts +90 -0
- package/dist/visibility/visibilitySystem.js +257 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/buildings.js +152 -0
- package/dist/world/carve.d.ts +7 -2
- package/dist/world/carve.js +17 -7
- package/dist/world/cellStates.d.ts +45 -0
- package/dist/world/cellStates.js +103 -0
- package/dist/world/environmentSummary.d.ts +10 -2
- package/dist/world/environmentSummary.js +11 -2
- package/dist/world/features.d.ts +56 -8
- package/dist/world/features.js +24 -3
- package/dist/world/gridInstances.d.ts +7 -8
- package/dist/world/gridInstances.js +145 -7
- package/dist/world/lod.d.ts +17 -0
- package/dist/world/lod.js +66 -0
- package/dist/world/mapLayers.d.ts +60 -0
- package/dist/world/mapLayers.js +44 -0
- package/dist/world/massing.d.ts +45 -0
- package/dist/world/massing.js +581 -0
- package/dist/world/minimap.d.ts +13 -1
- package/dist/world/minimap.js +21 -2
- package/dist/world/terrain.d.ts +42 -3
- package/dist/world/terrain.js +197 -16
- package/dist/world/water.d.ts +7 -0
- package/dist/world/water.js +19 -4
- package/dist/world/weather.d.ts +3 -18
- package/dist/world/weather.js +4 -17
- package/dist/world/windZones.d.ts +47 -0
- package/dist/world/windZones.js +59 -0
- package/llms.txt +2585 -1704
- package/package.json +12 -2
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { SpawnDirectorConfig } from "../ai/spawnDirector.js";
|
|
2
|
+
import type { Curve } from "../game/progression.js";
|
|
3
|
+
import type { ActionCodesMap } from "../input/actionBindings.js";
|
|
4
|
+
import type { GameContext } from "../runtime/gameContext.js";
|
|
5
|
+
export type Leveled = number | {
|
|
6
|
+
base: number;
|
|
7
|
+
perLevel: number;
|
|
8
|
+
min?: number;
|
|
9
|
+
max?: number;
|
|
10
|
+
} | {
|
|
11
|
+
table: readonly number[];
|
|
12
|
+
};
|
|
13
|
+
export declare function leveled(value: Leveled, level: number): number;
|
|
14
|
+
export interface CartridgePlayer {
|
|
15
|
+
kind: string;
|
|
16
|
+
health: number;
|
|
17
|
+
walkSpeed: number;
|
|
18
|
+
spawnAt?: readonly [number, number, number];
|
|
19
|
+
}
|
|
20
|
+
export interface CartridgeEnemy {
|
|
21
|
+
label: string;
|
|
22
|
+
health: number;
|
|
23
|
+
walkSpeed: number;
|
|
24
|
+
xp: number;
|
|
25
|
+
contact: {
|
|
26
|
+
damage: number;
|
|
27
|
+
intervalSeconds: number;
|
|
28
|
+
};
|
|
29
|
+
behavior?: "chase" | "none";
|
|
30
|
+
}
|
|
31
|
+
export interface CartridgeFlow {
|
|
32
|
+
start?: "auto" | "gate";
|
|
33
|
+
countdownSeconds?: number;
|
|
34
|
+
restart?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface WeaponCommon {
|
|
37
|
+
label: string;
|
|
38
|
+
damage: Leveled;
|
|
39
|
+
cooldownMs: Leveled;
|
|
40
|
+
maxLevel: number;
|
|
41
|
+
fxColor?: string;
|
|
42
|
+
fxEmissive?: string;
|
|
43
|
+
}
|
|
44
|
+
export type CartridgeWeapon = (WeaponCommon & {
|
|
45
|
+
kind: "projectile";
|
|
46
|
+
range: number;
|
|
47
|
+
speed: number;
|
|
48
|
+
}) | (WeaponCommon & {
|
|
49
|
+
kind: "orbit";
|
|
50
|
+
blades: Leveled;
|
|
51
|
+
radius: Leveled;
|
|
52
|
+
hitRadius: number;
|
|
53
|
+
angularSpeed: number;
|
|
54
|
+
}) | (WeaponCommon & {
|
|
55
|
+
kind: "pulse";
|
|
56
|
+
radius: Leveled;
|
|
57
|
+
durationSeconds: number;
|
|
58
|
+
}) | (WeaponCommon & {
|
|
59
|
+
kind: "custom";
|
|
60
|
+
fire(ctx: GameContext, run: CartridgeRun, args: WeaponFireArgs): void;
|
|
61
|
+
});
|
|
62
|
+
export interface WeaponFireArgs {
|
|
63
|
+
weaponId: string;
|
|
64
|
+
level: number;
|
|
65
|
+
damage: number;
|
|
66
|
+
playerId: string;
|
|
67
|
+
playerPosition: readonly [number, number, number];
|
|
68
|
+
enemyIds: readonly string[];
|
|
69
|
+
dt: number;
|
|
70
|
+
}
|
|
71
|
+
export type UpgradeEffect = {
|
|
72
|
+
kind: "weaponLevel";
|
|
73
|
+
weapon: string;
|
|
74
|
+
} | {
|
|
75
|
+
kind: "statBonus";
|
|
76
|
+
stat: string;
|
|
77
|
+
amount: number;
|
|
78
|
+
} | {
|
|
79
|
+
kind: "fieldAdd";
|
|
80
|
+
field: string;
|
|
81
|
+
amount: number;
|
|
82
|
+
} | {
|
|
83
|
+
kind: "fieldMultiply";
|
|
84
|
+
field: string;
|
|
85
|
+
factor: number;
|
|
86
|
+
} | {
|
|
87
|
+
kind: "custom";
|
|
88
|
+
apply(ctx: GameContext, run: CartridgeRun, stacks: number): void;
|
|
89
|
+
};
|
|
90
|
+
export interface CartridgeUpgrade {
|
|
91
|
+
id: string;
|
|
92
|
+
label: string;
|
|
93
|
+
weight: number;
|
|
94
|
+
maxStacks: number;
|
|
95
|
+
effect: UpgradeEffect;
|
|
96
|
+
}
|
|
97
|
+
export interface CartridgeProgression {
|
|
98
|
+
xp: Curve;
|
|
99
|
+
maxLevel: number;
|
|
100
|
+
draft: {
|
|
101
|
+
choices: number;
|
|
102
|
+
upgrades: readonly CartridgeUpgrade[];
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export interface CartridgeSpawning {
|
|
106
|
+
director: SpawnDirectorConfig;
|
|
107
|
+
placement: {
|
|
108
|
+
kind: "ring";
|
|
109
|
+
radius: number;
|
|
110
|
+
} | {
|
|
111
|
+
kind: "custom";
|
|
112
|
+
position(ctx: GameContext, run: CartridgeRun): readonly [number, number, number];
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export interface CartridgeXpGems {
|
|
116
|
+
collectRadius: number;
|
|
117
|
+
pullSpeed: number;
|
|
118
|
+
rarityThresholds: readonly (readonly [number, string])[];
|
|
119
|
+
defaultRarity: string;
|
|
120
|
+
}
|
|
121
|
+
export interface CartridgeRules {
|
|
122
|
+
win?: {
|
|
123
|
+
kind: "survive";
|
|
124
|
+
seconds: number;
|
|
125
|
+
} | {
|
|
126
|
+
kind: "custom";
|
|
127
|
+
check(ctx: GameContext, run: CartridgeRun): boolean;
|
|
128
|
+
};
|
|
129
|
+
lose?: {
|
|
130
|
+
kind: "playerDeath";
|
|
131
|
+
} | {
|
|
132
|
+
kind: "custom";
|
|
133
|
+
check(ctx: GameContext, run: CartridgeRun): boolean;
|
|
134
|
+
};
|
|
135
|
+
killLeaderboardStat?: string;
|
|
136
|
+
}
|
|
137
|
+
export interface CartridgeSpec {
|
|
138
|
+
seed?: string | number;
|
|
139
|
+
flow?: CartridgeFlow;
|
|
140
|
+
player: CartridgePlayer;
|
|
141
|
+
enemies: Record<string, CartridgeEnemy>;
|
|
142
|
+
combat: {
|
|
143
|
+
contactRadius: number;
|
|
144
|
+
};
|
|
145
|
+
spawning: CartridgeSpawning;
|
|
146
|
+
weapons: Record<string, CartridgeWeapon>;
|
|
147
|
+
progression: CartridgeProgression;
|
|
148
|
+
xpGems: CartridgeXpGems;
|
|
149
|
+
rules: CartridgeRules;
|
|
150
|
+
fields?: Record<string, number>;
|
|
151
|
+
input?: ActionCodesMap;
|
|
152
|
+
systems?: readonly ((ctx: GameContext, run: CartridgeRun, dt: number) => void)[];
|
|
153
|
+
}
|
|
154
|
+
export interface BoltFx {
|
|
155
|
+
id: number;
|
|
156
|
+
weaponId: string;
|
|
157
|
+
origin: readonly [number, number, number];
|
|
158
|
+
target: readonly [number, number, number];
|
|
159
|
+
firedAt: number;
|
|
160
|
+
travelSeconds: number;
|
|
161
|
+
}
|
|
162
|
+
export interface PulseFx {
|
|
163
|
+
id: number;
|
|
164
|
+
weaponId: string;
|
|
165
|
+
at: readonly [number, number, number];
|
|
166
|
+
firedAt: number;
|
|
167
|
+
durationSeconds: number;
|
|
168
|
+
maxRadius: number;
|
|
169
|
+
}
|
|
170
|
+
export type CartridgePhase = "start" | "countdown" | "playing" | "won" | "lost";
|
|
171
|
+
export interface CartridgeRun {
|
|
172
|
+
phase: CartridgePhase;
|
|
173
|
+
playingSeconds: number;
|
|
174
|
+
countdownRemaining: number;
|
|
175
|
+
kills: number;
|
|
176
|
+
weaponLevel(weaponId: string): number;
|
|
177
|
+
field(name: string): number;
|
|
178
|
+
pendingOffers: readonly {
|
|
179
|
+
id: string;
|
|
180
|
+
label: string;
|
|
181
|
+
}[] | null;
|
|
182
|
+
bolts: readonly BoltFx[];
|
|
183
|
+
pulses: readonly PulseFx[];
|
|
184
|
+
subscribe(listener: () => void): () => void;
|
|
185
|
+
}
|
|
186
|
+
export declare const WASD_KEYBINDS: ActionCodesMap;
|
|
187
|
+
export declare const XP_GEM_BASE_TYPE = "xp_gem";
|
|
188
|
+
export declare const HIT_EFFECT = "hit";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function leveled(value, level) {
|
|
2
|
+
if (typeof value === "number")
|
|
3
|
+
return value;
|
|
4
|
+
if ("table" in value) {
|
|
5
|
+
const index = Math.min(value.table.length - 1, Math.max(0, level - 1));
|
|
6
|
+
return value.table[index] ?? 0;
|
|
7
|
+
}
|
|
8
|
+
const raw = value.base + value.perLevel * (level - 1);
|
|
9
|
+
const floored = value.min === undefined ? raw : Math.max(value.min, raw);
|
|
10
|
+
return value.max === undefined ? floored : Math.min(value.max, floored);
|
|
11
|
+
}
|
|
12
|
+
export const WASD_KEYBINDS = {
|
|
13
|
+
moveForward: ["KeyW"],
|
|
14
|
+
moveBack: ["KeyS"],
|
|
15
|
+
moveLeft: ["KeyA"],
|
|
16
|
+
moveRight: ["KeyD"],
|
|
17
|
+
};
|
|
18
|
+
export const XP_GEM_BASE_TYPE = "xp_gem";
|
|
19
|
+
export const HIT_EFFECT = "hit";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { leveled } from "./spec.js";
|
|
2
|
+
export function validateCartridge(spec) {
|
|
3
|
+
const problems = [];
|
|
4
|
+
const enemyIds = new Set(Object.keys(spec.enemies));
|
|
5
|
+
const weaponIds = new Set(Object.keys(spec.weapons));
|
|
6
|
+
if (spec.player.health <= 0)
|
|
7
|
+
problems.push(`player "${spec.player.kind}": health must be positive`);
|
|
8
|
+
if (spec.player.walkSpeed <= 0)
|
|
9
|
+
problems.push(`player "${spec.player.kind}": walkSpeed must be positive`);
|
|
10
|
+
if (enemyIds.has(spec.player.kind))
|
|
11
|
+
problems.push(`player kind "${spec.player.kind}" collides with an enemy id`);
|
|
12
|
+
if (spec.flow?.countdownSeconds !== undefined && spec.flow.countdownSeconds < 0) {
|
|
13
|
+
problems.push("flow.countdownSeconds must not be negative");
|
|
14
|
+
}
|
|
15
|
+
for (const [id, enemy] of Object.entries(spec.enemies)) {
|
|
16
|
+
if (enemy.health <= 0)
|
|
17
|
+
problems.push(`enemy "${id}": health must be positive`);
|
|
18
|
+
if (enemy.walkSpeed <= 0)
|
|
19
|
+
problems.push(`enemy "${id}": walkSpeed must be positive`);
|
|
20
|
+
if (enemy.xp <= 0)
|
|
21
|
+
problems.push(`enemy "${id}": xp must be positive`);
|
|
22
|
+
if (enemy.contact.damage <= 0)
|
|
23
|
+
problems.push(`enemy "${id}": contact.damage must be positive`);
|
|
24
|
+
if (enemy.contact.intervalSeconds <= 0)
|
|
25
|
+
problems.push(`enemy "${id}": contact.intervalSeconds must be positive`);
|
|
26
|
+
}
|
|
27
|
+
for (const wave of spec.spawning.director.waves) {
|
|
28
|
+
for (const entry of wave.entries) {
|
|
29
|
+
if (!enemyIds.has(entry.id))
|
|
30
|
+
problems.push(`spawning wave references unknown enemy "${entry.id}"`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const [id, weapon] of Object.entries(spec.weapons)) {
|
|
34
|
+
for (let level = 1; level <= weapon.maxLevel; level += 1) {
|
|
35
|
+
if (leveled(weapon.damage, level) <= 0)
|
|
36
|
+
problems.push(`weapon "${id}": damage at level ${level} must be positive`);
|
|
37
|
+
if (leveled(weapon.cooldownMs, level) <= 0)
|
|
38
|
+
problems.push(`weapon "${id}": cooldownMs at level ${level} must be positive`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const seenUpgrades = new Set();
|
|
42
|
+
for (const upgrade of spec.progression.draft.upgrades) {
|
|
43
|
+
if (seenUpgrades.has(upgrade.id))
|
|
44
|
+
problems.push(`upgrade "${upgrade.id}": duplicate id`);
|
|
45
|
+
seenUpgrades.add(upgrade.id);
|
|
46
|
+
if (upgrade.maxStacks <= 0)
|
|
47
|
+
problems.push(`upgrade "${upgrade.id}": maxStacks must be positive`);
|
|
48
|
+
const effect = upgrade.effect;
|
|
49
|
+
if (effect.kind === "weaponLevel") {
|
|
50
|
+
if (!weaponIds.has(effect.weapon)) {
|
|
51
|
+
problems.push(`upgrade "${upgrade.id}": references unknown weapon "${effect.weapon}"`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const weapon = spec.weapons[effect.weapon];
|
|
55
|
+
if (1 + upgrade.maxStacks > weapon.maxLevel) {
|
|
56
|
+
problems.push(`upgrade "${upgrade.id}": maxStacks ${upgrade.maxStacks} exceeds weapon "${effect.weapon}" maxLevel ${weapon.maxLevel}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if ((effect.kind === "fieldAdd" || effect.kind === "fieldMultiply") && spec.fields?.[effect.field] === undefined) {
|
|
61
|
+
problems.push(`upgrade "${upgrade.id}": references undeclared field "${effect.field}"`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (spec.progression.draft.choices <= 0)
|
|
65
|
+
problems.push("progression.draft.choices must be positive");
|
|
66
|
+
if (spec.progression.maxLevel <= 1)
|
|
67
|
+
problems.push("progression.maxLevel must exceed 1");
|
|
68
|
+
const thresholds = spec.xpGems.rarityThresholds;
|
|
69
|
+
for (let i = 1; i < thresholds.length; i += 1) {
|
|
70
|
+
if (thresholds[i][0] >= thresholds[i - 1][0]) {
|
|
71
|
+
problems.push("xpGems.rarityThresholds must be sorted by descending value");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (spec.rules.win?.kind === "survive" && spec.rules.win.seconds <= 0) {
|
|
75
|
+
problems.push("rules.win.seconds must be positive");
|
|
76
|
+
}
|
|
77
|
+
return problems;
|
|
78
|
+
}
|
|
@@ -7,6 +7,16 @@ export interface AbilitySlotConfig {
|
|
|
7
7
|
resourceCost?: number;
|
|
8
8
|
castType?: AbilityCastType;
|
|
9
9
|
flashMs?: number;
|
|
10
|
+
/** Shared cooldown groups this slot participates in — ids declared in `AbilityKitOptions.groups`. */
|
|
11
|
+
groups?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
/** A cooldown shared across every member slot: casting any member starts it, and no member can cast while it runs. One group that every slot joins is an MMO global cooldown. */
|
|
14
|
+
export interface AbilityCooldownGroup {
|
|
15
|
+
id: string;
|
|
16
|
+
cooldownMs: number;
|
|
17
|
+
}
|
|
18
|
+
export interface AbilityKitOptions {
|
|
19
|
+
groups?: readonly AbilityCooldownGroup[];
|
|
10
20
|
}
|
|
11
21
|
export interface AbilitySlotSnapshot {
|
|
12
22
|
id: string;
|
|
@@ -15,12 +25,15 @@ export interface AbilitySlotSnapshot {
|
|
|
15
25
|
resourceCost: number;
|
|
16
26
|
charges: number;
|
|
17
27
|
chargesMax: number;
|
|
28
|
+
/** Max of the slot's own recharge and any blocking group cooldown. */
|
|
18
29
|
cooldownRemainingMs: number;
|
|
19
30
|
cooldownFraction: number;
|
|
31
|
+
/** Remaining ms on the slowest of the slot's shared cooldown groups; 0 when unblocked. */
|
|
32
|
+
groupRemainingMs: number;
|
|
20
33
|
justCast: boolean;
|
|
21
34
|
ready: boolean;
|
|
22
35
|
}
|
|
23
|
-
export type AbilityCastReason = "unknown-slot" | "cooldown" | "no-resource";
|
|
36
|
+
export type AbilityCastReason = "unknown-slot" | "cooldown" | "group-cooldown" | "no-resource";
|
|
24
37
|
export interface AbilitySlotRetune {
|
|
25
38
|
cooldownMs?: number;
|
|
26
39
|
resourceCost?: number;
|
|
@@ -41,7 +54,9 @@ export interface AbilityKit {
|
|
|
41
54
|
canCast(slotId: string, resourceAvailable?: number): AbilityCastResult;
|
|
42
55
|
cast(slotId: string, resourceAvailable?: number): AbilityCastResult;
|
|
43
56
|
tick(dtSeconds: number): void;
|
|
57
|
+
/** Remaining ms on a shared cooldown group; 0 when ready, null for an unknown group. */
|
|
58
|
+
groupRemaining(groupId: string): number | null;
|
|
44
59
|
reset(slotId?: string): void;
|
|
45
60
|
retuneSlot(slotId: string, patch: AbilitySlotRetune): boolean;
|
|
46
61
|
}
|
|
47
|
-
export declare function createAbilityKit(configs: readonly AbilitySlotConfig[]): AbilityKit;
|
|
62
|
+
export declare function createAbilityKit(configs: readonly AbilitySlotConfig[], options?: AbilityKitOptions): AbilityKit;
|
|
@@ -7,21 +7,24 @@ function normalizeConfig(config) {
|
|
|
7
7
|
resourceCost: Math.max(0, config.resourceCost ?? 0),
|
|
8
8
|
castType: config.castType ?? "instant",
|
|
9
9
|
flashMs: Math.max(0, config.flashMs ?? DEFAULT_FLASH_MS),
|
|
10
|
+
groups: config.groups ?? [],
|
|
10
11
|
};
|
|
11
12
|
}
|
|
12
|
-
function deriveState(runtime, resourceAvailable) {
|
|
13
|
+
function deriveState(runtime, resourceAvailable, group) {
|
|
13
14
|
if (runtime.flashRemainingMs > 0)
|
|
14
15
|
return "just-cast";
|
|
15
16
|
if (runtime.charges <= 0)
|
|
16
17
|
return "cooldown";
|
|
18
|
+
if (group.remainingMs > 0)
|
|
19
|
+
return "cooldown";
|
|
17
20
|
if (resourceAvailable < runtime.config.resourceCost)
|
|
18
21
|
return "no-resource";
|
|
19
22
|
return "ready";
|
|
20
23
|
}
|
|
21
|
-
function snapshotOf(runtime, resourceAvailable) {
|
|
24
|
+
function snapshotOf(runtime, resourceAvailable, group) {
|
|
22
25
|
const { config } = runtime;
|
|
23
|
-
const state = deriveState(runtime, resourceAvailable);
|
|
24
|
-
const
|
|
26
|
+
const state = deriveState(runtime, resourceAvailable, group);
|
|
27
|
+
const ownFraction = config.cooldownMs <= 0 ? 0 : Math.max(0, Math.min(1, runtime.rechargeRemainingMs / config.cooldownMs));
|
|
25
28
|
return {
|
|
26
29
|
id: config.id,
|
|
27
30
|
state,
|
|
@@ -29,19 +32,33 @@ function snapshotOf(runtime, resourceAvailable) {
|
|
|
29
32
|
resourceCost: config.resourceCost,
|
|
30
33
|
charges: runtime.charges,
|
|
31
34
|
chargesMax: config.chargesMax,
|
|
32
|
-
cooldownRemainingMs: runtime.rechargeRemainingMs,
|
|
33
|
-
cooldownFraction,
|
|
35
|
+
cooldownRemainingMs: Math.max(runtime.rechargeRemainingMs, group.remainingMs),
|
|
36
|
+
cooldownFraction: Math.max(ownFraction, group.fraction),
|
|
37
|
+
groupRemainingMs: group.remainingMs,
|
|
34
38
|
justCast: runtime.flashRemainingMs > 0,
|
|
35
|
-
ready: runtime.charges > 0 && resourceAvailable >= config.resourceCost,
|
|
39
|
+
ready: runtime.charges > 0 && group.remainingMs <= 0 && resourceAvailable >= config.resourceCost,
|
|
36
40
|
};
|
|
37
41
|
}
|
|
38
|
-
export function createAbilityKit(configs) {
|
|
42
|
+
export function createAbilityKit(configs, options = {}) {
|
|
43
|
+
const groupCooldowns = new Map();
|
|
44
|
+
const groupRemaining = new Map();
|
|
45
|
+
for (const group of options.groups ?? []) {
|
|
46
|
+
if (groupCooldowns.has(group.id))
|
|
47
|
+
throw new Error(`duplicate cooldown group: ${group.id}`);
|
|
48
|
+
groupCooldowns.set(group.id, Math.max(0, group.cooldownMs));
|
|
49
|
+
groupRemaining.set(group.id, 0);
|
|
50
|
+
}
|
|
39
51
|
const runtimes = new Map();
|
|
40
52
|
const order = [];
|
|
41
53
|
for (const config of configs) {
|
|
42
54
|
const normalized = normalizeConfig(config);
|
|
43
55
|
if (runtimes.has(normalized.id))
|
|
44
56
|
throw new Error(`duplicate ability slot: ${normalized.id}`);
|
|
57
|
+
for (const groupId of normalized.groups) {
|
|
58
|
+
if (!groupCooldowns.has(groupId)) {
|
|
59
|
+
throw new Error(`ability slot "${normalized.id}" references unknown cooldown group "${groupId}"`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
45
62
|
runtimes.set(normalized.id, {
|
|
46
63
|
config: normalized,
|
|
47
64
|
charges: normalized.chargesMax,
|
|
@@ -50,9 +67,22 @@ export function createAbilityKit(configs) {
|
|
|
50
67
|
});
|
|
51
68
|
order.push(normalized.id);
|
|
52
69
|
}
|
|
70
|
+
function groupBlockOf(runtime) {
|
|
71
|
+
let remainingMs = 0;
|
|
72
|
+
let fraction = 0;
|
|
73
|
+
for (const groupId of runtime.config.groups) {
|
|
74
|
+
const remaining = groupRemaining.get(groupId) ?? 0;
|
|
75
|
+
if (remaining <= remainingMs)
|
|
76
|
+
continue;
|
|
77
|
+
remainingMs = remaining;
|
|
78
|
+
const cooldownMs = groupCooldowns.get(groupId) ?? 0;
|
|
79
|
+
fraction = cooldownMs <= 0 ? 0 : Math.max(0, Math.min(1, remaining / cooldownMs));
|
|
80
|
+
}
|
|
81
|
+
return { remainingMs, fraction };
|
|
82
|
+
}
|
|
53
83
|
function fail(slotId, reason, resourceAvailable) {
|
|
54
84
|
const runtime = runtimes.get(slotId);
|
|
55
|
-
return { ok: false, reason, slot: runtime ? snapshotOf(runtime, resourceAvailable) : null };
|
|
85
|
+
return { ok: false, reason, slot: runtime ? snapshotOf(runtime, resourceAvailable, groupBlockOf(runtime)) : null };
|
|
56
86
|
}
|
|
57
87
|
function evaluate(slotId, resourceAvailable) {
|
|
58
88
|
const runtime = runtimes.get(slotId);
|
|
@@ -60,9 +90,11 @@ export function createAbilityKit(configs) {
|
|
|
60
90
|
return fail(slotId, "unknown-slot", resourceAvailable);
|
|
61
91
|
if (runtime.charges <= 0)
|
|
62
92
|
return fail(slotId, "cooldown", resourceAvailable);
|
|
93
|
+
if (groupBlockOf(runtime).remainingMs > 0)
|
|
94
|
+
return fail(slotId, "group-cooldown", resourceAvailable);
|
|
63
95
|
if (resourceAvailable < runtime.config.resourceCost)
|
|
64
96
|
return fail(slotId, "no-resource", resourceAvailable);
|
|
65
|
-
return { ok: true, slot: snapshotOf(runtime, resourceAvailable) };
|
|
97
|
+
return { ok: true, slot: snapshotOf(runtime, resourceAvailable, groupBlockOf(runtime)) };
|
|
66
98
|
}
|
|
67
99
|
return {
|
|
68
100
|
slots() {
|
|
@@ -73,10 +105,13 @@ export function createAbilityKit(configs) {
|
|
|
73
105
|
},
|
|
74
106
|
state(slotId, resourceAvailable = Number.POSITIVE_INFINITY) {
|
|
75
107
|
const runtime = runtimes.get(slotId);
|
|
76
|
-
return runtime === undefined ? null : snapshotOf(runtime, resourceAvailable);
|
|
108
|
+
return runtime === undefined ? null : snapshotOf(runtime, resourceAvailable, groupBlockOf(runtime));
|
|
77
109
|
},
|
|
78
110
|
snapshot(resourceAvailable = Number.POSITIVE_INFINITY) {
|
|
79
|
-
return order.map((slotId) =>
|
|
111
|
+
return order.map((slotId) => {
|
|
112
|
+
const runtime = runtimes.get(slotId);
|
|
113
|
+
return snapshotOf(runtime, resourceAvailable, groupBlockOf(runtime));
|
|
114
|
+
});
|
|
80
115
|
},
|
|
81
116
|
canCast(slotId, resourceAvailable = Number.POSITIVE_INFINITY) {
|
|
82
117
|
return evaluate(slotId, resourceAvailable);
|
|
@@ -90,12 +125,19 @@ export function createAbilityKit(configs) {
|
|
|
90
125
|
if (runtime.rechargeRemainingMs <= 0)
|
|
91
126
|
runtime.rechargeRemainingMs = runtime.config.cooldownMs;
|
|
92
127
|
runtime.flashRemainingMs = runtime.config.flashMs;
|
|
93
|
-
|
|
128
|
+
for (const groupId of runtime.config.groups) {
|
|
129
|
+
groupRemaining.set(groupId, groupCooldowns.get(groupId) ?? 0);
|
|
130
|
+
}
|
|
131
|
+
return { ok: true, slot: snapshotOf(runtime, resourceAvailable, groupBlockOf(runtime)) };
|
|
94
132
|
},
|
|
95
133
|
tick(dtSeconds) {
|
|
96
134
|
if (dtSeconds <= 0)
|
|
97
135
|
return;
|
|
98
136
|
const dtMs = dtSeconds * 1000;
|
|
137
|
+
for (const [groupId, remaining] of groupRemaining) {
|
|
138
|
+
if (remaining > 0)
|
|
139
|
+
groupRemaining.set(groupId, Math.max(0, remaining - dtMs));
|
|
140
|
+
}
|
|
99
141
|
for (const runtime of runtimes.values()) {
|
|
100
142
|
if (runtime.flashRemainingMs > 0)
|
|
101
143
|
runtime.flashRemainingMs = Math.max(0, runtime.flashRemainingMs - dtMs);
|
|
@@ -117,6 +159,9 @@ export function createAbilityKit(configs) {
|
|
|
117
159
|
runtime.rechargeRemainingMs = runtime.charges >= runtime.config.chargesMax ? 0 : remaining;
|
|
118
160
|
}
|
|
119
161
|
},
|
|
162
|
+
groupRemaining(groupId) {
|
|
163
|
+
return groupCooldowns.has(groupId) ? groupRemaining.get(groupId) ?? 0 : null;
|
|
164
|
+
},
|
|
120
165
|
reset(slotId) {
|
|
121
166
|
const targets = slotId === undefined ? runtimes.values() : [runtimes.get(slotId)].filter((r) => r !== undefined);
|
|
122
167
|
for (const runtime of targets) {
|
|
@@ -124,6 +169,10 @@ export function createAbilityKit(configs) {
|
|
|
124
169
|
runtime.rechargeRemainingMs = 0;
|
|
125
170
|
runtime.flashRemainingMs = 0;
|
|
126
171
|
}
|
|
172
|
+
if (slotId === undefined) {
|
|
173
|
+
for (const groupId of groupRemaining.keys())
|
|
174
|
+
groupRemaining.set(groupId, 0);
|
|
175
|
+
}
|
|
127
176
|
},
|
|
128
177
|
retuneSlot(slotId, patch) {
|
|
129
178
|
const runtime = runtimes.get(slotId);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface CastConfig {
|
|
2
|
+
abilityId: string;
|
|
3
|
+
castTimeMs: number;
|
|
4
|
+
/** Total movement distance tolerated before the cast auto-interrupts; default `0.04`. `Infinity` allows casting on the move. */
|
|
5
|
+
moveTolerance?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface CastBarSnapshot {
|
|
8
|
+
abilityId: string;
|
|
9
|
+
castTimeMs: number;
|
|
10
|
+
elapsedMs: number;
|
|
11
|
+
remainingMs: number;
|
|
12
|
+
/** Fill fraction in `[0, 1]` for the cast bar. */
|
|
13
|
+
fraction: number;
|
|
14
|
+
}
|
|
15
|
+
export type CastInterruptReason = "moved" | "cancelled" | "replaced";
|
|
16
|
+
export type CastEvent = {
|
|
17
|
+
kind: "completed";
|
|
18
|
+
abilityId: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: "interrupted";
|
|
21
|
+
abilityId: string;
|
|
22
|
+
reason: CastInterruptReason;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Per-entity cast-time state machine — begin, tick with game-time `dt` plus how far the caster
|
|
26
|
+
* moved, and act on the returned event. The runner owns timing and move-interruption only; the
|
|
27
|
+
* caller spends resources, starts cooldowns, and executes the ability when `completed` fires
|
|
28
|
+
* (compose with `abilityKit` — check readiness before `begin`, `cast` on completion).
|
|
29
|
+
*/
|
|
30
|
+
export interface CastRunner {
|
|
31
|
+
/** Start a cast; `false` while one is already in flight (interrupt or let it finish first). */
|
|
32
|
+
begin(config: CastConfig): boolean;
|
|
33
|
+
casting(): boolean;
|
|
34
|
+
state(): CastBarSnapshot | null;
|
|
35
|
+
/** Interrupt the in-flight cast now; returns the interruption event, or `null` when idle. */
|
|
36
|
+
interrupt(reason?: CastInterruptReason): CastEvent | null;
|
|
37
|
+
/** Advance by `dtSeconds`; `movedDistance` accumulates toward the config's `moveTolerance`. */
|
|
38
|
+
tick(dtSeconds: number, movedDistance?: number): CastEvent | null;
|
|
39
|
+
}
|
|
40
|
+
export declare function createCastRunner(): CastRunner;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const DEFAULT_MOVE_TOLERANCE = 0.04;
|
|
2
|
+
export function createCastRunner() {
|
|
3
|
+
let active = null;
|
|
4
|
+
function snapshot() {
|
|
5
|
+
if (active === null)
|
|
6
|
+
return null;
|
|
7
|
+
const fraction = active.castTimeMs <= 0 ? 1 : Math.min(1, active.elapsedMs / active.castTimeMs);
|
|
8
|
+
return {
|
|
9
|
+
abilityId: active.abilityId,
|
|
10
|
+
castTimeMs: active.castTimeMs,
|
|
11
|
+
elapsedMs: active.elapsedMs,
|
|
12
|
+
remainingMs: Math.max(0, active.castTimeMs - active.elapsedMs),
|
|
13
|
+
fraction,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
begin(config) {
|
|
18
|
+
if (active !== null)
|
|
19
|
+
return false;
|
|
20
|
+
active = {
|
|
21
|
+
abilityId: config.abilityId,
|
|
22
|
+
castTimeMs: Math.max(0, config.castTimeMs),
|
|
23
|
+
moveTolerance: config.moveTolerance ?? DEFAULT_MOVE_TOLERANCE,
|
|
24
|
+
elapsedMs: 0,
|
|
25
|
+
movedTotal: 0,
|
|
26
|
+
};
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
casting: () => active !== null,
|
|
30
|
+
state: snapshot,
|
|
31
|
+
interrupt(reason = "cancelled") {
|
|
32
|
+
if (active === null)
|
|
33
|
+
return null;
|
|
34
|
+
const abilityId = active.abilityId;
|
|
35
|
+
active = null;
|
|
36
|
+
return { kind: "interrupted", abilityId, reason };
|
|
37
|
+
},
|
|
38
|
+
tick(dtSeconds, movedDistance = 0) {
|
|
39
|
+
if (active === null)
|
|
40
|
+
return null;
|
|
41
|
+
active.movedTotal += Math.max(0, movedDistance);
|
|
42
|
+
if (active.movedTotal > active.moveTolerance) {
|
|
43
|
+
const abilityId = active.abilityId;
|
|
44
|
+
active = null;
|
|
45
|
+
return { kind: "interrupted", abilityId, reason: "moved" };
|
|
46
|
+
}
|
|
47
|
+
active.elapsedMs += Math.max(0, dtSeconds) * 1000;
|
|
48
|
+
if (active.elapsedMs >= active.castTimeMs) {
|
|
49
|
+
const abilityId = active.abilityId;
|
|
50
|
+
active = null;
|
|
51
|
+
return { kind: "completed", abilityId };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ComboPointsConfig {
|
|
2
|
+
max: number;
|
|
3
|
+
expireAfterSeconds?: number;
|
|
4
|
+
}
|
|
5
|
+
export interface ComboPoints {
|
|
6
|
+
points(): number;
|
|
7
|
+
gain(amount?: number): number;
|
|
8
|
+
spend(amount: number): boolean;
|
|
9
|
+
spendAll(): number;
|
|
10
|
+
tick(dtSeconds: number): void;
|
|
11
|
+
expiresIn(): number | null;
|
|
12
|
+
clear(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createComboPoints(config: ComboPointsConfig): ComboPoints;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function createComboPoints(config) {
|
|
2
|
+
const max = config.max;
|
|
3
|
+
const expireAfterSeconds = config.expireAfterSeconds;
|
|
4
|
+
let current = 0;
|
|
5
|
+
let remaining = null;
|
|
6
|
+
return {
|
|
7
|
+
points() {
|
|
8
|
+
return current;
|
|
9
|
+
},
|
|
10
|
+
gain(amount = 1) {
|
|
11
|
+
if (amount <= 0)
|
|
12
|
+
return current;
|
|
13
|
+
current = Math.min(max, current + amount);
|
|
14
|
+
remaining = expireAfterSeconds !== undefined ? expireAfterSeconds : null;
|
|
15
|
+
return current;
|
|
16
|
+
},
|
|
17
|
+
spend(amount) {
|
|
18
|
+
if (amount <= 0)
|
|
19
|
+
return true;
|
|
20
|
+
if (amount > current)
|
|
21
|
+
return false;
|
|
22
|
+
current -= amount;
|
|
23
|
+
if (current <= 0)
|
|
24
|
+
remaining = null;
|
|
25
|
+
return true;
|
|
26
|
+
},
|
|
27
|
+
spendAll() {
|
|
28
|
+
const spent = current;
|
|
29
|
+
current = 0;
|
|
30
|
+
remaining = null;
|
|
31
|
+
return spent;
|
|
32
|
+
},
|
|
33
|
+
tick(dtSeconds) {
|
|
34
|
+
if (dtSeconds <= 0 || remaining === null)
|
|
35
|
+
return;
|
|
36
|
+
remaining -= dtSeconds;
|
|
37
|
+
if (remaining <= 0) {
|
|
38
|
+
current = 0;
|
|
39
|
+
remaining = null;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
expiresIn() {
|
|
43
|
+
if (current <= 0 || remaining === null)
|
|
44
|
+
return null;
|
|
45
|
+
return remaining;
|
|
46
|
+
},
|
|
47
|
+
clear() {
|
|
48
|
+
current = 0;
|
|
49
|
+
remaining = null;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -27,7 +27,7 @@ export declare function resolveDefense(input: ResolveDefenseInput): DefenseResol
|
|
|
27
27
|
export interface DefensiveWindow {
|
|
28
28
|
open(nowMs: number): void;
|
|
29
29
|
close(): void;
|
|
30
|
-
isOpen(): boolean;
|
|
30
|
+
isOpen(nowMs: number): boolean;
|
|
31
31
|
evaluate(nowMs: number, attack: AttackMeta): DefenseResolution;
|
|
32
32
|
isInvulnerable(nowMs: number): boolean;
|
|
33
33
|
}
|