@jgengine/core 0.6.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 +167 -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 +2 -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 +281 -4
- 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 +35 -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 +83 -0
- package/dist/physics/physicsWorld.js +261 -1
- 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 +64 -2
- package/dist/runtime/gameContext.js +154 -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/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 +1 -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/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/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/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/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/walls.d.ts +61 -0
- package/dist/world/walls.js +221 -0
- package/dist/world/weather.d.ts +92 -0
- package/dist/world/weather.js +169 -0
- package/package.json +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Tile } from "./tacticalGrid.js";
|
|
2
|
+
export interface SurfaceKindDef {
|
|
3
|
+
id: string;
|
|
4
|
+
duration?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface SurfaceReaction {
|
|
7
|
+
when: readonly [string, string];
|
|
8
|
+
result: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface SurfaceLayerConfig {
|
|
11
|
+
kinds: readonly SurfaceKindDef[];
|
|
12
|
+
reactions?: readonly SurfaceReaction[];
|
|
13
|
+
}
|
|
14
|
+
export interface SurfacePatch {
|
|
15
|
+
duration?: number;
|
|
16
|
+
}
|
|
17
|
+
export type SurfaceEvent = {
|
|
18
|
+
kind: "apply";
|
|
19
|
+
tile: Tile;
|
|
20
|
+
surface: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "react";
|
|
23
|
+
tile: Tile;
|
|
24
|
+
consumed: readonly [string, string];
|
|
25
|
+
produced: string | null;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "expire";
|
|
28
|
+
tile: Tile;
|
|
29
|
+
surface: string;
|
|
30
|
+
};
|
|
31
|
+
export interface SurfaceCellKind {
|
|
32
|
+
surface: string;
|
|
33
|
+
remaining: number | null;
|
|
34
|
+
}
|
|
35
|
+
export interface SurfaceCell {
|
|
36
|
+
tile: Tile;
|
|
37
|
+
kinds: SurfaceCellKind[];
|
|
38
|
+
}
|
|
39
|
+
export type SurfaceLayerSnapshot = Record<string, Record<string, number | null>>;
|
|
40
|
+
export interface SurfaceLayer {
|
|
41
|
+
apply(tile: Tile, surface: string, patch?: SurfacePatch): SurfaceEvent[];
|
|
42
|
+
has(tile: Tile, surface: string): boolean;
|
|
43
|
+
kindsAt(tile: Tile): string[];
|
|
44
|
+
remove(tile: Tile, surface: string): void;
|
|
45
|
+
clearTile(tile: Tile): void;
|
|
46
|
+
clear(): void;
|
|
47
|
+
tick(dt: number): SurfaceEvent[];
|
|
48
|
+
cells(): SurfaceCell[];
|
|
49
|
+
capture(): SurfaceLayerSnapshot;
|
|
50
|
+
restore(snapshot: SurfaceLayerSnapshot): void;
|
|
51
|
+
}
|
|
52
|
+
export declare function createSurfaceLayer(config: SurfaceLayerConfig): SurfaceLayer;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
function key(tile) {
|
|
2
|
+
return `${tile[0]},${tile[1]}`;
|
|
3
|
+
}
|
|
4
|
+
function parseKey(k) {
|
|
5
|
+
const [c, r] = k.split(",");
|
|
6
|
+
return [Number(c), Number(r)];
|
|
7
|
+
}
|
|
8
|
+
function pairKey(a, b) {
|
|
9
|
+
return `${a}|${b}`;
|
|
10
|
+
}
|
|
11
|
+
export function createSurfaceLayer(config) {
|
|
12
|
+
const durations = new Map();
|
|
13
|
+
for (const k of config.kinds)
|
|
14
|
+
durations.set(k.id, k.duration);
|
|
15
|
+
const reactions = new Map();
|
|
16
|
+
for (const reaction of config.reactions ?? []) {
|
|
17
|
+
const [a, b] = reaction.when;
|
|
18
|
+
reactions.set(pairKey(a, b), reaction.result);
|
|
19
|
+
reactions.set(pairKey(b, a), reaction.result);
|
|
20
|
+
}
|
|
21
|
+
const cells = new Map();
|
|
22
|
+
function cellOf(tile) {
|
|
23
|
+
const k = key(tile);
|
|
24
|
+
let cell = cells.get(k);
|
|
25
|
+
if (cell === undefined) {
|
|
26
|
+
cell = new Map();
|
|
27
|
+
cells.set(k, cell);
|
|
28
|
+
}
|
|
29
|
+
return cell;
|
|
30
|
+
}
|
|
31
|
+
function initialRemaining(surface, patch) {
|
|
32
|
+
if (patch?.duration !== undefined)
|
|
33
|
+
return patch.duration;
|
|
34
|
+
const configured = durations.get(surface);
|
|
35
|
+
return configured === undefined ? null : configured;
|
|
36
|
+
}
|
|
37
|
+
function resolveReactions(tile, events) {
|
|
38
|
+
const cell = cellOf(tile);
|
|
39
|
+
for (;;) {
|
|
40
|
+
const active = [...cell.keys()];
|
|
41
|
+
let matched = null;
|
|
42
|
+
outer: for (let i = 0; i < active.length; i += 1) {
|
|
43
|
+
for (let j = i + 1; j < active.length; j += 1) {
|
|
44
|
+
const result = reactions.get(pairKey(active[i], active[j]));
|
|
45
|
+
if (result !== undefined) {
|
|
46
|
+
matched = { a: active[i], b: active[j], result };
|
|
47
|
+
break outer;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (matched === null)
|
|
52
|
+
return;
|
|
53
|
+
cell.delete(matched.a);
|
|
54
|
+
cell.delete(matched.b);
|
|
55
|
+
if (matched.result !== null)
|
|
56
|
+
cell.set(matched.result, initialRemaining(matched.result));
|
|
57
|
+
events.push({ kind: "react", tile, consumed: [matched.a, matched.b], produced: matched.result });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
apply: (tile, surface, patch) => {
|
|
62
|
+
const events = [];
|
|
63
|
+
const cell = cellOf(tile);
|
|
64
|
+
cell.set(surface, initialRemaining(surface, patch));
|
|
65
|
+
events.push({ kind: "apply", tile, surface });
|
|
66
|
+
resolveReactions(tile, events);
|
|
67
|
+
if (cells.get(key(tile))?.size === 0)
|
|
68
|
+
cells.delete(key(tile));
|
|
69
|
+
return events;
|
|
70
|
+
},
|
|
71
|
+
has: (tile, surface) => cells.get(key(tile))?.has(surface) ?? false,
|
|
72
|
+
kindsAt: (tile) => [...(cells.get(key(tile))?.keys() ?? [])],
|
|
73
|
+
remove: (tile, surface) => {
|
|
74
|
+
const cell = cells.get(key(tile));
|
|
75
|
+
if (cell === undefined)
|
|
76
|
+
return;
|
|
77
|
+
cell.delete(surface);
|
|
78
|
+
if (cell.size === 0)
|
|
79
|
+
cells.delete(key(tile));
|
|
80
|
+
},
|
|
81
|
+
clearTile: (tile) => {
|
|
82
|
+
cells.delete(key(tile));
|
|
83
|
+
},
|
|
84
|
+
clear: () => cells.clear(),
|
|
85
|
+
tick: (dt) => {
|
|
86
|
+
const events = [];
|
|
87
|
+
for (const [k, cell] of [...cells]) {
|
|
88
|
+
for (const [surface, remaining] of [...cell]) {
|
|
89
|
+
if (remaining === null)
|
|
90
|
+
continue;
|
|
91
|
+
const next = remaining - dt;
|
|
92
|
+
if (next <= 0) {
|
|
93
|
+
cell.delete(surface);
|
|
94
|
+
events.push({ kind: "expire", tile: parseKey(k), surface });
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
cell.set(surface, next);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (cell.size === 0)
|
|
101
|
+
cells.delete(k);
|
|
102
|
+
}
|
|
103
|
+
return events;
|
|
104
|
+
},
|
|
105
|
+
cells: () => {
|
|
106
|
+
const out = [];
|
|
107
|
+
for (const [k, cell] of cells) {
|
|
108
|
+
out.push({
|
|
109
|
+
tile: parseKey(k),
|
|
110
|
+
kinds: [...cell].map(([surface, remaining]) => ({ surface, remaining })),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
},
|
|
115
|
+
capture: () => {
|
|
116
|
+
const snapshot = {};
|
|
117
|
+
for (const [k, cell] of cells) {
|
|
118
|
+
const entry = {};
|
|
119
|
+
for (const [surface, remaining] of cell)
|
|
120
|
+
entry[surface] = remaining;
|
|
121
|
+
snapshot[k] = entry;
|
|
122
|
+
}
|
|
123
|
+
return snapshot;
|
|
124
|
+
},
|
|
125
|
+
restore: (snapshot) => {
|
|
126
|
+
cells.clear();
|
|
127
|
+
for (const [k, entry] of Object.entries(snapshot)) {
|
|
128
|
+
const cell = new Map();
|
|
129
|
+
for (const [surface, remaining] of Object.entries(entry))
|
|
130
|
+
cell.set(surface, remaining);
|
|
131
|
+
cells.set(k, cell);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type Tile = readonly [number, number];
|
|
2
|
+
export interface TacticalGridConfig {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
blocked?: readonly Tile[];
|
|
6
|
+
diagonal?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ReachableOptions {
|
|
9
|
+
passThrough?: readonly string[];
|
|
10
|
+
diagonal?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ReachableTile {
|
|
13
|
+
tile: Tile;
|
|
14
|
+
cost: number;
|
|
15
|
+
}
|
|
16
|
+
export type PushObstacle = "wall" | "edge";
|
|
17
|
+
export interface PushCollision {
|
|
18
|
+
mover: string;
|
|
19
|
+
into: string | PushObstacle;
|
|
20
|
+
at: Tile;
|
|
21
|
+
}
|
|
22
|
+
export interface PushMove {
|
|
23
|
+
id: string;
|
|
24
|
+
from: Tile;
|
|
25
|
+
to: Tile;
|
|
26
|
+
}
|
|
27
|
+
export interface PushResult {
|
|
28
|
+
moves: PushMove[];
|
|
29
|
+
collisions: PushCollision[];
|
|
30
|
+
}
|
|
31
|
+
export interface PushOptions {
|
|
32
|
+
distance?: number;
|
|
33
|
+
chain?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface TacticalGridSnapshot {
|
|
36
|
+
blocked: string[];
|
|
37
|
+
occupants: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
export interface TacticalGrid {
|
|
40
|
+
readonly width: number;
|
|
41
|
+
readonly height: number;
|
|
42
|
+
inBounds(tile: Tile): boolean;
|
|
43
|
+
isBlocked(tile: Tile): boolean;
|
|
44
|
+
setBlocked(tile: Tile, blocked: boolean): void;
|
|
45
|
+
occupantAt(tile: Tile): string | null;
|
|
46
|
+
tileOf(id: string): Tile | null;
|
|
47
|
+
place(id: string, tile: Tile): boolean;
|
|
48
|
+
remove(id: string): void;
|
|
49
|
+
move(id: string, tile: Tile): boolean;
|
|
50
|
+
neighbors(tile: Tile, diagonal?: boolean): Tile[];
|
|
51
|
+
reachable(from: Tile, budget: number, options?: ReachableOptions): ReachableTile[];
|
|
52
|
+
path(from: Tile, to: Tile, options?: ReachableOptions): Tile[] | null;
|
|
53
|
+
push(id: string, direction: Tile, options?: PushOptions): PushResult;
|
|
54
|
+
capture(): TacticalGridSnapshot;
|
|
55
|
+
restore(snapshot: TacticalGridSnapshot): void;
|
|
56
|
+
}
|
|
57
|
+
export declare function createTacticalGrid(config: TacticalGridConfig): TacticalGrid;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
function key(tile) {
|
|
2
|
+
return `${tile[0]},${tile[1]}`;
|
|
3
|
+
}
|
|
4
|
+
function parseKey(k) {
|
|
5
|
+
const [c, r] = k.split(",");
|
|
6
|
+
return [Number(c), Number(r)];
|
|
7
|
+
}
|
|
8
|
+
export function createTacticalGrid(config) {
|
|
9
|
+
const width = config.width;
|
|
10
|
+
const height = config.height;
|
|
11
|
+
const defaultDiagonal = config.diagonal ?? false;
|
|
12
|
+
const blocked = new Set((config.blocked ?? []).map(key));
|
|
13
|
+
const occupants = new Map();
|
|
14
|
+
const tiles = new Map();
|
|
15
|
+
function inBounds(tile) {
|
|
16
|
+
return tile[0] >= 0 && tile[0] < width && tile[1] >= 0 && tile[1] < height;
|
|
17
|
+
}
|
|
18
|
+
function isBlocked(tile) {
|
|
19
|
+
return blocked.has(key(tile));
|
|
20
|
+
}
|
|
21
|
+
function occupantAt(tile) {
|
|
22
|
+
return occupants.get(key(tile)) ?? null;
|
|
23
|
+
}
|
|
24
|
+
function tileOf(id) {
|
|
25
|
+
const t = tiles.get(id);
|
|
26
|
+
return t === undefined ? null : [t[0], t[1]];
|
|
27
|
+
}
|
|
28
|
+
function place(id, tile) {
|
|
29
|
+
if (!inBounds(tile) || isBlocked(tile))
|
|
30
|
+
return false;
|
|
31
|
+
if (occupants.has(key(tile)))
|
|
32
|
+
return false;
|
|
33
|
+
const prev = tiles.get(id);
|
|
34
|
+
if (prev !== undefined)
|
|
35
|
+
occupants.delete(key(prev));
|
|
36
|
+
occupants.set(key(tile), id);
|
|
37
|
+
tiles.set(id, [tile[0], tile[1]]);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
function remove(id) {
|
|
41
|
+
const t = tiles.get(id);
|
|
42
|
+
if (t === undefined)
|
|
43
|
+
return;
|
|
44
|
+
occupants.delete(key(t));
|
|
45
|
+
tiles.delete(id);
|
|
46
|
+
}
|
|
47
|
+
function move(id, tile) {
|
|
48
|
+
if (!tiles.has(id))
|
|
49
|
+
return false;
|
|
50
|
+
if (!inBounds(tile) || isBlocked(tile))
|
|
51
|
+
return false;
|
|
52
|
+
const holder = occupants.get(key(tile));
|
|
53
|
+
if (holder !== undefined && holder !== id)
|
|
54
|
+
return false;
|
|
55
|
+
const prev = tiles.get(id);
|
|
56
|
+
occupants.delete(key(prev));
|
|
57
|
+
occupants.set(key(tile), id);
|
|
58
|
+
tiles.set(id, [tile[0], tile[1]]);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
function neighbors(tile, diagonal = defaultDiagonal) {
|
|
62
|
+
const orthogonal = [
|
|
63
|
+
[tile[0] + 1, tile[1]],
|
|
64
|
+
[tile[0] - 1, tile[1]],
|
|
65
|
+
[tile[0], tile[1] + 1],
|
|
66
|
+
[tile[0], tile[1] - 1],
|
|
67
|
+
];
|
|
68
|
+
const diagonals = [
|
|
69
|
+
[tile[0] + 1, tile[1] + 1],
|
|
70
|
+
[tile[0] + 1, tile[1] - 1],
|
|
71
|
+
[tile[0] - 1, tile[1] + 1],
|
|
72
|
+
[tile[0] - 1, tile[1] - 1],
|
|
73
|
+
];
|
|
74
|
+
return (diagonal ? [...orthogonal, ...diagonals] : orthogonal).filter(inBounds);
|
|
75
|
+
}
|
|
76
|
+
function enterable(tile, passThrough) {
|
|
77
|
+
if (!inBounds(tile) || isBlocked(tile))
|
|
78
|
+
return false;
|
|
79
|
+
const holder = occupants.get(key(tile));
|
|
80
|
+
return holder === undefined || passThrough.has(holder);
|
|
81
|
+
}
|
|
82
|
+
function reachable(from, budget, options) {
|
|
83
|
+
const diagonal = options?.diagonal ?? defaultDiagonal;
|
|
84
|
+
const passThrough = new Set(options?.passThrough ?? []);
|
|
85
|
+
const mover = occupantAt(from);
|
|
86
|
+
if (mover !== null)
|
|
87
|
+
passThrough.add(mover);
|
|
88
|
+
const best = new Map([[key(from), 0]]);
|
|
89
|
+
const frontier = [from];
|
|
90
|
+
while (frontier.length > 0) {
|
|
91
|
+
const current = frontier.shift();
|
|
92
|
+
const cost = best.get(key(current));
|
|
93
|
+
if (cost >= budget)
|
|
94
|
+
continue;
|
|
95
|
+
for (const next of neighbors(current, diagonal)) {
|
|
96
|
+
if (!enterable(next, passThrough))
|
|
97
|
+
continue;
|
|
98
|
+
const nextCost = cost + 1;
|
|
99
|
+
const known = best.get(key(next));
|
|
100
|
+
if (known !== undefined && known <= nextCost)
|
|
101
|
+
continue;
|
|
102
|
+
best.set(key(next), nextCost);
|
|
103
|
+
frontier.push(next);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const result = [];
|
|
107
|
+
for (const [k, cost] of best) {
|
|
108
|
+
if (k === key(from))
|
|
109
|
+
continue;
|
|
110
|
+
result.push({ tile: parseKey(k), cost });
|
|
111
|
+
}
|
|
112
|
+
return result.sort((a, b) => a.cost - b.cost || a.tile[0] - b.tile[0] || a.tile[1] - b.tile[1]);
|
|
113
|
+
}
|
|
114
|
+
function path(from, to, options) {
|
|
115
|
+
if (!inBounds(from) || !inBounds(to))
|
|
116
|
+
return null;
|
|
117
|
+
const diagonal = options?.diagonal ?? defaultDiagonal;
|
|
118
|
+
const passThrough = new Set(options?.passThrough ?? []);
|
|
119
|
+
const mover = occupantAt(from);
|
|
120
|
+
if (mover !== null)
|
|
121
|
+
passThrough.add(mover);
|
|
122
|
+
if (key(from) === key(to))
|
|
123
|
+
return [[from[0], from[1]]];
|
|
124
|
+
const cameFrom = new Map();
|
|
125
|
+
const visited = new Set([key(from)]);
|
|
126
|
+
const frontier = [from];
|
|
127
|
+
while (frontier.length > 0) {
|
|
128
|
+
const current = frontier.shift();
|
|
129
|
+
for (const next of neighbors(current, diagonal)) {
|
|
130
|
+
if (visited.has(key(next)))
|
|
131
|
+
continue;
|
|
132
|
+
const isGoal = key(next) === key(to);
|
|
133
|
+
if (!isGoal && !enterable(next, passThrough))
|
|
134
|
+
continue;
|
|
135
|
+
if (!isGoal && isBlocked(next))
|
|
136
|
+
continue;
|
|
137
|
+
visited.add(key(next));
|
|
138
|
+
cameFrom.set(key(next), key(current));
|
|
139
|
+
if (isGoal) {
|
|
140
|
+
const chain = [to];
|
|
141
|
+
let step = key(to);
|
|
142
|
+
while (step !== key(from)) {
|
|
143
|
+
step = cameFrom.get(step);
|
|
144
|
+
chain.push(parseKey(step));
|
|
145
|
+
}
|
|
146
|
+
return chain.reverse();
|
|
147
|
+
}
|
|
148
|
+
frontier.push(next);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
function push(id, direction, options) {
|
|
154
|
+
const distance = options?.distance ?? 1;
|
|
155
|
+
const chain = options?.chain ?? false;
|
|
156
|
+
const result = { moves: [], collisions: [] };
|
|
157
|
+
const origins = new Map();
|
|
158
|
+
function record(unitId) {
|
|
159
|
+
const from = origins.get(unitId);
|
|
160
|
+
const to = tileOf(unitId);
|
|
161
|
+
if (from === undefined || to === null)
|
|
162
|
+
return;
|
|
163
|
+
if (from[0] === to[0] && from[1] === to[1])
|
|
164
|
+
return;
|
|
165
|
+
const existing = result.moves.find((m) => m.id === unitId);
|
|
166
|
+
if (existing !== undefined)
|
|
167
|
+
existing.to = to;
|
|
168
|
+
else
|
|
169
|
+
result.moves.push({ id: unitId, from, to });
|
|
170
|
+
}
|
|
171
|
+
function pushUnit(unitId, steps, depth) {
|
|
172
|
+
const start = tileOf(unitId);
|
|
173
|
+
if (start === null)
|
|
174
|
+
return;
|
|
175
|
+
if (!origins.has(unitId))
|
|
176
|
+
origins.set(unitId, start);
|
|
177
|
+
let taken = 0;
|
|
178
|
+
while (taken < steps) {
|
|
179
|
+
const cur = tileOf(unitId);
|
|
180
|
+
const next = [cur[0] + direction[0], cur[1] + direction[1]];
|
|
181
|
+
if (!inBounds(next)) {
|
|
182
|
+
result.collisions.push({ mover: unitId, into: "edge", at: cur });
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
if (isBlocked(next)) {
|
|
186
|
+
result.collisions.push({ mover: unitId, into: "wall", at: cur });
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
const occ = occupantAt(next);
|
|
190
|
+
if (occ !== null) {
|
|
191
|
+
result.collisions.push({ mover: unitId, into: occ, at: cur });
|
|
192
|
+
if (!chain || depth >= width * height)
|
|
193
|
+
break;
|
|
194
|
+
pushUnit(occ, steps - taken, depth + 1);
|
|
195
|
+
if (occupantAt(next) !== null)
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
move(unitId, next);
|
|
199
|
+
taken += 1;
|
|
200
|
+
}
|
|
201
|
+
record(unitId);
|
|
202
|
+
}
|
|
203
|
+
pushUnit(id, distance, 0);
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
width,
|
|
208
|
+
height,
|
|
209
|
+
inBounds,
|
|
210
|
+
isBlocked,
|
|
211
|
+
setBlocked: (tile, value) => {
|
|
212
|
+
if (value)
|
|
213
|
+
blocked.add(key(tile));
|
|
214
|
+
else
|
|
215
|
+
blocked.delete(key(tile));
|
|
216
|
+
},
|
|
217
|
+
occupantAt,
|
|
218
|
+
tileOf,
|
|
219
|
+
place,
|
|
220
|
+
remove,
|
|
221
|
+
move,
|
|
222
|
+
neighbors,
|
|
223
|
+
reachable,
|
|
224
|
+
path,
|
|
225
|
+
push,
|
|
226
|
+
capture: () => {
|
|
227
|
+
const occ = {};
|
|
228
|
+
for (const [k, unitId] of occupants)
|
|
229
|
+
occ[k] = unitId;
|
|
230
|
+
return { blocked: [...blocked], occupants: occ };
|
|
231
|
+
},
|
|
232
|
+
restore: (snapshot) => {
|
|
233
|
+
blocked.clear();
|
|
234
|
+
for (const k of snapshot.blocked)
|
|
235
|
+
blocked.add(k);
|
|
236
|
+
occupants.clear();
|
|
237
|
+
tiles.clear();
|
|
238
|
+
for (const [k, unitId] of Object.entries(snapshot.occupants)) {
|
|
239
|
+
occupants.set(k, unitId);
|
|
240
|
+
tiles.set(unitId, parseKey(k));
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface BeatClockConfig {
|
|
2
|
+
bpm: number;
|
|
3
|
+
/** Beats per bar/measure, for bar-relative UI (rhythm-game combo windows). Default 4. */
|
|
4
|
+
beatsPerBar?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface BeatSnapshot {
|
|
7
|
+
/** Fractional beat count since the clock started. */
|
|
8
|
+
beat: number;
|
|
9
|
+
beatIndex: number;
|
|
10
|
+
bar: number;
|
|
11
|
+
beatInBar: number;
|
|
12
|
+
/** 0..1 fractional position within the current beat. */
|
|
13
|
+
phase: number;
|
|
14
|
+
}
|
|
15
|
+
export interface BeatClock {
|
|
16
|
+
/** Advance by a game-time delta (already scaled by ctx.time — never wall-clock). Returns the resulting snapshot and fires onBeat once per newly crossed integer beat. */
|
|
17
|
+
advance(gameDt: number): BeatSnapshot;
|
|
18
|
+
snapshot(): BeatSnapshot;
|
|
19
|
+
/** Elapsed game-seconds since the clock started. */
|
|
20
|
+
now(): number;
|
|
21
|
+
beatDurationSec(): number;
|
|
22
|
+
bpm(): number;
|
|
23
|
+
}
|
|
24
|
+
export declare function createBeatClock(config: BeatClockConfig, onBeat?: (beatIndex: number) => void): BeatClock;
|
|
25
|
+
/** The absolute beat-time (game-seconds) that a press at `nowSec` quantizes to: the same instant if it lands on a beat boundary, else the next one. */
|
|
26
|
+
export declare function nextBeatTime(nowSec: number, beatDurationSec: number, epsilon?: number): number;
|
|
27
|
+
export interface BufferedAction<T> {
|
|
28
|
+
action: T;
|
|
29
|
+
bufferedAtSec: number;
|
|
30
|
+
fireAtSec: number;
|
|
31
|
+
}
|
|
32
|
+
export interface BeatInputBuffer<T> {
|
|
33
|
+
/** Buffer an action; it quantizes to fire on the next beat tick (or immediately if `nowSec` is already on a beat). Returns the resolved fire time. */
|
|
34
|
+
buffer(action: T, nowSec: number): number;
|
|
35
|
+
/** Drain and return every action whose fire time has arrived, in the order they were buffered. */
|
|
36
|
+
advance(nowSec: number): T[];
|
|
37
|
+
pendingCount(): number;
|
|
38
|
+
clear(): void;
|
|
39
|
+
}
|
|
40
|
+
export declare function createBeatInputBuffer<T>(beatDurationSec: number): BeatInputBuffer<T>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const MAX_BEAT_FIRES = 10_000;
|
|
2
|
+
export function createBeatClock(config, onBeat) {
|
|
3
|
+
const beatsPerBar = config.beatsPerBar !== undefined && config.beatsPerBar > 0 ? config.beatsPerBar : 4;
|
|
4
|
+
const beatDurationSec = 60 / config.bpm;
|
|
5
|
+
let elapsedSec = 0;
|
|
6
|
+
let lastBeatIndex = -1;
|
|
7
|
+
function toSnapshot() {
|
|
8
|
+
const beat = elapsedSec / beatDurationSec;
|
|
9
|
+
const beatIndex = Math.floor(beat);
|
|
10
|
+
return {
|
|
11
|
+
beat,
|
|
12
|
+
beatIndex,
|
|
13
|
+
bar: Math.floor(beatIndex / beatsPerBar),
|
|
14
|
+
beatInBar: ((beatIndex % beatsPerBar) + beatsPerBar) % beatsPerBar,
|
|
15
|
+
phase: beat - beatIndex,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function advance(gameDt) {
|
|
19
|
+
if (gameDt > 0)
|
|
20
|
+
elapsedSec += gameDt;
|
|
21
|
+
const snapshot = toSnapshot();
|
|
22
|
+
let fires = 0;
|
|
23
|
+
while (lastBeatIndex < snapshot.beatIndex && fires < MAX_BEAT_FIRES) {
|
|
24
|
+
lastBeatIndex += 1;
|
|
25
|
+
onBeat?.(lastBeatIndex);
|
|
26
|
+
fires += 1;
|
|
27
|
+
}
|
|
28
|
+
return snapshot;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
advance,
|
|
32
|
+
snapshot: toSnapshot,
|
|
33
|
+
now: () => elapsedSec,
|
|
34
|
+
beatDurationSec: () => beatDurationSec,
|
|
35
|
+
bpm: () => config.bpm,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const QUANTIZE_EPSILON = 1e-6;
|
|
39
|
+
/** The absolute beat-time (game-seconds) that a press at `nowSec` quantizes to: the same instant if it lands on a beat boundary, else the next one. */
|
|
40
|
+
export function nextBeatTime(nowSec, beatDurationSec, epsilon = QUANTIZE_EPSILON) {
|
|
41
|
+
const beatIndex = nowSec / beatDurationSec;
|
|
42
|
+
const rounded = Math.round(beatIndex);
|
|
43
|
+
if (Math.abs(beatIndex - rounded) <= epsilon)
|
|
44
|
+
return rounded * beatDurationSec;
|
|
45
|
+
return Math.ceil(beatIndex) * beatDurationSec;
|
|
46
|
+
}
|
|
47
|
+
export function createBeatInputBuffer(beatDurationSec) {
|
|
48
|
+
const pending = [];
|
|
49
|
+
return {
|
|
50
|
+
buffer(action, nowSec) {
|
|
51
|
+
const fireAtSec = nextBeatTime(nowSec, beatDurationSec);
|
|
52
|
+
pending.push({ action, bufferedAtSec: nowSec, fireAtSec });
|
|
53
|
+
return fireAtSec;
|
|
54
|
+
},
|
|
55
|
+
advance(nowSec) {
|
|
56
|
+
const due = [];
|
|
57
|
+
const remaining = [];
|
|
58
|
+
for (const entry of pending) {
|
|
59
|
+
if (entry.fireAtSec <= nowSec + QUANTIZE_EPSILON)
|
|
60
|
+
due.push(entry.action);
|
|
61
|
+
else
|
|
62
|
+
remaining.push(entry);
|
|
63
|
+
}
|
|
64
|
+
pending.length = 0;
|
|
65
|
+
pending.push(...remaining);
|
|
66
|
+
return due;
|
|
67
|
+
},
|
|
68
|
+
pendingCount: () => pending.length,
|
|
69
|
+
clear: () => {
|
|
70
|
+
pending.length = 0;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type CommitMode = "immediate" | "simultaneous" | "rewind";
|
|
2
|
+
export interface SubmittedAction<TAction> {
|
|
3
|
+
participant: string;
|
|
4
|
+
action: TAction;
|
|
5
|
+
}
|
|
6
|
+
export interface CommitOutcome<TAction> {
|
|
7
|
+
status: "committed" | "sealed" | "pending" | "rejected";
|
|
8
|
+
committed: SubmittedAction<TAction>[];
|
|
9
|
+
reason?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface CommitControllerConfig {
|
|
12
|
+
mode: CommitMode;
|
|
13
|
+
participants?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
export interface CommitController<TAction> {
|
|
16
|
+
readonly mode: CommitMode;
|
|
17
|
+
submit(participant: string, action: TAction): CommitOutcome<TAction>;
|
|
18
|
+
expected(participants: readonly string[]): void;
|
|
19
|
+
hasSubmitted(participant: string): boolean;
|
|
20
|
+
allReady(): boolean;
|
|
21
|
+
pending(): SubmittedAction<TAction>[];
|
|
22
|
+
reveal(): SubmittedAction<TAction>[];
|
|
23
|
+
commit(): SubmittedAction<TAction>[];
|
|
24
|
+
rewind(): SubmittedAction<TAction>[];
|
|
25
|
+
clear(): void;
|
|
26
|
+
}
|
|
27
|
+
export declare function createCommitController<TAction>(config: CommitControllerConfig): CommitController<TAction>;
|