@jgengine/core 0.8.0 → 0.10.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 (303) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/ai/flock.d.ts +36 -0
  3. package/dist/ai/flock.js +182 -0
  4. package/dist/ai/groupAssist.d.ts +19 -0
  5. package/dist/ai/groupAssist.js +59 -0
  6. package/dist/ai/laneSelect.d.ts +21 -0
  7. package/dist/ai/laneSelect.js +37 -0
  8. package/dist/ai/mobBrain.d.ts +68 -0
  9. package/dist/ai/mobBrain.js +115 -0
  10. package/dist/ai/spawnDirector.d.ts +13 -0
  11. package/dist/ai/spawnDirector.js +41 -17
  12. package/dist/ai/spawnPoint.d.ts +5 -0
  13. package/dist/ai/spawnPoint.js +5 -0
  14. package/dist/ai/threat.d.ts +2 -0
  15. package/dist/ai/threat.js +34 -0
  16. package/dist/audio/audioFalloff.d.ts +5 -2
  17. package/dist/audio/music.d.ts +53 -0
  18. package/dist/audio/music.js +34 -0
  19. package/dist/audio/synth.d.ts +60 -0
  20. package/dist/audio/synth.js +8 -0
  21. package/dist/authoring.d.ts +14 -0
  22. package/dist/authoring.js +14 -0
  23. package/dist/cartridge/runtime.d.ts +15 -0
  24. package/dist/cartridge/runtime.js +556 -0
  25. package/dist/cartridge/spec.d.ts +188 -0
  26. package/dist/cartridge/spec.js +19 -0
  27. package/dist/cartridge/validate.d.ts +2 -0
  28. package/dist/cartridge/validate.js +78 -0
  29. package/dist/combat/abilityKit.d.ts +17 -2
  30. package/dist/combat/abilityKit.js +62 -13
  31. package/dist/combat/castRunner.d.ts +40 -0
  32. package/dist/combat/castRunner.js +56 -0
  33. package/dist/combat/comboPoints.d.ts +14 -0
  34. package/dist/combat/comboPoints.js +52 -0
  35. package/dist/combat/defensiveWindow.d.ts +1 -1
  36. package/dist/combat/defensiveWindow.js +5 -3
  37. package/dist/combat/dotField.d.ts +43 -0
  38. package/dist/combat/dotField.js +48 -0
  39. package/dist/combat/projectiles.d.ts +33 -12
  40. package/dist/combat/projectiles.js +214 -129
  41. package/dist/combat/regenShield.d.ts +35 -0
  42. package/dist/combat/regenShield.js +45 -0
  43. package/dist/combat/resistance.d.ts +16 -4
  44. package/dist/combat/resistance.js +43 -7
  45. package/dist/combat/resourcePool.d.ts +20 -0
  46. package/dist/combat/resourcePool.js +55 -0
  47. package/dist/combat/shotOrigin.d.ts +52 -0
  48. package/dist/combat/shotOrigin.js +86 -0
  49. package/dist/combat/telegraph.d.ts +25 -0
  50. package/dist/combat/telegraph.js +47 -0
  51. package/dist/crafting/recipe.js +4 -1
  52. package/dist/devtools/devtools.d.ts +72 -24
  53. package/dist/devtools/devtools.js +449 -101
  54. package/dist/devtools/transformTunables.js +3 -3
  55. package/dist/devtools/tunableSchema.d.ts +140 -0
  56. package/dist/devtools/tunableSchema.js +476 -0
  57. package/dist/editor/commands.d.ts +65 -0
  58. package/dist/editor/commands.js +193 -0
  59. package/dist/editor/document.d.ts +40 -0
  60. package/dist/editor/document.js +153 -0
  61. package/dist/editor/index.d.ts +30 -0
  62. package/dist/editor/index.js +6 -0
  63. package/dist/editor/types.d.ts +69 -0
  64. package/dist/editor/types.js +29 -0
  65. package/dist/faction/factions.d.ts +33 -0
  66. package/dist/faction/factions.js +76 -0
  67. package/dist/faction/reputation.d.ts +32 -0
  68. package/dist/faction/reputation.js +82 -0
  69. package/dist/game/chat.d.ts +2 -0
  70. package/dist/game/chat.js +9 -1
  71. package/dist/game/chatFilter.d.ts +15 -0
  72. package/dist/game/chatFilter.js +55 -0
  73. package/dist/game/connectedPlayers.d.ts +28 -0
  74. package/dist/game/connectedPlayers.js +21 -0
  75. package/dist/game/controlGate.d.ts +4 -0
  76. package/dist/game/controlGate.js +7 -0
  77. package/dist/game/defineGame.d.ts +44 -1
  78. package/dist/game/defineGame.js +1 -0
  79. package/dist/game/events.d.ts +25 -0
  80. package/dist/game/gamePhase.d.ts +20 -0
  81. package/dist/game/gamePhase.js +21 -0
  82. package/dist/game/keyValueStore.d.ts +25 -0
  83. package/dist/game/keyValueStore.js +61 -0
  84. package/dist/game/levelSequence.d.ts +24 -2
  85. package/dist/game/levelSequence.js +51 -2
  86. package/dist/game/lootTable.d.ts +14 -1
  87. package/dist/game/lootTable.js +28 -7
  88. package/dist/game/modelAnimation.d.ts +8 -0
  89. package/dist/game/modelAnimation.js +18 -0
  90. package/dist/game/playableGame.d.ts +116 -4
  91. package/dist/game/playableGame.js +1 -1
  92. package/dist/game/quest.d.ts +7 -3
  93. package/dist/game/quest.js +17 -8
  94. package/dist/game/race.d.ts +39 -1
  95. package/dist/game/race.js +96 -11
  96. package/dist/game/recordBook.d.ts +27 -0
  97. package/dist/game/recordBook.js +86 -0
  98. package/dist/game/talents.d.ts +47 -0
  99. package/dist/game/talents.js +140 -0
  100. package/dist/index.d.ts +8 -0
  101. package/dist/index.js +8 -0
  102. package/dist/input/actionBindings.d.ts +2 -0
  103. package/dist/input/actionBindings.js +1 -0
  104. package/dist/input/axisInput.d.ts +33 -3
  105. package/dist/input/axisInput.js +60 -4
  106. package/dist/input/bindingOverrides.d.ts +25 -0
  107. package/dist/input/bindingOverrides.js +95 -0
  108. package/dist/input/pointerAxis.d.ts +30 -0
  109. package/dist/input/pointerAxis.js +31 -0
  110. package/dist/input/touchScheme.d.ts +6 -0
  111. package/dist/input/touchScheme.js +45 -7
  112. package/dist/interaction/contextMenu.d.ts +1 -0
  113. package/dist/interaction/contextMenu.js +1 -0
  114. package/dist/interaction/proximityPrompt.d.ts +3 -1
  115. package/dist/interaction/proximityPrompt.js +3 -3
  116. package/dist/meta/changelog.d.ts +1 -1
  117. package/dist/meta/changelog.js +44 -1
  118. package/dist/movement/dash.d.ts +3 -0
  119. package/dist/movement/dash.js +19 -5
  120. package/dist/movement/glideModel.d.ts +44 -0
  121. package/dist/movement/glideModel.js +54 -0
  122. package/dist/movement/grappleSwing.d.ts +34 -0
  123. package/dist/movement/grappleSwing.js +56 -0
  124. package/dist/movement/leaderTrail.d.ts +29 -0
  125. package/dist/movement/leaderTrail.js +67 -0
  126. package/dist/movement/movementModel.d.ts +4 -0
  127. package/dist/movement/movementModel.js +13 -0
  128. package/dist/movement/playerMovement.d.ts +41 -0
  129. package/dist/movement/playerMovement.js +181 -0
  130. package/dist/movement/steering.d.ts +27 -0
  131. package/dist/movement/steering.js +40 -0
  132. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  133. package/dist/multiplayer/feedWriteGate.js +12 -0
  134. package/dist/multiplayer/presenceModel.d.ts +14 -2
  135. package/dist/multiplayer/presenceModel.js +15 -3
  136. package/dist/nav/corridors.d.ts +30 -0
  137. package/dist/nav/corridors.js +69 -0
  138. package/dist/nav/navGrid.js +30 -4
  139. package/dist/nav/railGraph.d.ts +66 -0
  140. package/dist/nav/railGraph.js +129 -0
  141. package/dist/nav/timetable.d.ts +41 -0
  142. package/dist/nav/timetable.js +94 -0
  143. package/dist/physics/buoyancy.d.ts +6 -0
  144. package/dist/physics/buoyancy.js +14 -0
  145. package/dist/physics/flowTube.d.ts +32 -0
  146. package/dist/physics/flowTube.js +64 -0
  147. package/dist/physics/forceVolume.d.ts +27 -0
  148. package/dist/physics/forceVolume.js +38 -0
  149. package/dist/physics/kinematicVehicle.d.ts +52 -0
  150. package/dist/physics/kinematicVehicle.js +81 -0
  151. package/dist/physics/physicsWorld.d.ts +13 -6
  152. package/dist/physics/physicsWorld.js +169 -40
  153. package/dist/physics/ragdoll.js +1 -1
  154. package/dist/physics/vehicleBody.js +2 -1
  155. package/dist/random/rng.d.ts +2 -0
  156. package/dist/random/rng.js +2 -0
  157. package/dist/random/seedLink.d.ts +5 -0
  158. package/dist/random/seedLink.js +35 -0
  159. package/dist/render/postProcessing.d.ts +56 -0
  160. package/dist/render/postProcessing.js +1 -0
  161. package/dist/runtime/adapter.d.ts +19 -0
  162. package/dist/runtime/adapter.js +11 -5
  163. package/dist/runtime/cameraDirector.d.ts +6 -1
  164. package/dist/runtime/cameraDirector.js +6 -0
  165. package/dist/runtime/commandRunner.d.ts +2 -2
  166. package/dist/runtime/commandRunner.js +2 -2
  167. package/dist/runtime/gameContext.d.ts +72 -8
  168. package/dist/runtime/gameContext.js +374 -61
  169. package/dist/runtime/gameRuntime.d.ts +5 -1
  170. package/dist/runtime/gameRuntime.js +13 -12
  171. package/dist/runtime/hostPersistence.d.ts +9 -1
  172. package/dist/runtime/hostPersistence.js +23 -8
  173. package/dist/runtime/hostedGameRunner.d.ts +49 -0
  174. package/dist/runtime/hostedGameRunner.js +62 -0
  175. package/dist/runtime/hostedWorldSession.d.ts +65 -0
  176. package/dist/runtime/hostedWorldSession.js +57 -0
  177. package/dist/runtime/inputSnapshot.d.ts +18 -0
  178. package/dist/runtime/inputSnapshot.js +7 -0
  179. package/dist/runtime/motionIntents.d.ts +16 -6
  180. package/dist/runtime/motionIntents.js +31 -2
  181. package/dist/runtime/snapshot.d.ts +1 -0
  182. package/dist/runtime/snapshot.js +1 -1
  183. package/dist/runtime/visibility.d.ts +74 -0
  184. package/dist/runtime/visibility.js +127 -0
  185. package/dist/runtime/worldChannel.d.ts +58 -0
  186. package/dist/runtime/worldChannel.js +69 -0
  187. package/dist/runtime/worldMirror.d.ts +24 -0
  188. package/dist/runtime/worldMirror.js +31 -0
  189. package/dist/runtime/worldReplication.d.ts +46 -0
  190. package/dist/runtime/worldReplication.js +185 -0
  191. package/dist/runtime/worldSnapshot.d.ts +17 -0
  192. package/dist/runtime/worldSnapshot.js +15 -0
  193. package/dist/scene/assetCatalog.d.ts +1 -0
  194. package/dist/scene/colliders.d.ts +50 -0
  195. package/dist/scene/colliders.js +101 -0
  196. package/dist/scene/entityStats.d.ts +4 -0
  197. package/dist/scene/entityStats.js +21 -0
  198. package/dist/scene/entityStore.d.ts +51 -5
  199. package/dist/scene/entityStore.js +119 -12
  200. package/dist/scene/objectQuery.d.ts +13 -2
  201. package/dist/scene/objectQuery.js +34 -2
  202. package/dist/scene/objectStore.d.ts +2 -0
  203. package/dist/scene/objectStore.js +31 -12
  204. package/dist/scene/sceneRaycast.d.ts +80 -0
  205. package/dist/scene/sceneRaycast.js +284 -0
  206. package/dist/scene/spatial.d.ts +11 -16
  207. package/dist/scene/spatial.js +99 -38
  208. package/dist/sensor/recordingBuffer.d.ts +6 -0
  209. package/dist/sensor/recordingBuffer.js +7 -0
  210. package/dist/sensor/replayLoop.d.ts +39 -0
  211. package/dist/sensor/replayLoop.js +64 -0
  212. package/dist/sensor/visionCone.d.ts +31 -0
  213. package/dist/sensor/visionCone.js +72 -0
  214. package/dist/session/extraction.d.ts +2 -2
  215. package/dist/session/extraction.js +13 -2
  216. package/dist/settings/settingsModel.d.ts +97 -0
  217. package/dist/settings/settingsModel.js +100 -0
  218. package/dist/stats/statModifiers.d.ts +5 -2
  219. package/dist/stats/statModifiers.js +32 -14
  220. package/dist/store/observableKeyedStore.d.ts +4 -0
  221. package/dist/store/observableKeyedStore.js +17 -2
  222. package/dist/survival/decayMeter.d.ts +1 -1
  223. package/dist/survival/decayMeter.js +0 -1
  224. package/dist/tactics/fallingGrid.d.ts +2 -2
  225. package/dist/tactics/fallingGrid.js +2 -2
  226. package/dist/time/beatClock.d.ts +18 -0
  227. package/dist/time/beatClock.js +24 -0
  228. package/dist/time/idleProgress.d.ts +29 -0
  229. package/dist/time/idleProgress.js +31 -0
  230. package/dist/time/serverTick.d.ts +7 -1
  231. package/dist/time/serverTick.js +27 -4
  232. package/dist/time/stateSchedule.d.ts +57 -0
  233. package/dist/time/stateSchedule.js +121 -0
  234. package/dist/turn/commit.d.ts +2 -2
  235. package/dist/turn/commit.js +2 -2
  236. package/dist/ui/gameLayout.d.ts +125 -0
  237. package/dist/ui/gameLayout.js +129 -0
  238. package/dist/ui/hudLayout.d.ts +58 -0
  239. package/dist/ui/hudLayout.js +207 -0
  240. package/dist/ui/hudScale.d.ts +44 -0
  241. package/dist/ui/hudScale.js +55 -0
  242. package/dist/ui/orientation.d.ts +39 -0
  243. package/dist/ui/orientation.js +49 -0
  244. package/dist/ui/swingTimer.d.ts +42 -0
  245. package/dist/ui/swingTimer.js +38 -0
  246. package/dist/visibility/assetStreaming.d.ts +66 -0
  247. package/dist/visibility/assetStreaming.js +201 -0
  248. package/dist/visibility/bounds.d.ts +66 -0
  249. package/dist/visibility/bounds.js +113 -0
  250. package/dist/visibility/camera.d.ts +21 -0
  251. package/dist/visibility/camera.js +3 -0
  252. package/dist/visibility/config.d.ts +21 -0
  253. package/dist/visibility/config.js +1 -0
  254. package/dist/visibility/diagnostics.d.ts +26 -0
  255. package/dist/visibility/diagnostics.js +36 -0
  256. package/dist/visibility/distance.d.ts +11 -0
  257. package/dist/visibility/distance.js +30 -0
  258. package/dist/visibility/frustum.d.ts +47 -0
  259. package/dist/visibility/frustum.js +186 -0
  260. package/dist/visibility/occlusion.d.ts +36 -0
  261. package/dist/visibility/occlusion.js +45 -0
  262. package/dist/visibility/settings.d.ts +75 -0
  263. package/dist/visibility/settings.js +49 -0
  264. package/dist/visibility/simulationCulling.d.ts +34 -0
  265. package/dist/visibility/simulationCulling.js +36 -0
  266. package/dist/visibility/spatialIndex.d.ts +45 -0
  267. package/dist/visibility/spatialIndex.js +212 -0
  268. package/dist/visibility/visibilitySystem.d.ts +90 -0
  269. package/dist/visibility/visibilitySystem.js +257 -0
  270. package/dist/world/buildings.d.ts +6 -0
  271. package/dist/world/buildings.js +152 -0
  272. package/dist/world/carve.d.ts +7 -2
  273. package/dist/world/carve.js +17 -7
  274. package/dist/world/cellStates.d.ts +45 -0
  275. package/dist/world/cellStates.js +103 -0
  276. package/dist/world/environmentSummary.d.ts +21 -4
  277. package/dist/world/environmentSummary.js +22 -3
  278. package/dist/world/features.d.ts +185 -8
  279. package/dist/world/features.js +64 -6
  280. package/dist/world/gridInstances.d.ts +7 -8
  281. package/dist/world/gridInstances.js +145 -7
  282. package/dist/world/lod.d.ts +17 -0
  283. package/dist/world/lod.js +66 -0
  284. package/dist/world/mapLayers.d.ts +60 -0
  285. package/dist/world/mapLayers.js +44 -0
  286. package/dist/world/massing.d.ts +45 -0
  287. package/dist/world/massing.js +581 -0
  288. package/dist/world/minimap.d.ts +13 -1
  289. package/dist/world/minimap.js +21 -2
  290. package/dist/world/roads.d.ts +47 -0
  291. package/dist/world/roads.js +146 -0
  292. package/dist/world/streets.d.ts +80 -0
  293. package/dist/world/streets.js +144 -0
  294. package/dist/world/terrain.d.ts +69 -3
  295. package/dist/world/terrain.js +309 -16
  296. package/dist/world/water.d.ts +7 -0
  297. package/dist/world/water.js +19 -4
  298. package/dist/world/weather.d.ts +3 -18
  299. package/dist/world/weather.js +4 -17
  300. package/dist/world/windZones.d.ts +47 -0
  301. package/dist/world/windZones.js +59 -0
  302. package/llms.txt +3013 -1726
  303. package/package.json +12 -2
@@ -0,0 +1,581 @@
1
+ export const MASSING_COMPOSITIONS = [
2
+ "bar",
3
+ "split",
4
+ "cluster",
5
+ "stack",
6
+ "court",
7
+ "bridge",
8
+ "ring",
9
+ "capsule",
10
+ "megastructure",
11
+ ];
12
+ export const MASSING_PROFILES = [
13
+ "straight",
14
+ "stepped",
15
+ "tapered",
16
+ "top-heavy",
17
+ "offset",
18
+ "twisted",
19
+ ];
20
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
21
+ export const hashMassingSeed = (text) => [...text].reduce((sum, ch) => sum + ch.charCodeAt(0), 0);
22
+ export const massingBase = (spec, cap = 0.28) => Math.min(Math.max(0, spec.pilotis), Math.max(0, spec.height) * cap);
23
+ export const isVillageMassing = (spec) => spec.composition === "cluster" && spec.height < 36 && spec.articulation > 56 && spec.moduleDensity > 2;
24
+ export function massingFloorCount(spec) {
25
+ const usable = Math.max(7, spec.height - massingBase(spec));
26
+ return Math.max(1, Math.floor(usable / spec.floorHeight));
27
+ }
28
+ export function composeMassing(b) {
29
+ if (b.composition === "ring")
30
+ return [];
31
+ const base = massingBase(b);
32
+ const usable = Math.max(7, b.height - base);
33
+ const floorCount = Math.max(1, Math.floor(usable / b.floorHeight));
34
+ const articulationTierBonus = b.composition === "bar" || b.composition === "megastructure" ? Math.floor(b.articulation / 45) : 0;
35
+ const requestedTiers = Math.max(b.terraces, Math.ceil(floorCount / 7) + Math.floor((b.moduleDensity - 3) / 2) + articulationTierBonus);
36
+ const tierCount = Math.round(clamp(requestedTiers, 1, Math.max(1, Math.min(9, floorCount))));
37
+ const tierH = usable / tierCount;
38
+ const bodies = [];
39
+ const composition = b.composition;
40
+ const profileAt = (i) => {
41
+ const t = tierCount === 1 ? 0.5 : i / (tierCount - 1);
42
+ const amount = b.taper / 100;
43
+ let scaleW = 1;
44
+ let scaleD = 1;
45
+ let x = 0;
46
+ let z = 0;
47
+ let ry = 0;
48
+ if (b.profile === "stepped") {
49
+ scaleW = 1 - t * amount * 0.5;
50
+ scaleD = 1 - t * amount * 0.34;
51
+ z = -t * (b.cantilever + b.depth * 0.08);
52
+ }
53
+ if (b.profile === "tapered") {
54
+ scaleW = 1 - t * amount * 0.78;
55
+ scaleD = 1 - t * amount * 0.58;
56
+ }
57
+ if (b.profile === "top-heavy") {
58
+ scaleW = 0.66 + t * (0.34 + amount * 0.42);
59
+ scaleD = 0.76 + t * (0.24 + amount * 0.25);
60
+ }
61
+ if (b.profile === "offset") {
62
+ x = (t - 0.5) * (b.cantilever * 2 + (composition === "bar" ? 0 : b.voids * 0.1));
63
+ z = Math.sin(t * Math.PI * 2) * b.cantilever * 0.3;
64
+ scaleW = 1 - amount * 0.12 * t;
65
+ }
66
+ if (b.profile === "twisted") {
67
+ ry = (t - 0.5) * (0.16 + (composition === "megastructure" ? 0 : amount * 1.65));
68
+ scaleW = 1 - amount * 0.18 * t;
69
+ scaleD = 1 - amount * 0.12 * t;
70
+ }
71
+ x += t * b.cantilever * 0.42;
72
+ return { t, scaleW: Math.max(0.42, scaleW), scaleD: Math.max(0.46, scaleD), x, z, ry };
73
+ };
74
+ if (b.podiumHeight > 0 && composition !== "bridge")
75
+ bodies.push({
76
+ x: 0,
77
+ y: b.podiumHeight / 2,
78
+ z: 0,
79
+ w: b.width + Math.min(8, b.cantilever),
80
+ h: b.podiumHeight,
81
+ d: b.depth + Math.min(7, b.cantilever),
82
+ facade: true,
83
+ });
84
+ if (composition === "capsule") {
85
+ const seed = hashMassingSeed(b.seed);
86
+ const density = (b.moduleDensity - 1) / 4;
87
+ const desiredRows = Math.round(clamp(5 + Math.min(5, floorCount * 0.18) + density * 5, 5, 14));
88
+ const lanes = 1 + Number(b.moduleDensity >= 3 && b.depth > 18) + Number(b.moduleDensity === 5 && b.depth > 38);
89
+ const spines = Math.round(clamp(Math.max(b.cores, lanes), 1, 3));
90
+ const rows = Math.min(desiredRows, Math.max(4, Math.floor(64 / (spines * 2))));
91
+ const rowH = usable / rows;
92
+ const coreW = clamp(b.width * 0.13, 4.5, 10);
93
+ const coreD = clamp((b.depth / (spines + 1)) * 0.68, 4.5, 8.5);
94
+ const laneZ = Array.from({ length: spines }, (_, lane) => (lane - (spines - 1) / 2) * Math.min(13, b.depth * 0.3));
95
+ const structureH = base + usable;
96
+ for (const z of laneZ)
97
+ bodies.push({ x: 0, y: structureH / 2, z, w: coreW, h: structureH, d: coreD, facade: false, role: "core" });
98
+ const basePodW = clamp(b.width * (0.22 + b.moduleDensity * 0.012) + b.cantilever * 0.3, 8, 28);
99
+ const basePodD = clamp((b.depth / (spines + 1)) * 0.78, 4.8, 11);
100
+ const taper = clamp(b.taper / 65, 0, 1);
101
+ const articulation = b.articulation / 100;
102
+ const voidBandCount = b.voids < 20 ? 0 : b.voids < 42 ? 1 : b.voids < 60 ? 2 : 3;
103
+ const voidRows = [0.34, 0.61, 0.82].slice(0, voidBandCount).map((t) => Math.round((rows - 1) * t));
104
+ const capsuleProfile = (row) => {
105
+ const t = rows === 1 ? 0.5 : row / (rows - 1);
106
+ const smooth = t * t * (3 - 2 * t);
107
+ let reachScale = 1 - taper * 0.48 * smooth;
108
+ let depthScale = 1 - taper * 0.18 * smooth;
109
+ let driftX = t * b.cantilever * 0.08;
110
+ let driftZ = 0;
111
+ let turn = 0;
112
+ if (b.profile === "stepped") {
113
+ const band = Math.min(2, Math.floor(t * 3));
114
+ reachScale *= 1 - band * 0.08;
115
+ depthScale *= 1 - band * 0.05;
116
+ driftZ -= band * (b.depth * 0.035 + b.cantilever * 0.15);
117
+ }
118
+ if (b.profile === "tapered") {
119
+ reachScale *= 1 - smooth * 0.2;
120
+ depthScale *= 1 - smooth * 0.16;
121
+ }
122
+ if (b.profile === "top-heavy") {
123
+ reachScale *= 0.66 + smooth * 0.62;
124
+ depthScale *= 0.76 + smooth * 0.3;
125
+ }
126
+ if (b.profile === "offset") {
127
+ driftX += (t - 0.5) * (b.cantilever * 1.7 + b.width * (0.08 + taper * 0.12));
128
+ driftZ = Math.sin(t * Math.PI * 2) * b.depth * 0.06;
129
+ }
130
+ if (b.profile === "twisted") {
131
+ turn = (t - 0.5) * (0.55 + taper * 1.2);
132
+ driftX += Math.sin(t * Math.PI * 2) * b.cantilever * 0.18;
133
+ }
134
+ return {
135
+ t,
136
+ reachScale: clamp(reachScale, 0.38, 1.28),
137
+ depthScale: clamp(depthScale, 0.52, 1.18),
138
+ driftX,
139
+ driftZ,
140
+ turn,
141
+ };
142
+ };
143
+ for (let i = 0; i < rows; i++) {
144
+ if (voidRows.includes(i))
145
+ continue;
146
+ const p = capsuleProfile(i);
147
+ const podH = Math.min(rowH * (0.62 + articulation * 0.12), b.floorHeight * 1.65);
148
+ for (const side of [-1, 1])
149
+ for (let lane = 0; lane < spines; lane++) {
150
+ const rhythm = ((seed + i * 17 + lane * 11 + (side > 0 ? 3 : 0)) % 5) - 2;
151
+ const projection = 1 + rhythm * articulation * 0.045;
152
+ const podW = clamp(basePodW * p.reachScale * projection, 6.2, 28);
153
+ const podD = clamp(basePodD * p.depthScale * (1 + (((rhythm + 2) % 3) - 1) * articulation * 0.035), 4.4, 11);
154
+ const reach = coreW * 0.48 + podW * 0.44 + b.cantilever * 0.07 * (1 - p.t * 0.35);
155
+ const x = p.driftX + side * reach;
156
+ const z = p.driftZ + laneZ[lane] + (i % 2 ? podD * 0.12 : -podD * 0.12) + rhythm * articulation * 0.11;
157
+ const y = base + rowH * (i + 0.5) + rhythm * rowH * articulation * 0.018;
158
+ bodies.push({ x, y, z, w: podW, h: podH, d: podD, ry: p.turn, kind: "capsule", facade: false, role: "mass" });
159
+ bodies.push({
160
+ x: p.driftX + side * reach * 0.48,
161
+ y,
162
+ z: p.driftZ + laneZ[lane],
163
+ w: reach * 0.94,
164
+ h: Math.max(0.48, podH * 0.18),
165
+ d: podD * 0.58,
166
+ ry: p.turn,
167
+ facade: false,
168
+ role: "transfer",
169
+ });
170
+ }
171
+ }
172
+ const deckCount = Math.round(clamp(b.terraces, 0, 6));
173
+ for (let deck = 0; deck < deckCount; deck++) {
174
+ const row = Math.round(((deck + 1) / (deckCount + 1)) * (rows - 1));
175
+ const p = capsuleProfile(row);
176
+ bodies.push({
177
+ x: p.driftX,
178
+ y: base + rowH * (row + 0.12),
179
+ z: p.driftZ,
180
+ w: Math.max(coreW * 2.2, b.width * 0.24),
181
+ h: 0.48,
182
+ d: Math.max(coreD * 1.8, Math.abs(laneZ.at(-1) ?? 0) * 2 + coreD * 1.25),
183
+ ry: p.turn,
184
+ facade: false,
185
+ role: "transfer",
186
+ });
187
+ }
188
+ const occupiedRows = Array.from({ length: rows }, (_, i) => i).filter((i) => !voidRows.includes(i));
189
+ for (let branch = 0; branch < b.branches; branch++) {
190
+ const row = occupiedRows[Math.round(((branch + 1) / (b.branches + 1)) * Math.max(0, occupiedRows.length - 1))] ??
191
+ Math.floor(rows / 2);
192
+ const p = capsuleProfile(row);
193
+ const direction = branch % 2 ? 1 : -1;
194
+ const y = base + rowH * (row + 0.5);
195
+ const x = p.driftX + ((branch % 3) - 1) * coreW * 0.42;
196
+ const outerLane = Math.max(coreD * 0.5, ...laneZ.map((z) => Math.abs(z) + coreD * 0.5));
197
+ const armReach = outerLane + 5 + b.cantilever * (0.42 + (branch % 3) * 0.08) + (branch % 2) * 1.5;
198
+ const terminalW = clamp(basePodW * p.reachScale * (0.74 + b.moduleDensity * 0.035), 7, 22);
199
+ const terminalD = clamp(basePodD * (0.9 + (branch % 2) * 0.08), 4.8, 10);
200
+ bodies.push({
201
+ x,
202
+ y,
203
+ z: p.driftZ + direction * armReach * 0.5,
204
+ w: Math.max(1.1, coreW * 0.25 + b.moduleDensity * 0.14),
205
+ h: 0.62,
206
+ d: armReach,
207
+ ry: p.turn,
208
+ facade: false,
209
+ role: "transfer",
210
+ branch: true,
211
+ });
212
+ bodies.push({
213
+ x,
214
+ y: y + rowH * 0.08,
215
+ z: p.driftZ + direction * (armReach + terminalD * 0.46),
216
+ w: terminalW,
217
+ h: Math.min(rowH * 0.82, b.floorHeight * 1.75),
218
+ d: terminalD,
219
+ ry: p.turn,
220
+ kind: "capsule",
221
+ facade: false,
222
+ role: "mass",
223
+ branch: true,
224
+ });
225
+ }
226
+ if (b.crown > 20) {
227
+ const crownY = base + usable - Math.max(0.45, rowH * 0.16);
228
+ const crownReach = clamp(b.width * (0.2 + b.crown * 0.0016), 7, 20);
229
+ bodies.push({
230
+ x: 0,
231
+ y: crownY,
232
+ z: 0,
233
+ w: crownReach * 1.35,
234
+ h: 0.72,
235
+ d: Math.max(coreD * 1.6, b.depth * 0.28),
236
+ facade: false,
237
+ role: "transfer",
238
+ crown: true,
239
+ });
240
+ if (b.crown > 44)
241
+ for (const side of [-1, 1])
242
+ bodies.push({
243
+ x: side * crownReach * 0.44,
244
+ y: crownY + Math.min(rowH * 0.38, 2.4),
245
+ z: 0,
246
+ w: crownReach,
247
+ h: Math.min(rowH * 0.72, b.floorHeight * 1.55),
248
+ d: basePodD * 0.86,
249
+ kind: "capsule",
250
+ facade: false,
251
+ role: "mass",
252
+ crown: true,
253
+ });
254
+ if (b.crown > 76)
255
+ for (const zSide of [-1, 1])
256
+ bodies.push({
257
+ x: 0,
258
+ y: crownY + Math.min(rowH * 0.82, 4.4),
259
+ z: zSide * basePodD * 0.55,
260
+ w: crownReach * 0.76,
261
+ h: Math.min(rowH * 0.64, b.floorHeight * 1.4),
262
+ d: basePodD * 0.74,
263
+ kind: "capsule",
264
+ facade: false,
265
+ role: "mass",
266
+ crown: true,
267
+ });
268
+ }
269
+ return bodies;
270
+ }
271
+ if (composition === "megastructure") {
272
+ const seed = hashMassingSeed(b.seed);
273
+ const bays = Math.round(clamp(Math.round(b.width / Math.max(10, b.baySpacing * 2.25)) + b.moduleDensity - 2, 3, 10));
274
+ const bayW = b.width / bays;
275
+ const levels = Math.round(clamp(tierCount + b.moduleDensity - 2, 3, 9));
276
+ const levelH = usable / levels;
277
+ const frameDepth = Math.max(8, b.depth * 0.68);
278
+ const post = Math.max(0.75, Math.min(1.8, bayW * 0.085));
279
+ const frameXs = Array.from({ length: bays + 1 }, (_, i) => -b.width / 2 + i * bayW);
280
+ const structureH = base + usable;
281
+ for (const x of frameXs)
282
+ for (const z of [-frameDepth / 2, frameDepth / 2])
283
+ bodies.push({ x, y: structureH / 2, z, w: post, h: structureH, d: post, facade: false, role: "core" });
284
+ for (let i = 0; i <= levels; i++) {
285
+ const y = base + Math.min(usable - 0.45, i * levelH) + 0.3;
286
+ bodies.push({ x: 0, y, z: -frameDepth / 2, w: b.width + post, h: 0.72, d: post, facade: false, role: "transfer" });
287
+ bodies.push({ x: 0, y, z: frameDepth / 2, w: b.width + post, h: 0.72, d: post, facade: false, role: "transfer" });
288
+ for (let bay = 0; bay <= bays; bay += 2)
289
+ bodies.push({ x: frameXs[bay], y, z: 0, w: post, h: 0.72, d: frameDepth + post, facade: false, role: "transfer" });
290
+ }
291
+ for (let level = 0; level < levels; level++)
292
+ for (let bay = 0; bay < bays; bay++) {
293
+ const chance = (seed + level * 37 + bay * 61) % 100;
294
+ if (chance < clamp(b.voids * 0.72 - b.moduleDensity * 5, 8, 58))
295
+ continue;
296
+ const p = profileAt(Math.round((level / Math.max(1, levels - 1)) * (tierCount - 1)));
297
+ const side = (seed + level + bay) % 2 ? 1 : -1;
298
+ const jitter = (((seed + bay * 17 + level * 13) % 9) - 4) * b.articulation * 0.0028;
299
+ const moduleW = Math.max(4, bayW * (0.7 + b.moduleDensity * 0.035));
300
+ const moduleH = Math.max(b.floorHeight * 1.35, levelH * (0.38 + b.articulation * 0.0022));
301
+ const moduleD = Math.max(5, b.depth * (0.23 + b.moduleDensity * 0.025));
302
+ const x = -b.width / 2 + bayW * (bay + 0.5) + p.x * 0.24;
303
+ const z = side * (frameDepth * 0.23 + moduleD * 0.2 + b.cantilever * 0.1) + jitter * b.depth;
304
+ bodies.push({
305
+ x,
306
+ y: base + levelH * (level + 0.5),
307
+ z,
308
+ w: moduleW,
309
+ h: Math.min(levelH * 0.84, moduleH),
310
+ d: moduleD,
311
+ ry: p.ry,
312
+ facade: true,
313
+ role: "mass",
314
+ });
315
+ if (b.articulation > 64 && chance % 3 === 0)
316
+ bodies.push({
317
+ x: x + side * bayW * 0.12,
318
+ y: base + levelH * (level + 0.72),
319
+ z: -z * 0.76,
320
+ w: moduleW * 0.58,
321
+ h: moduleH * 0.48,
322
+ d: moduleD * 0.78,
323
+ ry: -p.ry,
324
+ facade: true,
325
+ role: "mass",
326
+ });
327
+ }
328
+ for (let branch = 0; branch < b.branches; branch++) {
329
+ const side = branch % 2 ? 1 : -1;
330
+ const level = 1 + ((branch * 2 + seed) % Math.max(2, levels - 1));
331
+ const y = base + levelH * (level + 0.42);
332
+ const x = -b.width * 0.32 + (branch % Math.max(2, bays - 1)) * (b.width / Math.max(2, bays - 1));
333
+ const reach = frameDepth * 0.5 + b.cantilever + 5 + (branch % 3) * 2;
334
+ bodies.push({
335
+ x,
336
+ y: y - levelH * 0.13,
337
+ z: side * reach * 0.52,
338
+ w: Math.max(1.1, bayW * 0.7),
339
+ h: 0.72,
340
+ d: reach,
341
+ facade: false,
342
+ role: "transfer",
343
+ });
344
+ bodies.push({
345
+ x,
346
+ y,
347
+ z: side * (reach + 2 + b.moduleDensity * 0.7),
348
+ w: Math.max(5, bayW * 0.9),
349
+ h: Math.max(b.floorHeight * 1.5, levelH * 0.56),
350
+ d: Math.max(6, b.depth * 0.24),
351
+ facade: true,
352
+ role: "mass",
353
+ });
354
+ }
355
+ return bodies;
356
+ }
357
+ if (isVillageMassing(b)) {
358
+ const seed = hashMassingSeed(b.seed);
359
+ const columns = Math.round(clamp(b.moduleDensity + Math.floor(b.articulation / 42), 4, 7));
360
+ const rows = Math.round(clamp(Math.ceil(b.depth / 9), 2, 6));
361
+ const cellW = b.width / columns;
362
+ const cellD = b.depth / rows;
363
+ for (let row = 0; row < rows; row++)
364
+ for (let col = 0; col < columns; col++) {
365
+ const chance = (seed + row * 41 + col * 29) % 100;
366
+ if (chance < b.voids * 0.46)
367
+ continue;
368
+ const laneShift = (row % 2 ? 0.16 : -0.16) * cellW;
369
+ const x = -b.width / 2 + cellW * (col + 0.5) + laneShift;
370
+ const z = -b.depth / 2 + cellD * (row + 0.5);
371
+ const variation = ((seed + row * 13 + col * 17) % 7) / 6;
372
+ const h = clamp(b.height * (0.38 + variation * 0.48), Math.max(5, b.floorHeight * 1.6), b.height * 0.9);
373
+ bodies.push({
374
+ x,
375
+ y: base + h / 2,
376
+ z,
377
+ w: Math.max(3.2, cellW * (0.62 + variation * 0.1)),
378
+ h,
379
+ d: Math.max(3.5, cellD * 0.68),
380
+ facade: true,
381
+ role: "mass",
382
+ });
383
+ }
384
+ if (b.crown > 54)
385
+ bodies.push({
386
+ x: b.width * 0.18,
387
+ y: base + b.height * 0.48,
388
+ z: -b.depth * 0.12,
389
+ w: Math.max(3.8, cellW * 0.7),
390
+ h: b.height * 0.96,
391
+ d: Math.max(4, cellD * 0.72),
392
+ facade: true,
393
+ role: "mass",
394
+ });
395
+ return bodies;
396
+ }
397
+ const needsSpine = b.cantilever > 2 ||
398
+ b.profile === "offset" ||
399
+ b.profile === "twisted" ||
400
+ composition === "stack" ||
401
+ composition === "cluster";
402
+ if (needsSpine)
403
+ bodies.push({
404
+ x: 0,
405
+ y: base + usable / 2,
406
+ z: 0,
407
+ w: Math.max(3.8, b.width * 0.14),
408
+ h: usable,
409
+ d: Math.max(4.2, b.depth * 0.22),
410
+ facade: false,
411
+ role: "core",
412
+ });
413
+ if (composition === "bridge") {
414
+ const coreW = Math.max(3.8, b.width * (0.14 + (100 - b.voids) * 0.0011));
415
+ const spread = Math.max(coreW * 0.7, b.width * (0.3 + b.voids * 0.0015));
416
+ const structureH = base + usable;
417
+ bodies.push({ x: -spread, y: structureH / 2, z: 0, w: coreW, h: structureH, d: b.depth * 0.72, facade: true, role: "core" }, { x: spread, y: structureH / 2, z: 0, w: coreW, h: structureH, d: b.depth * 0.72, facade: true, role: "core" });
418
+ for (let i = 0; i < tierCount; i++) {
419
+ const p = profileAt(i);
420
+ const bridgeH = Math.max(b.floorHeight * 1.45, tierH * (0.26 + b.voids * 0.002));
421
+ bodies.push({
422
+ x: p.x,
423
+ y: base + tierH * (i + 0.58),
424
+ z: p.z,
425
+ w: b.width * p.scaleW + Math.min(10, b.cantilever),
426
+ h: bridgeH,
427
+ d: b.depth * p.scaleD,
428
+ ry: p.ry,
429
+ facade: true,
430
+ });
431
+ }
432
+ return bodies;
433
+ }
434
+ for (let i = 0; i < tierCount; i++) {
435
+ const p = profileAt(i);
436
+ const y = base + tierH * (i + 0.5);
437
+ const h = tierH * 1.015;
438
+ const plateW = b.width * p.scaleW;
439
+ const plateD = b.depth * p.scaleD;
440
+ if (i > 0 && (b.cantilever > 1.2 || composition === "stack" || composition === "cluster"))
441
+ bodies.push({
442
+ x: p.x * 0.5,
443
+ y: base + tierH * i + 0.34,
444
+ z: p.z * 0.5,
445
+ w: Math.min(b.width + Math.abs(p.x), plateW + Math.abs(p.x) + 2),
446
+ h: 0.68,
447
+ d: Math.min(b.depth + Math.abs(p.z), plateD + Math.abs(p.z) + 1),
448
+ ry: p.ry * 0.5,
449
+ facade: false,
450
+ role: "transfer",
451
+ });
452
+ if (composition === "split") {
453
+ const gap = clamp(plateW * (0.08 + b.voids * 0.0042), 2, plateW * 0.48);
454
+ const wing = Math.max(2.8, (plateW - gap) / 2);
455
+ bodies.push({ x: p.x - (gap + wing) / 2, y, z: p.z, w: wing, h, d: plateD, ry: p.ry, facade: true }, { x: p.x + (gap + wing) / 2, y, z: p.z, w: wing, h, d: plateD, ry: p.ry, facade: true });
456
+ if (i % 2 === 1 || tierCount === 1)
457
+ bodies.push({
458
+ x: p.x,
459
+ y: y + h * 0.2,
460
+ z: p.z,
461
+ w: gap + 1.2,
462
+ h: Math.min(h * 0.32, b.floorHeight * 1.4),
463
+ d: plateD + Math.min(4, b.cantilever),
464
+ ry: p.ry,
465
+ facade: true,
466
+ });
467
+ }
468
+ else if (composition === "cluster") {
469
+ const moduleW = Math.max(4.2, plateW * (plateW > 32 ? 0.36 : 0.42));
470
+ const moduleD = Math.max(4.2, plateD * 0.42);
471
+ const drift = (i % 2 ? 1 : -1) * (b.cantilever * 0.28 + b.voids * 0.025);
472
+ bodies.push({ x: p.x - plateW * 0.27 + drift, y, z: p.z - plateD * 0.25, w: moduleW, h, d: moduleD, ry: p.ry, facade: true }, { x: p.x + plateW * 0.27 + drift, y, z: p.z + plateD * 0.25, w: moduleW, h, d: moduleD, ry: p.ry, facade: true });
473
+ if (b.moduleDensity > 3 || plateD > 21 || plateW > 34)
474
+ bodies.push({
475
+ x: p.x,
476
+ y: y - h * 0.1,
477
+ z: p.z + plateD * 0.3,
478
+ w: moduleW * 0.72,
479
+ h: h * 0.8,
480
+ d: moduleD * 0.76,
481
+ ry: p.ry,
482
+ facade: true,
483
+ });
484
+ if (b.moduleDensity === 5 && plateW > 42)
485
+ bodies.push({
486
+ x: p.x - plateW * 0.08,
487
+ y: y + h * 0.12,
488
+ z: p.z - plateD * 0.34,
489
+ w: moduleW * 0.62,
490
+ h: h * 0.76,
491
+ d: moduleD * 0.68,
492
+ ry: p.ry,
493
+ facade: true,
494
+ });
495
+ }
496
+ else if (composition === "stack") {
497
+ const alternate = i % 2 === 0 ? -1 : 1;
498
+ const drift = alternate * (b.cantilever * 0.42 + b.voids * 0.035);
499
+ bodies.push({
500
+ x: p.x + drift,
501
+ y,
502
+ z: p.z - drift * 0.35,
503
+ w: plateW * (i % 3 === 0 ? 0.92 : 1),
504
+ h,
505
+ d: plateD * (i % 2 === 0 ? 1 : 0.82),
506
+ ry: p.ry,
507
+ facade: true,
508
+ });
509
+ if (i % Math.max(1, 5 - b.moduleDensity) === 0)
510
+ bodies.push({
511
+ x: p.x - drift * 0.7,
512
+ y,
513
+ z: p.z + plateD * 0.28,
514
+ w: plateW * 0.46,
515
+ h: h * 0.72,
516
+ d: plateD * 0.48,
517
+ ry: p.ry,
518
+ facade: true,
519
+ });
520
+ }
521
+ else if (composition === "court") {
522
+ const edge = clamp(Math.min(plateW, plateD) * (0.13 + (100 - b.voids) * 0.0013), 3.2, 7.5);
523
+ const sideDepth = Math.max(3, plateD - edge * 2);
524
+ bodies.push({ x: p.x, y, z: p.z - plateD / 2 + edge / 2, w: plateW, h, d: edge, ry: p.ry, facade: true }, { x: p.x, y, z: p.z + plateD / 2 - edge / 2, w: plateW, h, d: edge, ry: p.ry, facade: true }, { x: p.x - plateW / 2 + edge / 2, y, z: p.z, w: edge, h, d: sideDepth, ry: p.ry, facade: true }, { x: p.x + plateW / 2 - edge / 2, y, z: p.z, w: edge, h, d: sideDepth, ry: p.ry, facade: true });
525
+ }
526
+ else if (b.articulation > 28 && plateW > 12) {
527
+ const pieces = Math.round(clamp(1 + Math.floor(b.articulation / 20) + Math.max(0, b.moduleDensity - 3), 2, 7));
528
+ const gap = clamp(0.35 + b.articulation * 0.018, 0.7, 2.2);
529
+ const pieceW = Math.max(3, (plateW - gap * (pieces - 1)) / pieces);
530
+ for (let piece = 0; piece < pieces; piece++) {
531
+ const variance = ((piece * 7 + i * 3) % 5) / 4;
532
+ const pieceH = h * (0.78 + variance * 0.22);
533
+ const pieceY = base + tierH * i + pieceH / 2;
534
+ const x = p.x - plateW / 2 + pieceW / 2 + piece * (pieceW + gap);
535
+ const z = p.z + (((piece + i) % 3) - 1) * b.articulation * 0.022;
536
+ bodies.push({
537
+ x: x + b.cantilever * 0.08,
538
+ y: pieceY,
539
+ z,
540
+ w: pieceW,
541
+ h: pieceH,
542
+ d: plateD * (0.8 + ((piece + i) % 2) * 0.2),
543
+ ry: p.ry,
544
+ facade: true,
545
+ role: "mass",
546
+ });
547
+ }
548
+ }
549
+ else {
550
+ bodies.push({ x: p.x + b.cantilever * 0.08, y, z: p.z, w: plateW, h, d: plateD, ry: p.ry, facade: true, role: "mass" });
551
+ }
552
+ }
553
+ if (b.branches > 0 && composition !== "court")
554
+ for (let branch = 0; branch < b.branches; branch++) {
555
+ const side = branch % 2 ? 1 : -1;
556
+ const y = base + usable * (0.26 + ((branch * 29) % 55) / 100);
557
+ const x = ((branch % 3) - 1) * b.width * 0.24;
558
+ const reach = b.depth * 0.5 + 3 + b.cantilever + (branch % 2) * 2;
559
+ bodies.push({
560
+ x,
561
+ y,
562
+ z: side * reach * 0.52,
563
+ w: Math.max(1.1, b.width * 0.14),
564
+ h: 0.55,
565
+ d: reach,
566
+ facade: false,
567
+ role: "transfer",
568
+ });
569
+ bodies.push({
570
+ x,
571
+ y: y + Math.max(2, b.floorHeight * 0.8),
572
+ z: side * (reach + 2),
573
+ w: Math.max(5, b.width * (0.16 + b.moduleDensity * 0.018)),
574
+ h: Math.max(b.floorHeight * 1.6, usable * 0.12),
575
+ d: Math.max(5, b.depth * 0.34),
576
+ facade: true,
577
+ role: "mass",
578
+ });
579
+ }
580
+ return bodies;
581
+ }
@@ -18,7 +18,11 @@ export interface MinimapView {
18
18
  worldRadius: number;
19
19
  /** Pixel diameter of the minimap. */
20
20
  size: number;
21
- /** Rotate the projection so this facing bearing points up (rotating minimap). Omit for north-up. */
21
+ /**
22
+ * Compass bearing that points up (rotating minimap) — pass
23
+ * `headingToBearing(yaw)` so the map spins under a fixed north-up player
24
+ * arrow. Omit for north-up.
25
+ */
22
26
  rotate?: number;
23
27
  }
24
28
  export interface MinimapPoint {
@@ -35,6 +39,14 @@ export interface MinimapPoint {
35
39
  * `view.rotate` to spin the map under a fixed north-up player arrow.
36
40
  */
37
41
  export declare function projectToMinimap(world: WorldXZ | readonly [number, number, number], view: MinimapView): MinimapPoint;
42
+ /**
43
+ * Invert `projectToMinimap` (#285.6): minimap pixel → world XZ, rotate-aware —
44
+ * click-to-pin, tap-to-ping, drag-to-set-waypoint map interactions.
45
+ */
46
+ export declare function unprojectFromMinimap(point: {
47
+ x: number;
48
+ y: number;
49
+ }, view: MinimapView): WorldXZ;
38
50
  /** Clamp a projected point to the minimap edge, preserving direction (edge markers). */
39
51
  export declare function clampToMinimapEdge(point: MinimapPoint, size: number): {
40
52
  x: number;
@@ -44,8 +44,8 @@ export function projectToMinimap(world, view) {
44
44
  if (view.rotate !== undefined && view.rotate !== 0) {
45
45
  const cos = Math.cos(view.rotate);
46
46
  const sin = Math.sin(view.rotate);
47
- const rx = dx * cos - dz * sin;
48
- const rz = dx * sin + dz * cos;
47
+ const rx = dx * cos + dz * sin;
48
+ const rz = dz * cos - dx * sin;
49
49
  dx = rx;
50
50
  dz = rz;
51
51
  }
@@ -59,6 +59,25 @@ export function projectToMinimap(world, view) {
59
59
  inside: distance <= view.worldRadius,
60
60
  };
61
61
  }
62
+ /**
63
+ * Invert `projectToMinimap` (#285.6): minimap pixel → world XZ, rotate-aware —
64
+ * click-to-pin, tap-to-ping, drag-to-set-waypoint map interactions.
65
+ */
66
+ export function unprojectFromMinimap(point, view) {
67
+ const half = view.size / 2;
68
+ const scale = view.worldRadius === 0 ? 0 : half / view.worldRadius;
69
+ let dx = scale === 0 ? 0 : (point.x - half) / scale;
70
+ let dz = scale === 0 ? 0 : (point.y - half) / scale;
71
+ if (view.rotate !== undefined && view.rotate !== 0) {
72
+ const cos = Math.cos(view.rotate);
73
+ const sin = Math.sin(view.rotate);
74
+ const rx = dx * cos - dz * sin;
75
+ const rz = dz * cos + dx * sin;
76
+ dx = rx;
77
+ dz = rz;
78
+ }
79
+ return [view.center[0] + dx, view.center[1] + dz];
80
+ }
62
81
  /** Clamp a projected point to the minimap edge, preserving direction (edge markers). */
63
82
  export function clampToMinimapEdge(point, size) {
64
83
  const half = size / 2;