@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.
Files changed (222) hide show
  1. package/CHANGELOG.md +167 -2
  2. package/dist/ai/crowd.d.ts +36 -0
  3. package/dist/ai/crowd.js +213 -0
  4. package/dist/ai/jobBoard.d.ts +49 -0
  5. package/dist/ai/jobBoard.js +155 -0
  6. package/dist/ai/spawnDirector.d.ts +56 -0
  7. package/dist/ai/spawnDirector.js +150 -0
  8. package/dist/ai/threat.d.ts +25 -0
  9. package/dist/ai/threat.js +67 -0
  10. package/dist/audio/audioFalloff.d.ts +38 -0
  11. package/dist/audio/audioFalloff.js +32 -0
  12. package/dist/board/laneBoard.d.ts +63 -0
  13. package/dist/board/laneBoard.js +114 -0
  14. package/dist/board/timelineBoard.d.ts +36 -0
  15. package/dist/board/timelineBoard.js +70 -0
  16. package/dist/cards/cardPile.d.ts +63 -0
  17. package/dist/cards/cardPile.js +166 -0
  18. package/dist/cards/modifierPipeline.d.ts +33 -0
  19. package/dist/cards/modifierPipeline.js +48 -0
  20. package/dist/combat/abilityKit.d.ts +42 -0
  21. package/dist/combat/abilityKit.js +129 -0
  22. package/dist/combat/animationState.d.ts +47 -0
  23. package/dist/combat/animationState.js +101 -0
  24. package/dist/combat/attackTags.d.ts +14 -0
  25. package/dist/combat/attackTags.js +26 -0
  26. package/dist/combat/breakMeters.d.ts +32 -0
  27. package/dist/combat/breakMeters.js +51 -0
  28. package/dist/combat/comboString.d.ts +35 -0
  29. package/dist/combat/comboString.js +59 -0
  30. package/dist/combat/death.d.ts +8 -0
  31. package/dist/combat/death.js +5 -2
  32. package/dist/combat/defensiveWindow.d.ts +34 -0
  33. package/dist/combat/defensiveWindow.js +66 -0
  34. package/dist/combat/downed.d.ts +35 -0
  35. package/dist/combat/downed.js +104 -0
  36. package/dist/combat/effects.d.ts +11 -0
  37. package/dist/combat/effects.js +22 -13
  38. package/dist/combat/hitReaction.d.ts +23 -0
  39. package/dist/combat/hitReaction.js +20 -0
  40. package/dist/combat/resistance.d.ts +13 -0
  41. package/dist/combat/resistance.js +46 -0
  42. package/dist/combat/telegraph.d.ts +27 -0
  43. package/dist/combat/telegraph.js +37 -0
  44. package/dist/crafting/crop.d.ts +59 -0
  45. package/dist/crafting/crop.js +181 -0
  46. package/dist/crafting/production.d.ts +64 -0
  47. package/dist/crafting/production.js +130 -0
  48. package/dist/crafting/recipe.d.ts +62 -0
  49. package/dist/crafting/recipe.js +101 -0
  50. package/dist/economy/sharedWallet.d.ts +28 -0
  51. package/dist/economy/sharedWallet.js +62 -0
  52. package/dist/economy/techTree.d.ts +40 -0
  53. package/dist/economy/techTree.js +66 -0
  54. package/dist/game/cosmetics.d.ts +25 -0
  55. package/dist/game/cosmetics.js +51 -0
  56. package/dist/game/defineGame.d.ts +2 -0
  57. package/dist/game/events.d.ts +62 -0
  58. package/dist/game/lootFilter.d.ts +33 -0
  59. package/dist/game/lootFilter.js +48 -0
  60. package/dist/game/ping.d.ts +67 -0
  61. package/dist/game/ping.js +86 -0
  62. package/dist/game/playableGame.d.ts +281 -4
  63. package/dist/game/race.d.ts +95 -0
  64. package/dist/game/race.js +216 -0
  65. package/dist/game/runDraft.d.ts +34 -0
  66. package/dist/game/runDraft.js +97 -0
  67. package/dist/game/social.d.ts +27 -0
  68. package/dist/game/social.js +16 -0
  69. package/dist/game/worldItem.d.ts +92 -0
  70. package/dist/game/worldItem.js +132 -0
  71. package/dist/input/axisInput.d.ts +43 -0
  72. package/dist/input/axisInput.js +70 -0
  73. package/dist/input/pointer.d.ts +24 -0
  74. package/dist/input/pointer.js +17 -0
  75. package/dist/interaction/contextMenu.d.ts +30 -0
  76. package/dist/interaction/contextMenu.js +22 -0
  77. package/dist/interaction/qte.d.ts +20 -0
  78. package/dist/interaction/qte.js +24 -0
  79. package/dist/interaction/skillCheck.d.ts +20 -0
  80. package/dist/interaction/skillCheck.js +26 -0
  81. package/dist/inventory/shapedGrid.d.ts +56 -0
  82. package/dist/inventory/shapedGrid.js +176 -0
  83. package/dist/inventory/storageTier.d.ts +52 -0
  84. package/dist/inventory/storageTier.js +77 -0
  85. package/dist/item/affix.d.ts +54 -0
  86. package/dist/item/affix.js +133 -0
  87. package/dist/item/durability.d.ts +51 -0
  88. package/dist/item/durability.js +78 -0
  89. package/dist/item/modularItem.d.ts +46 -0
  90. package/dist/item/modularItem.js +75 -0
  91. package/dist/meta/changelog.d.ts +1 -1
  92. package/dist/meta/changelog.js +35 -1
  93. package/dist/movement/dash.d.ts +37 -0
  94. package/dist/movement/dash.js +65 -0
  95. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  96. package/dist/multiplayer/combatSnapshot.js +89 -0
  97. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  98. package/dist/multiplayer/lagCompensation.js +154 -0
  99. package/dist/multiplayer/matchmaking.d.ts +33 -0
  100. package/dist/multiplayer/matchmaking.js +60 -0
  101. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  102. package/dist/multiplayer/simultaneousCommit.js +53 -0
  103. package/dist/nav/navGrid.d.ts +46 -0
  104. package/dist/nav/navGrid.js +248 -0
  105. package/dist/nav/pathFollow.d.ts +29 -0
  106. package/dist/nav/pathFollow.js +67 -0
  107. package/dist/physics/buoyancy.d.ts +49 -0
  108. package/dist/physics/buoyancy.js +112 -0
  109. package/dist/physics/carryable.d.ts +46 -0
  110. package/dist/physics/carryable.js +110 -0
  111. package/dist/physics/damageZones.d.ts +56 -0
  112. package/dist/physics/damageZones.js +107 -0
  113. package/dist/physics/forceVolume.d.ts +48 -0
  114. package/dist/physics/forceVolume.js +119 -0
  115. package/dist/physics/physicsWorld.d.ts +83 -0
  116. package/dist/physics/physicsWorld.js +261 -1
  117. package/dist/physics/ragdoll.d.ts +49 -0
  118. package/dist/physics/ragdoll.js +103 -0
  119. package/dist/physics/spatialGrid.d.ts +41 -0
  120. package/dist/physics/spatialGrid.js +138 -0
  121. package/dist/physics/structure.d.ts +83 -0
  122. package/dist/physics/structure.js +0 -0
  123. package/dist/physics/traversal.d.ts +75 -0
  124. package/dist/physics/traversal.js +138 -0
  125. package/dist/physics/vehicleBody.d.ts +93 -0
  126. package/dist/physics/vehicleBody.js +234 -0
  127. package/dist/runtime/gameContext.d.ts +64 -2
  128. package/dist/runtime/gameContext.js +154 -4
  129. package/dist/runtime/hostPersistence.d.ts +18 -0
  130. package/dist/runtime/hostPersistence.js +4 -0
  131. package/dist/runtime/persistenceScope.d.ts +32 -0
  132. package/dist/runtime/persistenceScope.js +64 -0
  133. package/dist/scene/autoTarget.d.ts +15 -0
  134. package/dist/scene/autoTarget.js +54 -0
  135. package/dist/scene/behaviors.d.ts +13 -1
  136. package/dist/scene/behaviors.js +3 -0
  137. package/dist/scene/captureCheck.d.ts +7 -0
  138. package/dist/scene/captureCheck.js +11 -0
  139. package/dist/scene/entityStore.d.ts +1 -1
  140. package/dist/scene/form.d.ts +40 -0
  141. package/dist/scene/form.js +56 -0
  142. package/dist/scene/mount.d.ts +62 -0
  143. package/dist/scene/mount.js +107 -0
  144. package/dist/scene/possession.d.ts +30 -0
  145. package/dist/scene/possession.js +51 -0
  146. package/dist/scene/roster.d.ts +25 -0
  147. package/dist/scene/roster.js +57 -0
  148. package/dist/scene/selection.d.ts +30 -0
  149. package/dist/scene/selection.js +38 -0
  150. package/dist/scene/stationClaim.d.ts +43 -0
  151. package/dist/scene/stationClaim.js +109 -0
  152. package/dist/sensor/frustumSensor.d.ts +60 -0
  153. package/dist/sensor/frustumSensor.js +104 -0
  154. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  155. package/dist/sensor/hiddenStateProbe.js +40 -0
  156. package/dist/sensor/recordingBuffer.d.ts +29 -0
  157. package/dist/sensor/recordingBuffer.js +61 -0
  158. package/dist/sensor/revealQuery.d.ts +22 -0
  159. package/dist/sensor/revealQuery.js +36 -0
  160. package/dist/session/contestedChannel.d.ts +36 -0
  161. package/dist/session/contestedChannel.js +100 -0
  162. package/dist/session/extraction.d.ts +54 -0
  163. package/dist/session/extraction.js +108 -0
  164. package/dist/session/ring.d.ts +36 -0
  165. package/dist/session/ring.js +88 -0
  166. package/dist/session/roundState.d.ts +55 -0
  167. package/dist/session/roundState.js +129 -0
  168. package/dist/stats/accumulatorMeter.d.ts +31 -0
  169. package/dist/stats/accumulatorMeter.js +81 -0
  170. package/dist/stats/eventMeter.d.ts +34 -0
  171. package/dist/stats/eventMeter.js +76 -0
  172. package/dist/stats/rollCheck.d.ts +15 -0
  173. package/dist/stats/rollCheck.js +15 -0
  174. package/dist/survival/decayMeter.d.ts +59 -0
  175. package/dist/survival/decayMeter.js +95 -0
  176. package/dist/survival/moodle.d.ts +48 -0
  177. package/dist/survival/moodle.js +91 -0
  178. package/dist/survival/regionHealth.d.ts +72 -0
  179. package/dist/survival/regionHealth.js +129 -0
  180. package/dist/tactics/predictiveQuery.d.ts +34 -0
  181. package/dist/tactics/predictiveQuery.js +44 -0
  182. package/dist/tactics/snapshot.d.ts +18 -0
  183. package/dist/tactics/snapshot.js +65 -0
  184. package/dist/tactics/surface.d.ts +52 -0
  185. package/dist/tactics/surface.js +135 -0
  186. package/dist/tactics/tacticalGrid.d.ts +57 -0
  187. package/dist/tactics/tacticalGrid.js +244 -0
  188. package/dist/time/beatClock.d.ts +40 -0
  189. package/dist/time/beatClock.js +73 -0
  190. package/dist/turn/commit.d.ts +27 -0
  191. package/dist/turn/commit.js +72 -0
  192. package/dist/turn/turnLoop.d.ts +58 -0
  193. package/dist/turn/turnLoop.js +216 -0
  194. package/dist/world/buildPermissions.d.ts +47 -0
  195. package/dist/world/buildPermissions.js +134 -0
  196. package/dist/world/carve.d.ts +105 -0
  197. package/dist/world/carve.js +175 -0
  198. package/dist/world/connectors.d.ts +40 -0
  199. package/dist/world/connectors.js +67 -0
  200. package/dist/world/envField.d.ts +71 -0
  201. package/dist/world/envField.js +96 -0
  202. package/dist/world/fog.d.ts +40 -0
  203. package/dist/world/fog.js +137 -0
  204. package/dist/world/markers.d.ts +52 -0
  205. package/dist/world/markers.js +96 -0
  206. package/dist/world/minimap.d.ts +42 -0
  207. package/dist/world/minimap.js +72 -0
  208. package/dist/world/placedStructureStore.d.ts +39 -0
  209. package/dist/world/placedStructureStore.js +95 -0
  210. package/dist/world/placementController.d.ts +48 -0
  211. package/dist/world/placementController.js +97 -0
  212. package/dist/world/realm.d.ts +61 -0
  213. package/dist/world/realm.js +66 -0
  214. package/dist/world/support.d.ts +27 -0
  215. package/dist/world/support.js +75 -0
  216. package/dist/world/terraform.d.ts +54 -0
  217. package/dist/world/terraform.js +186 -0
  218. package/dist/world/walls.d.ts +61 -0
  219. package/dist/world/walls.js +221 -0
  220. package/dist/world/weather.d.ts +92 -0
  221. package/dist/world/weather.js +169 -0
  222. package/package.json +1 -1
@@ -0,0 +1,89 @@
1
+ function cloneStats(stats) {
2
+ return { ...stats };
3
+ }
4
+ export function serializeBoard(args) {
5
+ return {
6
+ ownerId: args.ownerId,
7
+ units: args.units.map((unit) => ({ id: unit.id, stats: cloneStats(unit.stats) })),
8
+ stats: cloneStats(args.stats ?? {}),
9
+ seed: args.seed ?? 0,
10
+ capturedAt: args.capturedAt ?? 0,
11
+ };
12
+ }
13
+ export function cloneSnapshot(snapshot) {
14
+ return serializeBoard({
15
+ ownerId: snapshot.ownerId,
16
+ units: snapshot.units,
17
+ stats: snapshot.stats,
18
+ seed: snapshot.seed,
19
+ capturedAt: snapshot.capturedAt,
20
+ });
21
+ }
22
+ function mulberry32(seed) {
23
+ let a = seed >>> 0;
24
+ return () => {
25
+ a |= 0;
26
+ a = (a + 0x6d2b79f5) | 0;
27
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
28
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
29
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
30
+ };
31
+ }
32
+ function toLive(snapshot, rules) {
33
+ return snapshot.units.map((unit) => ({
34
+ id: unit.id,
35
+ attack: unit.stats[rules.attackStat] ?? 0,
36
+ health: unit.stats[rules.healthStat] ?? 0,
37
+ }));
38
+ }
39
+ export function replayCombat(a, b, rules) {
40
+ const maxRounds = rules.maxRounds ?? 100;
41
+ const critChance = rules.critChance ?? 0;
42
+ const critMultiplier = rules.critMultiplier ?? 2;
43
+ const prng = mulberry32((a.seed ^ (b.seed * 0x9e3779b1)) >>> 0);
44
+ const liveA = toLive(a, rules);
45
+ const liveB = toLive(b, rules);
46
+ const blows = [];
47
+ let indexA = 0;
48
+ let indexB = 0;
49
+ let rounds = 0;
50
+ const strike = (round, attacker, attackers, attackerIndex, defenders) => {
51
+ const source = attackers[attackerIndex];
52
+ const target = defenders[0];
53
+ const crit = critChance > 0 && prng() < critChance;
54
+ const damage = crit ? Math.round(source.attack * critMultiplier) : source.attack;
55
+ target.health -= damage;
56
+ blows.push({
57
+ round,
58
+ attacker,
59
+ attackerUnit: source.id,
60
+ defenderUnit: target.id,
61
+ damage,
62
+ crit,
63
+ defenderHealthAfter: target.health,
64
+ });
65
+ if (target.health <= 0)
66
+ defenders.shift();
67
+ };
68
+ while (liveA.length > 0 && liveB.length > 0 && rounds < maxRounds) {
69
+ rounds += 1;
70
+ strike(rounds, "a", liveA, indexA % liveA.length, liveB);
71
+ if (liveB.length === 0)
72
+ break;
73
+ strike(rounds, "b", liveB, indexB % liveB.length, liveA);
74
+ indexA += 1;
75
+ indexB += 1;
76
+ }
77
+ const winner = liveA.length > 0 && liveB.length === 0
78
+ ? "a"
79
+ : liveB.length > 0 && liveA.length === 0
80
+ ? "b"
81
+ : "draw";
82
+ return {
83
+ winner,
84
+ rounds,
85
+ blows,
86
+ survivorsA: liveA.map((u) => u.id),
87
+ survivorsB: liveB.map((u) => u.id),
88
+ };
89
+ }
@@ -0,0 +1,43 @@
1
+ export interface Vec3 {
2
+ x: number;
3
+ y: number;
4
+ z: number;
5
+ }
6
+ export interface PositionSample extends Vec3 {
7
+ t: number;
8
+ }
9
+ export interface PositionHistoryConfig {
10
+ historyMs: number;
11
+ maxSamples?: number;
12
+ }
13
+ export declare class PositionHistory {
14
+ private readonly historyMs;
15
+ private readonly maxSamples;
16
+ private readonly tracks;
17
+ constructor(config: PositionHistoryConfig);
18
+ record(entityId: string, t: number, position: Vec3): void;
19
+ private trim;
20
+ entities(): string[];
21
+ samples(entityId: string): readonly PositionSample[];
22
+ sampleAt(entityId: string, t: number): Vec3 | null;
23
+ prune(now: number): void;
24
+ forget(entityId: string): void;
25
+ }
26
+ export declare function createPositionHistory(config: PositionHistoryConfig): PositionHistory;
27
+ export declare function rewindTimestamp(nowMs: number, rttMs: number, interpDelayMs: number): number;
28
+ export interface HitscanRay {
29
+ origin: Vec3;
30
+ direction: Vec3;
31
+ maxDistance?: number;
32
+ }
33
+ export interface HitscanTarget {
34
+ entityId: string;
35
+ radius: number;
36
+ }
37
+ export interface HitscanHit {
38
+ entityId: string;
39
+ distance: number;
40
+ point: Vec3;
41
+ }
42
+ export declare function raySphereDistance(ray: HitscanRay, center: Vec3, radius: number): number | null;
43
+ export declare function resolveHitscan(history: PositionHistory, targets: readonly HitscanTarget[], ray: HitscanRay, atMs: number): HitscanHit | null;
@@ -0,0 +1,154 @@
1
+ const DEFAULT_MAX_SAMPLES = 256;
2
+ export class PositionHistory {
3
+ historyMs;
4
+ maxSamples;
5
+ tracks = new Map();
6
+ constructor(config) {
7
+ if (!(config.historyMs > 0)) {
8
+ throw new RangeError(`historyMs must be positive, got ${config.historyMs}`);
9
+ }
10
+ this.historyMs = config.historyMs;
11
+ this.maxSamples = config.maxSamples ?? DEFAULT_MAX_SAMPLES;
12
+ }
13
+ record(entityId, t, position) {
14
+ let track = this.tracks.get(entityId);
15
+ if (track === undefined) {
16
+ track = [];
17
+ this.tracks.set(entityId, track);
18
+ }
19
+ const sample = { t, x: position.x, y: position.y, z: position.z };
20
+ const last = track[track.length - 1];
21
+ if (last !== undefined && t <= last.t) {
22
+ if (t === last.t) {
23
+ track[track.length - 1] = sample;
24
+ return;
25
+ }
26
+ let index = track.length;
27
+ while (index > 0 && track[index - 1].t > t)
28
+ index -= 1;
29
+ track.splice(index, 0, sample);
30
+ }
31
+ else {
32
+ track.push(sample);
33
+ }
34
+ this.trim(track, t);
35
+ }
36
+ trim(track, newest) {
37
+ const cutoff = newest - this.historyMs;
38
+ let drop = 0;
39
+ while (drop < track.length - 1 && track[drop].t < cutoff)
40
+ drop += 1;
41
+ if (drop > 0)
42
+ track.splice(0, drop);
43
+ if (track.length > this.maxSamples) {
44
+ track.splice(0, track.length - this.maxSamples);
45
+ }
46
+ }
47
+ entities() {
48
+ return [...this.tracks.keys()];
49
+ }
50
+ samples(entityId) {
51
+ return this.tracks.get(entityId) ?? [];
52
+ }
53
+ sampleAt(entityId, t) {
54
+ const track = this.tracks.get(entityId);
55
+ if (track === undefined || track.length === 0)
56
+ return null;
57
+ const first = track[0];
58
+ const last = track[track.length - 1];
59
+ if (t <= first.t)
60
+ return { x: first.x, y: first.y, z: first.z };
61
+ if (t >= last.t)
62
+ return { x: last.x, y: last.y, z: last.z };
63
+ let lo = 0;
64
+ let hi = track.length - 1;
65
+ while (hi - lo > 1) {
66
+ const mid = (lo + hi) >> 1;
67
+ if (track[mid].t <= t)
68
+ lo = mid;
69
+ else
70
+ hi = mid;
71
+ }
72
+ const a = track[lo];
73
+ const b = track[hi];
74
+ const span = b.t - a.t;
75
+ const alpha = span === 0 ? 0 : (t - a.t) / span;
76
+ return {
77
+ x: a.x + (b.x - a.x) * alpha,
78
+ y: a.y + (b.y - a.y) * alpha,
79
+ z: a.z + (b.z - a.z) * alpha,
80
+ };
81
+ }
82
+ prune(now) {
83
+ for (const [entityId, track] of this.tracks) {
84
+ this.trim(track, now);
85
+ if (track.length === 0)
86
+ this.tracks.delete(entityId);
87
+ }
88
+ }
89
+ forget(entityId) {
90
+ this.tracks.delete(entityId);
91
+ }
92
+ }
93
+ export function createPositionHistory(config) {
94
+ return new PositionHistory(config);
95
+ }
96
+ export function rewindTimestamp(nowMs, rttMs, interpDelayMs) {
97
+ return nowMs - rttMs / 2 - interpDelayMs;
98
+ }
99
+ function normalize(v) {
100
+ const len = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
101
+ if (len === 0)
102
+ return null;
103
+ return { x: v.x / len, y: v.y / len, z: v.z / len };
104
+ }
105
+ export function raySphereDistance(ray, center, radius) {
106
+ const dir = normalize(ray.direction);
107
+ if (dir === null)
108
+ return null;
109
+ const ox = ray.origin.x - center.x;
110
+ const oy = ray.origin.y - center.y;
111
+ const oz = ray.origin.z - center.z;
112
+ const b = ox * dir.x + oy * dir.y + oz * dir.z;
113
+ const c = ox * ox + oy * oy + oz * oz - radius * radius;
114
+ if (c > 0 && b > 0)
115
+ return null;
116
+ const disc = b * b - c;
117
+ if (disc < 0)
118
+ return null;
119
+ const sqrtDisc = Math.sqrt(disc);
120
+ const tNear = -b - sqrtDisc;
121
+ const t = tNear >= 0 ? tNear : -b + sqrtDisc;
122
+ if (t < 0)
123
+ return null;
124
+ const maxDistance = ray.maxDistance ?? Number.POSITIVE_INFINITY;
125
+ if (t > maxDistance)
126
+ return null;
127
+ return t;
128
+ }
129
+ export function resolveHitscan(history, targets, ray, atMs) {
130
+ const dir = normalize(ray.direction);
131
+ if (dir === null)
132
+ return null;
133
+ let best = null;
134
+ for (const target of targets) {
135
+ const center = history.sampleAt(target.entityId, atMs);
136
+ if (center === null)
137
+ continue;
138
+ const distance = raySphereDistance(ray, center, target.radius);
139
+ if (distance === null)
140
+ continue;
141
+ if (best === null || distance < best.distance) {
142
+ best = {
143
+ entityId: target.entityId,
144
+ distance,
145
+ point: {
146
+ x: ray.origin.x + dir.x * distance,
147
+ y: ray.origin.y + dir.y * distance,
148
+ z: ray.origin.z + dir.z * distance,
149
+ },
150
+ };
151
+ }
152
+ }
153
+ return best;
154
+ }
@@ -0,0 +1,33 @@
1
+ export type SessionVisibility = "public" | "private";
2
+ export type SessionStatus = "open" | "running" | "closed";
3
+ export interface SessionListing {
4
+ serverId: string;
5
+ gameId: string;
6
+ status: SessionStatus;
7
+ visibility: SessionVisibility;
8
+ memberCount: number;
9
+ slotsPerServer: number;
10
+ label?: string;
11
+ mode?: string;
12
+ joinCode?: string;
13
+ tags?: readonly string[];
14
+ updatedAt: number;
15
+ }
16
+ export interface MatchFilter {
17
+ mode?: string;
18
+ tags?: readonly string[];
19
+ query?: string;
20
+ notFull?: boolean;
21
+ status?: readonly SessionStatus[];
22
+ includePrivate?: boolean;
23
+ }
24
+ export interface BrowseOptions {
25
+ limit?: number;
26
+ }
27
+ export declare function normalizeJoinCode(code: string): string;
28
+ export declare function generateJoinCode(random: () => number, length?: number): string;
29
+ export declare function hasSpace(listing: SessionListing): boolean;
30
+ export declare function matchesFilter(listing: SessionListing, filter: MatchFilter): boolean;
31
+ export declare function browseSessions(listings: readonly SessionListing[], filter?: MatchFilter, options?: BrowseOptions): SessionListing[];
32
+ export declare function findByJoinCode(listings: readonly SessionListing[], code: string): SessionListing | null;
33
+ export declare function quickMatch(listings: readonly SessionListing[], filter?: MatchFilter): SessionListing | null;
@@ -0,0 +1,60 @@
1
+ export function normalizeJoinCode(code) {
2
+ return code.toUpperCase().replace(/[^A-Z0-9]/g, "");
3
+ }
4
+ const CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
5
+ export function generateJoinCode(random, length = 6) {
6
+ let code = "";
7
+ for (let i = 0; i < length; i += 1) {
8
+ const index = Math.min(CODE_ALPHABET.length - 1, Math.floor(random() * CODE_ALPHABET.length));
9
+ code += CODE_ALPHABET[index];
10
+ }
11
+ return code;
12
+ }
13
+ export function hasSpace(listing) {
14
+ return listing.memberCount < listing.slotsPerServer;
15
+ }
16
+ export function matchesFilter(listing, filter) {
17
+ if (listing.visibility === "private" && filter.includePrivate !== true)
18
+ return false;
19
+ const statuses = filter.status ?? ["open", "running"];
20
+ if (!statuses.includes(listing.status))
21
+ return false;
22
+ if (filter.mode !== undefined && listing.mode !== filter.mode)
23
+ return false;
24
+ if (filter.notFull === true && !hasSpace(listing))
25
+ return false;
26
+ if (filter.tags !== undefined && filter.tags.length > 0) {
27
+ const owned = new Set(listing.tags ?? []);
28
+ if (!filter.tags.every((tag) => owned.has(tag)))
29
+ return false;
30
+ }
31
+ if (filter.query !== undefined && filter.query.length > 0) {
32
+ const haystack = (listing.label ?? "").toLowerCase();
33
+ if (!haystack.includes(filter.query.toLowerCase()))
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+ export function browseSessions(listings, filter = {}, options = {}) {
39
+ const matched = listings
40
+ .filter((listing) => matchesFilter(listing, filter))
41
+ .sort((a, b) => b.memberCount - a.memberCount || b.updatedAt - a.updatedAt);
42
+ return options.limit !== undefined ? matched.slice(0, options.limit) : matched;
43
+ }
44
+ export function findByJoinCode(listings, code) {
45
+ const normalized = normalizeJoinCode(code);
46
+ if (normalized.length === 0)
47
+ return null;
48
+ for (const listing of listings) {
49
+ if (listing.joinCode !== undefined && normalizeJoinCode(listing.joinCode) === normalized) {
50
+ return listing;
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ export function quickMatch(listings, filter = {}) {
56
+ const candidates = listings
57
+ .filter((listing) => matchesFilter(listing, { ...filter, notFull: true }))
58
+ .sort((a, b) => b.memberCount - a.memberCount || a.updatedAt - b.updatedAt);
59
+ return candidates[0] ?? null;
60
+ }
@@ -0,0 +1,34 @@
1
+ export type CommitPhase = "collecting" | "revealed";
2
+ export interface SealedCommit<TAction> {
3
+ playerId: string;
4
+ action: TAction;
5
+ sealedAt: number;
6
+ }
7
+ export interface CommitRoundConfig {
8
+ participants: readonly string[];
9
+ allowReseal?: boolean;
10
+ }
11
+ export type SealResult = {
12
+ ok: true;
13
+ allSealed: boolean;
14
+ } | {
15
+ ok: false;
16
+ reason: "unknown_participant" | "already_sealed" | "already_revealed";
17
+ };
18
+ export declare class CommitRound<TAction> {
19
+ private readonly order;
20
+ private readonly participants;
21
+ private readonly allowReseal;
22
+ private readonly sealed;
23
+ private revealedFlag;
24
+ constructor(config: CommitRoundConfig);
25
+ seal(playerId: string, action: TAction, at: number): SealResult;
26
+ hasSealed(playerId: string): boolean;
27
+ sealedCount(): number;
28
+ pending(): string[];
29
+ allSealed(): boolean;
30
+ get phase(): CommitPhase;
31
+ reveal(): readonly SealedCommit<TAction>[] | null;
32
+ }
33
+ export declare function createCommitRound<TAction>(config: CommitRoundConfig): CommitRound<TAction>;
34
+ export declare function resolveCommits<TAction, TResult>(reveals: readonly SealedCommit<TAction>[], resolver: (ordered: readonly SealedCommit<TAction>[]) => TResult): TResult;
@@ -0,0 +1,53 @@
1
+ export class CommitRound {
2
+ order;
3
+ participants;
4
+ allowReseal;
5
+ sealed = new Map();
6
+ revealedFlag = false;
7
+ constructor(config) {
8
+ if (config.participants.length === 0) {
9
+ throw new RangeError("a commit round needs at least one participant");
10
+ }
11
+ this.order = [...config.participants];
12
+ this.participants = new Set(config.participants);
13
+ this.allowReseal = config.allowReseal ?? false;
14
+ }
15
+ seal(playerId, action, at) {
16
+ if (this.revealedFlag)
17
+ return { ok: false, reason: "already_revealed" };
18
+ if (!this.participants.has(playerId))
19
+ return { ok: false, reason: "unknown_participant" };
20
+ if (this.sealed.has(playerId) && !this.allowReseal) {
21
+ return { ok: false, reason: "already_sealed" };
22
+ }
23
+ this.sealed.set(playerId, { playerId, action, sealedAt: at });
24
+ return { ok: true, allSealed: this.allSealed() };
25
+ }
26
+ hasSealed(playerId) {
27
+ return this.sealed.has(playerId);
28
+ }
29
+ sealedCount() {
30
+ return this.sealed.size;
31
+ }
32
+ pending() {
33
+ return this.order.filter((id) => !this.sealed.has(id));
34
+ }
35
+ allSealed() {
36
+ return this.sealed.size === this.participants.size;
37
+ }
38
+ get phase() {
39
+ return this.revealedFlag ? "revealed" : "collecting";
40
+ }
41
+ reveal() {
42
+ if (!this.allSealed())
43
+ return null;
44
+ this.revealedFlag = true;
45
+ return this.order.map((id) => this.sealed.get(id));
46
+ }
47
+ }
48
+ export function createCommitRound(config) {
49
+ return new CommitRound(config);
50
+ }
51
+ export function resolveCommits(reveals, resolver) {
52
+ return resolver(reveals);
53
+ }
@@ -0,0 +1,46 @@
1
+ import type { Aabb } from "../world/geometry.js";
2
+ export type NavPoint = readonly [number, number];
3
+ export interface NavGridConfig {
4
+ bounds: Aabb;
5
+ cellSize: number;
6
+ /** Allow 8-neighbour movement (no corner cutting). Default true. */
7
+ diagonal?: boolean;
8
+ }
9
+ export interface NavCell {
10
+ col: number;
11
+ row: number;
12
+ }
13
+ export interface FindPathOptions {
14
+ /** String-pull the raw grid path into straight runs where line-of-sight is clear. Default true. */
15
+ smooth?: boolean;
16
+ /** Diameter of the mover in world units — blocks cells within this margin of an obstacle. Default 0. */
17
+ clearance?: number;
18
+ }
19
+ export interface NavGrid {
20
+ readonly cols: number;
21
+ readonly rows: number;
22
+ readonly cellSize: number;
23
+ readonly bounds: Aabb;
24
+ readonly diagonal: boolean;
25
+ isWalkable(col: number, row: number): boolean;
26
+ setWalkable(col: number, row: number, walkable: boolean): void;
27
+ blockAabb(aabb: Aabb): void;
28
+ clearAabb(aabb: Aabb): void;
29
+ reset(walkable: boolean): void;
30
+ inBounds(col: number, row: number): boolean;
31
+ cellAt(point: NavPoint): NavCell;
32
+ center(col: number, row: number): NavPoint;
33
+ /** Nearest walkable cell to a point (spiral search); null if the whole grid is blocked. */
34
+ nearestWalkable(point: NavPoint): NavCell | null;
35
+ /** Unobstructed straight line between two world points (supercover cell walk). */
36
+ lineOfSight(from: NavPoint, to: NavPoint): boolean;
37
+ }
38
+ export declare function createNavGrid(config: NavGridConfig): NavGrid;
39
+ /**
40
+ * A* over the walkable grid. Returns a polyline of world-space `[x, z]` waypoints
41
+ * from `from` to `to`, or `null` when no route exists. Blocked start/goal snap to
42
+ * the nearest walkable cell so a click on an obstacle still routes to its edge.
43
+ */
44
+ export declare function findPath(grid: NavGrid, from: NavPoint, to: NavPoint, options?: FindPathOptions): NavPoint[] | null;
45
+ /** Remove waypoints the mover can skip because it has clear line-of-sight past them. */
46
+ export declare function smoothPath(grid: NavGrid, points: readonly NavPoint[]): NavPoint[];