@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,108 @@
|
|
|
1
|
+
import { createDeliveryQueue, insureLost, partitionOnDeath, resolveConsolation, } from "../inventory/storageTier.js";
|
|
2
|
+
import { createContestedChannel, } from "./contestedChannel.js";
|
|
3
|
+
function mergeAll(containers) {
|
|
4
|
+
const totals = new Map();
|
|
5
|
+
for (const container of containers) {
|
|
6
|
+
for (const stack of container.items) {
|
|
7
|
+
totals.set(stack.itemId, (totals.get(stack.itemId) ?? 0) + stack.count);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const [itemId, count] of totals)
|
|
12
|
+
if (count > 0)
|
|
13
|
+
out.push({ itemId, count });
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
export function createRaidSession(config) {
|
|
17
|
+
const points = new Map();
|
|
18
|
+
for (const point of config.extracts)
|
|
19
|
+
points.set(point.id, point);
|
|
20
|
+
const attempts = new Map();
|
|
21
|
+
const statuses = new Map();
|
|
22
|
+
const queue = createDeliveryQueue();
|
|
23
|
+
function statusOf(userId) {
|
|
24
|
+
return statuses.get(userId) ?? "in-raid";
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
beginExtract(userId, extractId, team) {
|
|
28
|
+
const point = points.get(extractId);
|
|
29
|
+
if (point === undefined)
|
|
30
|
+
return null;
|
|
31
|
+
if (statusOf(userId) === "extracted" || statusOf(userId) === "dead")
|
|
32
|
+
return null;
|
|
33
|
+
const channel = createContestedChannel({
|
|
34
|
+
duration: point.holdSeconds,
|
|
35
|
+
interruptOnDamage: point.interruptOnDamage ?? true,
|
|
36
|
+
favorability: point.favorability,
|
|
37
|
+
});
|
|
38
|
+
const event = channel.start(team ?? userId);
|
|
39
|
+
attempts.set(userId, { extractId, channel });
|
|
40
|
+
statuses.set(userId, "extracting");
|
|
41
|
+
return event;
|
|
42
|
+
},
|
|
43
|
+
tickExtract(userId, dt, occupants) {
|
|
44
|
+
const attempt = attempts.get(userId);
|
|
45
|
+
if (attempt === undefined)
|
|
46
|
+
return [];
|
|
47
|
+
const events = attempt.channel.tick(dt, occupants);
|
|
48
|
+
if (attempt.channel.phase() === "complete")
|
|
49
|
+
statuses.set(userId, "extracted");
|
|
50
|
+
return events;
|
|
51
|
+
},
|
|
52
|
+
damage(userId, reason) {
|
|
53
|
+
const attempt = attempts.get(userId);
|
|
54
|
+
if (attempt === undefined)
|
|
55
|
+
return null;
|
|
56
|
+
const event = attempt.channel.damage(reason);
|
|
57
|
+
if (attempt.channel.phase() === "interrupted")
|
|
58
|
+
statuses.set(userId, "in-raid");
|
|
59
|
+
return event;
|
|
60
|
+
},
|
|
61
|
+
cancelExtract(userId) {
|
|
62
|
+
attempts.delete(userId);
|
|
63
|
+
if (statusOf(userId) === "extracting")
|
|
64
|
+
statuses.set(userId, "in-raid");
|
|
65
|
+
},
|
|
66
|
+
resolveExtraction(userId, containers) {
|
|
67
|
+
const attempt = attempts.get(userId);
|
|
68
|
+
const extractId = attempt?.extractId ?? "";
|
|
69
|
+
attempts.delete(userId);
|
|
70
|
+
statuses.set(userId, "extracted");
|
|
71
|
+
return { userId, extractId, banked: mergeAll(containers) };
|
|
72
|
+
},
|
|
73
|
+
resolveDeath(userId, containers, now, rng) {
|
|
74
|
+
attempts.delete(userId);
|
|
75
|
+
statuses.set(userId, "dead");
|
|
76
|
+
const partition = partitionOnDeath(containers);
|
|
77
|
+
let scheduled = null;
|
|
78
|
+
if (config.insurance !== undefined) {
|
|
79
|
+
const insured = insureLost(partition.lost, config.insurance, userId, now, rng);
|
|
80
|
+
if (insured !== null)
|
|
81
|
+
scheduled = queue.schedule(insured);
|
|
82
|
+
}
|
|
83
|
+
const consolation = config.consolation !== undefined ? resolveConsolation(config.consolation, partition) : null;
|
|
84
|
+
return {
|
|
85
|
+
userId,
|
|
86
|
+
kept: partition.kept,
|
|
87
|
+
lost: partition.lost,
|
|
88
|
+
scheduled,
|
|
89
|
+
consolationLoadoutId: consolation?.loadoutId ?? null,
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
claimDeliveries: (now) => queue.claimDue(now),
|
|
93
|
+
pendingDeliveries: (userId) => queue.pending(userId),
|
|
94
|
+
deliveries: () => queue,
|
|
95
|
+
extractPoint: (extractId) => points.get(extractId),
|
|
96
|
+
status: statusOf,
|
|
97
|
+
playerSnapshot: (userId) => {
|
|
98
|
+
const attempt = attempts.get(userId);
|
|
99
|
+
const channel = attempt?.channel;
|
|
100
|
+
return {
|
|
101
|
+
status: statusOf(userId),
|
|
102
|
+
extractId: attempt?.extractId ?? null,
|
|
103
|
+
progress: channel?.progress() ?? 0,
|
|
104
|
+
remaining: channel?.remaining() ?? 0,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type RingPoint = [number, number];
|
|
2
|
+
export interface RingPhase {
|
|
3
|
+
startTime: number;
|
|
4
|
+
shrinkDuration: number;
|
|
5
|
+
fromRadius: number;
|
|
6
|
+
toRadius: number;
|
|
7
|
+
damagePerSecond: number;
|
|
8
|
+
center?: RingPoint;
|
|
9
|
+
}
|
|
10
|
+
export interface RingConfig {
|
|
11
|
+
center: RingPoint;
|
|
12
|
+
phases: readonly RingPhase[];
|
|
13
|
+
}
|
|
14
|
+
export interface RingSample {
|
|
15
|
+
center: RingPoint;
|
|
16
|
+
radius: number;
|
|
17
|
+
damagePerSecond: number;
|
|
18
|
+
phase: number;
|
|
19
|
+
shrinking: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface RingHit {
|
|
22
|
+
id: string;
|
|
23
|
+
damage: number;
|
|
24
|
+
distanceOutside: number;
|
|
25
|
+
}
|
|
26
|
+
export interface Ring {
|
|
27
|
+
at(time: number): RingSample;
|
|
28
|
+
isOutside(time: number, position: RingPoint): boolean;
|
|
29
|
+
distanceOutside(time: number, position: RingPoint): number;
|
|
30
|
+
damageOutside(time: number, dt: number, positions: Iterable<{
|
|
31
|
+
id: string;
|
|
32
|
+
position: RingPoint;
|
|
33
|
+
}>): RingHit[];
|
|
34
|
+
}
|
|
35
|
+
export declare function ringSampleAt(config: RingConfig, time: number): RingSample;
|
|
36
|
+
export declare function createRing(config: RingConfig): Ring;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
function lerp(from, to, t) {
|
|
2
|
+
return from + (to - from) * t;
|
|
3
|
+
}
|
|
4
|
+
function lerpPoint(from, to, t) {
|
|
5
|
+
return [lerp(from[0], to[0], t), lerp(from[1], to[1], t)];
|
|
6
|
+
}
|
|
7
|
+
function distance(a, b) {
|
|
8
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
9
|
+
}
|
|
10
|
+
export function ringSampleAt(config, time) {
|
|
11
|
+
const phases = config.phases;
|
|
12
|
+
if (phases.length === 0) {
|
|
13
|
+
return { center: config.center, radius: 0, damagePerSecond: 0, phase: -1, shrinking: false };
|
|
14
|
+
}
|
|
15
|
+
const first = phases[0];
|
|
16
|
+
if (time < first.startTime) {
|
|
17
|
+
return {
|
|
18
|
+
center: config.center,
|
|
19
|
+
radius: first.fromRadius,
|
|
20
|
+
damagePerSecond: 0,
|
|
21
|
+
phase: -1,
|
|
22
|
+
shrinking: false,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
let index = 0;
|
|
26
|
+
for (let i = 0; i < phases.length; i += 1) {
|
|
27
|
+
if (time >= phases[i].startTime)
|
|
28
|
+
index = i;
|
|
29
|
+
else
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
const phase = phases[index];
|
|
33
|
+
const prevCenter = index === 0 ? config.center : centerAfter(config, index - 1);
|
|
34
|
+
const target = phase.center ?? prevCenter;
|
|
35
|
+
const elapsed = time - phase.startTime;
|
|
36
|
+
if (elapsed < phase.shrinkDuration && phase.shrinkDuration > 0) {
|
|
37
|
+
const t = elapsed / phase.shrinkDuration;
|
|
38
|
+
return {
|
|
39
|
+
center: lerpPoint(prevCenter, target, t),
|
|
40
|
+
radius: lerp(phase.fromRadius, phase.toRadius, t),
|
|
41
|
+
damagePerSecond: phase.damagePerSecond,
|
|
42
|
+
phase: index,
|
|
43
|
+
shrinking: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
center: target,
|
|
48
|
+
radius: phase.toRadius,
|
|
49
|
+
damagePerSecond: phase.damagePerSecond,
|
|
50
|
+
phase: index,
|
|
51
|
+
shrinking: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function centerAfter(config, index) {
|
|
55
|
+
let center = config.center;
|
|
56
|
+
for (let i = 0; i <= index; i += 1) {
|
|
57
|
+
const phase = config.phases[i];
|
|
58
|
+
if (phase.center !== undefined)
|
|
59
|
+
center = phase.center;
|
|
60
|
+
}
|
|
61
|
+
return center;
|
|
62
|
+
}
|
|
63
|
+
export function createRing(config) {
|
|
64
|
+
return {
|
|
65
|
+
at: (time) => ringSampleAt(config, time),
|
|
66
|
+
isOutside: (time, position) => {
|
|
67
|
+
const sample = ringSampleAt(config, time);
|
|
68
|
+
return distance(position, sample.center) > sample.radius;
|
|
69
|
+
},
|
|
70
|
+
distanceOutside: (time, position) => {
|
|
71
|
+
const sample = ringSampleAt(config, time);
|
|
72
|
+
return Math.max(0, distance(position, sample.center) - sample.radius);
|
|
73
|
+
},
|
|
74
|
+
damageOutside: (time, dt, positions) => {
|
|
75
|
+
const sample = ringSampleAt(config, time);
|
|
76
|
+
const hits = [];
|
|
77
|
+
if (dt <= 0 || sample.damagePerSecond <= 0)
|
|
78
|
+
return hits;
|
|
79
|
+
for (const { id, position } of positions) {
|
|
80
|
+
const distanceOutside = distance(position, sample.center) - sample.radius;
|
|
81
|
+
if (distanceOutside > 0) {
|
|
82
|
+
hits.push({ id, damage: sample.damagePerSecond * dt, distanceOutside });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return hits;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type RoundPhase = "buy" | "live" | "end";
|
|
2
|
+
export interface RoundPhaseDurations {
|
|
3
|
+
buy: number;
|
|
4
|
+
live: number;
|
|
5
|
+
end: number;
|
|
6
|
+
}
|
|
7
|
+
export interface LossBonusRule {
|
|
8
|
+
base: number;
|
|
9
|
+
step: number;
|
|
10
|
+
max: number;
|
|
11
|
+
}
|
|
12
|
+
export interface RoundConfig {
|
|
13
|
+
phases: RoundPhaseDurations;
|
|
14
|
+
teams: readonly string[];
|
|
15
|
+
maxRounds?: number;
|
|
16
|
+
winReward?: number;
|
|
17
|
+
lossBonus?: LossBonusRule;
|
|
18
|
+
}
|
|
19
|
+
export type RoundEventKind = "phase.start" | "phase.end" | "round.win" | "round.economy" | "match.end";
|
|
20
|
+
export interface RoundEconomy {
|
|
21
|
+
team: string;
|
|
22
|
+
reward: number;
|
|
23
|
+
won: boolean;
|
|
24
|
+
lossStreak: number;
|
|
25
|
+
}
|
|
26
|
+
export interface RoundEvent {
|
|
27
|
+
kind: RoundEventKind;
|
|
28
|
+
round: number;
|
|
29
|
+
phase?: RoundPhase;
|
|
30
|
+
nextPhase?: RoundPhase;
|
|
31
|
+
winner?: string;
|
|
32
|
+
economy?: RoundEconomy[];
|
|
33
|
+
}
|
|
34
|
+
export type PhaseEndHook = (endingPhase: RoundPhase, nextPhase: RoundPhase, round: number) => void;
|
|
35
|
+
export interface RoundSnapshot {
|
|
36
|
+
round: number;
|
|
37
|
+
phase: RoundPhase;
|
|
38
|
+
timeLeft: number;
|
|
39
|
+
scores: Record<string, number>;
|
|
40
|
+
lossStreaks: Record<string, number>;
|
|
41
|
+
matchOver: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface RoundState {
|
|
44
|
+
tick(dt: number): RoundEvent[];
|
|
45
|
+
concludeRound(winner: string): RoundEvent[];
|
|
46
|
+
onPhaseEnd(hook: PhaseEndHook): () => void;
|
|
47
|
+
phase(): RoundPhase;
|
|
48
|
+
round(): number;
|
|
49
|
+
timeLeft(): number;
|
|
50
|
+
score(team: string): number;
|
|
51
|
+
economyFor(team: string): RoundEconomy;
|
|
52
|
+
snapshot(): RoundSnapshot;
|
|
53
|
+
}
|
|
54
|
+
export declare function lossBonusFor(rule: LossBonusRule | undefined, streak: number): number;
|
|
55
|
+
export declare function createRoundState(config: RoundConfig): RoundState;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const PHASE_ORDER = ["buy", "live", "end"];
|
|
2
|
+
export function lossBonusFor(rule, streak) {
|
|
3
|
+
if (rule === undefined)
|
|
4
|
+
return 0;
|
|
5
|
+
return Math.min(rule.max, rule.base + rule.step * Math.max(0, streak));
|
|
6
|
+
}
|
|
7
|
+
export function createRoundState(config) {
|
|
8
|
+
const durations = config.phases;
|
|
9
|
+
const teams = config.teams.length > 0 ? [...config.teams] : ["a", "b"];
|
|
10
|
+
const scores = {};
|
|
11
|
+
const lossStreaks = {};
|
|
12
|
+
for (const team of teams) {
|
|
13
|
+
scores[team] = 0;
|
|
14
|
+
lossStreaks[team] = 0;
|
|
15
|
+
}
|
|
16
|
+
let round = 1;
|
|
17
|
+
let phase = "buy";
|
|
18
|
+
let timeLeft = durations.buy;
|
|
19
|
+
let matchOver = false;
|
|
20
|
+
let pendingWinner = null;
|
|
21
|
+
const hooks = new Set();
|
|
22
|
+
function durationOf(next) {
|
|
23
|
+
return durations[next];
|
|
24
|
+
}
|
|
25
|
+
function nextPhaseOf(current) {
|
|
26
|
+
if (current === "end")
|
|
27
|
+
return "buy";
|
|
28
|
+
return PHASE_ORDER[PHASE_ORDER.indexOf(current) + 1];
|
|
29
|
+
}
|
|
30
|
+
function economyFor(team) {
|
|
31
|
+
const won = pendingWinner === team;
|
|
32
|
+
return {
|
|
33
|
+
team,
|
|
34
|
+
reward: won ? config.winReward ?? 0 : lossBonusFor(config.lossBonus, lossStreaks[team]),
|
|
35
|
+
won,
|
|
36
|
+
lossStreak: lossStreaks[team],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function settleEconomy(winner) {
|
|
40
|
+
const economy = [];
|
|
41
|
+
for (const team of teams) {
|
|
42
|
+
const won = team === winner;
|
|
43
|
+
const reward = won ? config.winReward ?? 0 : lossBonusFor(config.lossBonus, lossStreaks[team]);
|
|
44
|
+
economy.push({ team, reward, won, lossStreak: lossStreaks[team] });
|
|
45
|
+
}
|
|
46
|
+
for (const team of teams) {
|
|
47
|
+
if (team === winner)
|
|
48
|
+
lossStreaks[team] = 0;
|
|
49
|
+
else
|
|
50
|
+
lossStreaks[team] += 1;
|
|
51
|
+
}
|
|
52
|
+
return economy;
|
|
53
|
+
}
|
|
54
|
+
function enter(next, events) {
|
|
55
|
+
phase = next;
|
|
56
|
+
timeLeft = durationOf(next);
|
|
57
|
+
events.push({ kind: "phase.start", round, phase: next });
|
|
58
|
+
}
|
|
59
|
+
function endPhase(events) {
|
|
60
|
+
const ending = phase;
|
|
61
|
+
if (ending === "end") {
|
|
62
|
+
if (config.maxRounds !== undefined && round >= config.maxRounds) {
|
|
63
|
+
matchOver = true;
|
|
64
|
+
events.push({ kind: "phase.end", round, phase: ending, nextPhase: "buy" });
|
|
65
|
+
events.push({ kind: "match.end", round });
|
|
66
|
+
for (const hook of hooks)
|
|
67
|
+
hook(ending, "buy", round);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
round += 1;
|
|
71
|
+
pendingWinner = null;
|
|
72
|
+
}
|
|
73
|
+
const next = nextPhaseOf(ending);
|
|
74
|
+
events.push({ kind: "phase.end", round: ending === "end" ? round - 1 : round, phase: ending, nextPhase: next });
|
|
75
|
+
for (const hook of hooks)
|
|
76
|
+
hook(ending, next, ending === "end" ? round - 1 : round);
|
|
77
|
+
enter(next, events);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
tick(dt) {
|
|
81
|
+
if (dt <= 0 || matchOver)
|
|
82
|
+
return [];
|
|
83
|
+
const events = [];
|
|
84
|
+
timeLeft -= dt;
|
|
85
|
+
let guard = 0;
|
|
86
|
+
while (timeLeft <= 0 && !matchOver && guard < PHASE_ORDER.length + 2) {
|
|
87
|
+
const carry = timeLeft;
|
|
88
|
+
if (phase === "live")
|
|
89
|
+
pendingWinner = null;
|
|
90
|
+
endPhase(events);
|
|
91
|
+
if (!matchOver)
|
|
92
|
+
timeLeft += carry;
|
|
93
|
+
guard += 1;
|
|
94
|
+
}
|
|
95
|
+
return events;
|
|
96
|
+
},
|
|
97
|
+
concludeRound(winner) {
|
|
98
|
+
if (matchOver || phase !== "live")
|
|
99
|
+
return [];
|
|
100
|
+
const events = [];
|
|
101
|
+
pendingWinner = winner;
|
|
102
|
+
scores[winner] = (scores[winner] ?? 0) + 1;
|
|
103
|
+
events.push({ kind: "round.win", round, winner });
|
|
104
|
+
const economy = settleEconomy(winner);
|
|
105
|
+
events.push({ kind: "round.economy", round, winner, economy });
|
|
106
|
+
endPhase(events);
|
|
107
|
+
return events;
|
|
108
|
+
},
|
|
109
|
+
onPhaseEnd(hook) {
|
|
110
|
+
hooks.add(hook);
|
|
111
|
+
return () => {
|
|
112
|
+
hooks.delete(hook);
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
phase: () => phase,
|
|
116
|
+
round: () => round,
|
|
117
|
+
timeLeft: () => Math.max(0, timeLeft),
|
|
118
|
+
score: (team) => scores[team] ?? 0,
|
|
119
|
+
economyFor,
|
|
120
|
+
snapshot: () => ({
|
|
121
|
+
round,
|
|
122
|
+
phase,
|
|
123
|
+
timeLeft: Math.max(0, timeLeft),
|
|
124
|
+
scores: { ...scores },
|
|
125
|
+
lossStreaks: { ...lossStreaks },
|
|
126
|
+
matchOver,
|
|
127
|
+
}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface MeterTier {
|
|
2
|
+
id: string;
|
|
3
|
+
at: number;
|
|
4
|
+
}
|
|
5
|
+
export type MeterMode = "hold" | "reset";
|
|
6
|
+
export interface AccumulatorMeterConfig {
|
|
7
|
+
max: number;
|
|
8
|
+
mode?: MeterMode;
|
|
9
|
+
decayPerSecond?: number;
|
|
10
|
+
decayDelayMs?: number;
|
|
11
|
+
tiers?: readonly MeterTier[];
|
|
12
|
+
}
|
|
13
|
+
export interface MeterAddResult {
|
|
14
|
+
value: number;
|
|
15
|
+
fired: boolean;
|
|
16
|
+
overflow: number;
|
|
17
|
+
tier: string | null;
|
|
18
|
+
tierChanged: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface AccumulatorMeter {
|
|
21
|
+
value(): number;
|
|
22
|
+
fraction(): number;
|
|
23
|
+
tier(): string | null;
|
|
24
|
+
broke(): boolean;
|
|
25
|
+
add(amount: number): MeterAddResult;
|
|
26
|
+
drain(amount: number): void;
|
|
27
|
+
reset(): void;
|
|
28
|
+
tick(dtSeconds: number): void;
|
|
29
|
+
}
|
|
30
|
+
export declare function tierAt(value: number, tiers: readonly MeterTier[]): string | null;
|
|
31
|
+
export declare function createAccumulatorMeter(config: AccumulatorMeterConfig): AccumulatorMeter;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
function sortedTiers(tiers) {
|
|
2
|
+
if (tiers === undefined)
|
|
3
|
+
return [];
|
|
4
|
+
return [...tiers].sort((a, b) => a.at - b.at);
|
|
5
|
+
}
|
|
6
|
+
export function tierAt(value, tiers) {
|
|
7
|
+
let current = null;
|
|
8
|
+
for (const tier of tiers) {
|
|
9
|
+
if (value >= tier.at)
|
|
10
|
+
current = tier.id;
|
|
11
|
+
else
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
export function createAccumulatorMeter(config) {
|
|
17
|
+
const mode = config.mode ?? "hold";
|
|
18
|
+
const tiers = sortedTiers(config.tiers);
|
|
19
|
+
const decayPerSecond = config.decayPerSecond ?? 0;
|
|
20
|
+
const decayDelayMs = config.decayDelayMs ?? 0;
|
|
21
|
+
let value = 0;
|
|
22
|
+
let broken = false;
|
|
23
|
+
let idleMs = 0;
|
|
24
|
+
function clamp(next) {
|
|
25
|
+
return Math.max(0, Math.min(config.max, next));
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
value() {
|
|
29
|
+
return value;
|
|
30
|
+
},
|
|
31
|
+
fraction() {
|
|
32
|
+
return config.max <= 0 ? 0 : value / config.max;
|
|
33
|
+
},
|
|
34
|
+
tier() {
|
|
35
|
+
return tierAt(value, tiers);
|
|
36
|
+
},
|
|
37
|
+
broke() {
|
|
38
|
+
return broken;
|
|
39
|
+
},
|
|
40
|
+
add(amount) {
|
|
41
|
+
idleMs = 0;
|
|
42
|
+
const tierBefore = tierAt(value, tiers);
|
|
43
|
+
const raw = value + amount;
|
|
44
|
+
const fired = amount > 0 && raw >= config.max && !(mode === "hold" && broken);
|
|
45
|
+
if (mode === "reset" && fired) {
|
|
46
|
+
value = clamp(raw - config.max);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
value = clamp(raw);
|
|
50
|
+
if (fired && mode === "hold")
|
|
51
|
+
broken = true;
|
|
52
|
+
}
|
|
53
|
+
const tierAfter = tierAt(value, tiers);
|
|
54
|
+
return {
|
|
55
|
+
value,
|
|
56
|
+
fired,
|
|
57
|
+
overflow: fired && mode === "reset" ? Math.max(0, raw - config.max) : 0,
|
|
58
|
+
tier: tierAfter,
|
|
59
|
+
tierChanged: tierAfter !== tierBefore,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
drain(amount) {
|
|
63
|
+
value = clamp(value - amount);
|
|
64
|
+
if (value <= 0)
|
|
65
|
+
broken = false;
|
|
66
|
+
},
|
|
67
|
+
reset() {
|
|
68
|
+
value = 0;
|
|
69
|
+
broken = false;
|
|
70
|
+
idleMs = 0;
|
|
71
|
+
},
|
|
72
|
+
tick(dtSeconds) {
|
|
73
|
+
if (dtSeconds <= 0)
|
|
74
|
+
return;
|
|
75
|
+
idleMs += dtSeconds * 1000;
|
|
76
|
+
if (broken || decayPerSecond <= 0 || idleMs < decayDelayMs)
|
|
77
|
+
return;
|
|
78
|
+
value = clamp(value - decayPerSecond * dtSeconds);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type MeterMode, type MeterTier } from "./accumulatorMeter.js";
|
|
2
|
+
export interface EventMeterConfig {
|
|
3
|
+
max: number;
|
|
4
|
+
mode?: MeterMode;
|
|
5
|
+
decayPerSecond?: number;
|
|
6
|
+
decayDelayMs?: number;
|
|
7
|
+
tiers?: readonly MeterTier[];
|
|
8
|
+
gains: Record<string, number>;
|
|
9
|
+
resets?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export interface EventMeterFeedResult {
|
|
12
|
+
tag: string;
|
|
13
|
+
reset: boolean;
|
|
14
|
+
amount: number;
|
|
15
|
+
value: number;
|
|
16
|
+
fraction: number;
|
|
17
|
+
fired: boolean;
|
|
18
|
+
ready: boolean;
|
|
19
|
+
overflow: number;
|
|
20
|
+
tier: string | null;
|
|
21
|
+
tierChanged: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface EventMeter {
|
|
24
|
+
value(): number;
|
|
25
|
+
fraction(): number;
|
|
26
|
+
tier(): string | null;
|
|
27
|
+
ready(): boolean;
|
|
28
|
+
feed(tag: string, scale?: number): EventMeterFeedResult;
|
|
29
|
+
consume(): boolean;
|
|
30
|
+
drain(amount: number): void;
|
|
31
|
+
reset(): void;
|
|
32
|
+
tick(dtSeconds: number): void;
|
|
33
|
+
}
|
|
34
|
+
export declare function createEventMeter(config: EventMeterConfig): EventMeter;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createAccumulatorMeter, } from "./accumulatorMeter.js";
|
|
2
|
+
function readyOf(meter, mode) {
|
|
3
|
+
return mode === "hold" ? meter.broke() : meter.value() >= 0 && meter.fraction() >= 1;
|
|
4
|
+
}
|
|
5
|
+
export function createEventMeter(config) {
|
|
6
|
+
const mode = config.mode ?? "hold";
|
|
7
|
+
const resets = new Set(config.resets ?? []);
|
|
8
|
+
const meter = createAccumulatorMeter({
|
|
9
|
+
max: config.max,
|
|
10
|
+
mode,
|
|
11
|
+
decayPerSecond: config.decayPerSecond,
|
|
12
|
+
decayDelayMs: config.decayDelayMs,
|
|
13
|
+
tiers: config.tiers,
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
value() {
|
|
17
|
+
return meter.value();
|
|
18
|
+
},
|
|
19
|
+
fraction() {
|
|
20
|
+
return meter.fraction();
|
|
21
|
+
},
|
|
22
|
+
tier() {
|
|
23
|
+
return meter.tier();
|
|
24
|
+
},
|
|
25
|
+
ready() {
|
|
26
|
+
return readyOf(meter, mode);
|
|
27
|
+
},
|
|
28
|
+
feed(tag, scale = 1) {
|
|
29
|
+
const tierBefore = meter.tier();
|
|
30
|
+
if (resets.has(tag)) {
|
|
31
|
+
meter.reset();
|
|
32
|
+
return {
|
|
33
|
+
tag,
|
|
34
|
+
reset: true,
|
|
35
|
+
amount: 0,
|
|
36
|
+
value: meter.value(),
|
|
37
|
+
fraction: meter.fraction(),
|
|
38
|
+
fired: false,
|
|
39
|
+
ready: readyOf(meter, mode),
|
|
40
|
+
overflow: 0,
|
|
41
|
+
tier: meter.tier(),
|
|
42
|
+
tierChanged: meter.tier() !== tierBefore,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const amount = (config.gains[tag] ?? 0) * scale;
|
|
46
|
+
const result = meter.add(amount);
|
|
47
|
+
return {
|
|
48
|
+
tag,
|
|
49
|
+
reset: false,
|
|
50
|
+
amount,
|
|
51
|
+
value: result.value,
|
|
52
|
+
fraction: meter.fraction(),
|
|
53
|
+
fired: result.fired,
|
|
54
|
+
ready: readyOf(meter, mode),
|
|
55
|
+
overflow: result.overflow,
|
|
56
|
+
tier: result.tier,
|
|
57
|
+
tierChanged: result.tierChanged,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
consume() {
|
|
61
|
+
if (!readyOf(meter, mode))
|
|
62
|
+
return false;
|
|
63
|
+
meter.reset();
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
66
|
+
drain(amount) {
|
|
67
|
+
meter.drain(amount);
|
|
68
|
+
},
|
|
69
|
+
reset() {
|
|
70
|
+
meter.reset();
|
|
71
|
+
},
|
|
72
|
+
tick(dtSeconds) {
|
|
73
|
+
meter.tick(dtSeconds);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type CheckAdvantage = "advantage" | "disadvantage" | "normal";
|
|
2
|
+
export interface CheckInput {
|
|
3
|
+
modifier: number;
|
|
4
|
+
dc: number;
|
|
5
|
+
advantage?: CheckAdvantage;
|
|
6
|
+
diceSides?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface CheckResult {
|
|
9
|
+
rolls: readonly number[];
|
|
10
|
+
roll: number;
|
|
11
|
+
total: number;
|
|
12
|
+
success: boolean;
|
|
13
|
+
critical: "success" | "failure" | null;
|
|
14
|
+
}
|
|
15
|
+
export declare function rollCheck(input: CheckInput, rng?: () => number): CheckResult;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function rollCheck(input, rng = Math.random) {
|
|
2
|
+
const sides = input.diceSides ?? 20;
|
|
3
|
+
const advantage = input.advantage ?? "normal";
|
|
4
|
+
const rollDie = () => Math.floor(rng() * sides) + 1;
|
|
5
|
+
const rolls = advantage === "normal" ? [rollDie()] : [rollDie(), rollDie()];
|
|
6
|
+
const roll = advantage === "advantage"
|
|
7
|
+
? Math.max(...rolls)
|
|
8
|
+
: advantage === "disadvantage"
|
|
9
|
+
? Math.min(...rolls)
|
|
10
|
+
: rolls[0];
|
|
11
|
+
const total = roll + input.modifier;
|
|
12
|
+
const success = total >= input.dc;
|
|
13
|
+
const critical = roll === sides ? "success" : roll === 1 ? "failure" : null;
|
|
14
|
+
return { rolls, roll, total, success, critical };
|
|
15
|
+
}
|