@v12-engine/engine 0.0.0-stage → 0.1.1

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 (341) hide show
  1. package/LICENSE +21 -0
  2. package/dist/abilities/buffs.d.ts +83 -0
  3. package/dist/abilities/castTold.d.ts +12 -0
  4. package/dist/abilities/charges.d.ts +28 -0
  5. package/dist/abilities/dispel.d.ts +9 -0
  6. package/dist/abilities/effectFields.d.ts +10 -0
  7. package/dist/abilities/effects/buff.d.ts +8 -0
  8. package/dist/abilities/effects/core.d.ts +1 -0
  9. package/dist/abilities/effects/damage.d.ts +1 -0
  10. package/dist/abilities/effects/heal.d.ts +2 -0
  11. package/dist/abilities/effects/missile.d.ts +1 -0
  12. package/dist/abilities/effects/moves.d.ts +11 -0
  13. package/dist/abilities/effects/passives.d.ts +15 -0
  14. package/dist/abilities/effects/sight.d.ts +4 -0
  15. package/dist/abilities/effects/spawn.d.ts +2 -0
  16. package/dist/abilities/effects/timed.d.ts +4 -0
  17. package/dist/abilities/effects/trade.d.ts +1 -0
  18. package/dist/abilities/effects.d.ts +122 -0
  19. package/dist/abilities/land.d.ts +21 -0
  20. package/dist/abilities/scaled.d.ts +21 -0
  21. package/dist/abilities/text.d.ts +113 -0
  22. package/dist/ai/area.d.ts +53 -0
  23. package/dist/ai/brain.d.ts +528 -0
  24. package/dist/ai/budget.d.ts +67 -0
  25. package/dist/ai/checks.d.ts +115 -0
  26. package/dist/ai/computer.d.ts +166 -0
  27. package/dist/ai/defence.d.ts +108 -0
  28. package/dist/ai/define.d.ts +21 -0
  29. package/dist/ai/economy.d.ts +129 -0
  30. package/dist/ai/geometry.d.ts +35 -0
  31. package/dist/ai/goals.d.ts +360 -0
  32. package/dist/ai/index.d.ts +38 -0
  33. package/dist/ai/knowledge.d.ts +418 -0
  34. package/dist/ai/lanes.d.ts +232 -0
  35. package/dist/ai/melee/build.d.ts +61 -0
  36. package/dist/ai/melee/goals.d.ts +299 -0
  37. package/dist/ai/melee/heroes.d.ts +27 -0
  38. package/dist/ai/melee/index.d.ts +14 -0
  39. package/dist/ai/melee/production.d.ts +8 -0
  40. package/dist/ai/melee/surrender.d.ts +24 -0
  41. package/dist/ai/micro.d.ts +278 -0
  42. package/dist/ai/mission.d.ts +195 -0
  43. package/dist/ai/missionRules.d.ts +18 -0
  44. package/dist/ai/module.d.ts +116 -0
  45. package/dist/ai/options.d.ts +61 -0
  46. package/dist/ai/plans/build.d.ts +280 -0
  47. package/dist/ai/plans/placement.d.ts +82 -0
  48. package/dist/ai/plans/plans.d.ts +105 -0
  49. package/dist/ai/plans/production.d.ts +210 -0
  50. package/dist/ai/plans/purse.d.ts +73 -0
  51. package/dist/ai/plans/tech.d.ts +37 -0
  52. package/dist/ai/profiles.d.ts +204 -0
  53. package/dist/ai/reach.d.ts +4 -0
  54. package/dist/ai/report.d.ts +203 -0
  55. package/dist/ai/roles.d.ts +133 -0
  56. package/dist/ai/rules.d.ts +288 -0
  57. package/dist/ai/scouting.d.ts +98 -0
  58. package/dist/ai/settings.d.ts +73 -0
  59. package/dist/ai/squads.d.ts +429 -0
  60. package/dist/ai/strength.d.ts +167 -0
  61. package/dist/ai/task.d.ts +118 -0
  62. package/dist/ai/tasks/attack.d.ts +7 -0
  63. package/dist/ai/tasks/capture.d.ts +21 -0
  64. package/dist/ai/tasks/creep.d.ts +16 -0
  65. package/dist/ai/tasks/defend.d.ts +29 -0
  66. package/dist/ai/tasks/drop.d.ts +34 -0
  67. package/dist/ai/tasks/escort.d.ts +16 -0
  68. package/dist/ai/tasks/gather.d.ts +23 -0
  69. package/dist/ai/tasks/harass.d.ts +7 -0
  70. package/dist/ai/tasks/hold.d.ts +20 -0
  71. package/dist/ai/tasks/index.d.ts +32 -0
  72. package/dist/ai/tasks/move.d.ts +19 -0
  73. package/dist/ai/tasks/patrol.d.ts +8 -0
  74. package/dist/ai/tasks/retreat.d.ts +20 -0
  75. package/dist/ai/tasks/scout.d.ts +29 -0
  76. package/dist/ai/tasks/shared.d.ts +74 -0
  77. package/dist/ai/team.d.ts +155 -0
  78. package/dist/ai/threat.d.ts +91 -0
  79. package/dist/ai/view.d.ts +366 -0
  80. package/dist/ai/waves.d.ts +126 -0
  81. package/dist/ai.js +6239 -0
  82. package/dist/ai.js.map +1 -0
  83. package/dist/aiTesting.d.ts +179 -0
  84. package/dist/areas.d.ts +149 -0
  85. package/dist/attacks.d.ts +66 -0
  86. package/dist/attributes.d.ts +66 -0
  87. package/dist/brain-BvVdUhX8.js +7659 -0
  88. package/dist/brain-BvVdUhX8.js.map +1 -0
  89. package/dist/builds-C6LfgvEU.js +460 -0
  90. package/dist/builds-C6LfgvEU.js.map +1 -0
  91. package/dist/bytes.d.ts +6 -0
  92. package/dist/camps.d.ts +95 -0
  93. package/dist/capture.d.ts +131 -0
  94. package/dist/cellLayers.d.ts +75 -0
  95. package/dist/cinematics.d.ts +194 -0
  96. package/dist/clock.d.ts +32 -0
  97. package/dist/commands.d.ts +235 -0
  98. package/dist/components.d.ts +184 -0
  99. package/dist/computers.d.ts +108 -0
  100. package/dist/concealment.d.ts +61 -0
  101. package/dist/coreComponents-syuh2fPG.js +5149 -0
  102. package/dist/coreComponents-syuh2fPG.js.map +1 -0
  103. package/dist/coreComponents.d.ts +1 -0
  104. package/dist/dashing.d.ts +8 -0
  105. package/dist/debug.d.ts +36 -0
  106. package/dist/debug.js +3 -0
  107. package/dist/debugCommands-CzjrSm66.js +14739 -0
  108. package/dist/debugCommands-CzjrSm66.js.map +1 -0
  109. package/dist/debugCommands.d.ts +213 -0
  110. package/dist/debugging.d.ts +10 -0
  111. package/dist/definitions.d.ts +43 -0
  112. package/dist/defs.d.ts +1162 -0
  113. package/dist/denying.d.ts +81 -0
  114. package/dist/destructibles.d.ts +27 -0
  115. package/dist/determinism-BCymQwiE.js +518 -0
  116. package/dist/determinism-BCymQwiE.js.map +1 -0
  117. package/dist/determinism.d.ts +121 -0
  118. package/dist/dialog.d.ts +94 -0
  119. package/dist/effectRefs.d.ts +16 -0
  120. package/dist/effects.d.ts +80 -0
  121. package/dist/endings.d.ts +22 -0
  122. package/dist/engagement.d.ts +108 -0
  123. package/dist/engineEvents.d.ts +710 -0
  124. package/dist/entity.d.ts +409 -0
  125. package/dist/entityIndex.d.ts +50 -0
  126. package/dist/errors.d.ts +85 -0
  127. package/dist/events.d.ts +97 -0
  128. package/dist/exactMath.d.ts +63 -0
  129. package/dist/features.d.ts +10 -0
  130. package/dist/fields.d.ts +114 -0
  131. package/dist/fixture.d.ts +17 -0
  132. package/dist/fog.d.ts +110 -0
  133. package/dist/formation.d.ts +111 -0
  134. package/dist/friendlyFire.d.ts +26 -0
  135. package/dist/game-DiJEN-Ks.js +5800 -0
  136. package/dist/game-DiJEN-Ks.js.map +1 -0
  137. package/dist/game.d.ts +501 -0
  138. package/dist/gameData.d.ts +325 -0
  139. package/dist/gameFiles.d.ts +26 -0
  140. package/dist/gates.d.ts +28 -0
  141. package/dist/generate.d.ts +403 -0
  142. package/dist/generateWorker.d.ts +39 -0
  143. package/dist/grid.d.ts +53 -0
  144. package/dist/ground.d.ts +127 -0
  145. package/dist/groundSpeed.d.ts +22 -0
  146. package/dist/hero.d.ts +177 -0
  147. package/dist/home.d.ts +14 -0
  148. package/dist/hooks.d.ts +780 -0
  149. package/dist/hotReload.d.ts +76 -0
  150. package/dist/ids.d.ts +26 -0
  151. package/dist/index.d.ts +227 -0
  152. package/dist/index.js +4701 -0
  153. package/dist/index.js.map +1 -0
  154. package/dist/inspect-D9dWHmZ0.js +491 -0
  155. package/dist/inspect-D9dWHmZ0.js.map +1 -0
  156. package/dist/inspect.d.ts +176 -0
  157. package/dist/inventory.d.ts +18 -0
  158. package/dist/items.d.ts +91 -0
  159. package/dist/lanes.d.ts +213 -0
  160. package/dist/life.d.ts +53 -0
  161. package/dist/local-Dkovq42k.js +1379 -0
  162. package/dist/local-Dkovq42k.js.map +1 -0
  163. package/dist/log.d.ts +140 -0
  164. package/dist/manaBurn.d.ts +39 -0
  165. package/dist/map/file.d.ts +114 -0
  166. package/dist/map/images.d.ts +18 -0
  167. package/dist/map/look.d.ts +47 -0
  168. package/dist/map/pathProfile.d.ts +13 -0
  169. package/dist/map/resize.d.ts +54 -0
  170. package/dist/map/schema.d.ts +10 -0
  171. package/dist/map/terrain.d.ts +32 -0
  172. package/dist/map/types.d.ts +613 -0
  173. package/dist/map.d.ts +7 -0
  174. package/dist/mapCheck/cells.d.ts +19 -0
  175. package/dist/mapCheck/rule.d.ts +30 -0
  176. package/dist/mapCheck/rules/names.d.ts +3 -0
  177. package/dist/mapCheck/rules/objects.d.ts +3 -0
  178. package/dist/mapCheck/rules/paths.d.ts +3 -0
  179. package/dist/mapCheck/rules/players.d.ts +3 -0
  180. package/dist/mapCheck/rules/races.d.ts +3 -0
  181. package/dist/mapCheck/rules/random.d.ts +3 -0
  182. package/dist/mapCheck/rules/regions.d.ts +3 -0
  183. package/dist/mapCheck/rules/targets.d.ts +3 -0
  184. package/dist/mapCheck/rules/walking.d.ts +3 -0
  185. package/dist/mapCheck.d.ts +23 -0
  186. package/dist/mapFields.d.ts +364 -0
  187. package/dist/mapFile.d.ts +23 -0
  188. package/dist/mapMeasure.d.ts +95 -0
  189. package/dist/mapMerge.d.ts +19 -0
  190. package/dist/mapObjectives.d.ts +82 -0
  191. package/dist/mapObjects.d.ts +1 -0
  192. package/dist/math.d.ts +44 -0
  193. package/dist/messages.d.ts +150 -0
  194. package/dist/missionRules-C-bGxm0Q.js +523 -0
  195. package/dist/missionRules-C-bGxm0Q.js.map +1 -0
  196. package/dist/missionRules.d.ts +179 -0
  197. package/dist/mods.d.ts +146 -0
  198. package/dist/names.d.ts +93 -0
  199. package/dist/navigation/agentParams.d.ts +4 -0
  200. package/dist/navigation/air.d.ts +89 -0
  201. package/dist/navigation/builds.d.ts +140 -0
  202. package/dist/navigation/groundAreas.d.ts +23 -0
  203. package/dist/navigation/navigation.d.ts +127 -0
  204. package/dist/navigation/recast.d.ts +214 -0
  205. package/dist/navigation/sidePaths.d.ts +69 -0
  206. package/dist/navigation/sized.d.ts +1 -0
  207. package/dist/navigation-worker.js +11 -0
  208. package/dist/navigation-worker.js.map +1 -0
  209. package/dist/nearest.d.ts +8 -0
  210. package/dist/noise.d.ts +12 -0
  211. package/dist/objectives.d.ts +93 -0
  212. package/dist/orders/basic.d.ts +96 -0
  213. package/dist/orders/build.d.ts +91 -0
  214. package/dist/orders/cast.d.ts +105 -0
  215. package/dist/orders/facts.d.ts +14 -0
  216. package/dist/orders/harvest.d.ts +94 -0
  217. package/dist/orders/idle.d.ts +7 -0
  218. package/dist/orders/items.d.ts +14 -0
  219. package/dist/orders/queue.d.ts +24 -0
  220. package/dist/pack.schema.json +6621 -0
  221. package/dist/packs/director/index.d.ts +81 -0
  222. package/dist/packs/heroes/campaign.d.ts +137 -0
  223. package/dist/packs/heroes/carry.d.ts +59 -0
  224. package/dist/packs/heroes/commands.d.ts +6 -0
  225. package/dist/packs/heroes/hire.d.ts +9 -0
  226. package/dist/packs/heroes/index.d.ts +12 -0
  227. package/dist/packs/heroes/making.d.ts +16 -0
  228. package/dist/packs/heroes/respawn.d.ts +42 -0
  229. package/dist/packs/moba/index.d.ts +34 -0
  230. package/dist/packs/pack.d.ts +84 -0
  231. package/dist/packs-g5thvmGn.js +3199 -0
  232. package/dist/packs-g5thvmGn.js.map +1 -0
  233. package/dist/packs.d.ts +84 -0
  234. package/dist/paths.d.ts +51 -0
  235. package/dist/plain.d.ts +17 -0
  236. package/dist/player.d.ts +117 -0
  237. package/dist/portals.d.ts +66 -0
  238. package/dist/prefabs.d.ts +139 -0
  239. package/dist/presetBuildings.d.ts +52 -0
  240. package/dist/presetGround.d.ts +22 -0
  241. package/dist/profiles-B3NS9jwk.js +335 -0
  242. package/dist/profiles-B3NS9jwk.js.map +1 -0
  243. package/dist/races.d.ts +136 -0
  244. package/dist/randomGroups.d.ts +2 -0
  245. package/dist/registry.d.ts +464 -0
  246. package/dist/remains.d.ts +119 -0
  247. package/dist/replication.d.ts +103 -0
  248. package/dist/rng.d.ts +17 -0
  249. package/dist/rules.d.ts +395 -0
  250. package/dist/rulesCheck.d.ts +37 -0
  251. package/dist/runes.d.ts +41 -0
  252. package/dist/save.d.ts +277 -0
  253. package/dist/scenario.d.ts +56 -0
  254. package/dist/scheduler.d.ts +67 -0
  255. package/dist/score.d.ts +306 -0
  256. package/dist/sequences.d.ts +132 -0
  257. package/dist/server/doorman.d.ts +21 -0
  258. package/dist/server/gameServer.d.ts +346 -0
  259. package/dist/server/index.d.ts +24 -0
  260. package/dist/server/lobby.d.ts +1 -0
  261. package/dist/server/log.d.ts +13 -0
  262. package/dist/server/match.d.ts +24 -0
  263. package/dist/server/mode.d.ts +168 -0
  264. package/dist/server/navigation-worker.d.ts +1 -0
  265. package/dist/server/navigation.d.ts +11 -0
  266. package/dist/server/ops.d.ts +78 -0
  267. package/dist/server/packs.d.ts +15 -0
  268. package/dist/server/ratings.d.ts +62 -0
  269. package/dist/server/relay.d.ts +34 -0
  270. package/dist/server/rooms.d.ts +162 -0
  271. package/dist/server/seat.d.ts +2 -0
  272. package/dist/server/seatSettings.d.ts +15 -0
  273. package/dist/server/secret.d.ts +5 -0
  274. package/dist/server/start.d.ts +54 -0
  275. package/dist/server/websocket.d.ts +74 -0
  276. package/dist/server.js +2861 -0
  277. package/dist/server.js.map +1 -0
  278. package/dist/session/can.d.ts +20 -0
  279. package/dist/session/commandReplay.d.ts +92 -0
  280. package/dist/session/debug.d.ts +65 -0
  281. package/dist/session/delay.d.ts +15 -0
  282. package/dist/session/delta.d.ts +98 -0
  283. package/dist/session/entityState.d.ts +1 -0
  284. package/dist/session/local.d.ts +214 -0
  285. package/dist/session/memory.d.ts +27 -0
  286. package/dist/session/protocol.d.ts +1155 -0
  287. package/dist/session/remote.d.ts +530 -0
  288. package/dist/session/replay.d.ts +114 -0
  289. package/dist/session/seed.d.ts +7 -0
  290. package/dist/session/share.d.ts +78 -0
  291. package/dist/session/socket.d.ts +8 -0
  292. package/dist/session/wire.d.ts +153 -0
  293. package/dist/session/worker.d.ts +227 -0
  294. package/dist/shared.d.ts +171 -0
  295. package/dist/sides.d.ts +75 -0
  296. package/dist/sight.d.ts +27 -0
  297. package/dist/simulate-oXBiw1Ff.js +167 -0
  298. package/dist/simulate-oXBiw1Ff.js.map +1 -0
  299. package/dist/simulate-worker.d.ts +1 -0
  300. package/dist/simulate-worker.js +28 -0
  301. package/dist/simulate-worker.js.map +1 -0
  302. package/dist/simulate.d.ts +154 -0
  303. package/dist/smart.d.ts +9 -0
  304. package/dist/spatial.d.ts +62 -0
  305. package/dist/spawning.d.ts +33 -0
  306. package/dist/stances.d.ts +76 -0
  307. package/dist/start.d.ts +71 -0
  308. package/dist/stats.d.ts +122 -0
  309. package/dist/summons.d.ts +63 -0
  310. package/dist/systems/abilities.d.ts +3 -0
  311. package/dist/systems/combat.d.ts +71 -0
  312. package/dist/systems/combatTold.d.ts +14 -0
  313. package/dist/systems/construction.d.ts +34 -0
  314. package/dist/systems/economy.d.ts +21 -0
  315. package/dist/systems/fog.d.ts +19 -0
  316. package/dist/systems/life.d.ts +3 -0
  317. package/dist/systems/movement.d.ts +105 -0
  318. package/dist/systems/orders.d.ts +7 -0
  319. package/dist/systems/production.d.ts +49 -0
  320. package/dist/systems/productionQueue.d.ts +40 -0
  321. package/dist/systems/victory.d.ts +14 -0
  322. package/dist/talk.d.ts +65 -0
  323. package/dist/tech.d.ts +62 -0
  324. package/dist/techTree.d.ts +38 -0
  325. package/dist/terrain.d.ts +173 -0
  326. package/dist/terrainSave.d.ts +29 -0
  327. package/dist/testing.d.ts +58 -0
  328. package/dist/testing.js +822 -0
  329. package/dist/testing.js.map +1 -0
  330. package/dist/tiles.d.ts +34 -0
  331. package/dist/timers.d.ts +146 -0
  332. package/dist/tomes.d.ts +48 -0
  333. package/dist/trade.d.ts +28 -0
  334. package/dist/transport.d.ts +76 -0
  335. package/dist/tunables.d.ts +45 -0
  336. package/dist/upkeep.d.ts +16 -0
  337. package/dist/veterancy.d.ts +80 -0
  338. package/dist/walls.d.ts +86 -0
  339. package/dist/world.d.ts +1118 -0
  340. package/package.json +80 -5
  341. package/README.md +0 -3
package/dist/index.js ADDED
@@ -0,0 +1,4701 @@
1
+ import { $ as MissingGround, $c as refund, $l as commandHandled, $n as cinematicCommand, $o as REPATH_SPREAD, $r as defineHookType, $t as playerSlot, $u as xpGained, A as drawFromGroup, Aa as bool, Ac as readyAgain, Ad as inventorySlots, Ai as raceOf, Al as replicateEvent, An as logLine, Ar as allowed, As as defaultPickAttack, At as generateTerrain, Au as missileRemoved, B as fieldOptions, Ba as point, Bc as missingTech, Bd as angleTo, Bl as campCleared, Br as boardOrder, Bs as applyBuff, Bt as BLOCKS_WALK, Bu as projectileSpawned, Ca as fitToPlaces, Cc as xpWorth, Cd as innerEffects, Ci as unlockRefusal, Cl as scaledParts, Cn as withData, Co as waitUntil, Cr as lastTeamStanding, Cs as isShunned, Ct as updateMapTerrain, Cu as leftArea, Da as rowsFormation, Dd as passiveEffects, Di as defineRace, Dn as consoleSink, Dr as shareWith, Ds as spotFor, Dt as WATER_DEEP, Du as messageSent, Ea as noneFormation, Ec as queueOf, Ed as noStats, Ei as checkRaces, El as buffEnded, En as Logger, Er as give, Es as shun, Et as Terrain, Eu as manaChanged, F as tableItems, Fa as list, Fc as availableTo, Fd as orderSays, Fi as nearestId, Fl as attackStarted, Fn as dispelBuffs, Fs as rangeAgainst, Ft as LEGACY_TILESET, Fu as pauseAsked, G as mapPlayerData, Ga as defineEffect, Gc as engineMessage, Gd as distToRect, Gi as castTimeOf, Gl as channelEnded, Gn as takeCorpses, Gr as roomIn, Gs as isRooted, Gt as MAP_NEUTRAL, Gu as rustled, H as mapCampData, Ha as seconds, Hc as sellValue, Hd as closestPointOnRect, Hi as footprintCell, Hl as castStarted, Hn as summonEffects, Hr as cargo, Ht as DEFAULT_MAX_PLAYERS, Hu as resourceChanged, I as DEFAULT_HEIGHTS, Ia as number, Ic as availableToPlayer, Id as spawnStart, Ii as canHarvest, Il as boarded, In as dispelEffects, Is as reaches, It as MAX_TILES, Iu as pinged, J as objectFields, Jc as Player, Jd as lerp, Jl as checkpoint, Jn as autocastCommand, Js as isStunned, Jt as MAX_MAP_SIZE, Ju as unitHired, K as mapRegionData, Ka as effectTargets, Kc as toMessage, Kd as hashInts, Ki as declaredRefusal, Kl as channelStarted, Kn as worthless, Kr as shoreNear, Ks as isSilenced, Kt as MAX_LEVEL, Ku as teamChanged, La as object, Lc as chargesLeft, Ld as startingBase, Li as gathering, Ll as bounty, Ln as Path, Lr as BOARD_REACH, Ls as tooNear, Lt as defineTile, Lu as playerDefeated, Ma as ids, Mc as restoreCharges, Mi as setRace, Ml as teamMemory, Mn as rootLogger, Mr as ping, Ms as farthestReach, Mt as mapTileIds, Mu as morphCancelled, N as groupChoices, Na as index, Nc as abilityReach, Nd as defineComponent, Nl as abilityCast, Nn as coreEffects, Nr as pingKinds, Ns as groundAttack, Nt as playTileset, Nu as morphStarted, Oa as squareFormation, Oc as upgradeOf, Od as upgradesOf, Oi as isAvailable, Ol as SessionView, On as createLogRing, Or as CHAT_LENGTH, Os as ATTACK_STATS, Ot as WATER_NONE, Ou as missed, P as mapDrops, Pa as isObject, Pc as abilityRefusal, Pd as snapshotOf, Pi as editDistance, Pl as abilityImpact, Pn as homeOf, Po as plain, Ps as pickAttack, Pu as objectiveChanged, Qc as pay, Qd as vec2, Qi as validTarget, Ql as cinematicEnded, Qn as cinematicChoiceCommand, Qo as ARRIVE_RANGE, Qr as defineHook, Qs as silences, Qt as createMap, Qu as worldFailed, R as campFields, Ra as oneOf, Rc as costRefusal, Rd as TAU, Rl as buffApplied, Rn as corpseSystem, Ro as UPHILL_MISS, Rr as UNLOAD_REACH, Rs as AURA_LINGER, Rt as tileRemap, Ru as playerSurrendered, Sa as coreFormations, Sd as hourOf, Sl as formatNumber, Sn as valueAt, So as waitFor, Ss as isCrowded, St as tilesFor, Su as itemPicked, Ta as formationPoints, Tc as productionQueue, Td as isScenery, Ti as RANDOM_RACE, Tn as LOG_LEVELS, To as defineTimer, Tr as exchange, Ts as roomFor, Tt as profileReach, Tu as lineSaid, U as mapFields, Ua as string, Uc as targetRefusal, Ud as dist, Ul as cellsChanged, Un as summonHooks, Uo as canAttack, Ur as garrisonHooks, Us as hasBuff, Ut as MAP_CREEPS, Uu as resourcesDeposited, Va as rgb, Vc as requirementRefusal, Vd as clamp, Vi as BuildGrid, Vl as campReturned, Vn as illusion, Vo as attackReaches, Vr as canBoard, Vs as avoids, Vt as BRIDGE, Vu as researchFinished, W as mapObjectData, Wa as text, Wc as trainRefusal, Wd as distSq, Wi as castRange, Wl as cellsSeen, Wn as summoned, Wr as loadOrder, Ws as isCrowdControl, Wt as MAP_FORMAT, Wu as resourcesSpent, X as reachedByDefault, Xc as earn, Xd as rotateTowards, Xi as notReady, Xl as cinematicChoice, Xn as cancelTrainCommand, Xr as unloadOrder, Xs as removeBuff, Xt as PLAYER_COLORS, Xu as unloaded, Y as playerFields, Yc as currencyOf, Yd as lerpAngle, Yi as isSpell, Yl as chopped, Yn as cancelConstructionCommand, Yo as fireOnTheMove, Yr as unload, Ys as layerOf, Yt as MIN_MAP_SIZE, Yu as unitTrained, Z as regionFields, Za as startDash, Zc as giveBack, Zd as smoothstep, Zi as targetFacts, Zl as cinematicCued, Zn as castCommand, Zo as shootsUphill, Zs as shieldOf, Zt as RAMP_REACH, Zu as worked, _a as spendCharge, _c as reviveCost, _d as anyEffect, _l as underConstruction, _n as redefineIn, _o as defineSequence, _s as engageLayer, _t as unpackBytes, _u as heroLevelUp, aa as buy, ac as defaultHeroRules, ad as replicatedEvents, af as sin, ai as placementProblem, al as distanceToEdge, an as copyDefinition, ar as orderCommand, as as moveTo, at as groundHash, au as effectStarted, ba as FORMATION_GAP, bd as constructionOf, bi as morph, bl as abilityValues, bn as restoreRedefinitions, br as pausesLeft, bs as engagedWith, bt as mapCell, bu as inventoryChanged, ca as dropOnGround, cc as heroOfState, cd as cooldownOf, ci as siteOf, cl as fighting, cn as definitionIn, co as cueCinematic, cs as setAnchored, ct as savedGround, cu as entityDied, dc as heroesOf, dd as refreshVitals, dl as constructionSite, dn as editableValues, dr as smartCommand, ds as stepAside, dt as terrainPatch, du as entitySpawned, ea as createOrder, el as shortOf, en as COPY_OF, er as coreCommands, es as faceTowards, et as applyTerrainPatch, eu as commandRejected, fa as pickUp, fc as learn, fd as resisted, fn as effectFields, fr as stopCommand, fs as SHUN_TIME, ft as terrainPatchProblem, fu as environmentChanged, ga as shopStock, gc as respawning, gd as aimOf, gn as nested, gr as upgradeBuildingCommand, gs as defaultEngageRoom, gt as packBytes, gu as healed, ha as shopNear, hc as respawnIn, hd as CORE_STAT_KEYS, hi as upgradeLevel, hn as mergeValues, ho as Sequences, hr as trainCommand, hs as defaultEngageLayer, ht as saveTerrain, hu as groundReshaped, ia as atShop, ic as canLearn, if as hypot, ii as placementCells, il as detection, in as blankDefinition, is as makeWay, it as groundDifference, iu as damaged, j as drawFromTable, ja as id, jd as slotsOf, jl as snapshotPart, jn as rootConsole, jr as chat, js as farthestAcquisition, jt as Rng, ju as missileSpawned, ka as Invalid, kd as freeSlot, kn as errorFields, kr as PING_KINDS, ks as attackStat, kt as WATER_SHALLOW, ku as missileHit, la as findItem, lc as heroState, ld as invalidateStats, ll as moving, ln as definitionsIn, lo as playCinematic, lr as rallyCommand, ls as setGhost, lt as startedOn, lu as entityMorphed, ma as sell, mc as maxLevelOf, md as swingTiming, mn as fitValue, ms as bandOf, mt as loadTerrain, mu as gameOver, na as REACH, nc as scaled, nd as defineListener, nf as exactMath, ni as canRepair, nl as Entity, nn as NEW_DEFINITION, ns as isMovingTo, nt as findGround, nu as constructionFinished, oa as createItem, oc as grantXp, od as attackTiming, ol as edgeDistance, on as dataRulesProblem, oo as cinematic, os as moveToEntity, ot as groundOf, ou as effectStopped, pa as priceOf, pc as levelNeeded, pd as statsOf, pn as effectTypesOf, pr as surrenderCommand, ps as attackersOf, pt as withChanges, pu as exchanged, q as mapThings, qi as fire, ql as chatted, qn as attackCommand, qs as isSpellImmune, qt as MAX_MAP_PLAYERS, qu as traded, ra as SHOP_RANGE, rc as HEROES, rd as eventById, rl as deposit, rn as REQUIRED_FIELDS, ro as dashing, rs as lookAhead, rt as groundBase, ru as constructionStarted, sc as heroOf, sd as changedStats, sl as exposure, sn as definitionChanged, so as cinematicCue, sr as pauseCommand, st as isGroundChanges, su as enteredArea, ta as giveOrder, tc as casterFacts, td as defineEvent, tf as cos, ti as canPlaceStructure, tl as spend, tn as DATA_KINDS, tr as defineCommand, ts as halt, tt as cloneTerrain, tu as constructionCancelled, ua as giveItem, ud as mitigation, ui as playerFacts, ul as builtSoFar, un as editableFields, uo as regionCinematic, ur as researchCommand, us as standsInWay, ut as startingTerrain, uu as entityRemoved, va as takeItem, vc as reviveHero, vd as at, vi as meets, vl as STAT_WORDS, vn as redefinitions, vr as askPause, vs as engageWith, vt as adoptTileset, vu as heroRevived, wa as formationContext, wc as productionOf, wd as isNight, wi as unlocked, wl as valueText, wn as withValues, wo as Timers, ws as release, wt as PROFILE_SMOOTH, wu as lineEnded, xa as circleFormation, xd as footprintOf, xi as techOf, xl as describeAbility, xo as wait, xr as surrender, xs as isBlocked, xt as mapTerrain, xu as itemDropped, yc as setHeroLevel, yd as baseStats, yi as missing, yl as abilityText, yn as removeDefinition, ys as engagedLike, yt as blockByDoodads, yu as impact, z as defineMapField, za as optional, zc as gathers, zd as angleDiff, zi as harvesterOf, zl as buffRemoved, zn as corpses, zr as board, zt as BLOCKS_BUILD, zu as projectileRemoved } from "./debugCommands-CzjrSm66.js";
2
+ import { $ as siteWords, A as withConcealment, B as isMadeOf, C as patrolPathOrder, D as viewLevel, E as expose, F as ModError, G as defaultRules, H as satisfies, I as composeGame, J as groundSpeedOfCell, K as resolveRules, L as compositionOf, M as cellsIn, N as defineCellLayer, O as coreHooks, Q as siteKey, R as defineMod, S as followPathOrder, T as EXPOSED_FOR, U as checkDamageTable, V as recompose, W as damageMultiplier, Z as upkeepOf, _ as FogOfWar, a as createWorldFromMap, b as UnlistedError, c as World, d as prefabsFrom, f as spawnPrefab, g as FOG_VISIBLE, h as FOG_UNEXPLORED, i as createWorld, j as cellLayerValues, k as patchAt, l as definePrefab, m as FOG_EXPLORED, n as coreSystems, o as loadWorld, q as groundSpeedAt, r as createRegistry, s as maxPlayersOf, t as assembleGame, u as nestedPartId, v as MAX_FOG_TEAMS, w as SizedNavigation, x as attackPathOrder, y as Registry, z as gameHash } from "./game-DiJEN-Ks.js";
3
+ import { $ as ENTITY_DERIVED, A as mapLanes, At as tunings, B as captureAreaOf, C as LANE_SPAWNER_KIND, Cn as shapeReach, D as heroStrike, Dt as defineTunable, E as defaultWave, Et as spawnCamp, F as lastHitsOf, Ft as setGate, G as capturesOf, Gt as defineStat, H as capturePoint, I as mayDeny, K as capturing, Kt as engineStats, L as withLastHits, Lt as WorldMap, M as wavesSent, Mt as gateChanged, N as withLanes, Nt as gateStates, O as laneMinion, Ot as tunableChanged, P as denying, Pt as isGateOpen, R as CAPTURE_FIELDS, S as LANE_SPAWNER_FIELDS, Sn as shapeBounds, St as camps, T as coreVictory, Tt as respawnCamp, U as captured, V as captureOf, W as captures, Wt as Score, X as Scheduler, Y as PHASES, Yt as scoreData, Zt as scoreOn, _ as pileType, _n as defineArea, _t as campHooks, a as rankOf, b as LANE_END_REACH, bn as shapeAsBox, bt as campNamed, c as withVeterancy, ct as saveWorld, d as stance, dn as talk, dt as SidePaths, en as Objectives, et as ENTITY_SAVE, f as stanceCommand, ft as sideKnowledge, g as pileTaken, h as pileOf, hn as areaShapeOf, ht as DEFAULT_AVOIDANCE, i as grantVeterancy, in as playEffect, it as encodeSave, j as sendWave, jt as withTuning, k as lanesOf, kt as tuneCommand, l as DEFAULT_STANCE, ln as Dialog, m as withStances, mn as Areas, mt as AVOIDANCE_LEVELS, nn as objectiveVictory, nt as SaveError, o as rankedUp, on as stopEffect, ot as saveHeader, p as stanceOf, q as withCapture, qt as presetScore, r as VETERANCY_RANKS, rt as decodeSave, s as veterancy, st as saveProblem, tn as defineObjective, tt as SAVE_FORMAT, u as STANCES, un as LINE_SECONDS, ut as updateSave, v as rubbleType, vn as inShape, vt as campLayout, w as checkLanes, wt as campsOf, x as LANE_SPAWNERS, xn as shapeAt, xt as campRespawnSystem, y as withRemains, yn as shapeArea, yt as campMember, z as CAPTURE_KIND } from "./coreComponents-syuh2fPG.js";
4
+ import { a as navigationData, c as serveNavigationBuilds, i as memoryNavigationCache, o as navigationKey, s as navigationWorker, t as buildNavigationData } from "./builds-C6LfgvEU.js";
5
+ import { a as missionRulesOf, c as tellRules, d as checkMapObjectives, f as giveMapObjective, h as mapObjectivesOf, i as missionRuleState, l as MAP_OBJECTIVES, m as mapObjectives, n as MISSION_RULES, p as mapObjectivePlayers, r as checkMissionRules, s as ruleFired, t as MAP_RULES_KEY, u as MAP_OBJECTIVES_KEY } from "./missionRules-C-bGxm0Q.js";
6
+ import { A as connectAs, C as randomSeed, D as SessionHost, E as RemoteSession, F as readHostMessage, I as readPlayerMessage, M as loopback, N as toRuns, O as SessionRefused, P as isServerMessage, S as tilingNoise, _ as tilesPass, a as packsOf, b as mapCells, c as generateMap, d as resourcesPass, f as roadsPass, g as terrainPass, h as symmetricPlaces, i as packFromJson, j as fromRuns, k as connect, l as nearestDryGround, m as startsPass, n as checkPack, o as basinShape, p as scatterPass, r as checkPackSchema, s as clearGround, t as PackError, u as presetGenerator, v as MAP_RULES, w as delayed, x as layeredNoise, y as checkMap } from "./packs-g5thvmGn.js";
7
+ import { _ as hirerAt, c as makeHero, d as hireCommand, f as learnCommand, g as hireRefusal, h as hire, i as worldFingerprint, l as buyCommand, m as sellCommand, p as reviveCommand, r as saveDifference, s as withHeroes, u as heroCommands } from "./determinism-BCymQwiE.js";
8
+ import { _ as readMessage, d as paceOf, f as savesOf, g as protocolMismatch, h as isFiniteNumber, l as diff, m as isCommand, p as PROTOCOL_VERSION, s as apply, t as LocalSession, u as quantize } from "./local-Dkovq42k.js";
9
+ //#region src/map/schema.ts
10
+ /**
11
+ * Every field of a map, in the order its file has them (`format` before them, what the reader does not know of the
12
+ * file before that). `satisfies` checks that none of `GameMap` is left out
13
+ *
14
+ * @internal
15
+ */
16
+ var MAP_FIELDS = [
17
+ {
18
+ key: "name",
19
+ kind: "text",
20
+ written: "always",
21
+ empty: () => "Untitled"
22
+ },
23
+ {
24
+ key: "description",
25
+ kind: "text",
26
+ written: "always",
27
+ empty: () => ""
28
+ },
29
+ {
30
+ key: "game",
31
+ kind: "text",
32
+ written: "defined"
33
+ },
34
+ {
35
+ key: "width",
36
+ kind: "number",
37
+ written: "always",
38
+ required: true
39
+ },
40
+ {
41
+ key: "depth",
42
+ kind: "number",
43
+ written: "always",
44
+ required: true
45
+ },
46
+ {
47
+ key: "cliffHeight",
48
+ kind: "number",
49
+ written: "always",
50
+ empty: () => 3
51
+ },
52
+ {
53
+ key: "ground",
54
+ kind: "layer",
55
+ written: "always",
56
+ required: true,
57
+ perVertex: true,
58
+ what: "the ground"
59
+ },
60
+ {
61
+ key: "levels",
62
+ kind: "layer",
63
+ written: "always",
64
+ required: true,
65
+ what: "the levels"
66
+ },
67
+ {
68
+ key: "ramps",
69
+ kind: "layer",
70
+ written: "always",
71
+ required: true,
72
+ what: "the ramps"
73
+ },
74
+ {
75
+ key: "water",
76
+ kind: "layer",
77
+ written: "always",
78
+ required: true,
79
+ what: "the water"
80
+ },
81
+ {
82
+ key: "tiles",
83
+ kind: "layer",
84
+ written: "always",
85
+ required: true,
86
+ what: "the paint"
87
+ },
88
+ {
89
+ key: "cells",
90
+ kind: "layers",
91
+ written: "filled"
92
+ },
93
+ {
94
+ key: "pathing",
95
+ kind: "mask",
96
+ written: "filled",
97
+ what: "what is kept off cells"
98
+ },
99
+ {
100
+ key: "tileset",
101
+ kind: "names",
102
+ written: "always",
103
+ required: true,
104
+ what: "the tileset"
105
+ },
106
+ {
107
+ key: "players",
108
+ kind: "list",
109
+ written: "always",
110
+ required: true,
111
+ byId: true
112
+ },
113
+ {
114
+ key: "objects",
115
+ kind: "list",
116
+ written: "always",
117
+ required: true,
118
+ byId: true
119
+ },
120
+ {
121
+ key: "camps",
122
+ kind: "list",
123
+ written: "always",
124
+ required: true,
125
+ byId: true
126
+ },
127
+ {
128
+ key: "regions",
129
+ kind: "list",
130
+ written: "filled",
131
+ byId: true,
132
+ empty: () => []
133
+ },
134
+ {
135
+ key: "points",
136
+ kind: "list",
137
+ written: "filled",
138
+ byId: true,
139
+ empty: () => []
140
+ },
141
+ {
142
+ key: "paths",
143
+ kind: "list",
144
+ written: "filled",
145
+ byId: true,
146
+ empty: () => []
147
+ },
148
+ {
149
+ key: "doodads",
150
+ kind: "list",
151
+ written: "filled",
152
+ byId: true,
153
+ empty: () => []
154
+ },
155
+ {
156
+ key: "cameras",
157
+ kind: "list",
158
+ written: "filled",
159
+ byId: true,
160
+ empty: () => []
161
+ },
162
+ {
163
+ key: "randomGroups",
164
+ kind: "list",
165
+ written: "filled"
166
+ },
167
+ {
168
+ key: "itemTables",
169
+ kind: "list",
170
+ written: "filled"
171
+ },
172
+ {
173
+ key: "forces",
174
+ kind: "list",
175
+ written: "filled"
176
+ },
177
+ {
178
+ key: "races",
179
+ kind: "names",
180
+ written: "filled"
181
+ },
182
+ {
183
+ key: "author",
184
+ kind: "text",
185
+ written: "filled"
186
+ },
187
+ {
188
+ key: "silence",
189
+ kind: "names",
190
+ written: "filled"
191
+ },
192
+ {
193
+ key: "meantFor",
194
+ kind: "text",
195
+ written: "filled"
196
+ },
197
+ {
198
+ key: "explored",
199
+ kind: "boolean",
200
+ written: "defined"
201
+ },
202
+ {
203
+ key: "loading",
204
+ kind: "text",
205
+ written: "filled"
206
+ },
207
+ {
208
+ key: "preview",
209
+ kind: "text",
210
+ written: "filled"
211
+ },
212
+ {
213
+ key: "environment",
214
+ kind: "object",
215
+ written: "filled"
216
+ },
217
+ {
218
+ key: "definitions",
219
+ kind: "object",
220
+ written: "filled",
221
+ plain: true
222
+ },
223
+ {
224
+ key: "generated",
225
+ kind: "object",
226
+ written: "defined"
227
+ },
228
+ {
229
+ key: "day",
230
+ kind: "object",
231
+ written: "filled"
232
+ },
233
+ {
234
+ key: "bounds",
235
+ kind: "object",
236
+ written: "defined"
237
+ },
238
+ {
239
+ key: "data",
240
+ kind: "object",
241
+ written: "filled",
242
+ empty: () => ({}),
243
+ plain: true
244
+ },
245
+ {
246
+ key: "editor",
247
+ kind: "object",
248
+ written: "filled",
249
+ plain: true
250
+ }
251
+ ];
252
+ /** The fields as specs, for what walks them */
253
+ var MAP_FIELD_SPECS = MAP_FIELDS;
254
+ /** What a reader of this format knows in a file: `format` and every field. The rest is carried along (`GameMap.extra`) */
255
+ var KNOWN = /* @__PURE__ */ new Set(["format", ...MAP_FIELDS.map((field) => field.key)]);
256
+ /** The fields of a kind */
257
+ var fieldsOf = (...kinds) => MAP_FIELD_SPECS.filter((field) => kinds.includes(field.kind));
258
+ /** The lists of things with ids: merged thing by thing (`mergeMaps`), an entry to a line (`mapText`) */
259
+ var BY_ID$1 = MAP_FIELD_SPECS.filter((field) => field.byId).map((field) => field.key);
260
+ /** The layers of the ground every map has: merged cell by cell (`mergeMaps`), a row to a line (`mapText`) */
261
+ var LAYERS$1 = fieldsOf("layer").map((field) => field.key);
262
+ //#endregion
263
+ //#region src/map/file.ts
264
+ var isObject$1 = (value) => typeof value === "object" && value !== null;
265
+ /** The steps from each format to the one after it, by the format they start from */
266
+ var MIGRATIONS = {
267
+ 1: (file) => ({
268
+ ...file,
269
+ tileset: [...LEGACY_TILESET]
270
+ }),
271
+ 2: (file) => {
272
+ const width = Number(file.width);
273
+ const depth = Number(file.depth);
274
+ const split = (key, count, bytes) => {
275
+ const packed = file[key];
276
+ const whole = typeof packed === "string" ? unpackBytes(packed, count * bytes) : void 0;
277
+ return whole ? rowsOf(whole, bytes) : packed;
278
+ };
279
+ return {
280
+ ...file,
281
+ ground: split("ground", depth + 1, (width + 1) * 4),
282
+ levels: split("levels", depth, width),
283
+ ramps: split("ramps", depth, width),
284
+ water: split("water", depth, width),
285
+ tiles: split("tiles", depth, width)
286
+ };
287
+ }
288
+ };
289
+ /** The layers of cells of a map with anything in them, as the file has them: none when there are none */
290
+ function mapCells$1(map) {
291
+ const layers = Object.entries(map.cells ?? {}).filter(([, cells]) => cells.some((value) => value !== 0));
292
+ return layers.length ? Object.fromEntries(layers.sort(([a], [b]) => a < b ? -1 : 1).map(([id, cells]) => [id, rowsOf(cells, map.width)])) : void 0;
293
+ }
294
+ /** Bytes as a line of text for each row of `bytes` of them */
295
+ function rowsOf(bytes, row) {
296
+ const lines = [];
297
+ for (let at = 0; at < bytes.length; at += row) lines.push(pack(bytes.subarray(at, at + row)));
298
+ return lines;
299
+ }
300
+ /**
301
+ * A map file brought up to the format this reader knows, step by step. Throws for what is not a map file, and for
302
+ * one of a newer format: what changed in it cannot be known here. `to` and `steps` are the reader's own when left out
303
+ *
304
+ * @beta
305
+ */
306
+ function migrateMap(data, to = 3, steps = MIGRATIONS) {
307
+ if (!isObject$1(data)) throw new Error("Map file: not a map");
308
+ let file = data;
309
+ const format = file.format;
310
+ if (typeof format !== "number" || !Number.isInteger(format) || format < 1) throw new Error(`Map file: format ${String(format)} is not one this game reads`);
311
+ if (format > to) throw new Error(`Map file: format ${format} is newer than this game reads (${to}): it was saved by a newer version of the editor`);
312
+ for (let from = format; from < to; from++) {
313
+ const step = steps[from];
314
+ if (!step) throw new Error(`Map file: there is no way from format ${from} to ${from + 1}`);
315
+ file = {
316
+ ...step(file),
317
+ format: from + 1
318
+ };
319
+ }
320
+ return file;
321
+ }
322
+ var pack = packBytes;
323
+ function unpack(text, length, what) {
324
+ const bytes = unpackBytes(text, length);
325
+ if (!bytes) throw new Error(`Map file: ${what} does not match the size of the map`);
326
+ return bytes;
327
+ }
328
+ /** Whether a value holds anything: text not empty, a list or an object with something in it, a layer with a cell set */
329
+ function filled(value) {
330
+ if (value === void 0 || value === null || value === "" || value === false) return false;
331
+ if (value instanceof Uint8Array) return value.some((each) => each !== 0);
332
+ if (Array.isArray(value)) return value.length > 0;
333
+ if (typeof value === "object") return Object.keys(value).length > 0;
334
+ return true;
335
+ }
336
+ /** The bytes of a layer, and how many to a row: the heights by corner (four bytes each), the rest by cell */
337
+ function layerBytes(map, field, layer) {
338
+ return field.perVertex ? {
339
+ bytes: new Uint8Array(layer.buffer, layer.byteOffset, layer.byteLength),
340
+ row: (map.width + 1) * 4
341
+ } : {
342
+ bytes: layer,
343
+ row: map.width
344
+ };
345
+ }
346
+ /** A field as its file has it, or none where the file leaves it out (`MapFieldSpec.written`) */
347
+ function written(map, field) {
348
+ const value = map[field.key];
349
+ if (field.written === "defined" && value === void 0) return void 0;
350
+ if (field.kind === "layers") return mapCells$1(map);
351
+ if (field.written === "filled" && !filled(value)) return void 0;
352
+ switch (field.kind) {
353
+ case "layer":
354
+ case "mask": {
355
+ const { bytes, row } = layerBytes(map, field, value);
356
+ return rowsOf(bytes, row);
357
+ }
358
+ case "names": return field.written === "filled" ? [...value] : value;
359
+ case "object": return field.plain ? plain(value, field.key, "Map", "keep numbers, text, lists and plain objects with a map") : value;
360
+ default: return value;
361
+ }
362
+ }
363
+ /**
364
+ * A map as its file. Throws when what the game keeps with it (`data`) is not plain data, saying where
365
+ *
366
+ * @beta
367
+ */
368
+ function serializeMap(map) {
369
+ const file = {
370
+ ...map.extra,
371
+ format: 3
372
+ };
373
+ for (const field of MAP_FIELD_SPECS) {
374
+ const value = written(map, field);
375
+ if (value === void 0 || field.kind === "object" && field.written === "filled" && !filled(value)) continue;
376
+ file[field.key] = value;
377
+ }
378
+ return file;
379
+ }
380
+ /**
381
+ * The maps of map files, by the name of each file without its folder and `.vgmap.json`: `./maps/field.vgmap.json` is
382
+ * `field`. The files by their paths, read (`parseMap`): imported one by one, or Vite's
383
+ * `import.meta.glob('./maps/*.vgmap.json', { eager: true, import: 'default' })` where only a page reads them
384
+ *
385
+ * export const maps = mapsFrom({ './maps/field.vgmap.json': field, './maps/hills.vgmap.json': hills })
386
+ *
387
+ * Throws for two files of one name, and with the path of a file that cannot be read
388
+ *
389
+ * @beta
390
+ */
391
+ function mapsFrom(files) {
392
+ const maps = {};
393
+ for (const [path, file] of Object.entries(files)) {
394
+ const name = (path.split("/").pop() ?? path).replace(/\.vgmap\.json$|\.json$/, "");
395
+ if (name in maps) throw new Error(`mapsFrom: two files are named "${name}"`);
396
+ try {
397
+ maps[name] = parseMap(file);
398
+ } catch (error) {
399
+ throw new Error(`${path}: ${error.message}`);
400
+ }
401
+ }
402
+ return maps;
403
+ }
404
+ /**
405
+ * Reads a map file, of this format or an older one. Throws with what is wrong with it
406
+ *
407
+ * @beta
408
+ */
409
+ function parseMap(file) {
410
+ const data = migrateMap(file);
411
+ const width = Number(data.width);
412
+ const depth = Number(data.depth);
413
+ const inRange = (size) => Number.isInteger(size) && size >= 8 && size <= 512;
414
+ if (!inRange(width) || !inRange(depth)) throw new Error(`Map file: the size is out of range, between 8 and 512 cells each way`);
415
+ /** A layer of the ground as its rows have it, back in one piece: `bytes` to a row */
416
+ const rows = (lines, count, bytes, what) => {
417
+ if (!Array.isArray(lines) || lines.length !== count || lines.some((line) => typeof line !== "string")) throw new Error(`Map file: ${what} does not match the size of the map`);
418
+ const whole = new Uint8Array(count * bytes);
419
+ lines.forEach((line, row) => whole.set(unpack(line, bytes, what), row * bytes));
420
+ return whole;
421
+ };
422
+ const layer = (field, lines, what = field.what ?? field.key) => field.perVertex ? new Float32Array(rows(lines, depth + 1, (width + 1) * 4, what).buffer) : rows(lines, depth, width, what);
423
+ /** A list's entries, each an object, with an id where the list is of things with ids (`byId`) */
424
+ const entries = (field, list) => {
425
+ list.forEach((entry, at) => {
426
+ if (!isObject$1(entry) || Array.isArray(entry)) throw new Error(`Map file: ${field.key}[${at}] is not an object`);
427
+ if (field.byId && !Number.isFinite(entry.id)) throw new Error(`Map file: ${field.key}[${at}] has no id`);
428
+ });
429
+ return list;
430
+ };
431
+ const map = {};
432
+ for (const field of MAP_FIELD_SPECS) {
433
+ const value = data[field.key];
434
+ let read;
435
+ switch (field.kind) {
436
+ case "text":
437
+ read = typeof value === "string" ? value : void 0;
438
+ break;
439
+ case "number":
440
+ read = field.key === "width" ? width : field.key === "depth" ? depth : Number(value) || void 0;
441
+ break;
442
+ case "boolean":
443
+ read = typeof value === "boolean" ? value : void 0;
444
+ break;
445
+ case "layer":
446
+ read = layer(field, value);
447
+ break;
448
+ case "mask":
449
+ read = value === void 0 ? void 0 : layer(field, value);
450
+ break;
451
+ case "layers":
452
+ read = isObject$1(value) ? Object.fromEntries(Object.keys(value).map((id) => [id, layer(field, value[id], `the cells of ${id}`)])) : void 0;
453
+ break;
454
+ case "names":
455
+ if (field.required) {
456
+ if (!Array.isArray(value)) throw new Error(`Map file: ${field.key} is missing`);
457
+ if (value.length > 255 || value.some((id) => typeof id !== "string")) throw new Error("Map file: the tileset is not a list of tile names");
458
+ read = value;
459
+ } else read = Array.isArray(value) ? value.filter((name) => typeof name === "string") : void 0;
460
+ break;
461
+ case "list":
462
+ if (field.required && !Array.isArray(value)) throw new Error(`Map file: ${field.key} is missing`);
463
+ read = Array.isArray(value) ? entries(field, value) : void 0;
464
+ break;
465
+ case "object": read = isObject$1(value) && !(field.plain && Array.isArray(value)) ? value : void 0;
466
+ }
467
+ if (read === void 0) read = field.empty?.();
468
+ if (read !== void 0) map[field.key] = read;
469
+ }
470
+ map.objects = map.objects.map((object) => ({
471
+ ...object,
472
+ scale: object.scale ?? 1
473
+ }));
474
+ const extra = Object.fromEntries(Object.entries(data).filter(([key]) => !KNOWN.has(key)));
475
+ if (Object.keys(extra).length > 0) map.extra = extra;
476
+ return map;
477
+ }
478
+ /** A copy of a field's value that shares nothing with it */
479
+ function copied(field, value) {
480
+ switch (field.kind) {
481
+ case "layer":
482
+ case "mask": return value.slice();
483
+ case "layers": return Object.fromEntries(Object.entries(value).map(([id, cells]) => [id, cells.slice()]));
484
+ case "names": return [...value];
485
+ case "list":
486
+ case "object": return structuredClone(value);
487
+ default: return value;
488
+ }
489
+ }
490
+ /**
491
+ * A copy that shares nothing with the original
492
+ *
493
+ * @beta
494
+ */
495
+ function cloneMap(map) {
496
+ const copy = { ...map };
497
+ for (const field of MAP_FIELD_SPECS) {
498
+ const value = map[field.key];
499
+ if (value !== void 0) copy[field.key] = copied(field, value);
500
+ }
501
+ return copy;
502
+ }
503
+ /**
504
+ * The map without what its editor keeps of its own (`GameMap.editor`): what a game is given to play. The very map
505
+ * where it has none
506
+ *
507
+ * @beta
508
+ */
509
+ function withoutEditor(map) {
510
+ if (!map.editor) return map;
511
+ const { editor: _, ...rest } = map;
512
+ return rest;
513
+ }
514
+ //#endregion
515
+ //#region src/map/resize.ts
516
+ /**
517
+ * A map grown or cut on any side: the ground, cliffs, water and paint go on as they were at the old edge where it
518
+ * grows; everything on it moves along, and what is no longer on the map is gone. The map itself is left as it was.
519
+ * What the engine keeps moves (objects, camps, regions, points, paths, doodads, cameras, the players' starts, the bounds,
520
+ * the game's own kinds' things in `data`); what others keep moves with the `resizers` given (the client's looks:
521
+ * `mapLooksResizer`)
522
+ *
523
+ * @beta
524
+ */
525
+ function resizeMap(map, sides, resizers = []) {
526
+ const { left, right, top, bottom } = sides;
527
+ const width = map.width + left + right;
528
+ const depth = map.depth + top + bottom;
529
+ if (width < 8 || depth < 8 || width > 512 || depth > 512) throw new Error(`A map is between 8 and 512 cells each way, not ${width} × ${depth}`);
530
+ const copy = cloneMap(map);
531
+ const layer = (old, oldRow, oldColumn, row, column) => {
532
+ const out = new old.constructor(row * column);
533
+ for (let z = 0; z < column; z++) {
534
+ const from = clamp(z - top, 0, oldColumn - 1) * oldRow;
535
+ for (let x = 0; x < row; x++) out[z * row + x] = old[from + clamp(x - left, 0, oldRow - 1)];
536
+ }
537
+ return out;
538
+ };
539
+ copy.width = width;
540
+ copy.depth = depth;
541
+ const grown = (old, perVertex = false) => perVertex ? layer(old, map.width + 1, map.depth + 1, width + 1, depth + 1) : layer(old, map.width, map.depth, width, depth);
542
+ const into = copy;
543
+ for (const field of fieldsOf("layer", "mask", "layers")) {
544
+ const value = map[field.key];
545
+ if (!value) continue;
546
+ into[field.key] = field.kind === "layers" ? Object.fromEntries(Object.entries(value).map(([id, cells]) => [id, grown(cells)])) : grown(value, field.perVertex);
547
+ }
548
+ const on = (p) => p.x >= 0 && p.z >= 0 && p.x <= width && p.z <= depth;
549
+ const moved = (p) => ({
550
+ ...p,
551
+ x: p.x + left,
552
+ z: p.z + top
553
+ });
554
+ const inside = (p) => ({
555
+ ...p,
556
+ x: clamp(p.x, 0, width),
557
+ z: clamp(p.z, 0, depth)
558
+ });
559
+ const shape = (shape) => {
560
+ if (shape.kind !== "rect") {
561
+ const center = moved(shape);
562
+ return on(center) ? center : void 0;
563
+ }
564
+ const x0 = Math.max(shape.x - shape.width / 2 + left, 0);
565
+ const z0 = Math.max(shape.z - shape.depth / 2 + top, 0);
566
+ const x1 = Math.min(shape.x + shape.width / 2 + left, width);
567
+ const z1 = Math.min(shape.z + shape.depth / 2 + top, depth);
568
+ return x1 > x0 && z1 > z0 ? {
569
+ ...shape,
570
+ x: (x0 + x1) / 2,
571
+ z: (z0 + z1) / 2,
572
+ width: x1 - x0,
573
+ depth: z1 - z0
574
+ } : void 0;
575
+ };
576
+ copy.objects = copy.objects.map((object) => ({
577
+ ...object,
578
+ ...moved(object)
579
+ })).filter(on);
580
+ copy.camps = copy.camps.map((camp) => ({
581
+ ...camp,
582
+ ...moved(camp)
583
+ })).filter(on);
584
+ copy.points = copy.points.map((point) => ({
585
+ ...point,
586
+ ...moved(point)
587
+ })).filter(on);
588
+ copy.doodads = copy.doodads.map((doodad) => ({
589
+ ...doodad,
590
+ ...moved(doodad)
591
+ })).filter(on);
592
+ copy.cameras = copy.cameras.map((camera) => ({
593
+ ...camera,
594
+ ...inside(moved(camera))
595
+ }));
596
+ copy.paths = copy.paths.map((path) => ({
597
+ ...path,
598
+ points: path.points.map((p) => inside(moved(p)))
599
+ }));
600
+ copy.players = copy.players.map((player) => {
601
+ if (!player.start) return player;
602
+ const start = moved(player.start);
603
+ const { start: _, ...rest } = player;
604
+ return on(start) ? {
605
+ ...player,
606
+ start
607
+ } : rest;
608
+ });
609
+ copy.regions = copy.regions.flatMap((region) => {
610
+ const moved = shape(region.shape);
611
+ return moved ? [{
612
+ ...region,
613
+ shape: moved
614
+ }] : [];
615
+ });
616
+ for (const [kind, kept] of Object.entries(copy.data)) if (Array.isArray(kept) && kept.length && kept.every(isThing)) copy.data[kind] = kept.map((thing) => moved(thing)).filter(on);
617
+ if (map.bounds) {
618
+ const x = Math.max(map.bounds.x + left, 0);
619
+ const z = Math.max(map.bounds.z + top, 0);
620
+ const bounds = {
621
+ x,
622
+ z,
623
+ width: Math.min(map.bounds.x + left + map.bounds.width, width) - x,
624
+ depth: Math.min(map.bounds.z + top + map.bounds.depth, depth) - z
625
+ };
626
+ if (bounds.width > 0 && bounds.depth > 0) copy.bounds = bounds;
627
+ else delete copy.bounds;
628
+ }
629
+ const resize = {
630
+ sides,
631
+ width,
632
+ depth,
633
+ moved,
634
+ on,
635
+ inside,
636
+ shape
637
+ };
638
+ for (const resizer of resizers) resizer(copy, resize);
639
+ return copy;
640
+ }
641
+ /** A thing of a game's own kind, as `mapThings` reads one: an id, and a place */
642
+ var isThing = (thing) => typeof thing === "object" && thing !== null && Number.isInteger(thing.id) && Number.isFinite(thing.x) && Number.isFinite(thing.z);
643
+ //#endregion
644
+ //#region src/map/images.ts
645
+ /**
646
+ * Heights from the pixels of a picture (RGBA, as a canvas gives them): dark is `low`, light is `high`, the picture
647
+ * stretched over the corners of the map's ground. A heightmap made elsewhere, read into a map
648
+ *
649
+ * @beta
650
+ */
651
+ function heightsFromPixels(pixels, imageWidth, imageHeight, map, low, high) {
652
+ const row = map.width + 1;
653
+ const ground = new Float32Array(row * (map.depth + 1));
654
+ const at = (x, y) => {
655
+ const i = (clamp(y, 0, imageHeight - 1) * imageWidth + clamp(x, 0, imageWidth - 1)) * 4;
656
+ return (pixels[i] * .299 + pixels[i + 1] * .587 + pixels[i + 2] * .114) / 255;
657
+ };
658
+ for (let iz = 0; iz <= map.depth; iz++) for (let ix = 0; ix <= map.width; ix++) {
659
+ const fx = ix / map.width * (imageWidth - 1);
660
+ const fy = iz / map.depth * (imageHeight - 1);
661
+ const x = Math.floor(fx);
662
+ const y = Math.floor(fy);
663
+ const tx = fx - x;
664
+ const ty = fy - y;
665
+ const top = at(x, y) * (1 - tx) + at(x + 1, y) * tx;
666
+ const bottom = at(x, y + 1) * (1 - tx) + at(x + 1, y + 1) * tx;
667
+ ground[iz * row + ix] = low + (top * (1 - ty) + bottom * ty) * (high - low);
668
+ }
669
+ return ground;
670
+ }
671
+ /**
672
+ * The heights of a terrain as the pixels of a picture (RGBA), a pixel a corner: the lowest black, the highest white
673
+ *
674
+ * @beta
675
+ */
676
+ function heightsToPixels(heights, width, depth) {
677
+ let low = Infinity;
678
+ let high = -Infinity;
679
+ for (const h of heights) {
680
+ low = Math.min(low, h);
681
+ high = Math.max(high, h);
682
+ }
683
+ const span = high - low || 1;
684
+ const pixels = new Uint8ClampedArray((width + 1) * (depth + 1) * 4);
685
+ for (let i = 0; i < heights.length; i++) {
686
+ const value = Math.round((heights[i] - low) / span * 255);
687
+ pixels.set([
688
+ value,
689
+ value,
690
+ value,
691
+ 255
692
+ ], i * 4);
693
+ }
694
+ return {
695
+ pixels,
696
+ low,
697
+ high
698
+ };
699
+ }
700
+ //#endregion
701
+ //#region src/mapFile.ts
702
+ /** The order the parts of a map come in in its file. What is not listed comes after, by name */
703
+ var PARTS = [
704
+ "format",
705
+ "name",
706
+ "description",
707
+ "author",
708
+ "meantFor",
709
+ "loading",
710
+ "explored",
711
+ "silence",
712
+ "game",
713
+ "width",
714
+ "depth",
715
+ "cliffHeight",
716
+ "data",
717
+ "editor",
718
+ "players",
719
+ "regions",
720
+ "points",
721
+ "paths",
722
+ "doodads",
723
+ "cameras",
724
+ "randomGroups",
725
+ "itemTables",
726
+ "definitions",
727
+ "forces",
728
+ "races",
729
+ "environment",
730
+ "day",
731
+ "camps",
732
+ "objects",
733
+ "tileset",
734
+ "preview"
735
+ ];
736
+ /** The layers come last: lines nobody reads, a row of the ground to each (the map's table says which: `MAP_FIELDS`) */
737
+ var LAYERS = [...fieldsOf("layer", "layers").map((field) => field.key)];
738
+ /** Lists with one entry to a line, in the order of their ids */
739
+ var LISTS = new Set(BY_ID$1);
740
+ /** The order of what an entry holds. What is not listed comes after, by name */
741
+ var FIELDS = [
742
+ "id",
743
+ "name",
744
+ "type",
745
+ "owner",
746
+ "team",
747
+ "color",
748
+ "race",
749
+ "start",
750
+ "x",
751
+ "z",
752
+ "facing",
753
+ "scale",
754
+ "look",
755
+ "units",
756
+ "shape",
757
+ "points",
758
+ "width",
759
+ "paint",
760
+ "keepClear",
761
+ "profile",
762
+ "health",
763
+ "mana",
764
+ "level",
765
+ "abilities",
766
+ "items",
767
+ "amount",
768
+ "underConstruction",
769
+ "lifetime",
770
+ "orders",
771
+ "data"
772
+ ];
773
+ /** The keys of something in a set order: those listed first, in that order, then the rest by name */
774
+ function inOrder(value, first) {
775
+ const keys = Object.keys(value);
776
+ return [...first.filter((key) => keys.includes(key)), ...keys.filter((key) => !first.includes(key)).sort()];
777
+ }
778
+ var entryText = (entry) => `{ ${inOrder(entry, FIELDS).filter((key) => entry[key] !== void 0).map((key) => `${JSON.stringify(key)}: ${JSON.stringify(entry[key])}`).join(", ")} }`;
779
+ /**
780
+ * A map as the text of its file, written to be read by a person comparing two versions of it: every part on a line
781
+ * of its own in a set order, one object to a line in the order of their ids with what each holds in a set order
782
+ * too, and the layers last. Moving one object changes one line, whatever was done before
783
+ *
784
+ * @beta
785
+ */
786
+ function mapText(map) {
787
+ const file = serializeMap(map);
788
+ return `{\n${inOrder(file, [...PARTS, ...LAYERS]).sort((a, b) => Number(LAYERS.includes(a)) - Number(LAYERS.includes(b))).map((key) => {
789
+ const value = file[key];
790
+ if (key === "cells" && value && typeof value === "object") return `\t"cells": {\n${Object.entries(value).map(([id, rows]) => `\t\t${JSON.stringify(id)}: [\n${rows.map((row) => `\t\t\t${JSON.stringify(row)}`).join(",\n")}\n\t\t]`).join(",\n")}\n\t}`;
791
+ if (LAYERS.includes(key) && Array.isArray(value)) return `\t${JSON.stringify(key)}: [\n${value.map((row) => `\t\t${JSON.stringify(row)}`).join(",\n")}\n\t]`;
792
+ if (!LISTS.has(key) || !Array.isArray(value)) return `\t${JSON.stringify(key)}: ${JSON.stringify(value)}`;
793
+ if (!value.length) return `\t${JSON.stringify(key)}: []`;
794
+ const entries = [...value].sort((a, b) => a.id - b.id);
795
+ return `\t${JSON.stringify(key)}: [\n${entries.map((entry) => `\t\t${entryText(entry)}`).join(",\n")}\n\t]`;
796
+ }).join(",\n")}\n}\n`;
797
+ }
798
+ /**
799
+ * What a map file is called, without what comes after the first dot: the name of the map in small letters, with a
800
+ * dash for whatever is not a letter or a number. `The Pass!` is `the-pass`
801
+ *
802
+ * @beta
803
+ */
804
+ function mapFileStem(name) {
805
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "map";
806
+ }
807
+ /** A stem as the start of the name of a type: `the-pass` is `ThePass` */
808
+ var typeName = (stem) => {
809
+ const name = stem.replace(/(^|-)([a-z0-9])/g, (_, __, letter) => letter.toUpperCase());
810
+ return /^[0-9]/.test(name) ? `Map${name}` : name;
811
+ };
812
+ /** A name as TypeScript writes text, in single quotes as the rest of the code has it. Whatever is in it stays inside */
813
+ var quoted = (name) => `'${JSON.stringify(name).slice(1, -1).replace(/\\"/g, "\"").replace(/'/g, "\\'")}'`;
814
+ /** The names as a type: one to a line, so that naming one more thing changes one line */
815
+ var union = (names) => names.length ? [...names].sort().map((name) => `\n\t| ${quoted(name)}`).join("") : " never";
816
+ /**
817
+ * The names of what is on a map as TypeScript, for a game's code to use: `world.named<RoadNames>('gaet')` does not
818
+ * compile. The editor writes it next to the map (`road.names.ts` for `road.vgmap.json`), anew with every save
819
+ *
820
+ * @beta
821
+ */
822
+ function mapNamesSource(stem, map) {
823
+ const type = typeName(stem);
824
+ const objects = map.objects.flatMap((object) => object.name ? [object.name] : []);
825
+ const camps = map.camps.flatMap((camp) => camp.name ? [camp.name] : []);
826
+ const regions = map.regions.map((region) => region.name);
827
+ const points = map.points.map((point) => point.name);
828
+ const paths = map.paths.map((path) => path.name);
829
+ const lights = namesIn(map.environment, "lights");
830
+ const effects = namesIn(map.environment, "effects");
831
+ return [
832
+ `// Written by the map editor with ${stem}.vgmap.json, anew with every save: not to be changed by hand`,
833
+ "",
834
+ `/** What is named on the map, for \`world.named<${type}Names>(...)\` */`,
835
+ `export type ${type}Names =${union(objects)}`,
836
+ "",
837
+ `/** The camps that are named on the map, for \`campNamed(world, ...)\` */`,
838
+ `export type ${type}Camps =${union(camps)}`,
839
+ "",
840
+ `/** The regions of the map, for \`world.areas\` and \`enteredArea\` */`,
841
+ `export type ${type}Regions =${union(regions)}`,
842
+ "",
843
+ `/** The points of the map, for \`world.map.point(...)\` */`,
844
+ `export type ${type}Points =${union(points)}`,
845
+ "",
846
+ `/** The paths of the map, for \`world.map.path(...)\` */`,
847
+ `export type ${type}Paths =${union(paths)}`,
848
+ "",
849
+ `/** The lights of the map that are named, for \`client.lights.named(...)\` */`,
850
+ `export type ${type}Lights =${union(lights)}`,
851
+ "",
852
+ `/** The effects of the map that are named, for \`client.mapEffects.named(...)\` */`,
853
+ `export type ${type}Effects =${union(effects)}`,
854
+ "",
855
+ `/** The ids of the regions, points and paths by name: what a field that names one keeps (it is handed the name) */`,
856
+ `export const ${type}Ids = ${idsOf(map)} as const`,
857
+ ""
858
+ ].join("\n");
859
+ }
860
+ /** The names of the things of a list of a map's environment that have one (`lights`, `effects`), as plain data */
861
+ function namesIn(environment, list) {
862
+ const things = environment?.[list];
863
+ if (!Array.isArray(things)) return [];
864
+ return things.flatMap((thing) => typeof thing?.name === "string" && thing.name ? [thing.name] : []);
865
+ }
866
+ /** The ids of a map's regions, points and paths by name, as the text of an object */
867
+ function idsOf(map) {
868
+ const part = (things) => `{ ${things.map((thing) => `${JSON.stringify(thing.name)}: ${thing.id}`).join(", ")} }`;
869
+ return `{ regions: ${part(map.regions)}, points: ${part(map.points)}, paths: ${part(map.paths)} }`;
870
+ }
871
+ //#endregion
872
+ //#region src/mapMerge.ts
873
+ /** The parts of a map file that are lists of things with ids, merged thing by thing: the map's table says (`MAP_FIELDS`) */
874
+ var BY_ID = new Set(BY_ID$1);
875
+ /** What they are as text, to be compared: the same value, the same text */
876
+ var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
877
+ /**
878
+ * The three-way choice of one value: what only one side changed is that side's; both alike, either; both otherwise,
879
+ * ours, and a conflict. `undefined` is a value too: a part one side took out
880
+ */
881
+ function pick(base, ours, theirs) {
882
+ if (same(ours, theirs)) return {
883
+ value: ours,
884
+ conflict: false
885
+ };
886
+ if (same(ours, base)) return {
887
+ value: theirs,
888
+ conflict: false
889
+ };
890
+ if (same(theirs, base)) return {
891
+ value: ours,
892
+ conflict: false
893
+ };
894
+ return {
895
+ value: ours,
896
+ conflict: true
897
+ };
898
+ }
899
+ /** What a thing of a list is called in what is said: its kind and its id, and its name or type where it has one */
900
+ function nameOf(list, entry, id) {
901
+ return `${list === "objects" ? "object" : list.replace(/s$/, "")} ${id}${typeof entry?.name === "string" ? ` "${entry.name}"` : typeof entry?.type === "string" ? ` (${entry.type})` : ""}`;
902
+ }
903
+ /** Merges two lists of things by their ids: each thing chosen as a value of its own, in the order of their ids */
904
+ function mergeList(list, base, ours, theirs, conflicts) {
905
+ const byId = (entries) => new Map(entries.map((entry) => [entry.id, entry]));
906
+ const [b, o, t] = [
907
+ byId(base),
908
+ byId(ours),
909
+ byId(theirs)
910
+ ];
911
+ const ids = [.../* @__PURE__ */ new Set([
912
+ ...b.keys(),
913
+ ...o.keys(),
914
+ ...t.keys()
915
+ ])].sort((x, y) => x - y);
916
+ const merged = [];
917
+ for (const id of ids) {
918
+ const chosen = pick(b.get(id), o.get(id), t.get(id));
919
+ if (chosen.conflict) {
920
+ const what = nameOf(list, o.get(id) ?? t.get(id), id);
921
+ conflicts.push(o.has(id) && t.has(id) ? `${what} was changed on both sides: ours is kept` : `${what} was changed on one side and taken out on the other: ours is kept`);
922
+ }
923
+ if (chosen.value !== void 0) merged.push(chosen.value);
924
+ }
925
+ return merged;
926
+ }
927
+ /** Whether a value is a list of things with ids: what of a map's look is merged thing by thing (its lights, effects, decals) */
928
+ var listOfIds = (value) => Array.isArray(value) && value.every((entry) => !!entry && typeof entry === "object" && typeof entry.id === "number");
929
+ /**
930
+ * Merges what a map looks like (`GameMap.environment`, the client's to read) part by part: a list of things with ids
931
+ * (its lights, effects, decals, sounds) thing by thing, as the map's own lists are, and each other part as a value
932
+ */
933
+ function mergeEnvironment(base, ours, theirs, conflicts) {
934
+ const plain = (value) => !!value && typeof value === "object" && !Array.isArray(value);
935
+ if (![
936
+ base,
937
+ ours,
938
+ theirs
939
+ ].every((value) => value === void 0 || plain(value))) return pick(base, ours, theirs);
940
+ const [b, o, t] = [
941
+ base,
942
+ ours,
943
+ theirs
944
+ ].map((value) => value ?? {});
945
+ const merged = {};
946
+ for (const key of /* @__PURE__ */ new Set([
947
+ ...Object.keys(b),
948
+ ...Object.keys(o),
949
+ ...Object.keys(t)
950
+ ])) {
951
+ if ([
952
+ b[key],
953
+ o[key],
954
+ t[key]
955
+ ].every((value) => value === void 0 || listOfIds(value))) {
956
+ const list = mergeList(key, b[key] ?? [], o[key] ?? [], t[key] ?? [], conflicts);
957
+ if (list.length || o[key] !== void 0) merged[key] = list;
958
+ continue;
959
+ }
960
+ const chosen = pick(b[key], o[key], t[key]);
961
+ if (chosen.conflict) conflicts.push(`the map's look's ${key} was changed on both sides: ours is kept`);
962
+ if (chosen.value !== void 0) merged[key] = chosen.value;
963
+ }
964
+ return {
965
+ value: Object.keys(merged).length || ours !== void 0 ? merged : void 0,
966
+ conflict: false
967
+ };
968
+ }
969
+ /**
970
+ * Merges two versions of a map from the one they came from: parts and things by their ids (those of its look too: its
971
+ * lights, effects, decals), the ground cell by cell. Never throws for what both changed: ours is kept there, and said
972
+ * (`conflicts`). Maps of different sizes are not merged (ours, and said)
973
+ *
974
+ * @beta
975
+ */
976
+ function mergeMaps(base, ours, theirs) {
977
+ const conflicts = [];
978
+ if (ours.width !== theirs.width || ours.depth !== theirs.depth || base.width !== ours.width || base.depth !== ours.depth) return {
979
+ map: ours,
980
+ conflicts: ["the size of the map changed: ours is kept whole"]
981
+ };
982
+ const [b, o, t] = [
983
+ base,
984
+ ours,
985
+ theirs
986
+ ].map((map) => serializeMap(map));
987
+ const merged = { ...o };
988
+ const keys = /* @__PURE__ */ new Set([
989
+ ...Object.keys(b),
990
+ ...Object.keys(o),
991
+ ...Object.keys(t)
992
+ ]);
993
+ for (const key of keys) {
994
+ if (key === "format" || LAYERS$1.includes(key)) continue;
995
+ if (BY_ID.has(key) && [
996
+ b[key] ?? [],
997
+ o[key] ?? [],
998
+ t[key] ?? []
999
+ ].every(Array.isArray)) {
1000
+ const list = mergeList(key, b[key] ?? [], o[key] ?? [], t[key] ?? [], conflicts);
1001
+ if (list.length || o[key] !== void 0) merged[key] = list;
1002
+ else delete merged[key];
1003
+ continue;
1004
+ }
1005
+ const chosen = key === "environment" ? mergeEnvironment(b[key], o[key], t[key], conflicts) : pick(b[key], o[key], t[key]);
1006
+ if (chosen.conflict) conflicts.push(`the map's ${key} was changed on both sides: ours is kept`);
1007
+ if (chosen.value === void 0) delete merged[key];
1008
+ else merged[key] = chosen.value;
1009
+ }
1010
+ const map = parseMap(merged);
1011
+ const paintable = same(o.tileset, t.tileset) && same(b.tileset, o.tileset);
1012
+ for (const layer of LAYERS$1) {
1013
+ if (layer === "tiles" && !paintable) {
1014
+ if (!same(Array.from(ours.tiles), Array.from(theirs.tiles))) conflicts.push("the paint was changed on both sides with different tiles: ours is kept");
1015
+ map.tiles = ours.tiles.slice();
1016
+ continue;
1017
+ }
1018
+ const [from, mine, yours] = [
1019
+ base[layer],
1020
+ ours[layer],
1021
+ theirs[layer]
1022
+ ];
1023
+ const into = mine.slice();
1024
+ let both = 0;
1025
+ for (let i = 0; i < into.length; i++) {
1026
+ if (mine[i] === yours[i]) continue;
1027
+ if (mine[i] === from[i]) into[i] = yours[i];
1028
+ else if (yours[i] !== from[i]) both++;
1029
+ }
1030
+ if (both) conflicts.push(`${both} ${layer === "ground" ? "corners of the ground" : `cells of the ${layer === "tiles" ? "paint" : layer}`} were changed on both sides: ours is kept there`);
1031
+ map[layer] = into;
1032
+ }
1033
+ return {
1034
+ map,
1035
+ conflicts
1036
+ };
1037
+ }
1038
+ //#endregion
1039
+ //#region src/presetGround.ts
1040
+ /**
1041
+ * Ready-made grounds a game takes or leaves, beside its own: the tiles of an RTS's ground (grass, dark grass, dirt,
1042
+ * road, sand, rock, snow), which the client draws with its `TILE_LOOKS`, and the blight a race spoils ground with,
1043
+ * which the client draws with its `blightLook`. They say only what there is; what they do is the game's
1044
+ */
1045
+ /**
1046
+ * The tiles of an RTS's ground, by the ids the client's `TILE_LOOKS` has looks for: `tiles: presetTiles` in the rules,
1047
+ * `tiles: TILE_LOOKS` in the view. A game adds its own beside them, or takes some (`presetTiles.filter(...)`)
1048
+ *
1049
+ * @beta
1050
+ */
1051
+ var presetTiles = [
1052
+ defineTile({
1053
+ id: "grass",
1054
+ name: "Grass"
1055
+ }),
1056
+ defineTile({
1057
+ id: "darkGrass",
1058
+ name: "Dark grass"
1059
+ }),
1060
+ defineTile({
1061
+ id: "dirt",
1062
+ name: "Dirt"
1063
+ }),
1064
+ defineTile({
1065
+ id: "road",
1066
+ name: "Road"
1067
+ }),
1068
+ defineTile({
1069
+ id: "sand",
1070
+ name: "Sand"
1071
+ }),
1072
+ defineTile({
1073
+ id: "rock",
1074
+ name: "Rock"
1075
+ }),
1076
+ defineTile({
1077
+ id: "snow",
1078
+ name: "Snow"
1079
+ })
1080
+ ];
1081
+ /**
1082
+ * Blight: ground spoiled, a layer of cells a map's maker paints and the rules spread (`world.setCells('blight', …)`),
1083
+ * drawn by the client's `blightLook`. What it does (who builds on it, who heals on it) is the game's: melee's
1084
+ * undead in the example
1085
+ *
1086
+ * @beta
1087
+ */
1088
+ var presetBlight = defineCellLayer({
1089
+ id: "blight",
1090
+ name: "Blight",
1091
+ description: "Spoiled ground: what the game says of it, a race that builds on it, one that heals on it",
1092
+ color: [
1093
+ .45,
1094
+ .3,
1095
+ .55
1096
+ ]
1097
+ });
1098
+ //#endregion
1099
+ //#region src/presetBuildings.ts
1100
+ /**
1101
+ * A fountain: a building that keeps the game's aura on who is around it (`aura`, the id of a passive ability with an
1102
+ * `aura` effect: a fountain of health's regeneration, a fountain of mana's). It cannot be hurt; a map puts it down
1103
+ * for the neutral player, so the aura says `affects: 'all'` to reach everyone (the neutral player is nobody's ally).
1104
+ * What is given over the fountain's own goes over it: `presetFountain('healingWaters', { id: 'fountainOfHealth' })`
1105
+ *
1106
+ * @beta
1107
+ */
1108
+ function presetFountain(aura, over = {}) {
1109
+ return {
1110
+ id: "fountain",
1111
+ name: "Fountain",
1112
+ kind: "building",
1113
+ radius: 1.5,
1114
+ footprint: {
1115
+ w: 3,
1116
+ d: 3
1117
+ },
1118
+ stats: { health: 1e3 },
1119
+ sight: 6,
1120
+ invulnerable: true,
1121
+ abilities: [aura],
1122
+ ...over
1123
+ };
1124
+ }
1125
+ /**
1126
+ * A marketplace's abilities: one for each trade (`MarketRate`), cast at once (`target: 'none'`), with an `exchange`
1127
+ * effect that trades the caster's owner's resources at its rate. A building of the game's lists their ids in its
1128
+ * `abilities` (`abilities: market.map((ability) => ability.id)`), the abilities go in the rules' `abilities`. With
1129
+ * less than the amount to sell, the player is told and nothing goes. The computer player does not cast them in a
1130
+ * fight (their `ai` says not now); its brain's `spells` may
1131
+ *
1132
+ * @beta
1133
+ */
1134
+ function presetMarket(options) {
1135
+ return options.rates.map(({ sell, buy, rate, amount = 100, id, name, hotkey }) => ({
1136
+ id: id ?? `sell-${sell}-for-${buy}`,
1137
+ name: name ?? `Sell ${sell} for ${buy}`,
1138
+ description: `Trades ${amount} ${sell} for ${Math.floor(amount * rate + 1e-9)} ${buy}`,
1139
+ target: "none",
1140
+ maxLevel: 1,
1141
+ ...hotkey !== void 0 && { hotkey },
1142
+ ...options.cooldown !== void 0 && { cooldown: options.cooldown },
1143
+ effects: [{
1144
+ type: "exchange",
1145
+ sell,
1146
+ buy,
1147
+ rate,
1148
+ amount
1149
+ }],
1150
+ ai: () => null
1151
+ }));
1152
+ }
1153
+ //#endregion
1154
+ //#region src/packs/heroes/respawn.ts
1155
+ /**
1156
+ * A game whose heroes come back by themselves after a while, at their side's home (see `RespawnOptions`): a saved timer
1157
+ * each, shown on the HUD as a revive is, and waited for by the computer player in place of reviving at an altar.
1158
+ * Revived at an altar in the meantime, it is back then, and the timer comes to nothing. For a game with the heroes pack
1159
+ * on (`withHeroes()`): one without it is refused
1160
+ *
1161
+ * @beta
1162
+ */
1163
+ function withRespawn(options = {}) {
1164
+ const time = options.time ?? ((_world, hero) => 4 + 2 * (heroOf(hero)?.level ?? 1));
1165
+ const comeBack = defineTimer("respawn", (world, data) => {
1166
+ const hero = world.get(data.hero) ?? [...world.index.with(heroState, data.player)].find((each) => each.id === data.hero);
1167
+ const waiting = hero?.get(respawning);
1168
+ if (!hero || !waiting || data.since !== void 0 && waiting.since !== data.since) return;
1169
+ hero.remove(respawning);
1170
+ if (hero.alive || !hero.has(heroState)) return;
1171
+ const at = options.at?.(world, hero) ?? homeOf(world, hero.owner, hero.pos) ?? hero.pos;
1172
+ reviveHero(world, hero, at);
1173
+ });
1174
+ const fell = defineListener({
1175
+ id: "respawn.fell",
1176
+ event: entityDied,
1177
+ run(world, { entity }) {
1178
+ if (!entity.has(heroState) || options.heroes && !options.heroes(world, entity)) return;
1179
+ const wait = Math.max(0, time(world, entity));
1180
+ entity.set(respawning, {
1181
+ since: world.time,
1182
+ wait
1183
+ });
1184
+ world.timers.after(wait, comeBack, {
1185
+ hero: entity.id,
1186
+ player: entity.owner,
1187
+ since: world.time
1188
+ });
1189
+ }
1190
+ });
1191
+ return {
1192
+ id: "respawn",
1193
+ options,
1194
+ requires: [HEROES],
1195
+ components: [respawning],
1196
+ timers: [comeBack],
1197
+ listeners: [fell]
1198
+ };
1199
+ }
1200
+ /**
1201
+ * Recall (features-plan F2): a hero stands and channels for a few seconds, then is at its side's home (`homeOf`),
1202
+ * however far; hurt in the meantime, or told to do something else, and it stays where it is. An ability to give heroes
1203
+ * (`UnitTypeDef.abilities`), made of the engine's parts: a channel (`AbilityDef.channel`) that `breaksOnDamage`, and the
1204
+ * `teleport` effect when it is `finished`. Its id, name, how long it takes, its cooldown and key are the game's to say
1205
+ *
1206
+ * @beta
1207
+ */
1208
+ function recallAbility(options = {}) {
1209
+ const duration = options.duration ?? 6;
1210
+ return {
1211
+ id: options.id ?? "recall",
1212
+ name: options.name ?? "Recall",
1213
+ description: `After ${duration} seconds standing still, unhurt, the hero is back home.`,
1214
+ hotkey: options.hotkey ?? "B",
1215
+ target: "none",
1216
+ cooldown: options.cooldown ?? 20,
1217
+ maxLevel: 1,
1218
+ effects: [],
1219
+ channel: {
1220
+ duration,
1221
+ breaksOnDamage: true,
1222
+ finished: [{
1223
+ type: "teleport",
1224
+ to: "home"
1225
+ }]
1226
+ }
1227
+ };
1228
+ }
1229
+ //#endregion
1230
+ //#region src/friendlyFire.ts
1231
+ /**
1232
+ * The friendly fire pack's id
1233
+ *
1234
+ * @beta
1235
+ */
1236
+ var FRIENDLY_FIRE = "core.friendlyFire";
1237
+ /**
1238
+ * Friendly fire, as in classic RTS games: a unit told to attack (the attack command, never a right click) one of its own or
1239
+ * an ally's attacks it. Never of its own accord: units do not pick their own side to attack, nor fight back at it.
1240
+ * Nothing of a neutral player's (a rock, a critter) is made a target by it
1241
+ *
1242
+ * @beta
1243
+ */
1244
+ function withFriendlyFire(options = {}) {
1245
+ const attacks = options.attacks ?? (() => true);
1246
+ return {
1247
+ id: FRIENDLY_FIRE,
1248
+ options,
1249
+ hooks: [defineHook({
1250
+ hook: "canAttack",
1251
+ id: "friendlyFire",
1252
+ run(world, event) {
1253
+ const { attacker, target } = event;
1254
+ if (event.allowed || !event.ordered || world.player(target.owner).neutral) return;
1255
+ if (world.areEnemies(attacker.owner, target.owner)) return;
1256
+ if (attackReaches(world, attacker, target) && attacks(world, attacker, target)) event.allowed = true;
1257
+ }
1258
+ })]
1259
+ };
1260
+ }
1261
+ //#endregion
1262
+ //#region src/ids.ts
1263
+ /**
1264
+ * Ids the compiler knows. A game's ids are text to the engine (`world.spawn('skeleton', ...)`), and a slip of the
1265
+ * finger in one is found when the game runs, if it is found. Named once with `defineIds`, they are checked as they are
1266
+ * written, completed by the editor, and renamed in one place:
1267
+ *
1268
+ * export const UNIT = defineIds('skeleton', 'runner', 'brute')
1269
+ * export type TdUnit = IdOf<typeof UNIT> // 'skeleton' | 'runner' | 'brute'
1270
+ *
1271
+ * { id: UNIT.skeleton, ... } // in the unit's definition
1272
+ * world.spawn(UNIT.skeletn, ...) // does not compile
1273
+ * const view: GameView<TdUnit> = { units: { looks: { skeletn: ... } } } // nor does this
1274
+ *
1275
+ * It is a help, not a rule: plain text works wherever an id goes, and what the compiler cannot see is checked when the
1276
+ * game is put together (`defineGame`)
1277
+ *
1278
+ * @beta
1279
+ */
1280
+ function defineIds(...names) {
1281
+ const out = {};
1282
+ for (const name of names) {
1283
+ if (name in out) throw new Error(`defineIds: "${name}" is there twice`);
1284
+ out[name] = name;
1285
+ }
1286
+ return Object.freeze(out);
1287
+ }
1288
+ //#endregion
1289
+ //#region src/gameFiles.ts
1290
+ /**
1291
+ * A file of the game's own data, for `GameModule.files`: `defineGameFile('waves.json', wavesFile, readWaves)`, read by
1292
+ * the rules with `world.registry.file(waves)`
1293
+ *
1294
+ * @beta
1295
+ */
1296
+ function defineGameFile(id, data, read) {
1297
+ return {
1298
+ id,
1299
+ data,
1300
+ read
1301
+ };
1302
+ }
1303
+ //#endregion
1304
+ //#region src/packs/pack.ts
1305
+ /**
1306
+ * Whether a game has a pack on, by its id: `hasPack(game, HEROES)`, what the editor asks before it shows a hero's
1307
+ * fields
1308
+ *
1309
+ * @beta
1310
+ */
1311
+ function hasPack(game, id) {
1312
+ return !!game?.packs?.some((pack) => pack.id === id);
1313
+ }
1314
+ //#endregion
1315
+ //#region src/packs/director/index.ts
1316
+ /** @beta */
1317
+ var DIRECTOR = "director";
1318
+ /**
1319
+ * The cinematic playing on each player's screen, by its id, as their screen said (`cinematicPlaying`): whose
1320
+ * director's commands are taken
1321
+ *
1322
+ * @beta
1323
+ */
1324
+ var directing = defineComponent("director.playing", { default: () => ({}) });
1325
+ /**
1326
+ * The units a cinematic made invulnerable (`director` with `invulnerable`): nothing hurts or kills them until it says
1327
+ * otherwise
1328
+ *
1329
+ * @beta
1330
+ */
1331
+ var directorInvulnerable = defineComponent("director.invulnerable");
1332
+ /**
1333
+ * What a director's command does (`DirectorCommand.action`)
1334
+ *
1335
+ * @beta
1336
+ */
1337
+ var DIRECTOR_ACTIONS = [
1338
+ "spawn",
1339
+ "remove",
1340
+ "kill",
1341
+ "teleport",
1342
+ "scale",
1343
+ "invulnerable",
1344
+ "owner",
1345
+ "order",
1346
+ "facing"
1347
+ ];
1348
+ /**
1349
+ * The orders a director's command gives (`order`): to a spot, or at a unit
1350
+ *
1351
+ * @beta
1352
+ */
1353
+ var DIRECTOR_ORDERS = [
1354
+ "move",
1355
+ "attack",
1356
+ "attackMove",
1357
+ "patrol",
1358
+ "stop",
1359
+ "hold"
1360
+ ];
1361
+ /**
1362
+ * A player's screen says a cinematic began on it (`playing: true`, its id) or ended: while one plays there, the
1363
+ * director's commands it sends are taken
1364
+ *
1365
+ * @beta
1366
+ */
1367
+ var cinematicPlayingCommand = defineCommand("cinematicPlaying", {
1368
+ cinematic: string,
1369
+ playing: bool
1370
+ }, (world, player, command) => {
1371
+ const known = directorOptions(world).cinematics;
1372
+ if (command.playing && known && !known.includes(command.cinematic)) return refuse(world, player, `"${command.cinematic}" is no cinematic the director plays in (withDirector's cinematics)`);
1373
+ const now = { ...world.state(directing) };
1374
+ if (command.playing) now[player] = command.cinematic;
1375
+ else if (now[player] === command.cinematic) delete now[player];
1376
+ world.setState(directing, now);
1377
+ });
1378
+ /** The options the game's director pack was made with */
1379
+ var directorOptions = (world) => world.registry.module?.packs?.find((pack) => pack.id === "director")?.options ?? {};
1380
+ /** Turned away, and counted as what the player's own client never sends */
1381
+ var refuse = (world, player, reason) => world.events.emit(commandRejected, {
1382
+ player,
1383
+ type: "director",
1384
+ kind: "notDirecting",
1385
+ reason
1386
+ });
1387
+ /**
1388
+ * A cinematic's say over the game (see the top of this file): `action` with what it takes. `unit` is the one it is
1389
+ * done to (any player's); `spawn` puts one of `unitType` down for `owner` at `at`, turned to `facing`, named `name` for
1390
+ * what comes after it in the cinematic (`world.named`)
1391
+ *
1392
+ * @beta
1393
+ */
1394
+ var directorCommand = defineCommand("director", {
1395
+ cinematic: string,
1396
+ action: oneOf(...DIRECTOR_ACTIONS),
1397
+ unit: optional(id),
1398
+ unitType: optional(string),
1399
+ owner: optional(id),
1400
+ at: optional(point),
1401
+ facing: optional(number),
1402
+ scale: optional(number),
1403
+ on: optional(bool),
1404
+ name: optional(string),
1405
+ order: optional(oneOf(...DIRECTOR_ORDERS)),
1406
+ target: optional(id)
1407
+ }, (world, player, command) => {
1408
+ const playing = world.state(directing)[player];
1409
+ if (playing !== command.cinematic) {
1410
+ refuse(world, player, playing ? `the cinematic playing is "${playing}", not "${command.cinematic}"` : "no cinematic plays on their screen");
1411
+ return;
1412
+ }
1413
+ const { action } = command;
1414
+ if (action === "spawn") {
1415
+ if (!command.unitType || !world.registry.units.has(command.unitType)) return refuse(world, player, `there is no unit type "${command.unitType}"`);
1416
+ const owner = command.owner ?? player;
1417
+ if (!world.players.has(owner)) return refuse(world, player, `there is no player ${owner}`);
1418
+ if (!command.at) return refuse(world, player, "a unit put down needs where (at)");
1419
+ world.spawn(command.unitType, owner, command.at, {
1420
+ ...command.facing !== void 0 && { facing: command.facing },
1421
+ ...command.name && { name: command.name }
1422
+ });
1423
+ return;
1424
+ }
1425
+ const entity = command.unit === void 0 ? void 0 : world.get(command.unit);
1426
+ if (!entity?.alive) return refuse(world, player, `there is no unit ${command.unit}`);
1427
+ act(world, entity, command);
1428
+ });
1429
+ /** What a director's command does to a unit there is */
1430
+ function act(world, entity, command) {
1431
+ switch (command.action) {
1432
+ case "remove":
1433
+ world.kill(entity, void 0, false, "removed");
1434
+ break;
1435
+ case "kill":
1436
+ world.kill(entity);
1437
+ break;
1438
+ case "teleport":
1439
+ if (command.at) world.place(entity, command.at);
1440
+ break;
1441
+ case "scale":
1442
+ if (command.scale !== void 0 && command.scale > 0) entity.scale = command.scale;
1443
+ break;
1444
+ case "invulnerable":
1445
+ if (command.on === false) entity.remove(directorInvulnerable);
1446
+ else entity.set(directorInvulnerable, true);
1447
+ break;
1448
+ case "owner":
1449
+ if (command.owner !== void 0 && world.players.has(command.owner)) world.setOwner(entity, command.owner);
1450
+ break;
1451
+ case "facing":
1452
+ if (command.facing !== void 0) entity.facing = command.facing;
1453
+ break;
1454
+ case "order": {
1455
+ const kind = command.order ?? "move";
1456
+ const target = command.target === void 0 ? void 0 : world.get(command.target);
1457
+ const init = kind === "stop" || kind === "hold" ? { type: kind } : kind === "attack" && target ? {
1458
+ type: "attack",
1459
+ target: target.id
1460
+ } : command.at ? {
1461
+ type: kind === "attack" ? "attackMove" : kind,
1462
+ point: command.at
1463
+ } : void 0;
1464
+ if (init) giveOrder(world, entity, createOrder(world, init));
1465
+ break;
1466
+ }
1467
+ }
1468
+ }
1469
+ /** Nothing hurts or kills a unit a cinematic made invulnerable */
1470
+ var invulnerableHooks = [defineHook({
1471
+ hook: "beforeDamage",
1472
+ id: "director",
1473
+ run(_world, damage) {
1474
+ if (damage.target.has(directorInvulnerable)) damage.cancelled = true;
1475
+ }
1476
+ }), defineHook({
1477
+ hook: "beforeDeath",
1478
+ id: "director",
1479
+ run(_world, death) {
1480
+ if (death.entity.has(directorInvulnerable) && death.reason !== "removed") death.prevented = true;
1481
+ }
1482
+ })];
1483
+ /**
1484
+ * The director's pack (see the top of this file): its commands, what it keeps, and what keeps a unit it made
1485
+ * invulnerable so. `packs: [withDirector()]`, or `withDirector({ cinematics: ['intro'] })` for those alone
1486
+ *
1487
+ * @beta
1488
+ */
1489
+ function withDirector(options = {}) {
1490
+ return {
1491
+ id: DIRECTOR,
1492
+ options,
1493
+ commands: [cinematicPlayingCommand, directorCommand],
1494
+ components: [directing, directorInvulnerable],
1495
+ hooks: invulnerableHooks
1496
+ };
1497
+ }
1498
+ //#endregion
1499
+ //#region src/packs/moba/index.ts
1500
+ /**
1501
+ * The stats of a MOBA (`withMobaStats`): ability power, critical strikes, armor and magic penetration, life steal and
1502
+ * omnivamp, tenacity, slow resistance, heal and shield power, ability haste. The hero arena games' vocabulary and their
1503
+ * formulas, a pack a game opts into: one without it has none of these stats, and the engine's rules are the plainer
1504
+ * ones (no crits, mitigation by armor and magic resist alone, cooldowns as they are)
1505
+ */
1506
+ /**
1507
+ * The MOBA stats pack's id
1508
+ *
1509
+ * @beta
1510
+ */
1511
+ var MOBA_STATS = "core.mobaStats";
1512
+ /** The stats the pack brings, in the order a stat sheet shows them; `critDamage`'s default is the pack's option */
1513
+ var mobaStatDefs = (critDamage) => [
1514
+ {
1515
+ id: "abilityPower",
1516
+ name: "Ability power",
1517
+ description: "Makes abilities stronger, as far as they scale with it"
1518
+ },
1519
+ {
1520
+ id: "critChance",
1521
+ name: "Critical chance",
1522
+ description: "Chance of a basic attack to strike critically, 0..1",
1523
+ format: "percent"
1524
+ },
1525
+ {
1526
+ id: "critDamage",
1527
+ name: "Critical damage",
1528
+ description: "What a critical strike multiplies the damage with",
1529
+ default: critDamage,
1530
+ format: "percent"
1531
+ },
1532
+ {
1533
+ id: "armorPenetration",
1534
+ name: "Armor penetration",
1535
+ description: "Part of the target's armor that is ignored, 0..1",
1536
+ format: "percent"
1537
+ },
1538
+ {
1539
+ id: "lethality",
1540
+ name: "Lethality",
1541
+ description: "Armor of the target that is ignored, taken off after the part above"
1542
+ },
1543
+ {
1544
+ id: "magicPenetrationPercent",
1545
+ name: "Magic penetration %",
1546
+ description: "Part of the target's magic resist that is ignored, 0..1",
1547
+ format: "percent"
1548
+ },
1549
+ {
1550
+ id: "magicPenetration",
1551
+ name: "Magic penetration",
1552
+ description: "Magic resist of the target that is ignored, taken off after the part above"
1553
+ },
1554
+ {
1555
+ id: "lifeSteal",
1556
+ name: "Life steal",
1557
+ description: "Part of the damage of basic attacks that comes back as health",
1558
+ format: "percent"
1559
+ },
1560
+ {
1561
+ id: "omnivamp",
1562
+ name: "Omnivamp",
1563
+ description: "Part of all damage dealt that comes back as health",
1564
+ format: "percent"
1565
+ },
1566
+ {
1567
+ id: "tenacity",
1568
+ name: "Tenacity",
1569
+ description: "Part by which stuns, slows and the like are cut short, 0..1",
1570
+ format: "percent"
1571
+ },
1572
+ {
1573
+ id: "slowResist",
1574
+ name: "Slow resist",
1575
+ description: "Part by which slows are weakened, 0..1",
1576
+ format: "percent"
1577
+ },
1578
+ {
1579
+ id: "healShieldPower",
1580
+ name: "Heal and shield power",
1581
+ description: "Part by which the heals and shields this unit gives are stronger",
1582
+ format: "percent"
1583
+ },
1584
+ {
1585
+ id: "abilityHaste",
1586
+ name: "Ability haste",
1587
+ description: "Shortens cooldowns: 100 of it halve them"
1588
+ }
1589
+ ];
1590
+ /**
1591
+ * What is left of damage once the target's armor or magic resist, less what the source's penetration ignores, had its
1592
+ * say: the pack's `rules.combat.mitigation`
1593
+ *
1594
+ * @beta
1595
+ */
1596
+ function penetratingMitigation(type, target, source) {
1597
+ if (type === "true") return 1;
1598
+ let resist = type === "physical" ? target.armor : target.magicResist;
1599
+ if (source && resist > 0) {
1600
+ const part = type === "physical" ? source.armorPenetration : source.magicPenetrationPercent;
1601
+ const flat = type === "physical" ? source.lethality : source.magicPenetration;
1602
+ resist = Math.max(resist * (1 - part) - flat, 0);
1603
+ }
1604
+ return resisted(resist);
1605
+ }
1606
+ /** Health back for damage dealt: omnivamp of everything, life steal of basic attacks. Before the engine's own handlers */
1607
+ var lifeSteal = defineHook({
1608
+ hook: "afterDamage",
1609
+ id: "mobaStats.lifeSteal",
1610
+ priority: -1,
1611
+ run(world, { target, source, taken, attack }) {
1612
+ if (!source?.alive || taken <= 0 || source === target) return;
1613
+ const stats = statsOf(world, source);
1614
+ const back = taken * (stats.omnivamp + (attack ? stats.lifeSteal : 0));
1615
+ if (back > 0) world.heal(source, back);
1616
+ }
1617
+ });
1618
+ /** Stuns, slows and the like cut short by the target's tenacity: before what the game decides of a buff */
1619
+ var tenacity = defineHook({
1620
+ hook: "beforeBuff",
1621
+ id: "mobaStats.tenacity",
1622
+ priority: -1,
1623
+ run(world, event) {
1624
+ if (event.duration !== Infinity && isCrowdControl(event.buff)) event.duration *= 1 - statsOf(world, event.target).tenacity;
1625
+ }
1626
+ });
1627
+ /**
1628
+ * The stats of a MOBA, for a game that has them: ability power (what `scaling: { abilityPower }` reads), critical
1629
+ * strikes, armor and magic penetration, life steal and omnivamp, tenacity, slow resistance, heal and shield power,
1630
+ * ability haste, with the hero arena games' formulas for each. A game without it has none of these stats
1631
+ *
1632
+ * @beta
1633
+ */
1634
+ function withMobaStats(options = {}) {
1635
+ const { critDamage = 1.75, maxTenacity = .9 } = options;
1636
+ return {
1637
+ id: MOBA_STATS,
1638
+ options,
1639
+ stats: mobaStatDefs(critDamage),
1640
+ hooks: [lifeSteal, tenacity],
1641
+ rules: (rules) => {
1642
+ const { combat } = rules;
1643
+ const engine = defaultRules.combat;
1644
+ const own = (key, pack) => combat[key] === engine[key] ? pack : combat[key];
1645
+ const keep = combat.clampStats;
1646
+ return { combat: {
1647
+ critical: own("critical", (world, _attacker, stats) => stats.critChance > 0 && world.rng.next() < stats.critChance ? stats.critDamage : void 0),
1648
+ mitigation: own("mitigation", penetratingMitigation),
1649
+ clampStats(stats, all) {
1650
+ stats.critChance = clamp(stats.critChance, 0, 1);
1651
+ stats.tenacity = clamp(stats.tenacity, 0, maxTenacity);
1652
+ stats.slowResist = clamp(stats.slowResist, 0, 1);
1653
+ stats.armorPenetration = clamp(stats.armorPenetration, 0, 1);
1654
+ stats.magicPenetrationPercent = clamp(stats.magicPenetrationPercent, 0, 1);
1655
+ keep(stats, all);
1656
+ },
1657
+ cooldown: own("cooldown", (seconds, stats) => stats.abilityHaste > 0 ? seconds * 100 / (100 + stats.abilityHaste) : seconds),
1658
+ slowTaken: own("slowTaken", (stats, slow) => slow * (1 - stats.slowResist)),
1659
+ healPower: own("healPower", (world, source) => 1 + statsOf(world, source).healShieldPower)
1660
+ } };
1661
+ }
1662
+ };
1663
+ }
1664
+ //#endregion
1665
+ //#region src/techTree.ts
1666
+ /**
1667
+ * The tech tree of a game's rules: its things that are made or make something (scenery and the map's creeps left
1668
+ * out), the edges between them, and each one's tier. Tiers go by what makes what from what nothing makes and from the
1669
+ * buildings that make the builders, require nothing and are no other's upgrade (a hall; else, where everything is
1670
+ * made of something, the buildings that require nothing, are no upgrade and make something),
1671
+ * each thing after what it requires; a loop of requirements stops at the number of things
1672
+ *
1673
+ * @beta
1674
+ */
1675
+ function techTree(registry) {
1676
+ const units = registry.units.all();
1677
+ const upgrades = registry.upgrades.all();
1678
+ const edges = [];
1679
+ const known = /* @__PURE__ */ new Set([...units.map((unit) => unit.id), ...upgrades.map((upgrade) => upgrade.id)]);
1680
+ const add = (from, to, kind) => {
1681
+ if (known.has(from) && known.has(to) && from !== to) edges.push({
1682
+ from,
1683
+ to,
1684
+ kind
1685
+ });
1686
+ };
1687
+ for (const unit of units) {
1688
+ for (const id of unit.trains ?? []) add(unit.id, id, "trains");
1689
+ for (const id of unit.builds ?? []) add(unit.id, id, "builds");
1690
+ for (const id of unit.hires ?? []) add(unit.id, id, "hires");
1691
+ for (const id of unit.researches ?? []) add(unit.id, id, "researches");
1692
+ for (const id of upgradesOf(unit)) add(unit.id, id, "upgradesTo");
1693
+ for (const id of unit.requires ?? []) add(id, unit.id, "requires");
1694
+ }
1695
+ for (const upgrade of upgrades) for (const id of upgrade.requires?.[0] ?? []) add(id, upgrade.id, "requires");
1696
+ const inTree = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
1697
+ const makes = edges.filter((edge) => edge.kind !== "requires");
1698
+ const made = new Set(makes.map((edge) => edge.to));
1699
+ const needs = /* @__PURE__ */ new Map();
1700
+ for (const edge of edges) if (edge.kind === "requires") needs.set(edge.to, [...needs.get(edge.to) ?? [], edge.from]);
1701
+ const buildings = new Set(units.filter((unit) => unit.kind !== "unit").map((unit) => unit.id));
1702
+ const upgraded = new Set(edges.filter((edge) => edge.kind === "upgradesTo").map((edge) => edge.to));
1703
+ let start = [...inTree].filter((id) => !made.has(id));
1704
+ const builders = new Set(units.filter((unit) => unit.builds?.length).map((unit) => unit.id));
1705
+ const first = [...inTree].filter((id) => buildings.has(id) && !needs.has(id) && !upgraded.has(id));
1706
+ const halls = first.filter((id) => makes.some((edge) => edge.from === id && builders.has(edge.to)));
1707
+ start = [.../* @__PURE__ */ new Set([...start, ...halls])];
1708
+ if (!start.length) start = first.filter((id) => makes.some((edge) => edge.from === id));
1709
+ if (!start.length) start = [...inTree].filter((id) => !needs.has(id));
1710
+ const tier = new Map(start.map((id) => [id, 0]));
1711
+ for (let queue = [...start]; queue.length;) {
1712
+ const next = [];
1713
+ for (const id of queue) for (const edge of makes) if (edge.from === id && !tier.has(edge.to)) {
1714
+ tier.set(edge.to, tier.get(id) + 1);
1715
+ next.push(edge.to);
1716
+ }
1717
+ queue = next;
1718
+ }
1719
+ for (let pass = 0; pass < inTree.size; pass++) {
1720
+ let moved = false;
1721
+ for (const [id, required] of needs) {
1722
+ const after = Math.max(...required.map((other) => (tier.get(other) ?? 0) + 1));
1723
+ if (after > (tier.get(id) ?? 0)) {
1724
+ tier.set(id, after);
1725
+ moved = true;
1726
+ }
1727
+ }
1728
+ if (!moved) break;
1729
+ }
1730
+ const nodes = [];
1731
+ for (const unit of units) if (inTree.has(unit.id)) nodes.push({
1732
+ id: unit.id,
1733
+ kind: unit.kind === "unit" ? "unit" : "building",
1734
+ name: unit.name,
1735
+ tier: tier.get(unit.id) ?? 0,
1736
+ ...unit.race && { race: unit.race }
1737
+ });
1738
+ for (const upgrade of upgrades) if (inTree.has(upgrade.id)) nodes.push({
1739
+ id: upgrade.id,
1740
+ kind: "upgrade",
1741
+ name: upgrade.name,
1742
+ tier: tier.get(upgrade.id) ?? 0,
1743
+ ...upgrade.race && { race: upgrade.race }
1744
+ });
1745
+ return {
1746
+ nodes,
1747
+ edges
1748
+ };
1749
+ }
1750
+ //#endregion
1751
+ //#region src/hotReload.ts
1752
+ /**
1753
+ * A world took a new version of its game while it ran (`reloadGame`, hot reload): what the rules hear
1754
+ * (`world.events.on(gameReloaded, …)`), and every screen (`GameView.on`), to make again what it made of the old one
1755
+ *
1756
+ * @beta
1757
+ */
1758
+ var gameReloaded = defineEvent("core.gameReloaded", { replicate: "everyone" });
1759
+ /**
1760
+ * Puts a new version of a world's game into it while it runs, as a game's files are saved during development (hot
1761
+ * reload): its unit types, abilities, buffs, items, upgrades and races, the code of their effects and of its orders,
1762
+ * and its rules. What `world.redefine` changed while it ran stays over them, and its units take their types as they
1763
+ * are now. What the world was made of otherwise (its systems, hooks, components, resources) is not: a change to those
1764
+ * wants a new game, which is said (`hotRules` loads the page again for one). Throws for another game, or a module that is no game (as `createRegistry`)
1765
+ *
1766
+ * @beta
1767
+ */
1768
+ function reloadGame(world, game) {
1769
+ const { registry } = world;
1770
+ if (game.id !== registry.game) throw new Error(`The world plays "${registry.game}", not "${game.id}"`);
1771
+ registry.takeFrom(createRegistry(game));
1772
+ if (world.states.has(redefinitions)) restoreRedefinitions(registry, world.state(redefinitions));
1773
+ world.retype();
1774
+ world.score.listen();
1775
+ try {
1776
+ game.onReload?.(world);
1777
+ } catch (error) {
1778
+ world.log.error("game", `${game.id}'s onReload threw: ${error instanceof Error ? error.message : String(error)}`);
1779
+ }
1780
+ world.log.info("game", `Took a new version of ${game.name ?? game.id}: its definitions and rules (its systems and hooks are the game's as it began)`);
1781
+ world.events.emit(gameReloaded, { game: game.id });
1782
+ }
1783
+ /** What of a game a running world keeps as it was made (`reloadGame`): a new version that changes any loads the page again */
1784
+ var NOT_RELOADED = [
1785
+ "systems",
1786
+ "withoutSystems",
1787
+ "hooks",
1788
+ "withoutHooks",
1789
+ "components",
1790
+ "resources"
1791
+ ];
1792
+ /**
1793
+ * What a part of a game is, in words, to tell a new version's from the old: its code by its source, its data by its
1794
+ * values, so that a module evaluated again with the same code is the same
1795
+ */
1796
+ function shapeOf(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
1797
+ if (typeof value === "function") return `f:${String(value)}`;
1798
+ if (value === null || typeof value !== "object") return value === void 0 ? "u" : JSON.stringify(value);
1799
+ if (seen.has(value) || depth > 8) return "…";
1800
+ seen.add(value);
1801
+ const shape = Array.isArray(value) ? `[${value.map((each) => shapeOf(each, depth + 1, seen)).join(",")}]` : `{${Object.keys(value).sort().map((key) => `${key}:${shapeOf(value[key], depth + 1, seen)}`).join(",")}}`;
1802
+ seen.delete(value);
1803
+ return shape;
1804
+ }
1805
+ /** Who hears of a new version of a game's rules or of a mod: the client, which tells the games and editors of the page */
1806
+ var listeners = /* @__PURE__ */ new Set();
1807
+ var modListeners = /* @__PURE__ */ new Set();
1808
+ /**
1809
+ * Hears of every new version of a game's rules taken in the page (`hotRules`). Returns what stops it
1810
+ *
1811
+ * @beta
1812
+ */
1813
+ function onNewGameVersion(listener) {
1814
+ listeners.add(listener);
1815
+ return () => listeners.delete(listener);
1816
+ }
1817
+ /**
1818
+ * Hears of every new version of a mod taken in the page (`hotMod`). Returns what stops it
1819
+ *
1820
+ * @beta
1821
+ */
1822
+ function onNewModVersion(listener) {
1823
+ modListeners.add(listener);
1824
+ return () => modListeners.delete(listener);
1825
+ }
1826
+ /**
1827
+ * A mod's module's new version, taken while it runs (hot reload), as `hotRules` takes a game's: at its end,
1828
+ *
1829
+ * if (import.meta.hot) import.meta.hot.accept((module) => hotMod(import.meta.hot, goldRush, module?.goldRush))
1830
+ *
1831
+ * The mod given is made the new one in place, and every game of the page played with it is made again of its parts as
1832
+ * they are now (`recompose`) and takes that. What does not go (no new module, another mod, mods that no longer go
1833
+ * together) loads the page again
1834
+ *
1835
+ * @beta
1836
+ */
1837
+ function hotMod(hot, current, next) {
1838
+ const mod = next;
1839
+ if (!mod || typeof mod !== "object" || mod.id !== current.id) return hot?.invalidate(`${current.id}: its module is not there as it was`);
1840
+ try {
1841
+ replaceIn(current, mod);
1842
+ for (const listener of modListeners) listener(current);
1843
+ } catch (error) {
1844
+ rootLogger.warn("game", `The mod ${current.id}'s new version could not be taken while it runs: the page loads again`, { error: String(error) });
1845
+ hot?.invalidate();
1846
+ }
1847
+ }
1848
+ /**
1849
+ * Puts what `next` has into `current`, in its place: everything that holds `current` has the new version
1850
+ *
1851
+ * @beta
1852
+ */
1853
+ function replaceIn(current, next) {
1854
+ for (const key of Object.keys(current)) if (!(key in next)) delete current[key];
1855
+ Object.assign(current, next);
1856
+ }
1857
+ /**
1858
+ * A game's entry module's new version, taken while it runs (hot reload). At the module's end, the call to Vite written
1859
+ * out as it is (Vite finds a module that takes its own new versions by it):
1860
+ *
1861
+ * if (import.meta.hot) import.meta.hot.accept((module) => hotRules(import.meta.hot, melee, module?.melee))
1862
+ *
1863
+ * As it or a file it reads (its units, its `data.json`) is saved in the dev server, the module given is made the new
1864
+ * one in place, so that what holds it has the new one, and the games and editors of the page playing it take its
1865
+ * definitions and rules (`reloadGame`, through `onNewGameVersion`). What does not go loads the page again, saying why
1866
+ * (`hot.invalidate(reason)`): no new module, another game, a component defined another way, or a change to what a
1867
+ * running world keeps as it was made (its `systems`, `hooks`, `components`, `resources`, and `withoutSystems` and
1868
+ * `withoutHooks`, compared by their code and values). No client is pulled into the rules, which a server reads too
1869
+ *
1870
+ * @beta
1871
+ */
1872
+ function hotRules(hot, current, next) {
1873
+ const game = next;
1874
+ if (!game || typeof game !== "object" || game.id !== current.id) return hot?.invalidate(`${current.id}: its module is not there as it was`);
1875
+ const kept = NOT_RELOADED.filter((key) => shapeOf(current[key]) !== shapeOf(game[key]));
1876
+ if (kept.length) return hot?.invalidate(`${current.id}: its ${kept.join(", ")} changed, which a running game does not take`);
1877
+ try {
1878
+ replaceIn(current, game);
1879
+ for (const listener of listeners) listener(current);
1880
+ } catch (error) {
1881
+ rootLogger.warn("game", `${current.id}'s new version could not be taken while it runs: the page loads again`, { error: String(error) });
1882
+ hot?.invalidate();
1883
+ }
1884
+ }
1885
+ //#endregion
1886
+ //#region src/session/worker.ts
1887
+ /** Milliseconds between two words of where the time went */
1888
+ var TIMING_EVERY = 1e3;
1889
+ var firstOfMap = (map) => (map.players.find((player) => player.start) ?? map.players[0])?.id;
1890
+ function firstOfWorld(world) {
1891
+ for (const player of world.players.values()) if (!player.neutral && !player.creeps) return player.id;
1892
+ throw new Error("The world has no player to play: every one of its players is neutral");
1893
+ }
1894
+ /**
1895
+ * What a game's worker file does: `serveWorld(game, { start })`. It waits for the page to say what to play (a map, a
1896
+ * save, or nothing for a world made here in code), makes the world, runs it by the clock, and seats the page's player
1897
+ * at it. A new game asked for ends the one before: a restart is the same worker, its navigation built already
1898
+ *
1899
+ * @beta
1900
+ */
1901
+ function serveWorld(first, options = {}) {
1902
+ /** The game's version now: the one it began with, or the one it took last (`reload`) */
1903
+ let game = first;
1904
+ const port = options.port ?? globalThis;
1905
+ const post = (message, transfer) => port.postMessage(message, transfer);
1906
+ /** The game running now, and what ends it */
1907
+ let running;
1908
+ /** Whether the systems are measured, as the page's stats ask: off until they do */
1909
+ let profiling = false;
1910
+ /** Each start has a number: one overtaken while its world was made is let go */
1911
+ let starts = 0;
1912
+ const start = async (asked) => {
1913
+ const mine = ++starts;
1914
+ running?.end();
1915
+ running = void 0;
1916
+ const log = new Logger();
1917
+ const stopLog = log.to((entry) => post({
1918
+ vg: "log",
1919
+ entry
1920
+ }));
1921
+ let world;
1922
+ try {
1923
+ const seed = asked.seed ?? randomSeed();
1924
+ world = asked.from ? await loadWorld(asked.from, {
1925
+ game,
1926
+ log
1927
+ }) : asked.map ? await createWorldFromMap({
1928
+ map: asked.map,
1929
+ game,
1930
+ seed,
1931
+ log
1932
+ }) : options.world ? await options.world({
1933
+ rules: game,
1934
+ seed,
1935
+ args: asked.args,
1936
+ log
1937
+ }) : void 0;
1938
+ if (!world) throw new Error("A game in a worker needs a map, a save to go on from, or a world made in the worker (serveWorld world)");
1939
+ if (mine !== starts) return world.dispose();
1940
+ world.errorPolicy ??= "end";
1941
+ const player = asked.player ?? options.player?.(world, asked.map) ?? (asked.map && firstOfMap(asked.map)) ?? firstOfWorld(world);
1942
+ if (!asked.from) await options.start?.(world, {
1943
+ player,
1944
+ map: asked.map,
1945
+ args: asked.args
1946
+ });
1947
+ if (mine !== starts) return world.dispose();
1948
+ if (asked.reveal) world.fog.setRevealed(world.player(player).team, true);
1949
+ if (asked.debug) world.debugPlayers.add(player);
1950
+ const made = world;
1951
+ const host = new SessionHost(made, {
1952
+ ownsWorld: true,
1953
+ sendRate: made.tickRate,
1954
+ dropSilentAfter: Infinity
1955
+ });
1956
+ let hear = () => {};
1957
+ const transport = {
1958
+ send: (text) => post(text),
1959
+ onMessage: (handler) => hear = handler,
1960
+ onClose: () => {},
1961
+ close: () => {}
1962
+ };
1963
+ const stopCheckpoint = made.events.on(checkpoint, ({ label }) => {
1964
+ setTimeout(() => {
1965
+ if (running?.host === host) post({
1966
+ vg: "checkpoint",
1967
+ label,
1968
+ save: saveWorld(made)
1969
+ });
1970
+ }, 0);
1971
+ });
1972
+ made.scheduler.profiling = profiling;
1973
+ const timingTimer = setInterval(() => {
1974
+ const timing = host.takeTiming();
1975
+ post({
1976
+ vg: "timing",
1977
+ tick: timing.ticks ? timing.tickTime / timing.ticks : 0,
1978
+ tickMax: timing.tickMax,
1979
+ send: timing.sends ? timing.sendTime / timing.sends : 0,
1980
+ systems: made.scheduler.timing()
1981
+ });
1982
+ }, TIMING_EVERY);
1983
+ running = {
1984
+ host,
1985
+ hear: (text) => hear(text),
1986
+ end: () => {
1987
+ clearInterval(timingTimer);
1988
+ stopCheckpoint();
1989
+ stopLog();
1990
+ host.dispose();
1991
+ }
1992
+ };
1993
+ post({
1994
+ vg: "ready",
1995
+ player
1996
+ });
1997
+ host.connect(player, transport);
1998
+ host.start();
1999
+ } catch (error) {
2000
+ stopLog();
2001
+ world?.dispose();
2002
+ if (mine === starts) post({
2003
+ vg: "refused",
2004
+ message: error instanceof Error ? error.message : String(error),
2005
+ stack: error instanceof Error ? error.stack : void 0
2006
+ });
2007
+ }
2008
+ };
2009
+ const heard = (message) => {
2010
+ if (typeof message === "string") return running?.hear(message);
2011
+ const said = message;
2012
+ if (typeof said !== "object" || said === null) return;
2013
+ switch (said.vg) {
2014
+ case "start":
2015
+ start(said);
2016
+ return;
2017
+ case "reload":
2018
+ (async () => {
2019
+ const world = running?.host.world;
2020
+ if (!options.reload) {
2021
+ world?.log.warn("game", `${game.id} in a worker has no reload (serveWorld reload): it keeps the version it began with`);
2022
+ return;
2023
+ }
2024
+ try {
2025
+ const next = await options.reload(Date.now());
2026
+ if (!next) return;
2027
+ game = next;
2028
+ if (world && running?.host.world === world) reloadGame(world, next);
2029
+ } catch (error) {
2030
+ world?.log.error("game", `${game.id}'s new version could not be taken in the worker: ${error instanceof Error ? error.message : String(error)}`);
2031
+ }
2032
+ })();
2033
+ return;
2034
+ case "stop":
2035
+ starts++;
2036
+ running?.end();
2037
+ running = void 0;
2038
+ return;
2039
+ case "speed":
2040
+ if (running) running.host.speed = said.speed;
2041
+ return;
2042
+ case "advance": return running?.host.advance(said.ticks);
2043
+ case "save":
2044
+ try {
2045
+ if (!running) throw new Error("There is no game to save");
2046
+ post({
2047
+ vg: "saved",
2048
+ id: said.id,
2049
+ save: saveWorld(running.host.world)
2050
+ });
2051
+ } catch (error) {
2052
+ post({
2053
+ vg: "saved",
2054
+ id: said.id,
2055
+ error: error instanceof Error ? error.message : String(error)
2056
+ });
2057
+ }
2058
+ return;
2059
+ case "navigation":
2060
+ navigationData(said.build).then((data) => post({
2061
+ vg: "built",
2062
+ id: said.id,
2063
+ data
2064
+ }), (error) => post({
2065
+ vg: "built",
2066
+ id: said.id,
2067
+ error: error instanceof Error ? error.message : String(error)
2068
+ }));
2069
+ return;
2070
+ case "profile":
2071
+ profiling = said.on;
2072
+ if (running) running.host.world.scheduler.profiling = said.on;
2073
+ return;
2074
+ }
2075
+ };
2076
+ if (port.on) port.on("message", heard);
2077
+ else port.addEventListener?.("message", (event) => heard(event.data));
2078
+ }
2079
+ /**
2080
+ * A seat at a game run in a worker of this page (`workerSession`): a `RemoteSession` whose host is a message away,
2081
+ * with what a game played in the page has besides: its pace held and stepped (`speed`, `advance`), a save of it
2082
+ * (`save`), the checkpoint to start again from, what it says in its log, the debug tools' view, and where its time went
2083
+ *
2084
+ * @beta
2085
+ */
2086
+ var WorkerSession = class extends RemoteSession {
2087
+ /** What the world says (`World.log`), said again here: the page shows it with its own */
2088
+ log;
2089
+ checkpoint;
2090
+ /** The debug tools' view of the world, where they are on: over the session's own words, as a server's is */
2091
+ get debug() {
2092
+ return this.debugView;
2093
+ }
2094
+ /** Its pace held and stepped, saves of it, the debug tools' view where they are on, and where its time went and in which systems */
2095
+ can = {
2096
+ pace: paceOf(this),
2097
+ saves: savesOf(this),
2098
+ inspect: this.debugView,
2099
+ timing: {
2100
+ timing: () => this.timing(),
2101
+ profile: (on) => this.postToWorker({
2102
+ vg: "profile",
2103
+ on
2104
+ })
2105
+ }
2106
+ };
2107
+ pace = 1;
2108
+ timed;
2109
+ /** Milliseconds the page took to take in what the worker sent, each time, evened out: its share of a game run there */
2110
+ received = 0;
2111
+ postToWorker = () => {};
2112
+ saves = /* @__PURE__ */ new Map();
2113
+ nextSave = 0;
2114
+ stopHearing = () => {};
2115
+ /** What `workerSession` gives it once it is made: the way to the worker, and what stops hearing it */
2116
+ wire(post, log, stop) {
2117
+ this.postToWorker = post;
2118
+ this.log = log;
2119
+ this.stopHearing = stop;
2120
+ }
2121
+ /** What the worker said that is not the session's own text */
2122
+ heard(said) {
2123
+ if (said.vg === "checkpoint") this.checkpoint = {
2124
+ label: said.label,
2125
+ save: said.save
2126
+ };
2127
+ else if (said.vg === "timing") this.timed = {
2128
+ tick: said.tick,
2129
+ tickMax: said.tickMax,
2130
+ send: said.send,
2131
+ systems: said.systems
2132
+ };
2133
+ else if (said.vg === "saved") {
2134
+ const waiting = this.saves.get(said.id);
2135
+ this.saves.delete(said.id);
2136
+ if (said.save) waiting?.done(said.save);
2137
+ else waiting?.failed(new Error(said.error ?? "The game could not be saved"));
2138
+ }
2139
+ }
2140
+ /**
2141
+ * How far behind the newest tick the screen is shown, in ticks: one, and room for how unevenly they come (a tick
2142
+ * that took long comes late). Less than over a wire, where two are kept in hand: there is no line to stall here
2143
+ */
2144
+ get delay() {
2145
+ return this.interval + Math.min(this.jitter, this.limits.maxJitter ?? .2) * this.tickRate * 2;
2146
+ }
2147
+ get speed() {
2148
+ return this.pace;
2149
+ }
2150
+ set speed(speed) {
2151
+ this.pace = speed;
2152
+ this.postToWorker({
2153
+ vg: "speed",
2154
+ speed
2155
+ });
2156
+ }
2157
+ advance(ticks = 1) {
2158
+ this.postToWorker({
2159
+ vg: "advance",
2160
+ ticks
2161
+ });
2162
+ }
2163
+ /**
2164
+ * Asks the worker to take a new version of the game (hot reload): it loads the game again itself, as its file says
2165
+ * (`serveWorld` `reload`), and puts it into its world
2166
+ */
2167
+ reloadGame() {
2168
+ this.postToWorker({ vg: "reload" });
2169
+ }
2170
+ save() {
2171
+ return new Promise((done, failed) => {
2172
+ const id = ++this.nextSave;
2173
+ this.saves.set(id, {
2174
+ done,
2175
+ failed
2176
+ });
2177
+ this.postToWorker({
2178
+ vg: "save",
2179
+ id
2180
+ });
2181
+ });
2182
+ }
2183
+ timing() {
2184
+ return this.timed && {
2185
+ ...this.timed,
2186
+ receive: this.received
2187
+ };
2188
+ }
2189
+ /** Ends the game in the worker. The worker is the page's: it stays, for the next game, until the page ends it */
2190
+ dispose() {
2191
+ this.postToWorker({ vg: "stop" });
2192
+ this.stopHearing();
2193
+ for (const waiting of this.saves.values()) waiting.failed(/* @__PURE__ */ new Error("The game ended"));
2194
+ this.saves.clear();
2195
+ super.dispose();
2196
+ }
2197
+ };
2198
+ /**
2199
+ * Plays a game in a worker, and seats this page's player at it: the worker's file calls `serveWorld` with the same
2200
+ * game. Settles once the world is made there and its welcome is in; fails with what the worker said went wrong
2201
+ *
2202
+ * const session = await workerSession(new Worker(new URL('./game.worker.ts', import.meta.url), { type: 'module' }), { game, map })
2203
+ *
2204
+ * @beta
2205
+ */
2206
+ async function workerSession(worker, options) {
2207
+ const { game, navigation, maps, log: under, ...asked } = options;
2208
+ let toSession = () => {};
2209
+ let closed = () => {};
2210
+ let session;
2211
+ const builds = /* @__PURE__ */ new Map();
2212
+ let nextBuild = 0;
2213
+ let saying;
2214
+ const log = (under ?? rootLogger).child({ context: () => ({
2215
+ game: saying?.game,
2216
+ tick: saying?.tick
2217
+ }) });
2218
+ const post = (message) => worker.postMessage(message);
2219
+ let refuse = () => {};
2220
+ const refused = new Promise((_, reject) => refuse = reject);
2221
+ refused.catch(() => {});
2222
+ const heard = (message) => {
2223
+ if (typeof message === "string") {
2224
+ const began = performance.now();
2225
+ toSession(message);
2226
+ if (session) session.received += (performance.now() - began - session.received) * .1;
2227
+ return;
2228
+ }
2229
+ const said = message;
2230
+ if (typeof said !== "object" || said === null) return;
2231
+ if (said.vg === "refused") {
2232
+ const error = new Error(said.message);
2233
+ if (said.stack) error.stack = said.stack;
2234
+ refuse(error);
2235
+ } else if (said.vg === "log") {
2236
+ saying = said.entry;
2237
+ log.log(said.entry.level, said.entry.category, said.entry.message, said.entry.fields);
2238
+ saying = void 0;
2239
+ } else if (said.vg === "built") {
2240
+ const waiting = builds.get(said.id);
2241
+ builds.delete(said.id);
2242
+ if (said.data) waiting?.done(said.data);
2243
+ else waiting?.failed(new Error(said.error ?? "The navigation could not be built"));
2244
+ } else session?.heard(said);
2245
+ };
2246
+ const broke = (error) => {
2247
+ const reason = /* @__PURE__ */ new Error(`The game's worker stopped: ${error instanceof Error ? error.message : String(error?.message ?? error)}`);
2248
+ log.error("game", reason.message, { error: errorFields(error) });
2249
+ refuse(reason);
2250
+ closed();
2251
+ };
2252
+ const onMessage = (event) => heard(event.data);
2253
+ if (worker.on) {
2254
+ worker.on("message", heard);
2255
+ worker.on("error", broke);
2256
+ } else {
2257
+ worker.addEventListener?.("message", onMessage);
2258
+ worker.addEventListener?.("error", broke);
2259
+ }
2260
+ const stop = () => {
2261
+ if (worker.off) {
2262
+ worker.off("message", heard);
2263
+ worker.off("error", broke);
2264
+ } else {
2265
+ worker.removeEventListener?.("message", onMessage);
2266
+ worker.removeEventListener?.("error", broke);
2267
+ }
2268
+ };
2269
+ const transport = {
2270
+ send: (text) => post(text),
2271
+ onMessage: (handler) => toSession = handler,
2272
+ onClose: (handler) => closed = handler,
2273
+ close: () => {}
2274
+ };
2275
+ post({
2276
+ vg: "start",
2277
+ ...asked
2278
+ });
2279
+ try {
2280
+ session = await Promise.race([connectAs(transport, {
2281
+ game,
2282
+ maps,
2283
+ navigation: {
2284
+ ...navigation,
2285
+ builds: { builder: (build) => new Promise((done, failed) => {
2286
+ const id = ++nextBuild;
2287
+ builds.set(id, {
2288
+ done,
2289
+ failed
2290
+ });
2291
+ post({
2292
+ vg: "navigation",
2293
+ id,
2294
+ build: {
2295
+ positions: build.positions.slice(),
2296
+ indices: build.indices.slice(),
2297
+ config: build.config,
2298
+ ...build.speeds && { speeds: {
2299
+ ...build.speeds,
2300
+ values: build.speeds.values.slice()
2301
+ } }
2302
+ }
2303
+ });
2304
+ }) }
2305
+ }
2306
+ }, (...made) => new WorkerSession(...made)), refused]);
2307
+ } catch (error) {
2308
+ post({ vg: "stop" });
2309
+ stop();
2310
+ throw error;
2311
+ }
2312
+ session.wire(post, log, stop);
2313
+ return session;
2314
+ }
2315
+ //#endregion
2316
+ //#region src/session/socket.ts
2317
+ /**
2318
+ * The player's end of a connection to a server, over a WebSocket: what `connect` and a server's lobby talk through.
2319
+ * Resolves once the connection is open; fails when it cannot be made
2320
+ *
2321
+ * @beta
2322
+ */
2323
+ function openSocket(url) {
2324
+ return new Promise((resolve, reject) => {
2325
+ const socket = new WebSocket(url);
2326
+ let handler = () => {};
2327
+ let closed = () => {};
2328
+ socket.onmessage = (event) => handler(String(event.data));
2329
+ socket.onclose = () => closed();
2330
+ socket.onerror = () => reject(/* @__PURE__ */ new Error(`Could not connect to ${url}`));
2331
+ socket.onopen = () => resolve({
2332
+ send: (message) => {
2333
+ if (socket.readyState === WebSocket.OPEN) socket.send(message);
2334
+ },
2335
+ onMessage: (next) => handler = next,
2336
+ onClose: (next) => closed = next,
2337
+ close: () => socket.close()
2338
+ });
2339
+ });
2340
+ }
2341
+ //#endregion
2342
+ //#region src/session/share.ts
2343
+ /** How often the host looks whether the world went on, a second: it sends at its own rate */
2344
+ var LOOKS$2 = 30;
2345
+ /**
2346
+ * Shares the page's world through a relay, for others to watch. Fails when the relay cannot be reached
2347
+ *
2348
+ * @beta
2349
+ */
2350
+ async function shareGame(world, options) {
2351
+ const relay = await openSocket(options.url);
2352
+ const host = new SessionHost(world, { sendRate: options.sendRate });
2353
+ /** Each watcher's end of the connection, as the host holds it: what the relay brings from them goes to it */
2354
+ const ends = /* @__PURE__ */ new Map();
2355
+ let state = "shared";
2356
+ let changed = () => {};
2357
+ const ask = (request) => relay.send(JSON.stringify(request));
2358
+ const seat = (id) => {
2359
+ const end = {
2360
+ message: (_text) => {},
2361
+ closed: () => {}
2362
+ };
2363
+ ends.set(id, end);
2364
+ host.connect(options.player, {
2365
+ send: (text) => ask({
2366
+ type: "send",
2367
+ to: id,
2368
+ text
2369
+ }),
2370
+ onMessage: (handler) => end.message = handler,
2371
+ onClose: (handler) => end.closed = handler,
2372
+ close: () => {}
2373
+ }, {
2374
+ readOnly: true,
2375
+ observer: true
2376
+ });
2377
+ };
2378
+ let last = performance.now();
2379
+ const timer = setInterval(() => {
2380
+ const now = performance.now();
2381
+ host.publish((now - last) / 1e3);
2382
+ last = now;
2383
+ }, 1e3 / LOOKS$2);
2384
+ /** Over once: closed, or never shared as the key was taken */
2385
+ const stop = (next = "closed") => {
2386
+ if (state !== "shared") return;
2387
+ clearInterval(timer);
2388
+ host.dispose();
2389
+ ends.clear();
2390
+ state = next;
2391
+ changed();
2392
+ };
2393
+ relay.onMessage((text) => {
2394
+ const notice = JSON.parse(text);
2395
+ if (notice.type === "taken") {
2396
+ stop("taken");
2397
+ relay.close();
2398
+ } else if (notice.type === "watcher") {
2399
+ seat(notice.id);
2400
+ changed();
2401
+ } else if (notice.type === "from") ends.get(notice.watcher)?.message(notice.text);
2402
+ else if (notice.type === "gone") {
2403
+ ends.get(notice.watcher)?.closed();
2404
+ ends.delete(notice.watcher);
2405
+ changed();
2406
+ }
2407
+ });
2408
+ relay.onClose(() => stop());
2409
+ ask({
2410
+ type: "share",
2411
+ key: options.key,
2412
+ name: options.name
2413
+ });
2414
+ return {
2415
+ get state() {
2416
+ return state;
2417
+ },
2418
+ get watchers() {
2419
+ return ends.size;
2420
+ },
2421
+ onChange(handler) {
2422
+ changed = handler;
2423
+ },
2424
+ close() {
2425
+ stop();
2426
+ relay.close();
2427
+ }
2428
+ };
2429
+ }
2430
+ //#endregion
2431
+ //#region src/generateWorker.ts
2432
+ /**
2433
+ * What a worker's file of a game's maps does: it makes, with the game's generator (`GameModule.generator`) and its
2434
+ * registry, the map of each set of options it is sent, and sends it back. A worker's file of two lines, the game's rules
2435
+ * imported and this called with them; `port` is where it hears, as for `serveNavigationBuilds`: the worker's own scope
2436
+ * when left out
2437
+ *
2438
+ * @beta
2439
+ */
2440
+ function serveMapGenerator(rules, port = globalThis) {
2441
+ let registry;
2442
+ const asked = (message) => {
2443
+ const { id, options } = message;
2444
+ try {
2445
+ if (!rules.generator) throw new Error(`${rules.name} has no generator of maps (GameModule.generator)`);
2446
+ registry ??= createRegistry(rules);
2447
+ port.postMessage({
2448
+ id,
2449
+ made: generateMap(rules.generator, options, registry)
2450
+ });
2451
+ } catch (error) {
2452
+ port.postMessage({
2453
+ id,
2454
+ error: error instanceof Error ? error.message : String(error)
2455
+ });
2456
+ }
2457
+ };
2458
+ if (port.on) port.on("message", asked);
2459
+ else port.addEventListener?.("message", (event) => asked(event.data));
2460
+ }
2461
+ /**
2462
+ * Maps made in a worker whose file serves them (`serveMapGenerator`): the page makes the worker as its bundler wants it,
2463
+ * `mapGeneratorWorker(() => new Worker(new URL('./generate.worker.ts', import.meta.url), { type: 'module' }))`. Given
2464
+ * as a function, the worker is made at the first map asked for: a menu that is never opened fetches none of it
2465
+ *
2466
+ * @beta
2467
+ */
2468
+ function mapGeneratorWorker(given) {
2469
+ let next = 0;
2470
+ const waiting = /* @__PURE__ */ new Map();
2471
+ const answered = (answer) => {
2472
+ const wait = waiting.get(answer.id);
2473
+ if (!wait) return;
2474
+ waiting.delete(answer.id);
2475
+ if (answer.made) wait.done(answer.made);
2476
+ else wait.failed(new Error(answer.error ?? "The map could not be made"));
2477
+ };
2478
+ const broke = (error) => {
2479
+ const reason = /* @__PURE__ */ new Error(`The maps' worker stopped: ${error instanceof Error ? error.message : String(error?.message ?? error)}`);
2480
+ for (const wait of waiting.values()) wait.failed(reason);
2481
+ waiting.clear();
2482
+ };
2483
+ let made;
2484
+ const worker = () => {
2485
+ if (made) return made;
2486
+ made = typeof given === "function" ? given() : given;
2487
+ if (made.on) {
2488
+ made.on("message", (value) => answered(value));
2489
+ made.on("error", broke);
2490
+ } else {
2491
+ made.addEventListener?.("message", (event) => answered(event.data));
2492
+ made.addEventListener?.("error", broke);
2493
+ }
2494
+ return made;
2495
+ };
2496
+ if (typeof given !== "function") worker();
2497
+ return (options) => new Promise((done, failed) => {
2498
+ const id = ++next;
2499
+ waiting.set(id, {
2500
+ done,
2501
+ failed
2502
+ });
2503
+ worker().postMessage({
2504
+ id,
2505
+ options
2506
+ });
2507
+ });
2508
+ }
2509
+ //#endregion
2510
+ //#region src/rulesCheck.ts
2511
+ /**
2512
+ * Checks a game's rules against themselves (`RulesProblem`): run by `defineGame` with the view's checks, and in a
2513
+ * game's tests (`expect(checkRules(rules).filter((p) => p.level === 'error')).toEqual([])`). Nothing is thrown
2514
+ *
2515
+ * @beta
2516
+ */
2517
+ function checkRules(rules) {
2518
+ const problems = [];
2519
+ const ids = (list) => new Set((list ?? []).map((def) => def.id));
2520
+ const units = ids(rules.units);
2521
+ const abilities = ids(rules.abilities);
2522
+ const buffs = ids(rules.buffs);
2523
+ const items = ids(rules.items);
2524
+ const upgrades = ids(rules.upgrades);
2525
+ const resources = ids(rules.resources);
2526
+ /** The types of effect something handles: the engine's own, and the game's (`GameModule.effects`) */
2527
+ const handled = new Set([
2528
+ ...coreEffects,
2529
+ ...summonEffects,
2530
+ ...dispelEffects,
2531
+ ...rules.effects ?? []
2532
+ ].map((kind) => kind.type));
2533
+ const effectTypes = (type) => handled.has(type);
2534
+ const known = (where, id, set, what) => {
2535
+ if (id === void 0 || set.has(id)) return;
2536
+ const meant = nearestId(id, set);
2537
+ problems.push({
2538
+ level: "error",
2539
+ where,
2540
+ id,
2541
+ message: `${where} names "${id}", which is no ${what} of the rules${meant ? `. Did you mean "${meant}"?` : ""}`
2542
+ });
2543
+ };
2544
+ /** A number that does not do what it is there for: said as a warning, the engine making do with it */
2545
+ const odd = (where, id, message) => problems.push({
2546
+ level: "warning",
2547
+ where,
2548
+ id,
2549
+ message
2550
+ });
2551
+ /** Whether a number, or any of its levels (`Scaled`), is under a bound: at `least`, or above it where `above` */
2552
+ const under = (value, least, above = false) => value !== void 0 && (Array.isArray(value) ? value : [value]).some((each) => above ? each <= least : each < least);
2553
+ const all = (where, list, set, what) => list?.forEach((id, index) => known(`${where}[${index}]`, id, set, what));
2554
+ /** What a requirement names: a unit type (a building to have) or an upgrade */
2555
+ const requirable = /* @__PURE__ */ new Set([...units, ...upgrades]);
2556
+ for (const unit of rules.units) {
2557
+ const at = `units.${unit.id}`;
2558
+ all(`${at}.trains`, unit.trains, units, "unit type");
2559
+ all(`${at}.builds`, unit.builds, units, "unit type");
2560
+ all(`${at}.hires`, unit.hires, units, "unit type");
2561
+ all(`${at}.researches`, unit.researches, upgrades, "upgrade");
2562
+ all(`${at}.requires`, unit.requires, requirable, "unit type or upgrade");
2563
+ all(`${at}.sells`, unit.sells, items, "item");
2564
+ all(`${at}.abilities`, unit.abilities, abilities, "ability");
2565
+ if (typeof unit.upgradesTo === "string") known(`${at}.upgradesTo`, unit.upgradesTo, units, "unit type");
2566
+ else all(`${at}.upgradesTo`, unit.upgradesTo, units, "unit type");
2567
+ if (Array.isArray(unit.drops)) unit.drops.forEach((drop, index) => known(`${at}.drops[${index}].item`, drop.item, items, "item"));
2568
+ if (unit.hero) {
2569
+ all(`${at}.hero.learns`, unit.hero.learns, abilities, "ability");
2570
+ all(`${at}.hero.spells`, unit.hero.spells, abilities, "ability");
2571
+ known(`${at}.hero.meter`, unit.hero.meter, buffs, "buff");
2572
+ }
2573
+ }
2574
+ const effects = (where, list) => {
2575
+ list?.map(anyEffect).forEach((effect, index) => {
2576
+ const at = `${where}[${index}]`;
2577
+ if (!effectTypes(effect.type)) problems.push({
2578
+ level: "error",
2579
+ where: `${at}.type`,
2580
+ id: effect.type,
2581
+ message: `${at} is of the type "${effect.type}", which nothing handles (GameModule.effects)`
2582
+ });
2583
+ known(`${at}.buff`, effect.buff, buffs, "buff");
2584
+ known(`${at}.ifBuff`, effect.ifBuff, buffs, "buff");
2585
+ known(`${at}.whileBuff`, effect.whileBuff, buffs, "buff");
2586
+ known(`${at}.unit`, effect.unit, units, "unit type");
2587
+ known(`${at}.ability`, effect.ability, abilities, "ability");
2588
+ known(`${at}.sell`, effect.sell, resources, "resource");
2589
+ known(`${at}.buy`, effect.buy, resources, "resource");
2590
+ for (const field of ["arc", "bounce"]) if (effect[field] && effect.type !== "missile") odd(`${at}.${field}`, effect.type, `${at}.${field} is read of a missile alone, not of ${effect.type}`);
2591
+ if (effect.arc && !(effect.arc.height > 0)) odd(`${at}.arc.height`, effect.type, `${at}.arc.height is not above 0: it is thrown flat`);
2592
+ if (effect.arc?.time !== void 0 && !(effect.arc.time > 0)) odd(`${at}.arc.time`, effect.type, `${at}.arc.time is not above 0: it flies at its speed`);
2593
+ if (effect.bounce && under(effect.bounce.count, 1)) odd(`${at}.bounce.count`, effect.type, `${at}.bounce.count is under 1 at a level: it does not spring on there`);
2594
+ if (effect.bounce && !(effect.bounce.range > 0)) odd(`${at}.bounce.range`, effect.type, `${at}.bounce.range is not above 0: it finds nothing to spring to`);
2595
+ effects(`${at}.effects`, effect.effects);
2596
+ effects(`${at}.onEnd`, effect.onEnd);
2597
+ effects(`${at}.otherwise`, effect.otherwise);
2598
+ });
2599
+ };
2600
+ for (const ability of rules.abilities ?? []) {
2601
+ const at = `abilities.${ability.id}`;
2602
+ known(`${at}.charge`, ability.charge, buffs, "buff");
2603
+ if (ability.charges && under(ability.charges.count, 1)) odd(`${at}.charges.count`, ability.id, `${at}.charges.count is under 1 at a level: it holds 1 there`);
2604
+ if (ability.charges && under(ability.charges.refill, 0, true)) odd(`${at}.charges.refill`, ability.id, `${at}.charges.refill is not above 0 at a level: a use comes back at once there`);
2605
+ effects(`${at}.effects`, ability.effects);
2606
+ effects(`${at}.passive`, ability.passive);
2607
+ }
2608
+ for (const buff of rules.buffs ?? []) {
2609
+ const at = `buffs.${buff.id}`;
2610
+ if (Array.isArray(buff.onApply)) effects(`${at}.onApply`, buff.onApply);
2611
+ effects(`${at}.onTick.effects`, buff.onTick?.effects);
2612
+ }
2613
+ for (const race of rules.races ?? []) {
2614
+ const start = race.start;
2615
+ if (!start || "spawn" in start) continue;
2616
+ const at = `races.${race.id}.start`;
2617
+ known(`${at}.hall`, start.hall, units, "unit type");
2618
+ known(`${at}.workers.type`, start.workers?.type, units, "unit type");
2619
+ known(`${at}.onMine`, start.onMine, units, "unit type");
2620
+ start.units?.forEach((each, index) => known(`${at}.units[${index}].type`, each.type, units, "unit type"));
2621
+ }
2622
+ for (const item of rules.items ?? []) {
2623
+ const at = `items.${item.id}`;
2624
+ known(`${at}.ability`, item.ability, abilities, "ability");
2625
+ known(`${at}.passive`, item.passive, abilities, "ability");
2626
+ all(`${at}.from`, item.from, items, "item");
2627
+ }
2628
+ for (const upgrade of rules.upgrades ?? []) {
2629
+ const at = `upgrades.${upgrade.id}`;
2630
+ upgrade.requires?.forEach((level, index) => all(`${at}.requires[${index}]`, level, requirable, "unit type or upgrade"));
2631
+ upgrade.effects.forEach((effect, index) => {
2632
+ if (Array.isArray(effect.units)) all(`${at}.effects[${index}].units`, effect.units, units, "unit type");
2633
+ });
2634
+ }
2635
+ const needs = /* @__PURE__ */ new Map();
2636
+ for (const unit of rules.units) if (unit.requires?.length) needs.set(unit.id, unit.requires);
2637
+ for (const upgrade of rules.upgrades ?? []) if (upgrade.requires?.[0]?.length) needs.set(upgrade.id, upgrade.requires[0]);
2638
+ const reported = /* @__PURE__ */ new Set();
2639
+ for (const start of needs.keys()) {
2640
+ const loop = cycleFrom(start, needs);
2641
+ if (!loop) continue;
2642
+ const key = [...loop].sort().join(",");
2643
+ if (reported.has(key)) continue;
2644
+ reported.add(key);
2645
+ problems.push({
2646
+ level: "error",
2647
+ where: `requires`,
2648
+ id: start,
2649
+ message: `What is required waits on itself: ${[...loop, loop[0]].join(" → ")}. None of them can be had`
2650
+ });
2651
+ }
2652
+ const made = /* @__PURE__ */ new Set();
2653
+ for (const unit of rules.units) {
2654
+ for (const id of [
2655
+ ...unit.trains ?? [],
2656
+ ...unit.builds ?? [],
2657
+ ...unit.hires ?? [],
2658
+ ...unit.researches ?? []
2659
+ ]) made.add(id);
2660
+ for (const id of upgradesOf(unit)) made.add(id);
2661
+ }
2662
+ for (const unit of rules.units) {
2663
+ if (!unit.cost || made.has(unit.id)) continue;
2664
+ problems.push({
2665
+ level: "warning",
2666
+ where: `units.${unit.id}`,
2667
+ id: unit.id,
2668
+ message: `units.${unit.id} has a cost, and nothing trains, builds or hires it`
2669
+ });
2670
+ }
2671
+ for (const upgrade of rules.upgrades ?? []) {
2672
+ if (made.has(upgrade.id)) continue;
2673
+ problems.push({
2674
+ level: "warning",
2675
+ where: `upgrades.${upgrade.id}`,
2676
+ id: upgrade.id,
2677
+ message: `upgrades.${upgrade.id} is researched nowhere (UnitTypeDef.researches)`
2678
+ });
2679
+ }
2680
+ const had = /* @__PURE__ */ new Set();
2681
+ for (const unit of rules.units) for (const id of [
2682
+ ...unit.abilities ?? [],
2683
+ ...unit.hero?.learns ?? [],
2684
+ ...unit.hero?.spells ?? []
2685
+ ]) had.add(id);
2686
+ for (const item of rules.items ?? []) for (const id of [item.ability, item.passive]) if (id) had.add(id);
2687
+ const named = (ability) => {
2688
+ const walk = (list) => list?.map(anyEffect).forEach((effect) => (effect.ability && had.add(effect.ability), walk(effect.otherwise)));
2689
+ walk(ability.effects);
2690
+ walk(ability.passive);
2691
+ };
2692
+ for (const ability of rules.abilities ?? []) named(ability);
2693
+ for (const ability of rules.abilities ?? []) {
2694
+ if (had.has(ability.id)) continue;
2695
+ problems.push({
2696
+ level: "warning",
2697
+ where: `abilities.${ability.id}`,
2698
+ id: ability.id,
2699
+ message: `abilities.${ability.id} is on no unit, hero or item`
2700
+ });
2701
+ }
2702
+ return problems.map((problem) => {
2703
+ const at = locate(rules, problem.where);
2704
+ return at ? {
2705
+ ...problem,
2706
+ at
2707
+ } : problem;
2708
+ });
2709
+ }
2710
+ /**
2711
+ * The definition and field a place in the rules is (`abilities.holy-light.effects[0].buff`): the kind, the longest id
2712
+ * of it the place begins with (an id may have dots), and the rest as a path of the Data panel's (`effects.0.buff`)
2713
+ */
2714
+ function locate(rules, where) {
2715
+ const dot = where.indexOf(".");
2716
+ const kind = where.slice(0, dot);
2717
+ if (dot < 0 || !DATA_KINDS.includes(kind)) return void 0;
2718
+ const rest = where.slice(dot + 1);
2719
+ const id = (rules[kind] ?? []).map((def) => def.id).filter((each) => rest === each || rest.startsWith(`${each}.`)).sort((a, b) => b.length - a.length)[0];
2720
+ if (id === void 0) return void 0;
2721
+ return {
2722
+ kind,
2723
+ id,
2724
+ path: rest.slice(id.length + 1).replace(/\[(\d+)\]/g, ".$1")
2725
+ };
2726
+ }
2727
+ /** A loop of requirements through `start`, in order, or none */
2728
+ function cycleFrom(start, needs) {
2729
+ const path = [];
2730
+ const seen = /* @__PURE__ */ new Set();
2731
+ const visit = (id) => {
2732
+ if (id === start && path.length) return [...path];
2733
+ if (seen.has(id)) return void 0;
2734
+ seen.add(id);
2735
+ path.push(id);
2736
+ for (const next of needs.get(id) ?? []) {
2737
+ const found = visit(next);
2738
+ if (found) return found;
2739
+ }
2740
+ path.pop();
2741
+ };
2742
+ return visit(start);
2743
+ }
2744
+ //#endregion
2745
+ //#region src/attributes.ts
2746
+ /**
2747
+ * The attributes pack's id
2748
+ *
2749
+ * @beta
2750
+ */
2751
+ var ATTRIBUTES = "core.attributes";
2752
+ /**
2753
+ * The three attributes, in the order a hero's panel shows them
2754
+ *
2755
+ * @beta
2756
+ */
2757
+ var ATTRIBUTE_KEYS = [
2758
+ "strength",
2759
+ "agility",
2760
+ "intelligence"
2761
+ ];
2762
+ /**
2763
+ * What a point of each attribute gives, as classic RTS games have it: strength 25 health and 0.05 health a second, agility 0.3
2764
+ * armor and 2% attack speed, intelligence 15 mana and 0.05 mana a second
2765
+ *
2766
+ * @beta
2767
+ */
2768
+ var CLASSIC_ATTRIBUTES = {
2769
+ strength: { stats: {
2770
+ health: 25,
2771
+ healthRegen: .05
2772
+ } },
2773
+ agility: {
2774
+ stats: { armor: .3 },
2775
+ percent: { attackSpeed: .02 }
2776
+ },
2777
+ intelligence: { stats: {
2778
+ mana: 15,
2779
+ manaRegen: .05
2780
+ } }
2781
+ };
2782
+ var attributeStats = [
2783
+ {
2784
+ id: "strength",
2785
+ name: "Strength",
2786
+ description: "Each point adds to health and its regeneration",
2787
+ min: 0
2788
+ },
2789
+ {
2790
+ id: "agility",
2791
+ name: "Agility",
2792
+ description: "Each point adds to armor and attack speed",
2793
+ min: 0
2794
+ },
2795
+ {
2796
+ id: "intelligence",
2797
+ name: "Intelligence",
2798
+ description: "Each point adds to mana and its regeneration",
2799
+ min: 0
2800
+ }
2801
+ ];
2802
+ /** Whether a unit's type starts with any attribute */
2803
+ function startsWithAttributes(entity) {
2804
+ const stats = entity.def.stats;
2805
+ return !!stats && !!(stats.strength || stats.agility || stats.intelligence);
2806
+ }
2807
+ /**
2808
+ * What a unit has of each attribute, everything counted (its type, its level, items, buffs, research): its stats'
2809
+ * `strength`, `agility` and `intelligence`
2810
+ *
2811
+ * @beta
2812
+ */
2813
+ function attributesOf(world, entity) {
2814
+ const stats = statsOf(world, entity);
2815
+ return {
2816
+ strength: stats.strength ?? 0,
2817
+ agility: stats.agility ?? 0,
2818
+ intelligence: stats.intelligence ?? 0
2819
+ };
2820
+ }
2821
+ /**
2822
+ * A unit's primary attribute, as the attributes pack of its game says it (`AttributesOptions.primaryOf`, else its type's
2823
+ * `hero.primary`); none in a game without the pack
2824
+ *
2825
+ * @beta
2826
+ */
2827
+ function primaryAttribute(world, entity) {
2828
+ const pack = world.registry.assembly?.game.packs?.find((each) => each.id === ATTRIBUTES);
2829
+ if (!pack) return void 0;
2830
+ const options = pack.options;
2831
+ return options.primaryOf ? options.primaryOf(world, entity) : entity.def.hero?.primary;
2832
+ }
2833
+ /**
2834
+ * Hero attributes, as in classic RTS games: strength, agility and intelligence are stats (`ATTRIBUTE_KEYS`), and every point
2835
+ * of them adds to the others (`AttributesOptions.perPoint`, `CLASSIC_ATTRIBUTES` when left out), the primary one's
2836
+ * to the attack too (`hero.primary`, `primaryOf`). What a unit has of them is what it has of any stat: its type's
2837
+ * `stats`, its hero's `growth` by level, items' `bonus`, buffs, research and the `stats` hook, all of it added up
2838
+ * before the points are turned into the rest. A game without it has no attributes
2839
+ *
2840
+ * @beta
2841
+ */
2842
+ function withAttributes(options = {}) {
2843
+ const perPoint = {
2844
+ ...CLASSIC_ATTRIBUTES,
2845
+ ...options.perPoint
2846
+ };
2847
+ const primaryBonus = options.primary ?? { stats: { attackDamage: 1 } };
2848
+ const primaryOf = options.primaryOf ?? ((_world, entity) => entity.def.hero?.primary);
2849
+ const points = {
2850
+ id: "attributes",
2851
+ stamp: (entity) => startsWithAttributes(entity) ? heroOf(entity)?.level ?? 1 : 0,
2852
+ add() {},
2853
+ derive(world, entity, sum) {
2854
+ const strength = sum.get("strength");
2855
+ const agility = sum.get("agility");
2856
+ const intelligence = sum.get("intelligence");
2857
+ if (strength) sum.add(perPoint.strength, strength);
2858
+ if (agility) sum.add(perPoint.agility, agility);
2859
+ if (intelligence) sum.add(perPoint.intelligence, intelligence);
2860
+ const primary = primaryOf(world, entity);
2861
+ const of = primary === "strength" ? strength : primary === "agility" ? agility : primary === "intelligence" ? intelligence : 0;
2862
+ if (of) sum.add(primaryBonus, of);
2863
+ }
2864
+ };
2865
+ const made = defineHook({
2866
+ hook: "made",
2867
+ id: "attributes",
2868
+ run(world, entity) {
2869
+ if (startsWithAttributes(entity)) refreshVitals(world, entity);
2870
+ }
2871
+ });
2872
+ return {
2873
+ id: ATTRIBUTES,
2874
+ options,
2875
+ stats: attributeStats,
2876
+ statParts: [points],
2877
+ hooks: [made]
2878
+ };
2879
+ }
2880
+ //#endregion
2881
+ //#region src/manaBurn.ts
2882
+ /**
2883
+ * The mana burn pack's id
2884
+ *
2885
+ * @beta
2886
+ */
2887
+ var MANA_BURN = "core.manaBurn";
2888
+ /**
2889
+ * Takes up to `amount` of a unit's mana and hurts it by as much as it took (times `ratio`), from `source`: what the
2890
+ * `manaBurn` effect does, for a game's own rules. Gives back the mana it took: none of a unit that has none, and no
2891
+ * damage then
2892
+ *
2893
+ * @beta
2894
+ */
2895
+ function burnMana(world, target, amount, source, options = {}) {
2896
+ const burnt = Math.min(Math.max(amount, 0), target.mana);
2897
+ if (burnt <= 0) return 0;
2898
+ target.mana -= burnt;
2899
+ world.events.emit(manaChanged, {
2900
+ entity: target,
2901
+ amount: -burnt,
2902
+ ability: options.ability,
2903
+ source
2904
+ });
2905
+ const damage = burnt * (options.ratio ?? 1);
2906
+ if (damage > 0) world.damage(target, damage, source, {
2907
+ damageType: options.damageType ?? "magic",
2908
+ ability: options.ability
2909
+ });
2910
+ return burnt;
2911
+ }
2912
+ var manaBurnEffect = defineEffect("manaBurn", (context, effect) => {
2913
+ const { world, caster, level } = context;
2914
+ const amount = scaled(world, caster, at(effect.amount, level), effect.scaling, level);
2915
+ for (const entity of effectTargets(context, effect, "enemies")) burnMana(world, entity, amount, caster, {
2916
+ ratio: effect.ratio,
2917
+ damageType: effect.damageType,
2918
+ ability: context.ability.id
2919
+ });
2920
+ }, { fields: [
2921
+ "amount",
2922
+ "scaling",
2923
+ "damageType",
2924
+ {
2925
+ path: "ratio",
2926
+ kind: "number",
2927
+ min: 0,
2928
+ hint: "Times the mana taken it hurts by: 1 when left out"
2929
+ }
2930
+ ] });
2931
+ /**
2932
+ * Mana burn, as in classic RTS games: the `manaBurn` effect (`EffectTypes.manaBurn`) takes `amount` of what it reaches of its
2933
+ * mana and hurts it by as much (times `ratio`). The demon hunter's spell is an ability with it; Feedback is a passive
2934
+ * whose `onHit` has it. `burnMana` does it in code
2935
+ *
2936
+ * @beta
2937
+ */
2938
+ function withManaBurn() {
2939
+ return {
2940
+ id: MANA_BURN,
2941
+ effects: [manaBurnEffect]
2942
+ };
2943
+ }
2944
+ //#endregion
2945
+ //#region src/destructibles.ts
2946
+ /**
2947
+ * The destructibles pack's id
2948
+ *
2949
+ * @beta
2950
+ */
2951
+ var DESTRUCTIBLES = "core.destructibles";
2952
+ /**
2953
+ * Destructibles, as in classic RTS games: what is `destructible` (`UnitTypeDef.destructible`, a building of the neutral player
2954
+ * placed on the map: rocks, a barricade, a gate to break down) stands in the way until it is brought down, and any unit
2955
+ * whose attack reaches it attacks it when told to (the attack command), never of its own accord, as trees are felled
2956
+ * by what fells them. A gate that opens and shuts is the map's (`setGate`); one that is broken down is a destructible
2957
+ *
2958
+ * @beta
2959
+ */
2960
+ function withDestructibles(options = {}) {
2961
+ const destructible = options.destructible ?? ((_world, entity) => !!entity.def.destructible);
2962
+ return {
2963
+ id: DESTRUCTIBLES,
2964
+ options,
2965
+ hooks: [defineHook({
2966
+ hook: "canAttack",
2967
+ id: "destructibles",
2968
+ run(world, event) {
2969
+ if (event.allowed || !event.ordered) return;
2970
+ if (destructible(world, event.target) && attackReaches(world, event.attacker, event.target)) event.allowed = true;
2971
+ }
2972
+ })]
2973
+ };
2974
+ }
2975
+ //#endregion
2976
+ //#region src/portals.ts
2977
+ /**
2978
+ * The portals pack's id
2979
+ *
2980
+ * @beta
2981
+ */
2982
+ var PORTALS = "core.portals";
2983
+ /**
2984
+ * Where a waygate leads: a point of the map by its name (as the editor's field keeps it), or a place. A waygate without
2985
+ * it, or with `null`, is shut
2986
+ *
2987
+ * @beta
2988
+ */
2989
+ var waygate = defineComponent("core.waygate", { field: {
2990
+ name: "Leads to",
2991
+ hint: "The point of the map a unit that walks in comes out at",
2992
+ kind: "point",
2993
+ for: (def) => !!def.waygate
2994
+ } });
2995
+ /**
2996
+ * A unit went through a waygate, or a town portal: told to everyone who sees either end, for a sound and a flash
2997
+ *
2998
+ * @beta
2999
+ */
3000
+ var portalUsed = defineEvent("core.portalUsed", { replicate: "everyone" });
3001
+ /** How near a waygate's edge a unit is in it */
3002
+ var REACH$1 = .6;
3003
+ /**
3004
+ * Where a waygate leads, or nothing for one that is shut or leads to a point the map does not have
3005
+ *
3006
+ * @beta
3007
+ */
3008
+ function waygateTarget(world, gate) {
3009
+ const to = gate.get(waygate);
3010
+ if (!to || !gate.alive) return void 0;
3011
+ if (typeof to !== "string") return to;
3012
+ return world.map.points.some((point) => point.name === to) ? world.map.point(to) : void 0;
3013
+ }
3014
+ /**
3015
+ * Says where a waygate leads (a point of the map by its name, or a place), or shuts it (`null`): what the editor's
3016
+ * "Leads to" field says of a placed one, in code. Any building can be made a waygate so
3017
+ *
3018
+ * @beta
3019
+ */
3020
+ function setWaygate(gate, to) {
3021
+ gate.set(waygate, typeof to === "object" && to ? {
3022
+ x: to.x,
3023
+ z: to.z
3024
+ } : to);
3025
+ }
3026
+ /**
3027
+ * Puts a unit somewhere at once, on the nearest ground it can stand on: what a waygate and a town portal do, for a game's
3028
+ * own rules. Told to everyone (`portalUsed`)
3029
+ *
3030
+ * @beta
3031
+ */
3032
+ function portal(world, entity, to, gate) {
3033
+ const from = {
3034
+ x: entity.pos.x,
3035
+ z: entity.pos.z
3036
+ };
3037
+ const spot = (entity.flying ? world.air : world.nav).closestPoint(to, 6) ?? to;
3038
+ halt(world, entity);
3039
+ world.place(entity, spot);
3040
+ entity.prevPos.x = spot.x;
3041
+ entity.prevPos.z = spot.z;
3042
+ world.events.emit(portalUsed, {
3043
+ entity: entity.id,
3044
+ from,
3045
+ to: {
3046
+ x: spot.x,
3047
+ z: spot.z
3048
+ },
3049
+ gate: gate?.id
3050
+ });
3051
+ }
3052
+ /** Going through a waygate: walking into it, then out where it leads. Failed when it is shut, or there is no way to it */
3053
+ var enterOrder = {
3054
+ type: "enterWaygate",
3055
+ accepts(world, entity, init) {
3056
+ const gate = world.get(init.target);
3057
+ return !!entity.get(moving) && !!gate && !!waygateTarget(world, gate);
3058
+ },
3059
+ update(world, entity, order) {
3060
+ const gate = world.get(order.init.target);
3061
+ const to = gate && waygateTarget(world, gate);
3062
+ if (!gate || !to) return "failed";
3063
+ if (distanceToEdge(entity.pos, gate) - entity.def.radius <= REACH$1) {
3064
+ portal(world, entity, to, gate);
3065
+ return "done";
3066
+ }
3067
+ const m = entity.get(moving);
3068
+ if (m.status === "moving") return "running";
3069
+ if (m.status === "failed" && order.state.tried === world.tick - 1) return "failed";
3070
+ order.state.tried = world.tick;
3071
+ moveToEntity(world, entity, gate, REACH$1 * .5);
3072
+ return "running";
3073
+ }
3074
+ };
3075
+ /** Offsets around a spot, nearest first: where those a town portal brings stand */
3076
+ var AROUND = (() => {
3077
+ const all = [];
3078
+ for (let dz = -3; dz <= 3; dz++) for (let dx = -3; dx <= 3; dx++) all.push({
3079
+ x: dx * 1.2,
3080
+ z: dz * 1.2
3081
+ });
3082
+ return all.sort((a, b) => a.x * a.x + a.z * a.z - (b.x * b.x + b.z * b.z));
3083
+ })();
3084
+ /**
3085
+ * Takes a unit and as many as `count` of its owner's units within `radius` of it, nearest first, to its side's home
3086
+ * nearest `near` (`homeOf`), spread around it: what the `townPortal` effect does, in code. Gives back who went, none
3087
+ * when the side has no home
3088
+ *
3089
+ * @beta
3090
+ */
3091
+ function townPortal(world, caster, near, radius = 7, count = 12) {
3092
+ const home = homeOf(world, caster.owner, near);
3093
+ if (!home) return [];
3094
+ const going = [caster, ...world.spatial.query(caster.pos.x, caster.pos.z, radius).filter((entity) => entity !== caster && entity.alive && !entity.hidden && entity.owner === caster.owner && !!entity.get(moving) && distanceToEdge(caster.pos, entity) <= radius).map((entity) => ({
3095
+ entity,
3096
+ d: distSq(entity.pos, caster.pos)
3097
+ })).sort((a, b) => a.d - b.d || a.entity.id - b.entity.id).slice(0, Math.max(0, count)).map(({ entity }) => entity)];
3098
+ going.forEach((entity, i) => {
3099
+ const offset = AROUND[i % AROUND.length];
3100
+ portal(world, entity, {
3101
+ x: home.x + offset.x,
3102
+ z: home.z + offset.z
3103
+ });
3104
+ });
3105
+ return going;
3106
+ }
3107
+ var townPortalEffect = defineEffect("townPortal", (context, effect) => {
3108
+ const { world, caster, point, level } = context;
3109
+ townPortal(world, caster, point, at(effect.radius, level, 7), Math.round(at(effect.count, level, 12)));
3110
+ }, { fields: ["count"] });
3111
+ /**
3112
+ * Portals, as in classic RTS games: waygates (`UnitTypeDef.waygate`), which a unit told to (a right-click, as any building is
3113
+ * walked up to) walks into and comes out of where the gate leads (the "Leads to" field of a placed one, a point of the
3114
+ * map; `setWaygate` in code), and the `townPortal` effect (`EffectTypes.townPortal`), which takes the caster and its
3115
+ * units around it to its side's home nearest where it was aimed (`homeOf`: the map's `home.<team>` point, or the
3116
+ * nearest of its buildings). `portal` and `townPortal` do them in code
3117
+ *
3118
+ * @beta
3119
+ */
3120
+ function withPortals() {
3121
+ const intoGate = defineHook({
3122
+ hook: "beforeOrder",
3123
+ id: "portals",
3124
+ run(world, event) {
3125
+ const { order, entity } = event;
3126
+ if (order.type !== "move" || order.init.target === void 0) return;
3127
+ const gate = world.get(order.init.target);
3128
+ if (!gate?.def.waygate || !waygateTarget(world, gate) || !entity.get(moving)) return;
3129
+ event.rejected = true;
3130
+ giveOrder(world, entity, createOrder(world, {
3131
+ type: "enterWaygate",
3132
+ target: gate.id
3133
+ }, order.groupId), event.queued);
3134
+ }
3135
+ });
3136
+ return {
3137
+ id: PORTALS,
3138
+ orders: [enterOrder],
3139
+ components: [waygate],
3140
+ events: [portalUsed],
3141
+ effects: [townPortalEffect],
3142
+ hooks: [intoGate]
3143
+ };
3144
+ }
3145
+ //#endregion
3146
+ //#region src/runes.ts
3147
+ /**
3148
+ * The runes pack's id
3149
+ *
3150
+ * @beta
3151
+ */
3152
+ var RUNES = "core.runes";
3153
+ /**
3154
+ * Uses an item on the ground as a rune is used: whoever picked it casts its ability (where it stands, no mana), and it
3155
+ * is gone from the ground, told as an item picked (`itemPicked`). What the runes pack does as a unit walks over one;
3156
+ * false for an item not on the ground
3157
+ *
3158
+ * @beta
3159
+ */
3160
+ function usePowerUp(world, entity, groundId) {
3161
+ const index = world.items.findIndex((item) => item.id === groundId);
3162
+ if (index < 0) return false;
3163
+ const [item] = world.items.splice(index, 1);
3164
+ const instance = createItem(world, item.item, item.charges);
3165
+ instance.id = item.id;
3166
+ world.events.emit(itemPicked, {
3167
+ entity,
3168
+ item: instance
3169
+ });
3170
+ const ability = world.registry.item(item.item).ability;
3171
+ if (ability) fire(world, entity, world.registry.ability(ability), void 0, instance);
3172
+ return true;
3173
+ }
3174
+ /**
3175
+ * Runes, as in classic RTS games: an item that is a power-up (`ItemDef.powerUp`, `RunesOptions.powerUp` in code) is used as
3176
+ * it is picked up, by a unit that walks over it (`RunesOptions.takes`) or is told to pick it up, and never carried:
3177
+ * whoever picked it casts its ability where it stands. A rune of healing is an item whose ability heals around; a tome
3178
+ * one whose ability has the tomes pack's `tome` effect. `usePowerUp` does it in code
3179
+ *
3180
+ * @beta
3181
+ */
3182
+ function withRunes(options = {}) {
3183
+ const powerUp = options.powerUp ?? ((item) => !!item.powerUp);
3184
+ const takes = options.takes ?? ((world, entity) => entity.isUnit && !entity.hidden && !world.player(entity.owner).neutral && !entity.def.invulnerable);
3185
+ const walkedOver = {
3186
+ name: "runes",
3187
+ phase: "cleanup",
3188
+ every: Math.max(1, Math.round(options.every ?? 3)),
3189
+ update(world) {
3190
+ if (!world.items.length) return;
3191
+ for (const item of [...world.items]) {
3192
+ if (!powerUp(world.registry.item(item.item))) continue;
3193
+ for (const entity of world.spatial.query(item.x, item.z, REACH)) {
3194
+ if (!entity.alive || distanceToEdge(item, entity) > 1.2 || !takes(world, entity, item)) continue;
3195
+ usePowerUp(world, entity, item.id);
3196
+ break;
3197
+ }
3198
+ }
3199
+ }
3200
+ };
3201
+ const ordered = defineHook({
3202
+ hook: "beforePickUp",
3203
+ id: "runes",
3204
+ run(world, event) {
3205
+ if (!powerUp(world.registry.item(event.item))) return;
3206
+ event.cancelled = true;
3207
+ usePowerUp(world, event.entity, event.ground);
3208
+ }
3209
+ });
3210
+ return {
3211
+ id: RUNES,
3212
+ options,
3213
+ systems: [walkedOver],
3214
+ hooks: [ordered]
3215
+ };
3216
+ }
3217
+ //#endregion
3218
+ //#region src/tomes.ts
3219
+ /**
3220
+ * The tomes pack's id
3221
+ *
3222
+ * @beta
3223
+ */
3224
+ var TOMES = "core.tomes";
3225
+ /**
3226
+ * What a unit gained for good from tomes or the game's own rules (`gainStats`): saved with it
3227
+ *
3228
+ * @beta
3229
+ */
3230
+ var gainedStats = defineComponent("core.gainedStats");
3231
+ /**
3232
+ * Gives a unit stats for good, as a tome does: added to what it gained before, kept with it in a save, its health and
3233
+ * mana raised with them. For the tomes pack's `tome` effect and a game's own rules (a shrine that makes a unit
3234
+ * stronger); it works in a game with the tomes pack (`withTomes`), which adds what was gained to the stats
3235
+ *
3236
+ * @beta
3237
+ */
3238
+ function gainStats(world, entity, change) {
3239
+ const had = entity.get(gainedStats);
3240
+ const next = {
3241
+ stats: { ...had?.stats },
3242
+ percent: { ...had?.percent },
3243
+ times: (had?.times ?? 0) + 1
3244
+ };
3245
+ for (const key in change.stats) next.stats[key] = (next.stats[key] ?? 0) + change.stats[key];
3246
+ for (const key in change.percent) next.percent[key] = (next.percent[key] ?? 0) + change.percent[key];
3247
+ entity.set(gainedStats, next);
3248
+ invalidateStats(world, entity);
3249
+ refreshVitals(world, entity);
3250
+ }
3251
+ /** A tome read: what it gives for good, the experience and the levels it gives a hero */
3252
+ var tomeEffect = defineEffect("tome", (context, effect) => {
3253
+ const { world } = context;
3254
+ for (const entity of effectTargets(context, effect, "allies")) {
3255
+ if (effect.gain) gainStats(world, entity, effect.gain);
3256
+ if (effect.xp) grantXp(world, entity, effect.xp);
3257
+ for (let i = 0; i < (effect.levels ?? 0); i++) {
3258
+ const hero = heroOf(entity);
3259
+ if (!hero || hero.level >= maxLevelOf(world, entity)) break;
3260
+ grantXp(world, entity, world.rules.heroes.xpForLevel(hero.level + 1) - hero.xp);
3261
+ }
3262
+ }
3263
+ }, { fields: [{
3264
+ path: "xp",
3265
+ kind: "integer",
3266
+ min: 0,
3267
+ hint: "Experience a hero gains"
3268
+ }, {
3269
+ path: "levels",
3270
+ kind: "integer",
3271
+ min: 0,
3272
+ hint: "Levels a hero gains"
3273
+ }] });
3274
+ /** What was gained, added to the stats: worked out again each time something is gained */
3275
+ var gained = {
3276
+ id: "tomes",
3277
+ stamp: (entity) => entity.get(gainedStats)?.times ?? 0,
3278
+ add(_world, entity, sum) {
3279
+ const had = entity.get(gainedStats);
3280
+ if (had) sum.add(had);
3281
+ }
3282
+ };
3283
+ /**
3284
+ * Tomes, as in classic RTS games: the `tome` effect (`EffectTypes.tome`) gives what it reaches stats for good (`gain`: a
3285
+ * point of strength with the attributes pack), a hero experience (`xp`) and levels (`levels`). A tome is an item whose
3286
+ * ability has the effect, with a charge, used from the inventory, or as it is picked up with the runes pack
3287
+ * (`withRunes`). What a unit gained is kept with it and in a save (`gainedStats`), and a game gives it from its own
3288
+ * rules with `gainStats`
3289
+ *
3290
+ * @beta
3291
+ */
3292
+ function withTomes() {
3293
+ return {
3294
+ id: TOMES,
3295
+ components: [gainedStats],
3296
+ effects: [tomeEffect],
3297
+ statParts: [gained]
3298
+ };
3299
+ }
3300
+ //#endregion
3301
+ //#region src/walls.ts
3302
+ /**
3303
+ * A piece of wall took another shape, or turned: a piece came or went beside it
3304
+ *
3305
+ * @beta
3306
+ */
3307
+ var wallJoined = defineEvent("core.wallJoined", { replicate: "everyone" });
3308
+ /** The four sides, as the cell beside and the facing toward it */
3309
+ var SIDES = [
3310
+ {
3311
+ dx: 1,
3312
+ dz: 0,
3313
+ facing: 0
3314
+ },
3315
+ {
3316
+ dx: 0,
3317
+ dz: 1,
3318
+ facing: Math.PI / 2
3319
+ },
3320
+ {
3321
+ dx: -1,
3322
+ dz: 0,
3323
+ facing: Math.PI
3324
+ },
3325
+ {
3326
+ dx: 0,
3327
+ dz: -1,
3328
+ facing: -Math.PI / 2
3329
+ }
3330
+ ];
3331
+ var near = [];
3332
+ /** The live piece of a family standing on the cell of this point: by its footprint, a gate wider than a cell too */
3333
+ function pieceAt(world, family, x, z) {
3334
+ const cx = Math.floor(x) + .5;
3335
+ const cz = Math.floor(z) + .5;
3336
+ for (const other of world.spatial.query(cx, cz, 2, near)) {
3337
+ if (!other.alive || other.def.wall?.family !== family) continue;
3338
+ const rect = other.rect;
3339
+ if (Math.abs(cx - rect.x) < rect.hw && Math.abs(cz - rect.z) < rect.hd) return other;
3340
+ }
3341
+ }
3342
+ /** The cell just past a piece's side: the next cell for a piece of one, past its footprint for a wider one (a gate) */
3343
+ var besideOf = (rect, dx, dz) => ({
3344
+ x: rect.x + dx * (rect.hw + .5),
3345
+ z: rect.z + dz * (rect.hd + .5)
3346
+ });
3347
+ /**
3348
+ * The shape of a piece by the sides it has a piece of its family on (east, north, west, south), and which way it faces:
3349
+ * an end toward its one neighbour, a straight piece along its line, a corner toward the first of its two sides, a tee
3350
+ * toward the side it has none on
3351
+ *
3352
+ * @internal
3353
+ */
3354
+ function wallShapeOf(sides) {
3355
+ const count = sides.filter(Boolean).length;
3356
+ if (count === 0) return {
3357
+ shape: "single",
3358
+ facing: 0
3359
+ };
3360
+ if (count === 4) return {
3361
+ shape: "cross",
3362
+ facing: 0
3363
+ };
3364
+ if (count === 1) return {
3365
+ shape: "end",
3366
+ facing: SIDES[sides.indexOf(true)].facing
3367
+ };
3368
+ if (count === 3) return {
3369
+ shape: "tee",
3370
+ facing: SIDES[(sides.indexOf(false) + 2) % 4].facing
3371
+ };
3372
+ if (sides[0] === sides[2]) return {
3373
+ shape: "straight",
3374
+ facing: sides[0] ? 0 : Math.PI / 2
3375
+ };
3376
+ return {
3377
+ shape: "corner",
3378
+ facing: SIDES[sides[0] && sides[1] ? 0 : sides[1] && sides[2] ? 1 : sides[2] && sides[3] ? 2 : 3].facing
3379
+ };
3380
+ }
3381
+ /** Works out a piece's shape from its neighbours again, and says so where it changed */
3382
+ function reshape(world, piece) {
3383
+ const family = piece.def.wall?.family;
3384
+ if (!family || !piece.alive) return;
3385
+ const rect = piece.rect;
3386
+ const { shape, facing } = wallShapeOf(SIDES.map(({ dx, dz }) => {
3387
+ const beside = besideOf(rect, dx, dz);
3388
+ return !!pieceAt(world, family, beside.x, beside.z);
3389
+ }));
3390
+ const look = piece.def.wall?.gate ? piece.look : shape;
3391
+ if (piece.look === look && piece.facing === facing) return;
3392
+ piece.look = look;
3393
+ piece.facing = facing;
3394
+ world.events.emit(wallJoined, {
3395
+ entity: piece.id,
3396
+ shape,
3397
+ facing
3398
+ });
3399
+ }
3400
+ /** A piece and the pieces of its family beside it, shaped again: one came, or went */
3401
+ function reshapeAround(world, rect, family, self) {
3402
+ if (self) reshape(world, self);
3403
+ for (const { dx, dz } of SIDES) {
3404
+ const beside = besideOf(rect, dx, dz);
3405
+ const other = pieceAt(world, family, beside.x, beside.z);
3406
+ if (other && other !== self) reshape(world, other);
3407
+ }
3408
+ }
3409
+ /**
3410
+ * The cells of a wall along a drag, as the genre lays one: along the longer of its two directions first, then the
3411
+ * other, a cell's middle each (`x + 0.5`), from where it started to where it ended, each once
3412
+ *
3413
+ * @beta
3414
+ */
3415
+ function wallLine(from, to) {
3416
+ const x0 = Math.floor(from.x);
3417
+ const z0 = Math.floor(from.z);
3418
+ const x1 = Math.floor(to.x);
3419
+ const z1 = Math.floor(to.z);
3420
+ const cells = [];
3421
+ const stepX = Math.sign(x1 - x0);
3422
+ const stepZ = Math.sign(z1 - z0);
3423
+ const alongX = Math.abs(x1 - x0) >= Math.abs(z1 - z0);
3424
+ let x = x0;
3425
+ let z = z0;
3426
+ cells.push({
3427
+ x: x + .5,
3428
+ z: z + .5
3429
+ });
3430
+ const walk = (axis) => {
3431
+ if (axis === "x") while (x !== x1) {
3432
+ x += stepX;
3433
+ cells.push({
3434
+ x: x + .5,
3435
+ z: z + .5
3436
+ });
3437
+ }
3438
+ else while (z !== z1) {
3439
+ z += stepZ;
3440
+ cells.push({
3441
+ x: x + .5,
3442
+ z: z + .5
3443
+ });
3444
+ }
3445
+ };
3446
+ walk(alongX ? "x" : "z");
3447
+ walk(alongX ? "z" : "x");
3448
+ return cells;
3449
+ }
3450
+ /**
3451
+ * Lays a wall along a drag with the player's builders: a build order for each cell of the line (`wallLine`), queued
3452
+ * after one another (after what they were doing, with `queue`), for the first builder of theirs among the ids that can
3453
+ * build the type. What cannot be built where it is (a unit, a tree in the way) is refused there as one built by hand is
3454
+ *
3455
+ * @beta
3456
+ */
3457
+ var buildLineCommand = defineCommand("buildLine", {
3458
+ entities: ids,
3459
+ unitType: string,
3460
+ from: point,
3461
+ to: point,
3462
+ queue: optional(bool)
3463
+ }, (world, player, command) => {
3464
+ if (!world.registry.units.has(command.unitType)) return;
3465
+ const builder = command.entities.map((id) => world.get(id)).find((entity) => entity?.alive && entity.owner === player && entity.def.builds?.includes(command.unitType));
3466
+ if (!builder) return;
3467
+ wallLine(command.from, command.to).forEach((cell, index) => {
3468
+ giveOrder(world, builder, createOrder(world, {
3469
+ type: "build",
3470
+ unitType: command.unitType,
3471
+ point: cell
3472
+ }), index > 0 || !!command.queue);
3473
+ });
3474
+ });
3475
+ /**
3476
+ * A gate of a wall (`UnitTypeDef.wall.gate`): whether it stands open, every screen told. Open, it is in nobody's way,
3477
+ * whoever walks; shut, it is a piece of the wall. Its owner opens and shuts it (`wallGateCommand`, `setWallGate`)
3478
+ *
3479
+ * @beta
3480
+ */
3481
+ var wallGate = defineComponent("walls.gate", {
3482
+ replicate: "everyone",
3483
+ onSpawn(entity) {
3484
+ if (!entity.def.wall?.gate) return;
3485
+ entity.set(wallGate, { open: false });
3486
+ entity.look = "shut";
3487
+ },
3488
+ passable: (gate) => gate.open
3489
+ });
3490
+ /**
3491
+ * A gate of a wall opened or shut (`setWallGate`)
3492
+ *
3493
+ * @beta
3494
+ */
3495
+ var wallGateChanged = defineEvent("core.wallGateChanged", { replicate: "everyone" });
3496
+ /**
3497
+ * Opens or shuts a gate of a wall, for everyone: open, what walks goes through its cell, whoever's; shut, around it.
3498
+ * Its look is `open` or `shut`. False for what is no gate, or one still going up
3499
+ *
3500
+ * @beta
3501
+ */
3502
+ function setWallGate(world, gate, open) {
3503
+ const now = gate.get(wallGate);
3504
+ if (!now || !gate.alive || gate.has(constructionSite)) return false;
3505
+ if (now.open === open) return true;
3506
+ gate.set(wallGate, { open });
3507
+ gate.look = open ? "open" : "shut";
3508
+ if (open) world.nav.removeObstacle(gate.id);
3509
+ else if (!world.nav.addObstacle(gate.id, gate.rect)) world.log.warn("navigation", `There is no room for another obstacle: the gate ${gate.id} does not block the way`, { entity: gate.id });
3510
+ world.events.emit(wallGateChanged, {
3511
+ entity: gate.id,
3512
+ open
3513
+ });
3514
+ return true;
3515
+ }
3516
+ /**
3517
+ * Its owner opens or shuts a gate of a wall (`setWallGate`), for everyone
3518
+ *
3519
+ * @beta
3520
+ */
3521
+ var wallGateCommand = defineCommand("wallGate", {
3522
+ entity: id,
3523
+ open: bool
3524
+ }, (world, player, command) => {
3525
+ const gate = world.get(command.entity);
3526
+ if (gate?.owner === player) setWallGate(world, gate, command.open);
3527
+ });
3528
+ /**
3529
+ * Walls for a game: its pieces (types with `wall: { family }`) join with their neighbours as they are put down and
3530
+ * taken away, and a builder lays a line of them (`buildLineCommand`). A game's pieces draw by their shape with
3531
+ * `UnitLook.looks` (`single`, `end`, `straight`, `corner`, `tee`, `cross`); its gates (`wall.gate`) by `open` and `shut`,
3532
+ * opened and shut by their owner (`wallGateCommand`)
3533
+ *
3534
+ * @beta
3535
+ */
3536
+ function withWalls() {
3537
+ const spawnHook = defineHook({
3538
+ hook: "spawn",
3539
+ id: "core.walls.spawn",
3540
+ run(world, entity) {
3541
+ const family = entity.def.wall?.family;
3542
+ if (family) reshapeAround(world, entity.rect, family, entity);
3543
+ }
3544
+ });
3545
+ const removeHook = defineHook({
3546
+ hook: "remove",
3547
+ id: "core.walls.remove",
3548
+ run(world, entity) {
3549
+ const family = entity.def.wall?.family;
3550
+ if (family) reshapeAround(world, entity.rect, family);
3551
+ }
3552
+ });
3553
+ return {
3554
+ id: "walls",
3555
+ commands: [buildLineCommand, wallGateCommand],
3556
+ components: [wallGate],
3557
+ events: [wallJoined, wallGateChanged],
3558
+ hooks: [spawnHook, removeHook]
3559
+ };
3560
+ }
3561
+ //#endregion
3562
+ //#region src/mapMeasure.ts
3563
+ function walking(map, registry) {
3564
+ const { width, depth, levels, ramps } = map;
3565
+ const { open } = mapCells(map, registry, mapTerrain(map), true);
3566
+ const cellAt = (point) => {
3567
+ const cx = Math.floor(point.x);
3568
+ const cz = Math.floor(point.z);
3569
+ for (let r = 0; r <= 6; r++) for (let z = cz - r; z <= cz + r; z++) for (let x = cx - r; x <= cx + r; x++) {
3570
+ if (x < 0 || z < 0 || x >= width || z >= depth || Math.max(Math.abs(x - cx), Math.abs(z - cz)) !== r) continue;
3571
+ if (open[z * width + x]) return z * width + x;
3572
+ }
3573
+ return -1;
3574
+ };
3575
+ return {
3576
+ width,
3577
+ depth,
3578
+ open,
3579
+ passable: (a, b) => levels[a] === levels[b] || ramps[a] > 0 || ramps[b] > 0,
3580
+ cellAt
3581
+ };
3582
+ }
3583
+ /** The way on foot between two cells: A* over the cells, eight ways, corners not cut. Nothing when there is none */
3584
+ function walk(walking, from, to) {
3585
+ const { width, depth, open, passable } = walking;
3586
+ if (from < 0 || to < 0) return void 0;
3587
+ const cells = width * depth;
3588
+ const cost = new Float32Array(cells).fill(Infinity);
3589
+ const came = new Int32Array(cells).fill(-1);
3590
+ const closed = new Uint8Array(cells);
3591
+ const tx = to % width;
3592
+ const tz = Math.floor(to / width);
3593
+ const guess = (c) => {
3594
+ const dx = Math.abs(c % width - tx);
3595
+ const dz = Math.abs(Math.floor(c / width) - tz);
3596
+ return Math.max(dx, dz) + (Math.SQRT2 - 1) * Math.min(dx, dz);
3597
+ };
3598
+ const heap = [];
3599
+ const score = new Float32Array(cells);
3600
+ const push = (c) => {
3601
+ heap.push(c);
3602
+ let i = heap.length - 1;
3603
+ while (i > 0) {
3604
+ const parent = i - 1 >> 1;
3605
+ if (score[heap[parent]] <= score[heap[i]]) break;
3606
+ [heap[parent], heap[i]] = [heap[i], heap[parent]];
3607
+ i = parent;
3608
+ }
3609
+ };
3610
+ const pop = () => {
3611
+ const top = heap[0];
3612
+ const last = heap.pop();
3613
+ if (heap.length) {
3614
+ heap[0] = last;
3615
+ let i = 0;
3616
+ for (;;) {
3617
+ const left = i * 2 + 1;
3618
+ const right = left + 1;
3619
+ let least = i;
3620
+ if (left < heap.length && score[heap[left]] < score[heap[least]]) least = left;
3621
+ if (right < heap.length && score[heap[right]] < score[heap[least]]) least = right;
3622
+ if (least === i) break;
3623
+ [heap[least], heap[i]] = [heap[i], heap[least]];
3624
+ i = least;
3625
+ }
3626
+ }
3627
+ return top;
3628
+ };
3629
+ cost[from] = 0;
3630
+ score[from] = guess(from);
3631
+ push(from);
3632
+ while (heap.length) {
3633
+ const c = pop();
3634
+ if (closed[c]) continue;
3635
+ if (c === to) break;
3636
+ closed[c] = 1;
3637
+ const x = c % width;
3638
+ const z = (c - x) / width;
3639
+ for (let dz = -1; dz <= 1; dz++) for (let dx = -1; dx <= 1; dx++) {
3640
+ if (!dx && !dz) continue;
3641
+ const nx = x + dx;
3642
+ const nz = z + dz;
3643
+ if (nx < 0 || nz < 0 || nx >= width || nz >= depth) continue;
3644
+ const n = nz * width + nx;
3645
+ if (!open[n] || closed[n] || !passable(c, n)) continue;
3646
+ if (dx && dz) {
3647
+ const side1 = z * width + nx;
3648
+ const side2 = nz * width + x;
3649
+ if (!open[side1] || !open[side2] || !passable(c, side1) || !passable(c, side2)) continue;
3650
+ }
3651
+ const next = cost[c] + (dx && dz ? Math.SQRT2 : 1);
3652
+ if (next >= cost[n]) continue;
3653
+ cost[n] = next;
3654
+ came[n] = c;
3655
+ score[n] = next + guess(n);
3656
+ push(n);
3657
+ }
3658
+ }
3659
+ if (!Number.isFinite(cost[to])) return void 0;
3660
+ const points = [];
3661
+ for (let c = to; c >= 0; c = came[c]) points.push({
3662
+ x: c % width + .5,
3663
+ z: Math.floor(c / width) + .5
3664
+ });
3665
+ return {
3666
+ length: cost[to],
3667
+ points: points.reverse()
3668
+ };
3669
+ }
3670
+ /**
3671
+ * The way on foot between two points of a map, with what the checks count as in the way. Nothing when there is none
3672
+ *
3673
+ * @beta
3674
+ */
3675
+ function walkBetween(map, registry, from, to) {
3676
+ const cells = walking(map, registry);
3677
+ return walk(cells, cells.cellAt(from), cells.cellAt(to));
3678
+ }
3679
+ /**
3680
+ * How many cells each open cell is from what is not: half the width of the way there. A passage two cells wide has
3681
+ * cells 1 from its sides in its middle
3682
+ */
3683
+ function clearance(walking) {
3684
+ const { width, depth, open } = walking;
3685
+ const far = new Float32Array(width * depth);
3686
+ for (let c = 0; c < far.length; c++) far[c] = open[c] ? Infinity : 0;
3687
+ const step = (c, x, z, dx, dz, weight) => {
3688
+ const nx = x + dx;
3689
+ const nz = z + dz;
3690
+ const next = nx < 0 || nz < 0 || nx >= width || nz >= depth ? 0 : far[nz * width + nx];
3691
+ if (next + weight < far[c]) far[c] = next + weight;
3692
+ };
3693
+ for (let z = 0; z < depth; z++) for (let x = 0; x < width; x++) {
3694
+ const c = z * width + x;
3695
+ if (!far[c]) continue;
3696
+ step(c, x, z, -1, 0, 1);
3697
+ step(c, x, z, 0, -1, 1);
3698
+ step(c, x, z, -1, -1, Math.SQRT2);
3699
+ step(c, x, z, 1, -1, Math.SQRT2);
3700
+ }
3701
+ for (let z = depth - 1; z >= 0; z--) for (let x = width - 1; x >= 0; x--) {
3702
+ const c = z * width + x;
3703
+ if (!far[c]) continue;
3704
+ step(c, x, z, 1, 0, 1);
3705
+ step(c, x, z, 0, 1, 1);
3706
+ step(c, x, z, 1, 1, Math.SQRT2);
3707
+ step(c, x, z, -1, 1, Math.SQRT2);
3708
+ }
3709
+ return far;
3710
+ }
3711
+ /**
3712
+ * How far around a start counts as its ground, for building and trees
3713
+ *
3714
+ * @beta
3715
+ */
3716
+ var BUILD_REACH = 16;
3717
+ /**
3718
+ * How much more one start can have of something than another before it is not fair, as a part
3719
+ *
3720
+ * @internal
3721
+ */
3722
+ var FAIR_MARGIN = .15;
3723
+ /** The ends of a way, in cells, where the bases stand: the narrowest place is looked for between them */
3724
+ var BASE_ROOM = 8;
3725
+ /**
3726
+ * What a map is like to play on: the way on foot between every two starts, the narrowest place on those ways, and for
3727
+ * each start the nearest mine and how much it holds, the ground around it that can be built on, and its trees; side
3728
+ * by side, with what one start has much more of than another said. Then what the game measures of its own
3729
+ *
3730
+ * @beta
3731
+ */
3732
+ function measureMap(map, registry) {
3733
+ const cells = walking(map, registry);
3734
+ const terrain = mapTerrain(map);
3735
+ const grid = new BuildGrid(terrain);
3736
+ const { built } = mapCells(map, registry, terrain);
3737
+ const starts = map.players.filter((player) => player.start);
3738
+ const rushes = [];
3739
+ for (let i = 0; i < starts.length; i++) for (let j = i + 1; j < starts.length; j++) {
3740
+ const way = walk(cells, cells.cellAt(starts[i].start), cells.cellAt(starts[j].start));
3741
+ rushes.push({
3742
+ from: starts[i].id,
3743
+ to: starts[j].id,
3744
+ ...way && {
3745
+ length: Math.round(way.length * 10) / 10,
3746
+ points: way.points
3747
+ }
3748
+ });
3749
+ }
3750
+ const far = clearance(cells);
3751
+ let narrowest;
3752
+ for (const rush of rushes) {
3753
+ const points = rush.points ?? [];
3754
+ for (let i = BASE_ROOM; i < points.length - BASE_ROOM; i++) {
3755
+ const c = Math.floor(points[i].z) * cells.width + Math.floor(points[i].x);
3756
+ const width = Math.round((far[c] * 2 - 1) * 10) / 10;
3757
+ if (!narrowest || width < narrowest.width) narrowest = {
3758
+ width,
3759
+ at: points[i]
3760
+ };
3761
+ }
3762
+ }
3763
+ const mines = map.objects.filter((object) => {
3764
+ if (!registry.units.has(object.type)) return false;
3765
+ const def = registry.unit(object.type);
3766
+ return def.kind === "resource" && footprintOf(def).w >= 2;
3767
+ });
3768
+ const measures = starts.map((player) => {
3769
+ const start = player.start;
3770
+ const from = cells.cellAt(start);
3771
+ let mine;
3772
+ for (const object of mines) {
3773
+ const way = walk(cells, from, cells.cellAt(object));
3774
+ if (!way || mine && way.length >= mine.distance) continue;
3775
+ const def = registry.unit(object.type);
3776
+ mine = {
3777
+ distance: Math.round(way.length * 10) / 10,
3778
+ amount: object.amount ?? def.resource?.amount ?? 0,
3779
+ at: {
3780
+ x: object.x,
3781
+ z: object.z
3782
+ }
3783
+ };
3784
+ }
3785
+ let buildable = 0;
3786
+ for (let z = Math.floor(start.z - 16); z <= Math.ceil(start.z + 16); z++) for (let x = Math.floor(start.x - 16); x <= Math.ceil(start.x + 16); x++) {
3787
+ if (hypot(x + .5 - start.x, z + .5 - start.z) > 16) continue;
3788
+ if (grid.isBuildable(x, z) && !built[z * map.width + x]) buildable++;
3789
+ }
3790
+ const trees = map.objects.filter((object) => {
3791
+ if (!registry.units.has(object.type) || hypot(object.x - start.x, object.z - start.z) > 16) return false;
3792
+ const def = registry.unit(object.type);
3793
+ return def.kind === "resource" && footprintOf(def).w < 2;
3794
+ }).length;
3795
+ return {
3796
+ player: player.id,
3797
+ name: player.name,
3798
+ ...mine && { mine },
3799
+ buildable,
3800
+ trees
3801
+ };
3802
+ });
3803
+ const unfair = [];
3804
+ const compare = (what, value, more = "more") => {
3805
+ const known = measures.map((start) => ({
3806
+ start,
3807
+ value: value(start)
3808
+ })).filter((entry) => entry.value !== void 0);
3809
+ if (known.length < 2) return;
3810
+ const most = known.reduce((a, b) => b.value > a.value ? b : a);
3811
+ const least = known.reduce((a, b) => b.value < a.value ? b : a);
3812
+ if (most.value > least.value * 1.15 && most.value - least.value > 1) unfair.push(`${most.start.name} has ${more} ${what} than ${least.start.name}: ${most.value} against ${least.value}`);
3813
+ };
3814
+ compare("way to their nearest mine", (start) => start.mine?.distance, "a longer");
3815
+ compare("gold in their nearest mine", (start) => start.mine?.amount);
3816
+ compare("ground to build on", (start) => start.buildable);
3817
+ compare("trees near their start", (start) => start.trees);
3818
+ return {
3819
+ rushes: rushes.map(({ points: _, ...rush }) => rush),
3820
+ ...narrowest && { narrowest },
3821
+ starts: measures,
3822
+ unfair,
3823
+ game: registry.module?.editor?.measure?.(map, registry) ?? []
3824
+ };
3825
+ }
3826
+ /**
3827
+ * Seconds a unit type takes over a way on foot, at the speed it starts with
3828
+ *
3829
+ * @beta
3830
+ */
3831
+ var walkingTime = (registry, type, length) => {
3832
+ if (!registry.units.has(type)) return void 0;
3833
+ const speed = registry.unit(type).stats.moveSpeed ?? 0;
3834
+ return speed > 0 ? Math.round(length / speed * 10) / 10 : void 0;
3835
+ };
3836
+ //#endregion
3837
+ //#region src/scenario.ts
3838
+ /**
3839
+ * The format scenarios are written in now
3840
+ *
3841
+ * @beta
3842
+ */
3843
+ var SCENARIO_FORMAT = 1;
3844
+ /**
3845
+ * A scenario file as it is now, checked. Throws on what is wrong with it
3846
+ *
3847
+ * @beta
3848
+ */
3849
+ function readScenario(data) {
3850
+ if (typeof data !== "object" || data === null || Array.isArray(data)) throw new Error("A scenario is an object");
3851
+ const file = data;
3852
+ if (typeof file.format !== "number" || file.format > 1) throw new Error(`A scenario of format ${file.format}, which this engine does not know`);
3853
+ if (typeof file.name !== "string" || !file.name) throw new Error("A scenario has a name");
3854
+ if (typeof file.game !== "string" || !file.game) throw new Error(`The scenario ${file.name} does not say which game it is of`);
3855
+ if (file.kind === "map") parseMap(file.map);
3856
+ else if (file.kind === "save") {
3857
+ if (typeof file.save !== "string") throw new Error(`The scenario ${file.name} has no save in it`);
3858
+ } else throw new Error(`The scenario ${file.name} is neither a map nor a save`);
3859
+ return {
3860
+ ...file,
3861
+ format: 1
3862
+ };
3863
+ }
3864
+ /**
3865
+ * A map made for a test, as a scenario
3866
+ *
3867
+ * @beta
3868
+ */
3869
+ function scenarioOfMap(map, name, game, description) {
3870
+ return {
3871
+ format: 1,
3872
+ name,
3873
+ game,
3874
+ ...description && { description },
3875
+ kind: "map",
3876
+ map: serializeMap(map)
3877
+ };
3878
+ }
3879
+ /**
3880
+ * A world at the moment that matters, as a scenario
3881
+ *
3882
+ * @beta
3883
+ */
3884
+ function scenarioOfWorld(world, name, description) {
3885
+ return {
3886
+ format: 1,
3887
+ name,
3888
+ game: world.registry.game,
3889
+ ...description && { description },
3890
+ kind: "save",
3891
+ save: encodeSave(portableSave(world.registry, saveWorld(world)))
3892
+ };
3893
+ }
3894
+ /** Only what is the game's: what an app keeps of its own in the world (`Component.portable` false) left out */
3895
+ function portableSave(registry, save) {
3896
+ const keep = (values) => values && Object.fromEntries(Object.entries(values).filter(([id]) => registry.component(id)?.portable !== false));
3897
+ return {
3898
+ ...save,
3899
+ state: keep(save.state),
3900
+ players: save.players.map((player) => ({
3901
+ ...player,
3902
+ state: keep(player.state)
3903
+ })),
3904
+ entities: save.entities.map((entity) => entity.components ? {
3905
+ ...entity,
3906
+ components: keep(entity.components)
3907
+ } : entity),
3908
+ detached: save.detached.map((entity) => entity.components ? {
3909
+ ...entity,
3910
+ components: keep(entity.components)
3911
+ } : entity)
3912
+ };
3913
+ }
3914
+ /**
3915
+ * The world of a scenario, with the game's module: a map made into a world as a game would make it (with the seed 1,
3916
+ * so that it is the same every time), or a saved world loaded as it was
3917
+ *
3918
+ * @beta
3919
+ */
3920
+ async function scenarioWorld(scenario, game) {
3921
+ if (scenario.game !== game.id) throw new Error(`The scenario ${scenario.name} is of the game "${scenario.game}", not "${game.id}"`);
3922
+ if (scenario.kind === "map") return createWorldFromMap({
3923
+ map: parseMap(scenario.map),
3924
+ game,
3925
+ seed: 1
3926
+ });
3927
+ return loadWorld(decodeSave(scenario.save), { game });
3928
+ }
3929
+ //#endregion
3930
+ //#region src/packs/heroes/carry.ts
3931
+ /**
3932
+ * A hero of a world as a record, to carry into another (see `applyHeroRecord`). Throws for what is no hero
3933
+ *
3934
+ * @beta
3935
+ */
3936
+ function heroRecord(world, hero) {
3937
+ const state = heroOf(hero);
3938
+ if (!state) throw new Error(`${hero.def.id} is no hero to carry over`);
3939
+ const name = world.nameOf(hero);
3940
+ return {
3941
+ type: hero.def.id,
3942
+ ...name !== void 0 && { name },
3943
+ level: state.level,
3944
+ xp: state.xp,
3945
+ points: state.points,
3946
+ abilities: Object.fromEntries(hero.abilities.filter((ability) => ability.level > 0).map((ability) => [ability.id, ability.level])),
3947
+ items: (slotsOf(hero) ?? []).map((slot) => slot ? {
3948
+ item: slot.item,
3949
+ charges: slot.charges,
3950
+ ...slot.data && { data: structuredClone(slot.data) }
3951
+ } : null)
3952
+ };
3953
+ }
3954
+ /**
3955
+ * Makes a hero of a world what a record says it was: its level, experience and points, its abilities as far as they
3956
+ * were learned, and its items in their slots. In place of what it had. What the game no longer has, an ability or an
3957
+ * item, is left out. Whole: health and mana full, nothing cooling down
3958
+ *
3959
+ * @beta
3960
+ */
3961
+ function applyHeroRecord(world, hero, record) {
3962
+ const state = heroOf(hero);
3963
+ if (!state) throw new Error(`${hero.def.id} is no hero to carry a hero into`);
3964
+ state.level = Math.max(1, Math.min(Math.floor(record.level), maxLevelOf(world, hero)));
3965
+ state.xp = Math.max(record.xp, world.rules.heroes.xpForLevel(state.level));
3966
+ state.points = Math.max(Math.floor(record.points), 0);
3967
+ for (const ability of hero.abilities) {
3968
+ const level = record.abilities[ability.id] ?? 0;
3969
+ const max = world.registry.abilities.has(ability.id) ? world.registry.ability(ability.id).maxLevel ?? 3 : 0;
3970
+ ability.level = Math.min(level, max);
3971
+ readyAgain(ability);
3972
+ ability.autocast = ability.level > 0 && (world.registry.ability(ability.id).autocast ?? false);
3973
+ }
3974
+ const slots = slotsOf(hero);
3975
+ if (slots) {
3976
+ slots.fill(null);
3977
+ record.items.forEach((carried, slot) => {
3978
+ if (!carried || slot >= slots.length || !world.registry.items.has(carried.item)) return;
3979
+ const instance = createItem(world, carried.item, carried.charges);
3980
+ if (carried.data) instance.data = structuredClone(carried.data);
3981
+ slots[slot] = instance;
3982
+ });
3983
+ world.events.emit(inventoryChanged, { entity: hero });
3984
+ }
3985
+ refreshVitals(world, hero);
3986
+ hero.hp = hero.maxHp;
3987
+ hero.mana = hero.maxMana;
3988
+ }
3989
+ /**
3990
+ * A hero made from a record (see `heroRecord`) for a player at a point, under the name it had unless told another
3991
+ *
3992
+ * @beta
3993
+ */
3994
+ function heroFromRecord(world, record, owner, at, options = {}) {
3995
+ const name = options.name ?? record.name;
3996
+ const hero = world.spawn(record.type, owner, at, {
3997
+ ...options,
3998
+ ...name !== void 0 && { name }
3999
+ });
4000
+ applyHeroRecord(world, hero, record);
4001
+ return hero;
4002
+ }
4003
+ /**
4004
+ * A record read back from what was kept (a campaign's progress, a file), checked. Throws on what is wrong
4005
+ *
4006
+ * @beta
4007
+ */
4008
+ function readHeroRecord(data) {
4009
+ const record = data;
4010
+ if (typeof record !== "object" || record === null) throw new Error("A hero record is an object");
4011
+ if (typeof record.type !== "string" || !record.type) throw new Error("A hero record says what type of unit it is");
4012
+ const number = (value, what) => {
4013
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`A hero record's ${what} is a number`);
4014
+ return value;
4015
+ };
4016
+ const abilities = record.abilities ?? {};
4017
+ if (typeof abilities !== "object" || Array.isArray(abilities)) throw new Error("A hero record's abilities are levels by id");
4018
+ return {
4019
+ type: record.type,
4020
+ ...typeof record.name === "string" && { name: record.name },
4021
+ level: number(record.level, "level"),
4022
+ xp: number(record.xp ?? 0, "experience"),
4023
+ points: number(record.points ?? 0, "points"),
4024
+ abilities: Object.fromEntries(Object.entries(abilities).map(([id, level]) => [id, number(level, `level of ${id}`)])),
4025
+ items: (Array.isArray(record.items) ? record.items : []).map((slot) => slot && typeof slot === "object" && typeof slot.item === "string" ? {
4026
+ item: slot.item,
4027
+ charges: number(slot.charges ?? 0, "charges"),
4028
+ ...slot.data && { data: slot.data }
4029
+ } : null)
4030
+ };
4031
+ }
4032
+ //#endregion
4033
+ //#region src/packs/heroes/campaign.ts
4034
+ /**
4035
+ * The format campaign progress is written in now
4036
+ *
4037
+ * @internal
4038
+ */
4039
+ var CAMPAIGN_FORMAT = 1;
4040
+ /**
4041
+ * A campaign as it is before anything is played: at its first mission, with nothing carried
4042
+ *
4043
+ * @beta
4044
+ */
4045
+ function newCampaignProgress(campaign, first) {
4046
+ return {
4047
+ format: 1,
4048
+ campaign,
4049
+ at: first,
4050
+ done: {},
4051
+ heroes: [],
4052
+ data: {}
4053
+ };
4054
+ }
4055
+ /**
4056
+ * Progress as it was kept, checked and brought up to date. Throws on what is wrong
4057
+ *
4058
+ * @beta
4059
+ */
4060
+ function readCampaignProgress(data) {
4061
+ const kept = data;
4062
+ if (typeof kept !== "object" || kept === null) throw new Error("A campaign's progress is an object");
4063
+ if (typeof kept.format !== "number" || kept.format > 1) throw new Error(`Campaign progress of format ${kept.format}, which this engine does not know`);
4064
+ if (typeof kept.campaign !== "string" || typeof kept.at !== "string") throw new Error("A campaign's progress says which campaign it is and where it is at");
4065
+ const done = {};
4066
+ for (const [mission, result] of Object.entries(kept.done ?? {})) {
4067
+ const { seconds, best, finished } = result ?? {};
4068
+ if (typeof seconds !== "number" || !Number.isFinite(seconds)) throw new Error(`How long ${mission} took is a number`);
4069
+ done[mission] = {
4070
+ seconds,
4071
+ best: typeof best === "number" ? best : seconds,
4072
+ finished: typeof finished === "number" ? finished : 0
4073
+ };
4074
+ }
4075
+ return {
4076
+ format: 1,
4077
+ campaign: kept.campaign,
4078
+ at: kept.at,
4079
+ ...typeof kept.difficulty === "string" && { difficulty: kept.difficulty },
4080
+ done,
4081
+ heroes: (kept.heroes ?? []).map(readHeroRecord),
4082
+ data: typeof kept.data === "object" && kept.data !== null ? kept.data : {}
4083
+ };
4084
+ }
4085
+ /**
4086
+ * A mission won: how long it took, the best time kept, the heroes it ends with to carry on, the campaign at the
4087
+ * mission after it (`next`), or still at this one when it was the last, and what it carries of the game's own
4088
+ * (`data`: its keys over the ones kept, the rest kept as they were). A new progress: the one given is left as it was
4089
+ *
4090
+ * @beta
4091
+ */
4092
+ function missionWon(progress, mission, seconds, heroes, next, data) {
4093
+ const before = progress.done[mission];
4094
+ return {
4095
+ ...progress,
4096
+ at: next ?? progress.at,
4097
+ done: {
4098
+ ...progress.done,
4099
+ [mission]: {
4100
+ seconds,
4101
+ best: before ? Math.min(before.best, seconds) : seconds,
4102
+ finished: Date.now()
4103
+ }
4104
+ },
4105
+ heroes: structuredClone(heroes),
4106
+ data: data ? {
4107
+ ...progress.data,
4108
+ ...structuredClone(data)
4109
+ } : progress.data
4110
+ };
4111
+ }
4112
+ /**
4113
+ * A player's units as records to carry on (`UnitRecord`): those alive, not structures, not heroes (`carryOut` carries
4114
+ * those), and of them what `filter` keeps (the footmen, the three nearest the gate). In the order of their ids
4115
+ *
4116
+ * @beta
4117
+ */
4118
+ function carryUnits(world, player, filter = () => true) {
4119
+ return [...world.index.ownedBy(player)].filter((unit) => unit.alive && unit.isUnit && !heroOf(unit) && filter(unit)).sort((a, b) => a.id - b.id).map((unit) => {
4120
+ const name = world.nameOf(unit);
4121
+ const rank = unit.get(veterancy);
4122
+ const slots = slotsOf(unit);
4123
+ return {
4124
+ type: unit.def.id,
4125
+ ...name !== void 0 && { name },
4126
+ health: unit.maxHp > 0 ? Math.min(Math.max(unit.hp / unit.maxHp, 0), 1) : 1,
4127
+ ...rank && { veterancy: { ...rank } },
4128
+ ...slots && { items: slots.map((slot) => slot ? {
4129
+ item: slot.item,
4130
+ charges: slot.charges,
4131
+ ...slot.data && { data: structuredClone(slot.data) }
4132
+ } : null) }
4133
+ };
4134
+ });
4135
+ }
4136
+ /**
4137
+ * Brings carried units into a mission for a player, round a point (`spread` apart, in rings: 1.5 when left out), each
4138
+ * with its name, its share of health, its veterancy and its items. What the game no longer has, a type or an item, is
4139
+ * left out. The units made, in the order of the records
4140
+ *
4141
+ * @beta
4142
+ */
4143
+ function placeUnits(world, player, records, at, spread = 1.5) {
4144
+ const made = [];
4145
+ records.filter((record) => world.registry.units.has(record.type)).forEach((record, i) => {
4146
+ const point = ringPoint(at, i, spread);
4147
+ const unit = world.spawn(record.type, player, point, record.name !== void 0 ? { name: record.name } : {});
4148
+ if (record.veterancy) {
4149
+ unit.set(veterancy, { ...record.veterancy });
4150
+ refreshVitals(world, unit);
4151
+ }
4152
+ const slots = slotsOf(unit);
4153
+ if (slots && record.items) {
4154
+ slots.fill(null);
4155
+ record.items.forEach((carried, slot) => {
4156
+ if (!carried || slot >= slots.length || !world.registry.items.has(carried.item)) return;
4157
+ const instance = createItem(world, carried.item, carried.charges);
4158
+ if (carried.data) instance.data = structuredClone(carried.data);
4159
+ slots[slot] = instance;
4160
+ });
4161
+ world.events.emit(inventoryChanged, { entity: unit });
4162
+ }
4163
+ unit.hp = Math.max(1, Math.round(unit.maxHp * Math.min(Math.max(record.health, 0), 1)));
4164
+ made.push(unit);
4165
+ });
4166
+ return made;
4167
+ }
4168
+ /** The `index`-th place round a point: the point first, then rings of six, twelve, … `spread` apart */
4169
+ function ringPoint(at, index, spread) {
4170
+ if (index === 0) return {
4171
+ x: at.x,
4172
+ z: at.z
4173
+ };
4174
+ let ring = 1;
4175
+ let first = 1;
4176
+ while (index >= first + ring * 6) {
4177
+ first += ring * 6;
4178
+ ring++;
4179
+ }
4180
+ const angle = (index - first) / (ring * 6) * Math.PI * 2;
4181
+ return {
4182
+ x: at.x + sin(angle) * ring * spread,
4183
+ z: at.z + cos(angle) * ring * spread
4184
+ };
4185
+ }
4186
+ /**
4187
+ * What a player has researched, to carry on (`giveResearch`): each upgrade's level, by its id
4188
+ *
4189
+ * @beta
4190
+ */
4191
+ function carryResearch(world, player) {
4192
+ return Object.fromEntries([...world.player(player).upgrades].filter(([, level]) => level > 0));
4193
+ }
4194
+ /**
4195
+ * Gives a player research carried in (`carryResearch`): each upgrade up to its level, never down, no further than it
4196
+ * goes, and what it does besides stats (`UpgradeDef.apply`) to the units the player has, as research finishing does.
4197
+ * What the game no longer has is left out
4198
+ *
4199
+ * @beta
4200
+ */
4201
+ function giveResearch(world, player, research) {
4202
+ const who = world.player(player);
4203
+ for (const [upgrade, carried] of Object.entries(research)) {
4204
+ if (!world.registry.upgrades.has(upgrade)) continue;
4205
+ const def = world.registry.upgrade(upgrade);
4206
+ const to = Math.min(Math.max(Math.floor(carried), 0), def.maxLevel ?? 1);
4207
+ for (let level = (who.upgrades.get(upgrade) ?? 0) + 1; level <= to; level++) {
4208
+ who.setUpgrade(upgrade, level);
4209
+ if (def.apply) {
4210
+ for (const entity of [...world.index.ownedBy(player)]) if (entity.alive) def.apply(world, entity, level);
4211
+ }
4212
+ }
4213
+ who.researching.delete(upgrade);
4214
+ }
4215
+ }
4216
+ /**
4217
+ * What a player has of each resource, to carry on (`giveResources`): a copy, all of it, or of the kinds named
4218
+ *
4219
+ * @beta
4220
+ */
4221
+ function carryResources(world, player, kinds) {
4222
+ const resources = world.player(player).resources;
4223
+ return Object.fromEntries(Object.entries(resources).filter(([kind]) => !kinds || kinds.includes(kind)));
4224
+ }
4225
+ /**
4226
+ * Gives a player resources carried in (`carryResources`), on top of what the mission starts it with. What the game no
4227
+ * longer has is left out
4228
+ *
4229
+ * @beta
4230
+ */
4231
+ function giveResources(world, player, resources) {
4232
+ const purse = world.player(player).resources;
4233
+ const known = new Set(world.registry.resources.map((resource) => resource.id));
4234
+ for (const [kind, amount] of Object.entries(resources)) if (known.has(kind) && Number.isFinite(amount)) purse[kind] = (purse[kind] ?? 0) + amount;
4235
+ }
4236
+ /**
4237
+ * The heroes of a player as records to carry on (see `heroRecord`): fallen ones too, as a campaign brings them back
4238
+ *
4239
+ * @beta
4240
+ */
4241
+ function carryOut(world, player) {
4242
+ return heroesOf(world, player).map((hero) => heroRecord(world, hero));
4243
+ }
4244
+ /**
4245
+ * Brings carried heroes into a mission for a player. Each takes the place of the hero the map has for it: the one of
4246
+ * that player under the same name, else one of that player of the same type; a hero the map has none for is made at
4247
+ * `at`. The heroes, in the order of the records
4248
+ *
4249
+ * @beta
4250
+ */
4251
+ function carryIn(world, heroes, player, at) {
4252
+ const taken = /* @__PURE__ */ new Set();
4253
+ return heroes.map((record) => {
4254
+ const named = record.name !== void 0 ? world.named(record.name) : void 0;
4255
+ const placed = named && named.owner === player && heroOf(named) && !taken.has(named) ? named : heroesOf(world, player).find((hero) => hero.def.id === record.type && !taken.has(hero));
4256
+ const hero = placed ?? heroFromRecord(world, record, player, at);
4257
+ if (placed) applyHeroRecord(world, placed, record);
4258
+ taken.add(hero);
4259
+ return hero;
4260
+ });
4261
+ }
4262
+ //#endregion
4263
+ //#region src/session/commandReplay.ts
4264
+ /**
4265
+ * Replays of commands (determinism D4): a game kept as where it started and what its players told it, tick by tick,
4266
+ * and played back by running it again. A world comes out the same from the same start and the same commands (D1–D3),
4267
+ * so this is the whole game, from every side, in a few kilobytes where the frames a watcher was sent are megabytes.
4268
+ * Its hash every few seconds is kept with it: played back, a hash that differs says the game came apart there (rules
4269
+ * changed since, or a system a save does not bring back), and from then it is the game the rules make now
4270
+ */
4271
+ /**
4272
+ * Records a world from now as its start and the commands given to it from outside a tick (the players', by a
4273
+ * session or a server; a computer player's are its system's, which runs again), with its hash every `every` ticks
4274
+ * (300, ten seconds at 30 a second, when left out). Starting takes a save of the world, as saving does (it puts the
4275
+ * crowd back as a load does)
4276
+ *
4277
+ * @beta
4278
+ */
4279
+ function recordCommands(world, options) {
4280
+ const every = options.every ?? 300;
4281
+ const checkpointEvery = options.checkpoints ?? 0;
4282
+ const checkpoints = [];
4283
+ const from = world.tick;
4284
+ const start = encodeSave(saveWorld(world));
4285
+ const commands = [];
4286
+ const saves = [];
4287
+ const hashes = [];
4288
+ const unwatch = world.watch({
4289
+ command: (command) => void commands.push([world.tick, structuredClone(command)]),
4290
+ saved: () => void saves.push(world.tick),
4291
+ stepped: () => {
4292
+ if ((world.tick - from) % every === 0) hashes.push([world.tick, worldFingerprint(world)]);
4293
+ if (checkpointEvery > 0 && (world.tick - from) % checkpointEvery === 0) checkpoints.push([world.tick, encodeSave(saveWorld(world))]);
4294
+ }
4295
+ });
4296
+ let replay;
4297
+ return {
4298
+ get ticks() {
4299
+ return world.tick - from;
4300
+ },
4301
+ stop() {
4302
+ if (replay) return replay;
4303
+ unwatch();
4304
+ replay = {
4305
+ format: 2,
4306
+ kind: "commands",
4307
+ game: world.registry.game,
4308
+ rules: world.registry.hash,
4309
+ player: options.player,
4310
+ tickRate: world.tickRate,
4311
+ from,
4312
+ to: world.tick,
4313
+ start,
4314
+ commands,
4315
+ saves,
4316
+ hashes,
4317
+ ...checkpoints.length && { checkpoints },
4318
+ ...world.debugPlayers.size > 0 && { debug: [...world.debugPlayers] }
4319
+ };
4320
+ return replay;
4321
+ }
4322
+ };
4323
+ }
4324
+ /**
4325
+ * A replay of commands run again, to its end or a tick, with no screen: the world as it is then, the first tick whose
4326
+ * hash differs from the one kept (none when all were the same), and what differed first at the first checkpoint
4327
+ * (`recordCommands`' `checkpoints`) where the world was not the one recorded: the entity and its field
4328
+ * (`entities[id 11].pos.x`), and what each held. A replay with no checkpoints says the tick alone. What a test of a
4329
+ * replay, and a server checking one, use
4330
+ *
4331
+ * @beta
4332
+ */
4333
+ async function runReplay(replay, options) {
4334
+ const run = await replayRun(replay, options.game, options.from, options.components);
4335
+ run.stepTo(options.to ?? replay.to);
4336
+ return {
4337
+ world: run.world,
4338
+ apart: run.apart,
4339
+ difference: run.difference
4340
+ };
4341
+ }
4342
+ /**
4343
+ * `components`: those the app kept in the world recorded beside the game's (`CreateWorldOptions.components`: where the
4344
+ * screen was played from), without which its saves do not load
4345
+ */
4346
+ async function replayRun(replay, game, at = replay.from, components) {
4347
+ let checkpoint = [replay.from, replay.start];
4348
+ for (const kept of replay.checkpoints ?? []) if (kept[0] <= at && kept[0] > checkpoint[0]) checkpoint = kept;
4349
+ const [startTick, save0] = checkpoint;
4350
+ const world = await loadWorld(decodeSave(save0), {
4351
+ game,
4352
+ anyVersion: true,
4353
+ components
4354
+ });
4355
+ for (const player of replay.debug ?? []) world.debugPlayers.add(player);
4356
+ let command = replay.commands.findIndex(([tick]) => tick >= startTick);
4357
+ if (command < 0) command = replay.commands.length;
4358
+ let save = replay.saves.findIndex((tick) => tick > startTick);
4359
+ if (save < 0) save = replay.saves.length;
4360
+ let hash = replay.hashes.findIndex(([tick]) => tick > startTick);
4361
+ if (hash < 0) hash = replay.hashes.length;
4362
+ const ahead = (replay.checkpoints ?? []).filter(([tick]) => tick > startTick);
4363
+ let next = 0;
4364
+ let apart = null;
4365
+ let difference = null;
4366
+ const cameApart = (tick, what) => {
4367
+ if (apart === null) apart = tick;
4368
+ world.log.warn("replay", `The replay comes apart at tick ${tick}${what ?? ""}: the game is played on as these rules play it`, { tick });
4369
+ };
4370
+ return {
4371
+ world,
4372
+ get apart() {
4373
+ return apart;
4374
+ },
4375
+ get difference() {
4376
+ return difference;
4377
+ },
4378
+ stepTo(target, deadline = Infinity) {
4379
+ let steps = 0;
4380
+ while (world.tick < Math.min(target, replay.to) && !world.failure) {
4381
+ if (++steps % 8 === 0 && performance.now() > deadline) return false;
4382
+ while (command < replay.commands.length && replay.commands[command][0] <= world.tick) world.issue(structuredClone(replay.commands[command++][1]));
4383
+ while (save < replay.saves.length && replay.saves[save] <= world.tick) {
4384
+ saveWorld(world);
4385
+ save++;
4386
+ }
4387
+ world.step();
4388
+ while (hash < replay.hashes.length && replay.hashes[hash][0] < world.tick) hash++;
4389
+ if (hash < replay.hashes.length && replay.hashes[hash][0] === world.tick) {
4390
+ if (apart === null && worldFingerprint(world) !== replay.hashes[hash][1]) cameApart(world.tick);
4391
+ hash++;
4392
+ }
4393
+ while (next < ahead.length && ahead[next][0] < world.tick) next++;
4394
+ if (difference === null && next < ahead.length && ahead[next][0] === world.tick) {
4395
+ const now = decodeSave(encodeSave(saveWorld(world, { reseat: false })));
4396
+ const found = saveDifference(decodeSave(ahead[next][1]), now, world.registry);
4397
+ if (found) {
4398
+ difference = {
4399
+ tick: world.tick,
4400
+ ...found
4401
+ };
4402
+ cameApart(world.tick, `, first at ${found.path} (${JSON.stringify(found.a)} recorded, ${JSON.stringify(found.b)} now)`);
4403
+ }
4404
+ next++;
4405
+ }
4406
+ }
4407
+ return true;
4408
+ }
4409
+ };
4410
+ }
4411
+ /**
4412
+ * Two ends of a connection in the page, each message given to the other end once what runs now is done (a microtask):
4413
+ * in order, with no clock, and never inside the call that sent it
4414
+ */
4415
+ function pair() {
4416
+ const end = () => {
4417
+ let handler = () => {};
4418
+ let open = true;
4419
+ const transport = {
4420
+ send: (message) => {
4421
+ if (open) queueMicrotask(() => other(transport).deliver(message));
4422
+ },
4423
+ onMessage: (on) => void (handler = on),
4424
+ onClose: () => {},
4425
+ close: () => void (open = false),
4426
+ deliver: (message) => {
4427
+ if (open) handler(message);
4428
+ }
4429
+ };
4430
+ return transport;
4431
+ };
4432
+ const a = end();
4433
+ const b = end();
4434
+ const other = (one) => one === a ? b : a;
4435
+ return [a, b];
4436
+ }
4437
+ /** How often a replay being played looks whether its game is due a tick, a second */
4438
+ var LOOKS$1 = 30;
4439
+ /**
4440
+ * Milliseconds of a look a replay being played spends running its game on, at most: what a seek has to catch up on
4441
+ * (minute 15 is 27,000 ticks from the start) is run a slice a look, the page drawn between them
4442
+ *
4443
+ * @beta
4444
+ */
4445
+ var REPLAY_SLICE = 12;
4446
+ /**
4447
+ * Plays a replay of commands as a watcher's session: its world run again, `speed` times as fast, and served to the
4448
+ * session as a host serves one who watches (`SessionHost`), from `player`'s side, or `options.player`'s. A moment
4449
+ * asked for (`at`, `skipTo`) is run to a slice a look (`REPLAY_SLICE`), from the last checkpoint before it
4450
+ * (`recordCommands`' `checkpoints`) where it is behind: going back starts the world again from there, and the session
4451
+ * takes it up where it is (`RemoteSession.resume`). `behind` is how many ticks are still to run to it. `apart` is the
4452
+ * tick it came apart from the game recorded at, if it did, and `difference` what differed first at the first
4453
+ * checkpoint the world was not the one recorded (`runReplay`)
4454
+ *
4455
+ * @internal
4456
+ */
4457
+ function playCommands(replay, options) {
4458
+ let run;
4459
+ let host;
4460
+ let timer;
4461
+ let stopped = false;
4462
+ const within = (tick) => Math.min(Math.max(tick, replay.from), replay.to);
4463
+ let due = within(options.at ?? replay.from);
4464
+ /** The world made again for a moment behind it, while it is made */
4465
+ let going;
4466
+ const watcher = options.player ?? replay.player;
4467
+ /** A host for a world, with the watcher's end of a connection to it: the welcome goes out at once */
4468
+ const serve = (world) => {
4469
+ const [served, watching] = pair();
4470
+ const serving = new SessionHost(world, { sendRate: 10 });
4471
+ serving.connect(watcher, served, {
4472
+ readOnly: true,
4473
+ observer: true
4474
+ });
4475
+ return {
4476
+ serving,
4477
+ watching
4478
+ };
4479
+ };
4480
+ const made = replayRun(replay, options.game, due, options.components).then((first) => {
4481
+ run = first;
4482
+ const { serving, watching } = serve(first.world);
4483
+ host = serving;
4484
+ return connect(watching, { game: options.game });
4485
+ });
4486
+ /** Back to a moment before the world: made again from the last checkpoint before it, and the session taken there */
4487
+ const rewind = (session, to) => {
4488
+ going = replayRun(replay, options.game, to, options.components).then((again) => {
4489
+ if (stopped) return again.world.dispose();
4490
+ const { serving, watching } = serve(again.world);
4491
+ const resumed = session.resume(watching);
4492
+ host?.dispose();
4493
+ run?.world.dispose();
4494
+ run = again;
4495
+ host = serving;
4496
+ return resumed;
4497
+ }).catch((error) => run?.world.log.warn("replay", "The replay could not go back", { error: String(error) })).finally(() => going = void 0);
4498
+ };
4499
+ const player = {
4500
+ session: made.then((session) => {
4501
+ if (stopped) return session;
4502
+ let last = performance.now();
4503
+ timer = setInterval(() => {
4504
+ const now = performance.now();
4505
+ const seconds = (now - last) / 1e3;
4506
+ last = now;
4507
+ if (going || !run) return;
4508
+ if (Math.floor(due) < run.world.tick) return rewind(session, Math.floor(due));
4509
+ if (run.world.tick >= Math.floor(due)) due = Math.min(due + seconds * replay.tickRate * player.speed, replay.to);
4510
+ run.stepTo(Math.floor(due), now + 12);
4511
+ host.publish(seconds);
4512
+ }, 1e3 / LOOKS$1);
4513
+ return session;
4514
+ }),
4515
+ get tick() {
4516
+ return run?.world.tick ?? replay.from;
4517
+ },
4518
+ speed: 1,
4519
+ skipTo(tick) {
4520
+ due = within(tick);
4521
+ },
4522
+ get behind() {
4523
+ if (going) return Math.max(Math.floor(due) - replay.from, 1);
4524
+ return Math.max(Math.floor(due) - (run?.world.tick ?? replay.from), 0);
4525
+ },
4526
+ get ended() {
4527
+ return (run?.world.tick ?? replay.from) >= replay.to;
4528
+ },
4529
+ get apart() {
4530
+ return run?.apart ?? null;
4531
+ },
4532
+ get difference() {
4533
+ return run?.difference ?? null;
4534
+ },
4535
+ stop() {
4536
+ stopped = true;
4537
+ clearInterval(timer);
4538
+ host?.dispose();
4539
+ run?.world.dispose();
4540
+ }
4541
+ };
4542
+ return player;
4543
+ }
4544
+ //#endregion
4545
+ //#region src/session/replay.ts
4546
+ /** How often the recorder looks whether the world went on, a second: it keeps what changed at its own rate */
4547
+ var LOOKS = 30;
4548
+ /**
4549
+ * The tick a message says it brings, to answer that it got there: of a delta, from the head of its text where it comes
4550
+ * first; of the welcome, of the first frame it carries
4551
+ */
4552
+ var tickOf = (message) => {
4553
+ const found = /^\{"type":"delta","tick":(\d+)/.exec(message) ?? (message.startsWith("{\"type\":\"welcome\"") ? /"first":\{"tick":(\d+)/.exec(message) : null);
4554
+ return found ? Number(found[1]) : void 0;
4555
+ };
4556
+ /**
4557
+ * Records a game played in the page (`RecordOptions`): as its start and its players' commands (`kind: 'commands'`,
4558
+ * the default: `recordCommands`), or as an observer from `player`'s side sees it, what changed `sendRate` times a
4559
+ * second (`kind: 'frames'`). Frames are answered at once, as by a watcher who missed nothing, so what is kept is what
4560
+ * changed and not the whole world again
4561
+ *
4562
+ * @beta
4563
+ */
4564
+ function recordGame(world, options) {
4565
+ if ((options.kind ?? "commands") === "commands") return recordCommands(world, options);
4566
+ const host = new SessionHost(world, { sendRate: options.sendRate ?? 10 });
4567
+ const frames = [];
4568
+ const from = world.tick;
4569
+ let answer = () => {};
4570
+ host.connect(options.player, {
4571
+ send(message) {
4572
+ frames.push([world.tick, message]);
4573
+ const tick = tickOf(message);
4574
+ if (tick !== void 0) answer(JSON.stringify({
4575
+ type: "ack",
4576
+ tick
4577
+ }));
4578
+ },
4579
+ onMessage: (handler) => answer = handler,
4580
+ onClose: () => {},
4581
+ close: () => {}
4582
+ }, {
4583
+ readOnly: true,
4584
+ observer: true
4585
+ });
4586
+ let last = performance.now();
4587
+ const timer = setInterval(() => {
4588
+ const now = performance.now();
4589
+ host.publish((now - last) / 1e3);
4590
+ last = now;
4591
+ }, 1e3 / LOOKS);
4592
+ let replay;
4593
+ return {
4594
+ get ticks() {
4595
+ return world.tick - from;
4596
+ },
4597
+ stop() {
4598
+ if (replay) return replay;
4599
+ clearInterval(timer);
4600
+ host.dispose();
4601
+ replay = {
4602
+ format: 1,
4603
+ game: world.registry.game,
4604
+ player: options.player,
4605
+ tickRate: world.tickRate,
4606
+ from,
4607
+ to: world.tick,
4608
+ frames
4609
+ };
4610
+ return replay;
4611
+ }
4612
+ };
4613
+ }
4614
+ /**
4615
+ * Plays a replay back, as a watcher's session (`connect`): a replay of commands run again (`playCommands`), or what the
4616
+ * host sent coming in as it came, by the replay's ticks; `speed` times as fast. `game` is the game module it is of, or
4617
+ * the modules the page has, by id
4618
+ *
4619
+ * @beta
4620
+ */
4621
+ function playReplay(replay, options) {
4622
+ if (replay.format === 2) {
4623
+ const games = options.game;
4624
+ const game = "units" in games && typeof games.id === "string" ? games : games[replay.game];
4625
+ if (!game) throw new Error(`This page does not have the game the replay is of (${replay.game})`);
4626
+ return playCommands(replay, {
4627
+ game,
4628
+ at: options.at,
4629
+ components: options.components
4630
+ });
4631
+ }
4632
+ let deliver = () => {};
4633
+ const watching = () => ({
4634
+ send: () => {},
4635
+ onMessage: (handler) => void (deliver = handler),
4636
+ onClose: () => {},
4637
+ close: () => {}
4638
+ });
4639
+ const within = (tick) => Math.min(Math.max(tick, replay.from), replay.to);
4640
+ let next = 0;
4641
+ /** The moment it is asked to be at: the clock, or where a seek goes */
4642
+ let due = within(options.at ?? replay.from);
4643
+ let last = performance.now();
4644
+ /** The tick of the last frame handed to the session */
4645
+ const shown = () => next > 0 ? replay.frames[next - 1][0] : replay.from;
4646
+ /** Whether frames are still to be handed over to come to the moment asked */
4647
+ const pending = () => next < replay.frames.length && replay.frames[next][0] <= due;
4648
+ let session;
4649
+ const player = {
4650
+ session: connect(watching(), { game: options.game }),
4651
+ get tick() {
4652
+ return pending() ? shown() : due;
4653
+ },
4654
+ speed: 1,
4655
+ skipTo(target) {
4656
+ due = within(target);
4657
+ if (session && due < shown()) {
4658
+ next = 0;
4659
+ session.resume(watching()).catch(() => void 0);
4660
+ deliver(replay.frames[next++]?.[1] ?? "");
4661
+ }
4662
+ },
4663
+ get behind() {
4664
+ return pending() ? Math.max(Math.floor(due) - shown(), 1) : 0;
4665
+ },
4666
+ get ended() {
4667
+ return next >= replay.frames.length;
4668
+ },
4669
+ stop() {
4670
+ stopped = true;
4671
+ clearInterval(timer);
4672
+ }
4673
+ };
4674
+ /** Hands the session the frames up to the moment asked, until `deadline` is past */
4675
+ const flush = (deadline) => {
4676
+ let handed = 0;
4677
+ while (pending()) {
4678
+ deliver(replay.frames[next++][1]);
4679
+ if (++handed % 16 === 0 && performance.now() > deadline) return;
4680
+ }
4681
+ };
4682
+ let timer;
4683
+ let stopped = false;
4684
+ deliver(replay.frames[next++]?.[1] ?? "");
4685
+ player.session.then((made) => {
4686
+ if (stopped) return;
4687
+ session = made;
4688
+ last = performance.now();
4689
+ timer = setInterval(() => {
4690
+ const now = performance.now();
4691
+ if (!pending()) due = Math.min(due + (now - last) / 1e3 * replay.tickRate * player.speed, replay.to);
4692
+ last = now;
4693
+ flush(now + 12);
4694
+ }, 1e3 / LOOKS);
4695
+ }, () => void 0);
4696
+ return player;
4697
+ }
4698
+ //#endregion
4699
+ export { ARRIVE_RANGE, ATTACK_STATS, ATTRIBUTES, ATTRIBUTE_KEYS, AURA_LINGER, AVOIDANCE_LEVELS, Areas, BLOCKS_BUILD, BLOCKS_WALK, BOARD_REACH, BRIDGE, BUILD_REACH, BuildGrid, CAMPAIGN_FORMAT, CAPTURE_FIELDS, CAPTURE_KIND, CHAT_LENGTH, CLASSIC_ATTRIBUTES, COPY_OF, CORE_STAT_KEYS, DATA_KINDS, DEFAULT_AVOIDANCE, DEFAULT_HEIGHTS, DEFAULT_MAX_PLAYERS, DEFAULT_STANCE, DESTRUCTIBLES, DIRECTOR, DIRECTOR_ACTIONS, DIRECTOR_ORDERS, Dialog, ENTITY_DERIVED, ENTITY_SAVE, EXPOSED_FOR, Entity, FAIR_MARGIN, FOG_EXPLORED, FOG_UNEXPLORED, FOG_VISIBLE, FORMATION_GAP, FRIENDLY_FIRE, FogOfWar, HEROES, Invalid, LANE_END_REACH, LANE_SPAWNERS, LANE_SPAWNER_FIELDS, LANE_SPAWNER_KIND, LEGACY_TILESET, LINE_SECONDS, LOG_LEVELS, LocalSession, Logger, MANA_BURN, MAP_CREEPS, MAP_FORMAT, MAP_NEUTRAL, MAP_OBJECTIVES, MAP_OBJECTIVES_KEY, MAP_RULES, MAP_RULES_KEY, MAX_FOG_TEAMS, MAX_LEVEL, MAX_MAP_PLAYERS, MAX_MAP_SIZE, MAX_TILES, MIN_MAP_SIZE, MISSION_RULES, MOBA_STATS, MissingGround, ModError, NEW_DEFINITION, Objectives, PHASES, PING_KINDS, PLAYER_COLORS, PORTALS, PROFILE_SMOOTH, PROTOCOL_VERSION, PackError, Path, Player, RAMP_REACH, RANDOM_RACE, REACH, REPATH_SPREAD, REPLAY_SLICE, REQUIRED_FIELDS, RUNES, Registry, RemoteSession, Rng, SAVE_FORMAT, SCENARIO_FORMAT, SHOP_RANGE, SHUN_TIME, STANCES, STAT_WORDS, SaveError, Scheduler, Score, Sequences, SessionHost, SessionRefused, SessionView, SidePaths, SizedNavigation, TAU, TOMES, Terrain, Timers, UNLOAD_REACH, UPHILL_MISS, UnlistedError, VETERANCY_RANKS, WATER_DEEP, WATER_NONE, WATER_SHALLOW, WorkerSession, World, WorldMap, abilityCast, abilityImpact, abilityReach, abilityRefusal, abilityText, abilityValues, adoptTileset, aimOf, allowed, angleDiff, angleTo, anyEffect, applyBuff, apply as applyDelta, applyHeroRecord, applyTerrainPatch, areaShapeOf, askPause, assembleGame, at, atShop, attackCommand, attackPathOrder, attackReaches, attackStarted, attackStat, attackTiming, attackersOf, attributesOf, autocastCommand, availableTo, availableToPlayer, avoids, bandOf, baseStats, basinShape, blankDefinition, blockByDoodads, board, boardOrder, boarded, bool, bounty, buffApplied, buffEnded, buffRemoved, buildLineCommand, buildNavigationData, builtSoFar, burnMana, buy, buyCommand, campCleared, campFields, campHooks, campLayout, campMember, campNamed, campRespawnSystem, campReturned, camps, campsOf, canAttack, canBoard, canHarvest, canLearn, canPlaceStructure, canRepair, cancelConstructionCommand, cancelTrainCommand, captureAreaOf, captureOf, capturePoint, captured, captures, capturesOf, capturing, cargo, carryIn, carryOut, carryResearch, carryResources, carryUnits, castCommand, castRange, castStarted, castTimeOf, casterFacts, cellLayerValues, cellsChanged, cellsIn, cellsSeen, changedStats, channelEnded, channelStarted, chargesLeft, chat, chatted, checkDamageTable, checkLanes, checkMap, checkMapObjectives, checkMissionRules, checkPack, checkPackSchema, checkRaces, checkRules, checkpoint, chopped, cinematic, cinematicChoice, cinematicChoiceCommand, cinematicCommand, cinematicCue, cinematicCued, cinematicEnded, cinematicPlayingCommand, circleFormation, clamp, clearGround, cloneMap, cloneTerrain, closestPointOnRect, commandHandled, commandRejected, composeGame, compositionOf, connect, consoleSink, constructionCancelled, constructionFinished, constructionOf, constructionSite, constructionStarted, cooldownOf, copyDefinition, coreCommands, coreFormations, coreHooks, coreSystems, coreVictory, corpseSystem, corpses, costRefusal, createItem, createLogRing, createMap, createRegistry, createWorld, createWorldFromMap, cueCinematic, currencyOf, damageMultiplier, damaged, dashing, dataRulesProblem, declaredRefusal, decodeSave, defaultEngageLayer, defaultEngageRoom, defaultHeroRules, defaultPickAttack, defaultRules, defaultWave, defineArea, defineCellLayer, defineCommand, defineComponent, defineEffect, defineEvent, defineGameFile, defineHook, defineHookType, defineIds, defineListener, defineMapField, defineMod, defineObjective, definePrefab, defineRace, defineSequence, defineStat, defineTile, defineTimer, defineTunable, definitionChanged, definitionIn, definitionsIn, delayed, denying, deposit, describeAbility, detection, diff as diffFrames, directing, directorCommand, directorInvulnerable, dispelBuffs, dispelEffects, dist, distSq, distToRect, distanceToEdge, drawFromGroup, drawFromTable, dropOnGround, earn, edgeDistance, editDistance, editableFields, editableValues, effectFields, effectStarted, effectStopped, effectTargets, effectTypesOf, encodeSave, engageLayer, engageWith, engagedLike, engagedWith, engineMessage, engineStats, enteredArea, entityDied, entityMorphed, entityRemoved, entitySpawned, environmentChanged, errorFields, eventById, exactMath, exchange, exchanged, expose, exposure, faceTowards, farthestAcquisition, farthestReach, fieldOptions, fighting, findGround, findItem, fireOnTheMove, fitToPlaces, fitValue, followPathOrder, footprintCell, footprintOf, formatNumber, formationContext, formationPoints, freeSlot, fromRuns, gainStats, gainedStats, gameHash, gameOver, gameReloaded, garrisonHooks, gateChanged, gateStates, gathering, gathers, generateMap, generateTerrain, give, giveBack, giveItem, giveMapObjective, giveResearch, giveResources, grantVeterancy, grantXp, groundAttack, groundBase, groundDifference, groundHash, groundOf, groundReshaped, groundSpeedAt, groundSpeedOfCell, groupChoices, halt, harvesterOf, hasBuff, hasPack, hashInts, healed, heightsFromPixels, heightsToPixels, heroCommands, heroFromRecord, heroLevelUp, heroOf, heroOfState, heroRecord, heroRevived, heroState, heroStrike, heroesOf, hire, hireCommand, hireRefusal, hirerAt, homeOf, hotMod, hotRules, hourOf, id, ids, illusion, impact, inShape, index, innerEffects, invalidateStats, inventoryChanged, inventorySlots, isAvailable, isBlocked, isCommand, isCrowded, isFiniteNumber, isGateOpen, isGroundChanges, isMadeOf, isMovingTo, isNight, isObject, isRooted, isScenery, isServerMessage, isShunned, isSilenced, isSpell, isSpellImmune, isStunned, itemDropped, itemPicked, laneMinion, lanesOf, lastHitsOf, lastTeamStanding, layerOf, layeredNoise, learn, learnCommand, leftArea, lerp, lerpAngle, levelNeeded, lineEnded, lineSaid, list, loadOrder, loadTerrain, loadWorld, logLine, lookAhead, loopback, makeHero, makeWay, manaChanged, mapCampData, mapCell, mapCells, mapDrops, mapFields, mapFileStem, mapGeneratorWorker, mapLanes, mapNamesSource, mapObjectData, mapObjectivePlayers, mapObjectives, mapObjectivesOf, mapPlayerData, mapRegionData, mapTerrain, mapText, mapThings, mapTileIds, mapsFrom, maxLevelOf, maxPlayersOf, mayDeny, measureMap, meets, memoryNavigationCache, mergeMaps, mergeValues, messageSent, migrateMap, missed, missileHit, missileRemoved, missileSpawned, missing, missingTech, missionRuleState, missionRulesOf, missionWon, mitigation, morph, morphCancelled, morphStarted, moveTo, moveToEntity, moving, navigationKey, navigationWorker, nearestDryGround, nested, nestedPartId, newCampaignProgress, noStats, noneFormation, notReady, number, object, objectFields, objectiveChanged, objectiveVictory, onNewGameVersion, onNewModVersion, oneOf, openSocket, optional, orderCommand, orderSays, packFromJson, packsOf, parseMap, passiveEffects, patchAt, patrolPathOrder, pauseAsked, pauseCommand, pausesLeft, pay, penetratingMitigation, pickAttack, pickUp, pileOf, pileTaken, pileType, ping, pingKinds, pinged, placeUnits, placementCells, placementProblem, plain as plainData, playCinematic, playCommands, playEffect, playReplay, playTileset, playerDefeated, playerFacts, playerFields, playerSlot, playerSurrendered, point, portal, portalUsed, prefabsFrom, presetBlight, presetFountain, presetGenerator, presetMarket, presetScore, presetTiles, priceOf, primaryAttribute, productionOf, productionQueue, profileReach, projectileRemoved, projectileSpawned, protocolMismatch, quantize, queueOf, raceOf, rallyCommand, randomSeed, rangeAgainst, rankOf, rankedUp, reachedByDefault, reaches, readCampaignProgress, readHeroRecord, readHostMessage, readMessage, readPlayerMessage, readScenario, recallAbility, recompose, recordCommands, recordGame, redefineIn, redefinitions, refreshVitals, refund, regionCinematic, regionFields, release, reloadGame, removeBuff, removeDefinition, replaceIn, replicateEvent, replicatedEvents, requirementRefusal, researchCommand, researchFinished, resisted, resizeMap, resolveRules, resourceChanged, resourcesDeposited, resourcesPass, resourcesSpent, respawnCamp, respawnIn, respawning, restoreCharges, reviveCommand, reviveCost, reviveHero, rgb, roadsPass, roomFor, roomIn, rootConsole, rootLogger, rotateTowards, rowsFormation, rubbleType, ruleFired, runReplay, rustled, satisfies, saveDifference, saveHeader, saveProblem, saveTerrain, saveWorld, savedGround, scaled, scaledParts, scatterPass, scenarioOfMap, scenarioOfWorld, scenarioWorld, scoreData, scoreOn, seconds, sell, sellCommand, sellValue, sendWave, serializeMap, serveMapGenerator, serveNavigationBuilds, serveWorld, setAnchored, setGate, setGhost, setHeroLevel, setRace, setWallGate, setWaygate, shapeArea, shapeAsBox, shapeAt, shapeBounds, shapeReach, shareGame, shareWith, shieldOf, shootsUphill, shopNear, shopStock, shoreNear, shortOf, shun, sideKnowledge, silences, siteKey, siteOf, siteWords, slotsOf, smartCommand, smoothstep, snapshotOf, snapshotPart, spawnCamp, spawnPrefab, spawnStart, spend, spendCharge, spotFor, squareFormation, stance, stanceCommand, stanceOf, standsInWay, startDash, startedOn, startingBase, startingTerrain, startsPass, statsOf, stepAside, stopCommand, stopEffect, string, summonEffects, summonHooks, summoned, surrender, surrenderCommand, swingTiming, symmetricPlaces, tableItems, takeCorpses, takeItem, talk, targetFacts, targetRefusal, teamChanged, teamMemory, techOf, techTree, tellRules, terrainPass, terrainPatch, terrainPatchProblem, text, tileRemap, tilesFor, tilesPass, tilingNoise, toMessage, toRuns, tooNear, townPortal, traded, trainCommand, trainRefusal, tunableChanged, tuneCommand, tunings, underConstruction, unitHired, unitTrained, unload, unloadOrder, unloaded, unlockRefusal, unlocked, updateMapTerrain, updateSave, upgradeBuildingCommand, upgradeLevel, upgradeOf, upgradesOf, upkeepOf, usePowerUp, validTarget, valueAt, valueText, vec2, veterancy, viewLevel, wait, waitFor, waitUntil, walkBetween, walkingTime, wallGate, wallGateChanged, wallGateCommand, wallJoined, wallLine, wallShapeOf, wavesSent, waygate, waygateTarget, withAttributes, withCapture, withChanges, withConcealment, withData, withDestructibles, withDirector, withFriendlyFire, withHeroes, withLanes, withLastHits, withManaBurn, withMobaStats, withPortals, withRemains, withRespawn, withRunes, withStances, withTomes, withTuning, withValues, withVeterancy, withWalls, withoutEditor, worked, workerSession, worldFailed, worthless, xpGained, xpWorth };
4700
+
4701
+ //# sourceMappingURL=index.js.map