@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.
Files changed (244) hide show
  1. package/CHANGELOG.md +191 -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 +5 -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 +291 -5
  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 +53 -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 +219 -0
  116. package/dist/physics/physicsWorld.js +749 -0
  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 +66 -2
  128. package/dist/runtime/gameContext.js +157 -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/assetCatalog.d.ts +13 -0
  134. package/dist/scene/autoTarget.d.ts +15 -0
  135. package/dist/scene/autoTarget.js +54 -0
  136. package/dist/scene/behaviors.d.ts +13 -1
  137. package/dist/scene/behaviors.js +3 -0
  138. package/dist/scene/captureCheck.d.ts +7 -0
  139. package/dist/scene/captureCheck.js +11 -0
  140. package/dist/scene/entityStore.d.ts +7 -1
  141. package/dist/scene/entityStore.js +15 -1
  142. package/dist/scene/form.d.ts +40 -0
  143. package/dist/scene/form.js +56 -0
  144. package/dist/scene/mount.d.ts +62 -0
  145. package/dist/scene/mount.js +107 -0
  146. package/dist/scene/possession.d.ts +30 -0
  147. package/dist/scene/possession.js +51 -0
  148. package/dist/scene/roster.d.ts +25 -0
  149. package/dist/scene/roster.js +57 -0
  150. package/dist/scene/selection.d.ts +30 -0
  151. package/dist/scene/selection.js +38 -0
  152. package/dist/scene/stationClaim.d.ts +43 -0
  153. package/dist/scene/stationClaim.js +109 -0
  154. package/dist/sensor/frustumSensor.d.ts +60 -0
  155. package/dist/sensor/frustumSensor.js +104 -0
  156. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  157. package/dist/sensor/hiddenStateProbe.js +40 -0
  158. package/dist/sensor/recordingBuffer.d.ts +29 -0
  159. package/dist/sensor/recordingBuffer.js +61 -0
  160. package/dist/sensor/revealQuery.d.ts +22 -0
  161. package/dist/sensor/revealQuery.js +36 -0
  162. package/dist/session/contestedChannel.d.ts +36 -0
  163. package/dist/session/contestedChannel.js +100 -0
  164. package/dist/session/extraction.d.ts +54 -0
  165. package/dist/session/extraction.js +108 -0
  166. package/dist/session/ring.d.ts +36 -0
  167. package/dist/session/ring.js +88 -0
  168. package/dist/session/roundState.d.ts +55 -0
  169. package/dist/session/roundState.js +129 -0
  170. package/dist/stats/accumulatorMeter.d.ts +31 -0
  171. package/dist/stats/accumulatorMeter.js +81 -0
  172. package/dist/stats/eventMeter.d.ts +34 -0
  173. package/dist/stats/eventMeter.js +76 -0
  174. package/dist/stats/rollCheck.d.ts +15 -0
  175. package/dist/stats/rollCheck.js +15 -0
  176. package/dist/survival/decayMeter.d.ts +59 -0
  177. package/dist/survival/decayMeter.js +95 -0
  178. package/dist/survival/moodle.d.ts +48 -0
  179. package/dist/survival/moodle.js +91 -0
  180. package/dist/survival/regionHealth.d.ts +72 -0
  181. package/dist/survival/regionHealth.js +129 -0
  182. package/dist/tactics/predictiveQuery.d.ts +34 -0
  183. package/dist/tactics/predictiveQuery.js +44 -0
  184. package/dist/tactics/snapshot.d.ts +18 -0
  185. package/dist/tactics/snapshot.js +65 -0
  186. package/dist/tactics/surface.d.ts +52 -0
  187. package/dist/tactics/surface.js +135 -0
  188. package/dist/tactics/tacticalGrid.d.ts +57 -0
  189. package/dist/tactics/tacticalGrid.js +244 -0
  190. package/dist/time/beatClock.d.ts +40 -0
  191. package/dist/time/beatClock.js +73 -0
  192. package/dist/time/simClock.d.ts +62 -0
  193. package/dist/time/simClock.js +132 -0
  194. package/dist/turn/commit.d.ts +27 -0
  195. package/dist/turn/commit.js +72 -0
  196. package/dist/turn/turnLoop.d.ts +58 -0
  197. package/dist/turn/turnLoop.js +216 -0
  198. package/dist/world/buildPermissions.d.ts +47 -0
  199. package/dist/world/buildPermissions.js +134 -0
  200. package/dist/world/buildingIndex.d.ts +16 -0
  201. package/dist/world/buildingIndex.js +63 -0
  202. package/dist/world/buildings.d.ts +98 -0
  203. package/dist/world/buildings.js +320 -0
  204. package/dist/world/carve.d.ts +105 -0
  205. package/dist/world/carve.js +175 -0
  206. package/dist/world/connectors.d.ts +40 -0
  207. package/dist/world/connectors.js +67 -0
  208. package/dist/world/envField.d.ts +71 -0
  209. package/dist/world/envField.js +96 -0
  210. package/dist/world/features.d.ts +105 -1
  211. package/dist/world/features.js +95 -0
  212. package/dist/world/fog.d.ts +40 -0
  213. package/dist/world/fog.js +137 -0
  214. package/dist/world/markers.d.ts +52 -0
  215. package/dist/world/markers.js +96 -0
  216. package/dist/world/minimap.d.ts +42 -0
  217. package/dist/world/minimap.js +72 -0
  218. package/dist/world/placedStructureStore.d.ts +39 -0
  219. package/dist/world/placedStructureStore.js +95 -0
  220. package/dist/world/placementController.d.ts +48 -0
  221. package/dist/world/placementController.js +97 -0
  222. package/dist/world/realm.d.ts +61 -0
  223. package/dist/world/realm.js +66 -0
  224. package/dist/world/regions.d.ts +66 -0
  225. package/dist/world/regions.js +158 -0
  226. package/dist/world/scatter.d.ts +24 -0
  227. package/dist/world/scatter.js +126 -0
  228. package/dist/world/scatterItems.d.ts +38 -0
  229. package/dist/world/scatterItems.js +85 -0
  230. package/dist/world/support.d.ts +27 -0
  231. package/dist/world/support.js +75 -0
  232. package/dist/world/terraform.d.ts +54 -0
  233. package/dist/world/terraform.js +186 -0
  234. package/dist/world/terrain.d.ts +54 -0
  235. package/dist/world/terrain.js +181 -0
  236. package/dist/world/walls.d.ts +61 -0
  237. package/dist/world/walls.js +221 -0
  238. package/dist/world/water.d.ts +27 -0
  239. package/dist/world/water.js +125 -0
  240. package/dist/world/weather.d.ts +92 -0
  241. package/dist/world/weather.js +169 -0
  242. package/dist/world/wind.d.ts +17 -0
  243. package/dist/world/wind.js +40 -0
  244. package/package.json +1 -1
@@ -0,0 +1,40 @@
1
+ export interface FogBounds {
2
+ minX: number;
3
+ minZ: number;
4
+ maxX: number;
5
+ maxZ: number;
6
+ }
7
+ export interface FogConfig {
8
+ bounds: FogBounds;
9
+ cellSize: number;
10
+ /** Cells revealed on construction (start-of-run known area). */
11
+ revealed?: readonly number[];
12
+ }
13
+ export interface FogCells {
14
+ cols: number;
15
+ rows: number;
16
+ minX: number;
17
+ minZ: number;
18
+ cellSize: number;
19
+ /** Row-major flags; index = row * cols + col. */
20
+ revealed: readonly boolean[];
21
+ revealedCount: number;
22
+ }
23
+ /**
24
+ * Reveal-on-event fog of war over a fixed grid. Walking (`revealAlong`) and
25
+ * digging/acting (`reveal`) clear cells; once revealed a cell stays revealed.
26
+ * Pure and renderer-free — the shell/react map draws `cells()`.
27
+ */
28
+ export interface FogField {
29
+ reveal(x: number, z: number, radius?: number): number;
30
+ revealAlong(from: readonly [number, number], to: readonly [number, number], radius?: number): number;
31
+ isRevealed(x: number, z: number): boolean;
32
+ isRevealedCell(col: number, row: number): boolean;
33
+ revealedCount(): number;
34
+ cellCount(): number;
35
+ fraction(): number;
36
+ cells(): FogCells;
37
+ reset(): void;
38
+ subscribe(listener: () => void): () => void;
39
+ }
40
+ export declare function createFogField(config: FogConfig): FogField;
@@ -0,0 +1,137 @@
1
+ function clampInt(value, min, max) {
2
+ const rounded = Math.floor(value);
3
+ return rounded < min ? min : rounded > max ? max : rounded;
4
+ }
5
+ export function createFogField(config) {
6
+ const { minX, minZ, maxX, maxZ } = config.bounds;
7
+ const cellSize = config.cellSize;
8
+ if (cellSize <= 0)
9
+ throw new Error("createFogField: cellSize must be positive");
10
+ const cols = Math.max(1, Math.ceil((maxX - minX) / cellSize));
11
+ const rows = Math.max(1, Math.ceil((maxZ - minZ) / cellSize));
12
+ const total = cols * rows;
13
+ const revealed = new Array(total).fill(false);
14
+ let revealedCount = 0;
15
+ const listeners = new Set();
16
+ let snapshotCache = null;
17
+ function colOf(x) {
18
+ return clampInt((x - minX) / cellSize, 0, cols - 1);
19
+ }
20
+ function rowOf(z) {
21
+ return clampInt((z - minZ) / cellSize, 0, rows - 1);
22
+ }
23
+ function markCell(col, row) {
24
+ if (col < 0 || col >= cols || row < 0 || row >= rows)
25
+ return false;
26
+ const index = row * cols + col;
27
+ if (revealed[index])
28
+ return false;
29
+ revealed[index] = true;
30
+ revealedCount += 1;
31
+ return true;
32
+ }
33
+ function notify(changed) {
34
+ if (changed === 0)
35
+ return;
36
+ snapshotCache = null;
37
+ for (const listener of listeners)
38
+ listener();
39
+ }
40
+ function revealAt(x, z, radius) {
41
+ const centerCol = colOf(x);
42
+ const centerRow = rowOf(z);
43
+ let changed = markCell(centerCol, centerRow) ? 1 : 0;
44
+ if (radius <= 0)
45
+ return changed;
46
+ const span = Math.ceil(radius / cellSize);
47
+ const radiusSq = radius * radius;
48
+ for (let dr = -span; dr <= span; dr += 1) {
49
+ for (let dc = -span; dc <= span; dc += 1) {
50
+ if (dc === 0 && dr === 0)
51
+ continue;
52
+ const cellX = minX + (centerCol + dc + 0.5) * cellSize;
53
+ const cellZ = minZ + (centerRow + dr + 0.5) * cellSize;
54
+ const dx = cellX - x;
55
+ const dz = cellZ - z;
56
+ if (dx * dx + dz * dz > radiusSq)
57
+ continue;
58
+ if (markCell(centerCol + dc, centerRow + dr))
59
+ changed += 1;
60
+ }
61
+ }
62
+ return changed;
63
+ }
64
+ if (config.revealed !== undefined) {
65
+ for (const index of config.revealed) {
66
+ if (index >= 0 && index < total && !revealed[index]) {
67
+ revealed[index] = true;
68
+ revealedCount += 1;
69
+ }
70
+ }
71
+ snapshotCache = null;
72
+ }
73
+ return {
74
+ reveal(x, z, radius = 0) {
75
+ const changed = revealAt(x, z, radius);
76
+ notify(changed);
77
+ return changed;
78
+ },
79
+ revealAlong(from, to, radius = cellSize) {
80
+ const dx = to[0] - from[0];
81
+ const dz = to[1] - from[1];
82
+ const distance = Math.hypot(dx, dz);
83
+ const steps = Math.max(1, Math.ceil(distance / cellSize));
84
+ let changed = 0;
85
+ for (let step = 0; step <= steps; step += 1) {
86
+ const t = step / steps;
87
+ changed += revealAt(from[0] + dx * t, from[1] + dz * t, radius);
88
+ }
89
+ notify(changed);
90
+ return changed;
91
+ },
92
+ isRevealed(x, z) {
93
+ return revealed[rowOf(z) * cols + colOf(x)] === true;
94
+ },
95
+ isRevealedCell(col, row) {
96
+ if (col < 0 || col >= cols || row < 0 || row >= rows)
97
+ return false;
98
+ return revealed[row * cols + col] === true;
99
+ },
100
+ revealedCount() {
101
+ return revealedCount;
102
+ },
103
+ cellCount() {
104
+ return total;
105
+ },
106
+ fraction() {
107
+ return total === 0 ? 0 : revealedCount / total;
108
+ },
109
+ cells() {
110
+ if (snapshotCache === null) {
111
+ snapshotCache = {
112
+ cols,
113
+ rows,
114
+ minX,
115
+ minZ,
116
+ cellSize,
117
+ revealed: revealed.slice(),
118
+ revealedCount,
119
+ };
120
+ }
121
+ return snapshotCache;
122
+ },
123
+ reset() {
124
+ if (revealedCount === 0)
125
+ return;
126
+ revealed.fill(false);
127
+ revealedCount = 0;
128
+ notify(1);
129
+ },
130
+ subscribe(listener) {
131
+ listeners.add(listener);
132
+ return () => {
133
+ listeners.delete(listener);
134
+ };
135
+ },
136
+ };
137
+ }
@@ -0,0 +1,52 @@
1
+ export type MarkerPosition = readonly [number, number, number];
2
+ export interface MapMarker<TMeta = unknown> {
3
+ id: string;
4
+ kind: string;
5
+ position: MarkerPosition;
6
+ label?: string;
7
+ owner?: string;
8
+ createdAt: number;
9
+ expiresAt?: number;
10
+ meta?: TMeta;
11
+ }
12
+ export interface MarkerInput<TMeta = unknown> {
13
+ id?: string;
14
+ kind: string;
15
+ position: MarkerPosition;
16
+ label?: string;
17
+ owner?: string;
18
+ createdAt?: number;
19
+ expiresAt?: number;
20
+ meta?: TMeta;
21
+ }
22
+ export interface MarkerQuery {
23
+ kind?: string;
24
+ owner?: string;
25
+ near?: MarkerPosition;
26
+ radius?: number;
27
+ }
28
+ /**
29
+ * Visual descriptor for a marker kind. Games supply their own palette; the
30
+ * engine ships `DEFAULT_MARKER_KINDS` as a content-agnostic starting set that
31
+ * the react minimap/compass read for colors and glyphs.
32
+ */
33
+ export interface MarkerKindStyle {
34
+ id: string;
35
+ color: string;
36
+ glyph: string;
37
+ priority: number;
38
+ }
39
+ export declare const DEFAULT_MARKER_KINDS: Record<string, MarkerKindStyle>;
40
+ export declare function markerKindStyle(kind: string, styles?: Record<string, MarkerKindStyle>): MarkerKindStyle;
41
+ export interface MarkerSet<TMeta = unknown> {
42
+ add(marker: MarkerInput<TMeta>): string;
43
+ remove(id: string): boolean;
44
+ get(id: string): MapMarker<TMeta> | null;
45
+ list(): readonly MapMarker<TMeta>[];
46
+ query(query: MarkerQuery): MapMarker<TMeta>[];
47
+ prune(now: number): number;
48
+ clear(): void;
49
+ subscribe(listener: () => void): () => void;
50
+ snapshot(): readonly MapMarker<TMeta>[];
51
+ }
52
+ export declare function createMarkerSet<TMeta = unknown>(now?: () => number): MarkerSet<TMeta>;
@@ -0,0 +1,96 @@
1
+ import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
2
+ export const DEFAULT_MARKER_KINDS = {
3
+ player: { id: "player", color: "#4ade80", glyph: "◆", priority: 90 },
4
+ ally: { id: "ally", color: "#38bdf8", glyph: "▲", priority: 70 },
5
+ objective: { id: "objective", color: "#facc15", glyph: "★", priority: 100 },
6
+ location: { id: "location", color: "#e2e8f0", glyph: "⬡", priority: 40 },
7
+ loot: { id: "loot", color: "#c084fc", glyph: "◈", priority: 60 },
8
+ enemy: { id: "enemy", color: "#f87171", glyph: "✖", priority: 80 },
9
+ danger: { id: "danger", color: "#fb923c", glyph: "⚠", priority: 85 },
10
+ ping: { id: "ping", color: "#22d3ee", glyph: "◎", priority: 65 },
11
+ };
12
+ export function markerKindStyle(kind, styles = DEFAULT_MARKER_KINDS) {
13
+ return styles[kind] ?? { id: kind, color: "#e2e8f0", glyph: "•", priority: 30 };
14
+ }
15
+ function distanceXZ(a, b) {
16
+ return Math.hypot(a[0] - b[0], a[2] - b[2]);
17
+ }
18
+ export function createMarkerSet(now = Date.now) {
19
+ const store = createObservableKeyedStore();
20
+ let counter = 0;
21
+ function generateId() {
22
+ let id = `marker-${counter}`;
23
+ while (store.has(id)) {
24
+ counter += 1;
25
+ id = `marker-${counter}`;
26
+ }
27
+ counter += 1;
28
+ return id;
29
+ }
30
+ return {
31
+ add(input) {
32
+ const id = input.id ?? generateId();
33
+ const marker = {
34
+ id,
35
+ kind: input.kind,
36
+ position: input.position,
37
+ createdAt: input.createdAt ?? now(),
38
+ };
39
+ if (input.label !== undefined)
40
+ marker.label = input.label;
41
+ if (input.owner !== undefined)
42
+ marker.owner = input.owner;
43
+ if (input.expiresAt !== undefined)
44
+ marker.expiresAt = input.expiresAt;
45
+ if (input.meta !== undefined)
46
+ marker.meta = input.meta;
47
+ store.set(id, marker);
48
+ return id;
49
+ },
50
+ remove(id) {
51
+ const existed = store.has(id);
52
+ store.delete(id);
53
+ return existed;
54
+ },
55
+ get(id) {
56
+ return store.get(id) ?? null;
57
+ },
58
+ list() {
59
+ return store.arraySnapshot();
60
+ },
61
+ query(query) {
62
+ return store.arraySnapshot().filter((marker) => {
63
+ if (query.kind !== undefined && marker.kind !== query.kind)
64
+ return false;
65
+ if (query.owner !== undefined && marker.owner !== query.owner)
66
+ return false;
67
+ if (query.near !== undefined) {
68
+ const radius = query.radius ?? Number.POSITIVE_INFINITY;
69
+ if (distanceXZ(marker.position, query.near) > radius)
70
+ return false;
71
+ }
72
+ return true;
73
+ });
74
+ },
75
+ prune(at) {
76
+ let removed = 0;
77
+ for (const marker of store.arraySnapshot()) {
78
+ if (marker.expiresAt !== undefined && marker.expiresAt <= at) {
79
+ store.delete(marker.id);
80
+ removed += 1;
81
+ }
82
+ }
83
+ return removed;
84
+ },
85
+ clear() {
86
+ for (const marker of store.arraySnapshot())
87
+ store.delete(marker.id);
88
+ },
89
+ subscribe(listener) {
90
+ return store.subscribe(listener);
91
+ },
92
+ snapshot() {
93
+ return store.arraySnapshot();
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,42 @@
1
+ export type WorldXZ = readonly [number, number];
2
+ /**
3
+ * Compass bearing (radians, 0 = map north = −Z, increasing clockwise toward +X
4
+ * = east) from one world XZ point to another. Feeds both the minimap direction
5
+ * and the compass strip.
6
+ */
7
+ export declare function compassBearing(from: WorldXZ, to: WorldXZ): number;
8
+ /** Bearing of an entity facing direction given its `rotationY` (yaw) in radians. */
9
+ export declare function headingToBearing(yaw: number): number;
10
+ export type Cardinal = "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW";
11
+ export declare function bearingToCardinal(bearing: number): Cardinal;
12
+ /** Signed offset of `bearing` from `reference`, wrapped into (−π, π]. */
13
+ export declare function relativeBearing(bearing: number, reference: number): number;
14
+ export interface MinimapView {
15
+ /** World XZ at the minimap center (usually the player position). */
16
+ center: WorldXZ;
17
+ /** World-unit distance from center to the minimap edge. */
18
+ worldRadius: number;
19
+ /** Pixel diameter of the minimap. */
20
+ size: number;
21
+ /** Rotate the projection so this facing bearing points up (rotating minimap). Omit for north-up. */
22
+ rotate?: number;
23
+ }
24
+ export interface MinimapPoint {
25
+ x: number;
26
+ y: number;
27
+ /** Radial distance in world units. */
28
+ distance: number;
29
+ /** True when within `worldRadius` of center. */
30
+ inside: boolean;
31
+ }
32
+ /**
33
+ * Project a world XZ (or XYZ) point into minimap pixel space. Origin is the
34
+ * top-left of the `size×size` box; north (−Z) maps to −Y (up). Pass
35
+ * `view.rotate` to spin the map under a fixed north-up player arrow.
36
+ */
37
+ export declare function projectToMinimap(world: WorldXZ | readonly [number, number, number], view: MinimapView): MinimapPoint;
38
+ /** Clamp a projected point to the minimap edge, preserving direction (edge markers). */
39
+ export declare function clampToMinimapEdge(point: MinimapPoint, size: number): {
40
+ x: number;
41
+ y: number;
42
+ };
@@ -0,0 +1,72 @@
1
+ const TWO_PI = Math.PI * 2;
2
+ function normalizeAngle(angle) {
3
+ let value = angle % TWO_PI;
4
+ if (value < 0)
5
+ value += TWO_PI;
6
+ return value;
7
+ }
8
+ /**
9
+ * Compass bearing (radians, 0 = map north = −Z, increasing clockwise toward +X
10
+ * = east) from one world XZ point to another. Feeds both the minimap direction
11
+ * and the compass strip.
12
+ */
13
+ export function compassBearing(from, to) {
14
+ return normalizeAngle(Math.atan2(to[0] - from[0], -(to[1] - from[1])));
15
+ }
16
+ /** Bearing of an entity facing direction given its `rotationY` (yaw) in radians. */
17
+ export function headingToBearing(yaw) {
18
+ return normalizeAngle(Math.atan2(Math.sin(yaw), -Math.cos(yaw)));
19
+ }
20
+ const CARDINALS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
21
+ export function bearingToCardinal(bearing) {
22
+ const index = Math.round(normalizeAngle(bearing) / (Math.PI / 4)) % 8;
23
+ return CARDINALS[index];
24
+ }
25
+ /** Signed offset of `bearing` from `reference`, wrapped into (−π, π]. */
26
+ export function relativeBearing(bearing, reference) {
27
+ let delta = normalizeAngle(bearing) - normalizeAngle(reference);
28
+ if (delta > Math.PI)
29
+ delta -= TWO_PI;
30
+ if (delta <= -Math.PI)
31
+ delta += TWO_PI;
32
+ return delta;
33
+ }
34
+ /**
35
+ * Project a world XZ (or XYZ) point into minimap pixel space. Origin is the
36
+ * top-left of the `size×size` box; north (−Z) maps to −Y (up). Pass
37
+ * `view.rotate` to spin the map under a fixed north-up player arrow.
38
+ */
39
+ export function projectToMinimap(world, view) {
40
+ const worldX = world[0];
41
+ const worldZ = world.length === 3 ? world[2] : world[1];
42
+ let dx = worldX - view.center[0];
43
+ let dz = worldZ - view.center[1];
44
+ if (view.rotate !== undefined && view.rotate !== 0) {
45
+ const cos = Math.cos(view.rotate);
46
+ const sin = Math.sin(view.rotate);
47
+ const rx = dx * cos - dz * sin;
48
+ const rz = dx * sin + dz * cos;
49
+ dx = rx;
50
+ dz = rz;
51
+ }
52
+ const distance = Math.hypot(dx, dz);
53
+ const half = view.size / 2;
54
+ const scale = view.worldRadius === 0 ? 0 : half / view.worldRadius;
55
+ return {
56
+ x: half + dx * scale,
57
+ y: half + dz * scale,
58
+ distance,
59
+ inside: distance <= view.worldRadius,
60
+ };
61
+ }
62
+ /** Clamp a projected point to the minimap edge, preserving direction (edge markers). */
63
+ export function clampToMinimapEdge(point, size) {
64
+ const half = size / 2;
65
+ const dx = point.x - half;
66
+ const dy = point.y - half;
67
+ const length = Math.hypot(dx, dy);
68
+ if (length <= half || length === 0)
69
+ return { x: point.x, y: point.y };
70
+ const scale = half / length;
71
+ return { x: half + dx * scale, y: half + dy * scale };
72
+ }
@@ -0,0 +1,39 @@
1
+ export type StructureVec3 = readonly [number, number, number];
2
+ export interface PlacedStructure {
3
+ id: string;
4
+ catalogId: string;
5
+ position: StructureVec3;
6
+ rotationY: number;
7
+ plotId?: string;
8
+ data?: Readonly<Record<string, unknown>>;
9
+ }
10
+ export interface AddStructureInput {
11
+ id?: string;
12
+ catalogId: string;
13
+ position: StructureVec3;
14
+ rotationY?: number;
15
+ plotId?: string;
16
+ data?: Readonly<Record<string, unknown>>;
17
+ }
18
+ export interface StructureFilter {
19
+ plotId?: string;
20
+ catalogId?: string;
21
+ }
22
+ export interface PlacedStructureSnapshot {
23
+ structures: readonly PlacedStructure[];
24
+ }
25
+ export interface PlacedStructureStore {
26
+ add(input: AddStructureInput): PlacedStructure;
27
+ move(id: string, position: StructureVec3): boolean;
28
+ rotate(id: string, rotationY: number): boolean;
29
+ remove(id: string): boolean;
30
+ get(id: string): PlacedStructure | null;
31
+ list(filter?: StructureFilter): readonly PlacedStructure[];
32
+ clear(): void;
33
+ select(id: string | null): boolean;
34
+ selected(): PlacedStructure | null;
35
+ snapshot(): PlacedStructureSnapshot;
36
+ load(snapshot: PlacedStructureSnapshot): void;
37
+ subscribe(listener: () => void): () => void;
38
+ }
39
+ export declare function createPlacedStructureStore(): PlacedStructureStore;
@@ -0,0 +1,95 @@
1
+ import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
2
+ export function createPlacedStructureStore() {
3
+ const store = createObservableKeyedStore();
4
+ let counter = 1;
5
+ let selectedId = null;
6
+ function generateId() {
7
+ let id = `structure-${counter}`;
8
+ while (store.has(id)) {
9
+ counter += 1;
10
+ id = `structure-${counter}`;
11
+ }
12
+ counter += 1;
13
+ return id;
14
+ }
15
+ return {
16
+ add(input) {
17
+ if (input.id !== undefined && store.has(input.id)) {
18
+ throw new Error(`Structure id "${input.id}" already exists.`);
19
+ }
20
+ const id = input.id ?? generateId();
21
+ const structure = {
22
+ id,
23
+ catalogId: input.catalogId,
24
+ position: input.position,
25
+ rotationY: input.rotationY ?? 0,
26
+ ...(input.plotId === undefined ? {} : { plotId: input.plotId }),
27
+ ...(input.data === undefined ? {} : { data: input.data }),
28
+ };
29
+ store.set(id, structure);
30
+ return structure;
31
+ },
32
+ move(id, position) {
33
+ const current = store.get(id);
34
+ if (current === undefined)
35
+ return false;
36
+ store.set(id, { ...current, position });
37
+ return true;
38
+ },
39
+ rotate(id, rotationY) {
40
+ const current = store.get(id);
41
+ if (current === undefined)
42
+ return false;
43
+ store.set(id, { ...current, rotationY });
44
+ return true;
45
+ },
46
+ remove(id) {
47
+ const existed = store.has(id);
48
+ store.delete(id);
49
+ if (selectedId === id)
50
+ selectedId = null;
51
+ return existed;
52
+ },
53
+ get(id) {
54
+ return store.get(id) ?? null;
55
+ },
56
+ list(filter) {
57
+ const all = store.arraySnapshot();
58
+ if (filter === undefined)
59
+ return all;
60
+ return all.filter((structure) => (filter.plotId === undefined || structure.plotId === filter.plotId) &&
61
+ (filter.catalogId === undefined || structure.catalogId === filter.catalogId));
62
+ },
63
+ clear() {
64
+ for (const structure of store.arraySnapshot())
65
+ store.delete(structure.id);
66
+ selectedId = null;
67
+ },
68
+ select(id) {
69
+ if (id === null) {
70
+ selectedId = null;
71
+ return true;
72
+ }
73
+ if (!store.has(id))
74
+ return false;
75
+ selectedId = id;
76
+ return true;
77
+ },
78
+ selected() {
79
+ return selectedId === null ? null : store.get(selectedId) ?? null;
80
+ },
81
+ snapshot() {
82
+ return { structures: store.arraySnapshot() };
83
+ },
84
+ load(snapshot) {
85
+ for (const structure of store.arraySnapshot())
86
+ store.delete(structure.id);
87
+ selectedId = null;
88
+ for (const structure of snapshot.structures)
89
+ store.set(structure.id, structure);
90
+ },
91
+ subscribe(listener) {
92
+ return store.subscribe(listener);
93
+ },
94
+ };
95
+ }
@@ -0,0 +1,48 @@
1
+ import { type Aabb, type Footprint, type Vec2 } from "./geometry.js";
2
+ import { type PlacementRules } from "./placement.js";
3
+ export type SnapMode = "grid" | "free" | "surface";
4
+ export type PlacementVec3 = readonly [number, number, number];
5
+ export interface PlacementHit {
6
+ point: PlacementVec3;
7
+ normal: PlacementVec3;
8
+ }
9
+ export interface PlacementControllerConfig {
10
+ footprint: Footprint;
11
+ rules?: PlacementRules;
12
+ snapMode?: SnapMode;
13
+ grid?: number;
14
+ quarterTurns?: number;
15
+ }
16
+ export interface PlacementPreview {
17
+ center: Vec2;
18
+ y: number;
19
+ quarterTurns: number;
20
+ footprint: Footprint;
21
+ aabb: Aabb;
22
+ valid: boolean;
23
+ reason?: "out-of-bounds" | "overlap";
24
+ snapMode: SnapMode;
25
+ normal: PlacementVec3;
26
+ }
27
+ export interface PlacementCommit {
28
+ center: Vec2;
29
+ y: number;
30
+ rotationY: number;
31
+ quarterTurns: number;
32
+ footprint: Footprint;
33
+ aabb: Aabb;
34
+ }
35
+ export interface PlacementController {
36
+ hover(hit: PlacementHit): PlacementPreview;
37
+ current(): PlacementPreview | null;
38
+ rotate(steps?: number): PlacementPreview | null;
39
+ setSnapMode(mode: SnapMode): PlacementPreview | null;
40
+ cycleSnapMode(): SnapMode;
41
+ setFootprint(footprint: Footprint): void;
42
+ setRules(rules: PlacementRules): void;
43
+ setGrid(grid: number): void;
44
+ commit(): PlacementCommit | null;
45
+ reset(): void;
46
+ }
47
+ export declare function quarterTurnsToRotationY(quarterTurns: number): number;
48
+ export declare function createPlacementController(config: PlacementControllerConfig): PlacementController;