@v12-engine/engine 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abilities/amounts.d.ts +28 -0
- package/dist/abilities/amounts.js +74 -0
- package/dist/abilities/amounts.js.map +1 -0
- package/dist/abilities/buffs.d.ts +106 -6
- package/dist/abilities/buffs.js +443 -0
- package/dist/abilities/buffs.js.map +1 -0
- package/dist/abilities/castTold.d.ts +2 -0
- package/dist/abilities/castTold.js +66 -0
- package/dist/abilities/castTold.js.map +1 -0
- package/dist/abilities/chance.d.ts +38 -0
- package/dist/abilities/chance.js +77 -0
- package/dist/abilities/chance.js.map +1 -0
- package/dist/abilities/charges.d.ts +18 -0
- package/dist/abilities/charges.js +101 -0
- package/dist/abilities/charges.js.map +1 -0
- package/dist/abilities/control.d.ts +28 -0
- package/dist/abilities/control.js +113 -0
- package/dist/abilities/control.js.map +1 -0
- package/dist/abilities/dispel.js +51 -0
- package/dist/abilities/dispel.js.map +1 -0
- package/dist/abilities/effectFields.d.ts +1 -1
- package/dist/abilities/effectFields.js +676 -0
- package/dist/abilities/effectFields.js.map +1 -0
- package/dist/abilities/effectIndex.d.ts +27 -0
- package/dist/abilities/effectIndex.js +64 -0
- package/dist/abilities/effectIndex.js.map +1 -0
- package/dist/abilities/effects/buff.d.ts +9 -3
- package/dist/abilities/effects/buff.js +84 -0
- package/dist/abilities/effects/buff.js.map +1 -0
- package/dist/abilities/effects/conditions.d.ts +2 -0
- package/dist/abilities/effects/conditions.js +41 -0
- package/dist/abilities/effects/conditions.js.map +1 -0
- package/dist/abilities/effects/core.js +54 -0
- package/dist/abilities/effects/core.js.map +1 -0
- package/dist/abilities/effects/damage.d.ts +1 -1
- package/dist/abilities/effects/damage.js +43 -0
- package/dist/abilities/effects/damage.js.map +1 -0
- package/dist/abilities/effects/heal.d.ts +2 -2
- package/dist/abilities/effects/heal.js +51 -0
- package/dist/abilities/effects/heal.js.map +1 -0
- package/dist/abilities/effects/missile.d.ts +13 -1
- package/dist/abilities/effects/missile.js +104 -0
- package/dist/abilities/effects/missile.js.map +1 -0
- package/dist/abilities/effects/morph.d.ts +5 -0
- package/dist/abilities/effects/morph.js +43 -0
- package/dist/abilities/effects/morph.js.map +1 -0
- package/dist/abilities/effects/moves.d.ts +16 -4
- package/dist/abilities/effects/moves.js +212 -0
- package/dist/abilities/effects/moves.js.map +1 -0
- package/dist/abilities/effects/passives.d.ts +19 -9
- package/dist/abilities/effects/passives.js +131 -0
- package/dist/abilities/effects/passives.js.map +1 -0
- package/dist/abilities/effects/proxy.d.ts +5 -0
- package/dist/abilities/effects/proxy.js +60 -0
- package/dist/abilities/effects/proxy.js.map +1 -0
- package/dist/abilities/effects/sight.d.ts +2 -2
- package/dist/abilities/effects/sight.js +39 -0
- package/dist/abilities/effects/sight.js.map +1 -0
- package/dist/abilities/effects/spawn.d.ts +5 -2
- package/dist/abilities/effects/spawn.js +40 -0
- package/dist/abilities/effects/spawn.js.map +1 -0
- package/dist/abilities/effects/timed.d.ts +2 -2
- package/dist/abilities/effects/timed.js +96 -0
- package/dist/abilities/effects/timed.js.map +1 -0
- package/dist/abilities/effects/trade.d.ts +1 -1
- package/dist/abilities/effects/trade.js +25 -0
- package/dist/abilities/effects/trade.js.map +1 -0
- package/dist/abilities/effects.d.ts +97 -23
- package/dist/abilities/effects.js +860 -0
- package/dist/abilities/effects.js.map +1 -0
- package/dist/abilities/land.d.ts +38 -4
- package/dist/abilities/land.js +189 -0
- package/dist/abilities/land.js.map +1 -0
- package/dist/abilities/recast.d.ts +45 -0
- package/dist/abilities/recast.js +118 -0
- package/dist/abilities/recast.js.map +1 -0
- package/dist/abilities/scaled.js +40 -0
- package/dist/abilities/scaled.js.map +1 -0
- package/dist/abilities/screen.d.ts +6 -0
- package/dist/abilities/screen.js +83 -0
- package/dist/abilities/screen.js.map +1 -0
- package/dist/abilities/shapes.d.ts +66 -0
- package/dist/abilities/shapes.js +122 -0
- package/dist/abilities/shapes.js.map +1 -0
- package/dist/abilities/text.d.ts +14 -2
- package/dist/abilities/text.js +204 -0
- package/dist/abilities/text.js.map +1 -0
- package/dist/abilities/triggers.d.ts +98 -0
- package/dist/abilities/triggers.js +351 -0
- package/dist/abilities/triggers.js.map +1 -0
- package/dist/abilityChanges.d.ts +131 -0
- package/dist/abilityChanges.js +345 -0
- package/dist/abilityChanges.js.map +1 -0
- package/dist/ai/area.d.ts +4 -1
- package/dist/ai/brain.d.ts +4 -1
- package/dist/ai/brain.js +844 -0
- package/dist/ai/brain.js.map +1 -0
- package/dist/ai/budget.js +74 -0
- package/dist/ai/budget.js.map +1 -0
- package/dist/ai/checks.d.ts +9 -1
- package/dist/ai/checks.js +242 -0
- package/dist/ai/checks.js.map +1 -0
- package/dist/ai/computer.d.ts +11 -4
- package/dist/ai/computer.js +251 -0
- package/dist/ai/computer.js.map +1 -0
- package/dist/ai/defence.d.ts +4 -0
- package/dist/ai/defence.js +430 -0
- package/dist/ai/defence.js.map +1 -0
- package/dist/ai/define.d.ts +3 -2
- package/dist/ai/define.js +31 -0
- package/dist/ai/define.js.map +1 -0
- package/dist/ai/economy.d.ts +10 -7
- package/dist/ai/economy.js +780 -0
- package/dist/ai/economy.js.map +1 -0
- package/dist/ai/geometry.js +38 -0
- package/dist/ai/geometry.js.map +1 -0
- package/dist/ai/goals.d.ts +1 -0
- package/dist/ai/goals.js +560 -0
- package/dist/ai/goals.js.map +1 -0
- package/dist/ai/knowledge.d.ts +72 -7
- package/dist/ai/knowledge.js +804 -0
- package/dist/ai/knowledge.js.map +1 -0
- package/dist/ai/lanes.d.ts +10 -2
- package/dist/ai/lanes.js +715 -0
- package/dist/ai/lanes.js.map +1 -0
- package/dist/ai/melee/build.js +264 -0
- package/dist/ai/melee/build.js.map +1 -0
- package/dist/ai/melee/goals.d.ts +7 -0
- package/dist/ai/melee/goals.js +720 -0
- package/dist/ai/melee/goals.js.map +1 -0
- package/dist/ai/melee/heroes.js +68 -0
- package/dist/ai/melee/heroes.js.map +1 -0
- package/dist/ai/melee/index.d.ts +1 -1
- package/dist/ai/melee/index.js +51 -0
- package/dist/ai/melee/index.js.map +1 -0
- package/dist/ai/melee/production.js +29 -0
- package/dist/ai/melee/production.js.map +1 -0
- package/dist/ai/melee/surrender.js +64 -0
- package/dist/ai/melee/surrender.js.map +1 -0
- package/dist/ai/micro.d.ts +9 -0
- package/dist/ai/micro.js +1316 -0
- package/dist/ai/micro.js.map +1 -0
- package/dist/ai/mission.d.ts +9 -2
- package/dist/ai/mission.js +538 -0
- package/dist/ai/mission.js.map +1 -0
- package/dist/ai/missionRules.d.ts +1 -1
- package/dist/ai/missionRules.js +26 -0
- package/dist/ai/missionRules.js.map +1 -0
- package/dist/ai/module.d.ts +6 -2
- package/dist/ai/module.js +37 -0
- package/dist/ai/module.js.map +1 -0
- package/dist/ai/options.d.ts +3 -3
- package/dist/ai/plans/build.d.ts +20 -1
- package/dist/ai/plans/build.js +608 -0
- package/dist/ai/plans/build.js.map +1 -0
- package/dist/ai/plans/placement.d.ts +4 -1
- package/dist/ai/plans/placement.js +295 -0
- package/dist/ai/plans/placement.js.map +1 -0
- package/dist/ai/plans/plans.d.ts +9 -0
- package/dist/ai/plans/plans.js +110 -0
- package/dist/ai/plans/plans.js.map +1 -0
- package/dist/ai/plans/production.d.ts +14 -2
- package/dist/ai/plans/production.js +434 -0
- package/dist/ai/plans/production.js.map +1 -0
- package/dist/ai/plans/purse.d.ts +6 -3
- package/dist/ai/plans/purse.js +139 -0
- package/dist/ai/plans/purse.js.map +1 -0
- package/dist/ai/plans/tech.d.ts +6 -0
- package/dist/ai/plans/tech.js +122 -0
- package/dist/ai/plans/tech.js.map +1 -0
- package/dist/ai/profiles.d.ts +4 -4
- package/dist/{profiles-B3NS9jwk.js → ai/profiles.js} +3 -98
- package/dist/ai/profiles.js.map +1 -0
- package/dist/ai/reach.js +54 -0
- package/dist/ai/reach.js.map +1 -0
- package/dist/ai/report.d.ts +8 -0
- package/dist/ai/report.js +87 -0
- package/dist/ai/report.js.map +1 -0
- package/dist/ai/roles.d.ts +4 -0
- package/dist/ai/roles.js +207 -0
- package/dist/ai/roles.js.map +1 -0
- package/dist/ai/rules.d.ts +8 -2
- package/dist/ai/rules.js +416 -0
- package/dist/ai/rules.js.map +1 -0
- package/dist/ai/scouting.d.ts +6 -0
- package/dist/ai/scouting.js +324 -0
- package/dist/ai/scouting.js.map +1 -0
- package/dist/ai/settings.js +99 -0
- package/dist/ai/settings.js.map +1 -0
- package/dist/ai/squads.d.ts +21 -2
- package/dist/ai/squads.js +916 -0
- package/dist/ai/squads.js.map +1 -0
- package/dist/ai/strength.js +320 -0
- package/dist/ai/strength.js.map +1 -0
- package/dist/ai/task.d.ts +3 -1
- package/dist/ai/task.js +17 -0
- package/dist/ai/task.js.map +1 -0
- package/dist/ai/tasks/attack.js +24 -0
- package/dist/ai/tasks/attack.js.map +1 -0
- package/dist/ai/tasks/capture.d.ts +3 -3
- package/dist/ai/tasks/capture.js +37 -0
- package/dist/ai/tasks/capture.js.map +1 -0
- package/dist/ai/tasks/creep.js +29 -0
- package/dist/ai/tasks/creep.js.map +1 -0
- package/dist/ai/tasks/defend.js +61 -0
- package/dist/ai/tasks/defend.js.map +1 -0
- package/dist/ai/tasks/drop.js +81 -0
- package/dist/ai/tasks/drop.js.map +1 -0
- package/dist/ai/tasks/escort.js +59 -0
- package/dist/ai/tasks/escort.js.map +1 -0
- package/dist/ai/tasks/gather.js +48 -0
- package/dist/ai/tasks/gather.js.map +1 -0
- package/dist/ai/tasks/harass.js +39 -0
- package/dist/ai/tasks/harass.js.map +1 -0
- package/dist/ai/tasks/hold.js +32 -0
- package/dist/ai/tasks/hold.js.map +1 -0
- package/dist/ai/tasks/index.js +34 -0
- package/dist/ai/tasks/index.js.map +1 -0
- package/dist/ai/tasks/move.js +26 -0
- package/dist/ai/tasks/move.js.map +1 -0
- package/dist/ai/tasks/patrol.js +59 -0
- package/dist/ai/tasks/patrol.js.map +1 -0
- package/dist/ai/tasks/retreat.js +28 -0
- package/dist/ai/tasks/retreat.js.map +1 -0
- package/dist/ai/tasks/scout.js +73 -0
- package/dist/ai/tasks/scout.js.map +1 -0
- package/dist/ai/tasks/shared.d.ts +1 -1
- package/dist/ai/tasks/shared.js +137 -0
- package/dist/ai/tasks/shared.js.map +1 -0
- package/dist/ai/team.d.ts +18 -1
- package/dist/ai/team.js +492 -0
- package/dist/ai/team.js.map +1 -0
- package/dist/ai/threat.d.ts +6 -0
- package/dist/ai/threat.js +206 -0
- package/dist/ai/threat.js.map +1 -0
- package/dist/ai/view.d.ts +21 -7
- package/dist/ai/view.js +677 -0
- package/dist/ai/view.js.map +1 -0
- package/dist/ai/waves.d.ts +6 -0
- package/dist/ai/waves.js +410 -0
- package/dist/ai/waves.js.map +1 -0
- package/dist/ai.js +54 -6239
- package/dist/aiTesting.d.ts +6 -3
- package/dist/aiTesting.js +229 -0
- package/dist/aiTesting.js.map +1 -0
- package/dist/areas.d.ts +4 -1
- package/dist/areas.js +295 -0
- package/dist/areas.js.map +1 -0
- package/dist/attacks.d.ts +3 -3
- package/dist/attacks.js +127 -0
- package/dist/attacks.js.map +1 -0
- package/dist/burning.d.ts +267 -0
- package/dist/burning.js +795 -0
- package/dist/burning.js.map +1 -0
- package/dist/bytes.js +21 -0
- package/dist/bytes.js.map +1 -0
- package/dist/callings.d.ts +71 -0
- package/dist/callings.js +157 -0
- package/dist/callings.js.map +1 -0
- package/dist/cameraOrders.d.ts +78 -0
- package/dist/cameraOrders.js +114 -0
- package/dist/cameraOrders.js.map +1 -0
- package/dist/camps.d.ts +70 -4
- package/dist/camps.js +501 -0
- package/dist/camps.js.map +1 -0
- package/dist/capture.d.ts +15 -3
- package/dist/capture.js +304 -0
- package/dist/capture.js.map +1 -0
- package/dist/cellLayers.d.ts +78 -13
- package/dist/cellLayers.js +275 -0
- package/dist/cellLayers.js.map +1 -0
- package/dist/cellPatches.d.ts +24 -0
- package/dist/cellPatches.js +73 -0
- package/dist/cellPatches.js.map +1 -0
- package/dist/cinematics.d.ts +10 -5
- package/dist/cinematics.js +161 -0
- package/dist/cinematics.js.map +1 -0
- package/dist/clock.d.ts +2 -2
- package/dist/clock.js +78 -0
- package/dist/clock.js.map +1 -0
- package/dist/commands.d.ts +36 -4
- package/dist/commands.js +636 -0
- package/dist/commands.js.map +1 -0
- package/dist/components.d.ts +31 -10
- package/dist/components.js +74 -0
- package/dist/components.js.map +1 -0
- package/dist/computers.d.ts +4 -4
- package/dist/computers.js +194 -0
- package/dist/computers.js.map +1 -0
- package/dist/concealment.d.ts +25 -2
- package/dist/concealment.js +409 -0
- package/dist/concealment.js.map +1 -0
- package/dist/coreComponents.js +130 -0
- package/dist/coreComponents.js.map +1 -0
- package/dist/createGame.d.ts +306 -0
- package/dist/createGame.js +405 -0
- package/dist/createGame.js.map +1 -0
- package/dist/dashing.d.ts +18 -2
- package/dist/dashing.js +41 -0
- package/dist/dashing.js.map +1 -0
- package/dist/debug.d.ts +8 -0
- package/dist/debug.js +5 -3
- package/dist/debug2.js +76 -0
- package/dist/debug2.js.map +1 -0
- package/dist/debugCommands.d.ts +6 -6
- package/dist/debugCommands.js +537 -0
- package/dist/debugCommands.js.map +1 -0
- package/dist/debugging.d.ts +1 -0
- package/dist/defaultSets.d.ts +183 -0
- package/dist/defaultSets.js +216 -0
- package/dist/defaultSets.js.map +1 -0
- package/dist/definitions.d.ts +6 -0
- package/dist/definitions.js +78 -0
- package/dist/definitions.js.map +1 -0
- package/dist/defs.d.ts +1895 -165
- package/dist/defs.js +380 -0
- package/dist/defs.js.map +1 -0
- package/dist/denying.d.ts +4 -4
- package/dist/denying.js +106 -0
- package/dist/denying.js.map +1 -0
- package/dist/destructibles.d.ts +2 -2
- package/dist/destructibles.js +36 -0
- package/dist/destructibles.js.map +1 -0
- package/dist/determinism.d.ts +4 -4
- package/dist/determinism.js +247 -0
- package/dist/determinism.js.map +1 -0
- package/dist/dialog.d.ts +17 -2
- package/dist/dialog.js +108 -0
- package/dist/dialog.js.map +1 -0
- package/dist/effectRefs.js +28 -0
- package/dist/effectRefs.js.map +1 -0
- package/dist/effects.d.ts +27 -11
- package/dist/effects.js +110 -0
- package/dist/effects.js.map +1 -0
- package/dist/endings.d.ts +1 -1
- package/dist/endings.js +78 -0
- package/dist/endings.js.map +1 -0
- package/dist/engagement.d.ts +40 -6
- package/dist/engagement.js +530 -0
- package/dist/engagement.js.map +1 -0
- package/dist/engineEvents.d.ts +72 -9
- package/dist/engineEvents.js +520 -0
- package/dist/engineEvents.js.map +1 -0
- package/dist/entity.d.ts +275 -26
- package/dist/entity.js +441 -0
- package/dist/entity.js.map +1 -0
- package/dist/entityIndex.d.ts +10 -3
- package/dist/entityIndex.js +149 -0
- package/dist/entityIndex.js.map +1 -0
- package/dist/errors.d.ts +6 -2
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +11 -5
- package/dist/events.js +158 -0
- package/dist/events.js.map +1 -0
- package/dist/exactMath.js +273 -0
- package/dist/exactMath.js.map +1 -0
- package/dist/features.d.ts +1 -1
- package/dist/features.js +127 -0
- package/dist/features.js.map +1 -0
- package/dist/fields.d.ts +25 -5
- package/dist/fields.js +140 -0
- package/dist/fields.js.map +1 -0
- package/dist/fixture.d.ts +3 -3
- package/dist/fixture.js +512 -0
- package/dist/fixture.js.map +1 -0
- package/dist/fog.d.ts +26 -3
- package/dist/fog.js +333 -0
- package/dist/fog.js.map +1 -0
- package/dist/foliage.d.ts +476 -0
- package/dist/foliage.js +1478 -0
- package/dist/foliage.js.map +1 -0
- package/dist/foliageDebug.d.ts +57 -0
- package/dist/foliageDebug.js +172 -0
- package/dist/foliageDebug.js.map +1 -0
- package/dist/foliageRegrowth.d.ts +20 -0
- package/dist/foliageRegrowth.js +332 -0
- package/dist/foliageRegrowth.js.map +1 -0
- package/dist/formState.d.ts +67 -0
- package/dist/formState.js +65 -0
- package/dist/formState.js.map +1 -0
- package/dist/formation.d.ts +46 -8
- package/dist/formation.js +297 -0
- package/dist/formation.js.map +1 -0
- package/dist/forms.d.ts +58 -0
- package/dist/forms.js +146 -0
- package/dist/forms.js.map +1 -0
- package/dist/friendlyFire.d.ts +2 -2
- package/dist/friendlyFire.js +37 -0
- package/dist/friendlyFire.js.map +1 -0
- package/dist/game.d.ts +141 -48
- package/dist/game.js +834 -0
- package/dist/game.js.map +1 -0
- package/dist/gameData.d.ts +96 -22
- package/dist/gameData.js +1690 -0
- package/dist/gameData.js.map +1 -0
- package/dist/gameFiles.d.ts +3 -3
- package/dist/gameFiles.js +14 -0
- package/dist/gameFiles.js.map +1 -0
- package/dist/gates.js +92 -0
- package/dist/gates.js.map +1 -0
- package/dist/generate.d.ts +28 -1
- package/dist/generate.js +1062 -0
- package/dist/generate.js.map +1 -0
- package/dist/generateWorker.d.ts +7 -3
- package/dist/generateWorker.js +86 -0
- package/dist/generateWorker.js.map +1 -0
- package/dist/grid.d.ts +9 -0
- package/dist/grid.js +102 -0
- package/dist/grid.js.map +1 -0
- package/dist/ground.d.ts +17 -1
- package/dist/ground.js +375 -0
- package/dist/ground.js.map +1 -0
- package/dist/groundObjects.d.ts +70 -0
- package/dist/groundObjects.js +371 -0
- package/dist/groundObjects.js.map +1 -0
- package/dist/groundSpeed.d.ts +20 -3
- package/dist/groundSpeed.js +117 -0
- package/dist/groundSpeed.js.map +1 -0
- package/dist/hero.d.ts +126 -10
- package/dist/hero.js +432 -0
- package/dist/hero.js.map +1 -0
- package/dist/holds.d.ts +94 -0
- package/dist/holds.js +305 -0
- package/dist/holds.js.map +1 -0
- package/dist/home.d.ts +1 -1
- package/dist/home.js +30 -0
- package/dist/home.js.map +1 -0
- package/dist/hooks.d.ts +339 -57
- package/dist/hooks.js +105 -0
- package/dist/hooks.js.map +1 -0
- package/dist/hotReload.d.ts +6 -5
- package/dist/hotReload.js +145 -0
- package/dist/hotReload.js.map +1 -0
- package/dist/ids.d.ts +3 -3
- package/dist/ids.js +30 -0
- package/dist/ids.js.map +1 -0
- package/dist/idsFile.d.ts +36 -0
- package/dist/idsFile.js +134 -0
- package/dist/idsFile.js.map +1 -0
- package/dist/index.d.ts +105 -68
- package/dist/index.js +194 -4701
- package/dist/inspect.d.ts +29 -2
- package/dist/{inspect-D9dWHmZ0.js → inspect.js} +21 -12
- package/dist/inspect.js.map +1 -0
- package/dist/inventory.d.ts +14 -1
- package/dist/inventory.js +84 -0
- package/dist/inventory.js.map +1 -0
- package/dist/items.d.ts +136 -8
- package/dist/items.js +585 -0
- package/dist/items.js.map +1 -0
- package/dist/lanes.d.ts +12 -3
- package/dist/lanes.js +496 -0
- package/dist/lanes.js.map +1 -0
- package/dist/life.d.ts +6 -2
- package/dist/life.js +287 -0
- package/dist/life.js.map +1 -0
- package/dist/log.d.ts +26 -3
- package/dist/log.js +192 -0
- package/dist/log.js.map +1 -0
- package/dist/manaBurn.d.ts +2 -2
- package/dist/manaBurn.js +75 -0
- package/dist/manaBurn.js.map +1 -0
- package/dist/map/file.d.ts +56 -3
- package/dist/map/file.js +264 -0
- package/dist/map/file.js.map +1 -0
- package/dist/map/images.js +61 -0
- package/dist/map/images.js.map +1 -0
- package/dist/map/look.d.ts +1 -1
- package/dist/map/look.js +30 -0
- package/dist/map/look.js.map +1 -0
- package/dist/map/pathProfile.js +183 -0
- package/dist/map/pathProfile.js.map +1 -0
- package/dist/map/resize.d.ts +4 -0
- package/dist/map/resize.js +136 -0
- package/dist/map/resize.js.map +1 -0
- package/dist/map/schema.js +257 -0
- package/dist/map/schema.js.map +1 -0
- package/dist/map/terrain.js +214 -0
- package/dist/map/terrain.js.map +1 -0
- package/dist/map/types.d.ts +92 -13
- package/dist/map/types.js +251 -0
- package/dist/map/types.js.map +1 -0
- package/dist/mapCheck/cells.js +94 -0
- package/dist/mapCheck/cells.js.map +1 -0
- package/dist/mapCheck/rules/foliage.d.ts +9 -0
- package/dist/mapCheck/rules/foliage.js +83 -0
- package/dist/mapCheck/rules/foliage.js.map +1 -0
- package/dist/mapCheck/rules/names.js +39 -0
- package/dist/mapCheck/rules/names.js.map +1 -0
- package/dist/mapCheck/rules/objects.d.ts +1 -1
- package/dist/mapCheck/rules/objects.js +56 -0
- package/dist/mapCheck/rules/objects.js.map +1 -0
- package/dist/mapCheck/rules/paths.js +32 -0
- package/dist/mapCheck/rules/paths.js.map +1 -0
- package/dist/mapCheck/rules/players.js +20 -0
- package/dist/mapCheck/rules/players.js.map +1 -0
- package/dist/mapCheck/rules/races.js +56 -0
- package/dist/mapCheck/rules/races.js.map +1 -0
- package/dist/mapCheck/rules/random.js +47 -0
- package/dist/mapCheck/rules/random.js.map +1 -0
- package/dist/mapCheck/rules/regions.js +30 -0
- package/dist/mapCheck/rules/regions.js.map +1 -0
- package/dist/mapCheck/rules/targets.js +31 -0
- package/dist/mapCheck/rules/targets.js.map +1 -0
- package/dist/mapCheck/rules/walking.js +45 -0
- package/dist/mapCheck/rules/walking.js.map +1 -0
- package/dist/mapCheck.d.ts +2 -2
- package/dist/mapCheck.js +63 -0
- package/dist/mapCheck.js.map +1 -0
- package/dist/mapFields.d.ts +25 -2
- package/dist/mapFields.js +134 -0
- package/dist/mapFields.js.map +1 -0
- package/dist/mapFile.js +176 -0
- package/dist/mapFile.js.map +1 -0
- package/dist/mapMeasure.d.ts +8 -0
- package/dist/mapMeasure.js +283 -0
- package/dist/mapMeasure.js.map +1 -0
- package/dist/mapMerge.d.ts +1 -0
- package/dist/mapMerge.js +172 -0
- package/dist/mapMerge.js.map +1 -0
- package/dist/mapObjectives.js +138 -0
- package/dist/mapObjectives.js.map +1 -0
- package/dist/mapObjects.js +80 -0
- package/dist/mapObjects.js.map +1 -0
- package/dist/math.d.ts +37 -7
- package/dist/math.js +155 -0
- package/dist/math.js.map +1 -0
- package/dist/messages.d.ts +71 -4
- package/dist/messages.js +29 -0
- package/dist/messages.js.map +1 -0
- package/dist/missionRules.d.ts +10 -2
- package/dist/{missionRules-C-bGxm0Q.js → missionRules.js} +30 -150
- package/dist/missionRules.js.map +1 -0
- package/dist/mods.d.ts +39 -20
- package/dist/mods.js +271 -0
- package/dist/mods.js.map +1 -0
- package/dist/moves.d.ts +87 -0
- package/dist/moves.js +539 -0
- package/dist/moves.js.map +1 -0
- package/dist/names.d.ts +7 -0
- package/dist/names.js +122 -0
- package/dist/names.js.map +1 -0
- package/dist/navigation/agentParams.js +14 -0
- package/dist/navigation/agentParams.js.map +1 -0
- package/dist/navigation/air.d.ts +7 -3
- package/dist/navigation/air.js +401 -0
- package/dist/navigation/air.js.map +1 -0
- package/dist/navigation/builds.d.ts +34 -2
- package/dist/navigation/builds.js +287 -0
- package/dist/navigation/builds.js.map +1 -0
- package/dist/navigation/groundAreas.d.ts +59 -1
- package/dist/navigation/groundAreas.js +407 -0
- package/dist/navigation/groundAreas.js.map +1 -0
- package/dist/navigation/navigation.d.ts +98 -9
- package/dist/navigation/recast.d.ts +194 -18
- package/dist/navigation/recast.js +1379 -0
- package/dist/navigation/recast.js.map +1 -0
- package/dist/navigation/sidePaths.d.ts +8 -3
- package/dist/navigation/sidePaths.js +241 -0
- package/dist/navigation/sidePaths.js.map +1 -0
- package/dist/navigation/sized.js +243 -0
- package/dist/navigation/sized.js.map +1 -0
- package/dist/nearest.js +32 -0
- package/dist/nearest.js.map +1 -0
- package/dist/noise.js +59 -0
- package/dist/noise.js.map +1 -0
- package/dist/objectives.d.ts +12 -3
- package/dist/objectives.js +144 -0
- package/dist/objectives.js.map +1 -0
- package/dist/orders/basic.d.ts +5 -5
- package/dist/orders/basic.js +417 -0
- package/dist/orders/basic.js.map +1 -0
- package/dist/orders/build.js +278 -0
- package/dist/orders/build.js.map +1 -0
- package/dist/orders/cast.d.ts +13 -3
- package/dist/orders/cast.js +558 -0
- package/dist/orders/cast.js.map +1 -0
- package/dist/orders/facts.js +16 -0
- package/dist/orders/facts.js.map +1 -0
- package/dist/orders/harvest.d.ts +21 -5
- package/dist/orders/harvest.js +540 -0
- package/dist/orders/harvest.js.map +1 -0
- package/dist/orders/idle.d.ts +3 -1
- package/dist/orders/idle.js +234 -0
- package/dist/orders/idle.js.map +1 -0
- package/dist/orders/items.js +92 -0
- package/dist/orders/items.js.map +1 -0
- package/dist/orders/queue.js +108 -0
- package/dist/orders/queue.js.map +1 -0
- package/dist/orders/waiting.d.ts +39 -0
- package/dist/orders/waiting.js +149 -0
- package/dist/orders/waiting.js.map +1 -0
- package/dist/pack.schema.js +13 -0
- package/dist/pack.schema.js.map +1 -0
- package/dist/pack.schema.json +7622 -841
- package/dist/packs/director/index.d.ts +13 -5
- package/dist/packs/director/index.js +204 -0
- package/dist/packs/director/index.js.map +1 -0
- package/dist/packs/heroes/campaign.d.ts +3 -2
- package/dist/packs/heroes/campaign.js +241 -0
- package/dist/packs/heroes/campaign.js.map +1 -0
- package/dist/packs/heroes/carry.d.ts +6 -0
- package/dist/packs/heroes/carry.js +112 -0
- package/dist/packs/heroes/carry.js.map +1 -0
- package/dist/packs/heroes/commands.js +115 -0
- package/dist/packs/heroes/commands.js.map +1 -0
- package/dist/packs/heroes/fallen.d.ts +43 -0
- package/dist/packs/heroes/fallen.js +61 -0
- package/dist/packs/heroes/fallen.js.map +1 -0
- package/dist/packs/heroes/hire.js +60 -0
- package/dist/packs/heroes/hire.js.map +1 -0
- package/dist/packs/heroes/index.d.ts +7 -6
- package/dist/packs/heroes/index.js +130 -0
- package/dist/packs/heroes/index.js.map +1 -0
- package/dist/packs/heroes/loop.d.ts +95 -0
- package/dist/packs/heroes/loop.js +250 -0
- package/dist/packs/heroes/loop.js.map +1 -0
- package/dist/packs/heroes/making.d.ts +6 -3
- package/dist/packs/heroes/making.js +55 -0
- package/dist/packs/heroes/making.js.map +1 -0
- package/dist/packs/heroes/respawn.d.ts +3 -3
- package/dist/packs/heroes/respawn.js +87 -0
- package/dist/packs/heroes/respawn.js.map +1 -0
- package/dist/packs/heroes/sets.d.ts +32 -0
- package/dist/packs/heroes/sets.js +51 -0
- package/dist/packs/heroes/sets.js.map +1 -0
- package/dist/packs/heroes/spells.d.ts +16 -0
- package/dist/packs/heroes/spells.js +269 -0
- package/dist/packs/heroes/spells.js.map +1 -0
- package/dist/packs/items/defaults.d.ts +43 -0
- package/dist/packs/items/defaults.js +1504 -0
- package/dist/packs/items/defaults.js.map +1 -0
- package/dist/packs/items/index.d.ts +27 -0
- package/dist/packs/items/index.js +214 -0
- package/dist/packs/items/index.js.map +1 -0
- package/dist/packs/moba/index.d.ts +3 -3
- package/dist/packs/moba/index.js +174 -0
- package/dist/packs/moba/index.js.map +1 -0
- package/dist/packs/pack.d.ts +15 -15
- package/dist/packs/pack.js +14 -0
- package/dist/packs/pack.js.map +1 -0
- package/dist/packs.d.ts +15 -7
- package/dist/packs.js +222 -0
- package/dist/packs.js.map +1 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +109 -0
- package/dist/paths.js.map +1 -0
- package/dist/plain.js +57 -0
- package/dist/plain.js.map +1 -0
- package/dist/player.d.ts +33 -10
- package/dist/player.js +159 -0
- package/dist/player.js.map +1 -0
- package/dist/portals.d.ts +1 -1
- package/dist/portals.js +194 -0
- package/dist/portals.js.map +1 -0
- package/dist/prefabs.d.ts +25 -2
- package/dist/prefabs.js +239 -0
- package/dist/prefabs.js.map +1 -0
- package/dist/presetBuildings.js +58 -0
- package/dist/presetBuildings.js.map +1 -0
- package/dist/presetFoliage.d.ts +27 -0
- package/dist/presetFoliage.js +98 -0
- package/dist/presetFoliage.js.map +1 -0
- package/dist/presetGround.js +65 -0
- package/dist/presetGround.js.map +1 -0
- package/dist/proxies.d.ts +12 -0
- package/dist/proxies.js +79 -0
- package/dist/proxies.js.map +1 -0
- package/dist/query.d.ts +56 -0
- package/dist/query.js +88 -0
- package/dist/query.js.map +1 -0
- package/dist/races.d.ts +15 -14
- package/dist/races.js +172 -0
- package/dist/races.js.map +1 -0
- package/dist/randomGroups.js +91 -0
- package/dist/randomGroups.js.map +1 -0
- package/dist/registry.d.ts +117 -53
- package/dist/registry.js +323 -0
- package/dist/registry.js.map +1 -0
- package/dist/remains.d.ts +11 -9
- package/dist/remains.js +188 -0
- package/dist/remains.js.map +1 -0
- package/dist/replication.d.ts +12 -7
- package/dist/replication.js +100 -0
- package/dist/replication.js.map +1 -0
- package/dist/rng.d.ts +2 -0
- package/dist/rng.js +40 -0
- package/dist/rng.js.map +1 -0
- package/dist/rules.d.ts +186 -99
- package/dist/rules.js +179 -0
- package/dist/rules.js.map +1 -0
- package/dist/rulesCheck.d.ts +5 -4
- package/dist/rulesCheck.js +693 -0
- package/dist/rulesCheck.js.map +1 -0
- package/dist/runes.d.ts +2 -2
- package/dist/runes.js +80 -0
- package/dist/runes.js.map +1 -0
- package/dist/save.d.ts +37 -12
- package/dist/save.js +532 -0
- package/dist/save.js.map +1 -0
- package/dist/scenario.d.ts +2 -2
- package/dist/scenario.js +99 -0
- package/dist/scenario.js.map +1 -0
- package/dist/scheduler.d.ts +3 -0
- package/dist/scheduler.js +156 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/score.d.ts +19 -19
- package/dist/score.js +709 -0
- package/dist/score.js.map +1 -0
- package/dist/sequences.d.ts +10 -6
- package/dist/sequences.js +206 -0
- package/dist/sequences.js.map +1 -0
- package/dist/server/doorman.js +154 -0
- package/dist/server/doorman.js.map +1 -0
- package/dist/server/gameServer.d.ts +16 -1
- package/dist/server/gameServer.js +643 -0
- package/dist/server/gameServer.js.map +1 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/lobby.js +199 -0
- package/dist/server/lobby.js.map +1 -0
- package/dist/server/log.js +18 -0
- package/dist/server/log.js.map +1 -0
- package/dist/server/match.js +267 -0
- package/dist/server/match.js.map +1 -0
- package/dist/server/mode.d.ts +18 -9
- package/dist/server/mode.js +220 -0
- package/dist/server/mode.js.map +1 -0
- package/dist/{navigation-worker.js → server/navigation-worker.js} +1 -1
- package/dist/server/navigation-worker.js.map +1 -0
- package/dist/server/navigation.js +39 -0
- package/dist/server/navigation.js.map +1 -0
- package/dist/server/ops.d.ts +4 -0
- package/dist/server/ops.js +137 -0
- package/dist/server/ops.js.map +1 -0
- package/dist/server/packs.d.ts +3 -3
- package/dist/server/packs.js +32 -0
- package/dist/server/packs.js.map +1 -0
- package/dist/server/ratings.d.ts +5 -0
- package/dist/server/ratings.js +104 -0
- package/dist/server/ratings.js.map +1 -0
- package/dist/server/relay.d.ts +12 -2
- package/dist/server/relay.js +144 -0
- package/dist/server/relay.js.map +1 -0
- package/dist/server/rooms.d.ts +9 -2
- package/dist/server/rooms.js +442 -0
- package/dist/server/rooms.js.map +1 -0
- package/dist/server/seat.js +116 -0
- package/dist/server/seat.js.map +1 -0
- package/dist/server/seatSettings.d.ts +3 -3
- package/dist/server/seatSettings.js +63 -0
- package/dist/server/seatSettings.js.map +1 -0
- package/dist/server/secret.js +13 -0
- package/dist/server/secret.js.map +1 -0
- package/dist/server/start.d.ts +13 -6
- package/dist/server/start.js +76 -0
- package/dist/server/start.js.map +1 -0
- package/dist/server/websocket.d.ts +9 -1
- package/dist/server/websocket.js +306 -0
- package/dist/server/websocket.js.map +1 -0
- package/dist/server.js +15 -2861
- package/dist/session/can.js +26 -0
- package/dist/session/can.js.map +1 -0
- package/dist/session/commandReplay.d.ts +8 -2
- package/dist/session/commandReplay.js +293 -0
- package/dist/session/commandReplay.js.map +1 -0
- package/dist/session/debug.js +151 -0
- package/dist/session/debug.js.map +1 -0
- package/dist/session/delay.js +62 -0
- package/dist/session/delay.js.map +1 -0
- package/dist/session/delta.d.ts +14 -1
- package/dist/session/delta.js +407 -0
- package/dist/session/delta.js.map +1 -0
- package/dist/session/entityState.js +415 -0
- package/dist/session/entityState.js.map +1 -0
- package/dist/session/local.d.ts +47 -5
- package/dist/session/local.js +670 -0
- package/dist/session/local.js.map +1 -0
- package/dist/session/memory.d.ts +20 -2
- package/dist/session/memory.js +131 -0
- package/dist/session/memory.js.map +1 -0
- package/dist/session/protocol.d.ts +325 -31
- package/dist/session/protocol.js +55 -0
- package/dist/session/protocol.js.map +1 -0
- package/dist/session/remote.d.ts +74 -12
- package/dist/session/remote.js +1394 -0
- package/dist/session/remote.js.map +1 -0
- package/dist/session/replay.d.ts +11 -2
- package/dist/session/replay.js +160 -0
- package/dist/session/replay.js.map +1 -0
- package/dist/session/seed.js +12 -0
- package/dist/session/seed.js.map +1 -0
- package/dist/session/share.d.ts +5 -1
- package/dist/session/share.js +94 -0
- package/dist/session/share.js.map +1 -0
- package/dist/session/socket.d.ts +1 -1
- package/dist/session/socket.js +29 -0
- package/dist/session/socket.js.map +1 -0
- package/dist/session/wire.d.ts +5 -0
- package/dist/session/wire.js +84 -0
- package/dist/session/wire.js.map +1 -0
- package/dist/session/worker.d.ts +77 -12
- package/dist/session/worker.js +499 -0
- package/dist/session/worker.js.map +1 -0
- package/dist/shared.d.ts +31 -5
- package/dist/shared.js +166 -0
- package/dist/shared.js.map +1 -0
- package/dist/sides.d.ts +9 -3
- package/dist/sides.js +149 -0
- package/dist/sides.js.map +1 -0
- package/dist/sight.js +85 -0
- package/dist/sight.js.map +1 -0
- package/dist/simulate-worker.js +1 -1
- package/dist/simulate.d.ts +12 -8
- package/dist/{simulate-oXBiw1Ff.js → simulate.js} +10 -11
- package/dist/simulate.js.map +1 -0
- package/dist/smart.js +77 -0
- package/dist/smart.js.map +1 -0
- package/dist/spatial.d.ts +32 -16
- package/dist/spatial.js +234 -0
- package/dist/spatial.js.map +1 -0
- package/dist/spawning.d.ts +1 -0
- package/dist/spawning.js +224 -0
- package/dist/spawning.js.map +1 -0
- package/dist/stances.d.ts +6 -3
- package/dist/stances.js +169 -0
- package/dist/stances.js.map +1 -0
- package/dist/start.d.ts +10 -10
- package/dist/start.js +75 -0
- package/dist/start.js.map +1 -0
- package/dist/stats.d.ts +42 -16
- package/dist/stats.js +290 -0
- package/dist/stats.js.map +1 -0
- package/dist/summoned.d.ts +52 -0
- package/dist/summoned.js +38 -0
- package/dist/summoned.js.map +1 -0
- package/dist/summons.d.ts +10 -13
- package/dist/summons.js +271 -0
- package/dist/summons.js.map +1 -0
- package/dist/systems/abilities.js +224 -0
- package/dist/systems/abilities.js.map +1 -0
- package/dist/systems/combat.d.ts +35 -1
- package/dist/systems/combat.js +677 -0
- package/dist/systems/combat.js.map +1 -0
- package/dist/systems/combatTold.d.ts +3 -1
- package/dist/systems/combatTold.js +85 -0
- package/dist/systems/combatTold.js.map +1 -0
- package/dist/systems/construction.js +80 -0
- package/dist/systems/construction.js.map +1 -0
- package/dist/systems/economy.d.ts +0 -6
- package/dist/systems/economy.js +128 -0
- package/dist/systems/economy.js.map +1 -0
- package/dist/systems/fog.d.ts +1 -1
- package/dist/systems/fog.js +120 -0
- package/dist/systems/fog.js.map +1 -0
- package/dist/systems/life.js +32 -0
- package/dist/systems/life.js.map +1 -0
- package/dist/systems/movement.d.ts +24 -12
- package/dist/systems/movement.js +1247 -0
- package/dist/systems/movement.js.map +1 -0
- package/dist/systems/orders.js +64 -0
- package/dist/systems/orders.js.map +1 -0
- package/dist/systems/production.js +322 -0
- package/dist/systems/production.js.map +1 -0
- package/dist/systems/productionQueue.js +120 -0
- package/dist/systems/productionQueue.js.map +1 -0
- package/dist/systems/victory.d.ts +1 -1
- package/dist/systems/victory.js +56 -0
- package/dist/systems/victory.js.map +1 -0
- package/dist/talk.d.ts +6 -6
- package/dist/talk.js +153 -0
- package/dist/talk.js.map +1 -0
- package/dist/targeting.d.ts +142 -0
- package/dist/targeting.js +49 -0
- package/dist/targeting.js.map +1 -0
- package/dist/tech.d.ts +20 -3
- package/dist/tech.js +239 -0
- package/dist/tech.js.map +1 -0
- package/dist/techTree.d.ts +7 -0
- package/dist/techTree.js +90 -0
- package/dist/techTree.js.map +1 -0
- package/dist/terrain.d.ts +59 -7
- package/dist/terrain.js +389 -0
- package/dist/terrain.js.map +1 -0
- package/dist/terrainSave.d.ts +6 -0
- package/dist/terrainSave.js +74 -0
- package/dist/terrainSave.js.map +1 -0
- package/dist/testing.d.ts +8 -7
- package/dist/testing.js +13 -739
- package/dist/testing.js.map +1 -1
- package/dist/tiles.d.ts +1 -1
- package/dist/tiles.js +43 -0
- package/dist/tiles.js.map +1 -0
- package/dist/timers.d.ts +29 -4
- package/dist/timers.js +242 -0
- package/dist/timers.js.map +1 -0
- package/dist/tomes.d.ts +4 -4
- package/dist/tomes.js +94 -0
- package/dist/tomes.js.map +1 -0
- package/dist/trade.js +103 -0
- package/dist/trade.js.map +1 -0
- package/dist/transport.d.ts +6 -1
- package/dist/transport.js +281 -0
- package/dist/transport.js.map +1 -0
- package/dist/tunables.d.ts +10 -3
- package/dist/tunables.js +56 -0
- package/dist/tunables.js.map +1 -0
- package/dist/unitResources.d.ts +67 -0
- package/dist/unitResources.js +266 -0
- package/dist/unitResources.js.map +1 -0
- package/dist/upkeep.d.ts +6 -1
- package/dist/upkeep.js +26 -0
- package/dist/upkeep.js.map +1 -0
- package/dist/veterancy.d.ts +8 -3
- package/dist/veterancy.js +131 -0
- package/dist/veterancy.js.map +1 -0
- package/dist/vision.d.ts +36 -0
- package/dist/vision.js +133 -0
- package/dist/vision.js.map +1 -0
- package/dist/walls.d.ts +1 -1
- package/dist/walls.js +278 -0
- package/dist/walls.js.map +1 -0
- package/dist/world.d.ts +606 -88
- package/dist/world.js +1263 -0
- package/dist/world.js.map +1 -0
- package/package.json +2 -2
- package/dist/ai.js.map +0 -1
- package/dist/attributes.d.ts +0 -66
- package/dist/brain-BvVdUhX8.js +0 -7659
- package/dist/brain-BvVdUhX8.js.map +0 -1
- package/dist/builds-C6LfgvEU.js +0 -460
- package/dist/builds-C6LfgvEU.js.map +0 -1
- package/dist/coreComponents-syuh2fPG.js +0 -5149
- package/dist/coreComponents-syuh2fPG.js.map +0 -1
- package/dist/debugCommands-CzjrSm66.js +0 -14739
- package/dist/debugCommands-CzjrSm66.js.map +0 -1
- package/dist/determinism-BCymQwiE.js +0 -518
- package/dist/determinism-BCymQwiE.js.map +0 -1
- package/dist/game-DiJEN-Ks.js +0 -5800
- package/dist/game-DiJEN-Ks.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/inspect-D9dWHmZ0.js.map +0 -1
- package/dist/local-Dkovq42k.js +0 -1379
- package/dist/local-Dkovq42k.js.map +0 -1
- package/dist/missionRules-C-bGxm0Q.js.map +0 -1
- package/dist/navigation-worker.js.map +0 -1
- package/dist/packs-g5thvmGn.js +0 -3199
- package/dist/packs-g5thvmGn.js.map +0 -1
- package/dist/profiles-B3NS9jwk.js.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/simulate-oXBiw1Ff.js.map +0 -1
package/dist/packs-g5thvmGn.js
DELETED
|
@@ -1,3199 +0,0 @@
|
|
|
1
|
-
import { $ as MissingGround, $d as atan, Cn as withData, F as tableItems, Fc as availableTo, Hi as footprintCell, J as objectFields, Kc as toMessage, L as TARGET_FIELDS, N as groupChoices, Q as targetName, Qt as createMap, Qu as worldFailed, R as campFields, U as mapFields, Vi as BuildGrid, X as reachedByDefault, Y as playerFields, Z as regionFields, _n as redefineIn, af as sin, bn as restoreRedefinitions, ci as siteOf, ct as savedGround, ef as atan2, et as applyTerrainPatch, ft as terrainPatchProblem, hn as mergeValues, ht as saveTerrain, if as hypot, ii as placementCells, jt as Rng, kn as errorFields, mt as loadTerrain, ot as groundOf, pt as withChanges, st as isGroundChanges, tf as cos, tn as DATA_KINDS, vn as redefinitions, xd as footprintOf, xt as mapTerrain } from "./debugCommands-CzjrSm66.js";
|
|
2
|
-
import { P as groundKnownTo, X as hasGroundSpeeds, Y as groundSpeedOn, p as AirNavigation, r as createRegistry, s as maxPlayersOf } from "./game-DiJEN-Ks.js";
|
|
3
|
-
import { Sn as shapeBounds, gt as RecastNavigation } from "./coreComponents-syuh2fPG.js";
|
|
4
|
-
import { _ as readMessage, a as RemoteDebug, c as bytesByKey, g as protocolMismatch, h as isFiniteNumber, i as LocalDebug, l as diff, n as endSharedCaptures, o as askDebug, r as shareCaptures, s as apply, t as LocalSession, u as quantize } from "./local-Dkovq42k.js";
|
|
5
|
-
//#region src/session/wire.ts
|
|
6
|
-
/** The game's own messages to a player: the rest are a server's or a relay's (`ServerToPlayer`) */
|
|
7
|
-
var GAME_TYPES = /* @__PURE__ */ new Set([
|
|
8
|
-
"welcome",
|
|
9
|
-
"pong",
|
|
10
|
-
"ground",
|
|
11
|
-
"failed",
|
|
12
|
-
"delta",
|
|
13
|
-
"debug"
|
|
14
|
-
]);
|
|
15
|
-
/**
|
|
16
|
-
* Whether a message to a player is a server's or a relay's, not the game's
|
|
17
|
-
*
|
|
18
|
-
* @internal
|
|
19
|
-
*/
|
|
20
|
-
var isServerMessage = (message) => !GAME_TYPES.has(message.type);
|
|
21
|
-
/** Every type a player is sent, each said once: what is not one of them is not read */
|
|
22
|
-
var HOST_TYPES = {
|
|
23
|
-
welcome: true,
|
|
24
|
-
pong: true,
|
|
25
|
-
ground: true,
|
|
26
|
-
failed: true,
|
|
27
|
-
delta: true,
|
|
28
|
-
debug: true,
|
|
29
|
-
lobby: true,
|
|
30
|
-
seated: true,
|
|
31
|
-
watching: true,
|
|
32
|
-
full: true,
|
|
33
|
-
refused: true,
|
|
34
|
-
lobbyChat: true,
|
|
35
|
-
paused: true,
|
|
36
|
-
result: true,
|
|
37
|
-
waiting: true,
|
|
38
|
-
ended: true
|
|
39
|
-
};
|
|
40
|
-
/** Every type a host or a server is sent */
|
|
41
|
-
var PLAYER_TYPES = {
|
|
42
|
-
command: true,
|
|
43
|
-
ack: true,
|
|
44
|
-
ping: true,
|
|
45
|
-
ground: true,
|
|
46
|
-
debug: true,
|
|
47
|
-
join: true,
|
|
48
|
-
start: true,
|
|
49
|
-
sit: true,
|
|
50
|
-
ready: true,
|
|
51
|
-
name: true,
|
|
52
|
-
resume: true,
|
|
53
|
-
setting: true,
|
|
54
|
-
seatSetting: true,
|
|
55
|
-
admin: true,
|
|
56
|
-
lobbyChat: true,
|
|
57
|
-
kick: true,
|
|
58
|
-
watch: true
|
|
59
|
-
};
|
|
60
|
-
/** A message as text, an object of a known type, or nothing: not JSON, not an object, a type that is not one of `types` */
|
|
61
|
-
function read(text, types) {
|
|
62
|
-
let message;
|
|
63
|
-
try {
|
|
64
|
-
message = JSON.parse(text);
|
|
65
|
-
} catch {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (typeof message !== "object" || message === null || Array.isArray(message)) return void 0;
|
|
69
|
-
const { type } = message;
|
|
70
|
-
return typeof type === "string" && Object.hasOwn(types, type) ? message : void 0;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* What a host or a server sent, as text: one of its messages, or nothing for what is not (not JSON, not an object of a
|
|
74
|
-
* type a player is sent). Its fields are its receiver's to check
|
|
75
|
-
*
|
|
76
|
-
* @internal
|
|
77
|
-
*/
|
|
78
|
-
var readHostMessage = (text) => read(text, HOST_TYPES);
|
|
79
|
-
/**
|
|
80
|
-
* What a player sent, as text: one of its messages, or nothing for what is not
|
|
81
|
-
*
|
|
82
|
-
* @internal
|
|
83
|
-
*/
|
|
84
|
-
var readPlayerMessage = (text) => read(text, PLAYER_TYPES);
|
|
85
|
-
//#endregion
|
|
86
|
-
//#region src/session/remote.ts
|
|
87
|
-
/**
|
|
88
|
-
* Two ends of a connection in the page, `[client, host]`: everything sent is text, delivered after the latency the
|
|
89
|
-
* options give. The whole networked way, in one page and in tests
|
|
90
|
-
*
|
|
91
|
-
* @beta
|
|
92
|
-
*/
|
|
93
|
-
function loopback(options = {}) {
|
|
94
|
-
const { latency = 0, jitter = 0, loss = 0, random = Math.random } = options;
|
|
95
|
-
/** One end: what it is handed to deliver, and the transport its side holds */
|
|
96
|
-
class End {
|
|
97
|
-
lossy;
|
|
98
|
-
peer;
|
|
99
|
-
bytesSent = 0;
|
|
100
|
-
messagesSent = 0;
|
|
101
|
-
handler = () => {};
|
|
102
|
-
closed = () => {};
|
|
103
|
-
open = true;
|
|
104
|
-
/** When the last message sent is due: none overtakes the one before */
|
|
105
|
-
last = 0;
|
|
106
|
-
constructor(lossy) {
|
|
107
|
-
this.lossy = lossy;
|
|
108
|
-
}
|
|
109
|
-
send(message) {
|
|
110
|
-
if (!this.open) return;
|
|
111
|
-
this.bytesSent += message.length;
|
|
112
|
-
this.messagesSent++;
|
|
113
|
-
if (this.lossy && loss > 0 && message.startsWith("{\"type\":\"delta\"") && random() < loss) return;
|
|
114
|
-
const due = Math.max(performance.now() + latency + random() * jitter, this.last);
|
|
115
|
-
this.last = due;
|
|
116
|
-
const { peer } = this;
|
|
117
|
-
setTimeout(() => peer.deliver(message), Math.max(due - performance.now(), 0));
|
|
118
|
-
}
|
|
119
|
-
deliver(message) {
|
|
120
|
-
if (this.open) this.handler(message);
|
|
121
|
-
}
|
|
122
|
-
onMessage(handler) {
|
|
123
|
-
this.handler = handler;
|
|
124
|
-
}
|
|
125
|
-
onClose(handler) {
|
|
126
|
-
this.closed = handler;
|
|
127
|
-
}
|
|
128
|
-
close() {
|
|
129
|
-
this.shut();
|
|
130
|
-
const { peer } = this;
|
|
131
|
-
setTimeout(() => peer.shut(), Math.max(this.last - performance.now(), latency, 0));
|
|
132
|
-
}
|
|
133
|
-
shut() {
|
|
134
|
-
if (!this.open) return;
|
|
135
|
-
this.open = false;
|
|
136
|
-
this.closed();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const client = new End(false);
|
|
140
|
-
const host = new End(true);
|
|
141
|
-
client.peer = host;
|
|
142
|
-
host.peer = client;
|
|
143
|
-
return [client, host];
|
|
144
|
-
}
|
|
145
|
-
/** Whether what a player sent as a command is one to look at: an object with a type. Its fields are its definition's to check */
|
|
146
|
-
var isCommand = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string";
|
|
147
|
-
/**
|
|
148
|
-
* Runs of equal values: how many 0s, then how many 1s, and so on
|
|
149
|
-
*
|
|
150
|
-
* @internal
|
|
151
|
-
*/
|
|
152
|
-
function toRuns(cells) {
|
|
153
|
-
const runs = [];
|
|
154
|
-
let value = 0;
|
|
155
|
-
let count = 0;
|
|
156
|
-
for (let i = 0; i < cells.length; i++) {
|
|
157
|
-
const cell = cells[i] ? 1 : 0;
|
|
158
|
-
if (cell === value) count++;
|
|
159
|
-
else {
|
|
160
|
-
runs.push(count);
|
|
161
|
-
value = cell;
|
|
162
|
-
count = 1;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
runs.push(count);
|
|
166
|
-
return runs;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Cells back from `toRuns`, into `out`
|
|
170
|
-
*
|
|
171
|
-
* @internal
|
|
172
|
-
*/
|
|
173
|
-
function fromRuns(runs, out) {
|
|
174
|
-
let at = 0;
|
|
175
|
-
let value = 0;
|
|
176
|
-
for (const run of runs) {
|
|
177
|
-
out.fill(value, at, Math.min(at + run, out.length));
|
|
178
|
-
at += run;
|
|
179
|
-
value ^= 1;
|
|
180
|
-
}
|
|
181
|
-
return out;
|
|
182
|
-
}
|
|
183
|
-
/** The fog for the wire: nothing of its cells when the whole map is lit, which they say nothing about then */
|
|
184
|
-
var fogMessage = (fog) => fog.revealed ? {
|
|
185
|
-
revealed: true,
|
|
186
|
-
visible: [],
|
|
187
|
-
explored: []
|
|
188
|
-
} : {
|
|
189
|
-
revealed: false,
|
|
190
|
-
visible: toRuns(fog.visible),
|
|
191
|
-
explored: toRuns(fog.explored)
|
|
192
|
-
};
|
|
193
|
-
/** The runs of a team's fog, by its cells, as they were at a version of the fog: the seats of a team share them */
|
|
194
|
-
var fogRuns = /* @__PURE__ */ new WeakMap();
|
|
195
|
-
var revealedFog = {
|
|
196
|
-
revealed: true,
|
|
197
|
-
visible: [],
|
|
198
|
-
explored: []
|
|
199
|
-
};
|
|
200
|
-
/** The fog for the wire, its runs made once for every seat of a team while it stays as it is */
|
|
201
|
-
function fogMessageOf(fog) {
|
|
202
|
-
if (fog.revealed) return revealedFog;
|
|
203
|
-
const known = fogRuns.get(fog.visible);
|
|
204
|
-
if (known && known.version === fog.version) return known.message;
|
|
205
|
-
const message = fogMessage(fog);
|
|
206
|
-
fogRuns.set(fog.visible, {
|
|
207
|
-
version: fog.version,
|
|
208
|
-
message
|
|
209
|
-
});
|
|
210
|
-
return message;
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* The entities of a frame as the wire has them (`quantize`), one list between the seats of a round told the very same
|
|
214
|
-
* (the seats of a side, the observers: what they are told of an entity is one object between them, `LocalSession`), so
|
|
215
|
-
* that it is rounded once and a diff of the same two lists is worked out once (`diff`). `made` is this round's so far
|
|
216
|
-
*/
|
|
217
|
-
function quantizedList(entities, made) {
|
|
218
|
-
for (const { told, sent } of made) {
|
|
219
|
-
if (told.length !== entities.length) continue;
|
|
220
|
-
let alike = true;
|
|
221
|
-
for (let i = 0; i < told.length; i++) if (told[i] !== entities[i]) {
|
|
222
|
-
alike = false;
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
if (alike) return sent;
|
|
226
|
-
}
|
|
227
|
-
const sent = entities.map(quantize);
|
|
228
|
-
made.push({
|
|
229
|
-
told: entities,
|
|
230
|
-
sent
|
|
231
|
-
});
|
|
232
|
-
return sent;
|
|
233
|
-
}
|
|
234
|
-
/** Milliseconds between two pings, to measure the time to the host and back */
|
|
235
|
-
var PING_EVERY = 2e3;
|
|
236
|
-
/** Seconds of jitter the interpolation buffer makes room for, at most */
|
|
237
|
-
var MAX_JITTER = .2;
|
|
238
|
-
var emptyTiming = (since, byKey = false) => ({
|
|
239
|
-
since,
|
|
240
|
-
ticks: 0,
|
|
241
|
-
tickTime: 0,
|
|
242
|
-
tickMax: 0,
|
|
243
|
-
sends: 0,
|
|
244
|
-
sendTime: 0,
|
|
245
|
-
sendMax: 0,
|
|
246
|
-
parts: {
|
|
247
|
-
capture: 0,
|
|
248
|
-
quantize: 0,
|
|
249
|
-
fog: 0,
|
|
250
|
-
diff: 0,
|
|
251
|
-
stringify: 0
|
|
252
|
-
},
|
|
253
|
-
bytes: 0,
|
|
254
|
-
...byKey && { keys: {} }
|
|
255
|
-
});
|
|
256
|
-
/** Frames kept on either side, at most: more than a second and a half of ticks a player has not said it got */
|
|
257
|
-
var KEPT_FRAMES = 64;
|
|
258
|
-
/**
|
|
259
|
-
* Bytes of the frames kept for one player, at most (`SessionHostOptions.keptBytes`): 8 MB, some thirty frames of a
|
|
260
|
-
* thousand units (a frame of them is about 250 KB). A player who has said nothing for longer is sent everything again
|
|
261
|
-
*/
|
|
262
|
-
var KEPT_BYTES = 8388608;
|
|
263
|
-
/** What each list of entities a frame keeps takes as the wire writes it: one list between the seats of a side (`quantizedList`) */
|
|
264
|
-
var listBytes = /* @__PURE__ */ new WeakMap();
|
|
265
|
-
/** Entities of a list written out to tell what the list takes: the rest are taken to be as big */
|
|
266
|
-
var SAMPLED = 8;
|
|
267
|
-
/**
|
|
268
|
-
* What a frame kept for a player takes, about: its entities as the wire writes them (some of them written out, the rest
|
|
269
|
-
* taken to be as big: writing out all of a thousand units' at every send costs more than the send itself), its fog,
|
|
270
|
-
* its players
|
|
271
|
-
*/
|
|
272
|
-
function frameBytes(frame) {
|
|
273
|
-
const { entities, players } = frame.snapshot;
|
|
274
|
-
let bytes = listBytes.get(entities);
|
|
275
|
-
if (bytes === void 0) {
|
|
276
|
-
const sampled = Math.min(entities.length, SAMPLED);
|
|
277
|
-
let written = 0;
|
|
278
|
-
for (let i = 0; i < sampled; i++) written += JSON.stringify(entities[Math.floor(i * entities.length / sampled)]).length;
|
|
279
|
-
listBytes.set(entities, bytes = sampled ? Math.round(written / sampled * entities.length) : 0);
|
|
280
|
-
}
|
|
281
|
-
let fog = listBytes.get(frame.fog);
|
|
282
|
-
if (fog === void 0) listBytes.set(frame.fog, fog = JSON.stringify(frame.fog).length);
|
|
283
|
-
let told = listBytes.get(players);
|
|
284
|
-
if (told === void 0) listBytes.set(players, told = JSON.stringify(players).length);
|
|
285
|
-
return bytes + fog + told;
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Snapshots waiting for the screen, at most (`RemoteSession.update`): three seconds of them at 20 a second. A page
|
|
289
|
-
* whose screen stops asking (a tab in the background) folds the oldest into the next, their events kept, rather than
|
|
290
|
-
* keeping every one for as long as it is away
|
|
291
|
-
*/
|
|
292
|
-
var KEPT_SNAPSHOTS = 64;
|
|
293
|
-
/** Events the folded snapshots carry together, at most: the newest kept. What is the screen's only (the state is in the snapshot) */
|
|
294
|
-
var KEPT_SCREEN_EVENTS = 2e3;
|
|
295
|
-
/**
|
|
296
|
-
* Runs a world for players connected to it over transports: the server's job, done anywhere. It steps the world by
|
|
297
|
-
* the clock (`start`) or by hand (`update`), and at most `sendRate` times a second sends every player what changed in
|
|
298
|
-
* what they may know (`Delta`), as their `LocalSession` seat sees it: against the last tick they said they got, with
|
|
299
|
-
* the events they have not. Their commands come in as they arrive, and go in at the next tick
|
|
300
|
-
*
|
|
301
|
-
* @beta
|
|
302
|
-
*/
|
|
303
|
-
var SessionHost = class {
|
|
304
|
-
world;
|
|
305
|
-
seats = /* @__PURE__ */ new Set();
|
|
306
|
-
accumulator = 0;
|
|
307
|
-
sinceSent = Infinity;
|
|
308
|
-
timer;
|
|
309
|
-
last = 0;
|
|
310
|
-
sendEvery;
|
|
311
|
-
/** Simulation speed multiplier, 0 pauses */
|
|
312
|
-
speed = 1;
|
|
313
|
-
constructor(world, options = {}) {
|
|
314
|
-
this.world = world;
|
|
315
|
-
this.sendEvery = 1 / (options.sendRate ?? 20);
|
|
316
|
-
this.maxTicks = options.maxTicksPerUpdate ?? 8;
|
|
317
|
-
this.onSlowTick = options.onSlowTick;
|
|
318
|
-
this.ownsWorld = options.ownsWorld ?? false;
|
|
319
|
-
this.dropSilentAfter = options.dropSilentAfter ?? 15;
|
|
320
|
-
this.keptEvents = options.keptEvents ?? 600;
|
|
321
|
-
this.keptFrames = Math.max(2, options.keptFrames ?? KEPT_FRAMES);
|
|
322
|
-
this.keptBytes = Math.max(1, options.keptBytes ?? KEPT_BYTES);
|
|
323
|
-
this.groundPart = Math.max(1, options.groundPart ?? GROUND_PART);
|
|
324
|
-
this.byKey = options.bytesByKey ?? false;
|
|
325
|
-
this.timing = emptyTiming(performance.now(), this.byKey);
|
|
326
|
-
this.onError = options.onError;
|
|
327
|
-
this.stopFailed = world.events.on(worldFailed, (failure) => {
|
|
328
|
-
for (const seat of this.seats) this.post(seat, {
|
|
329
|
-
type: "failed",
|
|
330
|
-
message: failure.message
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
onError;
|
|
335
|
-
stopFailed;
|
|
336
|
-
dropSilentAfter;
|
|
337
|
-
keptFrames;
|
|
338
|
-
keptBytes;
|
|
339
|
-
groundPart;
|
|
340
|
-
keptEvents;
|
|
341
|
-
/** Whether the characters sent are counted by what they carry (`bytesByKey`) */
|
|
342
|
-
byKey;
|
|
343
|
-
ownsWorld;
|
|
344
|
-
maxTicks;
|
|
345
|
-
onSlowTick;
|
|
346
|
-
/**
|
|
347
|
-
* Seats a player at the world, on the host's end of a transport. The welcome goes out at once: everything the player
|
|
348
|
-
* may know, also to one who comes back. `readOnly` for who only watches: their commands go nowhere but their lines
|
|
349
|
-
* of chat and their pings, which go to who watches alone (`observers`, said by `name`); and with `observer` they see
|
|
350
|
-
* everything, from `player`'s side. `allow` decides on each command before it goes in: a limit of how many, a game
|
|
351
|
-
* paused. One it lets go is answered, refused (the `command` event of its `seq`): with what it gives for why, or
|
|
352
|
-
* `cannotDoThat` for `false`
|
|
353
|
-
*/
|
|
354
|
-
connect(player, transport, options = {}) {
|
|
355
|
-
const session = new LocalSession(this.world, player, { observer: options.observer });
|
|
356
|
-
const seat = {
|
|
357
|
-
transport,
|
|
358
|
-
session,
|
|
359
|
-
frames: /* @__PURE__ */ new Map(),
|
|
360
|
-
frameSizes: /* @__PURE__ */ new Map(),
|
|
361
|
-
keptBytes: 0,
|
|
362
|
-
acked: -1,
|
|
363
|
-
heardUpTo: -1,
|
|
364
|
-
events: [],
|
|
365
|
-
eventsMade: 0,
|
|
366
|
-
sentAt: /* @__PURE__ */ new Map(),
|
|
367
|
-
roundTrip: 100,
|
|
368
|
-
heard: this.world.tick,
|
|
369
|
-
answers: [],
|
|
370
|
-
groundSent: /* @__PURE__ */ new Set()
|
|
371
|
-
};
|
|
372
|
-
this.seats.add(seat);
|
|
373
|
-
transport.onMessage((text) => {
|
|
374
|
-
const message = readMessage(text);
|
|
375
|
-
if (!message) return;
|
|
376
|
-
if (message.type === "ack") {
|
|
377
|
-
if (isFiniteNumber(message.tick)) this.acknowledge(seat, message.tick, isFiniteNumber(message.events) ? message.events : void 0);
|
|
378
|
-
} else if (message.type === "ping") {
|
|
379
|
-
if (isFiniteNumber(message.at)) this.post(seat, {
|
|
380
|
-
type: "pong",
|
|
381
|
-
at: message.at
|
|
382
|
-
});
|
|
383
|
-
} else if (message.type === "ground") {
|
|
384
|
-
const parts = this.groundParts();
|
|
385
|
-
const { part } = message;
|
|
386
|
-
if (parts && Number.isInteger(part) && part >= 0 && part < parts.length && !seat.groundSent.has(part)) {
|
|
387
|
-
seat.groundSent.add(part);
|
|
388
|
-
this.post(seat, {
|
|
389
|
-
type: "ground",
|
|
390
|
-
part,
|
|
391
|
-
parts: parts.length,
|
|
392
|
-
text: parts[part]
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
} else if (message.type === "debug") {
|
|
396
|
-
if (!this.world.debugPlayers.has(player)) return;
|
|
397
|
-
seat.debug ??= {
|
|
398
|
-
view: new LocalDebug(this.world),
|
|
399
|
-
sentAt: -Infinity
|
|
400
|
-
};
|
|
401
|
-
askDebug(seat.debug.view, message);
|
|
402
|
-
seat.debug.sentAt = -Infinity;
|
|
403
|
-
} else if (message.type === "command" && options.readOnly) {
|
|
404
|
-
const { command } = message;
|
|
405
|
-
if (!isCommand(command) || command.type !== "chat" && command.type !== "ping") return;
|
|
406
|
-
session.send({
|
|
407
|
-
...command,
|
|
408
|
-
to: "observers",
|
|
409
|
-
by: options.name ?? "A watcher"
|
|
410
|
-
});
|
|
411
|
-
this.heardOutside = true;
|
|
412
|
-
} else if (message.type === "command") {
|
|
413
|
-
const allowed = options.allow?.() ?? true;
|
|
414
|
-
const seq = isFiniteNumber(message.seq) ? message.seq : void 0;
|
|
415
|
-
if (!isCommand(message.command)) return;
|
|
416
|
-
if ((message.command.type === "chat" || message.command.type === "ping") && message.command.to === "observers") return;
|
|
417
|
-
if (allowed === true) {
|
|
418
|
-
session.send(message.command, seq);
|
|
419
|
-
if (this.world.registry.command(message.command.type)?.outsideTick) this.heardOutside = true;
|
|
420
|
-
} else if (seq !== void 0) seat.answers.push({
|
|
421
|
-
type: "command",
|
|
422
|
-
seq,
|
|
423
|
-
refusal: allowed ? toMessage(allowed) : { key: "cannotDoThat" }
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
transport.onClose(() => this.leave(seat));
|
|
428
|
-
const { registry } = this.world;
|
|
429
|
-
this.post(seat, {
|
|
430
|
-
type: "welcome",
|
|
431
|
-
protocol: 13,
|
|
432
|
-
player,
|
|
433
|
-
tickRate: this.world.tickRate,
|
|
434
|
-
game: registry.game,
|
|
435
|
-
hash: registry.dataHash,
|
|
436
|
-
terrain: savedGround(groundKnownTo(this.world, options.observer ? void 0 : this.world.player(player).team), this.world.ground, this.world.registry.module?.maps, this.world.registry.module ?? {}),
|
|
437
|
-
...this.world.map.environment && { environment: this.world.map.environment },
|
|
438
|
-
...this.world.map.doodads.length > 0 && { doodads: [...this.world.map.doodads] },
|
|
439
|
-
...this.world.map.regions.length > 0 && { regions: [...this.world.map.regions] },
|
|
440
|
-
...this.world.map.cameras.length > 0 && { cameras: [...this.world.map.cameras] },
|
|
441
|
-
...this.world.map.paths.length > 0 && { paths: [...this.world.map.paths] },
|
|
442
|
-
...this.world.states.has(redefinitions) && { redefinitions: structuredClone(this.world.state(redefinitions)) },
|
|
443
|
-
first: this.deltaFor(seat),
|
|
444
|
-
...options.observer ? { observer: true } : {},
|
|
445
|
-
...this.world.debugPlayers.has(player) && { debug: true }
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
/** The ground the world started as, whole, in parts small enough to go one at a time: made the first time one is asked for */
|
|
449
|
-
groundParts() {
|
|
450
|
-
const base = this.world.ground;
|
|
451
|
-
if (!base) return void 0;
|
|
452
|
-
if (!this.ground) {
|
|
453
|
-
const text = JSON.stringify(saveTerrain(base.terrain));
|
|
454
|
-
this.ground = [];
|
|
455
|
-
for (let i = 0; i < text.length; i += this.groundPart) this.ground.push(text.slice(i, i + this.groundPart));
|
|
456
|
-
}
|
|
457
|
-
return this.ground;
|
|
458
|
-
}
|
|
459
|
-
ground;
|
|
460
|
-
/** The player has the frame of `tick`: what comes next is made against it, and what it has is not sent again */
|
|
461
|
-
acknowledge(seat, tick, events) {
|
|
462
|
-
if (events !== void 0) seat.events = seat.events.filter((sent) => sent.n > events);
|
|
463
|
-
if (tick <= seat.heardUpTo || tick > this.world.tick) return;
|
|
464
|
-
seat.heardUpTo = tick;
|
|
465
|
-
seat.heard = this.world.tick;
|
|
466
|
-
const sent = seat.sentAt.get(tick) ?? (seat.whole?.tick === tick ? seat.whole.at : void 0);
|
|
467
|
-
if (sent !== void 0) seat.roundTrip += (performance.now() - sent - seat.roundTrip) * .2;
|
|
468
|
-
if (seat.whole && tick >= seat.whole.tick) seat.whole = void 0;
|
|
469
|
-
for (const kept of seat.sentAt.keys()) if (kept <= tick) seat.sentAt.delete(kept);
|
|
470
|
-
seat.events = seat.events.filter((sent) => sent.tick > tick);
|
|
471
|
-
if (!seat.frames.has(tick)) return;
|
|
472
|
-
seat.acked = tick;
|
|
473
|
-
for (const kept of [...seat.frames.keys()]) if (kept < tick) this.forget(seat, kept);
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* Whether a player is sent nothing this round: it has no frame kept to make a delta against, and the whole one it
|
|
477
|
-
* was sent last is on its way still. Another would be as big, and come no sooner than the answer to the first
|
|
478
|
-
*/
|
|
479
|
-
holding(seat, now) {
|
|
480
|
-
return !!seat.whole && !seat.frames.has(seat.acked) && now - seat.whole.at <= seat.roundTrip * 1.5 + this.sendEvery * 1e3;
|
|
481
|
-
}
|
|
482
|
-
leave(seat) {
|
|
483
|
-
if (!this.seats.delete(seat)) return;
|
|
484
|
-
seat.session.dispose();
|
|
485
|
-
seat.debug?.view.dispose();
|
|
486
|
-
if (this.seats.size === 0) this.stop();
|
|
487
|
-
}
|
|
488
|
-
/** How many players are connected */
|
|
489
|
-
get players() {
|
|
490
|
-
return this.seats.size;
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* How much the host keeps for each connected player, for a test of its limits (`keptFrames`, `keptEvents`): the
|
|
494
|
-
* frames it may make a delta against, when each was sent, and the events not yet said to be had
|
|
495
|
-
*
|
|
496
|
-
* @internal
|
|
497
|
-
*/
|
|
498
|
-
kept() {
|
|
499
|
-
return [...this.seats].map((seat) => ({
|
|
500
|
-
frames: seat.frames.size,
|
|
501
|
-
bytes: seat.keptBytes,
|
|
502
|
-
sentAt: seat.sentAt.size,
|
|
503
|
-
events: seat.events.length
|
|
504
|
-
}));
|
|
505
|
-
}
|
|
506
|
-
post(seat, message) {
|
|
507
|
-
seat.transport.send(JSON.stringify(message));
|
|
508
|
-
}
|
|
509
|
-
/** Lets `dt` seconds pass: the ticks that fit into it, then what changed to everyone, when it is time to send */
|
|
510
|
-
update(dt) {
|
|
511
|
-
const { world } = this;
|
|
512
|
-
let stepped = false;
|
|
513
|
-
this.accumulator += dt * this.speed * world.cheats.speed;
|
|
514
|
-
this.sinceSent += dt;
|
|
515
|
-
let ticks = 0;
|
|
516
|
-
while (this.accumulator >= world.dt) {
|
|
517
|
-
if (ticks++ >= this.maxTicks) {
|
|
518
|
-
this.accumulator = 0;
|
|
519
|
-
break;
|
|
520
|
-
}
|
|
521
|
-
const began = performance.now();
|
|
522
|
-
world.step();
|
|
523
|
-
const took = performance.now() - began;
|
|
524
|
-
const { timing, totals } = this;
|
|
525
|
-
timing.ticks++;
|
|
526
|
-
timing.tickTime += took;
|
|
527
|
-
timing.tickMax = Math.max(timing.tickMax, took);
|
|
528
|
-
totals.ticks++;
|
|
529
|
-
totals.tickTime += took;
|
|
530
|
-
totals.lastTick = took;
|
|
531
|
-
if (took > world.dt * 1e3) this.onSlowTick?.(took, world.tick);
|
|
532
|
-
this.accumulator -= world.dt;
|
|
533
|
-
stepped = true;
|
|
534
|
-
}
|
|
535
|
-
if (stepped || this.heardOutside) this.sendDue();
|
|
536
|
-
}
|
|
537
|
-
/** What the debug tools of a seat follow, as the world stands: when they follow, list or log anything */
|
|
538
|
-
sendDebug(seat, debug, now) {
|
|
539
|
-
debug.sentAt = now;
|
|
540
|
-
const { view } = debug;
|
|
541
|
-
view.refresh();
|
|
542
|
-
if (!view.inspection && !view.world && !view.logging) return;
|
|
543
|
-
this.post(seat, {
|
|
544
|
-
type: "debug",
|
|
545
|
-
inspection: view.inspection,
|
|
546
|
-
world: view.world,
|
|
547
|
-
log: view.logging ? [...view.log()] : []
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
/** Moves the game on by `ticks` at the next update, however fast it goes: a step of a held game (`speed` 0) */
|
|
551
|
-
advance(ticks = 1) {
|
|
552
|
-
this.accumulator += this.world.dt * Math.max(0, Math.floor(ticks));
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* Sends everyone what changed, when it is time to, for a world something else steps: the page's own game, shared with
|
|
556
|
-
* those who watch it (`shareGame`). `dt` is the seconds since the last call. Nothing goes while the world stands still
|
|
557
|
-
*/
|
|
558
|
-
publish(dt) {
|
|
559
|
-
this.sinceSent += dt;
|
|
560
|
-
if (this.world.tick !== this.sentTick || this.heardOutside) this.sendDue();
|
|
561
|
-
}
|
|
562
|
-
/** The tick what changed went out at last */
|
|
563
|
-
sentTick = -1;
|
|
564
|
-
/** Talk was carried out since what changed went out last: it goes out with no tick to wait for (RV14) */
|
|
565
|
-
heardOutside = false;
|
|
566
|
-
/** What changed to everyone, when the rate allows */
|
|
567
|
-
sendDue() {
|
|
568
|
-
if (this.sinceSent < this.sendEvery) return;
|
|
569
|
-
this.sentTick = this.world.tick;
|
|
570
|
-
this.heardOutside = false;
|
|
571
|
-
this.sinceSent = Math.min(this.sinceSent - this.sendEvery, this.sendEvery);
|
|
572
|
-
const began = performance.now();
|
|
573
|
-
const silent = this.dropSilentAfter * this.world.tickRate;
|
|
574
|
-
shareCaptures(this.world);
|
|
575
|
-
this.roundEntities = [];
|
|
576
|
-
try {
|
|
577
|
-
for (const seat of [...this.seats]) {
|
|
578
|
-
if (this.world.tick - seat.heard > silent) {
|
|
579
|
-
seat.transport.close();
|
|
580
|
-
this.leave(seat);
|
|
581
|
-
} else if (!this.holding(seat, began)) {
|
|
582
|
-
const delta = this.deltaFor(seat);
|
|
583
|
-
const made = performance.now();
|
|
584
|
-
const text = JSON.stringify({
|
|
585
|
-
type: "delta",
|
|
586
|
-
...delta
|
|
587
|
-
});
|
|
588
|
-
this.timing.parts.stringify += performance.now() - made;
|
|
589
|
-
this.timing.bytes += text.length;
|
|
590
|
-
if (this.timing.keys) bytesByKey(delta, text, this.timing.keys);
|
|
591
|
-
seat.transport.send(text);
|
|
592
|
-
}
|
|
593
|
-
if (seat.debug && began - seat.debug.sentAt >= 100) this.sendDebug(seat, seat.debug, began);
|
|
594
|
-
}
|
|
595
|
-
} finally {
|
|
596
|
-
endSharedCaptures(this.world);
|
|
597
|
-
this.roundEntities = [];
|
|
598
|
-
}
|
|
599
|
-
const took = performance.now() - began;
|
|
600
|
-
const { timing, totals } = this;
|
|
601
|
-
timing.sends++;
|
|
602
|
-
timing.sendTime += took;
|
|
603
|
-
timing.sendMax = Math.max(timing.sendMax, took);
|
|
604
|
-
totals.sends++;
|
|
605
|
-
totals.sendTime += took;
|
|
606
|
-
}
|
|
607
|
-
/**
|
|
608
|
-
* Where the host's time went since it was last asked (`takeTiming`): ticks of the simulation, and rounds of sending
|
|
609
|
-
* everyone what changed (the snapshots, the diffs, the JSON), each in milliseconds, added up and at most
|
|
610
|
-
*/
|
|
611
|
-
timing;
|
|
612
|
-
/**
|
|
613
|
-
* Where the host's time went since it started, never counted anew: ticks and rounds of sending, with the
|
|
614
|
-
* milliseconds they took, and how long the last tick took. What a server's numbers are made of (`/metrics`)
|
|
615
|
-
*/
|
|
616
|
-
totals = {
|
|
617
|
-
ticks: 0,
|
|
618
|
-
tickTime: 0,
|
|
619
|
-
lastTick: 0,
|
|
620
|
-
sends: 0,
|
|
621
|
-
sendTime: 0
|
|
622
|
-
};
|
|
623
|
-
/** Where the time went since the last call, and starts counting anew: for a server to print */
|
|
624
|
-
takeTiming() {
|
|
625
|
-
const now = performance.now();
|
|
626
|
-
const timing = {
|
|
627
|
-
...this.timing,
|
|
628
|
-
seconds: (now - this.timing.since) / 1e3
|
|
629
|
-
};
|
|
630
|
-
this.timing = emptyTiming(now, this.byKey);
|
|
631
|
-
return timing;
|
|
632
|
-
}
|
|
633
|
-
/** The lists of entities told this round so far, and what each was sent as, for the seats told the same to share one (`quantizedList`) */
|
|
634
|
-
roundEntities = [];
|
|
635
|
-
/** What the player knows now, as what changed since the tick they last said they got */
|
|
636
|
-
deltaFor(seat) {
|
|
637
|
-
const { session } = seat;
|
|
638
|
-
const { parts } = this.timing;
|
|
639
|
-
const began = performance.now();
|
|
640
|
-
const snapshot = session.snapshot();
|
|
641
|
-
const captured = performance.now();
|
|
642
|
-
const told = seat.answers.length ? [...snapshot.events, ...seat.answers.splice(0)] : snapshot.events;
|
|
643
|
-
if (told.length) seat.events.push({
|
|
644
|
-
tick: snapshot.tick,
|
|
645
|
-
n: ++seat.eventsMade,
|
|
646
|
-
events: told
|
|
647
|
-
});
|
|
648
|
-
let over = seat.events.length - this.keptEvents;
|
|
649
|
-
while (over-- > 0 && seat.events[0].sentAt !== void 0) seat.events.shift();
|
|
650
|
-
const entities = quantizedList(snapshot.entities, this.roundEntities);
|
|
651
|
-
const quantized = performance.now();
|
|
652
|
-
const fog = fogMessageOf(session.fog());
|
|
653
|
-
const fogged = performance.now();
|
|
654
|
-
const frame = {
|
|
655
|
-
snapshot: {
|
|
656
|
-
...snapshot,
|
|
657
|
-
entities,
|
|
658
|
-
events: []
|
|
659
|
-
},
|
|
660
|
-
fog,
|
|
661
|
-
camps: session.camps().map((camp) => ({ ...camp })),
|
|
662
|
-
team: session.team
|
|
663
|
-
};
|
|
664
|
-
const now = performance.now();
|
|
665
|
-
const overdue = seat.roundTrip * 1.5 + this.sendEvery * 1e3;
|
|
666
|
-
const events = seat.events.filter((sent) => sent.sentAt === void 0 || now - sent.sentAt > overdue);
|
|
667
|
-
for (const sent of events) sent.sentAt = now;
|
|
668
|
-
const base = seat.frames.get(seat.acked);
|
|
669
|
-
const delta = {
|
|
670
|
-
...diff(base, frame, this.world.registry),
|
|
671
|
-
events: events.map(({ tick, n, events }) => ({
|
|
672
|
-
tick,
|
|
673
|
-
n,
|
|
674
|
-
events
|
|
675
|
-
}))
|
|
676
|
-
};
|
|
677
|
-
if (!base) seat.whole = {
|
|
678
|
-
tick: frame.snapshot.tick,
|
|
679
|
-
at: now
|
|
680
|
-
};
|
|
681
|
-
parts.capture += captured - began;
|
|
682
|
-
parts.quantize += quantized - captured;
|
|
683
|
-
parts.fog += fogged - quantized;
|
|
684
|
-
parts.diff += performance.now() - fogged;
|
|
685
|
-
this.keep(seat, frame);
|
|
686
|
-
seat.sentAt.set(frame.snapshot.tick, now);
|
|
687
|
-
if (seat.sentAt.size > this.keptFrames) seat.sentAt.delete(seat.sentAt.keys().next().value);
|
|
688
|
-
return delta;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Keeps the frame a player was sent, to make the next delta against: the oldest let go past `keptFrames` of them or
|
|
692
|
-
* `keptBytes` together, the newest kept whatever it takes. A player who says nothing for long is sent everything
|
|
693
|
-
* again, once its base is no longer kept
|
|
694
|
-
*/
|
|
695
|
-
keep(seat, frame) {
|
|
696
|
-
const { tick } = frame.snapshot;
|
|
697
|
-
const bytes = frameBytes(frame);
|
|
698
|
-
seat.frames.set(tick, frame);
|
|
699
|
-
seat.frameSizes.set(tick, bytes);
|
|
700
|
-
seat.keptBytes += bytes;
|
|
701
|
-
while (seat.frames.size > 1 && (seat.frames.size > this.keptFrames || seat.keptBytes > this.keptBytes)) this.forget(seat, seat.frames.keys().next().value);
|
|
702
|
-
}
|
|
703
|
-
/** Lets go of a frame kept for a player */
|
|
704
|
-
forget(seat, tick) {
|
|
705
|
-
if (!seat.frames.delete(tick)) return;
|
|
706
|
-
seat.keptBytes -= seat.frameSizes.get(tick) ?? 0;
|
|
707
|
-
seat.frameSizes.delete(tick);
|
|
708
|
-
}
|
|
709
|
-
/** Runs the world by the clock, `rate` updates a second, until `stop` or `dispose` */
|
|
710
|
-
start(rate = this.world.tickRate) {
|
|
711
|
-
this.stop();
|
|
712
|
-
this.last = performance.now();
|
|
713
|
-
this.timer = setInterval(() => {
|
|
714
|
-
const now = performance.now();
|
|
715
|
-
const dt = Math.min((now - this.last) / 1e3, .25);
|
|
716
|
-
this.last = now;
|
|
717
|
-
try {
|
|
718
|
-
this.update(dt);
|
|
719
|
-
} catch (error) {
|
|
720
|
-
this.stop();
|
|
721
|
-
if (this.onError) this.onError(error);
|
|
722
|
-
else this.world.log.error("server", "stepping the game threw: it is stopped", { error: errorFields(error) });
|
|
723
|
-
}
|
|
724
|
-
}, 1e3 / rate);
|
|
725
|
-
}
|
|
726
|
-
stop() {
|
|
727
|
-
if (this.timer !== void 0) clearInterval(this.timer);
|
|
728
|
-
this.timer = void 0;
|
|
729
|
-
}
|
|
730
|
-
/** Whether it runs by the clock */
|
|
731
|
-
get running() {
|
|
732
|
-
return this.timer !== void 0;
|
|
733
|
-
}
|
|
734
|
-
/** Stops, and closes every connection. The world goes too, when it is the host's (`ownsWorld`) */
|
|
735
|
-
/**
|
|
736
|
-
* Stops, and lets every player go: their connections closed, unless `closeTransports` is false (a server that keeps
|
|
737
|
-
* them for its lobby, after the game)
|
|
738
|
-
*/
|
|
739
|
-
dispose(options = {}) {
|
|
740
|
-
this.stop();
|
|
741
|
-
this.stopFailed();
|
|
742
|
-
for (const seat of [...this.seats]) {
|
|
743
|
-
if (options.closeTransports !== false) seat.transport.close();
|
|
744
|
-
this.leave(seat);
|
|
745
|
-
}
|
|
746
|
-
if (this.ownsWorld) this.world.dispose();
|
|
747
|
-
}
|
|
748
|
-
};
|
|
749
|
-
/**
|
|
750
|
-
* What the player's side keeps of the map, to answer the screen without asking the host: a placement grid and the
|
|
751
|
-
* ways to walk of the map as it was made, with the structures the player has seen on them. What they have not seen
|
|
752
|
-
* is not there: a path preview never goes around a building in the dark, and gives it away
|
|
753
|
-
*/
|
|
754
|
-
var ClientWorld = class {
|
|
755
|
-
terrain;
|
|
756
|
-
registry;
|
|
757
|
-
nav;
|
|
758
|
-
air;
|
|
759
|
-
grid;
|
|
760
|
-
known = /* @__PURE__ */ new Map();
|
|
761
|
-
seen = /* @__PURE__ */ new Set();
|
|
762
|
-
constructor(terrain, registry, nav, air) {
|
|
763
|
-
this.terrain = terrain;
|
|
764
|
-
this.registry = registry;
|
|
765
|
-
this.nav = nav;
|
|
766
|
-
this.air = air;
|
|
767
|
-
this.grid = new BuildGrid(terrain);
|
|
768
|
-
}
|
|
769
|
-
/** Whether a resource of a kind that it knows of covers just this ground, with nothing it knows of standing on it */
|
|
770
|
-
resourceAt(kind, cellX, cellZ, w, d) {
|
|
771
|
-
let found = false;
|
|
772
|
-
for (const structure of this.known.values()) {
|
|
773
|
-
if (structure.cellX !== cellX || structure.cellZ !== cellZ || structure.w !== w || structure.d !== d) continue;
|
|
774
|
-
if (structure.resource !== kind) return false;
|
|
775
|
-
found = true;
|
|
776
|
-
}
|
|
777
|
-
return found;
|
|
778
|
-
}
|
|
779
|
-
/** Brings the structures up to date with a snapshot: those in it stand, those no longer in it are gone */
|
|
780
|
-
sync(snapshot) {
|
|
781
|
-
const { seen, known, grid, nav } = this;
|
|
782
|
-
seen.clear();
|
|
783
|
-
let changed = false;
|
|
784
|
-
for (const entity of snapshot.entities) {
|
|
785
|
-
if (!this.registry.units.has(entity.type)) continue;
|
|
786
|
-
const def = this.registry.unit(entity.type);
|
|
787
|
-
if (!def.footprint) continue;
|
|
788
|
-
seen.add(entity.id);
|
|
789
|
-
if (known.has(entity.id)) continue;
|
|
790
|
-
const site = siteOf(def, entity);
|
|
791
|
-
const { w, d } = footprintOf(def);
|
|
792
|
-
known.set(entity.id, {
|
|
793
|
-
cellX: site.cellX,
|
|
794
|
-
cellZ: site.cellZ,
|
|
795
|
-
w,
|
|
796
|
-
d,
|
|
797
|
-
...def.resource && { resource: def.resource.kind }
|
|
798
|
-
});
|
|
799
|
-
grid.occupy(site.cellX, site.cellZ, w, d, entity.id);
|
|
800
|
-
nav.addObstacle(entity.id, site);
|
|
801
|
-
changed = true;
|
|
802
|
-
}
|
|
803
|
-
let released = false;
|
|
804
|
-
for (const [id, structure] of known) {
|
|
805
|
-
if (seen.has(id)) continue;
|
|
806
|
-
known.delete(id);
|
|
807
|
-
grid.release(structure.cellX, structure.cellZ, structure.w, structure.d, id);
|
|
808
|
-
nav.removeObstacle(id);
|
|
809
|
-
changed = released = true;
|
|
810
|
-
}
|
|
811
|
-
if (released) for (const [id, structure] of known) grid.occupy(structure.cellX, structure.cellZ, structure.w, structure.d, id);
|
|
812
|
-
if (changed) nav.update(0);
|
|
813
|
-
}
|
|
814
|
-
};
|
|
815
|
-
/**
|
|
816
|
-
* The host said no, with why: to come back to a game that is over, or a player let go. Trying again does not help
|
|
817
|
-
*
|
|
818
|
-
* @beta
|
|
819
|
-
*/
|
|
820
|
-
var SessionRefused = class extends Error {
|
|
821
|
-
reason;
|
|
822
|
-
constructor(reason) {
|
|
823
|
-
super(reason);
|
|
824
|
-
this.reason = reason;
|
|
825
|
-
this.name = "SessionRefused";
|
|
826
|
-
}
|
|
827
|
-
};
|
|
828
|
-
/**
|
|
829
|
-
* A player's seat at a world that runs elsewhere, over a transport (see `connect`). It rebuilds what the host sends
|
|
830
|
-
* as changes (`Delta`) into whole snapshots, keeping the objects of what did not change, says which ticks it got, and
|
|
831
|
-
* answers the screen's questions from those and its own copy of the map (`ClientWorld`). No debug access, and no
|
|
832
|
-
* checkpoints: those belong to a simulation in the page
|
|
833
|
-
*
|
|
834
|
-
* @beta
|
|
835
|
-
*/
|
|
836
|
-
var RemoteSession = class {
|
|
837
|
-
transport;
|
|
838
|
-
map;
|
|
839
|
-
limits;
|
|
840
|
-
/**
|
|
841
|
-
* A seat over a wire can none of what a game in this page can (`SessionCan`), but the debug tools' view of the world
|
|
842
|
-
* for a player the host lets use them (`inspect`, said over the wire)
|
|
843
|
-
*/
|
|
844
|
-
can;
|
|
845
|
-
/** The debug tools' view of the world, for a player the host lets use them (the welcome's `debug`) */
|
|
846
|
-
get debugView() {
|
|
847
|
-
return this.remoteDebug;
|
|
848
|
-
}
|
|
849
|
-
remoteDebug;
|
|
850
|
-
playerId;
|
|
851
|
-
team;
|
|
852
|
-
terrain;
|
|
853
|
-
environment;
|
|
854
|
-
doodads;
|
|
855
|
-
regions;
|
|
856
|
-
cameras;
|
|
857
|
-
paths;
|
|
858
|
-
registry;
|
|
859
|
-
tickRate;
|
|
860
|
-
alpha = 1;
|
|
861
|
-
/** What the player knows by tick, for the deltas to come: from the base of the last one on */
|
|
862
|
-
frames = /* @__PURE__ */ new Map();
|
|
863
|
-
latest;
|
|
864
|
-
incoming = [];
|
|
865
|
-
/** What the host's definitions were changed to while it ran (`world.redefine`), as this screen heard it */
|
|
866
|
-
redefined = {};
|
|
867
|
-
/** The number of the last batch of events that went to the screen (`Delta.events`): those sent again are not applied twice */
|
|
868
|
-
eventsUpTo = -1;
|
|
869
|
-
fogState;
|
|
870
|
-
closed = false;
|
|
871
|
-
/** The tick the screen shows, between the last two snapshots given out */
|
|
872
|
-
renderTick;
|
|
873
|
-
/** The tick of the snapshot given out last, and of the one before it */
|
|
874
|
-
shownTick;
|
|
875
|
-
shownBefore;
|
|
876
|
-
/** Ticks between two snapshots, evened out */
|
|
877
|
-
interval = 1;
|
|
878
|
-
/** How far off the time snapshots arrive at is from the ticks between them, in seconds, evened out */
|
|
879
|
-
jitter = 0;
|
|
880
|
-
/** When the last snapshot arrived, and its tick */
|
|
881
|
-
arrival;
|
|
882
|
-
constructor(transport, welcome, registry, map, limits = {}) {
|
|
883
|
-
this.transport = transport;
|
|
884
|
-
this.map = map;
|
|
885
|
-
this.limits = limits;
|
|
886
|
-
this.playerId = welcome.player;
|
|
887
|
-
this.tickRate = welcome.tickRate;
|
|
888
|
-
this.observer = welcome.observer ?? false;
|
|
889
|
-
this.remoteDebug = welcome.debug ? new RemoteDebug((ask) => this.tell({
|
|
890
|
-
type: "debug",
|
|
891
|
-
...ask
|
|
892
|
-
})) : void 0;
|
|
893
|
-
this.can = this.remoteDebug ? { inspect: this.remoteDebug } : {};
|
|
894
|
-
this.registry = registry;
|
|
895
|
-
this.terrain = map.terrain;
|
|
896
|
-
this.environment = welcome.environment;
|
|
897
|
-
this.doodads = welcome.doodads ?? [];
|
|
898
|
-
this.regions = welcome.regions ?? [];
|
|
899
|
-
this.cameras = welcome.cameras ?? [];
|
|
900
|
-
this.paths = welcome.paths ?? [];
|
|
901
|
-
restoreRedefinitions(registry, welcome.redefinitions);
|
|
902
|
-
this.heardRedefined(welcome.redefinitions);
|
|
903
|
-
const cells = this.terrain.width * this.terrain.depth;
|
|
904
|
-
this.fogState = {
|
|
905
|
-
width: this.terrain.width,
|
|
906
|
-
depth: this.terrain.depth,
|
|
907
|
-
version: 0,
|
|
908
|
-
revealed: false,
|
|
909
|
-
visible: new Uint8Array(cells),
|
|
910
|
-
explored: new Uint8Array(cells)
|
|
911
|
-
};
|
|
912
|
-
const first = this.take(welcome.first);
|
|
913
|
-
this.latest = first;
|
|
914
|
-
this.team = first.team;
|
|
915
|
-
this.incoming.length = 0;
|
|
916
|
-
this.shownTick = this.shownBefore = first.snapshot.tick;
|
|
917
|
-
this.attach(transport);
|
|
918
|
-
}
|
|
919
|
-
/** Talks through a transport from now on: what comes in is taken, and its closing is a drop */
|
|
920
|
-
attach(transport) {
|
|
921
|
-
this.transport = transport;
|
|
922
|
-
this.closed = false;
|
|
923
|
-
this.leaving = false;
|
|
924
|
-
transport.onMessage((text) => {
|
|
925
|
-
const message = readHostMessage(text);
|
|
926
|
-
if (message) this.receive(message);
|
|
927
|
-
});
|
|
928
|
-
transport.onClose(() => {
|
|
929
|
-
if (this.transport !== transport) return;
|
|
930
|
-
this.closed = true;
|
|
931
|
-
clearInterval(this.pinger);
|
|
932
|
-
if (!this.leaving) this.onDisconnect?.();
|
|
933
|
-
});
|
|
934
|
-
this.sendPing();
|
|
935
|
-
clearInterval(this.pinger);
|
|
936
|
-
this.pinger = setInterval(() => this.sendPing(), this.limits.pingEvery ?? PING_EVERY);
|
|
937
|
-
}
|
|
938
|
-
/** Whether the host has gone */
|
|
939
|
-
get disconnected() {
|
|
940
|
-
return this.closed;
|
|
941
|
-
}
|
|
942
|
-
/**
|
|
943
|
-
* Told when the connection drops, not by this side's leaving (`dispose`): to try again (`resume`), or to say the
|
|
944
|
-
* game is lost
|
|
945
|
-
*/
|
|
946
|
-
onDisconnect;
|
|
947
|
-
/** This side closed it: not a drop */
|
|
948
|
-
leaving = false;
|
|
949
|
-
/**
|
|
950
|
-
* Goes on with the same game over a new connection, once the host has welcomed it there again (a server does, to who
|
|
951
|
-
* comes back with their seat's token): everything known sent again, and the screen catches up. What the host says
|
|
952
|
-
* besides the game before the welcome goes to `onServer`; a `refused` fails it with the reason, as does a connection
|
|
953
|
-
* that closes first, or a host that runs another game
|
|
954
|
-
*/
|
|
955
|
-
async resume(transport) {
|
|
956
|
-
const welcome = await new Promise((resolve, reject) => {
|
|
957
|
-
transport.onClose(() => reject(/* @__PURE__ */ new Error("The host closed the connection before it took the player back")));
|
|
958
|
-
transport.onMessage((text) => {
|
|
959
|
-
const message = readHostMessage(text);
|
|
960
|
-
if (!message) return;
|
|
961
|
-
if (message.type === "welcome") resolve(message);
|
|
962
|
-
else if (message.type === "refused") reject(new SessionRefused(String(message.reason ?? "refused")));
|
|
963
|
-
else if (isServerMessage(message)) this.onServer?.(message);
|
|
964
|
-
});
|
|
965
|
-
});
|
|
966
|
-
const problem = welcomeProblem(welcome);
|
|
967
|
-
if (problem) {
|
|
968
|
-
transport.close();
|
|
969
|
-
throw new Error(problem);
|
|
970
|
-
}
|
|
971
|
-
if (welcome.game !== this.registry.game || welcome.hash !== this.registry.dataHash || welcome.player !== this.playerId) {
|
|
972
|
-
transport.close();
|
|
973
|
-
throw new Error("The host took the player back to another game");
|
|
974
|
-
}
|
|
975
|
-
restoreRedefinitions(this.registry, welcome.redefinitions);
|
|
976
|
-
this.heardRedefined(welcome.redefinitions);
|
|
977
|
-
this.eventsUpTo = -1;
|
|
978
|
-
this.attach(transport);
|
|
979
|
-
this.frames.clear();
|
|
980
|
-
this.incoming.length = 0;
|
|
981
|
-
this.arrival = void 0;
|
|
982
|
-
this.interval = 1;
|
|
983
|
-
this.jitter = 0;
|
|
984
|
-
if (welcome.first.tick < this.shownTick) {
|
|
985
|
-
this.shownTick = this.shownBefore = welcome.first.tick;
|
|
986
|
-
this.renderTick = void 0;
|
|
987
|
-
this.eventsUpTo = welcome.first.tick - 1;
|
|
988
|
-
}
|
|
989
|
-
this.take(welcome.first);
|
|
990
|
-
this.remoteDebug?.askAgain();
|
|
991
|
-
}
|
|
992
|
-
/** Keeps what the host's definitions were changed to, over what was kept before */
|
|
993
|
-
heardRedefined(data) {
|
|
994
|
-
for (const [kind, byId] of Object.entries(data ?? {})) for (const [id, values] of Object.entries(byId)) {
|
|
995
|
-
const kept = this.redefined[kind] ??= {};
|
|
996
|
-
kept[id] = mergeValues(kept[id] ?? {}, values);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
/**
|
|
1000
|
-
* Takes a new version of the game into this screen's registry (hot reload, a game in a worker): its definitions as
|
|
1001
|
-
* the new version has them, what the host changed of them while it ran over them again
|
|
1002
|
-
*/
|
|
1003
|
-
takeGame(game) {
|
|
1004
|
-
this.registry.takeFrom(createRegistry(game));
|
|
1005
|
-
restoreRedefinitions(this.registry, this.redefined);
|
|
1006
|
-
}
|
|
1007
|
-
/** Takes in a message of the host's: what the transport hands it, and what came in while the session was made */
|
|
1008
|
-
receive(message) {
|
|
1009
|
-
if (message.type === "delta") this.take(message);
|
|
1010
|
-
else if (message.type === "debug") this.remoteDebug?.take(message);
|
|
1011
|
-
else if (message.type === "pong") {
|
|
1012
|
-
const took = performance.now() - message.at;
|
|
1013
|
-
this.ping = this.ping === void 0 ? took : this.ping + (took - this.ping) * .3;
|
|
1014
|
-
} else if (message.type === "failed") {
|
|
1015
|
-
if (typeof message.message === "string") this.failure = { message: message.message };
|
|
1016
|
-
} else if (isServerMessage(message)) {
|
|
1017
|
-
if (message.type === "paused") {
|
|
1018
|
-
this.pause = message.paused === true ? {
|
|
1019
|
-
...typeof message.player === "number" && { player: message.player },
|
|
1020
|
-
...typeof message.seconds === "number" && { until: Date.now() + message.seconds * 1e3 }
|
|
1021
|
-
} : null;
|
|
1022
|
-
this.onPause?.(this.pause);
|
|
1023
|
-
}
|
|
1024
|
-
this.onServer?.(message);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
/**
|
|
1028
|
-
* The game paused by the server, for everyone: by a player (`player`, until a time at most, in `Date.now()`'s
|
|
1029
|
-
* milliseconds) or an admin (neither). Null while it goes on
|
|
1030
|
-
*/
|
|
1031
|
-
pause = null;
|
|
1032
|
-
/** Told each time the server pauses the game or lets it go on */
|
|
1033
|
-
onPause;
|
|
1034
|
-
/** Says something to the server beside the game: a lobby's message (`{ type: 'resume' }`) */
|
|
1035
|
-
tell(message) {
|
|
1036
|
-
if (!this.closed) this.transport.send(JSON.stringify(message));
|
|
1037
|
-
}
|
|
1038
|
-
/** Why the game ended on an error in its rules, once the host said so */
|
|
1039
|
-
failure = null;
|
|
1040
|
-
/** Milliseconds a message takes to the host and back, evened out: measured every couple of seconds */
|
|
1041
|
-
ping;
|
|
1042
|
-
/** Whether the player only watches, and sees everything */
|
|
1043
|
-
observer;
|
|
1044
|
-
pinger;
|
|
1045
|
-
/** Asks the host to answer at once, to measure the time there and back */
|
|
1046
|
-
sendPing() {
|
|
1047
|
-
if (!this.closed) this.transport.send(JSON.stringify({
|
|
1048
|
-
type: "ping",
|
|
1049
|
-
at: performance.now()
|
|
1050
|
-
}));
|
|
1051
|
-
}
|
|
1052
|
-
/** Told of what the host says besides the game: a server's messages (see `ConnectOptions.onServer`) */
|
|
1053
|
-
onServer;
|
|
1054
|
-
/** Rebuilds the frame a delta makes, says it got it, and has its snapshot wait for the screen */
|
|
1055
|
-
take(delta) {
|
|
1056
|
-
const base = delta.base === null ? void 0 : this.frames.get(delta.base);
|
|
1057
|
-
if (delta.base !== null && !base) return void 0;
|
|
1058
|
-
const frame = apply(base, delta, this.latest);
|
|
1059
|
-
const { snapshot } = frame;
|
|
1060
|
-
this.frames.set(snapshot.tick, frame);
|
|
1061
|
-
for (const tick of this.frames.keys()) if (delta.base !== null && tick < delta.base) this.frames.delete(tick);
|
|
1062
|
-
if (delta.rest.fog) this.takeFog(delta.rest.fog);
|
|
1063
|
-
if (delta.rest.team !== void 0) this.team = delta.rest.team;
|
|
1064
|
-
const events = [];
|
|
1065
|
-
for (const sent of delta.events) {
|
|
1066
|
-
if (sent.n <= this.eventsUpTo) continue;
|
|
1067
|
-
events.push(...sent.events);
|
|
1068
|
-
this.eventsUpTo = sent.n;
|
|
1069
|
-
for (const event of sent.events) {
|
|
1070
|
-
if (event.type === "redefined") {
|
|
1071
|
-
redefineIn(this.registry, event.kind, event.definition, event.values);
|
|
1072
|
-
this.heardRedefined({ [event.kind]: { [event.definition]: event.values } });
|
|
1073
|
-
}
|
|
1074
|
-
if (event.type === "ground" && !terrainPatchProblem(this.terrain, event.patch)) applyTerrainPatch(this.terrain, event.patch);
|
|
1075
|
-
if (event.type !== "cells") continue;
|
|
1076
|
-
const layer = this.terrain.cellLayer(event.layer);
|
|
1077
|
-
for (const c of event.at) if (c >= 0 && c < layer.length) layer[c] = event.value;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
this.incoming.push({
|
|
1081
|
-
...snapshot,
|
|
1082
|
-
events
|
|
1083
|
-
});
|
|
1084
|
-
if (this.incoming.length > (this.limits.keptSnapshots ?? KEPT_SNAPSHOTS)) this.fold();
|
|
1085
|
-
this.arrived(snapshot.tick);
|
|
1086
|
-
this.map.sync(snapshot);
|
|
1087
|
-
this.latest = frame;
|
|
1088
|
-
if (!this.closed) this.transport.send(JSON.stringify({
|
|
1089
|
-
type: "ack",
|
|
1090
|
-
tick: snapshot.tick,
|
|
1091
|
-
events: this.eventsUpTo
|
|
1092
|
-
}));
|
|
1093
|
-
return frame;
|
|
1094
|
-
}
|
|
1095
|
-
/**
|
|
1096
|
-
* The oldest snapshot waiting let go, its events put before the next one's: the screen, when it asks again, jumps
|
|
1097
|
-
* to the newest anyway. The state is in every snapshot; the events (deaths, sounds, lines said) are what would be lost
|
|
1098
|
-
*/
|
|
1099
|
-
fold() {
|
|
1100
|
-
const oldest = this.incoming.shift();
|
|
1101
|
-
if (!oldest.events.length) return;
|
|
1102
|
-
const next = this.incoming[0];
|
|
1103
|
-
const events = oldest.events.concat(next.events);
|
|
1104
|
-
const mostEvents = this.limits.keptScreenEvents ?? KEPT_SCREEN_EVENTS;
|
|
1105
|
-
this.incoming[0] = {
|
|
1106
|
-
...next,
|
|
1107
|
-
events: events.length > mostEvents ? events.slice(-mostEvents) : events
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
|
-
/**
|
|
1111
|
-
* Snapshots come in and not yet given to the screen (`update`): a few normally, never more than a few seconds of
|
|
1112
|
-
* them, however long the screen does not ask
|
|
1113
|
-
*
|
|
1114
|
-
* @beta
|
|
1115
|
-
*/
|
|
1116
|
-
get waiting() {
|
|
1117
|
-
return this.incoming.length;
|
|
1118
|
-
}
|
|
1119
|
-
takeFog(message) {
|
|
1120
|
-
const fog = this.fogState;
|
|
1121
|
-
fog.revealed = message.revealed;
|
|
1122
|
-
fromRuns(message.visible, fog.visible);
|
|
1123
|
-
fromRuns(message.explored, fog.explored);
|
|
1124
|
-
fog.version++;
|
|
1125
|
-
}
|
|
1126
|
-
snapshot() {
|
|
1127
|
-
return this.latest.snapshot;
|
|
1128
|
-
}
|
|
1129
|
-
/** The tick the screen is shown at, between two snapshots: behind the newest by `delay` */
|
|
1130
|
-
get shownAt() {
|
|
1131
|
-
return this.renderTick ?? this.shownTick;
|
|
1132
|
-
}
|
|
1133
|
-
/**
|
|
1134
|
-
* The ticks between two snapshots, and the jitter the interpolation makes room for, as it has them evened out. A
|
|
1135
|
-
* test may set the jitter, as a late line would have
|
|
1136
|
-
*
|
|
1137
|
-
* @internal
|
|
1138
|
-
*/
|
|
1139
|
-
get pacing() {
|
|
1140
|
-
return {
|
|
1141
|
-
interval: this.interval,
|
|
1142
|
-
jitter: this.jitter
|
|
1143
|
-
};
|
|
1144
|
-
}
|
|
1145
|
-
/** @internal */
|
|
1146
|
-
set pacing(pacing) {
|
|
1147
|
-
this.jitter = pacing.jitter;
|
|
1148
|
-
}
|
|
1149
|
-
/** How far behind the newest snapshot the screen is shown, in ticks: about two snapshots, and room for the jitter */
|
|
1150
|
-
get delay() {
|
|
1151
|
-
return this.interval * 2 + Math.min(this.jitter, this.limits.maxJitter ?? .2) * this.tickRate * 2;
|
|
1152
|
-
}
|
|
1153
|
-
/** Takes note of when a snapshot arrived, for how far apart they come and how unevenly */
|
|
1154
|
-
arrived(tick) {
|
|
1155
|
-
const now = performance.now();
|
|
1156
|
-
const last = this.arrival;
|
|
1157
|
-
this.arrival = {
|
|
1158
|
-
at: now,
|
|
1159
|
-
tick
|
|
1160
|
-
};
|
|
1161
|
-
if (!last || tick <= last.tick) return;
|
|
1162
|
-
const ticks = tick - last.tick;
|
|
1163
|
-
this.interval += (ticks - this.interval) * .1;
|
|
1164
|
-
const off = Math.abs((now - last.at) / 1e3 - ticks / this.tickRate);
|
|
1165
|
-
this.jitter += (off - this.jitter) * .1;
|
|
1166
|
-
}
|
|
1167
|
-
/**
|
|
1168
|
-
* Lets the screen's clock go on by `dt`, and gives out the snapshots it has come to, oldest first: the screen draws
|
|
1169
|
-
* between the last two (`alpha`). The time is the host's: the clock follows the snapshots, not the other way around
|
|
1170
|
-
*/
|
|
1171
|
-
update(dt) {
|
|
1172
|
-
const newest = this.incoming.length ? this.incoming[this.incoming.length - 1].tick : this.shownTick;
|
|
1173
|
-
const target = newest - this.delay;
|
|
1174
|
-
if (this.renderTick === void 0 || Math.abs(target - this.renderTick) > this.tickRate) this.renderTick = target;
|
|
1175
|
-
else {
|
|
1176
|
-
const rate = 1 + Math.max(-.1, Math.min((target - this.renderTick) * .05, .1));
|
|
1177
|
-
this.renderTick += dt * this.tickRate * rate;
|
|
1178
|
-
}
|
|
1179
|
-
this.renderTick = Math.min(this.renderTick, newest);
|
|
1180
|
-
const out = [];
|
|
1181
|
-
while (this.incoming.length && this.renderTick >= this.shownTick) {
|
|
1182
|
-
const next = this.incoming.shift();
|
|
1183
|
-
this.shownBefore = this.shownTick;
|
|
1184
|
-
this.shownTick = next.tick;
|
|
1185
|
-
out.push(next);
|
|
1186
|
-
}
|
|
1187
|
-
const span = this.shownTick - this.shownBefore;
|
|
1188
|
-
this.alpha = span > 0 ? Math.max(0, Math.min((this.renderTick - this.shownBefore) / span, 1)) : 1;
|
|
1189
|
-
return out;
|
|
1190
|
-
}
|
|
1191
|
-
send(command) {
|
|
1192
|
-
const seq = ++this.lastSeq;
|
|
1193
|
-
if (!this.closed) this.transport.send(JSON.stringify({
|
|
1194
|
-
type: "command",
|
|
1195
|
-
seq,
|
|
1196
|
-
command
|
|
1197
|
-
}));
|
|
1198
|
-
return seq;
|
|
1199
|
-
}
|
|
1200
|
-
/** The number of the last command sent */
|
|
1201
|
-
lastSeq = 0;
|
|
1202
|
-
fog() {
|
|
1203
|
-
return this.fogState;
|
|
1204
|
-
}
|
|
1205
|
-
camps() {
|
|
1206
|
-
return this.latest.camps;
|
|
1207
|
-
}
|
|
1208
|
-
/**
|
|
1209
|
-
* Whether the player could build this here, as far as they can tell: on their copy of the map, with the structures
|
|
1210
|
-
* they know, on ground they have explored. The game's own say (the `canPlace` hook) is the host's: it may still refuse
|
|
1211
|
-
*/
|
|
1212
|
-
canPlace(unitType, point) {
|
|
1213
|
-
if (!this.registry.units.has(unitType)) return false;
|
|
1214
|
-
const def = this.registry.unit(unitType);
|
|
1215
|
-
const site = siteOf(def, point);
|
|
1216
|
-
const { w, d } = footprintOf(def);
|
|
1217
|
-
if (def.builtOn !== void 0 ? !this.map.resourceAt(def.builtOn, site.cellX, site.cellZ, w, d) : !this.map.grid.canPlace(site.cellX, site.cellZ, w, d)) return false;
|
|
1218
|
-
const fog = this.fogState;
|
|
1219
|
-
if (fog.revealed) return true;
|
|
1220
|
-
const explored = (x, z) => fog.explored[Math.floor(z) * fog.width + Math.floor(x)] === 1;
|
|
1221
|
-
return explored(site.x - site.hw + .5, site.z - site.hd + .5) && explored(site.x + site.hw - .5, site.z - site.hd + .5) && explored(site.x - site.hw + .5, site.z + site.hd - .5) && explored(site.x + site.hw - .5, site.z + site.hd - .5);
|
|
1222
|
-
}
|
|
1223
|
-
/** Which cells of a building's ground are in the way, as `canPlace` sees them: on the player's copy of the map, explored */
|
|
1224
|
-
placeCells(unitType, point) {
|
|
1225
|
-
if (!this.registry.units.has(unitType)) return void 0;
|
|
1226
|
-
const fog = this.fogState;
|
|
1227
|
-
return placementCells(this.registry.unit(unitType), point, (x, z) => this.map.grid.canPlace(x, z, 1, 1), fog.revealed ? void 0 : (x, z) => fog.explored[z * fog.width + x] === 1);
|
|
1228
|
-
}
|
|
1229
|
-
/** The way a unit would walk, on the player's copy of the map: around the structures they know, and no others */
|
|
1230
|
-
findPath(from, to, flying = false) {
|
|
1231
|
-
return (flying ? this.map.air : this.map.nav).findPath(from, to);
|
|
1232
|
-
}
|
|
1233
|
-
dispose() {
|
|
1234
|
-
clearInterval(this.pinger);
|
|
1235
|
-
this.leaving = true;
|
|
1236
|
-
if (!this.closed) this.transport.close();
|
|
1237
|
-
this.closed = true;
|
|
1238
|
-
this.map.nav.dispose();
|
|
1239
|
-
this.map.air.dispose();
|
|
1240
|
-
}
|
|
1241
|
-
};
|
|
1242
|
-
/** How much of the ground goes in one message, for a player who does not have its map: well under what a connection holds */
|
|
1243
|
-
var GROUND_PART = 262144;
|
|
1244
|
-
/**
|
|
1245
|
-
* The ground of a welcome: all of it, or the map it started as among the player's (S36), the changes put on it. A
|
|
1246
|
-
* player without that map asks the host for it, a part at a time, and puts the changes on what it is sent
|
|
1247
|
-
*/
|
|
1248
|
-
async function groundOfWelcome(transport, ground, maps, game, early, dropped) {
|
|
1249
|
-
try {
|
|
1250
|
-
return groundOf(ground, maps, game).terrain;
|
|
1251
|
-
} catch (error) {
|
|
1252
|
-
if (!(error instanceof MissingGround) || !isGroundChanges(ground)) throw error;
|
|
1253
|
-
}
|
|
1254
|
-
const whole = await new Promise((resolve, reject) => {
|
|
1255
|
-
const parts = [];
|
|
1256
|
-
transport.onClose(() => {
|
|
1257
|
-
dropped();
|
|
1258
|
-
reject(/* @__PURE__ */ new Error("The host closed the connection while the ground was sent"));
|
|
1259
|
-
});
|
|
1260
|
-
transport.onMessage((text) => {
|
|
1261
|
-
const message = readHostMessage(text);
|
|
1262
|
-
if (message?.type !== "ground") {
|
|
1263
|
-
early.push(text);
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
1266
|
-
parts[message.part] = message.text;
|
|
1267
|
-
if (message.part + 1 < message.parts) transport.send(JSON.stringify({
|
|
1268
|
-
type: "ground",
|
|
1269
|
-
part: message.part + 1
|
|
1270
|
-
}));
|
|
1271
|
-
else {
|
|
1272
|
-
transport.onMessage((later) => early.push(later));
|
|
1273
|
-
resolve(JSON.parse(parts.join("")));
|
|
1274
|
-
}
|
|
1275
|
-
});
|
|
1276
|
-
transport.send(JSON.stringify({
|
|
1277
|
-
type: "ground",
|
|
1278
|
-
part: 0
|
|
1279
|
-
}));
|
|
1280
|
-
});
|
|
1281
|
-
return withChanges(loadTerrain(whole), ground);
|
|
1282
|
-
}
|
|
1283
|
-
/**
|
|
1284
|
-
* What is wrong with a welcome, before anything of it is used: another version of the wire (`PROTOCOL_VERSION`), or a
|
|
1285
|
-
* shape that is not one. What comes after it is the host's word, trusted as the welcome said it would be
|
|
1286
|
-
*/
|
|
1287
|
-
function welcomeProblem(welcome) {
|
|
1288
|
-
if (welcome.protocol !== 13) return protocolMismatch(welcome.protocol);
|
|
1289
|
-
return Number.isInteger(welcome.player) && isFiniteNumber(welcome.tickRate) && welcome.tickRate > 0 && typeof welcome.game === "string" && typeof welcome.hash === "string" && typeof welcome.terrain === "object" && welcome.terrain !== null && typeof welcome.first === "object" && welcome.first !== null ? void 0 : "The host sent a welcome this player cannot read";
|
|
1290
|
-
}
|
|
1291
|
-
/**
|
|
1292
|
-
* Connects to a host over a transport (the player's end of it), and waits for the welcome: what the session is
|
|
1293
|
-
* made of. Fails when the host runs another version of the game, or of the engine's wire (`PROTOCOL_VERSION`), and
|
|
1294
|
-
* when the connection closes before the session is made: while the map is made from the welcome too
|
|
1295
|
-
*
|
|
1296
|
-
* @beta
|
|
1297
|
-
*/
|
|
1298
|
-
function connect(transport, options) {
|
|
1299
|
-
return connectAs(transport, options, (...made) => new RemoteSession(...made));
|
|
1300
|
-
}
|
|
1301
|
-
/** `connect`, for a session of a kind of its own: `make` is given what a `RemoteSession` is made of */
|
|
1302
|
-
async function connectAs(transport, options, make) {
|
|
1303
|
-
let dropped = false;
|
|
1304
|
-
const drop = () => {
|
|
1305
|
-
dropped = true;
|
|
1306
|
-
};
|
|
1307
|
-
const welcome = await new Promise((resolve, reject) => {
|
|
1308
|
-
transport.onClose(() => {
|
|
1309
|
-
drop();
|
|
1310
|
-
reject(/* @__PURE__ */ new Error("The host closed the connection before the game started"));
|
|
1311
|
-
});
|
|
1312
|
-
transport.onMessage((text) => {
|
|
1313
|
-
const message = readHostMessage(text);
|
|
1314
|
-
if (!message) return;
|
|
1315
|
-
if (message.type === "welcome") resolve(message);
|
|
1316
|
-
else if (isServerMessage(message)) options.onServer?.(message);
|
|
1317
|
-
});
|
|
1318
|
-
});
|
|
1319
|
-
const problem = welcomeProblem(welcome);
|
|
1320
|
-
if (problem) {
|
|
1321
|
-
transport.close();
|
|
1322
|
-
throw new Error(problem);
|
|
1323
|
-
}
|
|
1324
|
-
const game = "id" in options.game && typeof options.game.id === "string" ? options.game : options.game[welcome.game];
|
|
1325
|
-
const own = "id" in options.game && typeof options.game.id === "string" ? options.game.id : void 0;
|
|
1326
|
-
const others = otherPacks(welcome.game, game?.id ?? own);
|
|
1327
|
-
if (!game) {
|
|
1328
|
-
transport.close();
|
|
1329
|
-
throw new Error(others ?? `The host runs ${welcome.game}, which this player does not have`);
|
|
1330
|
-
}
|
|
1331
|
-
const registry = createRegistry(game);
|
|
1332
|
-
if (welcome.game !== registry.game || welcome.hash !== registry.dataHash) {
|
|
1333
|
-
transport.close();
|
|
1334
|
-
throw new Error(others ?? `The host runs ${welcome.game} (${welcome.hash}), this is ${registry.game} (${registry.dataHash}): not the same game`);
|
|
1335
|
-
}
|
|
1336
|
-
const early = [];
|
|
1337
|
-
transport.onMessage((text) => early.push(text));
|
|
1338
|
-
transport.onClose(drop);
|
|
1339
|
-
const lost = "The host closed the connection while the map was made";
|
|
1340
|
-
if (dropped) throw new Error(lost);
|
|
1341
|
-
const terrain = await groundOfWelcome(transport, welcome.terrain, options.maps ?? game.maps, game, early, drop);
|
|
1342
|
-
transport.onClose(drop);
|
|
1343
|
-
const speedAt = hasGroundSpeeds(registry) ? (x, z) => groundSpeedOn(registry, terrain, x, z) : void 0;
|
|
1344
|
-
const nav = await RecastNavigation.create(terrain, {
|
|
1345
|
-
agentRadius: .5,
|
|
1346
|
-
...speedAt && { speedAt },
|
|
1347
|
-
...options.navigation
|
|
1348
|
-
});
|
|
1349
|
-
if (dropped) {
|
|
1350
|
-
nav.dispose();
|
|
1351
|
-
throw new Error(lost);
|
|
1352
|
-
}
|
|
1353
|
-
const session = make(transport, welcome, registry, new ClientWorld(terrain, registry, nav, new AirNavigation(terrain.width, terrain.depth)), options.limits);
|
|
1354
|
-
session.onServer = options.onServer;
|
|
1355
|
-
for (const text of early) {
|
|
1356
|
-
const message = readHostMessage(text);
|
|
1357
|
-
if (message) session.receive(message);
|
|
1358
|
-
}
|
|
1359
|
-
return session;
|
|
1360
|
-
}
|
|
1361
|
-
/**
|
|
1362
|
-
* What a player is told who joins a game of the same base as theirs with other packs of content or mods
|
|
1363
|
-
* (`melee+gnomes` against `melee`): the host's and theirs. Nothing for games of other bases, or the same
|
|
1364
|
-
*/
|
|
1365
|
-
function otherPacks(host, own) {
|
|
1366
|
-
if (own === void 0 || own === host) return void 0;
|
|
1367
|
-
const [hostBase, ...hostPacks] = host.split("+");
|
|
1368
|
-
const [ownBase, ...ownPacks] = own.split("+");
|
|
1369
|
-
if (hostBase !== ownBase) return void 0;
|
|
1370
|
-
const list = (packs) => packs.length ? packs.join(", ") : "none";
|
|
1371
|
-
return `The host plays ${hostBase} with the packs ${list(hostPacks)}, this player with ${list(ownPacks)}: the host's are the game's`;
|
|
1372
|
-
}
|
|
1373
|
-
//#endregion
|
|
1374
|
-
//#region src/session/delay.ts
|
|
1375
|
-
/** The tick a message to a player says it brings: of a delta from the head of its text, of the welcome from the frame it carries */
|
|
1376
|
-
var tickOf = (message) => {
|
|
1377
|
-
const found = /^\{"type":"delta","tick":(\d+)/.exec(message) ?? (message.startsWith("{\"type\":\"welcome\"") ? /"first":\{"tick":(\d+)/.exec(message) : null);
|
|
1378
|
-
return found ? Number(found[1]) : void 0;
|
|
1379
|
-
};
|
|
1380
|
-
/** How often what is held is looked at, to send on what is due: a second's tenth is near enough for a delay of seconds */
|
|
1381
|
-
var LOOK_EVERY = 100;
|
|
1382
|
-
/**
|
|
1383
|
-
* The host's end of a watcher's transport, with what goes to the watcher held for `seconds` (see the top of this
|
|
1384
|
-
* file). Closing it closes the watcher's, and what is held goes with it
|
|
1385
|
-
*
|
|
1386
|
-
* @beta
|
|
1387
|
-
*/
|
|
1388
|
-
function delayed(transport, seconds) {
|
|
1389
|
-
const held = [];
|
|
1390
|
-
let hostHears = () => {};
|
|
1391
|
-
let hostClosed = () => {};
|
|
1392
|
-
const timer = setInterval(() => {
|
|
1393
|
-
const now = performance.now();
|
|
1394
|
-
let due = 0;
|
|
1395
|
-
while (due < held.length && held[due].at <= now) due++;
|
|
1396
|
-
for (const { text } of held.splice(0, due)) transport.send(text);
|
|
1397
|
-
}, LOOK_EVERY);
|
|
1398
|
-
transport.onMessage((text) => {
|
|
1399
|
-
if (text.startsWith("{\"type\":\"ack\"")) return;
|
|
1400
|
-
hostHears(text);
|
|
1401
|
-
});
|
|
1402
|
-
transport.onClose(() => {
|
|
1403
|
-
clearInterval(timer);
|
|
1404
|
-
held.length = 0;
|
|
1405
|
-
hostClosed();
|
|
1406
|
-
});
|
|
1407
|
-
return {
|
|
1408
|
-
send(text) {
|
|
1409
|
-
held.push({
|
|
1410
|
-
at: performance.now() + seconds * 1e3,
|
|
1411
|
-
text
|
|
1412
|
-
});
|
|
1413
|
-
const tick = tickOf(text);
|
|
1414
|
-
if (tick !== void 0) queueMicrotask(() => hostHears(JSON.stringify({
|
|
1415
|
-
type: "ack",
|
|
1416
|
-
tick
|
|
1417
|
-
})));
|
|
1418
|
-
},
|
|
1419
|
-
onMessage(handler) {
|
|
1420
|
-
hostHears = handler;
|
|
1421
|
-
},
|
|
1422
|
-
onClose(handler) {
|
|
1423
|
-
hostClosed = handler;
|
|
1424
|
-
},
|
|
1425
|
-
close() {
|
|
1426
|
-
clearInterval(timer);
|
|
1427
|
-
held.length = 0;
|
|
1428
|
-
transport.close();
|
|
1429
|
-
}
|
|
1430
|
-
};
|
|
1431
|
-
}
|
|
1432
|
-
//#endregion
|
|
1433
|
-
//#region src/session/seed.ts
|
|
1434
|
-
/**
|
|
1435
|
-
* A seed of the moment, for a game no other was (the clock: a session's, never the rules'): where every new world made without a seed of its own gets its seed
|
|
1436
|
-
* (a map played in a page, on a server, in a worker). The same seed, the same game
|
|
1437
|
-
*
|
|
1438
|
-
* @beta
|
|
1439
|
-
*/
|
|
1440
|
-
var randomSeed = () => Date.now() % 1e5;
|
|
1441
|
-
//#endregion
|
|
1442
|
-
//#region src/noise.ts
|
|
1443
|
-
/**
|
|
1444
|
-
* Noise that repeats: a lattice of random values that wraps around, read smoothly between them, so that whatever is
|
|
1445
|
-
* made of it tiles. Seeded (`Rng`), so the same seed makes the same noise everywhere. What the map generator shapes
|
|
1446
|
-
* its ground with; the client makes the ground's textures with the same (`packages/client`, its own copy, so that a
|
|
1447
|
-
* worker of the page's loads nothing of the engine)
|
|
1448
|
-
*/
|
|
1449
|
-
/**
|
|
1450
|
-
* A value noise that repeats every `period` cells of its lattice: between 0 and 1, smooth
|
|
1451
|
-
*
|
|
1452
|
-
* @internal
|
|
1453
|
-
*/
|
|
1454
|
-
function tilingNoise(period, seed) {
|
|
1455
|
-
const size = Math.max(1, Math.round(period));
|
|
1456
|
-
const rng = new Rng(seed);
|
|
1457
|
-
const values = Float32Array.from({ length: size * size }, () => rng.next());
|
|
1458
|
-
return (u, v) => {
|
|
1459
|
-
const x = u * size;
|
|
1460
|
-
const z = v * size;
|
|
1461
|
-
let x0 = Math.floor(x);
|
|
1462
|
-
let z0 = Math.floor(z);
|
|
1463
|
-
const fx = x - x0;
|
|
1464
|
-
const fz = z - z0;
|
|
1465
|
-
x0 = (x0 % size + size) % size;
|
|
1466
|
-
z0 = (z0 % size + size) % size;
|
|
1467
|
-
const x1 = x0 + 1 === size ? 0 : x0 + 1;
|
|
1468
|
-
const z1 = z0 + 1 === size ? 0 : z0 + 1;
|
|
1469
|
-
const sx = fx * fx * (3 - 2 * fx);
|
|
1470
|
-
const sz = fz * fz * (3 - 2 * fz);
|
|
1471
|
-
const r0 = z0 * size;
|
|
1472
|
-
const r1 = z1 * size;
|
|
1473
|
-
const top = values[r0 + x0] + (values[r0 + x1] - values[r0 + x0]) * sx;
|
|
1474
|
-
return top + (values[r1 + x0] + (values[r1 + x1] - values[r1 + x0]) * sx - top) * sz;
|
|
1475
|
-
};
|
|
1476
|
-
}
|
|
1477
|
-
/**
|
|
1478
|
-
* Several noises, each finer and fainter than the one before: what looks like ground and not like a pattern. 0..1
|
|
1479
|
-
*
|
|
1480
|
-
* @beta
|
|
1481
|
-
*/
|
|
1482
|
-
function layeredNoise(seed, periods) {
|
|
1483
|
-
const noises = periods.map((period, i) => tilingNoise(period, seed + i * 101));
|
|
1484
|
-
const weights = periods.map((_, i) => 1 / (1 << i));
|
|
1485
|
-
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
|
1486
|
-
if (noises.length === 1) {
|
|
1487
|
-
const [only] = noises;
|
|
1488
|
-
return (u, v) => only(u, v);
|
|
1489
|
-
}
|
|
1490
|
-
return (u, v) => {
|
|
1491
|
-
let sum = 0;
|
|
1492
|
-
for (let i = 0; i < noises.length; i++) sum += noises[i](u, v) * weights[i];
|
|
1493
|
-
return sum / total;
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
//#endregion
|
|
1497
|
-
//#region src/mapCheck/rules/players.ts
|
|
1498
|
-
/** How many players the map has, against what the game allows */
|
|
1499
|
-
var playerRules = {
|
|
1500
|
-
id: "players",
|
|
1501
|
-
check(map, registry, say, _context) {
|
|
1502
|
-
const players = map.players.filter((player) => player.id !== 90 && player.id !== 0).length;
|
|
1503
|
-
const most = registry.module ? maxPlayersOf(registry.module) : 32;
|
|
1504
|
-
if (players > most) say({
|
|
1505
|
-
message: `The map has ${players} players, and the game allows ${most}`,
|
|
1506
|
-
error: true,
|
|
1507
|
-
rule: "too-many-players"
|
|
1508
|
-
});
|
|
1509
|
-
}
|
|
1510
|
-
};
|
|
1511
|
-
//#endregion
|
|
1512
|
-
//#region src/mapCheck/rules/objects.ts
|
|
1513
|
-
/** What is placed and the camps: of types the game has, on the map, out of deep water */
|
|
1514
|
-
var objectRules = {
|
|
1515
|
-
id: "objects",
|
|
1516
|
-
check(map, registry, say, { terrain, onMap }) {
|
|
1517
|
-
for (const object of map.objects) {
|
|
1518
|
-
if (!registry.units.has(object.type)) {
|
|
1519
|
-
say({
|
|
1520
|
-
message: `"${object.type}" is no unit type of the game: it is left out`,
|
|
1521
|
-
at: object,
|
|
1522
|
-
rule: "unknown-type",
|
|
1523
|
-
about: { object: object.id }
|
|
1524
|
-
});
|
|
1525
|
-
continue;
|
|
1526
|
-
}
|
|
1527
|
-
if (!onMap(object)) {
|
|
1528
|
-
say({
|
|
1529
|
-
message: `${registry.unit(object.type).name} stands off the map`,
|
|
1530
|
-
error: true,
|
|
1531
|
-
at: object,
|
|
1532
|
-
rule: "off-map",
|
|
1533
|
-
about: { object: object.id }
|
|
1534
|
-
});
|
|
1535
|
-
continue;
|
|
1536
|
-
}
|
|
1537
|
-
const def = registry.unit(object.type);
|
|
1538
|
-
if (!def.flying && terrain.waterAt(object.x, object.z) === 2) say({
|
|
1539
|
-
message: `${def.name} stands in deep water`,
|
|
1540
|
-
at: object,
|
|
1541
|
-
rule: "deep-water",
|
|
1542
|
-
about: { object: object.id }
|
|
1543
|
-
});
|
|
1544
|
-
}
|
|
1545
|
-
for (const camp of map.camps) {
|
|
1546
|
-
const strangers = camp.units.filter((type) => !type.startsWith("@") && !registry.units.has(type));
|
|
1547
|
-
if (strangers.length) say({
|
|
1548
|
-
message: `A camp has creeps the game does not have: ${strangers.join(", ")}`,
|
|
1549
|
-
at: camp,
|
|
1550
|
-
rule: "unknown-creeps",
|
|
1551
|
-
about: { camp: camp.id }
|
|
1552
|
-
});
|
|
1553
|
-
if (!onMap(camp)) say({
|
|
1554
|
-
message: "A camp stands off the map",
|
|
1555
|
-
error: true,
|
|
1556
|
-
at: camp,
|
|
1557
|
-
rule: "off-map",
|
|
1558
|
-
about: { camp: camp.id }
|
|
1559
|
-
});
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
};
|
|
1563
|
-
//#endregion
|
|
1564
|
-
//#region src/mapCheck/rules/random.ts
|
|
1565
|
-
/** Random groups and item tables: something to draw from, and by names there are */
|
|
1566
|
-
var randomRules = {
|
|
1567
|
-
id: "random",
|
|
1568
|
-
check(map, registry, say, _context) {
|
|
1569
|
-
const groups = new Map((map.randomGroups ?? []).map((group) => [group.name, group]));
|
|
1570
|
-
const tables = new Set((map.itemTables ?? []).map((table) => table.name));
|
|
1571
|
-
for (const group of groups.values()) if (!groupChoices(group, registry).length) say({
|
|
1572
|
-
message: `The random group ${group.name} has nothing of the game to draw from`,
|
|
1573
|
-
error: true,
|
|
1574
|
-
rule: "empty-random-group"
|
|
1575
|
-
});
|
|
1576
|
-
for (const table of map.itemTables ?? []) if (!tableItems(table, registry).length) say({
|
|
1577
|
-
message: `The item table ${table.name} has nothing of the game to draw from`,
|
|
1578
|
-
error: true,
|
|
1579
|
-
rule: "empty-item-table"
|
|
1580
|
-
});
|
|
1581
|
-
for (const object of map.objects) {
|
|
1582
|
-
if (object.random && !groups.has(object.random)) say({
|
|
1583
|
-
message: `It is drawn from "${object.random}", which is no random group of the map`,
|
|
1584
|
-
error: true,
|
|
1585
|
-
at: object,
|
|
1586
|
-
rule: "unknown-random-group",
|
|
1587
|
-
about: { object: object.id }
|
|
1588
|
-
});
|
|
1589
|
-
if (object.drops && !tables.has(object.drops)) say({
|
|
1590
|
-
message: `It drops from "${object.drops}", which is no item table of the map`,
|
|
1591
|
-
error: true,
|
|
1592
|
-
at: object,
|
|
1593
|
-
rule: "unknown-item-table",
|
|
1594
|
-
about: { object: object.id }
|
|
1595
|
-
});
|
|
1596
|
-
}
|
|
1597
|
-
for (const camp of map.camps) for (const unit of camp.units) if (unit.startsWith("@") && !groups.has(unit.slice(1))) say({
|
|
1598
|
-
message: `A camp has "${unit}", which is no random group of the map`,
|
|
1599
|
-
error: true,
|
|
1600
|
-
at: camp,
|
|
1601
|
-
rule: "unknown-random-group",
|
|
1602
|
-
about: { camp: camp.id }
|
|
1603
|
-
});
|
|
1604
|
-
}
|
|
1605
|
-
};
|
|
1606
|
-
//#endregion
|
|
1607
|
-
//#region src/mapCheck/rules/names.ts
|
|
1608
|
-
/** One thing a name, and the cameras players start looking from */
|
|
1609
|
-
var nameRules = {
|
|
1610
|
-
id: "names",
|
|
1611
|
-
check(map, _registry, say, _context) {
|
|
1612
|
-
const named = /* @__PURE__ */ new Map();
|
|
1613
|
-
const name = (value, at, about) => {
|
|
1614
|
-
if (value) named.set(value, [...named.get(value) ?? [], {
|
|
1615
|
-
at,
|
|
1616
|
-
about
|
|
1617
|
-
}]);
|
|
1618
|
-
};
|
|
1619
|
-
for (const object of map.objects) name(object.name, object, { object: object.id });
|
|
1620
|
-
for (const camp of map.camps) name(camp.name, camp, { camp: camp.id });
|
|
1621
|
-
for (const region of map.regions) name(region.name, region.shape, { region: region.id });
|
|
1622
|
-
for (const point of map.points) name(point.name, point, { point: point.id });
|
|
1623
|
-
for (const path of map.paths) name(path.name, path.points[0] ?? {
|
|
1624
|
-
x: 0,
|
|
1625
|
-
z: 0
|
|
1626
|
-
}, { path: path.id });
|
|
1627
|
-
for (const [value, places] of named) if (places.length > 1) say({
|
|
1628
|
-
message: `"${value}" is the name of ${places.length} things: the rules find only one`,
|
|
1629
|
-
error: true,
|
|
1630
|
-
at: places[1].at,
|
|
1631
|
-
rule: "name-twice",
|
|
1632
|
-
about: places[1].about
|
|
1633
|
-
});
|
|
1634
|
-
for (const player of map.players) if (player.camera && !map.cameras.some((camera) => camera.name === player.camera)) say({
|
|
1635
|
-
message: `${player.name} starts looking from "${player.camera}", which is no camera of the map`,
|
|
1636
|
-
at: player.start,
|
|
1637
|
-
rule: "unknown-camera",
|
|
1638
|
-
about: { player: player.id }
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1641
|
-
};
|
|
1642
|
-
//#endregion
|
|
1643
|
-
//#region src/mapCheck/rules/regions.ts
|
|
1644
|
-
/** Regions on the map */
|
|
1645
|
-
var regionRules = {
|
|
1646
|
-
id: "regions",
|
|
1647
|
-
check(map, _registry, say, _context) {
|
|
1648
|
-
const { width, depth } = map;
|
|
1649
|
-
for (const region of map.regions) {
|
|
1650
|
-
const { shape } = region;
|
|
1651
|
-
const reach = shapeBounds(shape);
|
|
1652
|
-
if (reach.x1 <= 0 || reach.z1 <= 0 || reach.x0 >= width || reach.z0 >= depth) say({
|
|
1653
|
-
message: `The region ${region.name} is outside the map`,
|
|
1654
|
-
error: true,
|
|
1655
|
-
at: shape,
|
|
1656
|
-
rule: "region-off-map",
|
|
1657
|
-
about: { region: region.id }
|
|
1658
|
-
});
|
|
1659
|
-
else if (reach.x0 < 0 || reach.z0 < 0 || reach.x1 > width || reach.z1 > depth) say({
|
|
1660
|
-
message: `The region ${region.name} goes past the edge of the map`,
|
|
1661
|
-
at: shape,
|
|
1662
|
-
rule: "region-past-edge",
|
|
1663
|
-
about: { region: region.id }
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
};
|
|
1668
|
-
//#endregion
|
|
1669
|
-
//#region src/mapCheck/rules/paths.ts
|
|
1670
|
-
/** Paths on the map: on it, and what they paint and carve there to be had */
|
|
1671
|
-
var pathRules = {
|
|
1672
|
-
id: "paths",
|
|
1673
|
-
check(map, registry, say, { onMap }) {
|
|
1674
|
-
for (const path of map.paths) {
|
|
1675
|
-
const about = { path: path.id };
|
|
1676
|
-
if (path.points.some((point) => !onMap(point))) say({
|
|
1677
|
-
message: `The path ${path.name} goes off the map`,
|
|
1678
|
-
at: path.points[0],
|
|
1679
|
-
rule: "path-off-map",
|
|
1680
|
-
about
|
|
1681
|
-
});
|
|
1682
|
-
for (const tile of [path.paint, path.profile?.ground]) if (tile !== void 0 && !map.tileset.includes(tile) && !registry.tiles.some((def) => def.id === tile)) say({
|
|
1683
|
-
message: `The path ${path.name} paints with ${tile}, which neither the map's tiles nor the game's are`,
|
|
1684
|
-
at: path.points[0],
|
|
1685
|
-
rule: "path-tile-missing",
|
|
1686
|
-
about
|
|
1687
|
-
});
|
|
1688
|
-
if (path.profile && path.points.length < 2) say({
|
|
1689
|
-
message: `The path ${path.name} carves its bed with one point: it carves nothing until it has two`,
|
|
1690
|
-
at: path.points[0],
|
|
1691
|
-
rule: "path-profile-short",
|
|
1692
|
-
about
|
|
1693
|
-
});
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
};
|
|
1697
|
-
//#endregion
|
|
1698
|
-
//#region src/mapCheck/cells.ts
|
|
1699
|
-
/** The steepest ground something on foot walks, in radians: as the navigation mesh has it */
|
|
1700
|
-
var WALKABLE_SLOPE = 38 * Math.PI / 180;
|
|
1701
|
-
/**
|
|
1702
|
-
* Which cells of a map something on foot can stand on (1) and which have a building or a mine on them (`built`): for
|
|
1703
|
-
* the checks, and for what the editor shows of where one can walk and build. `steep` counts slopes too steep to walk
|
|
1704
|
-
*
|
|
1705
|
-
* @beta
|
|
1706
|
-
*/
|
|
1707
|
-
function mapCells(map, registry, terrain = mapTerrain(map), steep = false) {
|
|
1708
|
-
const { width, depth } = map;
|
|
1709
|
-
const open = new Uint8Array(width * depth);
|
|
1710
|
-
const built = new Uint8Array(width * depth);
|
|
1711
|
-
for (let c = 0; c < open.length; c++) {
|
|
1712
|
-
const x = c % width;
|
|
1713
|
-
const z = (c - x) / width;
|
|
1714
|
-
open[c] = (terrain.water[c] !== 2 || terrain.blocked[c] === 3) && terrain.blocked[c] !== 2 && terrain.inPlay(x, z) && (!steep || terrain.slopeAt(x + .5, z + .5) <= WALKABLE_SLOPE) ? 1 : 0;
|
|
1715
|
-
}
|
|
1716
|
-
for (const object of map.objects) {
|
|
1717
|
-
if (!registry.units.has(object.type)) continue;
|
|
1718
|
-
const def = registry.unit(object.type);
|
|
1719
|
-
if (def.kind === "unit") continue;
|
|
1720
|
-
const { w, d } = footprintOf(def);
|
|
1721
|
-
const corner = footprintCell(object.x, object.z, w, d);
|
|
1722
|
-
for (let z = corner.z; z < corner.z + d; z++) for (let x = corner.x; x < corner.x + w; x++) {
|
|
1723
|
-
if (x < 0 || z < 0 || x >= width || z >= depth) continue;
|
|
1724
|
-
open[z * width + x] = 0;
|
|
1725
|
-
built[z * width + x] = 1;
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
return {
|
|
1729
|
-
open,
|
|
1730
|
-
built
|
|
1731
|
-
};
|
|
1732
|
-
}
|
|
1733
|
-
/** Cells something on foot can stand on, and which of them it can get to from which: a number per cell, 0 for none */
|
|
1734
|
-
function walkingAreas(map, registry) {
|
|
1735
|
-
const { width, depth, levels, ramps } = map;
|
|
1736
|
-
const { open } = mapCells(map, registry);
|
|
1737
|
-
const passable = (a, b) => levels[a] === levels[b] || ramps[a] > 0 || ramps[b] > 0;
|
|
1738
|
-
const area = new Int32Array(width * depth);
|
|
1739
|
-
let next = 0;
|
|
1740
|
-
const queue = new Int32Array(width * depth);
|
|
1741
|
-
for (let start = 0; start < area.length; start++) {
|
|
1742
|
-
if (!open[start] || area[start]) continue;
|
|
1743
|
-
next++;
|
|
1744
|
-
let head = 0;
|
|
1745
|
-
let tail = 0;
|
|
1746
|
-
queue[tail++] = start;
|
|
1747
|
-
area[start] = next;
|
|
1748
|
-
while (head < tail) {
|
|
1749
|
-
const c = queue[head++];
|
|
1750
|
-
const x = c % width;
|
|
1751
|
-
const z = (c - x) / width;
|
|
1752
|
-
for (const [nx, nz] of [
|
|
1753
|
-
[x - 1, z],
|
|
1754
|
-
[x + 1, z],
|
|
1755
|
-
[x, z - 1],
|
|
1756
|
-
[x, z + 1]
|
|
1757
|
-
]) {
|
|
1758
|
-
if (nx < 0 || nz < 0 || nx >= width || nz >= depth) continue;
|
|
1759
|
-
const n = nz * width + nx;
|
|
1760
|
-
if (!open[n] || area[n] || !passable(c, n)) continue;
|
|
1761
|
-
area[n] = next;
|
|
1762
|
-
queue[tail++] = n;
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
/** The area a point is in, or the one next to it, for what stands on cells nothing walks on: a mine, a town hall */
|
|
1767
|
-
const at = (point) => {
|
|
1768
|
-
const cx = Math.floor(point.x);
|
|
1769
|
-
const cz = Math.floor(point.z);
|
|
1770
|
-
for (let r = 0; r <= 4; r++) for (let z = cz - r; z <= cz + r; z++) for (let x = cx - r; x <= cx + r; x++) {
|
|
1771
|
-
if (x < 0 || z < 0 || x >= width || z >= depth) continue;
|
|
1772
|
-
if (Math.max(Math.abs(x - cx), Math.abs(z - cz)) !== r) continue;
|
|
1773
|
-
const found = area[z * width + x];
|
|
1774
|
-
if (found) return found;
|
|
1775
|
-
}
|
|
1776
|
-
return 0;
|
|
1777
|
-
};
|
|
1778
|
-
return {
|
|
1779
|
-
area,
|
|
1780
|
-
at
|
|
1781
|
-
};
|
|
1782
|
-
}
|
|
1783
|
-
//#endregion
|
|
1784
|
-
//#region src/mapCheck/rules/walking.ts
|
|
1785
|
-
/** Who can walk to whom: starts cut off from each other, mines nobody can walk to */
|
|
1786
|
-
var walkingRules = {
|
|
1787
|
-
id: "walking",
|
|
1788
|
-
check(map, registry, say, _context) {
|
|
1789
|
-
const starts = map.players.filter((player) => player.start);
|
|
1790
|
-
if (starts.length) {
|
|
1791
|
-
const { at } = walkingAreas(map, registry);
|
|
1792
|
-
const areas = starts.map((player) => at(player.start));
|
|
1793
|
-
starts.forEach((player, i) => {
|
|
1794
|
-
if (!areas[i]) say({
|
|
1795
|
-
message: `${player.name} starts where nothing can walk`,
|
|
1796
|
-
error: true,
|
|
1797
|
-
at: player.start,
|
|
1798
|
-
rule: "start-blocked",
|
|
1799
|
-
about: { player: player.id }
|
|
1800
|
-
});
|
|
1801
|
-
else if (areas.some((other, j) => j !== i && other && other !== areas[i])) say({
|
|
1802
|
-
message: `${player.name} cannot walk to every other start: a cliff, water or what is built is in the way`,
|
|
1803
|
-
at: player.start,
|
|
1804
|
-
rule: "starts-apart",
|
|
1805
|
-
about: { player: player.id }
|
|
1806
|
-
});
|
|
1807
|
-
});
|
|
1808
|
-
const reached = new Set(areas.filter(Boolean));
|
|
1809
|
-
for (const object of map.objects) {
|
|
1810
|
-
if (!registry.units.has(object.type)) continue;
|
|
1811
|
-
const def = registry.unit(object.type);
|
|
1812
|
-
if (!(registry.module?.editor?.reach ?? reachedByDefault)(def)) continue;
|
|
1813
|
-
if (!reached.has(at(object))) say({
|
|
1814
|
-
message: `Nobody can walk to the ${def.name} here`,
|
|
1815
|
-
at: object,
|
|
1816
|
-
rule: "unreachable",
|
|
1817
|
-
about: { object: object.id }
|
|
1818
|
-
});
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
};
|
|
1823
|
-
//#endregion
|
|
1824
|
-
//#region src/mapCheck/rules/races.ts
|
|
1825
|
-
/**
|
|
1826
|
-
* What is wrong with the races of a map: a player fixed to a race the game has not got, or the map offering one, are
|
|
1827
|
-
* errors (a world is not made from it); what is placed for a player that their race may not have is a warning. What
|
|
1828
|
-
* the map offers is for the players it fixes no race for: one fixed to another is as the map means it
|
|
1829
|
-
*/
|
|
1830
|
-
function raceProblems(map, registry) {
|
|
1831
|
-
const problems = [];
|
|
1832
|
-
const known = (race) => race === "random" || registry.races.has(race);
|
|
1833
|
-
const none = registry.races.size === 0;
|
|
1834
|
-
for (const race of map.races ?? []) {
|
|
1835
|
-
if (race === "random" || registry.races.has(race)) continue;
|
|
1836
|
-
problems.push({
|
|
1837
|
-
message: `The map offers the race "${race}", which ${none ? "the game does not have: it has no races" : "is no race of the game"}`,
|
|
1838
|
-
error: true,
|
|
1839
|
-
rule: "unknown-race"
|
|
1840
|
-
});
|
|
1841
|
-
}
|
|
1842
|
-
for (const player of map.players) {
|
|
1843
|
-
if (player.race === void 0) continue;
|
|
1844
|
-
if (!known(player.race)) problems.push({
|
|
1845
|
-
message: `${player.name} plays the race "${player.race}", which ${none ? "the game does not have: it has no races" : "is no race of the game"}`,
|
|
1846
|
-
error: true,
|
|
1847
|
-
at: player.start,
|
|
1848
|
-
rule: "unknown-race",
|
|
1849
|
-
about: { player: player.id }
|
|
1850
|
-
});
|
|
1851
|
-
}
|
|
1852
|
-
const fixed = new Map(map.players.filter((player) => player.race && player.race !== "random" && registry.races.has(player.race)).map((player) => [player.id, player]));
|
|
1853
|
-
for (const object of map.objects) {
|
|
1854
|
-
const owner = fixed.get(object.owner);
|
|
1855
|
-
if (!owner || !registry.units.has(object.type)) continue;
|
|
1856
|
-
const def = registry.unit(object.type);
|
|
1857
|
-
if (availableTo(def, owner.race)) continue;
|
|
1858
|
-
problems.push({
|
|
1859
|
-
message: `${def.name} is ${registry.race(def.race).name}, and ${owner.name} plays ${registry.race(owner.race).name}`,
|
|
1860
|
-
at: object,
|
|
1861
|
-
rule: "other-race",
|
|
1862
|
-
about: { object: object.id }
|
|
1863
|
-
});
|
|
1864
|
-
}
|
|
1865
|
-
return problems;
|
|
1866
|
-
}
|
|
1867
|
-
//#endregion
|
|
1868
|
-
//#region src/mapCheck.ts
|
|
1869
|
-
/**
|
|
1870
|
-
* The engine's checks of a map, in the order they run: players, what is placed, random groups, names, regions, paths,
|
|
1871
|
-
* walking, races, the fields that name things
|
|
1872
|
-
*
|
|
1873
|
-
* @beta
|
|
1874
|
-
*/
|
|
1875
|
-
var MAP_RULES = [
|
|
1876
|
-
playerRules,
|
|
1877
|
-
objectRules,
|
|
1878
|
-
randomRules,
|
|
1879
|
-
nameRules,
|
|
1880
|
-
regionRules,
|
|
1881
|
-
pathRules,
|
|
1882
|
-
walkingRules,
|
|
1883
|
-
{
|
|
1884
|
-
id: "races",
|
|
1885
|
-
check(map, registry, say) {
|
|
1886
|
-
for (const problem of raceProblems(map, registry)) say(problem);
|
|
1887
|
-
}
|
|
1888
|
-
},
|
|
1889
|
-
{
|
|
1890
|
-
id: "targets",
|
|
1891
|
-
check(map, registry, say, _context) {
|
|
1892
|
-
const naming = (fields, data, about, at) => {
|
|
1893
|
-
for (const field of fields) {
|
|
1894
|
-
const kind = field.kind;
|
|
1895
|
-
const id = data?.[field.id];
|
|
1896
|
-
if (!TARGET_FIELDS.includes(field.kind) || typeof id !== "number") continue;
|
|
1897
|
-
if (targetName(map, kind, id) === void 0) say({
|
|
1898
|
-
message: `${field.name} names a ${kind} the map does not have`,
|
|
1899
|
-
error: true,
|
|
1900
|
-
rule: "missing-target",
|
|
1901
|
-
...at && { at },
|
|
1902
|
-
...about && { about }
|
|
1903
|
-
});
|
|
1904
|
-
}
|
|
1905
|
-
};
|
|
1906
|
-
for (const object of map.objects) naming(objectFields(registry, object.type), object.data, { object: object.id }, object);
|
|
1907
|
-
for (const camp of map.camps) naming(campFields(registry), camp.data, { camp: camp.id }, camp);
|
|
1908
|
-
for (const region of map.regions) naming(regionFields(registry), region.data, { region: region.id });
|
|
1909
|
-
for (const player of map.players) naming(playerFields(registry), player.data, { player: player.id }, player.start);
|
|
1910
|
-
naming(mapFields(registry), map.data, void 0);
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
];
|
|
1914
|
-
/**
|
|
1915
|
-
* What is wrong with a map, before it is played: what the game does not have, names used twice or named and not
|
|
1916
|
-
* there, what stands off the map or in deep water, starts that cannot reach each other on foot, mines nobody can walk
|
|
1917
|
-
* to, regions outside the map (`MAP_RULES`); then the checks given (`more`: the client's of what the map looks like,
|
|
1918
|
-
* a game view's own, as the editor runs them); then the game's own (`MapEditorDef.check`). Errors keep it from being
|
|
1919
|
-
* played; the rest are warnings. Runs without the editor, for a test that goes over every map of a project
|
|
1920
|
-
*
|
|
1921
|
-
* @beta
|
|
1922
|
-
*/
|
|
1923
|
-
function checkMap(map, registry, more = []) {
|
|
1924
|
-
const problems = [];
|
|
1925
|
-
const { width, depth } = map;
|
|
1926
|
-
let terrain;
|
|
1927
|
-
const context = {
|
|
1928
|
-
get terrain() {
|
|
1929
|
-
return terrain ??= mapTerrain(map);
|
|
1930
|
-
},
|
|
1931
|
-
onMap: (p) => p.x >= 0 && p.z >= 0 && p.x <= width && p.z <= depth
|
|
1932
|
-
};
|
|
1933
|
-
const say = (problem) => void problems.push(problem);
|
|
1934
|
-
for (const rule of [...MAP_RULES, ...more]) rule.check(map, registry, say, context);
|
|
1935
|
-
const own = (registry.assembly?.check?.(map, registry) ?? []).map((problem) => ({
|
|
1936
|
-
rule: "game",
|
|
1937
|
-
...problem
|
|
1938
|
-
}));
|
|
1939
|
-
const silenced = /* @__PURE__ */ new Set([...registry.module?.editor?.silence ?? [], ...map.silence ?? []]);
|
|
1940
|
-
return [...problems, ...own].filter((problem) => !problem.rule || !silenced.has(problem.rule));
|
|
1941
|
-
}
|
|
1942
|
-
//#endregion
|
|
1943
|
-
//#region src/generate.ts
|
|
1944
|
-
/**
|
|
1945
|
-
* Maps made by a generator (world plan G1): a map from a seed and a few sliders, made of passes a game adds to,
|
|
1946
|
-
* replaces or leaves out, each a function of a context that holds the map, the seed's numbers, noise that repeats
|
|
1947
|
-
* across the map, the symmetry and what the passes before put down. Deterministic: the engine's `Rng`, nothing of
|
|
1948
|
-
* the clock. The engine's own passes shape the ground (`terrainPass`), paint it (`tilesPass`), seat the players
|
|
1949
|
-
* (`startsPass`), scatter things over it (`scatterPass`: trees now, rocks and houses the same way later), put
|
|
1950
|
-
* resources near the starts (`resourcesPass`) and lay roads (`roadsPass`); `generateMap` runs them and checks the map
|
|
1951
|
-
* (`checkMap`), trying the next seed where a start is cut off or blocked. A game declares its generator
|
|
1952
|
-
* (`GameModule.generator`), for the editor and the page to find; `presetGenerator` is the engine's to take whole
|
|
1953
|
-
*/
|
|
1954
|
-
/**
|
|
1955
|
-
* The map a generator makes of options: its passes run in order on a map of the size and players asked, with the
|
|
1956
|
-
* game's tiles; then checked (`checkMap`). A map with a problem that keeps it from being played (a start cut off from
|
|
1957
|
-
* the others, or where nothing walks) is thrown away and the next seed tried, `tries` times; the last is given with
|
|
1958
|
-
* its problems. The map says how it was made (`GameMap.generated`)
|
|
1959
|
-
*
|
|
1960
|
-
* @beta
|
|
1961
|
-
*/
|
|
1962
|
-
function generateMap(generator, options, registry) {
|
|
1963
|
-
const tries = Math.max(1, generator.tries ?? 4);
|
|
1964
|
-
let last;
|
|
1965
|
-
for (let attempt = 0; attempt < tries; attempt++) {
|
|
1966
|
-
const seed = options.seed + attempt;
|
|
1967
|
-
const map = createMap({
|
|
1968
|
-
name: options.name ?? `Generated ${seed}`,
|
|
1969
|
-
width: options.width,
|
|
1970
|
-
depth: options.depth,
|
|
1971
|
-
players: options.players,
|
|
1972
|
-
game: registry.module?.id,
|
|
1973
|
-
tileset: registry.tileIds
|
|
1974
|
-
});
|
|
1975
|
-
const ctx = createContext(map, {
|
|
1976
|
-
...options,
|
|
1977
|
-
seed
|
|
1978
|
-
}, registry, generator);
|
|
1979
|
-
for (const pass of generator.passes) pass.run(ctx);
|
|
1980
|
-
map.generated = {
|
|
1981
|
-
seed,
|
|
1982
|
-
options: {
|
|
1983
|
-
...options,
|
|
1984
|
-
seed
|
|
1985
|
-
}
|
|
1986
|
-
};
|
|
1987
|
-
const problems = checkMap(map, registry);
|
|
1988
|
-
last = {
|
|
1989
|
-
map,
|
|
1990
|
-
problems,
|
|
1991
|
-
seed
|
|
1992
|
-
};
|
|
1993
|
-
if (!problems.some(unplayable)) return last;
|
|
1994
|
-
}
|
|
1995
|
-
return last;
|
|
1996
|
-
}
|
|
1997
|
-
/** What keeps a map from being played, so that the next seed is tried: an error, or a start cut off from the others or blocked */
|
|
1998
|
-
var unplayable = (problem) => problem.error || problem.rule === "starts-apart" || problem.rule === "start-blocked";
|
|
1999
|
-
var SYMMETRIES = [
|
|
2000
|
-
"none",
|
|
2001
|
-
"x",
|
|
2002
|
-
"z",
|
|
2003
|
-
"both",
|
|
2004
|
-
"half",
|
|
2005
|
-
"quarter"
|
|
2006
|
-
];
|
|
2007
|
-
/**
|
|
2008
|
-
* The places a point is mirrored or turned to, the point first (as the editor's `mirrors`)
|
|
2009
|
-
*
|
|
2010
|
-
* @beta
|
|
2011
|
-
*/
|
|
2012
|
-
function symmetricPlaces(symmetry, width, depth, x, z) {
|
|
2013
|
-
const out = [{
|
|
2014
|
-
x,
|
|
2015
|
-
z
|
|
2016
|
-
}];
|
|
2017
|
-
const add = (px, pz) => {
|
|
2018
|
-
if (!out.some((other) => Math.abs(other.x - px) < 1e-6 && Math.abs(other.z - pz) < 1e-6)) out.push({
|
|
2019
|
-
x: px,
|
|
2020
|
-
z: pz
|
|
2021
|
-
});
|
|
2022
|
-
};
|
|
2023
|
-
switch (symmetry) {
|
|
2024
|
-
case "x":
|
|
2025
|
-
add(width - x, z);
|
|
2026
|
-
break;
|
|
2027
|
-
case "z":
|
|
2028
|
-
add(x, depth - z);
|
|
2029
|
-
break;
|
|
2030
|
-
case "both":
|
|
2031
|
-
add(width - x, z);
|
|
2032
|
-
add(x, depth - z);
|
|
2033
|
-
add(width - x, depth - z);
|
|
2034
|
-
break;
|
|
2035
|
-
case "half":
|
|
2036
|
-
add(width - x, depth - z);
|
|
2037
|
-
break;
|
|
2038
|
-
case "quarter": {
|
|
2039
|
-
const cx = width / 2;
|
|
2040
|
-
const cz = depth / 2;
|
|
2041
|
-
for (let k = 1; k < 4; k++) {
|
|
2042
|
-
let px = x - cx;
|
|
2043
|
-
let pz = z - cz;
|
|
2044
|
-
for (let i = 0; i < k; i++) [px, pz] = [-pz, px];
|
|
2045
|
-
add(cx + px, cz + pz);
|
|
2046
|
-
}
|
|
2047
|
-
break;
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
return out;
|
|
2051
|
-
}
|
|
2052
|
-
/** How many places a mirrored set has at most: four, for quarter turns and both ways */
|
|
2053
|
-
var MIRRORS = 4;
|
|
2054
|
-
/** The first of a point's mirrored set, by row then column: where a noise is read, so the noise is mirrored as the map is */
|
|
2055
|
-
function firstPlace(symmetry, width, depth, x, z, out) {
|
|
2056
|
-
out.x = x;
|
|
2057
|
-
out.z = z;
|
|
2058
|
-
const take = (px, pz) => takeFirst(out, px, pz);
|
|
2059
|
-
switch (symmetry) {
|
|
2060
|
-
case "x":
|
|
2061
|
-
take(width - x, z);
|
|
2062
|
-
break;
|
|
2063
|
-
case "z":
|
|
2064
|
-
take(x, depth - z);
|
|
2065
|
-
break;
|
|
2066
|
-
case "both":
|
|
2067
|
-
take(width - x, z);
|
|
2068
|
-
take(x, depth - z);
|
|
2069
|
-
take(width - x, depth - z);
|
|
2070
|
-
break;
|
|
2071
|
-
case "half":
|
|
2072
|
-
take(width - x, depth - z);
|
|
2073
|
-
break;
|
|
2074
|
-
case "quarter": {
|
|
2075
|
-
const cx = width / 2;
|
|
2076
|
-
const cz = depth / 2;
|
|
2077
|
-
let px = x - cx;
|
|
2078
|
-
let pz = z - cz;
|
|
2079
|
-
for (let k = 1; k < 4; k++) {
|
|
2080
|
-
[px, pz] = [-pz, px];
|
|
2081
|
-
take(cx + px, cz + pz);
|
|
2082
|
-
}
|
|
2083
|
-
break;
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
/** The earlier of a place and another, by row then column, kept in `out` */
|
|
2088
|
-
function takeFirst(out, px, pz) {
|
|
2089
|
-
if (pz < out.z - 1e-6 || Math.abs(pz - out.z) < 1e-6 && px < out.x) {
|
|
2090
|
-
out.x = px;
|
|
2091
|
-
out.z = pz;
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
function createContext(map, options, registry, generator) {
|
|
2095
|
-
const { width, depth } = map;
|
|
2096
|
-
const symmetry = options.symmetry ?? "none";
|
|
2097
|
-
if (!SYMMETRIES.includes(symmetry)) throw new Error(`generateMap: there is no symmetry "${symmetry}": ${SYMMETRIES.join(", ")}`);
|
|
2098
|
-
if (symmetry === "quarter" && (width !== depth || width % 2)) throw new Error("generateMap: quarter turns need a square map with even sides");
|
|
2099
|
-
const rng = new Rng(options.seed);
|
|
2100
|
-
const defaults = Object.fromEntries(generator.params.map((param) => [param.id, param.default]));
|
|
2101
|
-
const given = options.params ?? {};
|
|
2102
|
-
const noises = /* @__PURE__ */ new Map();
|
|
2103
|
-
const placed = [];
|
|
2104
|
-
let nextId = 1;
|
|
2105
|
-
const mirrors = new Int32Array(width * depth * MIRRORS).fill(-1);
|
|
2106
|
-
const canon = new Int32Array(width * depth);
|
|
2107
|
-
for (let c = 0; c < canon.length; c++) {
|
|
2108
|
-
const cx = c % width;
|
|
2109
|
-
const cz = (c - cx) / width;
|
|
2110
|
-
let first = c;
|
|
2111
|
-
let n = 0;
|
|
2112
|
-
for (const place of symmetricPlaces(symmetry, width, depth, cx + .5, cz + .5)) {
|
|
2113
|
-
const mx = Math.floor(place.x);
|
|
2114
|
-
const mz = Math.floor(place.z);
|
|
2115
|
-
if (mx < 0 || mz < 0 || mx >= width || mz >= depth) continue;
|
|
2116
|
-
const m = mz * width + mx;
|
|
2117
|
-
mirrors[c * MIRRORS + n++] = m;
|
|
2118
|
-
first = Math.min(first, m);
|
|
2119
|
-
}
|
|
2120
|
-
canon[c] = first;
|
|
2121
|
-
}
|
|
2122
|
-
const GRID = 8;
|
|
2123
|
-
const cols = Math.ceil(width / GRID);
|
|
2124
|
-
const rows = Math.ceil(depth / GRID);
|
|
2125
|
-
const grid = Array.from({ length: cols * rows }, () => []);
|
|
2126
|
-
let widest = 0;
|
|
2127
|
-
const point = {
|
|
2128
|
-
x: 0,
|
|
2129
|
-
z: 0
|
|
2130
|
-
};
|
|
2131
|
-
const drawn = /* @__PURE__ */ new Map();
|
|
2132
|
-
return {
|
|
2133
|
-
map,
|
|
2134
|
-
rng,
|
|
2135
|
-
options,
|
|
2136
|
-
registry,
|
|
2137
|
-
param: (id, fallback) => given[id] ?? defaults[id] ?? fallback,
|
|
2138
|
-
symmetric: (x, z) => symmetricPlaces(symmetry, width, depth, x, z),
|
|
2139
|
-
canonical: (cx, cz) => canon[cz * width + cx] === cz * width + cx,
|
|
2140
|
-
once: (x, z, draw) => {
|
|
2141
|
-
const cx = Math.min(Math.max(Math.floor(x), 0), width - 1);
|
|
2142
|
-
const cz = Math.min(Math.max(Math.floor(z), 0), depth - 1);
|
|
2143
|
-
const key = canon[cz * width + cx];
|
|
2144
|
-
if (drawn.has(key)) return drawn.get(key);
|
|
2145
|
-
const value = draw();
|
|
2146
|
-
drawn.set(key, value);
|
|
2147
|
-
return value;
|
|
2148
|
-
},
|
|
2149
|
-
mirrorsOf: (c) => {
|
|
2150
|
-
const out = [];
|
|
2151
|
-
for (let i = 0; i < MIRRORS; i++) {
|
|
2152
|
-
const m = mirrors[c * MIRRORS + i];
|
|
2153
|
-
if (m >= 0) out.push(m);
|
|
2154
|
-
}
|
|
2155
|
-
return out;
|
|
2156
|
-
},
|
|
2157
|
-
setCell: (array, c, value) => {
|
|
2158
|
-
for (let i = 0; i < MIRRORS; i++) {
|
|
2159
|
-
const m = mirrors[c * MIRRORS + i];
|
|
2160
|
-
if (m >= 0) array[m] = value;
|
|
2161
|
-
}
|
|
2162
|
-
},
|
|
2163
|
-
noise(name, cells = [
|
|
2164
|
-
32,
|
|
2165
|
-
16,
|
|
2166
|
-
8
|
|
2167
|
-
]) {
|
|
2168
|
-
let known = noises.get(name);
|
|
2169
|
-
if (!known) {
|
|
2170
|
-
let salt = 0;
|
|
2171
|
-
for (const letter of name) salt = salt * 31 + letter.charCodeAt(0) >>> 0;
|
|
2172
|
-
const size = Math.max(width, depth);
|
|
2173
|
-
const noise = layeredNoise(options.seed + salt, cells.map((each) => Math.max(1, Math.round(size / each))));
|
|
2174
|
-
known = symmetry === "none" ? (x, z) => noise(x / size, z / size) : (x, z) => {
|
|
2175
|
-
firstPlace(symmetry, width, depth, x, z, point);
|
|
2176
|
-
return noise(point.x / size, point.z / size);
|
|
2177
|
-
};
|
|
2178
|
-
noises.set(name, known);
|
|
2179
|
-
}
|
|
2180
|
-
return known;
|
|
2181
|
-
},
|
|
2182
|
-
placed,
|
|
2183
|
-
mark: (kind, x, z, radius) => {
|
|
2184
|
-
placed.push({
|
|
2185
|
-
kind,
|
|
2186
|
-
x,
|
|
2187
|
-
z,
|
|
2188
|
-
radius
|
|
2189
|
-
});
|
|
2190
|
-
widest = Math.max(widest, radius);
|
|
2191
|
-
const gx = Math.min(Math.max(Math.floor(x / GRID), 0), cols - 1);
|
|
2192
|
-
const gz = Math.min(Math.max(Math.floor(z / GRID), 0), rows - 1);
|
|
2193
|
-
grid[gz * cols + gx].push(placed.length - 1);
|
|
2194
|
-
},
|
|
2195
|
-
free: (x, z, radius, kinds) => {
|
|
2196
|
-
const reach = Math.ceil((radius + widest) / GRID);
|
|
2197
|
-
const gx = Math.floor(x / GRID);
|
|
2198
|
-
const gz = Math.floor(z / GRID);
|
|
2199
|
-
for (let dz = -reach; dz <= reach; dz++) for (let dx = -reach; dx <= reach; dx++) {
|
|
2200
|
-
const gxx = gx + dx;
|
|
2201
|
-
const gzz = gz + dz;
|
|
2202
|
-
if (gxx < 0 || gzz < 0 || gxx >= cols || gzz >= rows) continue;
|
|
2203
|
-
for (const i of grid[gzz * cols + gxx]) {
|
|
2204
|
-
const each = placed[i];
|
|
2205
|
-
if (kinds && !kinds.includes(each.kind)) continue;
|
|
2206
|
-
if (hypot(each.x - x, each.z - z) < radius + each.radius) return false;
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
return true;
|
|
2210
|
-
},
|
|
2211
|
-
levelAt: (x, z) => {
|
|
2212
|
-
const cx = Math.min(Math.max(Math.floor(x), 0), width - 1);
|
|
2213
|
-
const cz = Math.min(Math.max(Math.floor(z), 0), depth - 1);
|
|
2214
|
-
return map.levels[cz * width + cx];
|
|
2215
|
-
},
|
|
2216
|
-
walkable: (x, z) => {
|
|
2217
|
-
if (x < 0 || z < 0 || x >= width || z >= depth) return false;
|
|
2218
|
-
return map.water[Math.floor(z) * width + Math.floor(x)] !== 2;
|
|
2219
|
-
},
|
|
2220
|
-
addDoodad(doodad) {
|
|
2221
|
-
const { scale = 1, yaw = 0, ...rest } = doodad;
|
|
2222
|
-
const made = {
|
|
2223
|
-
id: nextId++,
|
|
2224
|
-
yaw,
|
|
2225
|
-
scale: typeof scale === "number" ? [
|
|
2226
|
-
scale,
|
|
2227
|
-
scale,
|
|
2228
|
-
scale
|
|
2229
|
-
] : scale,
|
|
2230
|
-
...rest
|
|
2231
|
-
};
|
|
2232
|
-
map.doodads.push(made);
|
|
2233
|
-
return made;
|
|
2234
|
-
},
|
|
2235
|
-
addObject(type, owner, x, z, more = {}) {
|
|
2236
|
-
const made = {
|
|
2237
|
-
id: nextId++,
|
|
2238
|
-
type,
|
|
2239
|
-
owner,
|
|
2240
|
-
x,
|
|
2241
|
-
z,
|
|
2242
|
-
facing: 0,
|
|
2243
|
-
scale: 1,
|
|
2244
|
-
...more
|
|
2245
|
-
};
|
|
2246
|
-
map.objects.push(made);
|
|
2247
|
-
return made;
|
|
2248
|
-
},
|
|
2249
|
-
addCamp(x, z, units, more = {}) {
|
|
2250
|
-
const made = {
|
|
2251
|
-
id: nextId++,
|
|
2252
|
-
x,
|
|
2253
|
-
z,
|
|
2254
|
-
units,
|
|
2255
|
-
...more
|
|
2256
|
-
};
|
|
2257
|
-
map.camps.push(made);
|
|
2258
|
-
return made;
|
|
2259
|
-
},
|
|
2260
|
-
addRegion(name, shape, more = {}) {
|
|
2261
|
-
const made = {
|
|
2262
|
-
id: nextId++,
|
|
2263
|
-
name,
|
|
2264
|
-
shape,
|
|
2265
|
-
...more
|
|
2266
|
-
};
|
|
2267
|
-
map.regions.push(made);
|
|
2268
|
-
return made;
|
|
2269
|
-
}
|
|
2270
|
-
};
|
|
2271
|
-
}
|
|
2272
|
-
/**
|
|
2273
|
-
* The lie of a bay, for `terrainPass`'s `shape`: the corners high, the ground falling to a sea in the middle, an
|
|
2274
|
-
* island in it. With `water` at 0.25–0.35 the sea takes the middle and the shore runs round it; the starts in the
|
|
2275
|
-
* corners (`startsPass` with a spread of 0.4), the way between them along the shore
|
|
2276
|
-
*
|
|
2277
|
-
* @beta
|
|
2278
|
-
*/
|
|
2279
|
-
function basinShape(options = {}) {
|
|
2280
|
-
const island = Math.min(Math.max(options.island ?? .14, 0), .5);
|
|
2281
|
-
const roughness = Math.min(Math.max(options.roughness ?? .25, 0), 1);
|
|
2282
|
-
const height = Math.min(Math.max(options.height ?? .4, 0), 1);
|
|
2283
|
-
return (x, z, noise, map) => {
|
|
2284
|
-
const d = hypot((x / map.width - .5) * 2, (z / map.depth - .5) * 2) / Math.SQRT2;
|
|
2285
|
-
const r = island > 0 ? d / island : 1;
|
|
2286
|
-
return d + (r < 1 ? (1 - r * r * r * r) * height : 0) + (noise - .5) * roughness;
|
|
2287
|
-
};
|
|
2288
|
-
}
|
|
2289
|
-
/** Whether a cell is on the map */
|
|
2290
|
-
var on = (map, cx, cz) => cx >= 0 && cz >= 0 && cx < map.width && cz < map.depth;
|
|
2291
|
-
/** The four cells next to one */
|
|
2292
|
-
var NEIGHBOURS = [
|
|
2293
|
-
[-1, 0],
|
|
2294
|
-
[1, 0],
|
|
2295
|
-
[0, -1],
|
|
2296
|
-
[0, 1]
|
|
2297
|
-
];
|
|
2298
|
-
/** How steep a cell is, in radians, from the heights at its corners: as `Terrain.slopeAt` has it, read straight off the terrain */
|
|
2299
|
-
function cellSlope(terrain, cx, cz) {
|
|
2300
|
-
const { heights, vertsX } = terrain;
|
|
2301
|
-
const v = cz * vertsX + cx;
|
|
2302
|
-
const dx = (heights[v + 1] + heights[v + vertsX + 1] - heights[v] - heights[v + vertsX]) / 2;
|
|
2303
|
-
const dz = (heights[v + vertsX] + heights[v + vertsX + 1] - heights[v] - heights[v + 1]) / 2;
|
|
2304
|
-
return atan(hypot(dx, dz));
|
|
2305
|
-
}
|
|
2306
|
-
/** The engine's passes reach the mirror table of the context they are given; a context of a game's own has none, and mirrors by `symmetric` */
|
|
2307
|
-
function internals(ctx) {
|
|
2308
|
-
const own = ctx;
|
|
2309
|
-
if (own.mirrorsOf && own.setCell) return own;
|
|
2310
|
-
const { width } = ctx.map;
|
|
2311
|
-
const mirrorsOf = (c) => {
|
|
2312
|
-
const cx = c % width;
|
|
2313
|
-
const cz = (c - cx) / width;
|
|
2314
|
-
return ctx.symmetric(cx + .5, cz + .5).flatMap((place) => {
|
|
2315
|
-
const mx = Math.floor(place.x);
|
|
2316
|
-
const mz = Math.floor(place.z);
|
|
2317
|
-
return on(ctx.map, mx, mz) ? [mz * width + mx] : [];
|
|
2318
|
-
});
|
|
2319
|
-
};
|
|
2320
|
-
return {
|
|
2321
|
-
mirrorsOf,
|
|
2322
|
-
setCell: (array, c, value) => mirrorsOf(c).forEach((m) => array[m] = value)
|
|
2323
|
-
};
|
|
2324
|
-
}
|
|
2325
|
-
/**
|
|
2326
|
-
* The ground: a height field of noise, mirrored as the map is, quantised into cliff levels with the ground rolling
|
|
2327
|
-
* within each, water in the low ground, and ramps cut between the levels so that every part can be walked to
|
|
2328
|
-
*
|
|
2329
|
-
* @beta
|
|
2330
|
-
*/
|
|
2331
|
-
function terrainPass(options = {}) {
|
|
2332
|
-
return {
|
|
2333
|
-
id: "terrain",
|
|
2334
|
-
name: "The ground",
|
|
2335
|
-
run(ctx) {
|
|
2336
|
-
const { map } = ctx;
|
|
2337
|
-
const { width, depth } = map;
|
|
2338
|
-
const levels = Math.min(Math.max(Math.round(options.levels ?? ctx.param("levels", 2)), 1), 4);
|
|
2339
|
-
const waterShare = Math.min(Math.max(options.water ?? ctx.param("water", .15), 0), .6);
|
|
2340
|
-
const roughness = Math.min(Math.max(options.roughness ?? ctx.param("roughness", .4), 0), 1);
|
|
2341
|
-
const hills = Math.max(4, options.hills ?? ctx.param("hills", 24));
|
|
2342
|
-
const plateau = ctx.noise("plateau", [hills * 2, hills]);
|
|
2343
|
-
const roll = ctx.noise("roll", [
|
|
2344
|
-
hills / 2,
|
|
2345
|
-
hills / 4,
|
|
2346
|
-
hills / 8
|
|
2347
|
-
]);
|
|
2348
|
-
const values = new Float32Array(width * depth);
|
|
2349
|
-
for (let c = 0; c < values.length; c++) {
|
|
2350
|
-
const x = c % width + .5;
|
|
2351
|
-
const z = Math.floor(c / width) + .5;
|
|
2352
|
-
values[c] = options.shape ? options.shape(x, z, plateau(x, z), map) : plateau(x, z);
|
|
2353
|
-
}
|
|
2354
|
-
const stride = Math.max(1, Math.floor(values.length / 16384));
|
|
2355
|
-
const sorted = Float32Array.from({ length: Math.floor(values.length / stride) }, (_, i) => values[i * stride]).sort();
|
|
2356
|
-
const quantile = (q) => sorted[Math.min(Math.max(Math.round(q * (sorted.length - 1)), 0), sorted.length - 1)];
|
|
2357
|
-
const waterAt = quantile(waterShare);
|
|
2358
|
-
const steps = Array.from({ length: levels - 1 }, (_, i) => quantile(waterShare + (1 - waterShare) * (i + 1) / levels));
|
|
2359
|
-
for (let c = 0; c < values.length; c++) {
|
|
2360
|
-
let level = 0;
|
|
2361
|
-
for (const step of steps) if (values[c] >= step) level++;
|
|
2362
|
-
map.levels[c] = level;
|
|
2363
|
-
map.water[c] = values[c] < waterAt ? 2 : 0;
|
|
2364
|
-
}
|
|
2365
|
-
const { setCell } = internals(ctx);
|
|
2366
|
-
const counts = /* @__PURE__ */ new Int32Array(5);
|
|
2367
|
-
for (let round = 0; round < 2; round++) {
|
|
2368
|
-
const before = map.levels.slice();
|
|
2369
|
-
for (let c = 0; c < before.length; c++) {
|
|
2370
|
-
const cx = c % width;
|
|
2371
|
-
const cz = (c - cx) / width;
|
|
2372
|
-
if (!ctx.canonical(cx, cz)) continue;
|
|
2373
|
-
counts.fill(0);
|
|
2374
|
-
for (let dz = -1; dz <= 1; dz++) for (let dx = -1; dx <= 1; dx++) if (on(map, cx + dx, cz + dz)) counts[before[(cz + dz) * width + cx + dx]]++;
|
|
2375
|
-
let best = before[c];
|
|
2376
|
-
let most = counts[best];
|
|
2377
|
-
for (let level = 0; level < counts.length; level++) if (counts[level] > most) [best, most] = [level, counts[level]];
|
|
2378
|
-
if (best !== before[c]) setCell(map.levels, c, best);
|
|
2379
|
-
}
|
|
2380
|
-
}
|
|
2381
|
-
for (let c = 0; c < values.length; c++) if (map.levels[c] > 0) map.water[c] = 0;
|
|
2382
|
-
const deep = map.water.slice();
|
|
2383
|
-
for (let c = 0; c < deep.length; c++) {
|
|
2384
|
-
if (deep[c] !== 2) continue;
|
|
2385
|
-
const cx = c % width;
|
|
2386
|
-
const cz = (c - cx) / width;
|
|
2387
|
-
let rim = false;
|
|
2388
|
-
for (const [dx, dz] of NEIGHBOURS) if (on(map, cx + dx, cz + dz) && deep[(cz + dz) * width + cx + dx] !== 2) rim = true;
|
|
2389
|
-
if (rim) map.water[c] = 1;
|
|
2390
|
-
}
|
|
2391
|
-
const vx = width + 1;
|
|
2392
|
-
for (let iz = 0; iz <= depth; iz++) for (let ix = 0; ix <= width; ix++) {
|
|
2393
|
-
const v = iz * vx + ix;
|
|
2394
|
-
map.ground[v] = 1 + (roll(ix, iz) - .5) * roughness * 1.5;
|
|
2395
|
-
}
|
|
2396
|
-
if (options.ramps !== false) cutRamps(ctx, Math.max(2, Math.round(options.rampWidth ?? 3)));
|
|
2397
|
-
}
|
|
2398
|
-
};
|
|
2399
|
-
}
|
|
2400
|
-
/**
|
|
2401
|
-
* Ramps between the levels, so that every part of the ground joins the rest: the parts of a level (a flood fill on the
|
|
2402
|
-
* level map, water left out) joined by a ramp each across the straightest stretch of cliff between two of them, the
|
|
2403
|
-
* nearest the middle of both first, until all are one; mirrored as the map is
|
|
2404
|
-
*/
|
|
2405
|
-
function cutRamps(ctx, rampWidth) {
|
|
2406
|
-
const { map } = ctx;
|
|
2407
|
-
const { width, depth, levels, water, ramps } = map;
|
|
2408
|
-
const { setCell } = internals(ctx);
|
|
2409
|
-
const part = new Int32Array(width * depth).fill(-1);
|
|
2410
|
-
const parts = [];
|
|
2411
|
-
const queue = new Int32Array(width * depth);
|
|
2412
|
-
for (let start = 0; start < part.length; start++) {
|
|
2413
|
-
if (part[start] >= 0 || water[start] === 2) continue;
|
|
2414
|
-
const id = parts.length;
|
|
2415
|
-
const sum = {
|
|
2416
|
-
cells: 0,
|
|
2417
|
-
x: 0,
|
|
2418
|
-
z: 0
|
|
2419
|
-
};
|
|
2420
|
-
parts.push(sum);
|
|
2421
|
-
let head = 0;
|
|
2422
|
-
let tail = 0;
|
|
2423
|
-
queue[tail++] = start;
|
|
2424
|
-
part[start] = id;
|
|
2425
|
-
while (head < tail) {
|
|
2426
|
-
const c = queue[head++];
|
|
2427
|
-
const cx = c % width;
|
|
2428
|
-
const cz = (c - cx) / width;
|
|
2429
|
-
sum.cells++;
|
|
2430
|
-
sum.x += cx + .5;
|
|
2431
|
-
sum.z += cz + .5;
|
|
2432
|
-
for (const [dx, dz] of NEIGHBOURS) {
|
|
2433
|
-
const nx = cx + dx;
|
|
2434
|
-
const nz = cz + dz;
|
|
2435
|
-
if (!on(map, nx, nz)) continue;
|
|
2436
|
-
const n = nz * width + nx;
|
|
2437
|
-
if (part[n] >= 0 || water[n] === 2 || levels[n] !== levels[c]) continue;
|
|
2438
|
-
part[n] = id;
|
|
2439
|
-
queue[tail++] = n;
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
}
|
|
2443
|
-
for (const each of parts) {
|
|
2444
|
-
each.x /= each.cells;
|
|
2445
|
-
each.z /= each.cells;
|
|
2446
|
-
}
|
|
2447
|
-
if (parts.length < 2) return;
|
|
2448
|
-
const half = Math.floor(rampWidth / 2);
|
|
2449
|
-
const candidates = [];
|
|
2450
|
-
const straight = (cx, cz, dx, dz) => {
|
|
2451
|
-
const low = cz * width + cx;
|
|
2452
|
-
const high = (cz + dz) * width + cx + dx;
|
|
2453
|
-
if (Math.abs(levels[low] - levels[high]) !== 1) return void 0;
|
|
2454
|
-
const cells = [];
|
|
2455
|
-
for (let along = -half; along < rampWidth - half; along++) for (let deep = -1; deep <= 2; deep++) {
|
|
2456
|
-
const x = cx + dz * along + dx * deep;
|
|
2457
|
-
const z = cz + dx * along + dz * deep;
|
|
2458
|
-
if (!on(map, x, z)) return void 0;
|
|
2459
|
-
const c = z * width + x;
|
|
2460
|
-
if (water[c] !== 0) return void 0;
|
|
2461
|
-
if (levels[c] !== (deep <= 0 ? levels[low] : levels[high])) return void 0;
|
|
2462
|
-
if (deep >= 0 && deep <= 1) cells.push(c);
|
|
2463
|
-
}
|
|
2464
|
-
return cells;
|
|
2465
|
-
};
|
|
2466
|
-
for (let cz = 0; cz < depth; cz++) for (let cx = 0; cx < width; cx++) {
|
|
2467
|
-
if (!ctx.canonical(cx, cz)) continue;
|
|
2468
|
-
for (const [dx, dz] of [[1, 0], [0, 1]]) {
|
|
2469
|
-
if (!on(map, cx + dx, cz + dz)) continue;
|
|
2470
|
-
const cells = straight(cx, cz, dx, dz);
|
|
2471
|
-
if (!cells) continue;
|
|
2472
|
-
const a = part[cz * width + cx];
|
|
2473
|
-
const b = part[(cz + dz) * width + cx + dx];
|
|
2474
|
-
if (a < 0 || b < 0 || a === b) continue;
|
|
2475
|
-
const mx = (parts[a].x + parts[b].x) / 2;
|
|
2476
|
-
const mz = (parts[a].z + parts[b].z) / 2;
|
|
2477
|
-
candidates.push({
|
|
2478
|
-
a,
|
|
2479
|
-
b,
|
|
2480
|
-
cells,
|
|
2481
|
-
cost: hypot(cx + .5 - mx, cz + .5 - mz)
|
|
2482
|
-
});
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
2485
|
-
candidates.sort((p, q) => p.cost - q.cost);
|
|
2486
|
-
const root = parts.map((_, i) => i);
|
|
2487
|
-
const find = (i) => root[i] === i ? i : root[i] = find(root[i]);
|
|
2488
|
-
for (const candidate of candidates) {
|
|
2489
|
-
const a = find(candidate.a);
|
|
2490
|
-
const b = find(candidate.b);
|
|
2491
|
-
if (a === b) continue;
|
|
2492
|
-
root[a] = b;
|
|
2493
|
-
for (const c of candidate.cells) setCell(ramps, c, 1);
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
/**
|
|
2497
|
-
* The paint: each cell a tile by what the ground is there, through the game's tiles by name
|
|
2498
|
-
*
|
|
2499
|
-
* @beta
|
|
2500
|
-
*/
|
|
2501
|
-
function tilesPass(options = {}) {
|
|
2502
|
-
return {
|
|
2503
|
-
id: "tiles",
|
|
2504
|
-
name: "The paint",
|
|
2505
|
-
run(ctx) {
|
|
2506
|
-
const { map, registry } = ctx;
|
|
2507
|
-
const { width, depth } = map;
|
|
2508
|
-
const tiles = registry.tileIds;
|
|
2509
|
-
if (!tiles.length) return;
|
|
2510
|
-
const terrain = mapTerrain(map);
|
|
2511
|
-
const moisture = ctx.noise("moisture", [20, 10]);
|
|
2512
|
-
let top = 0;
|
|
2513
|
-
for (const level of map.levels) top = Math.max(top, level);
|
|
2514
|
-
const by = options.by ?? ((cell, ids) => {
|
|
2515
|
-
const has = (id) => ids.includes(id) ? id : void 0;
|
|
2516
|
-
if (cell.shore) return has("sand");
|
|
2517
|
-
if (cell.slope > .5) return has("rock");
|
|
2518
|
-
if (cell.level === top && top >= 3) return has("snow") ?? has("rock");
|
|
2519
|
-
if (cell.moisture < .35) return has("dirt");
|
|
2520
|
-
});
|
|
2521
|
-
for (let cz = 0; cz < depth; cz++) for (let cx = 0; cx < width; cx++) {
|
|
2522
|
-
const c = cz * width + cx;
|
|
2523
|
-
if (map.water[c] === 2) continue;
|
|
2524
|
-
let shore = map.water[c] === 1;
|
|
2525
|
-
if (!shore) {
|
|
2526
|
-
for (const [dx, dz] of NEIGHBOURS) if (on(map, cx + dx, cz + dz) && map.water[(cz + dz) * width + cx + dx] !== 0) shore = true;
|
|
2527
|
-
}
|
|
2528
|
-
const id = by({
|
|
2529
|
-
x: cx,
|
|
2530
|
-
z: cz,
|
|
2531
|
-
level: map.levels[c],
|
|
2532
|
-
slope: cellSlope(terrain, cx, cz),
|
|
2533
|
-
moisture: moisture(cx + .5, cz + .5),
|
|
2534
|
-
shore
|
|
2535
|
-
}, tiles);
|
|
2536
|
-
map.tiles[c] = id ? registry.tile(id) : 0;
|
|
2537
|
-
}
|
|
2538
|
-
}
|
|
2539
|
-
};
|
|
2540
|
-
}
|
|
2541
|
-
/**
|
|
2542
|
-
* The starts: as far apart as the symmetry allows (mirrored from one, or around the middle), each on flat, dry ground
|
|
2543
|
-
* (a clearing made for it), the player's `start` set, and what the game puts there
|
|
2544
|
-
*
|
|
2545
|
-
* @beta
|
|
2546
|
-
*/
|
|
2547
|
-
function startsPass(options = {}) {
|
|
2548
|
-
return {
|
|
2549
|
-
id: "starts",
|
|
2550
|
-
name: "The starts",
|
|
2551
|
-
run(ctx) {
|
|
2552
|
-
const { map, rng } = ctx;
|
|
2553
|
-
const { width, depth } = map;
|
|
2554
|
-
const players = map.players.filter((player) => player.id !== 0 && player.id !== 90);
|
|
2555
|
-
if (!players.length) return;
|
|
2556
|
-
const clearing = Math.max(2, options.clearing ?? 6);
|
|
2557
|
-
const spread = Math.min(Math.max(options.spread ?? .36, .1), .48);
|
|
2558
|
-
const symmetry = ctx.options.symmetry ?? "none";
|
|
2559
|
-
const setSize = ctx.symmetric(width * .3, depth * .2).length;
|
|
2560
|
-
const seeds = Math.ceil(players.length / setSize);
|
|
2561
|
-
const places = [];
|
|
2562
|
-
const angle0 = symmetry === "none" ? rng.next() * Math.PI * 2 : symmetry === "x" ? Math.PI : symmetry === "z" ? Math.PI / 2 : Math.PI * 1.25;
|
|
2563
|
-
for (let i = 0; i < seeds + players.length && places.length < players.length; i++) {
|
|
2564
|
-
const angle = angle0 + i * Math.PI * 2 / Math.max(seeds * setSize, 1);
|
|
2565
|
-
const x = width / 2 + cos(angle) * width * spread;
|
|
2566
|
-
const z = depth / 2 + sin(angle) * depth * spread;
|
|
2567
|
-
for (const place of ctx.symmetric(x, z)) if (places.length < players.length) places.push(place);
|
|
2568
|
-
}
|
|
2569
|
-
players.forEach((player, i) => {
|
|
2570
|
-
const at = dryGround(ctx, places[i], clearing);
|
|
2571
|
-
clear(ctx, at, clearing);
|
|
2572
|
-
player.start = {
|
|
2573
|
-
x: at.x,
|
|
2574
|
-
z: at.z
|
|
2575
|
-
};
|
|
2576
|
-
ctx.mark("start", at.x, at.z, clearing);
|
|
2577
|
-
options.from?.(player, at, ctx);
|
|
2578
|
-
});
|
|
2579
|
-
}
|
|
2580
|
-
};
|
|
2581
|
-
}
|
|
2582
|
-
/**
|
|
2583
|
-
* The nearest place to one that is not in the water, at a cell's middle: for a pass of a game's own that puts something
|
|
2584
|
-
* down where the ground may be wet
|
|
2585
|
-
*
|
|
2586
|
-
* @beta
|
|
2587
|
-
*/
|
|
2588
|
-
var nearestDryGround = (ctx, at, reach) => dryGround(ctx, at, reach);
|
|
2589
|
-
/**
|
|
2590
|
-
* The ground around a place made flat and dry: one level (the one there, or `level`), no water, no ramps, the ground at
|
|
2591
|
-
* one height eased into what is around; mirrored as the map is, unless `alone`. What a start's clearing and a mine's
|
|
2592
|
-
* are made with, for a pass of a game's own
|
|
2593
|
-
*
|
|
2594
|
-
* @beta
|
|
2595
|
-
*/
|
|
2596
|
-
var clearGround = (ctx, at, radius, alone = false, level) => clear(ctx, at, radius, alone, level ?? ctx.levelAt(at.x, at.z));
|
|
2597
|
-
/** The nearest place to one that is not in the water, at a cell's middle */
|
|
2598
|
-
function dryGround(ctx, at, reach) {
|
|
2599
|
-
const { map } = ctx;
|
|
2600
|
-
const { width, depth } = map;
|
|
2601
|
-
const cx = Math.min(Math.max(Math.floor(at.x), 1), width - 2);
|
|
2602
|
-
const cz = Math.min(Math.max(Math.floor(at.z), 1), depth - 2);
|
|
2603
|
-
for (let r = 0; r <= reach * 3; r++) for (let dz = -r; dz <= r; dz++) for (let dx = -r; dx <= r; dx++) {
|
|
2604
|
-
if (Math.max(Math.abs(dx), Math.abs(dz)) !== r) continue;
|
|
2605
|
-
const x = cx + dx;
|
|
2606
|
-
const z = cz + dz;
|
|
2607
|
-
if (x < 1 || z < 1 || x >= width - 1 || z >= depth - 1) continue;
|
|
2608
|
-
if (map.water[z * width + x] === 0) return {
|
|
2609
|
-
x: x + .5,
|
|
2610
|
-
z: z + .5
|
|
2611
|
-
};
|
|
2612
|
-
}
|
|
2613
|
-
return {
|
|
2614
|
-
x: cx + .5,
|
|
2615
|
-
z: cz + .5
|
|
2616
|
-
};
|
|
2617
|
-
}
|
|
2618
|
-
/** The ground around a place made flat and dry: one level, no water, no ramps, the ground at one height. Mirrored, unless `alone` */
|
|
2619
|
-
function clear(ctx, at, radius, alone = false, level = ctx.levelAt(at.x, at.z)) {
|
|
2620
|
-
const { map } = ctx;
|
|
2621
|
-
const { width, depth } = map;
|
|
2622
|
-
const vx = width + 1;
|
|
2623
|
-
let height = 0;
|
|
2624
|
-
let count = 0;
|
|
2625
|
-
for (let iz = Math.max(Math.floor(at.z - radius), 0); iz <= Math.min(Math.ceil(at.z + radius), depth); iz++) for (let ix = Math.max(Math.floor(at.x - radius), 0); ix <= Math.min(Math.ceil(at.x + radius), width); ix++) {
|
|
2626
|
-
height += map.ground[iz * vx + ix];
|
|
2627
|
-
count++;
|
|
2628
|
-
}
|
|
2629
|
-
height /= Math.max(count, 1);
|
|
2630
|
-
for (const place of alone ? [at] : ctx.symmetric(at.x, at.z)) {
|
|
2631
|
-
for (let cz = Math.max(Math.floor(place.z - radius), 0); cz < Math.min(Math.ceil(place.z + radius), depth); cz++) for (let cx = Math.max(Math.floor(place.x - radius), 0); cx < Math.min(Math.ceil(place.x + radius), width); cx++) {
|
|
2632
|
-
if (hypot(cx + .5 - place.x, cz + .5 - place.z) > radius) continue;
|
|
2633
|
-
const c = cz * width + cx;
|
|
2634
|
-
map.levels[c] = level;
|
|
2635
|
-
map.water[c] = 0;
|
|
2636
|
-
map.ramps[c] = 0;
|
|
2637
|
-
}
|
|
2638
|
-
for (let iz = Math.max(Math.floor(place.z - radius), 0); iz <= Math.min(Math.ceil(place.z + radius), depth); iz++) for (let ix = Math.max(Math.floor(place.x - radius), 0); ix <= Math.min(Math.ceil(place.x + radius), width); ix++) {
|
|
2639
|
-
const d = hypot(ix - place.x, iz - place.z);
|
|
2640
|
-
if (d > radius) continue;
|
|
2641
|
-
const t = Math.min(Math.max((d - radius * .6) / (radius * .4), 0), 1);
|
|
2642
|
-
const v = iz * vx + ix;
|
|
2643
|
-
map.ground[v] = height + (map.ground[v] - height) * t * t * (3 - 2 * t);
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
/**
|
|
2648
|
-
* Things scattered over the map: as many as the density says, in patches of a noise, at least `spacing` apart, off
|
|
2649
|
-
* the starts and what else they avoid, where they may go; each put down by `what`, mirrored as the map is. Trees now;
|
|
2650
|
-
* rocks, houses, ruins and the rest the same way, with another `what`
|
|
2651
|
-
*
|
|
2652
|
-
* @beta
|
|
2653
|
-
*/
|
|
2654
|
-
function scatterPass(options) {
|
|
2655
|
-
return {
|
|
2656
|
-
id: `scatter:${options.kind}`,
|
|
2657
|
-
name: `Scattered: ${options.kind}`,
|
|
2658
|
-
run(ctx) {
|
|
2659
|
-
const { map, rng } = ctx;
|
|
2660
|
-
const { width, depth } = map;
|
|
2661
|
-
const density = Math.min(Math.max(options.density ?? ctx.param(options.kind, .12), 0), 1);
|
|
2662
|
-
if (density <= 0) return;
|
|
2663
|
-
const spacing = Math.max(.5, options.spacing ?? 1.5);
|
|
2664
|
-
const radius = options.radius ?? spacing / 2;
|
|
2665
|
-
const avoid = options.avoid ?? 10;
|
|
2666
|
-
const patches = Math.min(Math.max(options.patches ?? .6, 0), 1);
|
|
2667
|
-
const noise = ctx.noise(`scatter:${options.kind}`, [
|
|
2668
|
-
24,
|
|
2669
|
-
12,
|
|
2670
|
-
6
|
|
2671
|
-
]);
|
|
2672
|
-
let made;
|
|
2673
|
-
const terrain = () => made ??= mapTerrain(map);
|
|
2674
|
-
const cell = spacing;
|
|
2675
|
-
const cols = Math.ceil(width / cell);
|
|
2676
|
-
const rows = Math.ceil(depth / cell);
|
|
2677
|
-
const grid = Array.from({ length: cols * rows }, () => []);
|
|
2678
|
-
const tooClose = (x, z) => {
|
|
2679
|
-
const gx = Math.floor(x / cell);
|
|
2680
|
-
const gz = Math.floor(z / cell);
|
|
2681
|
-
for (let dz = -1; dz <= 1; dz++) for (let dx = -1; dx <= 1; dx++) {
|
|
2682
|
-
const gxx = gx + dx;
|
|
2683
|
-
const gzz = gz + dz;
|
|
2684
|
-
if (gxx < 0 || gzz < 0 || gxx >= cols || gzz >= rows) continue;
|
|
2685
|
-
for (const other of grid[gzz * cols + gxx]) if (hypot(other.x - x, other.z - z) < spacing) return true;
|
|
2686
|
-
}
|
|
2687
|
-
return false;
|
|
2688
|
-
};
|
|
2689
|
-
const put = (x, z) => {
|
|
2690
|
-
grid[Math.floor(z / cell) * cols + Math.floor(x / cell)].push({
|
|
2691
|
-
x,
|
|
2692
|
-
z
|
|
2693
|
-
});
|
|
2694
|
-
ctx.mark(options.kind, x, z, radius);
|
|
2695
|
-
options.what(ctx, {
|
|
2696
|
-
x,
|
|
2697
|
-
z
|
|
2698
|
-
});
|
|
2699
|
-
};
|
|
2700
|
-
for (let cz = 0; cz < depth; cz++) for (let cx = 0; cx < width; cx++) {
|
|
2701
|
-
if (!ctx.canonical(cx, cz)) continue;
|
|
2702
|
-
const c = cz * width + cx;
|
|
2703
|
-
const x = cx + rng.next();
|
|
2704
|
-
const z = cz + rng.next();
|
|
2705
|
-
const n = noise(x, z);
|
|
2706
|
-
const chance = density * (1 - patches + patches * Math.min(Math.max((n - .35) / .3, 0), 1) * 2);
|
|
2707
|
-
if (rng.next() >= chance) continue;
|
|
2708
|
-
if (map.water[c] !== 0) continue;
|
|
2709
|
-
if (options.where) {
|
|
2710
|
-
const shore = NEIGHBOURS.some(([dx, dz]) => on(map, cx + dx, cz + dz) && map.water[(cz + dz) * width + cx + dx] !== 0);
|
|
2711
|
-
if (!options.where({
|
|
2712
|
-
x,
|
|
2713
|
-
z,
|
|
2714
|
-
level: map.levels[c],
|
|
2715
|
-
slope: cellSlope(terrain(), cx, cz),
|
|
2716
|
-
noise: n,
|
|
2717
|
-
shore
|
|
2718
|
-
})) continue;
|
|
2719
|
-
}
|
|
2720
|
-
if (map.ramps[c]) continue;
|
|
2721
|
-
if (!ctx.free(x, z, avoid, ["start"])) continue;
|
|
2722
|
-
if (options.avoidKinds && Object.entries(options.avoidKinds).some(([kind, far]) => !ctx.free(x, z, far, [kind]))) continue;
|
|
2723
|
-
const places = ctx.symmetric(x, z).filter((place) => place.x >= 0 && place.z >= 0 && place.x < width && place.z < depth);
|
|
2724
|
-
if (places.some((place) => tooClose(place.x, place.z))) continue;
|
|
2725
|
-
for (const place of places) put(place.x, place.z);
|
|
2726
|
-
}
|
|
2727
|
-
}
|
|
2728
|
-
};
|
|
2729
|
-
}
|
|
2730
|
-
/**
|
|
2731
|
-
* Resources: one near each start (towards the map's middle, as far as `main` says), and more spread over the map,
|
|
2732
|
-
* mirrored as the map is, each put down by the game
|
|
2733
|
-
*
|
|
2734
|
-
* @beta
|
|
2735
|
-
*/
|
|
2736
|
-
function resourcesPass(options) {
|
|
2737
|
-
return {
|
|
2738
|
-
id: "resources",
|
|
2739
|
-
name: "The resources",
|
|
2740
|
-
run(ctx) {
|
|
2741
|
-
const { map, rng } = ctx;
|
|
2742
|
-
const { width, depth } = map;
|
|
2743
|
-
const main = options.main ?? 8;
|
|
2744
|
-
const spacing = options.spacing ?? 20;
|
|
2745
|
-
const clearing = options.clearing ?? 3;
|
|
2746
|
-
for (const player of map.players) {
|
|
2747
|
-
if (!player.start || main === false) continue;
|
|
2748
|
-
const toward = atan2(depth / 2 - player.start.z, width / 2 - player.start.x) + (rng.next() - .5) * .8;
|
|
2749
|
-
const at = dryGround(ctx, {
|
|
2750
|
-
x: player.start.x + cos(toward) * main,
|
|
2751
|
-
z: player.start.z + sin(toward) * main
|
|
2752
|
-
}, 4);
|
|
2753
|
-
if (clearing > 0) clear(ctx, at, clearing, true, ctx.levelAt(player.start.x, player.start.z));
|
|
2754
|
-
ctx.mark("resource", at.x, at.z, 3);
|
|
2755
|
-
options.from(ctx, at, player);
|
|
2756
|
-
}
|
|
2757
|
-
const extra = Math.max(0, Math.round(options.extra ?? 2));
|
|
2758
|
-
let placed = 0;
|
|
2759
|
-
for (let attempt = 0; attempt < extra * 40 && placed < extra; attempt++) {
|
|
2760
|
-
const x = 4 + rng.next() * (width - 8);
|
|
2761
|
-
const z = 4 + rng.next() * (depth - 8);
|
|
2762
|
-
if (!ctx.canonical(Math.floor(x), Math.floor(z))) continue;
|
|
2763
|
-
const places = ctx.symmetric(x, z);
|
|
2764
|
-
if (!places.every((place) => ctx.walkable(place.x, place.z) && ctx.free(place.x, place.z, spacing, ["start", "resource"]))) continue;
|
|
2765
|
-
for (const place of places) {
|
|
2766
|
-
const at = dryGround(ctx, place, 4);
|
|
2767
|
-
if (clearing > 0) clear(ctx, at, clearing, true);
|
|
2768
|
-
ctx.mark("resource", at.x, at.z, 3);
|
|
2769
|
-
options.from(ctx, at);
|
|
2770
|
-
}
|
|
2771
|
-
placed++;
|
|
2772
|
-
}
|
|
2773
|
-
}
|
|
2774
|
-
};
|
|
2775
|
-
}
|
|
2776
|
-
/**
|
|
2777
|
-
* Roads between the starts (or places given), the way something on foot would go (over the level map, by ramps, around
|
|
2778
|
-
* water; the gentlest ground preferred), painted with a tile
|
|
2779
|
-
*
|
|
2780
|
-
* @beta
|
|
2781
|
-
*/
|
|
2782
|
-
function roadsPass(options) {
|
|
2783
|
-
return {
|
|
2784
|
-
id: "roads",
|
|
2785
|
-
name: "The roads",
|
|
2786
|
-
run(ctx) {
|
|
2787
|
-
const { map, registry } = ctx;
|
|
2788
|
-
if (!registry.tileIds.includes(options.tile)) return;
|
|
2789
|
-
const tile = registry.tile(options.tile);
|
|
2790
|
-
const wide = Math.max(1, Math.round(options.width ?? 2));
|
|
2791
|
-
const places = options.between ?? map.players.flatMap((player) => player.start ? [player.start] : []);
|
|
2792
|
-
const terrain = mapTerrain(map);
|
|
2793
|
-
for (let i = 0; i < places.length; i++) for (let j = i + 1; j < places.length; j++) {
|
|
2794
|
-
const path = findPath(map, terrain, places[i], places[j]);
|
|
2795
|
-
for (const c of path) {
|
|
2796
|
-
const cx = c % map.width;
|
|
2797
|
-
const cz = (c - cx) / map.width;
|
|
2798
|
-
for (let dz = 0; dz < wide; dz++) for (let dx = 0; dx < wide; dx++) {
|
|
2799
|
-
const x = cx + dx - Math.floor(wide / 2);
|
|
2800
|
-
const z = cz + dz - Math.floor(wide / 2);
|
|
2801
|
-
if (on(map, x, z) && map.water[z * map.width + x] === 0) map.tiles[z * map.width + x] = tile;
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
};
|
|
2807
|
-
}
|
|
2808
|
-
/** The cells of the way from one place to another over the level map: A* with the gentlest ground preferred; nothing where there is none */
|
|
2809
|
-
function findPath(map, terrain, from, to) {
|
|
2810
|
-
const { width, depth, levels, ramps, water } = map;
|
|
2811
|
-
const start = Math.floor(from.z) * width + Math.floor(from.x);
|
|
2812
|
-
const goal = Math.floor(to.z) * width + Math.floor(to.x);
|
|
2813
|
-
const passable = (a, b) => levels[a] === levels[b] || ramps[a] > 0 || ramps[b] > 0;
|
|
2814
|
-
const cost = new Float32Array(width * depth).fill(Infinity);
|
|
2815
|
-
const came = new Int32Array(width * depth).fill(-1);
|
|
2816
|
-
const closed = new Uint8Array(width * depth);
|
|
2817
|
-
const heapCells = [];
|
|
2818
|
-
const heapCosts = [];
|
|
2819
|
-
const push = (estimate, c) => {
|
|
2820
|
-
heapCells.push(c);
|
|
2821
|
-
heapCosts.push(estimate);
|
|
2822
|
-
let i = heapCells.length - 1;
|
|
2823
|
-
while (i > 0) {
|
|
2824
|
-
const parent = i - 1 >> 1;
|
|
2825
|
-
if (heapCosts[parent] <= heapCosts[i]) break;
|
|
2826
|
-
[heapCosts[parent], heapCosts[i]] = [heapCosts[i], heapCosts[parent]];
|
|
2827
|
-
[heapCells[parent], heapCells[i]] = [heapCells[i], heapCells[parent]];
|
|
2828
|
-
i = parent;
|
|
2829
|
-
}
|
|
2830
|
-
};
|
|
2831
|
-
const pop = () => {
|
|
2832
|
-
const top = heapCells[0];
|
|
2833
|
-
const lastCell = heapCells.pop();
|
|
2834
|
-
const lastCost = heapCosts.pop();
|
|
2835
|
-
if (heapCells.length) {
|
|
2836
|
-
heapCells[0] = lastCell;
|
|
2837
|
-
heapCosts[0] = lastCost;
|
|
2838
|
-
let i = 0;
|
|
2839
|
-
for (;;) {
|
|
2840
|
-
const l = i * 2 + 1;
|
|
2841
|
-
const r = l + 1;
|
|
2842
|
-
let best = i;
|
|
2843
|
-
if (l < heapCells.length && heapCosts[l] < heapCosts[best]) best = l;
|
|
2844
|
-
if (r < heapCells.length && heapCosts[r] < heapCosts[best]) best = r;
|
|
2845
|
-
if (best === i) break;
|
|
2846
|
-
[heapCosts[best], heapCosts[i]] = [heapCosts[i], heapCosts[best]];
|
|
2847
|
-
[heapCells[best], heapCells[i]] = [heapCells[i], heapCells[best]];
|
|
2848
|
-
i = best;
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
return top;
|
|
2852
|
-
};
|
|
2853
|
-
const h = (c) => hypot(c % width - goal % width, Math.floor(c / width) - Math.floor(goal / width));
|
|
2854
|
-
cost[start] = 0;
|
|
2855
|
-
push(h(start), start);
|
|
2856
|
-
while (heapCells.length) {
|
|
2857
|
-
const c = pop();
|
|
2858
|
-
if (closed[c]) continue;
|
|
2859
|
-
closed[c] = 1;
|
|
2860
|
-
if (c === goal) break;
|
|
2861
|
-
const cx = c % width;
|
|
2862
|
-
const cz = (c - cx) / width;
|
|
2863
|
-
for (const [dx, dz] of NEIGHBOURS) {
|
|
2864
|
-
const nx = cx + dx;
|
|
2865
|
-
const nz = cz + dz;
|
|
2866
|
-
if (!on(map, nx, nz)) continue;
|
|
2867
|
-
const n = nz * width + nx;
|
|
2868
|
-
if (closed[n] || water[n] === 2 || !passable(c, n)) continue;
|
|
2869
|
-
const step = 1 + terrain.slopeAt(nx + .5, nz + .5) * 4 + (water[n] ? 2 : 0);
|
|
2870
|
-
const next = cost[c] + step;
|
|
2871
|
-
if (next >= cost[n]) continue;
|
|
2872
|
-
cost[n] = next;
|
|
2873
|
-
came[n] = c;
|
|
2874
|
-
push(next + h(n), n);
|
|
2875
|
-
}
|
|
2876
|
-
}
|
|
2877
|
-
if (came[goal] < 0 && goal !== start) return [];
|
|
2878
|
-
const path = [];
|
|
2879
|
-
for (let c = goal; c >= 0; c = came[c]) {
|
|
2880
|
-
path.push(c);
|
|
2881
|
-
if (c === start) break;
|
|
2882
|
-
}
|
|
2883
|
-
return path.reverse();
|
|
2884
|
-
}
|
|
2885
|
-
/**
|
|
2886
|
-
* A generator to take whole: hills and water, the paint, the starts, trees in patches and rocks about, with the
|
|
2887
|
-
* parameters `levels`, `water`, `roughness`, `hills`, `trees` and `rocks`; what the trees and rocks are is the game's
|
|
2888
|
-
* to say
|
|
2889
|
-
*
|
|
2890
|
-
* @beta
|
|
2891
|
-
*/
|
|
2892
|
-
function presetGenerator(options = {}) {
|
|
2893
|
-
const { trees = [], rocks = [], treeScale = {} } = options;
|
|
2894
|
-
const scale = treeScale.scale ?? 1;
|
|
2895
|
-
const vary = treeScale.vary ?? .2;
|
|
2896
|
-
return {
|
|
2897
|
-
params: [
|
|
2898
|
-
{
|
|
2899
|
-
id: "levels",
|
|
2900
|
-
name: "Cliff levels",
|
|
2901
|
-
description: "How many levels the ground rises through",
|
|
2902
|
-
default: 2,
|
|
2903
|
-
min: 1,
|
|
2904
|
-
max: 4,
|
|
2905
|
-
step: 1
|
|
2906
|
-
},
|
|
2907
|
-
{
|
|
2908
|
-
id: "water",
|
|
2909
|
-
name: "Water",
|
|
2910
|
-
description: "How much of the map is under water",
|
|
2911
|
-
default: .15,
|
|
2912
|
-
min: 0,
|
|
2913
|
-
max: .5,
|
|
2914
|
-
step: .05
|
|
2915
|
-
},
|
|
2916
|
-
{
|
|
2917
|
-
id: "roughness",
|
|
2918
|
-
name: "Roughness",
|
|
2919
|
-
description: "How much the ground rolls",
|
|
2920
|
-
default: .4,
|
|
2921
|
-
min: 0,
|
|
2922
|
-
max: 1,
|
|
2923
|
-
step: .05
|
|
2924
|
-
},
|
|
2925
|
-
{
|
|
2926
|
-
id: "hills",
|
|
2927
|
-
name: "Hills",
|
|
2928
|
-
description: "How many cells a hill is across",
|
|
2929
|
-
default: 24,
|
|
2930
|
-
min: 8,
|
|
2931
|
-
max: 64,
|
|
2932
|
-
step: 4
|
|
2933
|
-
},
|
|
2934
|
-
{
|
|
2935
|
-
id: "trees",
|
|
2936
|
-
name: "Trees",
|
|
2937
|
-
description: "How much of the map is woods",
|
|
2938
|
-
default: trees.length ? .12 : 0,
|
|
2939
|
-
min: 0,
|
|
2940
|
-
max: .5,
|
|
2941
|
-
step: .02
|
|
2942
|
-
},
|
|
2943
|
-
{
|
|
2944
|
-
id: "rocks",
|
|
2945
|
-
name: "Rocks",
|
|
2946
|
-
description: "How many rocks lie about",
|
|
2947
|
-
default: rocks.length ? .02 : 0,
|
|
2948
|
-
min: 0,
|
|
2949
|
-
max: .2,
|
|
2950
|
-
step: .01
|
|
2951
|
-
}
|
|
2952
|
-
],
|
|
2953
|
-
passes: [
|
|
2954
|
-
terrainPass(),
|
|
2955
|
-
tilesPass(),
|
|
2956
|
-
startsPass({ from: options.start }),
|
|
2957
|
-
...trees.length ? [scatterPass({
|
|
2958
|
-
kind: "trees",
|
|
2959
|
-
spacing: 1.6,
|
|
2960
|
-
what: (ctx, at) => {
|
|
2961
|
-
const size = scale * (1 + (ctx.rng.next() - .5) * 2 * vary);
|
|
2962
|
-
ctx.addDoodad({
|
|
2963
|
-
model: trees[ctx.rng.int(0, trees.length - 1)],
|
|
2964
|
-
x: at.x,
|
|
2965
|
-
z: at.z,
|
|
2966
|
-
yaw: ctx.rng.next() * Math.PI * 2,
|
|
2967
|
-
scale: size,
|
|
2968
|
-
blocks: "walk",
|
|
2969
|
-
radius: .5
|
|
2970
|
-
});
|
|
2971
|
-
}
|
|
2972
|
-
})] : [],
|
|
2973
|
-
...rocks.length ? [scatterPass({
|
|
2974
|
-
kind: "rocks",
|
|
2975
|
-
spacing: 3,
|
|
2976
|
-
patches: .3,
|
|
2977
|
-
avoidKinds: { trees: 1 },
|
|
2978
|
-
what: (ctx, at) => {
|
|
2979
|
-
const size = scale * (.8 + ctx.rng.next() * .6);
|
|
2980
|
-
ctx.addDoodad({
|
|
2981
|
-
model: rocks[ctx.rng.int(0, rocks.length - 1)],
|
|
2982
|
-
x: at.x,
|
|
2983
|
-
z: at.z,
|
|
2984
|
-
yaw: ctx.rng.next() * Math.PI * 2,
|
|
2985
|
-
scale: size,
|
|
2986
|
-
blocks: "walk",
|
|
2987
|
-
radius: .6
|
|
2988
|
-
});
|
|
2989
|
-
}
|
|
2990
|
-
})] : []
|
|
2991
|
-
]
|
|
2992
|
-
};
|
|
2993
|
-
}
|
|
2994
|
-
//#endregion
|
|
2995
|
-
//#region src/packs.ts
|
|
2996
|
-
/**
|
|
2997
|
-
* Why a pack's file is not one, where it is first wrong
|
|
2998
|
-
*
|
|
2999
|
-
* @beta
|
|
3000
|
-
*/
|
|
3001
|
-
var PackError = class extends Error {};
|
|
3002
|
-
var ID = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
3003
|
-
var VERSION = /^\d+(\.\d+){0,2}$/;
|
|
3004
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3005
|
-
/**
|
|
3006
|
-
* Checks a pack's file as it is read (the schema's rules a loader needs: the pack's schema has all of them):
|
|
3007
|
-
* its id and version, and that each list is of definitions with an id, once each. Throws `PackError` saying where
|
|
3008
|
-
*
|
|
3009
|
-
* @beta
|
|
3010
|
-
*/
|
|
3011
|
-
function checkPack(json, where = "the pack") {
|
|
3012
|
-
const wrong = (why) => {
|
|
3013
|
-
throw new PackError(`${where}: ${why}`);
|
|
3014
|
-
};
|
|
3015
|
-
if (!isRecord(json)) return wrong("is no object");
|
|
3016
|
-
if (typeof json.id !== "string" || !ID.test(json.id)) wrong("its id is letters, digits, dots and dashes");
|
|
3017
|
-
if (typeof json.version !== "string" || !VERSION.test(json.version)) wrong("its version is major.minor.patch");
|
|
3018
|
-
if (json.requires !== void 0 && (!Array.isArray(json.requires) || json.requires.some((each) => typeof each !== "string"))) wrong("requires is a list of words");
|
|
3019
|
-
for (const kind of DATA_KINDS) {
|
|
3020
|
-
const list = json[kind];
|
|
3021
|
-
if (list === void 0) continue;
|
|
3022
|
-
if (!Array.isArray(list)) wrong(`${kind} is no list`);
|
|
3023
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3024
|
-
for (const [at, def] of list.entries()) {
|
|
3025
|
-
if (!isRecord(def) || typeof def.id !== "string" || !def.id) wrong(`${kind}[${at}] has no id`);
|
|
3026
|
-
const id = def.id;
|
|
3027
|
-
if (seen.has(id)) wrong(`${kind} has "${id}" twice`);
|
|
3028
|
-
seen.add(id);
|
|
3029
|
-
}
|
|
3030
|
-
}
|
|
3031
|
-
for (const part of [
|
|
3032
|
-
"rules",
|
|
3033
|
-
"data",
|
|
3034
|
-
"looks"
|
|
3035
|
-
]) if (json[part] !== void 0 && !isRecord(json[part])) wrong(`${part} is no object`);
|
|
3036
|
-
const known = /* @__PURE__ */ new Set([
|
|
3037
|
-
"$schema",
|
|
3038
|
-
"id",
|
|
3039
|
-
"version",
|
|
3040
|
-
"name",
|
|
3041
|
-
"requires",
|
|
3042
|
-
...DATA_KINDS,
|
|
3043
|
-
"rules",
|
|
3044
|
-
"data",
|
|
3045
|
-
"looks"
|
|
3046
|
-
]);
|
|
3047
|
-
const unknown = Object.keys(json).find((key) => !known.has(key));
|
|
3048
|
-
if (unknown) wrong(`"${unknown}" is nothing a pack has`);
|
|
3049
|
-
return json;
|
|
3050
|
-
}
|
|
3051
|
-
/** A word with its article: an object, a number */
|
|
3052
|
-
var an = (word) => `${/^[aeiou]/.test(word) ? "an" : "a"} ${word}`;
|
|
3053
|
-
/** The type of a value as a schema says it */
|
|
3054
|
-
var typeOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : Number.isInteger(value) ? "integer" : typeof value;
|
|
3055
|
-
/**
|
|
3056
|
-
* Checks a pack's file against the pack's schema (`@v12-engine/engine/schemas/pack.schema.json`, made of `PackFile`):
|
|
3057
|
-
* where it is first wrong, in words with its place (`units[2].stats.health: is a string, not a number`), or nothing
|
|
3058
|
-
* for one that is right. A small reader of the schema, as far as it uses JSON Schema: what a page that loads packs
|
|
3059
|
-
* fetches with them, so that a game without packs carries none of it
|
|
3060
|
-
*
|
|
3061
|
-
* @beta
|
|
3062
|
-
*/
|
|
3063
|
-
function checkPackSchema(json, schema) {
|
|
3064
|
-
const root = schema;
|
|
3065
|
-
const resolve = (part) => {
|
|
3066
|
-
let at = part;
|
|
3067
|
-
for (let hops = 0; at.$ref && hops < 32; hops++) at = root.definitions?.[decodeURIComponent(at.$ref.replace("#/definitions/", ""))] ?? {};
|
|
3068
|
-
return at;
|
|
3069
|
-
};
|
|
3070
|
-
const check = (value, given, where) => {
|
|
3071
|
-
const part = resolve(given);
|
|
3072
|
-
const type = typeOf(value);
|
|
3073
|
-
if (part.type !== void 0) {
|
|
3074
|
-
const types = Array.isArray(part.type) ? part.type : [part.type];
|
|
3075
|
-
if (!types.some((each) => each === type || each === "number" && type === "integer")) return `${where || "the pack"}: is ${an(type)}, not ${types.join(" or ")}`;
|
|
3076
|
-
}
|
|
3077
|
-
if ("const" in part && value !== part.const) return `${where || "the pack"}: is not ${JSON.stringify(part.const)}`;
|
|
3078
|
-
if (part.enum && !part.enum.includes(value)) return `${where || "the pack"}: is none of ${part.enum.map((each) => JSON.stringify(each)).join(", ")}`;
|
|
3079
|
-
if (part.not && check(value, part.not, where) === void 0) return `${where || "the pack"}: is what it may not be`;
|
|
3080
|
-
if (part.anyOf) {
|
|
3081
|
-
const wrongs = part.anyOf.map((each) => check(value, each, where));
|
|
3082
|
-
if (wrongs.every((wrong) => wrong !== void 0)) return wrongs.reduce((deepest, wrong) => wrong.length > deepest.length ? wrong : deepest);
|
|
3083
|
-
}
|
|
3084
|
-
if (type === "object" && (part.properties || part.required || part.additionalProperties !== void 0 || part.propertyNames)) {
|
|
3085
|
-
const object = value;
|
|
3086
|
-
for (const field of part.required ?? []) if (!(field in object)) return `${where || "the pack"}: has no ${field}`;
|
|
3087
|
-
for (const [field, each] of Object.entries(object)) {
|
|
3088
|
-
const at = where ? `${where}.${field}` : field;
|
|
3089
|
-
if (part.propertyNames) {
|
|
3090
|
-
const name = check(field, part.propertyNames, at);
|
|
3091
|
-
if (name) return name;
|
|
3092
|
-
}
|
|
3093
|
-
const own = part.properties?.[field];
|
|
3094
|
-
if (own) {
|
|
3095
|
-
const wrong = check(each, own, at);
|
|
3096
|
-
if (wrong) return wrong;
|
|
3097
|
-
} else if (part.additionalProperties === false) return `${at}: is nothing ${where || "a pack"} has`;
|
|
3098
|
-
else if (typeof part.additionalProperties === "object") {
|
|
3099
|
-
const wrong = check(each, part.additionalProperties, at);
|
|
3100
|
-
if (wrong) return wrong;
|
|
3101
|
-
}
|
|
3102
|
-
}
|
|
3103
|
-
}
|
|
3104
|
-
if (type === "array") {
|
|
3105
|
-
const list = value;
|
|
3106
|
-
if (part.minItems !== void 0 && list.length < part.minItems) return `${where || "the pack"}: has fewer than ${part.minItems}`;
|
|
3107
|
-
if (part.maxItems !== void 0 && list.length > part.maxItems) return `${where || "the pack"}: has more than ${part.maxItems}`;
|
|
3108
|
-
if (part.items) for (const [at, each] of list.entries()) {
|
|
3109
|
-
const item = Array.isArray(part.items) ? part.items[at] : part.items;
|
|
3110
|
-
const wrong = item && check(each, item, `${where}[${at}]`);
|
|
3111
|
-
if (wrong) return wrong;
|
|
3112
|
-
}
|
|
3113
|
-
}
|
|
3114
|
-
};
|
|
3115
|
-
return check(json, root, "");
|
|
3116
|
-
}
|
|
3117
|
-
/** FNV-1a, twice over with different starts, of a text: as the game's hash is */
|
|
3118
|
-
function hashOf(text) {
|
|
3119
|
-
let a = 2166136261;
|
|
3120
|
-
let b = 1523705862;
|
|
3121
|
-
for (let i = 0; i < text.length; i++) {
|
|
3122
|
-
const c = text.charCodeAt(i);
|
|
3123
|
-
a = Math.imul(a ^ c, 16777619);
|
|
3124
|
-
b = Math.imul(b ^ c, 1540483477);
|
|
3125
|
-
}
|
|
3126
|
-
return (a >>> 0).toString(16).padStart(8, "0") + (b >>> 0).toString(16).padStart(8, "0");
|
|
3127
|
-
}
|
|
3128
|
-
var byId = (defs) => [...defs].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
3129
|
-
/** The builder's collection of a kind of the game's data */
|
|
3130
|
-
var collectionOf = (game, kind) => game[kind];
|
|
3131
|
-
/**
|
|
3132
|
-
* Makes a mod of a pack's file (its text, or what was parsed of it): checked, then applying its definitions (each
|
|
3133
|
-
* list in the order of its ids, a definition the game has put in its place), its `data` over the whole and its
|
|
3134
|
-
* `rules`. Its hash, of the text as given, goes with the game's mods
|
|
3135
|
-
*
|
|
3136
|
-
* @beta
|
|
3137
|
-
*/
|
|
3138
|
-
function packFromJson(json, where) {
|
|
3139
|
-
const text = typeof json === "string" ? json : JSON.stringify(json);
|
|
3140
|
-
let parsed;
|
|
3141
|
-
try {
|
|
3142
|
-
parsed = typeof json === "string" ? JSON.parse(json) : json;
|
|
3143
|
-
} catch (error) {
|
|
3144
|
-
throw new PackError(`${where ?? "the pack"}: is not JSON (${error instanceof Error ? error.message : String(error)})`);
|
|
3145
|
-
}
|
|
3146
|
-
const pack = checkPack(parsed, where);
|
|
3147
|
-
return {
|
|
3148
|
-
id: pack.id,
|
|
3149
|
-
version: pack.version,
|
|
3150
|
-
...pack.name !== void 0 && { name: pack.name },
|
|
3151
|
-
...pack.requires !== void 0 && { requires: pack.requires },
|
|
3152
|
-
pack,
|
|
3153
|
-
hash: hashOf(text),
|
|
3154
|
-
apply(game) {
|
|
3155
|
-
for (const kind of DATA_KINDS) {
|
|
3156
|
-
const defs = pack[kind];
|
|
3157
|
-
if (!defs) continue;
|
|
3158
|
-
const collection = collectionOf(game, kind);
|
|
3159
|
-
for (const def of byId(defs)) {
|
|
3160
|
-
const own = structuredClone(def);
|
|
3161
|
-
if (collection.has(def.id)) collection.override(def.id, () => own);
|
|
3162
|
-
else collection.add(own);
|
|
3163
|
-
}
|
|
3164
|
-
}
|
|
3165
|
-
if (pack.data) layData(game, pack.data);
|
|
3166
|
-
if (pack.rules) game.rules(pack.rules);
|
|
3167
|
-
}
|
|
3168
|
-
};
|
|
3169
|
-
}
|
|
3170
|
-
/** A `data.json`'s values laid over the game as the builder has it: each definition it changes put in its place, those it takes out gone */
|
|
3171
|
-
function layData(game, data) {
|
|
3172
|
-
const before = game.current;
|
|
3173
|
-
const after = withData(before, data);
|
|
3174
|
-
for (const kind of DATA_KINDS) {
|
|
3175
|
-
const collection = collectionOf(game, kind);
|
|
3176
|
-
const was = new Map((before[kind] ?? []).map((def) => [def.id, def]));
|
|
3177
|
-
const now = after[kind] ?? [];
|
|
3178
|
-
for (const def of now) {
|
|
3179
|
-
if (!was.has(def.id)) collection.add(def);
|
|
3180
|
-
else if (was.get(def.id) !== def) collection.override(def.id, () => def);
|
|
3181
|
-
was.delete(def.id);
|
|
3182
|
-
}
|
|
3183
|
-
for (const id of was.keys()) collection.remove(id);
|
|
3184
|
-
}
|
|
3185
|
-
if (after.ai !== before.ai) game.set("ai", after.ai);
|
|
3186
|
-
}
|
|
3187
|
-
/**
|
|
3188
|
-
* Whether a game was made with packs, and which: those of the mods its composition kept a hash of, by id and version
|
|
3189
|
-
*
|
|
3190
|
-
* @beta
|
|
3191
|
-
*/
|
|
3192
|
-
var packsOf = (game) => (game.mods ?? []).filter((mod) => mod.hash !== void 0).map(({ id, version }) => ({
|
|
3193
|
-
id,
|
|
3194
|
-
version
|
|
3195
|
-
}));
|
|
3196
|
-
//#endregion
|
|
3197
|
-
export { connectAs as A, randomSeed as C, SessionHost as D, RemoteSession as E, readHostMessage as F, readPlayerMessage as I, loopback as M, toRuns as N, SessionRefused as O, isServerMessage as P, tilingNoise as S, MAX_JITTER as T, tilesPass as _, packsOf as a, mapCells as b, generateMap as c, resourcesPass as d, roadsPass as f, terrainPass as g, symmetricPlaces as h, packFromJson as i, fromRuns as j, connect as k, nearestDryGround as l, startsPass as m, checkPack as n, basinShape as o, scatterPass as p, checkPackSchema as r, clearGround as s, PackError as t, presetGenerator as u, MAP_RULES as v, delayed as w, layeredNoise as x, checkMap as y };
|
|
3198
|
-
|
|
3199
|
-
//# sourceMappingURL=packs-g5thvmGn.js.map
|