@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/server.js
CHANGED
|
@@ -1,2861 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* A builder of navigations in a worker thread of this process (S37): what a server builds with, so that the world of
|
|
17
|
-
* one game being made does not hold up the ticks of the others (two seconds for a map of 512 cells a side). One
|
|
18
|
-
* thread, made the first time and kept, which keeps the process alive only while it builds. Nothing where the
|
|
19
|
-
* worker's file is not there: in this repository, which runs the engine as it is written, it builds on the thread
|
|
20
|
-
* that asks
|
|
21
|
-
*
|
|
22
|
-
* @beta
|
|
23
|
-
*/
|
|
24
|
-
function nodeNavigationBuilder() {
|
|
25
|
-
const script = new URL("navigation-worker.js", import.meta.url);
|
|
26
|
-
if (script.protocol !== "file:" || !existsSync(fileURLToPath(script))) return void 0;
|
|
27
|
-
let made;
|
|
28
|
-
let pending = 0;
|
|
29
|
-
return async (build) => {
|
|
30
|
-
made ??= import("node:worker_threads").then(({ Worker }) => {
|
|
31
|
-
const worker = new Worker(script);
|
|
32
|
-
return {
|
|
33
|
-
worker,
|
|
34
|
-
builder: navigationWorker(worker)
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
|
-
const { worker, builder } = await made;
|
|
38
|
-
if (pending++ === 0) worker.ref();
|
|
39
|
-
try {
|
|
40
|
-
return await builder(build);
|
|
41
|
-
} finally {
|
|
42
|
-
if (--pending === 0) worker.unref();
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
//#endregion
|
|
47
|
-
//#region src/server/log.ts
|
|
48
|
-
/**
|
|
49
|
-
* The logger of a server, from what it was started with. A logger: one under it. A function of lines: a logger of its
|
|
50
|
-
* own that gives it the server's own entries as their words, and everything else (a game's, a warning) as a whole
|
|
51
|
-
* line. Nothing: one under the root, which says warnings and errors only, as a server said nothing else before
|
|
52
|
-
*/
|
|
53
|
-
function serverLogger(given) {
|
|
54
|
-
if (given instanceof Logger) return given.child();
|
|
55
|
-
if (!given) return rootLogger.child({ level: "warn" });
|
|
56
|
-
const logger = new Logger();
|
|
57
|
-
logger.to((entry) => given(entry.category === "server" && entry.level === "info" ? entry.message : logLine(entry)));
|
|
58
|
-
return logger;
|
|
59
|
-
}
|
|
60
|
-
//#endregion
|
|
61
|
-
//#region src/server/ratings.ts
|
|
62
|
-
/**
|
|
63
|
-
* The id a player is rated by: a hash of their key or login id, safe to show and keep. The same key, the same id
|
|
64
|
-
*
|
|
65
|
-
* @beta
|
|
66
|
-
*/
|
|
67
|
-
var ratedId = (key) => createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
68
|
-
/**
|
|
69
|
-
* Ratings kept in a file (`file`, JSON, read once and written after every game) or in memory: Elo, each side's
|
|
70
|
-
* players against the average of the other's, `k` 32, starting at 1500. The computer counts as `start`, and is not
|
|
71
|
-
* rated; a game with no winner, or with players of one side only, changes nothing
|
|
72
|
-
*
|
|
73
|
-
* @beta
|
|
74
|
-
*/
|
|
75
|
-
function createRatings(options = {}) {
|
|
76
|
-
const k = options.k ?? 32;
|
|
77
|
-
const start = options.start ?? 1500;
|
|
78
|
-
let byMode = {};
|
|
79
|
-
const ready = (async () => {
|
|
80
|
-
if (!options.file) return;
|
|
81
|
-
let text;
|
|
82
|
-
try {
|
|
83
|
-
text = await readFile(options.file, "utf8");
|
|
84
|
-
} catch (error) {
|
|
85
|
-
if (error.code === "ENOENT") return;
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
88
|
-
byMode = JSON.parse(text);
|
|
89
|
-
})();
|
|
90
|
-
ready.catch(() => void 0);
|
|
91
|
-
let writing = Promise.resolve();
|
|
92
|
-
const keep = () => {
|
|
93
|
-
if (!options.file) return writing;
|
|
94
|
-
const file = options.file;
|
|
95
|
-
writing = writing.then(async () => {
|
|
96
|
-
await mkdir(dirname(file), { recursive: true });
|
|
97
|
-
await writeFile(file, JSON.stringify(byMode, null, 1));
|
|
98
|
-
});
|
|
99
|
-
return writing;
|
|
100
|
-
};
|
|
101
|
-
/** A mode's players, best first: by rating, then by more games, then by id, so the order is always the same */
|
|
102
|
-
const ranked = (mode) => Object.entries(byMode[mode] ?? {}).sort(([a, x], [b, y]) => y.rating - x.rating || y.games - x.games || (a < b ? -1 : 1)).map(([id, rating], index) => ({
|
|
103
|
-
id,
|
|
104
|
-
place: index + 1,
|
|
105
|
-
...rating
|
|
106
|
-
}));
|
|
107
|
-
return {
|
|
108
|
-
ready,
|
|
109
|
-
get: (mode, player) => byMode[mode]?.[player],
|
|
110
|
-
top: (mode, count) => ranked(mode).slice(0, Math.max(0, count)),
|
|
111
|
-
placeOf: (mode, player) => ranked(mode).find((each) => each.id === player),
|
|
112
|
-
async record(result) {
|
|
113
|
-
await ready;
|
|
114
|
-
if (result.winner === null || result.failed) return;
|
|
115
|
-
const seats = result.seats.filter((seat) => seat.outcome !== "none");
|
|
116
|
-
const rated = seats.filter((seat) => !seat.ai && seat.rated);
|
|
117
|
-
if (!rated.length) return;
|
|
118
|
-
const table = byMode[result.mode] ??= {};
|
|
119
|
-
const ratingOf = (seat) => !seat.ai && seat.rated ? table[seat.rated]?.rating ?? start : start;
|
|
120
|
-
const sides = /* @__PURE__ */ new Map();
|
|
121
|
-
for (const seat of seats) {
|
|
122
|
-
const side = seat.outcome;
|
|
123
|
-
sides.set(side, [...sides.get(side) ?? [], seat]);
|
|
124
|
-
}
|
|
125
|
-
const winners = sides.get("victory") ?? [];
|
|
126
|
-
const losers = sides.get("defeat") ?? [];
|
|
127
|
-
if (!winners.length || !losers.length) return;
|
|
128
|
-
const average = (list) => list.reduce((sum, seat) => sum + ratingOf(seat), 0) / list.length;
|
|
129
|
-
const changes = rated.map((seat) => {
|
|
130
|
-
const won = seat.outcome === "victory";
|
|
131
|
-
const theirs = average(won ? losers : winners);
|
|
132
|
-
const mine = ratingOf(seat);
|
|
133
|
-
const expected = 1 / (1 + 10 ** ((theirs - mine) / 400));
|
|
134
|
-
return {
|
|
135
|
-
seat,
|
|
136
|
-
won,
|
|
137
|
-
next: mine + k * ((won ? 1 : 0) - expected)
|
|
138
|
-
};
|
|
139
|
-
});
|
|
140
|
-
for (const { seat, won, next } of changes) {
|
|
141
|
-
const before = table[seat.rated] ?? {
|
|
142
|
-
rating: start,
|
|
143
|
-
games: 0,
|
|
144
|
-
wins: 0,
|
|
145
|
-
name: seat.name
|
|
146
|
-
};
|
|
147
|
-
table[seat.rated] = {
|
|
148
|
-
rating: Math.round(next),
|
|
149
|
-
games: before.games + 1,
|
|
150
|
-
wins: before.wins + (won ? 1 : 0),
|
|
151
|
-
name: seat.name
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
await keep();
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
//#endregion
|
|
159
|
-
//#region src/server/secret.ts
|
|
160
|
-
/** A secret as bytes of one length, whatever its own: what is compared takes as long whatever it is */
|
|
161
|
-
var digest = (text) => createHash("sha256").update(text).digest();
|
|
162
|
-
/**
|
|
163
|
-
* Whether what was brought is the secret (an admin's key, a key to watch, to the numbers), compared in a time that
|
|
164
|
-
* says nothing of how much of it is right (RV37)
|
|
165
|
-
*/
|
|
166
|
-
var sameSecret = (brought, secret) => timingSafeEqual(digest(brought), digest(secret));
|
|
167
|
-
//#endregion
|
|
168
|
-
//#region src/server/doorman.ts
|
|
169
|
-
/**
|
|
170
|
-
* Where one who joins a server's game goes (`Doorman`): their seat again, a free seat, a seat the game decides
|
|
171
|
-
* (`GameServerOptions.seat`), watching, or away with why (design review E12). It decides and says so; the server seats,
|
|
172
|
-
* tells and closes
|
|
173
|
-
*/
|
|
174
|
-
/**
|
|
175
|
-
* Decides where each who joins a server goes
|
|
176
|
-
*
|
|
177
|
-
* @internal
|
|
178
|
-
*/
|
|
179
|
-
var Doorman = class {
|
|
180
|
-
server;
|
|
181
|
-
constructor(server) {
|
|
182
|
-
this.server = server;
|
|
183
|
-
}
|
|
184
|
-
/** Why one who joins speaks another version of the wire, or nothing: told before any seat is taken or welcome sent that it would misread */
|
|
185
|
-
wrongWire(join) {
|
|
186
|
-
if (join.protocol === void 0 || join.protocol === 13) return void 0;
|
|
187
|
-
return `this server runs another version of the engine (protocol 13, yours is ${join.protocol}): update the game`;
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Where one who joins from `address` goes: decided now, or once the game said (`GameServerOptions.seat`), a promise
|
|
191
|
-
* then. Their wire is the server's (`wrongWire`)
|
|
192
|
-
*/
|
|
193
|
-
place(join, address) {
|
|
194
|
-
const { server } = this;
|
|
195
|
-
const theirs = join.token ? server.seats.find((each) => each.token === join.token) : void 0;
|
|
196
|
-
if (theirs) return {
|
|
197
|
-
to: "seat",
|
|
198
|
-
seat: theirs,
|
|
199
|
-
name: join.name
|
|
200
|
-
};
|
|
201
|
-
if (server.draining) return {
|
|
202
|
-
to: "refuse",
|
|
203
|
-
reason: "the server is stopping"
|
|
204
|
-
};
|
|
205
|
-
if (join.back) return {
|
|
206
|
-
to: "refuse",
|
|
207
|
-
reason: "the game you were in is over"
|
|
208
|
-
};
|
|
209
|
-
if (server.options.seat && !join.watch) return this.asked(join, address);
|
|
210
|
-
const free = !join.watch && server.phase === "lobby" ? server.seats.find((each) => !each.ai && !each.connected) : void 0;
|
|
211
|
-
if (!free) return this.noSeat(join, address);
|
|
212
|
-
return {
|
|
213
|
-
to: "seat",
|
|
214
|
-
seat: free,
|
|
215
|
-
name: join.name,
|
|
216
|
-
rated: join.who ? ratedId(join.who) : void 0
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
/** Where the game says one who joins sits (`seat`), checked as it answered: a seat may have been taken meanwhile */
|
|
220
|
-
async asked(join, address) {
|
|
221
|
-
const { server } = this;
|
|
222
|
-
let answer;
|
|
223
|
-
try {
|
|
224
|
-
answer = await server.options.seat({
|
|
225
|
-
address,
|
|
226
|
-
name: join.name,
|
|
227
|
-
ticket: join.ticket,
|
|
228
|
-
free: server.seats.filter((seat) => seat.isFree).map((seat) => server.publicSeat(seat)),
|
|
229
|
-
phase: server.phase
|
|
230
|
-
});
|
|
231
|
-
} catch (error) {
|
|
232
|
-
server.logger.error("server", "deciding a seat threw: they are refused", {
|
|
233
|
-
error: errorFields(error),
|
|
234
|
-
name: join.name
|
|
235
|
-
});
|
|
236
|
-
return {
|
|
237
|
-
to: "refuse",
|
|
238
|
-
reason: "the server could not seat you"
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
if (!answer || typeof answer !== "object") return {
|
|
242
|
-
to: "refuse",
|
|
243
|
-
reason: "the server could not seat you"
|
|
244
|
-
};
|
|
245
|
-
if ("refuse" in answer) return {
|
|
246
|
-
to: "refuse",
|
|
247
|
-
reason: String(answer.refuse)
|
|
248
|
-
};
|
|
249
|
-
return this.answered(join, answer, address);
|
|
250
|
-
}
|
|
251
|
-
/** The game's answer, checked against the seats as they are now */
|
|
252
|
-
answered(join, answer, address) {
|
|
253
|
-
const { server } = this;
|
|
254
|
-
const { player, id } = answer;
|
|
255
|
-
let seat;
|
|
256
|
-
if (player === void 0) seat = server.phase === "lobby" ? server.seats.find((each) => each.isFree) : void 0;
|
|
257
|
-
else {
|
|
258
|
-
seat = server.seats.find((each) => each.player === player);
|
|
259
|
-
if (!seat) return {
|
|
260
|
-
to: "refuse",
|
|
261
|
-
reason: `there is no seat ${player}`
|
|
262
|
-
};
|
|
263
|
-
if (seat.ai) return {
|
|
264
|
-
to: "refuse",
|
|
265
|
-
reason: "that seat is the computer's"
|
|
266
|
-
};
|
|
267
|
-
if (!seat.isFree && (id === void 0 || seat.id !== id)) return {
|
|
268
|
-
to: "refuse",
|
|
269
|
-
reason: "that seat is taken"
|
|
270
|
-
};
|
|
271
|
-
if (seat.isFree && server.phase !== "lobby") return {
|
|
272
|
-
to: "refuse",
|
|
273
|
-
reason: "the game has started"
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
if (!seat) return this.noSeat(join, address);
|
|
277
|
-
const rate = answer.rate || join.who || id;
|
|
278
|
-
return {
|
|
279
|
-
to: "seat",
|
|
280
|
-
seat,
|
|
281
|
-
name: answer.name ?? join.name,
|
|
282
|
-
named: answer.name !== void 0,
|
|
283
|
-
id,
|
|
284
|
-
rated: rate !== void 0 ? ratedId(rate) : void 0
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
/** No seat for who joined: they watch, if they may, or are told so */
|
|
288
|
-
noSeat(join, address) {
|
|
289
|
-
const { phase } = this.server;
|
|
290
|
-
if (phase === "lobby" && !join.watch) return { to: "full" };
|
|
291
|
-
if (this.mayWatch(join, address)) return { to: "watch" };
|
|
292
|
-
if (phase === "lobby") return { to: "full" };
|
|
293
|
-
return {
|
|
294
|
-
to: "refuse",
|
|
295
|
-
reason: "watching is not open"
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
/** Whether who asks may watch, as the server was told (`watch`) */
|
|
299
|
-
mayWatch(join, address) {
|
|
300
|
-
const { watch } = this.server.options;
|
|
301
|
-
if (!watch) return false;
|
|
302
|
-
if (watch === true) return true;
|
|
303
|
-
if (typeof watch === "object") return join.key !== void 0 && sameSecret(join.key, watch.key);
|
|
304
|
-
const seated = this.server.seats.some((seat) => seat.connected && seat.connection?.address === address);
|
|
305
|
-
return watch({
|
|
306
|
-
address,
|
|
307
|
-
name: join.name,
|
|
308
|
-
key: join.key,
|
|
309
|
-
seated,
|
|
310
|
-
phase: this.server.phase
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
//#endregion
|
|
315
|
-
//#region src/server/seat.ts
|
|
316
|
-
/** A token to come back to a seat with: what only who has it knows */
|
|
317
|
-
function randomToken() {
|
|
318
|
-
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
319
|
-
crypto.getRandomValues(bytes);
|
|
320
|
-
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* A seat of a server's game and who holds it
|
|
324
|
-
*
|
|
325
|
-
* @internal
|
|
326
|
-
*/
|
|
327
|
-
var Seat = class {
|
|
328
|
-
mode;
|
|
329
|
-
computer;
|
|
330
|
-
player;
|
|
331
|
-
name;
|
|
332
|
-
/** A computer plays it */
|
|
333
|
-
ai;
|
|
334
|
-
/** Someone is in it now */
|
|
335
|
-
connected = false;
|
|
336
|
-
ready = false;
|
|
337
|
-
settings;
|
|
338
|
-
/** Who it is rated as (`ratedId`), where the server rates */
|
|
339
|
-
rated;
|
|
340
|
-
rating;
|
|
341
|
-
/** What lets who held it back in: theirs while they hold it, in the lobby and in the game */
|
|
342
|
-
token;
|
|
343
|
-
/** Who holds it, as the `seat` hook said (`SeatAnswer.id`) */
|
|
344
|
-
id;
|
|
345
|
-
/** When what an honest client never sends came from it, in this game: the last `cheating.seconds`' worth, and how many in all */
|
|
346
|
-
cheats = {
|
|
347
|
-
at: [],
|
|
348
|
-
total: 0
|
|
349
|
-
};
|
|
350
|
-
/** The connection of who is in it */
|
|
351
|
-
connection;
|
|
352
|
-
constructor(mode, computer, settings) {
|
|
353
|
-
this.mode = mode;
|
|
354
|
-
this.computer = computer;
|
|
355
|
-
this.player = mode.player;
|
|
356
|
-
this.name = mode.name;
|
|
357
|
-
this.ai = computer;
|
|
358
|
-
this.settings = settings;
|
|
359
|
-
}
|
|
360
|
-
/** Nobody in it, nor holding it in a game to come back to, nor a computer */
|
|
361
|
-
get isFree() {
|
|
362
|
-
return !this.ai && !this.connected && this.token === void 0;
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Taken by a connection: whoever was in it before is closed out (a player back from another page), and its token is
|
|
366
|
-
* kept, or made. A name it is given is taken. Gives the token
|
|
367
|
-
*/
|
|
368
|
-
take(connection, name) {
|
|
369
|
-
if (this.connection && this.connection !== connection) this.connection.close();
|
|
370
|
-
this.connected = true;
|
|
371
|
-
this.connection = connection;
|
|
372
|
-
this.token ??= randomToken();
|
|
373
|
-
if (name) this.name = name.slice(0, 24);
|
|
374
|
-
return this.token;
|
|
375
|
-
}
|
|
376
|
-
/** Whoever holds another seat moves here, in the lobby: their connection, token, who they are and their name; not ready */
|
|
377
|
-
takeFrom(other) {
|
|
378
|
-
this.connected = true;
|
|
379
|
-
this.connection = other.connection;
|
|
380
|
-
this.token = other.token;
|
|
381
|
-
this.id = other.id;
|
|
382
|
-
this.rated = other.rated;
|
|
383
|
-
this.name = other.name;
|
|
384
|
-
this.ready = false;
|
|
385
|
-
other.free();
|
|
386
|
-
}
|
|
387
|
-
/** Its connection gone, held for who had it to come back to (a game on): their token, who they are and their name stay */
|
|
388
|
-
drop() {
|
|
389
|
-
this.connected = false;
|
|
390
|
-
this.connection = void 0;
|
|
391
|
-
}
|
|
392
|
-
/** Given up: free for anyone, with the mode's name, nobody's token, not ready */
|
|
393
|
-
free() {
|
|
394
|
-
this.drop();
|
|
395
|
-
this.token = void 0;
|
|
396
|
-
this.id = void 0;
|
|
397
|
-
this.rated = void 0;
|
|
398
|
-
this.ready = false;
|
|
399
|
-
this.name = this.mode.name;
|
|
400
|
-
}
|
|
401
|
-
/** Held no more by who it was: their token and who they are no good (a kick). Their connection is closed by the caller */
|
|
402
|
-
revoke() {
|
|
403
|
-
this.token = void 0;
|
|
404
|
-
this.id = void 0;
|
|
405
|
-
this.rated = void 0;
|
|
406
|
-
}
|
|
407
|
-
/** What a new game starts it with: nobody's cheats, not ready, the computer's where the server says */
|
|
408
|
-
newGame() {
|
|
409
|
-
this.cheats = {
|
|
410
|
-
at: [],
|
|
411
|
-
total: 0
|
|
412
|
-
};
|
|
413
|
-
this.ready = false;
|
|
414
|
-
this.ai = this.computer;
|
|
415
|
-
}
|
|
416
|
-
/** New, for a new game with nobody: free, and as a new game starts it */
|
|
417
|
-
reset() {
|
|
418
|
-
this.free();
|
|
419
|
-
this.newGame();
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
//#endregion
|
|
423
|
-
//#region src/server/seatSettings.ts
|
|
424
|
-
/** A name as a lobby shows it: `insane` as `Insane` */
|
|
425
|
-
var titled = (id) => id.charAt(0).toUpperCase() + id.slice(1);
|
|
426
|
-
/**
|
|
427
|
-
* What a lobby offers a computer's seat of a game that says how its computers play (`GameModule.ai`, AI plan D1): a
|
|
428
|
-
* difficulty and a personality, the engine's and the game's own. Nothing for a game that says nothing, or `lobby: false`
|
|
429
|
-
*/
|
|
430
|
-
function computerSettingsOf(game) {
|
|
431
|
-
if (!game.ai || game.ai.lobby === false) return {};
|
|
432
|
-
const registry = { module: game };
|
|
433
|
-
const difficulties = Object.keys(difficultiesOf(registry));
|
|
434
|
-
const personalities = Object.keys(personalitiesOf(registry));
|
|
435
|
-
return {
|
|
436
|
-
difficulty: {
|
|
437
|
-
label: "Difficulty",
|
|
438
|
-
options: difficulties,
|
|
439
|
-
labels: Object.fromEntries(difficulties.map((id) => [id, titled(id)])),
|
|
440
|
-
default: game.ai.defaults?.difficulty ?? "normal"
|
|
441
|
-
},
|
|
442
|
-
personality: {
|
|
443
|
-
label: "Personality",
|
|
444
|
-
options: personalities,
|
|
445
|
-
labels: Object.fromEntries(personalities.map((id) => [id, titled(id)])),
|
|
446
|
-
default: game.ai.defaults?.personality ?? "balanced"
|
|
447
|
-
}
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* What a game offers to choose for each seat in a lobby (`ServerHooks.seatSettings`), with a race among them where the
|
|
452
|
-
* game has races and does not offer its own choice of one: each race, by its name, and `random` where there are two
|
|
453
|
-
* or more; only those of
|
|
454
|
-
* `races` where given (the races a map offers, `GameMap.races`). The first is chosen until someone does
|
|
455
|
-
*
|
|
456
|
-
* @beta
|
|
457
|
-
*/
|
|
458
|
-
function seatSettingsOf(game, races) {
|
|
459
|
-
const own = {
|
|
460
|
-
...computerSettingsOf(game),
|
|
461
|
-
...game.server?.seatSettings
|
|
462
|
-
};
|
|
463
|
-
const all = game.races ?? [];
|
|
464
|
-
if (!all.length || own.race) return own;
|
|
465
|
-
const offered = races?.length ? all.filter((race) => races.includes(race.id)) : all;
|
|
466
|
-
const options = [...offered.map((race) => race.id), ...offered.length > 1 ? [RANDOM_RACE] : []];
|
|
467
|
-
return {
|
|
468
|
-
race: {
|
|
469
|
-
label: "Race",
|
|
470
|
-
options: options.length ? options : [all[0].id],
|
|
471
|
-
labels: {
|
|
472
|
-
...Object.fromEntries(all.map((race) => [race.id, race.name])),
|
|
473
|
-
[RANDOM_RACE]: "Random"
|
|
474
|
-
},
|
|
475
|
-
default: options[0] ?? all[0].id
|
|
476
|
-
},
|
|
477
|
-
...own
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
//#endregion
|
|
481
|
-
//#region src/server/lobby.ts
|
|
482
|
-
/**
|
|
483
|
-
* A server's seats and settings, and when its game starts
|
|
484
|
-
*
|
|
485
|
-
* @internal
|
|
486
|
-
*/
|
|
487
|
-
var Lobby = class {
|
|
488
|
-
server;
|
|
489
|
-
/** Its seats, in the mode's order */
|
|
490
|
-
seats;
|
|
491
|
-
/** The game's settings (`ServerHooks.settings`), as chosen */
|
|
492
|
-
settings = {};
|
|
493
|
-
/** What each seat can have chosen for it: the game's (`ServerHooks.seatSettings`), and a race where it has races (`seatSettingsOf`) */
|
|
494
|
-
seatOffered;
|
|
495
|
-
/** Back in the lobby after a game, with its players: it starts once they are ready, not as the seats fill */
|
|
496
|
-
again = false;
|
|
497
|
-
/** The votes to let a seated player go (`voteKick`), by who it is about: who voted */
|
|
498
|
-
kickVotes = /* @__PURE__ */ new Map();
|
|
499
|
-
hooks;
|
|
500
|
-
constructor(server) {
|
|
501
|
-
this.server = server;
|
|
502
|
-
const { mode, ai, settings } = server.options;
|
|
503
|
-
this.hooks = mode.game.server ?? {};
|
|
504
|
-
this.seatOffered = mode.seatSettings ?? seatSettingsOf(mode.game);
|
|
505
|
-
for (const [name, setting] of Object.entries(this.hooks.settings ?? {})) {
|
|
506
|
-
const wanted = settings?.[name];
|
|
507
|
-
this.settings[name] = wanted !== void 0 && setting.options.includes(wanted) ? wanted : setting.default;
|
|
508
|
-
}
|
|
509
|
-
this.seats = mode.seats.map((seat) => new Seat(seat, !!ai?.includes(seat.player), {
|
|
510
|
-
...Object.fromEntries(Object.entries(this.seatOffered).map(([name, setting]) => [name, setting.default])),
|
|
511
|
-
...Object.fromEntries(Object.entries(seat.preset ?? {}).filter(([name]) => Object.hasOwn(this.seatOffered, name))),
|
|
512
|
-
...seat.fixed
|
|
513
|
-
}));
|
|
514
|
-
}
|
|
515
|
-
/** A seat by its player */
|
|
516
|
-
seat(player) {
|
|
517
|
-
return this.seats.find((each) => each.player === player);
|
|
518
|
-
}
|
|
519
|
-
/** A seat as everyone is told it (the lobby, the hooks) */
|
|
520
|
-
publicSeat(seat) {
|
|
521
|
-
const { ratings, mode } = this.server.options;
|
|
522
|
-
const team = seat.settings?.team !== void 0 ? Number(seat.settings.team) : seat.mode.team;
|
|
523
|
-
return {
|
|
524
|
-
player: seat.player,
|
|
525
|
-
name: seat.name,
|
|
526
|
-
ai: seat.ai,
|
|
527
|
-
connected: seat.connected,
|
|
528
|
-
...team !== void 0 && { team },
|
|
529
|
-
...this.server.phase === "lobby" && !seat.ai && { ready: !!seat.ready },
|
|
530
|
-
...seat.settings && Object.keys(seat.settings).length > 0 && { settings: { ...seat.settings } },
|
|
531
|
-
...seat.mode.fixed && Object.keys(seat.mode.fixed).length > 0 && { fixed: Object.keys(seat.mode.fixed) },
|
|
532
|
-
...seat.rated && { rated: seat.rated },
|
|
533
|
-
...seat.rated && ratings && { rating: ratings.get(mode.id, seat.rated)?.rating ?? void 0 },
|
|
534
|
-
...this.kickVotes.get(seat.player)?.size && { kick: {
|
|
535
|
-
votes: this.kickVotes.get(seat.player).size,
|
|
536
|
-
needed: this.kickNeeded(seat)
|
|
537
|
-
} }
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
/** The players who may vote to let one go: the others seated and there */
|
|
541
|
-
voters(about) {
|
|
542
|
-
return this.seats.filter((seat) => seat !== about && seat.connected && !seat.ai);
|
|
543
|
-
}
|
|
544
|
-
/** How many votes let a seated player go: most of the others seated and there */
|
|
545
|
-
kickNeeded(about) {
|
|
546
|
-
return Math.floor(this.voters(about).length / 2) + 1;
|
|
547
|
-
}
|
|
548
|
-
/**
|
|
549
|
-
* A seated player votes to let another go, in the lobby: once most of the others seated and there have, the seat
|
|
550
|
-
* voted on is given back (its player is the server's to let go, `GameServer.kick`). Nothing for a seat nobody is in,
|
|
551
|
-
* a computer's, or one's own
|
|
552
|
-
*/
|
|
553
|
-
voteKick(by, player) {
|
|
554
|
-
const about = this.seat(player);
|
|
555
|
-
if (this.server.phase !== "lobby" || !about || about === by || !about.connected || about.ai || !by.connected) return void 0;
|
|
556
|
-
const votes = this.kickVotes.get(player) ?? /* @__PURE__ */ new Set();
|
|
557
|
-
votes.add(by.player);
|
|
558
|
-
this.kickVotes.set(player, votes);
|
|
559
|
-
if (this.voters(about).filter((seat) => votes.has(seat.player)).length >= this.kickNeeded(about)) {
|
|
560
|
-
this.kickVotes.delete(player);
|
|
561
|
-
return about;
|
|
562
|
-
}
|
|
563
|
-
this.broadcast();
|
|
564
|
-
}
|
|
565
|
-
/** A player left their seat, or moved: the votes about them and theirs go */
|
|
566
|
-
forgetVotes(player) {
|
|
567
|
-
this.kickVotes.delete(player);
|
|
568
|
-
for (const votes of this.kickVotes.values()) votes.delete(player);
|
|
569
|
-
}
|
|
570
|
-
/** The rating of the players in its seats, on average: none where nobody rated sits or there are no ratings */
|
|
571
|
-
rating() {
|
|
572
|
-
const { ratings, mode } = this.server.options;
|
|
573
|
-
if (!ratings) return void 0;
|
|
574
|
-
const rated = this.seats.filter((seat) => seat.connected && seat.rated).map((seat) => ratings.get(mode.id, seat.rated)?.rating ?? void 0).filter((rating) => rating !== void 0);
|
|
575
|
-
return rated.length ? Math.round(rated.reduce((sum, rating) => sum + rating, 0) / rated.length) : void 0;
|
|
576
|
-
}
|
|
577
|
-
/** Tells everyone the lobby as it is now */
|
|
578
|
-
broadcast() {
|
|
579
|
-
const { mode } = this.server.options;
|
|
580
|
-
this.server.tellAll({
|
|
581
|
-
type: "lobby",
|
|
582
|
-
mode: mode.id,
|
|
583
|
-
game: mode.game.id,
|
|
584
|
-
phase: this.server.phase,
|
|
585
|
-
seats: this.seats.map((seat) => this.publicSeat(seat)),
|
|
586
|
-
settings: { ...this.settings },
|
|
587
|
-
offered: this.hooks.settings ?? {},
|
|
588
|
-
seatOffered: this.seatOffered
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
/** A player took a seat in the lobby: the game starts once every seat is filled, unless they are back from one ("Again") */
|
|
592
|
-
seated() {
|
|
593
|
-
this.broadcast();
|
|
594
|
-
if ((this.server.options.autoStart ?? true) && !this.again && this.seats.every((each) => each.ai || each.connected)) this.server.startGame();
|
|
595
|
-
}
|
|
596
|
-
/** A player left the lobby: their seat is free for anyone */
|
|
597
|
-
left(seat) {
|
|
598
|
-
this.forgetVotes(seat.player);
|
|
599
|
-
seat.free();
|
|
600
|
-
this.broadcast();
|
|
601
|
-
}
|
|
602
|
-
/** A seated player moves to another seat, free, in the lobby: their own is free for anyone then. The seat moved to, or none */
|
|
603
|
-
move(from, player) {
|
|
604
|
-
const to = this.seat(player);
|
|
605
|
-
if (!to || to === from || this.server.phase !== "lobby" || !to.isFree) return void 0;
|
|
606
|
-
this.forgetVotes(from.player);
|
|
607
|
-
to.takeFrom(from);
|
|
608
|
-
this.server.logger.info("server", `${to.name} moved from seat ${from.player} to ${to.player}`, {
|
|
609
|
-
name: to.name,
|
|
610
|
-
from: from.player,
|
|
611
|
-
player: to.player
|
|
612
|
-
});
|
|
613
|
-
this.broadcast();
|
|
614
|
-
return to;
|
|
615
|
-
}
|
|
616
|
-
/** A seated player says they are ready, or not: once every player seated is, the game starts */
|
|
617
|
-
ready(seat, ready) {
|
|
618
|
-
if (this.server.phase !== "lobby" || seat.ready === ready) return;
|
|
619
|
-
seat.ready = ready;
|
|
620
|
-
this.broadcast();
|
|
621
|
-
if (this.seats.every((each) => !each.connected || each.ready)) this.server.startGame();
|
|
622
|
-
}
|
|
623
|
-
/** A seated player's new name */
|
|
624
|
-
rename(seat, name) {
|
|
625
|
-
if (this.server.phase !== "lobby") return;
|
|
626
|
-
const trimmed = name.trim().slice(0, 24);
|
|
627
|
-
if (!trimmed || trimmed === seat.name) return;
|
|
628
|
-
seat.name = trimmed;
|
|
629
|
-
this.broadcast();
|
|
630
|
-
}
|
|
631
|
-
/** A setting of the game a seated player chose: one it offers, with a value it offers */
|
|
632
|
-
set(name, value) {
|
|
633
|
-
if (this.server.phase !== "lobby" || !Object.hasOwn(this.hooks.settings ?? {}, name)) return;
|
|
634
|
-
if (!this.hooks.settings[name].options.includes(value)) return;
|
|
635
|
-
this.settings[name] = value;
|
|
636
|
-
this.broadcast();
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* A setting of a seat chosen by a seated player (`by`): their own seat, or one no other player holds (a computer's,
|
|
640
|
-
* or one nobody took yet). Not what the mode fixes
|
|
641
|
-
*/
|
|
642
|
-
setSeat(by, player, name, value) {
|
|
643
|
-
if (this.server.phase !== "lobby" || !Object.hasOwn(this.seatOffered, name)) return;
|
|
644
|
-
if (!this.seatOffered[name].options.includes(value)) return;
|
|
645
|
-
const seat = this.seat(player);
|
|
646
|
-
if (!seat || seat !== by && seat.connected && !seat.ai) return;
|
|
647
|
-
if (seat.mode.fixed && Object.hasOwn(seat.mode.fixed, name)) return;
|
|
648
|
-
if (this.seatOffered[name].unique && this.seats.some((other) => other !== seat && other.settings?.[name] === value)) return;
|
|
649
|
-
seat.settings = {
|
|
650
|
-
...seat.settings,
|
|
651
|
-
[name]: value
|
|
652
|
-
};
|
|
653
|
-
this.broadcast();
|
|
654
|
-
}
|
|
655
|
-
/** A game starts: what nobody joined is the computer's */
|
|
656
|
-
fill() {
|
|
657
|
-
this.kickVotes.clear();
|
|
658
|
-
for (const seat of this.seats) if (!seat.connected) seat.ai = true;
|
|
659
|
-
}
|
|
660
|
-
/** Back after a game, with who is still there in their seats (and who comes again with `keepToken`): the seats of those gone are free */
|
|
661
|
-
backFromGame(keepToken) {
|
|
662
|
-
this.again = true;
|
|
663
|
-
for (const seat of this.seats) {
|
|
664
|
-
seat.newGame();
|
|
665
|
-
if (!seat.connected && seat.token !== keepToken) seat.free();
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
/** New, for a new game with nobody: every seat free */
|
|
669
|
-
reset() {
|
|
670
|
-
this.again = false;
|
|
671
|
-
for (const seat of this.seats) seat.reset();
|
|
672
|
-
}
|
|
673
|
-
};
|
|
674
|
-
//#endregion
|
|
675
|
-
//#region src/server/ops.ts
|
|
676
|
-
/** The numbers of a process: its memory */
|
|
677
|
-
var memory = () => {
|
|
678
|
-
const { rss, heapUsed } = process.memoryUsage();
|
|
679
|
-
return {
|
|
680
|
-
rss,
|
|
681
|
-
heapUsed
|
|
682
|
-
};
|
|
683
|
-
};
|
|
684
|
-
/**
|
|
685
|
-
* The numbers of the rooms of a process, in Prometheus's text: each by its room, where there is more than one
|
|
686
|
-
*
|
|
687
|
-
* @internal
|
|
688
|
-
*/
|
|
689
|
-
function metricsText(rooms) {
|
|
690
|
-
const lines = [];
|
|
691
|
-
const each = (name, help, type, value) => {
|
|
692
|
-
lines.push(`# HELP vg_${name} ${help}`, `# TYPE vg_${name} ${type}`);
|
|
693
|
-
for (const [room, metrics] of rooms) lines.push(`vg_${name}${room ? `{room=${JSON.stringify(room)}}` : ""} ${value(metrics)}`);
|
|
694
|
-
};
|
|
695
|
-
each("up", "Whether it takes new games: 0 once it is stopping", "gauge", (m) => m.draining ? 0 : 1);
|
|
696
|
-
each("games_running", "Games running now", "gauge", (m) => m.games.running);
|
|
697
|
-
each("games_played_total", "Games played to their end", "counter", (m) => m.games.played);
|
|
698
|
-
each("players", "Seats held by someone connected", "gauge", (m) => m.players);
|
|
699
|
-
each("watching", "Connections watching a game", "gauge", (m) => m.watching);
|
|
700
|
-
each("connections", "Connections open", "gauge", (m) => m.connections);
|
|
701
|
-
each("ticks_total", "Ticks of the simulation stepped", "counter", (m) => m.ticks);
|
|
702
|
-
each("tick_seconds_total", "Seconds the ticks took", "counter", (m) => round(m.tickSeconds));
|
|
703
|
-
each("last_tick_seconds", "How long the last tick took", "gauge", (m) => round(m.lastTickSeconds));
|
|
704
|
-
each("sends_total", "Rounds of sending what changed", "counter", (m) => m.sends);
|
|
705
|
-
each("send_seconds_total", "Seconds the sending took", "counter", (m) => round(m.sendSeconds));
|
|
706
|
-
each("bytes_sent", "Bytes sent to those connected now", "gauge", (m) => m.bytesSent);
|
|
707
|
-
each("uptime_seconds", "Seconds since it started", "gauge", (m) => round(m.uptime));
|
|
708
|
-
const { rss, heapUsed } = memory();
|
|
709
|
-
lines.push("# HELP vg_protocol_version The version of the wire its players have to speak", "# TYPE vg_protocol_version gauge", `vg_protocol_version 13`, "# HELP vg_memory_bytes Memory of the process", "# TYPE vg_memory_bytes gauge", `vg_memory_bytes{kind="rss"} ${rss}`, `vg_memory_bytes{kind="heap"} ${heapUsed}`);
|
|
710
|
-
return `${lines.join("\n")}\n`;
|
|
711
|
-
}
|
|
712
|
-
var round = (value) => Math.round(value * 1e6) / 1e6;
|
|
713
|
-
/**
|
|
714
|
-
* Answers the health and numbers addresses for the rooms of a process; false for any other request, which goes to
|
|
715
|
-
* what answered it before. `draining` is whether the process takes no new games
|
|
716
|
-
*/
|
|
717
|
-
function answerOps(request, response, options, state, games, ratings) {
|
|
718
|
-
const { pathname, searchParams } = new URL(request.url ?? "/", "http://server");
|
|
719
|
-
const best = options?.ratings ?? "/ratings";
|
|
720
|
-
if (best && ratings?.ratings && pathname === best && (request.method === "GET" || request.method === "HEAD")) {
|
|
721
|
-
const mode = searchParams.get("mode") ?? ratings.modes[0] ?? "";
|
|
722
|
-
const count = Math.min(Math.max(Number(searchParams.get("n")) || 10, 1), 100);
|
|
723
|
-
const player = searchParams.get("player");
|
|
724
|
-
const top = ratings.ratings.top?.(mode, count) ?? [];
|
|
725
|
-
const own = player ? ratings.ratings.placeOf?.(mode, player) : void 0;
|
|
726
|
-
response.writeHead(200, {
|
|
727
|
-
"content-type": "application/json",
|
|
728
|
-
"cache-control": "no-store",
|
|
729
|
-
"access-control-allow-origin": "*"
|
|
730
|
-
});
|
|
731
|
-
response.end(JSON.stringify({
|
|
732
|
-
mode,
|
|
733
|
-
modes: ratings.modes,
|
|
734
|
-
top,
|
|
735
|
-
...own && { player: own }
|
|
736
|
-
}));
|
|
737
|
-
return true;
|
|
738
|
-
}
|
|
739
|
-
const health = options?.health ?? "/health";
|
|
740
|
-
const metrics = options?.metrics ?? "/metrics";
|
|
741
|
-
const list = options?.games ?? "/games";
|
|
742
|
-
if (request.method !== "GET" && request.method !== "HEAD") return false;
|
|
743
|
-
if (list && games && pathname === list) {
|
|
744
|
-
response.writeHead(200, {
|
|
745
|
-
"content-type": "application/json",
|
|
746
|
-
"cache-control": "no-store",
|
|
747
|
-
"access-control-allow-origin": "*"
|
|
748
|
-
});
|
|
749
|
-
response.end(JSON.stringify({
|
|
750
|
-
draining: state().draining,
|
|
751
|
-
...games()
|
|
752
|
-
}));
|
|
753
|
-
return true;
|
|
754
|
-
}
|
|
755
|
-
if (health && pathname === health) {
|
|
756
|
-
const { draining } = state();
|
|
757
|
-
response.writeHead(draining ? 503 : 200, {
|
|
758
|
-
"content-type": "application/json",
|
|
759
|
-
"cache-control": "no-store"
|
|
760
|
-
});
|
|
761
|
-
response.end(JSON.stringify({
|
|
762
|
-
ok: !draining,
|
|
763
|
-
draining
|
|
764
|
-
}));
|
|
765
|
-
return true;
|
|
766
|
-
}
|
|
767
|
-
if (metrics && pathname === metrics) {
|
|
768
|
-
if (options?.metricsKey && !sameSecret(request.headers.authorization ?? "", `Bearer ${options.metricsKey}`)) {
|
|
769
|
-
response.writeHead(401, {
|
|
770
|
-
"content-type": "text/plain",
|
|
771
|
-
"www-authenticate": "Bearer"
|
|
772
|
-
});
|
|
773
|
-
response.end("The numbers take a key\n");
|
|
774
|
-
return true;
|
|
775
|
-
}
|
|
776
|
-
const { rooms, draining } = state();
|
|
777
|
-
if (searchParams.has("json")) {
|
|
778
|
-
response.writeHead(200, {
|
|
779
|
-
"content-type": "application/json",
|
|
780
|
-
"cache-control": "no-store"
|
|
781
|
-
});
|
|
782
|
-
response.end(JSON.stringify({
|
|
783
|
-
draining,
|
|
784
|
-
protocol: 13,
|
|
785
|
-
memory: memory(),
|
|
786
|
-
rooms: Object.fromEntries(rooms)
|
|
787
|
-
}));
|
|
788
|
-
} else {
|
|
789
|
-
response.writeHead(200, {
|
|
790
|
-
"content-type": "text/plain; version=0.0.4",
|
|
791
|
-
"cache-control": "no-store"
|
|
792
|
-
});
|
|
793
|
-
response.end(metricsText(rooms));
|
|
794
|
-
}
|
|
795
|
-
return true;
|
|
796
|
-
}
|
|
797
|
-
return false;
|
|
798
|
-
}
|
|
799
|
-
/** A game's result as a file of its own in `dir` (`GameServerOptions.resultsDir`): by when it ended and its mode */
|
|
800
|
-
async function writeResult(dir, result) {
|
|
801
|
-
await mkdir(dir, { recursive: true });
|
|
802
|
-
await writeFile(join(dir, `${Date.now()}-${result.mode.replace(/[^\w-]/g, "_")}.json`), JSON.stringify(result, null, 2));
|
|
803
|
-
}
|
|
804
|
-
//#endregion
|
|
805
|
-
//#region src/server/match.ts
|
|
806
|
-
/**
|
|
807
|
-
* A server's game (`Match`): its world and the host that sends it, a pause, what a cheating client gets, the end of a
|
|
808
|
-
* game everyone left, and its result told and kept (design review E12). The server makes one when its lobby starts a
|
|
809
|
-
* game, seats the connections at it, and lets go of it back in the lobby
|
|
810
|
-
*/
|
|
811
|
-
/**
|
|
812
|
-
* A game a server runs, from its world made to its result kept
|
|
813
|
-
*
|
|
814
|
-
* @internal
|
|
815
|
-
*/
|
|
816
|
-
var Match = class Match {
|
|
817
|
-
server;
|
|
818
|
-
world;
|
|
819
|
-
host;
|
|
820
|
-
/** Who paused the game: an admin, or a player (with a pause of theirs, for so long at most). Nobody while it runs */
|
|
821
|
-
pausedBy;
|
|
822
|
-
/** What the players are told of a game that did not end by its rules: an error thrown out of a tick, a cheat, the server stopping */
|
|
823
|
-
failed;
|
|
824
|
-
hooks;
|
|
825
|
-
resumeTimer;
|
|
826
|
-
/** Counts down to the end of a game every player has left */
|
|
827
|
-
abandoned;
|
|
828
|
-
watcher;
|
|
829
|
-
statsTimer;
|
|
830
|
-
constructor(server, world, host) {
|
|
831
|
-
this.server = server;
|
|
832
|
-
this.world = world;
|
|
833
|
-
this.host = host;
|
|
834
|
-
this.hooks = server.options.mode.game.server ?? {};
|
|
835
|
-
}
|
|
836
|
-
/**
|
|
837
|
-
* Makes the world of a game with these settings, the computer in the seats that are its, and its host: the game's
|
|
838
|
-
* `onGameStart` told. Nobody is connected to it yet, and it does not run until `run`
|
|
839
|
-
*/
|
|
840
|
-
static async make(server, settings) {
|
|
841
|
-
const { mode } = server.options;
|
|
842
|
-
const { seats } = server;
|
|
843
|
-
const world = await mode.makeWorld({ ...settings }, {
|
|
844
|
-
log: server.logger,
|
|
845
|
-
seats: Object.fromEntries(seats.map((seat) => [seat.player, { ...seat.settings }])),
|
|
846
|
-
...server.navigation && { navigation: server.navigation }
|
|
847
|
-
});
|
|
848
|
-
world.errorPolicy ??= "end";
|
|
849
|
-
server.fill();
|
|
850
|
-
const modeSeats = seats.map((seat) => seat.mode);
|
|
851
|
-
for (const seat of seats) {
|
|
852
|
-
if (!seat.ai || !mode.ai) continue;
|
|
853
|
-
const seatSettings = { ...seat.settings };
|
|
854
|
-
const brain = typeof mode.brain === "function" ? mode.brain(seat.mode, modeSeats, seatSettings) : mode.brain;
|
|
855
|
-
world.computers.seat(seat.player, {
|
|
856
|
-
...mode.ai(seat.mode, modeSeats, seatSettings),
|
|
857
|
-
brain
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
let match;
|
|
861
|
-
world.events.on(commandRejected, ({ player, type, kind, reason }) => match.cheated(player, kind, reason, type));
|
|
862
|
-
world.events.on(pauseAsked, ({ player, seconds }) => match.pause(true, {
|
|
863
|
-
player,
|
|
864
|
-
seconds
|
|
865
|
-
}));
|
|
866
|
-
const host = new SessionHost(world, {
|
|
867
|
-
sendRate: server.options.sendRate,
|
|
868
|
-
ownsWorld: true,
|
|
869
|
-
dropSilentAfter: server.options.dropSilentAfter,
|
|
870
|
-
keptBytes: server.options.keptBytes,
|
|
871
|
-
onError: (error) => {
|
|
872
|
-
server.logger.error("server", "stepping the game threw: the game ends", {
|
|
873
|
-
error: errorFields(error),
|
|
874
|
-
game: mode.game.id
|
|
875
|
-
});
|
|
876
|
-
if (match) match.failed = "The game stopped: an error on the server";
|
|
877
|
-
server.end();
|
|
878
|
-
},
|
|
879
|
-
onSlowTick: (ms, tick) => {
|
|
880
|
-
if (tick % 30 === 0) server.logger.warn("server", `tick ${tick} took ${ms.toFixed(1)} ms`, {
|
|
881
|
-
ms,
|
|
882
|
-
tick
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
});
|
|
886
|
-
match = new Match(server, world, host);
|
|
887
|
-
match.hooks.onGameStart?.(world, seats.map((seat) => server.publicSeat(seat)), { ...settings });
|
|
888
|
-
return match;
|
|
889
|
-
}
|
|
890
|
-
/** Runs it, those seated connected to it: its ticks, the line of its time every `stats` seconds, and the look out for its end */
|
|
891
|
-
run() {
|
|
892
|
-
const { server, host, world } = this;
|
|
893
|
-
host.start();
|
|
894
|
-
server.logger.info("server", `the game started: ${server.seats.map((seat) => `${seat.name}${seat.ai ? " (computer)" : ""}`).join(", ")}`);
|
|
895
|
-
if (server.options.stats) {
|
|
896
|
-
host.takeTiming();
|
|
897
|
-
this.statsTimer = setInterval(() => this.printStats(), server.options.stats * 1e3);
|
|
898
|
-
}
|
|
899
|
-
this.watcher = setInterval(() => {
|
|
900
|
-
if (world.winner !== null || world.failure) server.end();
|
|
901
|
-
}, 250);
|
|
902
|
-
}
|
|
903
|
-
/** A seat's client sent what an honest one never does: counted, said, and answered as the game says (`onCheat`) */
|
|
904
|
-
cheated(player, kind, reason, type) {
|
|
905
|
-
const { server } = this;
|
|
906
|
-
const seat = server.seats.find((each) => each.player === player);
|
|
907
|
-
if (!seat || seat.ai || !seat.connection) return;
|
|
908
|
-
const { kickAfter = 20, seconds = 10 } = server.options.cheating ?? {};
|
|
909
|
-
const now = performance.now();
|
|
910
|
-
const { cheats } = seat;
|
|
911
|
-
cheats.at = cheats.at.filter((at) => now - at < seconds * 1e3);
|
|
912
|
-
cheats.at.push(now);
|
|
913
|
-
cheats.total++;
|
|
914
|
-
const report = {
|
|
915
|
-
seat: server.publicSeat(seat),
|
|
916
|
-
address: seat.connection.address,
|
|
917
|
-
kind,
|
|
918
|
-
reason,
|
|
919
|
-
...type !== void 0 && { type },
|
|
920
|
-
recent: cheats.at.length,
|
|
921
|
-
total: cheats.total
|
|
922
|
-
};
|
|
923
|
-
let answer;
|
|
924
|
-
try {
|
|
925
|
-
answer = server.options.onCheat ? server.options.onCheat(report) : cheats.at.length >= kickAfter ? "kick" : "warn";
|
|
926
|
-
} catch (error) {
|
|
927
|
-
server.logger.error("server", "onCheat threw: a warning, as without it", { error: errorFields(error) });
|
|
928
|
-
answer = "warn";
|
|
929
|
-
}
|
|
930
|
-
const fields = {
|
|
931
|
-
name: seat.name,
|
|
932
|
-
player: seat.player,
|
|
933
|
-
address: report.address,
|
|
934
|
-
reason,
|
|
935
|
-
type,
|
|
936
|
-
recent: report.recent,
|
|
937
|
-
total: report.total
|
|
938
|
-
};
|
|
939
|
-
const said = `${seat.name} sent what their client never does: ${reason} (${report.recent} in ${seconds} s, ${report.total} in all)`;
|
|
940
|
-
if (answer === "ignore" || answer === void 0) return server.logger.debug("server", said, fields);
|
|
941
|
-
server.logger.warn("server", said, fields);
|
|
942
|
-
if (answer === "kick") server.kick(seat.player, "sent what the game does not let a player send");
|
|
943
|
-
else if (answer === "end") {
|
|
944
|
-
this.failed = `The game ended: ${seat.name} sent what the game does not let a player send`;
|
|
945
|
-
server.end();
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
/**
|
|
949
|
-
* Pauses the game for everyone, or lets it go on. A player's pause (`by`, from the `pause` command) ends by itself
|
|
950
|
-
* after its seconds, or when a seated player says `resume`; an admin's, when the admin says so. A player's pause is
|
|
951
|
-
* refused while an admin's holds: its timer would end the admin's
|
|
952
|
-
*/
|
|
953
|
-
pause(paused, by) {
|
|
954
|
-
const { server, world } = this;
|
|
955
|
-
if (paused && by && this.pausedBy === "admin") return;
|
|
956
|
-
this.unpause();
|
|
957
|
-
this.host.speed = paused ? 0 : 1;
|
|
958
|
-
if (paused) {
|
|
959
|
-
this.pausedBy = by?.player ?? "admin";
|
|
960
|
-
if (by) this.resumeTimer = setTimeout(() => this.pause(false), by.seconds * 1e3);
|
|
961
|
-
}
|
|
962
|
-
for (const seat of server.seats) if (!seat.ai) world.message(seat.player, { key: paused ? "gamePaused" : "gameResumed" }, "info");
|
|
963
|
-
server.tellAll({
|
|
964
|
-
type: "paused",
|
|
965
|
-
paused,
|
|
966
|
-
...paused && by && {
|
|
967
|
-
player: by.player,
|
|
968
|
-
seconds: by.seconds
|
|
969
|
-
}
|
|
970
|
-
});
|
|
971
|
-
const who = by ? server.seats.find((seat) => seat.player === by.player)?.name : void 0;
|
|
972
|
-
server.logger.info("server", paused ? `paused${who ? ` by ${who}, ${by.seconds} s at most` : ""}` : "resumed");
|
|
973
|
-
}
|
|
974
|
-
/** No pause on: its timer stopped */
|
|
975
|
-
unpause() {
|
|
976
|
-
clearTimeout(this.resumeTimer);
|
|
977
|
-
this.resumeTimer = void 0;
|
|
978
|
-
this.pausedBy = void 0;
|
|
979
|
-
}
|
|
980
|
-
/**
|
|
981
|
-
* Every player left the game: it ends after a while (`abandonAfter`), unless one comes back. Nobody else would end it:
|
|
982
|
-
* the host stops with nobody to send to, the world with it, and no winner is ever decided
|
|
983
|
-
*/
|
|
984
|
-
abandon() {
|
|
985
|
-
const after = this.server.options.abandonAfter ?? 60;
|
|
986
|
-
if (!Number.isFinite(after) || this.abandoned !== void 0) return;
|
|
987
|
-
this.server.logger.info("server", `everyone left: the game ends in ${after} seconds unless someone comes back`);
|
|
988
|
-
this.abandoned = setTimeout(() => {
|
|
989
|
-
this.abandoned = void 0;
|
|
990
|
-
if (!this.server.seats.some((each) => each.connected)) this.server.end();
|
|
991
|
-
}, after * 1e3);
|
|
992
|
-
}
|
|
993
|
-
/** Someone came back to the game, or it ended: it is not abandoned */
|
|
994
|
-
keep() {
|
|
995
|
-
clearTimeout(this.abandoned);
|
|
996
|
-
this.abandoned = void 0;
|
|
997
|
-
}
|
|
998
|
-
/**
|
|
999
|
-
* A line of where the host's time went since the last one: ticks a second against the game's rate, how long a tick
|
|
1000
|
-
* of the simulation takes on average and at most, against the time a tick lasts, and the same for sending
|
|
1001
|
-
*/
|
|
1002
|
-
printStats() {
|
|
1003
|
-
const { host, world, server } = this;
|
|
1004
|
-
const timing = host.takeTiming();
|
|
1005
|
-
const budget = 1e3 / world.tickRate;
|
|
1006
|
-
const average = (total, count) => count ? total / count : 0;
|
|
1007
|
-
const tick = average(timing.tickTime, timing.ticks);
|
|
1008
|
-
const send = average(timing.sendTime, timing.sends);
|
|
1009
|
-
const busy = (timing.tickTime + timing.sendTime) / (timing.seconds * 1e3) * 100;
|
|
1010
|
-
server.logger.info("server", [
|
|
1011
|
-
`ticks ${(timing.ticks / timing.seconds).toFixed(1)}/s of ${world.tickRate}`,
|
|
1012
|
-
`step avg ${tick.toFixed(2)} ms, max ${timing.tickMax.toFixed(2)} ms (${(tick / budget * 100).toFixed(1)}% of ${budget.toFixed(1)} ms)`,
|
|
1013
|
-
`send avg ${send.toFixed(2)} ms, max ${timing.sendMax.toFixed(2)} ms, ${(timing.sends / timing.seconds).toFixed(1)}/s`,
|
|
1014
|
-
`busy ${busy.toFixed(1)}%`,
|
|
1015
|
-
`${world.entities.size} entities, ${server.seats.filter((seat) => seat.connected).length} players and ${server.watching} watching`
|
|
1016
|
-
].join(" | "));
|
|
1017
|
-
}
|
|
1018
|
-
/** Its timers stopped: no pause, no end looked out for or counted down to, no line of its time */
|
|
1019
|
-
stop() {
|
|
1020
|
-
this.keep();
|
|
1021
|
-
this.unpause();
|
|
1022
|
-
clearInterval(this.watcher);
|
|
1023
|
-
clearInterval(this.statsTimer);
|
|
1024
|
-
}
|
|
1025
|
-
/** The game is over: its timers stopped, the result told to everyone and kept. The game goes on being shown */
|
|
1026
|
-
async end() {
|
|
1027
|
-
const { server, world } = this;
|
|
1028
|
-
const { options } = server;
|
|
1029
|
-
this.stop();
|
|
1030
|
-
const result = {
|
|
1031
|
-
game: options.mode.game.id,
|
|
1032
|
-
mode: options.mode.id,
|
|
1033
|
-
winner: world.winner,
|
|
1034
|
-
duration: world.time,
|
|
1035
|
-
...(world.failure || this.failed) && { failed: world.failure?.message ?? this.failed },
|
|
1036
|
-
seats: server.seats.map((seat) => {
|
|
1037
|
-
const team = world.player(seat.player).team;
|
|
1038
|
-
return {
|
|
1039
|
-
...server.publicSeat(seat),
|
|
1040
|
-
outcome: world.winner === null ? "none" : world.winner === team ? "victory" : "defeat"
|
|
1041
|
-
};
|
|
1042
|
-
})
|
|
1043
|
-
};
|
|
1044
|
-
this.hooks.onGameEnd?.(world, result);
|
|
1045
|
-
server.tellAll({
|
|
1046
|
-
type: "result",
|
|
1047
|
-
result
|
|
1048
|
-
});
|
|
1049
|
-
server.logger.info("server", `the game ended: ${result.seats.map((seat) => `${seat.name} ${seat.outcome}`).join(", ")}`);
|
|
1050
|
-
try {
|
|
1051
|
-
if (this.hooks.saveResult) await this.hooks.saveResult(result);
|
|
1052
|
-
else if (options.resultsDir) await writeResult(options.resultsDir, result);
|
|
1053
|
-
} catch (error) {
|
|
1054
|
-
server.logger.error("server", "the result could not be kept", { error: errorFields(error) });
|
|
1055
|
-
}
|
|
1056
|
-
try {
|
|
1057
|
-
await options.ratings?.record(result);
|
|
1058
|
-
} catch (error) {
|
|
1059
|
-
server.logger.error("server", "the ratings could not be kept", { error: errorFields(error) });
|
|
1060
|
-
}
|
|
1061
|
-
return result;
|
|
1062
|
-
}
|
|
1063
|
-
};
|
|
1064
|
-
//#endregion
|
|
1065
|
-
//#region src/server/websocket.ts
|
|
1066
|
-
var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
1067
|
-
var TEXT = 1;
|
|
1068
|
-
var CONTINUATION = 0;
|
|
1069
|
-
var CLOSE = 8;
|
|
1070
|
-
var PING = 9;
|
|
1071
|
-
var PONG = 10;
|
|
1072
|
-
/**
|
|
1073
|
-
* Starts a WebSocket server, and resolves once it listens
|
|
1074
|
-
*
|
|
1075
|
-
* @beta
|
|
1076
|
-
*/
|
|
1077
|
-
function startSocketServer(options) {
|
|
1078
|
-
const maxMessage = options.maxMessage ?? 65536;
|
|
1079
|
-
const maxConnections = options.maxConnections ?? 1024;
|
|
1080
|
-
const limits = {
|
|
1081
|
-
idle: (options.idleTimeout ?? 60) * 1e3,
|
|
1082
|
-
buffered: options.maxBuffered ?? 16777216
|
|
1083
|
-
};
|
|
1084
|
-
let handler = () => {};
|
|
1085
|
-
const open = /* @__PURE__ */ new Set();
|
|
1086
|
-
/** Each connection's own check: pinged when quiet, closed when silent too long */
|
|
1087
|
-
const sweeps = /* @__PURE__ */ new Set();
|
|
1088
|
-
const sweeper = setInterval(() => {
|
|
1089
|
-
for (const sweep of sweeps) sweep();
|
|
1090
|
-
}, Math.max(Math.min(limits.idle / 3, 2e4), 50));
|
|
1091
|
-
sweeper.unref?.();
|
|
1092
|
-
const server = createServer(options.onRequest ?? ((_request, response) => {
|
|
1093
|
-
response.writeHead(426, { "content-type": "text/plain" });
|
|
1094
|
-
response.end("A game server: connect with a WebSocket\n");
|
|
1095
|
-
}));
|
|
1096
|
-
server.on("upgrade", (request, socket) => {
|
|
1097
|
-
const key = request.headers["sec-websocket-key"];
|
|
1098
|
-
if (request.headers.upgrade?.toLowerCase() !== "websocket" || typeof key !== "string") {
|
|
1099
|
-
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
1100
|
-
return;
|
|
1101
|
-
}
|
|
1102
|
-
if (open.size >= maxConnections) {
|
|
1103
|
-
socket.end("HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\n\r\n");
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
const accept = createHash("sha1").update(key + GUID).digest("base64");
|
|
1107
|
-
const offered = String(request.headers["sec-websocket-extensions"] ?? "");
|
|
1108
|
-
const compressed = (options.compress ?? true) && /(^|,)\s*permessage-deflate\b/.test(offered);
|
|
1109
|
-
const extension = compressed ? "Sec-WebSocket-Extensions: permessage-deflate; client_no_context_takeover\r\n" : "";
|
|
1110
|
-
socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n${extension}\r\n`);
|
|
1111
|
-
open.add(socket);
|
|
1112
|
-
const { connection, sweep } = connectionOf(socket, request, maxMessage, compressed, limits, options.log ?? rootLogger);
|
|
1113
|
-
sweeps.add(sweep);
|
|
1114
|
-
socket.on("close", () => {
|
|
1115
|
-
open.delete(socket);
|
|
1116
|
-
sweeps.delete(sweep);
|
|
1117
|
-
});
|
|
1118
|
-
handler(connection);
|
|
1119
|
-
});
|
|
1120
|
-
return new Promise((resolve) => server.listen(options.port, () => {
|
|
1121
|
-
const address = server.address();
|
|
1122
|
-
resolve({
|
|
1123
|
-
server,
|
|
1124
|
-
port: typeof address === "object" && address ? address.port : options.port,
|
|
1125
|
-
onConnection: (next) => handler = next,
|
|
1126
|
-
close: () => new Promise((done) => {
|
|
1127
|
-
clearInterval(sweeper);
|
|
1128
|
-
for (const socket of open) socket.destroy();
|
|
1129
|
-
server.close(() => done());
|
|
1130
|
-
})
|
|
1131
|
-
});
|
|
1132
|
-
}));
|
|
1133
|
-
}
|
|
1134
|
-
/** The end every compressed message has, which the wire leaves off (RFC 7692): an empty block of a sync flush */
|
|
1135
|
-
var SYNC_TAIL = Buffer.from([
|
|
1136
|
-
0,
|
|
1137
|
-
0,
|
|
1138
|
-
255,
|
|
1139
|
-
255
|
|
1140
|
-
]);
|
|
1141
|
-
/** Messages shorter than this go as they are: compressing them would save next to nothing */
|
|
1142
|
-
var COMPRESS_FROM = 64;
|
|
1143
|
-
/** Milliseconds a connection closed from here stays open for the other end's own close, which says it has read everything */
|
|
1144
|
-
var CLOSE_WAIT = 1e3;
|
|
1145
|
-
/** A frame to send: a server's are never masked. `deflated` marks a compressed message (RSV1) */
|
|
1146
|
-
function frame(opcode, payload, deflated = false) {
|
|
1147
|
-
const length = payload.length;
|
|
1148
|
-
const head = length < 126 ? Buffer.alloc(2) : length < 65536 ? Buffer.alloc(4) : Buffer.alloc(10);
|
|
1149
|
-
head[0] = 128 | (deflated ? 64 : 0) | opcode;
|
|
1150
|
-
if (length < 126) head[1] = length;
|
|
1151
|
-
else if (length < 65536) {
|
|
1152
|
-
head[1] = 126;
|
|
1153
|
-
head.writeUInt16BE(length, 2);
|
|
1154
|
-
} else {
|
|
1155
|
-
head[1] = 127;
|
|
1156
|
-
head.writeBigUInt64BE(BigInt(length), 2);
|
|
1157
|
-
}
|
|
1158
|
-
return Buffer.concat([head, payload]);
|
|
1159
|
-
}
|
|
1160
|
-
/**
|
|
1161
|
-
* Compresses messages one after another against all the ones before (`permessage-deflate` with context): a delta
|
|
1162
|
-
* says much of what the one before it said, which the compressor remembers. In order, as zlib works in the background
|
|
1163
|
-
*/
|
|
1164
|
-
var Compressor = class {
|
|
1165
|
-
deflate = createDeflateRaw({
|
|
1166
|
-
windowBits: 15,
|
|
1167
|
-
memLevel: 8
|
|
1168
|
-
});
|
|
1169
|
-
chunks = [];
|
|
1170
|
-
/** The last message being compressed: the next waits for it, so that they go out in order */
|
|
1171
|
-
queue = Promise.resolve();
|
|
1172
|
-
/** Closed: what is still queued is let go, not written into a stream that is gone */
|
|
1173
|
-
closed = false;
|
|
1174
|
-
constructor() {
|
|
1175
|
-
this.deflate.on("data", (chunk) => this.chunks.push(chunk));
|
|
1176
|
-
}
|
|
1177
|
-
/** Compresses a message, and hands it on when done: in the order they were given */
|
|
1178
|
-
compress(message, done) {
|
|
1179
|
-
this.queue = this.queue.then(() => new Promise((resolve) => {
|
|
1180
|
-
if (this.closed) return resolve();
|
|
1181
|
-
this.deflate.write(message);
|
|
1182
|
-
this.deflate.flush(constants.Z_SYNC_FLUSH, () => {
|
|
1183
|
-
const all = Buffer.concat(this.chunks.splice(0));
|
|
1184
|
-
done(all.subarray(0, all.length - SYNC_TAIL.length));
|
|
1185
|
-
resolve();
|
|
1186
|
-
});
|
|
1187
|
-
}));
|
|
1188
|
-
}
|
|
1189
|
-
/** Runs once what is being compressed has gone: for what goes as it is, in its turn */
|
|
1190
|
-
after(run) {
|
|
1191
|
-
this.queue = this.queue.then(run);
|
|
1192
|
-
}
|
|
1193
|
-
close() {
|
|
1194
|
-
if (this.closed) return;
|
|
1195
|
-
this.closed = true;
|
|
1196
|
-
this.deflate.close();
|
|
1197
|
-
}
|
|
1198
|
-
};
|
|
1199
|
-
function connectionOf(socket, request, maxMessage, compressed, limits, log) {
|
|
1200
|
-
let onMessage = () => {};
|
|
1201
|
-
let onClose = () => {};
|
|
1202
|
-
let closed = false;
|
|
1203
|
-
let buffer = Buffer.alloc(0);
|
|
1204
|
-
/** The parts of a text message sent in several frames, until its last, and whether it came compressed */
|
|
1205
|
-
let parts = [];
|
|
1206
|
-
let deflated = false;
|
|
1207
|
-
let bytesSent = 0;
|
|
1208
|
-
const compressor = compressed ? new Compressor() : void 0;
|
|
1209
|
-
/** Whether the close frame has gone: nothing more is written after it */
|
|
1210
|
-
let ended = false;
|
|
1211
|
-
/** When something last came in */
|
|
1212
|
-
let heardAt = Date.now();
|
|
1213
|
-
const write = (data) => {
|
|
1214
|
-
if (ended || socket.destroyed) return;
|
|
1215
|
-
bytesSent += data.length;
|
|
1216
|
-
socket.write(data);
|
|
1217
|
-
if (socket.writableLength > limits.buffered) {
|
|
1218
|
-
log.warn("server", "a connection took in too slowly, it is closed", {
|
|
1219
|
-
waiting: socket.writableLength,
|
|
1220
|
-
address: request.socket.remoteAddress
|
|
1221
|
-
});
|
|
1222
|
-
ended = true;
|
|
1223
|
-
socket.destroy();
|
|
1224
|
-
}
|
|
1225
|
-
};
|
|
1226
|
-
const shut = () => {
|
|
1227
|
-
if (closed) return;
|
|
1228
|
-
closed = true;
|
|
1229
|
-
onClose();
|
|
1230
|
-
};
|
|
1231
|
-
/** Our close has gone, and the other end's is waited for, a moment: what comes in is read for that alone */
|
|
1232
|
-
let lingering;
|
|
1233
|
-
const end = () => {
|
|
1234
|
-
clearTimeout(lingering);
|
|
1235
|
-
lingering = void 0;
|
|
1236
|
-
socket.end();
|
|
1237
|
-
};
|
|
1238
|
-
/** `answering`: the other end closed first, and has nothing more to say or to read */
|
|
1239
|
-
const close = (code = 1e3, answering = false) => {
|
|
1240
|
-
if (closed) return;
|
|
1241
|
-
const payload = Buffer.alloc(2);
|
|
1242
|
-
payload.writeUInt16BE(code, 0);
|
|
1243
|
-
const finish = () => {
|
|
1244
|
-
write(frame(CLOSE, payload));
|
|
1245
|
-
ended = true;
|
|
1246
|
-
compressor?.close();
|
|
1247
|
-
if (answering || socket.destroyed) return end();
|
|
1248
|
-
lingering = setTimeout(end, CLOSE_WAIT);
|
|
1249
|
-
lingering.unref?.();
|
|
1250
|
-
};
|
|
1251
|
-
if (compressor) compressor.after(finish);
|
|
1252
|
-
else finish();
|
|
1253
|
-
shut();
|
|
1254
|
-
};
|
|
1255
|
-
socket.on("data", (data) => {
|
|
1256
|
-
if (closed && !lingering) return;
|
|
1257
|
-
heardAt = Date.now();
|
|
1258
|
-
buffer = Buffer.concat([buffer, data]);
|
|
1259
|
-
for (;;) {
|
|
1260
|
-
if (closed && !lingering || buffer.length < 2) return;
|
|
1261
|
-
const fin = (buffer[0] & 128) !== 0;
|
|
1262
|
-
const rsv1 = (buffer[0] & 64) !== 0;
|
|
1263
|
-
const opcode = buffer[0] & 15;
|
|
1264
|
-
const masked = (buffer[1] & 128) !== 0;
|
|
1265
|
-
let length = buffer[1] & 127;
|
|
1266
|
-
let at = 2;
|
|
1267
|
-
if (length === 126) {
|
|
1268
|
-
if (buffer.length < 4) return;
|
|
1269
|
-
length = buffer.readUInt16BE(2);
|
|
1270
|
-
at = 4;
|
|
1271
|
-
} else if (length === 127) {
|
|
1272
|
-
if (buffer.length < 10) return;
|
|
1273
|
-
const long = buffer.readBigUInt64BE(2);
|
|
1274
|
-
if (long > BigInt(maxMessage)) return closed ? end() : close(1009);
|
|
1275
|
-
length = Number(long);
|
|
1276
|
-
at = 10;
|
|
1277
|
-
}
|
|
1278
|
-
if (!masked) return closed ? end() : close(1002);
|
|
1279
|
-
if (length > maxMessage) return closed ? end() : close(1009);
|
|
1280
|
-
if (buffer.length < at + 4 + length) return;
|
|
1281
|
-
const mask = buffer.subarray(at, at + 4);
|
|
1282
|
-
const payload = Buffer.from(buffer.subarray(at + 4, at + 4 + length));
|
|
1283
|
-
for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i & 3];
|
|
1284
|
-
buffer = buffer.subarray(at + 4 + length);
|
|
1285
|
-
if (closed) {
|
|
1286
|
-
if (opcode === CLOSE) return end();
|
|
1287
|
-
continue;
|
|
1288
|
-
}
|
|
1289
|
-
if (opcode === TEXT || opcode === CONTINUATION) {
|
|
1290
|
-
if (opcode === TEXT) deflated = rsv1 && compressed;
|
|
1291
|
-
parts.push(payload);
|
|
1292
|
-
if (parts.reduce((sum, part) => sum + part.length, 0) > maxMessage) return close(1009);
|
|
1293
|
-
if (!fin) continue;
|
|
1294
|
-
let data = Buffer.concat(parts);
|
|
1295
|
-
parts = [];
|
|
1296
|
-
if (deflated) try {
|
|
1297
|
-
data = inflateRawSync(Buffer.concat([data, SYNC_TAIL]), {
|
|
1298
|
-
maxOutputLength: maxMessage,
|
|
1299
|
-
finishFlush: constants.Z_SYNC_FLUSH
|
|
1300
|
-
});
|
|
1301
|
-
} catch {
|
|
1302
|
-
return close(1009);
|
|
1303
|
-
}
|
|
1304
|
-
try {
|
|
1305
|
-
onMessage(data.toString("utf8"));
|
|
1306
|
-
} catch (error) {
|
|
1307
|
-
log.error("server", "a message could not be handled, the connection is closed", {
|
|
1308
|
-
error: errorFields(error),
|
|
1309
|
-
address: request.socket.remoteAddress
|
|
1310
|
-
});
|
|
1311
|
-
return close(1011);
|
|
1312
|
-
}
|
|
1313
|
-
} else if (opcode === PING) write(frame(PONG, payload));
|
|
1314
|
-
else if (opcode === CLOSE) return close(1e3, true);
|
|
1315
|
-
}
|
|
1316
|
-
});
|
|
1317
|
-
const gone = () => {
|
|
1318
|
-
clearTimeout(lingering);
|
|
1319
|
-
lingering = void 0;
|
|
1320
|
-
ended = true;
|
|
1321
|
-
compressor?.close();
|
|
1322
|
-
shut();
|
|
1323
|
-
};
|
|
1324
|
-
socket.on("close", gone);
|
|
1325
|
-
socket.on("error", gone);
|
|
1326
|
-
const connection = {
|
|
1327
|
-
url: request.url ?? "/",
|
|
1328
|
-
address: request.socket.remoteAddress ?? "",
|
|
1329
|
-
compressed,
|
|
1330
|
-
get bytesSent() {
|
|
1331
|
-
return bytesSent;
|
|
1332
|
-
},
|
|
1333
|
-
send(message) {
|
|
1334
|
-
if (closed) return;
|
|
1335
|
-
const data = Buffer.from(message, "utf8");
|
|
1336
|
-
if (!compressor || data.length < COMPRESS_FROM) {
|
|
1337
|
-
if (compressor) compressor.after(() => write(frame(TEXT, data)));
|
|
1338
|
-
else write(frame(TEXT, data));
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
compressor.compress(data, (deflatedData) => write(frame(TEXT, deflatedData, true)));
|
|
1342
|
-
},
|
|
1343
|
-
onMessage(next) {
|
|
1344
|
-
onMessage = next;
|
|
1345
|
-
},
|
|
1346
|
-
onClose(next) {
|
|
1347
|
-
onClose = next;
|
|
1348
|
-
},
|
|
1349
|
-
close: () => close()
|
|
1350
|
-
};
|
|
1351
|
-
/** Pings a connection that has been quiet, and closes one that has said nothing for longer than it may */
|
|
1352
|
-
const sweep = () => {
|
|
1353
|
-
if (closed) return;
|
|
1354
|
-
const quiet = Date.now() - heardAt;
|
|
1355
|
-
if (quiet > limits.idle) return close(1001);
|
|
1356
|
-
if (quiet > limits.idle / 3) write(frame(PING, Buffer.alloc(0)));
|
|
1357
|
-
};
|
|
1358
|
-
return {
|
|
1359
|
-
connection,
|
|
1360
|
-
sweep
|
|
1361
|
-
};
|
|
1362
|
-
}
|
|
1363
|
-
//#endregion
|
|
1364
|
-
//#region src/server/gameServer.ts
|
|
1365
|
-
/** The messages of the lobby: each told to everyone, or a key tried (`lobbyRate`) */
|
|
1366
|
-
var LOBBY_MESSAGES = /* @__PURE__ */ new Set([
|
|
1367
|
-
"join",
|
|
1368
|
-
"sit",
|
|
1369
|
-
"ready",
|
|
1370
|
-
"resume",
|
|
1371
|
-
"name",
|
|
1372
|
-
"start",
|
|
1373
|
-
"setting",
|
|
1374
|
-
"seatSetting",
|
|
1375
|
-
"admin",
|
|
1376
|
-
"lobbyChat",
|
|
1377
|
-
"kick"
|
|
1378
|
-
]);
|
|
1379
|
-
/** How long a line said in the lobby may be */
|
|
1380
|
-
var LOBBY_LINE = 200;
|
|
1381
|
-
/** A token bucket: so many at once, filled again at so many a second */
|
|
1382
|
-
var Bucket = class {
|
|
1383
|
-
rate;
|
|
1384
|
-
tokens;
|
|
1385
|
-
toppedUp = performance.now();
|
|
1386
|
-
constructor(rate) {
|
|
1387
|
-
this.rate = rate;
|
|
1388
|
-
this.tokens = rate.burst;
|
|
1389
|
-
}
|
|
1390
|
-
/** Whether one more goes now: taken from the bucket, when it does */
|
|
1391
|
-
take() {
|
|
1392
|
-
const now = performance.now();
|
|
1393
|
-
this.tokens = Math.min(this.rate.burst, this.tokens + (now - this.toppedUp) / 1e3 * this.rate.perSecond);
|
|
1394
|
-
this.toppedUp = now;
|
|
1395
|
-
if (this.tokens < 1) return false;
|
|
1396
|
-
this.tokens--;
|
|
1397
|
-
return true;
|
|
1398
|
-
}
|
|
1399
|
-
};
|
|
1400
|
-
/**
|
|
1401
|
-
* A server of a game: its connections, who joins and where they go (`Doorman`), its lobby (`Lobby`: the seats, the
|
|
1402
|
-
* settings, when a game starts) and its game while one is on (`Match`)
|
|
1403
|
-
*
|
|
1404
|
-
* @beta
|
|
1405
|
-
*/
|
|
1406
|
-
var GameServer = class GameServer {
|
|
1407
|
-
source;
|
|
1408
|
-
options;
|
|
1409
|
-
navigation;
|
|
1410
|
-
phase = "lobby";
|
|
1411
|
-
clients = /* @__PURE__ */ new Set();
|
|
1412
|
-
/** The keys of those the lobby voted out (`kick`): kept out until the server starts afresh (`reset`) */
|
|
1413
|
-
kickedOut = /* @__PURE__ */ new Set();
|
|
1414
|
-
/** Where each who joins goes */
|
|
1415
|
-
doorman = new Doorman(this);
|
|
1416
|
-
/** The seats, the settings, and when a game starts */
|
|
1417
|
-
lobby;
|
|
1418
|
-
/** The game on, or the one that ended while its players are still on its screen */
|
|
1419
|
-
match;
|
|
1420
|
-
constructor(source, options, logger, navigation) {
|
|
1421
|
-
this.source = source;
|
|
1422
|
-
this.options = options;
|
|
1423
|
-
this.navigation = navigation;
|
|
1424
|
-
const server = this;
|
|
1425
|
-
this.logger = logger;
|
|
1426
|
-
this.lobby = new Lobby({
|
|
1427
|
-
options,
|
|
1428
|
-
logger,
|
|
1429
|
-
get phase() {
|
|
1430
|
-
return server.phase;
|
|
1431
|
-
},
|
|
1432
|
-
tellAll: (message) => this.tellAll(message),
|
|
1433
|
-
startGame: () => void this.startGame()
|
|
1434
|
-
});
|
|
1435
|
-
source.onConnection((connection) => this.accept(connection));
|
|
1436
|
-
}
|
|
1437
|
-
/** Starts a server, and resolves once it listens */
|
|
1438
|
-
static async start(options) {
|
|
1439
|
-
const logger = serverLogger(options.log);
|
|
1440
|
-
const builder = options.navigationWorker !== false ? nodeNavigationBuilder() : void 0;
|
|
1441
|
-
let server;
|
|
1442
|
-
const socket = await startSocketServer({
|
|
1443
|
-
port: options.port,
|
|
1444
|
-
compress: options.compress,
|
|
1445
|
-
idleTimeout: options.idleTimeout,
|
|
1446
|
-
maxBuffered: options.maxBuffered,
|
|
1447
|
-
maxConnections: options.maxConnections,
|
|
1448
|
-
log: logger,
|
|
1449
|
-
onRequest: (request, response) => {
|
|
1450
|
-
if (server && answerOps(request, response, options.http, () => ({
|
|
1451
|
-
draining: server.draining,
|
|
1452
|
-
rooms: /* @__PURE__ */ new Map([["", server.metrics()]])
|
|
1453
|
-
}), () => ({ games: [server.listing("")] }), {
|
|
1454
|
-
ratings: options.ratings,
|
|
1455
|
-
modes: [options.mode.id]
|
|
1456
|
-
})) return;
|
|
1457
|
-
response.writeHead(426, { "content-type": "text/plain" });
|
|
1458
|
-
response.end("A game server: connect with a WebSocket\n");
|
|
1459
|
-
}
|
|
1460
|
-
});
|
|
1461
|
-
server = new GameServer(socket, options, logger, builder && { builder });
|
|
1462
|
-
return server.begin();
|
|
1463
|
-
}
|
|
1464
|
-
/**
|
|
1465
|
-
* A server whose connections come from elsewhere (`ConnectionSource`): a room of `GameRooms`, those a port it shares
|
|
1466
|
-
* hands it. Closing it closes those, and leaves where they come from to whoever owns it
|
|
1467
|
-
*/
|
|
1468
|
-
static async attach(source, options) {
|
|
1469
|
-
return new GameServer(source, {
|
|
1470
|
-
...options,
|
|
1471
|
-
port: source.port
|
|
1472
|
-
}, serverLogger(options.log)).begin();
|
|
1473
|
-
}
|
|
1474
|
-
/** Says it is there, and starts a game at once where the computer has every seat */
|
|
1475
|
-
async begin() {
|
|
1476
|
-
const { options, source } = this;
|
|
1477
|
-
this.log(`${options.mode.name} (${options.mode.game.id}) on port ${source.port}, ${this.seats.length} seats`);
|
|
1478
|
-
if ((options.autoStart ?? true) && this.seats.every((seat) => seat.ai)) await this.startGame();
|
|
1479
|
-
return this;
|
|
1480
|
-
}
|
|
1481
|
-
/**
|
|
1482
|
-
* Its seats, in the mode's order: the server's own (its doorman's), a seat of a player is `publicSeat`'s
|
|
1483
|
-
*
|
|
1484
|
-
* @internal
|
|
1485
|
-
*/
|
|
1486
|
-
get seats() {
|
|
1487
|
-
return this.lobby.seats;
|
|
1488
|
-
}
|
|
1489
|
-
/** The game's settings (`ServerHooks.settings`), as chosen in the lobby */
|
|
1490
|
-
get settings() {
|
|
1491
|
-
return this.lobby.settings;
|
|
1492
|
-
}
|
|
1493
|
-
/** The world of the game on, or of the one that ended while its players are still on its screen */
|
|
1494
|
-
get world() {
|
|
1495
|
-
return this.match?.world;
|
|
1496
|
-
}
|
|
1497
|
-
/** What sends the game to its players, while there is one */
|
|
1498
|
-
get host() {
|
|
1499
|
-
return this.match?.host;
|
|
1500
|
-
}
|
|
1501
|
-
/** Who paused the game: an admin, or a player (with a pause of theirs, for so long at most). Nobody while it runs */
|
|
1502
|
-
get pausedBy() {
|
|
1503
|
-
return this.match?.pausedBy;
|
|
1504
|
-
}
|
|
1505
|
-
/** When it started, for its uptime */
|
|
1506
|
-
startedAt = performance.now();
|
|
1507
|
-
/** Games played to their end */
|
|
1508
|
-
played = 0;
|
|
1509
|
-
/** What the hosts of the games before this one counted: the numbers go on from there */
|
|
1510
|
-
past = {
|
|
1511
|
-
ticks: 0,
|
|
1512
|
-
tickTime: 0,
|
|
1513
|
-
sends: 0,
|
|
1514
|
-
sendTime: 0
|
|
1515
|
-
};
|
|
1516
|
-
/** It takes no new games: it is stopping once the running one is over (`close({ drain })`) */
|
|
1517
|
-
draining = false;
|
|
1518
|
-
/** Tells a stop that waits that the running game is over */
|
|
1519
|
-
drained;
|
|
1520
|
-
/** What a list of games says of it (`GET /games`, `GameListing`), as the room it is reached at */
|
|
1521
|
-
listing(room) {
|
|
1522
|
-
const { mode } = this.options;
|
|
1523
|
-
const rating = this.lobby.rating();
|
|
1524
|
-
return {
|
|
1525
|
-
room,
|
|
1526
|
-
game: mode.game.id,
|
|
1527
|
-
mode: mode.name,
|
|
1528
|
-
phase: this.phase,
|
|
1529
|
-
players: this.seats.filter((seat) => !seat.ai && seat.connected).length,
|
|
1530
|
-
free: this.seats.filter((seat) => !seat.ai && seat.isFree).length,
|
|
1531
|
-
computer: this.seats.filter((seat) => seat.ai).length,
|
|
1532
|
-
watchers: this.watching,
|
|
1533
|
-
settings: { ...this.settings },
|
|
1534
|
-
draining: this.draining,
|
|
1535
|
-
...rating !== void 0 && { rating }
|
|
1536
|
-
};
|
|
1537
|
-
}
|
|
1538
|
-
/** How many are connected to it, playing or watching: none, for a room that may be closed */
|
|
1539
|
-
get connections() {
|
|
1540
|
-
return this.clients.size;
|
|
1541
|
-
}
|
|
1542
|
-
/** How many watch */
|
|
1543
|
-
get watching() {
|
|
1544
|
-
return [...this.clients].filter((client) => client.watching).length;
|
|
1545
|
-
}
|
|
1546
|
-
/** Its numbers now: what `/metrics` says (see `ServerMetrics`) */
|
|
1547
|
-
metrics() {
|
|
1548
|
-
const totals = this.host?.totals;
|
|
1549
|
-
return {
|
|
1550
|
-
phase: this.phase,
|
|
1551
|
-
draining: this.draining,
|
|
1552
|
-
uptime: (performance.now() - this.startedAt) / 1e3,
|
|
1553
|
-
games: {
|
|
1554
|
-
running: this.phase === "game" ? 1 : 0,
|
|
1555
|
-
played: this.played
|
|
1556
|
-
},
|
|
1557
|
-
players: this.seats.filter((seat) => seat.connected).length,
|
|
1558
|
-
watching: this.watching,
|
|
1559
|
-
connections: this.clients.size,
|
|
1560
|
-
ticks: this.past.ticks + (totals?.ticks ?? 0),
|
|
1561
|
-
tickSeconds: (this.past.tickTime + (totals?.tickTime ?? 0)) / 1e3,
|
|
1562
|
-
lastTickSeconds: (totals?.lastTick ?? 0) / 1e3,
|
|
1563
|
-
sends: this.past.sends + (totals?.sends ?? 0),
|
|
1564
|
-
sendSeconds: (this.past.sendTime + (totals?.sendTime ?? 0)) / 1e3,
|
|
1565
|
-
bytesSent: this.bytesSent
|
|
1566
|
-
};
|
|
1567
|
-
}
|
|
1568
|
-
/** The game let go of, what its host counted kept for the numbers */
|
|
1569
|
-
dropMatch(options) {
|
|
1570
|
-
const { match } = this;
|
|
1571
|
-
if (!match) return;
|
|
1572
|
-
const { host } = match;
|
|
1573
|
-
match.stop();
|
|
1574
|
-
this.past.ticks += host.totals.ticks;
|
|
1575
|
-
this.past.tickTime += host.totals.tickTime;
|
|
1576
|
-
this.past.sends += host.totals.sends;
|
|
1577
|
-
this.past.sendTime += host.totals.sendTime;
|
|
1578
|
-
host.dispose(options);
|
|
1579
|
-
this.match = void 0;
|
|
1580
|
-
}
|
|
1581
|
-
get port() {
|
|
1582
|
-
return this.source.port;
|
|
1583
|
-
}
|
|
1584
|
-
/** Bytes sent to those connected now, as they went over the wire: compressed, where they are */
|
|
1585
|
-
get bytesSent() {
|
|
1586
|
-
let total = 0;
|
|
1587
|
-
for (const client of this.clients) total += client.connection.bytesSent;
|
|
1588
|
-
return total;
|
|
1589
|
-
}
|
|
1590
|
-
/** What the server and its games say: see `GameServerOptions.log` */
|
|
1591
|
-
logger;
|
|
1592
|
-
log(line, fields) {
|
|
1593
|
-
this.logger.info("server", line, fields);
|
|
1594
|
-
}
|
|
1595
|
-
accept(connection) {
|
|
1596
|
-
const { options } = this;
|
|
1597
|
-
const client = {
|
|
1598
|
-
connection,
|
|
1599
|
-
watching: false,
|
|
1600
|
-
commands: new Bucket(options.rate ?? {
|
|
1601
|
-
perSecond: 30,
|
|
1602
|
-
burst: 60
|
|
1603
|
-
}),
|
|
1604
|
-
messages: new Bucket(options.messageRate ?? {
|
|
1605
|
-
perSecond: 100,
|
|
1606
|
-
burst: 200
|
|
1607
|
-
}),
|
|
1608
|
-
lobby: new Bucket(options.lobbyRate ?? {
|
|
1609
|
-
perSecond: 4,
|
|
1610
|
-
burst: 20
|
|
1611
|
-
}),
|
|
1612
|
-
toGame: () => {},
|
|
1613
|
-
fromGameClosed: () => {},
|
|
1614
|
-
relay: void 0
|
|
1615
|
-
};
|
|
1616
|
-
client.relay = {
|
|
1617
|
-
send: (message) => connection.send(message),
|
|
1618
|
-
onMessage: (handler) => client.toGame = handler,
|
|
1619
|
-
onClose: (handler) => client.fromGameClosed = handler,
|
|
1620
|
-
close: () => connection.close()
|
|
1621
|
-
};
|
|
1622
|
-
this.clients.add(client);
|
|
1623
|
-
connection.onMessage((text) => this.receive(client, text));
|
|
1624
|
-
connection.onClose(() => this.leave(client));
|
|
1625
|
-
const within = options.joinWithin ?? 10;
|
|
1626
|
-
if (Number.isFinite(within)) {
|
|
1627
|
-
client.deadline = setTimeout(() => {
|
|
1628
|
-
if (client.joined || client.joining || !this.clients.has(client)) return;
|
|
1629
|
-
this.log("a connection that did not join in time was closed", { address: connection.address });
|
|
1630
|
-
connection.close();
|
|
1631
|
-
}, Math.max(0, within) * 1e3);
|
|
1632
|
-
client.deadline.unref?.();
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
tell(client, message) {
|
|
1636
|
-
client.connection.send(JSON.stringify(message));
|
|
1637
|
-
}
|
|
1638
|
-
/** Tells everyone who joined: never a connection that has not (RV38) */
|
|
1639
|
-
tellAll(message) {
|
|
1640
|
-
for (const client of this.clients) if (client.joined) this.tell(client, message);
|
|
1641
|
-
}
|
|
1642
|
-
receive(client, text) {
|
|
1643
|
-
if (!this.clients.has(client)) return;
|
|
1644
|
-
const message = readPlayerMessage(text);
|
|
1645
|
-
const { seat } = client;
|
|
1646
|
-
if (!(message?.type === "command" && seat && this.phase === "game") && !client.messages.take()) return;
|
|
1647
|
-
if (!message) return;
|
|
1648
|
-
if (LOBBY_MESSAGES.has(message.type) && !client.lobby.take()) return;
|
|
1649
|
-
switch (message.type) {
|
|
1650
|
-
case "join": return this.join(client, {
|
|
1651
|
-
type: "join",
|
|
1652
|
-
name: typeof message.name === "string" ? message.name : void 0,
|
|
1653
|
-
token: typeof message.token === "string" ? message.token : void 0,
|
|
1654
|
-
ticket: typeof message.ticket === "string" ? message.ticket : void 0,
|
|
1655
|
-
watch: message.watch === true,
|
|
1656
|
-
key: typeof message.key === "string" ? message.key : void 0,
|
|
1657
|
-
who: typeof message.who === "string" && message.who.length <= 128 ? message.who : void 0,
|
|
1658
|
-
back: message.back === true,
|
|
1659
|
-
protocol: isFiniteNumber(message.protocol) ? message.protocol : void 0
|
|
1660
|
-
});
|
|
1661
|
-
case "sit":
|
|
1662
|
-
if (typeof message.player === "number") this.move(client, message.player);
|
|
1663
|
-
return;
|
|
1664
|
-
case "ready":
|
|
1665
|
-
if (seat && typeof message.ready === "boolean") this.lobby.ready(seat, message.ready);
|
|
1666
|
-
return;
|
|
1667
|
-
case "resume":
|
|
1668
|
-
if (seat && this.phase === "game" && this.pausedBy !== void 0 && this.pausedBy !== "admin") this.pause(false);
|
|
1669
|
-
return;
|
|
1670
|
-
case "name":
|
|
1671
|
-
if (seat && typeof message.name === "string") this.lobby.rename(seat, message.name);
|
|
1672
|
-
return;
|
|
1673
|
-
case "start":
|
|
1674
|
-
if (seat && this.phase === "lobby") this.startGame();
|
|
1675
|
-
return;
|
|
1676
|
-
case "setting": {
|
|
1677
|
-
const { name, value } = message;
|
|
1678
|
-
if (seat && typeof name === "string" && typeof value === "string") this.lobby.set(name, value);
|
|
1679
|
-
return;
|
|
1680
|
-
}
|
|
1681
|
-
case "seatSetting": {
|
|
1682
|
-
const { player, name, value } = message;
|
|
1683
|
-
if (seat && typeof name === "string" && typeof value === "string" && typeof player === "number") this.lobby.setSeat(seat, player, name, value);
|
|
1684
|
-
return;
|
|
1685
|
-
}
|
|
1686
|
-
case "lobbyChat": {
|
|
1687
|
-
const text = typeof message.text === "string" ? message.text.trim().slice(0, LOBBY_LINE) : "";
|
|
1688
|
-
if (!client.joined || this.phase !== "lobby" || !text) return;
|
|
1689
|
-
return this.tellAll({
|
|
1690
|
-
type: "lobbyChat",
|
|
1691
|
-
...seat && { player: seat.player },
|
|
1692
|
-
name: seat?.name ?? "A watcher",
|
|
1693
|
-
text
|
|
1694
|
-
});
|
|
1695
|
-
}
|
|
1696
|
-
case "kick": {
|
|
1697
|
-
if (!seat || typeof message.player !== "number") return;
|
|
1698
|
-
const out = this.lobby.voteKick(seat, message.player);
|
|
1699
|
-
if (!out) return;
|
|
1700
|
-
const theirs = [...this.clients].find((each) => each.seat === out);
|
|
1701
|
-
if (theirs?.who) this.kickedOut.add(theirs.who);
|
|
1702
|
-
return this.kick(out.player, "let go by a vote of the lobby");
|
|
1703
|
-
}
|
|
1704
|
-
case "admin": {
|
|
1705
|
-
const { key, action } = message;
|
|
1706
|
-
if (typeof key !== "string" || action !== "pause" && action !== "resume" && action !== "stop") return;
|
|
1707
|
-
return this.admin(client, {
|
|
1708
|
-
type: "admin",
|
|
1709
|
-
key,
|
|
1710
|
-
action
|
|
1711
|
-
});
|
|
1712
|
-
}
|
|
1713
|
-
default: client.toGame(text);
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
/** Whether a command of this client's goes in: a bucket that fills at the rate allowed. Why not, when it does not */
|
|
1717
|
-
allow(client) {
|
|
1718
|
-
return client.commands.take() || engineMessage("tooManyCommands");
|
|
1719
|
-
}
|
|
1720
|
-
join(client, message) {
|
|
1721
|
-
if (client.seat || client.watching || client.joining) return;
|
|
1722
|
-
client.who = message.who;
|
|
1723
|
-
client.name = message.name?.trim().slice(0, 24) || void 0;
|
|
1724
|
-
if (message.who && this.kickedOut.has(message.who)) return this.enter(client, message, {
|
|
1725
|
-
to: "refuse",
|
|
1726
|
-
reason: "let go by a vote of the lobby"
|
|
1727
|
-
});
|
|
1728
|
-
const wrong = this.doorman.wrongWire(message);
|
|
1729
|
-
if (wrong) {
|
|
1730
|
-
this.log(`${message.name ?? "someone"} was refused: protocol ${message.protocol}, this server's is 13`, {
|
|
1731
|
-
name: message.name,
|
|
1732
|
-
protocol: message.protocol
|
|
1733
|
-
});
|
|
1734
|
-
return this.enter(client, message, {
|
|
1735
|
-
to: "refuse",
|
|
1736
|
-
reason: wrong
|
|
1737
|
-
});
|
|
1738
|
-
}
|
|
1739
|
-
if (this.phase === "ended" && !message.back && message.token && this.seats.some((each) => each.token === message.token)) this.backToLobby(message.token);
|
|
1740
|
-
const door = this.doorman.place(message, client.connection.address);
|
|
1741
|
-
if (!(door instanceof Promise)) return this.enter(client, message, door);
|
|
1742
|
-
client.joining = true;
|
|
1743
|
-
door.then((placed) => {
|
|
1744
|
-
client.joining = false;
|
|
1745
|
-
if (this.clients.has(client) && !client.seat && !client.watching) this.enter(client, message, placed);
|
|
1746
|
-
});
|
|
1747
|
-
}
|
|
1748
|
-
/** Takes who joined where the doorman said: a seat, watching, told every seat is taken, or away with why */
|
|
1749
|
-
enter(client, message, door) {
|
|
1750
|
-
if (door.to !== "refuse") {
|
|
1751
|
-
client.joined = true;
|
|
1752
|
-
clearTimeout(client.deadline);
|
|
1753
|
-
}
|
|
1754
|
-
switch (door.to) {
|
|
1755
|
-
case "seat":
|
|
1756
|
-
if ("id" in door) door.seat.id = door.id;
|
|
1757
|
-
if ("rated" in door) door.seat.rated = door.rated;
|
|
1758
|
-
return this.sit(client, door.seat, door.name, door.named);
|
|
1759
|
-
case "watch": return this.watch(client);
|
|
1760
|
-
case "full": return this.tell(client, { type: "full" });
|
|
1761
|
-
case "refuse":
|
|
1762
|
-
this.log(`${message.name ?? "someone"} was refused: ${door.reason}`, {
|
|
1763
|
-
name: message.name,
|
|
1764
|
-
reason: door.reason
|
|
1765
|
-
});
|
|
1766
|
-
this.tell(client, {
|
|
1767
|
-
type: "refused",
|
|
1768
|
-
reason: door.reason
|
|
1769
|
-
});
|
|
1770
|
-
return client.connection.close();
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
/** A seated player moves to another seat, free, in the lobby */
|
|
1774
|
-
move(client, player) {
|
|
1775
|
-
if (!client.seat) return;
|
|
1776
|
-
const to = this.lobby.move(client.seat, player);
|
|
1777
|
-
if (!to) return;
|
|
1778
|
-
client.seat = to;
|
|
1779
|
-
this.tell(client, {
|
|
1780
|
-
type: "seated",
|
|
1781
|
-
player: to.player,
|
|
1782
|
-
token: to.token
|
|
1783
|
-
});
|
|
1784
|
-
}
|
|
1785
|
-
/** Seats a client, and lets go whoever had the seat before: a player back from another page. A name the game gave goes in any phase */
|
|
1786
|
-
sit(client, seat, name, named = false) {
|
|
1787
|
-
const token = seat.take(client.connection, this.phase === "lobby" || named ? name : void 0);
|
|
1788
|
-
client.seat = seat;
|
|
1789
|
-
this.tell(client, {
|
|
1790
|
-
type: "seated",
|
|
1791
|
-
player: seat.player,
|
|
1792
|
-
token
|
|
1793
|
-
});
|
|
1794
|
-
this.log(`${seat.name} took seat ${seat.player}`, {
|
|
1795
|
-
name: seat.name,
|
|
1796
|
-
player: seat.player
|
|
1797
|
-
});
|
|
1798
|
-
this.hooks.onPlayerJoin?.(this.publicSeat(seat), this.world);
|
|
1799
|
-
if (this.phase === "lobby") return this.lobby.seated();
|
|
1800
|
-
this.match?.keep();
|
|
1801
|
-
this.seatInGame(client, seat);
|
|
1802
|
-
}
|
|
1803
|
-
/** The game's hooks */
|
|
1804
|
-
get hooks() {
|
|
1805
|
-
return this.options.mode.game.server ?? {};
|
|
1806
|
-
}
|
|
1807
|
-
/**
|
|
1808
|
-
* Who comes once the seats are taken watches, when the server lets them (`watch`): everything, the whole map without fog, every player's resources. From
|
|
1809
|
-
* the side of the world's neutral player, so that nobody's units are theirs; from the first seat's, in a world without one
|
|
1810
|
-
*/
|
|
1811
|
-
watch(client) {
|
|
1812
|
-
client.watching = true;
|
|
1813
|
-
const { world, host } = this;
|
|
1814
|
-
if (this.phase === "lobby" || !host || !world) {
|
|
1815
|
-
this.tell(client, {
|
|
1816
|
-
type: "watching",
|
|
1817
|
-
player: this.seats[0].player
|
|
1818
|
-
});
|
|
1819
|
-
return this.lobby.broadcast();
|
|
1820
|
-
}
|
|
1821
|
-
const side = [...world.players.values()].find((player) => player.neutral)?.id ?? this.seats[0].player;
|
|
1822
|
-
this.tell(client, {
|
|
1823
|
-
type: "watching",
|
|
1824
|
-
player: side
|
|
1825
|
-
});
|
|
1826
|
-
const delay = this.options.watchDelay ?? 0;
|
|
1827
|
-
host.connect(side, delay > 0 ? delayed(client.relay, delay) : client.relay, {
|
|
1828
|
-
readOnly: true,
|
|
1829
|
-
observer: true,
|
|
1830
|
-
name: client.name
|
|
1831
|
-
});
|
|
1832
|
-
if (!host.running && this.phase === "game") host.start();
|
|
1833
|
-
}
|
|
1834
|
-
seatInGame(client, seat) {
|
|
1835
|
-
const { host } = this;
|
|
1836
|
-
if (!host) return;
|
|
1837
|
-
if (this.options.debug) this.world?.debugPlayers.add(seat.player);
|
|
1838
|
-
host.connect(seat.player, client.relay, { allow: () => this.allow(client) });
|
|
1839
|
-
if (!host.running && this.phase === "game") host.start();
|
|
1840
|
-
}
|
|
1841
|
-
leave(client) {
|
|
1842
|
-
if (!this.clients.delete(client)) return;
|
|
1843
|
-
clearTimeout(client.deadline);
|
|
1844
|
-
client.fromGameClosed();
|
|
1845
|
-
const { seat } = client;
|
|
1846
|
-
if (!seat || seat.connection !== client.connection) return;
|
|
1847
|
-
seat.drop();
|
|
1848
|
-
this.log(`${seat.name} left seat ${seat.player}`, {
|
|
1849
|
-
name: seat.name,
|
|
1850
|
-
player: seat.player
|
|
1851
|
-
});
|
|
1852
|
-
this.hooks.onPlayerLeave?.(this.publicSeat(seat), this.world);
|
|
1853
|
-
const anyone = this.seats.some((each) => each.connected);
|
|
1854
|
-
if (this.phase === "lobby") this.lobby.left(seat);
|
|
1855
|
-
else if (this.phase === "ended" && !anyone) this.reset();
|
|
1856
|
-
else if (this.phase === "game" && !anyone) this.match?.abandon();
|
|
1857
|
-
}
|
|
1858
|
-
/**
|
|
1859
|
-
* A seat as everyone is told it (the lobby, the hooks)
|
|
1860
|
-
*
|
|
1861
|
-
* @internal
|
|
1862
|
-
*/
|
|
1863
|
-
publicSeat(seat) {
|
|
1864
|
-
return this.lobby.publicSeat(seat);
|
|
1865
|
-
}
|
|
1866
|
-
/** Makes the world, seats everyone at it, and runs it */
|
|
1867
|
-
async startGame() {
|
|
1868
|
-
if (this.phase !== "lobby" || this.draining) return;
|
|
1869
|
-
this.phase = "game";
|
|
1870
|
-
this.lobby.again = false;
|
|
1871
|
-
const server = this;
|
|
1872
|
-
const match = await Match.make({
|
|
1873
|
-
options: this.options,
|
|
1874
|
-
logger: this.logger,
|
|
1875
|
-
seats: this.seats,
|
|
1876
|
-
navigation: this.navigation,
|
|
1877
|
-
fill: () => this.lobby.fill(),
|
|
1878
|
-
get watching() {
|
|
1879
|
-
return server.watching;
|
|
1880
|
-
},
|
|
1881
|
-
publicSeat: (seat) => this.publicSeat(seat),
|
|
1882
|
-
tellAll: (message) => this.tellAll(message),
|
|
1883
|
-
kick: (player, reason) => this.kick(player, reason),
|
|
1884
|
-
end: () => void this.endGame()
|
|
1885
|
-
}, this.settings);
|
|
1886
|
-
this.match = match;
|
|
1887
|
-
this.lobby.broadcast();
|
|
1888
|
-
for (const client of this.clients) if (client.seat) this.seatInGame(client, client.seat);
|
|
1889
|
-
else if (client.watching) this.watch(client);
|
|
1890
|
-
match.run();
|
|
1891
|
-
}
|
|
1892
|
-
/** Lets a player go for good: told why, their connection closed, and their token and id no good any more */
|
|
1893
|
-
kick(player, reason) {
|
|
1894
|
-
const held = this.lobby.seat(player);
|
|
1895
|
-
if (!held) return;
|
|
1896
|
-
const client = [...this.clients].find((each) => each.seat === held);
|
|
1897
|
-
held.revoke();
|
|
1898
|
-
this.log(`${held.name} was let go from seat ${held.player}: ${reason}`, {
|
|
1899
|
-
name: held.name,
|
|
1900
|
-
player: held.player,
|
|
1901
|
-
reason
|
|
1902
|
-
});
|
|
1903
|
-
if (!client) return;
|
|
1904
|
-
this.tell(client, {
|
|
1905
|
-
type: "refused",
|
|
1906
|
-
reason
|
|
1907
|
-
});
|
|
1908
|
-
client.connection.close();
|
|
1909
|
-
}
|
|
1910
|
-
/** The game is over: the result told to everyone and kept. The game goes on being shown until everyone has left */
|
|
1911
|
-
async endGame() {
|
|
1912
|
-
const { match } = this;
|
|
1913
|
-
if (!match || this.phase !== "game") return void 0;
|
|
1914
|
-
this.phase = "ended";
|
|
1915
|
-
this.played++;
|
|
1916
|
-
const result = await match.end();
|
|
1917
|
-
if (!this.seats.some((seat) => seat.connected)) this.reset();
|
|
1918
|
-
this.drained?.();
|
|
1919
|
-
return result;
|
|
1920
|
-
}
|
|
1921
|
-
/**
|
|
1922
|
-
* Back to the lobby after a game, with who is still there in their seats: a player of it who joins again ("Again",
|
|
1923
|
-
* with their seat's token) takes theirs, the seats of those gone are free
|
|
1924
|
-
*/
|
|
1925
|
-
backToLobby(keepToken) {
|
|
1926
|
-
this.dropMatch({ closeTransports: false });
|
|
1927
|
-
for (const client of this.clients) {
|
|
1928
|
-
client.toGame = () => {};
|
|
1929
|
-
client.fromGameClosed = () => {};
|
|
1930
|
-
}
|
|
1931
|
-
this.phase = "lobby";
|
|
1932
|
-
this.lobby.backFromGame(keepToken);
|
|
1933
|
-
this.log("back to the lobby, for another game");
|
|
1934
|
-
this.lobby.broadcast();
|
|
1935
|
-
}
|
|
1936
|
-
/** Back to the lobby, for a new game: the old one gone, every seat free */
|
|
1937
|
-
reset() {
|
|
1938
|
-
this.dropMatch();
|
|
1939
|
-
this.phase = "lobby";
|
|
1940
|
-
this.lobby.reset();
|
|
1941
|
-
this.kickedOut.clear();
|
|
1942
|
-
this.log("back to the lobby");
|
|
1943
|
-
}
|
|
1944
|
-
admin(client, message) {
|
|
1945
|
-
if (!this.options.adminKey || !sameSecret(message.key, this.options.adminKey)) return this.tell(client, {
|
|
1946
|
-
type: "refused",
|
|
1947
|
-
reason: "not an admin"
|
|
1948
|
-
});
|
|
1949
|
-
if (message.action === "stop") {
|
|
1950
|
-
this.endGame();
|
|
1951
|
-
return;
|
|
1952
|
-
}
|
|
1953
|
-
this.pause(message.action === "pause");
|
|
1954
|
-
}
|
|
1955
|
-
/**
|
|
1956
|
-
* Pauses the game for everyone, or lets it go on. A player's pause (`by`, from the `pause` command) ends by itself
|
|
1957
|
-
* after its seconds, or when a seated player says `resume`; an admin's, when the admin says so
|
|
1958
|
-
*/
|
|
1959
|
-
pause(paused, by) {
|
|
1960
|
-
this.match?.pause(paused, by);
|
|
1961
|
-
}
|
|
1962
|
-
/**
|
|
1963
|
-
* Stops the server: every connection closed. With `drain`, it first takes no new games (its health says so, for a
|
|
1964
|
-
* load balancer to send nobody new) and waits for the running one to end, up to so many seconds: then it is ended
|
|
1965
|
-
* (no winner, the players told the server stopped) and the server closes
|
|
1966
|
-
*/
|
|
1967
|
-
async close(options = {}) {
|
|
1968
|
-
if (options.drain && !this.draining) {
|
|
1969
|
-
this.draining = true;
|
|
1970
|
-
this.log(`stopping: no new games, ${this.phase === "game" ? `the running one has ${options.drain} s to end` : "none running"}`);
|
|
1971
|
-
if (this.phase === "game") {
|
|
1972
|
-
let limit;
|
|
1973
|
-
const over = await new Promise((resolve) => {
|
|
1974
|
-
this.drained = () => resolve(true);
|
|
1975
|
-
limit = setTimeout(() => resolve(false), options.drain * 1e3);
|
|
1976
|
-
});
|
|
1977
|
-
clearTimeout(limit);
|
|
1978
|
-
this.drained = void 0;
|
|
1979
|
-
if (!over) {
|
|
1980
|
-
if (this.match) this.match.failed = "The server stopped";
|
|
1981
|
-
await this.endGame();
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
this.dropMatch();
|
|
1986
|
-
await this.source.close();
|
|
1987
|
-
}
|
|
1988
|
-
};
|
|
1989
|
-
//#endregion
|
|
1990
|
-
//#region src/server/mode.ts
|
|
1991
|
-
/**
|
|
1992
|
-
* A computer player that goes for everyone else's base: what a seat of the computer does when the server says nothing
|
|
1993
|
-
* else. As hard as its seat's `difficulty` says and of the `personality` it says, where the game offers them to choose
|
|
1994
|
-
* (`ServerHooks.seatSettings`, `seatSettingsOf`); a name the game does not have is left to the defaults
|
|
1995
|
-
*
|
|
1996
|
-
* @beta
|
|
1997
|
-
*/
|
|
1998
|
-
var againstTheRest = (seat, seats, settings = {}) => ({
|
|
1999
|
-
enemyBases: seats.filter((other) => other !== seat && other.base).map((other) => other.base),
|
|
2000
|
-
...settings.difficulty && { difficulty: settings.difficulty },
|
|
2001
|
-
...settings.personality && { personality: settings.personality }
|
|
2002
|
-
});
|
|
2003
|
-
/**
|
|
2004
|
-
* The race chosen for each seat in the lobby, by player: none where nobody chose one, or the game has no races
|
|
2005
|
-
*
|
|
2006
|
-
* @beta
|
|
2007
|
-
*/
|
|
2008
|
-
function racesOfSeats(seats) {
|
|
2009
|
-
if (!seats) return void 0;
|
|
2010
|
-
const races = {};
|
|
2011
|
-
for (const [player, settings] of Object.entries(seats)) if (settings.race !== void 0) races[Number(player)] = settings.race;
|
|
2012
|
-
return Object.keys(races).length ? races : void 0;
|
|
2013
|
-
}
|
|
2014
|
-
/**
|
|
2015
|
-
* The seats a map has room for: a seat for each of its players with a start, the creeps apart
|
|
2016
|
-
*
|
|
2017
|
-
* @beta
|
|
2018
|
-
*/
|
|
2019
|
-
var seatsOf = (map) => map.players.filter((player) => player.start && player.id !== 90).map((player) => ({
|
|
2020
|
-
player: player.id,
|
|
2021
|
-
name: player.name,
|
|
2022
|
-
base: player.start,
|
|
2023
|
-
team: player.team,
|
|
2024
|
-
preset: {
|
|
2025
|
-
team: String(player.team),
|
|
2026
|
-
color: String(nearestColor(player.color) + 1)
|
|
2027
|
-
},
|
|
2028
|
-
...player.race !== void 0 && { fixed: { race: player.race } }
|
|
2029
|
-
}));
|
|
2030
|
-
/**
|
|
2031
|
-
* The names of the players' colours (`PLAYER_COLORS`), in their order, as a lobby shows them
|
|
2032
|
-
*
|
|
2033
|
-
* @beta
|
|
2034
|
-
*/
|
|
2035
|
-
var PLAYER_COLOR_NAMES = [
|
|
2036
|
-
"Blue",
|
|
2037
|
-
"Red",
|
|
2038
|
-
"Teal",
|
|
2039
|
-
"Purple",
|
|
2040
|
-
"Yellow",
|
|
2041
|
-
"Orange",
|
|
2042
|
-
"Green",
|
|
2043
|
-
"Pink",
|
|
2044
|
-
"Grey",
|
|
2045
|
-
"Light blue",
|
|
2046
|
-
"Dark green",
|
|
2047
|
-
"Brown",
|
|
2048
|
-
"Maroon",
|
|
2049
|
-
"Navy",
|
|
2050
|
-
"Turquoise",
|
|
2051
|
-
"Violet",
|
|
2052
|
-
"Wheat",
|
|
2053
|
-
"Peach",
|
|
2054
|
-
"Mint",
|
|
2055
|
-
"Lavender",
|
|
2056
|
-
"Coal",
|
|
2057
|
-
"Snow",
|
|
2058
|
-
"Emerald",
|
|
2059
|
-
"Peanut"
|
|
2060
|
-
];
|
|
2061
|
-
/** The colour of the players' (`PLAYER_COLORS`) nearest one, by its index */
|
|
2062
|
-
function nearestColor(color) {
|
|
2063
|
-
let best = 0;
|
|
2064
|
-
let least = Infinity;
|
|
2065
|
-
PLAYER_COLORS.forEach((each, index) => {
|
|
2066
|
-
const [r, g, b] = [
|
|
2067
|
-
each[0] - color[0],
|
|
2068
|
-
each[1] - color[1],
|
|
2069
|
-
each[2] - color[2]
|
|
2070
|
-
];
|
|
2071
|
-
const apart = r * r + g * g + b * b;
|
|
2072
|
-
if (apart < least) {
|
|
2073
|
-
least = apart;
|
|
2074
|
-
best = index;
|
|
2075
|
-
}
|
|
2076
|
-
});
|
|
2077
|
-
return best;
|
|
2078
|
-
}
|
|
2079
|
-
/**
|
|
2080
|
-
* What a lobby offers to choose for each seat of a map's game beside the game's own: a team, of as many as there are
|
|
2081
|
-
* seats, and a colour of the players' (`PLAYER_COLORS`, by `PLAYER_COLOR_NAMES`), no two seats the same. Each seat has
|
|
2082
|
-
* the map's until chosen otherwise (`ModeSeat.preset`)
|
|
2083
|
-
*
|
|
2084
|
-
* @beta
|
|
2085
|
-
*/
|
|
2086
|
-
function seatLooksOf(seats) {
|
|
2087
|
-
const teams = Math.max(seats.length, ...seats.map((seat) => seat.team ?? 0));
|
|
2088
|
-
const numbers = (count) => Array.from({ length: count }, (_, i) => String(i + 1));
|
|
2089
|
-
return {
|
|
2090
|
-
team: {
|
|
2091
|
-
label: "Team",
|
|
2092
|
-
options: numbers(teams),
|
|
2093
|
-
labels: Object.fromEntries(numbers(teams).map((id) => [id, `Team ${id}`])),
|
|
2094
|
-
default: "1"
|
|
2095
|
-
},
|
|
2096
|
-
color: {
|
|
2097
|
-
label: "Colour",
|
|
2098
|
-
options: numbers(PLAYER_COLORS.length),
|
|
2099
|
-
labels: Object.fromEntries(PLAYER_COLOR_NAMES.map((name, i) => [String(i + 1), name])),
|
|
2100
|
-
default: "1",
|
|
2101
|
-
unique: true
|
|
2102
|
-
}
|
|
2103
|
-
};
|
|
2104
|
-
}
|
|
2105
|
-
/**
|
|
2106
|
-
* The teams and colours chosen in the lobby (`seatLooksOf`), by player, where they are not the map's: what
|
|
2107
|
-
* `MapWorldOptions.teams` and `colors` take
|
|
2108
|
-
*
|
|
2109
|
-
* @beta
|
|
2110
|
-
*/
|
|
2111
|
-
function looksOfSeats(seats, modeSeats) {
|
|
2112
|
-
const teams = {};
|
|
2113
|
-
const colors = {};
|
|
2114
|
-
for (const seat of modeSeats) {
|
|
2115
|
-
const chosen = seats?.[seat.player];
|
|
2116
|
-
if (!chosen) continue;
|
|
2117
|
-
const team = Number(chosen.team);
|
|
2118
|
-
if (chosen.team !== void 0 && chosen.team !== seat.preset?.team && Number.isInteger(team) && team > 0) teams[seat.player] = team;
|
|
2119
|
-
const color = PLAYER_COLORS[Number(chosen.color) - 1];
|
|
2120
|
-
if (chosen.color !== void 0 && chosen.color !== seat.preset?.color && color) colors[seat.player] = [...color];
|
|
2121
|
-
}
|
|
2122
|
-
return {
|
|
2123
|
-
...Object.keys(teams).length && { teams },
|
|
2124
|
-
...Object.keys(colors).length && { colors }
|
|
2125
|
-
};
|
|
2126
|
-
}
|
|
2127
|
-
/**
|
|
2128
|
-
* A game on a map of it: a seat for each player the map has a start for, and a world made from the map for each game
|
|
2129
|
-
*
|
|
2130
|
-
* @beta
|
|
2131
|
-
*/
|
|
2132
|
-
function mapMode(game, map, options = {}) {
|
|
2133
|
-
const { start } = options;
|
|
2134
|
-
const seats = options.seats ?? seatsOf(map);
|
|
2135
|
-
return {
|
|
2136
|
-
id: `map:${map.name}`,
|
|
2137
|
-
name: map.name,
|
|
2138
|
-
game,
|
|
2139
|
-
seats,
|
|
2140
|
-
seatSettings: {
|
|
2141
|
-
...seatSettingsOf(game, map.races),
|
|
2142
|
-
...seatLooksOf(seats)
|
|
2143
|
-
},
|
|
2144
|
-
async makeWorld(settings, context) {
|
|
2145
|
-
const made = structuredClone(map);
|
|
2146
|
-
const world = await createWorldFromMap({
|
|
2147
|
-
map: made,
|
|
2148
|
-
game,
|
|
2149
|
-
seed: randomSeed(),
|
|
2150
|
-
log: context?.log,
|
|
2151
|
-
races: racesOfSeats(context?.seats),
|
|
2152
|
-
...looksOfSeats(context?.seats, seats),
|
|
2153
|
-
...context?.navigation && { navigation: { builds: context.navigation } }
|
|
2154
|
-
});
|
|
2155
|
-
try {
|
|
2156
|
-
await start?.(world, {
|
|
2157
|
-
map: made,
|
|
2158
|
-
settings
|
|
2159
|
-
});
|
|
2160
|
-
} catch (error) {
|
|
2161
|
-
world.dispose();
|
|
2162
|
-
throw error;
|
|
2163
|
-
}
|
|
2164
|
-
return world;
|
|
2165
|
-
},
|
|
2166
|
-
...options.ai !== null && { ai: options.ai ?? againstTheRest },
|
|
2167
|
-
...options.brain !== void 0 && { brain: options.brain }
|
|
2168
|
-
};
|
|
2169
|
-
}
|
|
2170
|
-
/**
|
|
2171
|
-
* A game on a map made by a generator (`generateMap`): the map made once, with the seed given or a new one, and a
|
|
2172
|
-
* game of it as on any map (`mapMode`): a seat for each start, the world made from it for each game. For a room of a
|
|
2173
|
-
* random map (`RoomKind.room` as a function: a new map for each room made)
|
|
2174
|
-
*
|
|
2175
|
-
* @beta
|
|
2176
|
-
*/
|
|
2177
|
-
function generatedMode(game, generator, options) {
|
|
2178
|
-
const { start, ai, brain, seats, ...generating } = options;
|
|
2179
|
-
const seed = options.seed ?? 1 + Math.floor(Math.random() * 999999);
|
|
2180
|
-
const { map } = generateMap(generator, {
|
|
2181
|
-
...generating,
|
|
2182
|
-
seed,
|
|
2183
|
-
name: options.name ?? `A random map (${seed})`
|
|
2184
|
-
}, createRegistry(game));
|
|
2185
|
-
return {
|
|
2186
|
-
...mapMode(game, map, {
|
|
2187
|
-
start,
|
|
2188
|
-
ai,
|
|
2189
|
-
brain,
|
|
2190
|
-
seats
|
|
2191
|
-
}),
|
|
2192
|
-
id: `generated:${map.generated?.seed ?? seed}`,
|
|
2193
|
-
map
|
|
2194
|
-
};
|
|
2195
|
-
}
|
|
2196
|
-
//#endregion
|
|
2197
|
-
//#region src/server/start.ts
|
|
2198
|
-
/**
|
|
2199
|
-
* Starts a server of a game, and resolves once it listens: a lobby that seats who joins, then games of it, one after
|
|
2200
|
-
* the other, each on a world made anew.
|
|
2201
|
-
*
|
|
2202
|
-
* const server = await startServer({ game: rules, map, start: (world) => startMission(world) })
|
|
2203
|
-
* // server.port, server.close()
|
|
2204
|
-
*
|
|
2205
|
-
* The parts are there on their own: `GameServer.start` with a `GameMode`, `mapMode`, `SessionHost`
|
|
2206
|
-
*
|
|
2207
|
-
* @beta
|
|
2208
|
-
*/
|
|
2209
|
-
async function startServer(options) {
|
|
2210
|
-
const { game, port = 8787, map, makeWorld, seats, start, computer, mode: givenMode, name, ...server } = options;
|
|
2211
|
-
return GameServer.start({
|
|
2212
|
-
...server,
|
|
2213
|
-
port,
|
|
2214
|
-
mode: givenMode ?? modeOf(game, {
|
|
2215
|
-
map,
|
|
2216
|
-
makeWorld,
|
|
2217
|
-
seats,
|
|
2218
|
-
start,
|
|
2219
|
-
computer,
|
|
2220
|
-
name
|
|
2221
|
-
})
|
|
2222
|
-
});
|
|
2223
|
-
}
|
|
2224
|
-
/** The mode a server runs, from what it was started with */
|
|
2225
|
-
function modeOf(game, options) {
|
|
2226
|
-
const { map, makeWorld, start } = options;
|
|
2227
|
-
if (!map && !makeWorld) throw new Error("startServer: a server needs a map of the game, or a makeWorld, to make the world of each game from");
|
|
2228
|
-
const seats = options.seats ?? (map ? seatsOf(map) : void 0);
|
|
2229
|
-
if (!seats?.length) throw new Error(map ? `startServer: the map "${map.name}" has no player with a start. Give its players starts, or say the seats` : "startServer: say the seats players may take (seats), with a world made in code");
|
|
2230
|
-
const { computer } = options;
|
|
2231
|
-
const brain = computer && typeof computer === "object" ? computer : void 0;
|
|
2232
|
-
const ai = brain ? againstTheRest : computer;
|
|
2233
|
-
const base = map && !makeWorld ? mapMode(game, map, {
|
|
2234
|
-
seats,
|
|
2235
|
-
ai,
|
|
2236
|
-
brain
|
|
2237
|
-
}) : void 0;
|
|
2238
|
-
return {
|
|
2239
|
-
id: base?.id ?? game.id,
|
|
2240
|
-
name: options.name ?? base?.name ?? game.name,
|
|
2241
|
-
game,
|
|
2242
|
-
seats,
|
|
2243
|
-
async makeWorld(settings, context) {
|
|
2244
|
-
const world = base ? await base.makeWorld(settings, context) : await makeWorld(settings, context);
|
|
2245
|
-
try {
|
|
2246
|
-
await start?.(world, {
|
|
2247
|
-
map,
|
|
2248
|
-
settings
|
|
2249
|
-
});
|
|
2250
|
-
} catch (error) {
|
|
2251
|
-
world.dispose();
|
|
2252
|
-
throw error;
|
|
2253
|
-
}
|
|
2254
|
-
return world;
|
|
2255
|
-
},
|
|
2256
|
-
...ai !== null && { ai: ai ?? againstTheRest },
|
|
2257
|
-
...brain && { brain }
|
|
2258
|
-
};
|
|
2259
|
-
}
|
|
2260
|
-
//#endregion
|
|
2261
|
-
//#region src/server/rooms.ts
|
|
2262
|
-
/** What a room is reached at: the first part of the address a connection asked for */
|
|
2263
|
-
var roomOf = (url) => new URL(url, "ws://server").pathname.split("/")[1] ?? "";
|
|
2264
|
-
/** A name for a room: letters, digits, `-` and `_` */
|
|
2265
|
-
var isRoomName = (name) => /^[\w-]+$/.test(name);
|
|
2266
|
-
/** What an address that asked for games too often is told */
|
|
2267
|
-
var TOO_OFTEN = "too many games asked for: wait a minute";
|
|
2268
|
-
/** The most a body of a request to make a room may be: a kind and a few settings */
|
|
2269
|
-
var MAX_BODY = 4096;
|
|
2270
|
-
/**
|
|
2271
|
-
* Where a room's game server gets its connections (`ConnectionSource`): those the shared port hands it for the room's
|
|
2272
|
-
* name. Its server is told of a connection closing after the room has let go of it, and its `close` closes those in
|
|
2273
|
-
* the room, never the port
|
|
2274
|
-
*/
|
|
2275
|
-
var RoomConnections = class {
|
|
2276
|
-
port;
|
|
2277
|
-
handler = () => {};
|
|
2278
|
-
open = /* @__PURE__ */ new Set();
|
|
2279
|
-
constructor(port) {
|
|
2280
|
-
this.port = port;
|
|
2281
|
-
}
|
|
2282
|
-
onConnection(handler) {
|
|
2283
|
-
this.handler = handler;
|
|
2284
|
-
}
|
|
2285
|
-
/** A connection to the room, handed to its server */
|
|
2286
|
-
hand(connection) {
|
|
2287
|
-
let closed = () => {};
|
|
2288
|
-
const theirs = {
|
|
2289
|
-
get url() {
|
|
2290
|
-
return connection.url;
|
|
2291
|
-
},
|
|
2292
|
-
get address() {
|
|
2293
|
-
return connection.address;
|
|
2294
|
-
},
|
|
2295
|
-
get bytesSent() {
|
|
2296
|
-
return connection.bytesSent;
|
|
2297
|
-
},
|
|
2298
|
-
get compressed() {
|
|
2299
|
-
return connection.compressed;
|
|
2300
|
-
},
|
|
2301
|
-
send: (message) => connection.send(message),
|
|
2302
|
-
onMessage: (next) => connection.onMessage(next),
|
|
2303
|
-
onClose: (next) => void (closed = next),
|
|
2304
|
-
close: () => connection.close()
|
|
2305
|
-
};
|
|
2306
|
-
this.open.add(theirs);
|
|
2307
|
-
connection.onClose(() => {
|
|
2308
|
-
this.open.delete(theirs);
|
|
2309
|
-
closed();
|
|
2310
|
-
});
|
|
2311
|
-
this.handler(theirs);
|
|
2312
|
-
}
|
|
2313
|
-
async close() {
|
|
2314
|
-
for (const connection of [...this.open]) connection.close();
|
|
2315
|
-
}
|
|
2316
|
-
};
|
|
2317
|
-
/**
|
|
2318
|
-
* Many games in a process (S58): game servers that share a port, each a room reached at its name, as a server of its
|
|
2319
|
-
* own would be at its port. One process, one thread: each room's games step in turn, so the rooms of a process are as
|
|
2320
|
-
* many as its ticks have time for (`/metrics` says how long they take). A connection to a room there is not is told so
|
|
2321
|
-
* and closed. Rooms are added in code (`add`), or made by players of the kinds it offers (`kinds`, `POST /games`) and
|
|
2322
|
-
* closed once empty
|
|
2323
|
-
*
|
|
2324
|
-
* @beta
|
|
2325
|
-
*/
|
|
2326
|
-
var GameRooms = class GameRooms {
|
|
2327
|
-
socket;
|
|
2328
|
-
options;
|
|
2329
|
-
logger;
|
|
2330
|
-
servers = /* @__PURE__ */ new Map();
|
|
2331
|
-
/** Where each room's connections come from, by its name */
|
|
2332
|
-
doors = /* @__PURE__ */ new Map();
|
|
2333
|
-
/** The rooms players made, and since when each has been empty (nobody connected): what closes them */
|
|
2334
|
-
made = /* @__PURE__ */ new Map();
|
|
2335
|
-
/** When each address last asked for rooms (`POST /games`): a few a minute each */
|
|
2336
|
-
askedBy = /* @__PURE__ */ new Map();
|
|
2337
|
-
sweeper;
|
|
2338
|
-
nextMade = 1;
|
|
2339
|
-
/** Who waits to be matched, by kind, in the order they came */
|
|
2340
|
-
queues = /* @__PURE__ */ new Map();
|
|
2341
|
-
constructor(socket, options, logger) {
|
|
2342
|
-
this.socket = socket;
|
|
2343
|
-
this.options = options;
|
|
2344
|
-
this.logger = logger;
|
|
2345
|
-
}
|
|
2346
|
-
/** The rooms there are now, by name */
|
|
2347
|
-
get rooms() {
|
|
2348
|
-
return this.servers;
|
|
2349
|
-
}
|
|
2350
|
-
/** Starts the rooms, and resolves once they listen */
|
|
2351
|
-
static async start(options) {
|
|
2352
|
-
for (const name of Object.keys(options.rooms)) if (!isRoomName(name)) throw new Error(`GameRooms: "${name}" is no name for a room: letters, digits, - and _`);
|
|
2353
|
-
const logger = serverLogger(options.log);
|
|
2354
|
-
let rooms;
|
|
2355
|
-
const socket = await startSocketServer({
|
|
2356
|
-
port: options.port ?? 8787,
|
|
2357
|
-
compress: options.compress,
|
|
2358
|
-
idleTimeout: options.idleTimeout,
|
|
2359
|
-
maxBuffered: options.maxBuffered,
|
|
2360
|
-
maxConnections: options.maxConnections,
|
|
2361
|
-
log: logger,
|
|
2362
|
-
onRequest: (request, response) => {
|
|
2363
|
-
if (!rooms) return;
|
|
2364
|
-
if (rooms.answerMake(request, response)) return;
|
|
2365
|
-
if (answerOps(request, response, options.http, () => ({
|
|
2366
|
-
draining: rooms.draining,
|
|
2367
|
-
rooms: rooms.metrics()
|
|
2368
|
-
}), () => ({
|
|
2369
|
-
games: rooms.listing(),
|
|
2370
|
-
kinds: rooms.kinds()
|
|
2371
|
-
}), {
|
|
2372
|
-
ratings: options.ratings,
|
|
2373
|
-
modes: rooms.modes()
|
|
2374
|
-
})) return;
|
|
2375
|
-
response.writeHead(426, { "content-type": "text/plain" });
|
|
2376
|
-
response.end(`Game rooms: connect with a WebSocket to one of ${[...rooms.servers.keys()].join(", ")}\n`);
|
|
2377
|
-
}
|
|
2378
|
-
});
|
|
2379
|
-
rooms = new GameRooms(socket, options, logger);
|
|
2380
|
-
socket.onConnection((connection) => rooms.connect(connection));
|
|
2381
|
-
try {
|
|
2382
|
-
for (const [name, room] of Object.entries(options.rooms)) await rooms.add(name, room);
|
|
2383
|
-
} catch (error) {
|
|
2384
|
-
await rooms.close();
|
|
2385
|
-
throw error;
|
|
2386
|
-
}
|
|
2387
|
-
if (options.kinds && Object.keys(options.kinds).length) rooms.sweeper = setInterval(() => void rooms.sweep(), 1e3);
|
|
2388
|
-
return rooms;
|
|
2389
|
-
}
|
|
2390
|
-
/** A kind it offers, by id: its own, never what every object has (`constructor`), whatever a page sends */
|
|
2391
|
-
kind(id) {
|
|
2392
|
-
const { kinds } = this.options;
|
|
2393
|
-
return kinds && Object.hasOwn(kinds, id) ? kinds[id] : void 0;
|
|
2394
|
-
}
|
|
2395
|
-
/** The address of the queue, or none where there is no matchmaking */
|
|
2396
|
-
get queuePath() {
|
|
2397
|
-
const { matchmaking, kinds } = this.options;
|
|
2398
|
-
if (matchmaking === false || !kinds || !Object.keys(kinds).length) return void 0;
|
|
2399
|
-
return typeof matchmaking === "object" && matchmaking.path || "queue";
|
|
2400
|
-
}
|
|
2401
|
-
/** A connection to a room: handed to its game server, or told there is no such room. The queue's, to the queue */
|
|
2402
|
-
connect(connection) {
|
|
2403
|
-
const name = roomOf(connection.url);
|
|
2404
|
-
if (name === this.queuePath) return this.waitInQueue(connection);
|
|
2405
|
-
const door = this.doors.get(name);
|
|
2406
|
-
if (!door) {
|
|
2407
|
-
connection.send(JSON.stringify({
|
|
2408
|
-
type: "refused",
|
|
2409
|
-
reason: `there is no room "${name}"`
|
|
2410
|
-
}));
|
|
2411
|
-
return connection.close();
|
|
2412
|
-
}
|
|
2413
|
-
door.hand(connection);
|
|
2414
|
-
}
|
|
2415
|
-
/** Opens a room at a name, as `rooms` does: in code, while the rooms run */
|
|
2416
|
-
async add(name, room) {
|
|
2417
|
-
if (!isRoomName(name)) throw new Error(`GameRooms: "${name}" is no name for a room: letters, digits, - and _`);
|
|
2418
|
-
if (this.servers.has(name)) throw new Error(`GameRooms: there is a room "${name}" already`);
|
|
2419
|
-
const door = new RoomConnections(this.socket.port);
|
|
2420
|
-
this.doors.set(name, door);
|
|
2421
|
-
const server = await GameServer.attach(door, {
|
|
2422
|
-
...room,
|
|
2423
|
-
ratings: room.ratings ?? this.options.ratings,
|
|
2424
|
-
log: this.logger.child({ context: () => ({ fields: { room: name } }) })
|
|
2425
|
-
});
|
|
2426
|
-
this.servers.set(name, server);
|
|
2427
|
-
return server;
|
|
2428
|
-
}
|
|
2429
|
-
/** Closes a room and lets go of its name: those in it are let go */
|
|
2430
|
-
async remove(name) {
|
|
2431
|
-
const server = this.servers.get(name);
|
|
2432
|
-
if (!server) return;
|
|
2433
|
-
this.servers.delete(name);
|
|
2434
|
-
this.made.delete(name);
|
|
2435
|
-
await server.close();
|
|
2436
|
-
this.doors.delete(name);
|
|
2437
|
-
}
|
|
2438
|
-
/**
|
|
2439
|
-
* Makes a room of a kind it offers (`kinds`), with the settings asked for: what `POST /games` does. Its name is the
|
|
2440
|
-
* kind's and a number. Refused (with why) for a kind it does not offer, too many made, or while it stops
|
|
2441
|
-
*/
|
|
2442
|
-
async make(kind, settings = {}, seat) {
|
|
2443
|
-
const offered = this.kind(kind);
|
|
2444
|
-
if (!offered) return { refused: `there is no kind of game "${kind}"` };
|
|
2445
|
-
if (this.draining) return { refused: "the server is stopping: it takes no new games" };
|
|
2446
|
-
if (this.made.size >= (this.options.maxMade ?? 16)) return { refused: "there are as many games as there may be: join one" };
|
|
2447
|
-
let name = `${kind}-${this.nextMade++}`;
|
|
2448
|
-
while (this.servers.has(name)) name = `${kind}-${this.nextMade++}`;
|
|
2449
|
-
this.made.set(name, {
|
|
2450
|
-
kind,
|
|
2451
|
-
emptySince: Date.now(),
|
|
2452
|
-
joined: false,
|
|
2453
|
-
matched: !!seat
|
|
2454
|
-
});
|
|
2455
|
-
try {
|
|
2456
|
-
const room = typeof offered.room === "function" ? offered.room(settings) : offered.room;
|
|
2457
|
-
await this.add(name, {
|
|
2458
|
-
...room,
|
|
2459
|
-
settings: {
|
|
2460
|
-
...room.settings,
|
|
2461
|
-
...settings
|
|
2462
|
-
},
|
|
2463
|
-
...seat && { seat }
|
|
2464
|
-
});
|
|
2465
|
-
} catch (error) {
|
|
2466
|
-
this.made.delete(name);
|
|
2467
|
-
throw error;
|
|
2468
|
-
}
|
|
2469
|
-
this.logger.info("server", `made the room ${name} (${offered.name})`);
|
|
2470
|
-
return { room: name };
|
|
2471
|
-
}
|
|
2472
|
-
/** A room a kind makes, as its listing and matchmaking read it: the one it makes, or one it made once (a kind of a function) */
|
|
2473
|
-
samples = /* @__PURE__ */ new Map();
|
|
2474
|
-
sampleOf(kind) {
|
|
2475
|
-
const { room } = this.kind(kind);
|
|
2476
|
-
if (typeof room !== "function") return room;
|
|
2477
|
-
let sample = this.samples.get(kind);
|
|
2478
|
-
if (!sample) this.samples.set(kind, sample = room({}));
|
|
2479
|
-
return sample;
|
|
2480
|
-
}
|
|
2481
|
-
/** How many of a kind's seats are for players: those the computer does not play */
|
|
2482
|
-
seatsFor(kind) {
|
|
2483
|
-
const room = this.sampleOf(kind);
|
|
2484
|
-
return room.mode.seats.filter((seat) => !room.ai?.includes(seat.player)).length;
|
|
2485
|
-
}
|
|
2486
|
-
/** A connection to the queue: it says the kind it waits for, is told how many wait, and its ticket once matched */
|
|
2487
|
-
waitInQueue(connection) {
|
|
2488
|
-
let waitingFor;
|
|
2489
|
-
const leave = () => {
|
|
2490
|
-
if (!waitingFor) return;
|
|
2491
|
-
const queue = this.queues.get(waitingFor) ?? [];
|
|
2492
|
-
const at = queue.findIndex((each) => each.connection === connection);
|
|
2493
|
-
if (at >= 0) queue.splice(at, 1);
|
|
2494
|
-
this.tellQueue(waitingFor);
|
|
2495
|
-
waitingFor = void 0;
|
|
2496
|
-
};
|
|
2497
|
-
const within = this.options.joinWithin ?? 10;
|
|
2498
|
-
const deadline = Number.isFinite(within) ? setTimeout(() => !waitingFor && connection.close(), Math.max(0, within) * 1e3) : void 0;
|
|
2499
|
-
deadline?.unref?.();
|
|
2500
|
-
connection.onClose(() => {
|
|
2501
|
-
clearTimeout(deadline);
|
|
2502
|
-
leave();
|
|
2503
|
-
});
|
|
2504
|
-
connection.onMessage((text) => {
|
|
2505
|
-
let message;
|
|
2506
|
-
try {
|
|
2507
|
-
message = JSON.parse(text);
|
|
2508
|
-
} catch {
|
|
2509
|
-
return;
|
|
2510
|
-
}
|
|
2511
|
-
if (message.type === "leave") return leave();
|
|
2512
|
-
if (message.type !== "queue") return;
|
|
2513
|
-
const kind = String(message.kind ?? "");
|
|
2514
|
-
if (!this.kind(kind)) return connection.send(JSON.stringify({
|
|
2515
|
-
type: "refused",
|
|
2516
|
-
reason: `there is no kind of game "${kind}"`
|
|
2517
|
-
}));
|
|
2518
|
-
if (this.askedTooOften(connection.address)) return connection.send(JSON.stringify({
|
|
2519
|
-
type: "refused",
|
|
2520
|
-
reason: TOO_OFTEN
|
|
2521
|
-
}));
|
|
2522
|
-
clearTimeout(deadline);
|
|
2523
|
-
leave();
|
|
2524
|
-
waitingFor = kind;
|
|
2525
|
-
const queue = this.queues.get(kind) ?? [];
|
|
2526
|
-
this.queues.set(kind, queue);
|
|
2527
|
-
queue.push({
|
|
2528
|
-
connection,
|
|
2529
|
-
...typeof message.name === "string" && { name: message.name.slice(0, 24) }
|
|
2530
|
-
});
|
|
2531
|
-
this.match(kind).catch((error) => this.logger.error("server", "matchmaking failed", { error: String(error) }));
|
|
2532
|
-
});
|
|
2533
|
-
}
|
|
2534
|
-
/**
|
|
2535
|
-
* Whether an address has asked for games too often to ask again now: a few a minute each, made (`POST /games`) or
|
|
2536
|
-
* waited for in the queue, so that nobody fills every place there is by asking over and over. Counted when it has not
|
|
2537
|
-
*/
|
|
2538
|
-
askedTooOften(address) {
|
|
2539
|
-
const now = Date.now();
|
|
2540
|
-
const recent = (this.askedBy.get(address) ?? []).filter((at) => now - at < 6e4);
|
|
2541
|
-
if (recent.length >= 3) return true;
|
|
2542
|
-
recent.push(now);
|
|
2543
|
-
this.askedBy.set(address, recent);
|
|
2544
|
-
if (this.askedBy.size > 1024) {
|
|
2545
|
-
for (const [each, times] of this.askedBy) if (times.every((at) => now - at >= 6e4)) this.askedBy.delete(each);
|
|
2546
|
-
}
|
|
2547
|
-
return false;
|
|
2548
|
-
}
|
|
2549
|
-
/** Tells everyone waiting for a kind how many wait, of how many it takes */
|
|
2550
|
-
tellQueue(kind) {
|
|
2551
|
-
const queue = this.queues.get(kind) ?? [];
|
|
2552
|
-
const needed = this.seatsFor(kind);
|
|
2553
|
-
for (const each of queue) each.connection.send(JSON.stringify({
|
|
2554
|
-
type: "queued",
|
|
2555
|
-
kind,
|
|
2556
|
-
waiting: queue.length,
|
|
2557
|
-
needed
|
|
2558
|
-
}));
|
|
2559
|
-
}
|
|
2560
|
-
/** As many wait for a kind as its seats: a room of it for them, which lets in only them, and a ticket each */
|
|
2561
|
-
async match(kind) {
|
|
2562
|
-
const queue = this.queues.get(kind);
|
|
2563
|
-
const needed = this.seatsFor(kind);
|
|
2564
|
-
if (queue.length < needed) return this.tellQueue(kind);
|
|
2565
|
-
const players = queue.splice(0, needed);
|
|
2566
|
-
const room = this.sampleOf(kind);
|
|
2567
|
-
const seats = room.mode.seats.filter((seat) => !room.ai?.includes(seat.player));
|
|
2568
|
-
const tickets = new Map(players.map((player, i) => [crypto.randomUUID(), {
|
|
2569
|
-
player: seats[i].player,
|
|
2570
|
-
name: player.name
|
|
2571
|
-
}]));
|
|
2572
|
-
const made = await this.make(kind, {}, (request) => {
|
|
2573
|
-
const ticket = request.ticket ? tickets.get(request.ticket) : void 0;
|
|
2574
|
-
return ticket ? {
|
|
2575
|
-
player: ticket.player,
|
|
2576
|
-
name: ticket.name,
|
|
2577
|
-
id: request.ticket
|
|
2578
|
-
} : { refuse: "this game is for the players matched to it" };
|
|
2579
|
-
}).catch((error) => ({ refused: error instanceof Error ? error.message : String(error) }));
|
|
2580
|
-
const given = [...tickets.keys()];
|
|
2581
|
-
players.forEach(({ connection }, i) => connection.send(JSON.stringify("room" in made ? {
|
|
2582
|
-
type: "matched",
|
|
2583
|
-
kind,
|
|
2584
|
-
room: made.room,
|
|
2585
|
-
ticket: given[i]
|
|
2586
|
-
} : {
|
|
2587
|
-
type: "refused",
|
|
2588
|
-
reason: made.refused
|
|
2589
|
-
})));
|
|
2590
|
-
this.tellQueue(kind);
|
|
2591
|
-
}
|
|
2592
|
-
/** Closes the rooms players made that have been empty long enough */
|
|
2593
|
-
async sweep() {
|
|
2594
|
-
const now = Date.now();
|
|
2595
|
-
const emptyFor = (this.options.emptyFor ?? 60) * 1e3;
|
|
2596
|
-
for (const [name, made] of [...this.made]) {
|
|
2597
|
-
const server = this.servers.get(name);
|
|
2598
|
-
if (!server) continue;
|
|
2599
|
-
if (server.connections > 0) {
|
|
2600
|
-
made.emptySince = now;
|
|
2601
|
-
made.joined = true;
|
|
2602
|
-
continue;
|
|
2603
|
-
}
|
|
2604
|
-
const wait = !made.joined ? Math.min(emptyFor, 2e4) : server.phase === "game" ? Math.max(emptyFor, (server.options.abandonAfter ?? 60) * 1e3) : emptyFor;
|
|
2605
|
-
if (now - made.emptySince >= wait) await this.remove(name);
|
|
2606
|
-
}
|
|
2607
|
-
}
|
|
2608
|
-
/** `POST /games`: a room made of a kind it offers, `{ kind, settings }`, answered with its name. And what a page asks first */
|
|
2609
|
-
answerMake(request, response) {
|
|
2610
|
-
const list = this.options.http?.games ?? "/games";
|
|
2611
|
-
const { pathname } = new URL(request.url ?? "/", "http://server");
|
|
2612
|
-
if (!list || pathname !== list || request.method !== "POST" && request.method !== "OPTIONS") return false;
|
|
2613
|
-
const cors = {
|
|
2614
|
-
"access-control-allow-origin": "*",
|
|
2615
|
-
"access-control-allow-methods": "GET, POST",
|
|
2616
|
-
"access-control-allow-headers": "content-type"
|
|
2617
|
-
};
|
|
2618
|
-
if (request.method === "OPTIONS") {
|
|
2619
|
-
response.writeHead(204, cors);
|
|
2620
|
-
response.end();
|
|
2621
|
-
return true;
|
|
2622
|
-
}
|
|
2623
|
-
const answer = (status, body) => {
|
|
2624
|
-
response.writeHead(status, {
|
|
2625
|
-
"content-type": "application/json",
|
|
2626
|
-
"cache-control": "no-store",
|
|
2627
|
-
...cors
|
|
2628
|
-
});
|
|
2629
|
-
response.end(JSON.stringify(body));
|
|
2630
|
-
};
|
|
2631
|
-
if (this.askedTooOften(request.socket.remoteAddress ?? "")) {
|
|
2632
|
-
answer(429, { refused: TOO_OFTEN });
|
|
2633
|
-
return true;
|
|
2634
|
-
}
|
|
2635
|
-
let body = "";
|
|
2636
|
-
request.on("data", (chunk) => {
|
|
2637
|
-
body += chunk.toString();
|
|
2638
|
-
if (body.length > MAX_BODY) request.destroy();
|
|
2639
|
-
});
|
|
2640
|
-
request.on("end", () => {
|
|
2641
|
-
let asked;
|
|
2642
|
-
try {
|
|
2643
|
-
asked = JSON.parse(body || "{}");
|
|
2644
|
-
} catch {
|
|
2645
|
-
return answer(400, { refused: "what was asked is not JSON" });
|
|
2646
|
-
}
|
|
2647
|
-
const settings = asked.settings && typeof asked.settings === "object" ? Object.fromEntries(Object.entries(asked.settings).filter(([, value]) => typeof value === "string")) : {};
|
|
2648
|
-
this.make(String(asked.kind ?? ""), settings).then((made) => answer("room" in made ? 201 : 409, made), (error) => answer(500, { refused: error instanceof Error ? error.message : String(error) }));
|
|
2649
|
-
});
|
|
2650
|
-
return true;
|
|
2651
|
-
}
|
|
2652
|
-
/** The port they share */
|
|
2653
|
-
get port() {
|
|
2654
|
-
return this.socket.port;
|
|
2655
|
-
}
|
|
2656
|
-
/** Whether they take no new games: stopping */
|
|
2657
|
-
get draining() {
|
|
2658
|
-
return [...this.servers.values()].some((room) => room.draining);
|
|
2659
|
-
}
|
|
2660
|
-
/** The modes the rooms play, each once, in their order: what the ratings are kept by (`/ratings`) */
|
|
2661
|
-
modes() {
|
|
2662
|
-
return [...new Set([...this.servers.values()].map((room) => room.options.mode.id))];
|
|
2663
|
-
}
|
|
2664
|
-
/** Every room's game, as a list of games says it (`GET /games`) */
|
|
2665
|
-
listing() {
|
|
2666
|
-
return [...this.servers].map(([name, room]) => room.listing(name));
|
|
2667
|
-
}
|
|
2668
|
-
/** The kinds of game players may make a room of (`kinds`), by id, with their names and the settings they offer */
|
|
2669
|
-
kinds() {
|
|
2670
|
-
const queue = this.queuePath;
|
|
2671
|
-
return Object.entries(this.options.kinds ?? {}).map(([id, kind]) => ({
|
|
2672
|
-
id,
|
|
2673
|
-
name: kind.name,
|
|
2674
|
-
settings: { ...this.sampleOf(id).mode.game.server?.settings },
|
|
2675
|
-
...queue && {
|
|
2676
|
-
queue,
|
|
2677
|
-
waiting: this.queues.get(id)?.length ?? 0,
|
|
2678
|
-
needed: this.seatsFor(id)
|
|
2679
|
-
}
|
|
2680
|
-
}));
|
|
2681
|
-
}
|
|
2682
|
-
/** The numbers of each room, by its name */
|
|
2683
|
-
metrics() {
|
|
2684
|
-
return new Map([...this.servers].map(([name, room]) => [name, room.metrics()]));
|
|
2685
|
-
}
|
|
2686
|
-
/** Stops every room (see `GameServer.close`: with `drain`, each waits for its game, all at once), then the port */
|
|
2687
|
-
async close(options = {}) {
|
|
2688
|
-
if (this.sweeper) clearInterval(this.sweeper);
|
|
2689
|
-
for (const queue of this.queues.values()) for (const { connection } of queue) connection.close();
|
|
2690
|
-
this.queues.clear();
|
|
2691
|
-
await Promise.all([...this.servers.values()].map((room) => room.close(options)));
|
|
2692
|
-
await this.socket.close();
|
|
2693
|
-
}
|
|
2694
|
-
};
|
|
2695
|
-
//#endregion
|
|
2696
|
-
//#region src/server/relay.ts
|
|
2697
|
-
/** @beta */
|
|
2698
|
-
var WatchRelay = class WatchRelay {
|
|
2699
|
-
socket;
|
|
2700
|
-
shares = /* @__PURE__ */ new Map();
|
|
2701
|
-
watchers = /* @__PURE__ */ new Map();
|
|
2702
|
-
next = 1;
|
|
2703
|
-
/** What the relay says: see `RelayOptions.log` */
|
|
2704
|
-
logger;
|
|
2705
|
-
constructor(socket, options) {
|
|
2706
|
-
this.socket = socket;
|
|
2707
|
-
this.logger = serverLogger(options.log);
|
|
2708
|
-
socket.onConnection((connection) => this.accept(connection));
|
|
2709
|
-
}
|
|
2710
|
-
/** Starts a relay, and resolves once it listens */
|
|
2711
|
-
static async start(options) {
|
|
2712
|
-
const socket = await startSocketServer({
|
|
2713
|
-
port: options.port,
|
|
2714
|
-
compress: options.compress,
|
|
2715
|
-
maxMessage: 16777216
|
|
2716
|
-
});
|
|
2717
|
-
const relay = new WatchRelay(socket, options);
|
|
2718
|
-
relay.logger.info("server", `a relay for games watched, on port ${socket.port}`, { port: socket.port });
|
|
2719
|
-
return relay;
|
|
2720
|
-
}
|
|
2721
|
-
get port() {
|
|
2722
|
-
return this.socket.port;
|
|
2723
|
-
}
|
|
2724
|
-
/** How many watch what is shared under a key */
|
|
2725
|
-
watching(key) {
|
|
2726
|
-
return [...this.watchers.values()].filter((watcher) => watcher.key === key).length;
|
|
2727
|
-
}
|
|
2728
|
-
accept(connection) {
|
|
2729
|
-
let role;
|
|
2730
|
-
connection.onMessage((text) => {
|
|
2731
|
-
if (role && "watcher" in role) return this.fromWatcher(role.watcher, text);
|
|
2732
|
-
const message = readMessage(text);
|
|
2733
|
-
if (!message) return;
|
|
2734
|
-
if (!role && message.type === "share") {
|
|
2735
|
-
if (typeof message.key === "string" && typeof message.name === "string") role = this.share(connection, {
|
|
2736
|
-
type: "share",
|
|
2737
|
-
key: message.key,
|
|
2738
|
-
name: message.name
|
|
2739
|
-
});
|
|
2740
|
-
} else if (!role && message.type === "watch") {
|
|
2741
|
-
if (typeof message.key === "string") role = { watcher: this.watch(connection, message.key) };
|
|
2742
|
-
} else if (role && "share" in role && message.type === "send" && typeof message.to === "number" && typeof message.text === "string") {
|
|
2743
|
-
const watcher = this.watchers.get(message.to);
|
|
2744
|
-
if (watcher?.key === role.share.key) watcher.connection.send(message.text);
|
|
2745
|
-
}
|
|
2746
|
-
});
|
|
2747
|
-
connection.onClose(() => {
|
|
2748
|
-
if (role && "share" in role) this.unshare(role.share);
|
|
2749
|
-
else if (role && "watcher" in role) this.leave(role.watcher);
|
|
2750
|
-
});
|
|
2751
|
-
}
|
|
2752
|
-
tell(connection, notice) {
|
|
2753
|
-
connection.send(JSON.stringify(notice));
|
|
2754
|
-
}
|
|
2755
|
-
share(host, message) {
|
|
2756
|
-
if (this.shares.has(message.key)) {
|
|
2757
|
-
this.tell(host, {
|
|
2758
|
-
type: "taken",
|
|
2759
|
-
key: message.key
|
|
2760
|
-
});
|
|
2761
|
-
return;
|
|
2762
|
-
}
|
|
2763
|
-
const share = {
|
|
2764
|
-
key: message.key,
|
|
2765
|
-
name: message.name.slice(0, 80),
|
|
2766
|
-
host
|
|
2767
|
-
};
|
|
2768
|
-
this.shares.set(share.key, share);
|
|
2769
|
-
this.tell(host, {
|
|
2770
|
-
type: "shared",
|
|
2771
|
-
key: share.key
|
|
2772
|
-
});
|
|
2773
|
-
this.logger.info("server", `${share.name} is shared under ${share.key}`, { name: share.name });
|
|
2774
|
-
for (const watcher of this.watchers.values()) if (watcher.key === share.key) this.tell(host, {
|
|
2775
|
-
type: "watcher",
|
|
2776
|
-
id: watcher.id
|
|
2777
|
-
});
|
|
2778
|
-
return { share };
|
|
2779
|
-
}
|
|
2780
|
-
unshare(share) {
|
|
2781
|
-
if (this.shares.get(share.key) !== share) return;
|
|
2782
|
-
this.shares.delete(share.key);
|
|
2783
|
-
this.logger.info("server", `${share.name} is no longer shared`, { name: share.name });
|
|
2784
|
-
for (const watcher of this.watchers.values()) if (watcher.key === share.key) this.tell(watcher.connection, {
|
|
2785
|
-
type: "ended",
|
|
2786
|
-
key: share.key
|
|
2787
|
-
});
|
|
2788
|
-
}
|
|
2789
|
-
watch(connection, key) {
|
|
2790
|
-
const watcher = {
|
|
2791
|
-
id: this.next++,
|
|
2792
|
-
connection,
|
|
2793
|
-
key
|
|
2794
|
-
};
|
|
2795
|
-
this.watchers.set(watcher.id, watcher);
|
|
2796
|
-
const share = this.shares.get(key);
|
|
2797
|
-
if (share) this.tell(share.host, {
|
|
2798
|
-
type: "watcher",
|
|
2799
|
-
id: watcher.id
|
|
2800
|
-
});
|
|
2801
|
-
else this.tell(connection, {
|
|
2802
|
-
type: "waiting",
|
|
2803
|
-
key
|
|
2804
|
-
});
|
|
2805
|
-
return watcher;
|
|
2806
|
-
}
|
|
2807
|
-
fromWatcher(watcher, text) {
|
|
2808
|
-
const share = this.shares.get(watcher.key);
|
|
2809
|
-
if (share) this.tell(share.host, {
|
|
2810
|
-
type: "from",
|
|
2811
|
-
watcher: watcher.id,
|
|
2812
|
-
text
|
|
2813
|
-
});
|
|
2814
|
-
}
|
|
2815
|
-
leave(watcher) {
|
|
2816
|
-
if (!this.watchers.delete(watcher.id)) return;
|
|
2817
|
-
const share = this.shares.get(watcher.key);
|
|
2818
|
-
if (share) this.tell(share.host, {
|
|
2819
|
-
type: "gone",
|
|
2820
|
-
watcher: watcher.id
|
|
2821
|
-
});
|
|
2822
|
-
}
|
|
2823
|
-
/** Stops the relay: every connection closed */
|
|
2824
|
-
async close() {
|
|
2825
|
-
await this.socket.close();
|
|
2826
|
-
}
|
|
2827
|
-
};
|
|
2828
|
-
var pack_schema_default = {
|
|
2829
|
-
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2830
|
-
$ref: "#/definitions/PackFile",
|
|
2831
|
-
definitions: /* @__PURE__ */ JSON.parse("{\"PackFile\":{\"type\":\"object\",\"properties\":{\"$schema\":{\"type\":\"string\"},\"id\":{\"type\":\"string\",\"description\":\"Letters, digits, dots and dashes: what the game composed of it is called by (`melee+<id>`)\"},\"version\":{\"type\":\"string\",\"description\":\"`major.minor.patch`\"},\"name\":{\"type\":\"string\"},\"requires\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"The game and the packs or mods it is made for, as a mod's (`ModDef.requires`)\"},\"units\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/UnitTypeDef\"}},\"abilities\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/AbilityDef\"}},\"buffs\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/BuffDef\"}},\"items\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/ItemDef\"}},\"upgrades\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/UpgradeDef\"}},\"races\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/RaceDef\"}},\"rules\":{\"$ref\":\"#/definitions/RulesInit\",\"description\":\"The rules it changes, as data (`GameBuilder.rules`)\"},\"data\":{\"$ref\":\"#/definitions/GameData\",\"description\":\"Values over the definitions, as a `data.json` has them (`GameData`): the editor's Data panel writes here for a pack\"},\"looks\":{\"type\":\"object\",\"additionalProperties\":{},\"description\":\"What its units look like, as a `looks.json` has them: the client's to lay over the game's view\"}},\"required\":[\"id\",\"version\"],\"additionalProperties\":false,\"description\":\"A pack of content as its file has it (`packs/<id>/pack.json`): plain data, checked against the pack's schema (`@v12-engine/engine/schemas/pack.schema.json`) as it is read. Its definitions are added to the game's, or put in the place of those of the same id; then its `data` is laid over the whole as a `data.json` is (`withData`), and its `rules` over the game's\"},\"UnitTypeDef\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"basedOn\":{\"type\":\"string\",\"description\":\"The definition this one is a copy of, made from the data (`copyOf` in a `data.json`, the editor's Copy as new): what it is drawn, heard and pictured as where the game gives it no look of its own\"},\"kind\":{\"$ref\":\"#/definitions/EntityKind\"},\"radius\":{\"type\":\"number\",\"description\":\"Collision radius\"},\"footprint\":{\"type\":\"object\",\"properties\":{\"w\":{\"type\":\"number\"},\"d\":{\"type\":\"number\"}},\"required\":[\"w\",\"d\"],\"additionalProperties\":false,\"description\":\"Size in grid cells. Required for buildings and resources\"},\"stats\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"health\":{\"type\":\"number\"},\"attackDamage\":{\"type\":\"number\",\"description\":\"What a basic attack deals, before the armor of the target\"},\"attackSpeed\":{\"type\":\"number\",\"description\":\"Attacks per second\"},\"attackRange\":{\"type\":\"number\",\"description\":\"Measured between the edges of attacker and target\"},\"armor\":{\"type\":\"number\",\"description\":\"Against physical damage: 100 of it halve what arrives\"},\"magicResist\":{\"type\":\"number\",\"description\":\"The same against magic damage\"},\"healthRegen\":{\"type\":\"number\",\"description\":\"Per second\"},\"mana\":{\"type\":\"number\"},\"manaRegen\":{\"type\":\"number\",\"description\":\"Per second\"},\"moveSpeed\":{\"type\":\"number\",\"description\":\"Units per second. Who has none cannot move\"},\"sight\":{\"type\":\"number\",\"description\":\"How far it sees. At night the unit type says how far that is\"}},\"required\":[\"health\"],\"description\":\"What it starts with. What is left out is 0, a critical strike is worth 175%\"},\"initialMana\":{\"type\":\"number\",\"description\":\"Mana it comes into the world with. All of it when left out\"},\"sight\":{\"type\":\"number\",\"description\":\"Vision radius by day\"},\"sightNight\":{\"type\":\"number\",\"description\":\"Vision radius at night. A part of the one by day when left out\"},\"flying\":{\"type\":\"boolean\",\"description\":\"Flies: over cliffs, trees and buildings, out of reach of whatever cannot hit the air\"},\"naval\":{\"type\":\"boolean\",\"description\":\"Goes on deep water and nowhere else: a boat. Sent onto land, it goes to the water nearest where it was sent. On the ground, as far as what can hit it goes\"},\"waygate\":{\"type\":\"boolean\",\"description\":\"A waygate, in a game with the portals pack (`withPortals`): a unit told to it walks in and is at once where it leads (its `waygate` field, a point of the map; `setWaygate` in code)\"},\"destructible\":{\"type\":\"boolean\",\"description\":\"Rocks, a barricade, a gate to break down, in a game with the destructibles pack (`withDestructibles`): it stands in the way as any building does until it is brought down, and any unit whose attack reaches it attacks it when told to, never of its own accord\"},\"transport\":{\"type\":\"object\",\"properties\":{\"capacity\":{\"type\":\"number\"},\"armed\":{\"type\":\"number\",\"description\":\"Attack damage each one inside adds; or in code, all of what they add, by who is inside (none for nobody)\"}},\"required\":[\"capacity\"],\"additionalProperties\":false,\"description\":\"Carries others: a boat, or a building they go into (a garrison). `takes` says who may get in; `armed`, for a building with an attack, is the attack damage each one inside adds, and it attacks only while someone is inside, as an orc burrow does. What a building holds comes out when it falls; what a boat holds goes down with it\"},\"turnRate\":{\"type\":\"number\",\"description\":\"Radians per second\"},\"attacks\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/AttackDef\"},\"description\":\"How it attacks, as the classic RTS two weapons: against each target the first that reaches it (its `targets`), or the one the game picks (`GameRules.combat.pickAttack`). It swings one at a time: a hit's cooldown is that attack's, whichever swings next. None for what does not attack\"},\"hero\":{\"$ref\":\"#/definitions/HeroDef\"},\"level\":{\"type\":\"number\",\"description\":\"How dangerous it is, for creeps. Shown to the player, and added up per camp\"},\"bounty\":{\"type\":\"array\",\"minItems\":2,\"items\":[{\"type\":\"number\",\"title\":\"min\"},{\"type\":\"number\",\"title\":\"max\"}],\"maxItems\":2,\"description\":\"Gold for whoever kills it, rolled between the two\"},\"corpse\":{\"type\":\"boolean\",\"description\":\"Whether it leaves a corpse when it dies, for spells to use (see `raiseDead`). Units do, unless this says no: machines, spirits. Decided for each death in code: false for none, true or seconds for one (how long it lies)\"},\"spellImmune\":{\"type\":\"boolean\",\"description\":\"Immune to spells: abilities do not touch it, of friends or enemies, unless they say they get through (`piercesImmunity`). Decided in code whenever it is asked: while it stands on holy ground. The player's screen only knows the plain `true` of it, to grey out a target; the simulation goes by the code\"},\"xp\":{\"type\":\"number\",\"description\":\"Experience heroes get for killing it. Follows from its cost when left out\"},\"requires\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Building types the player has to have before this can be built or trained\"},\"race\":{\"type\":\"string\",\"description\":\"The race it is of (`GameModule.races`): only its players may build or train it. Everyone's when left out: creeps, what is shared. What a player may have is the game's to change (the `available` hook)\"},\"provides\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Building types this one counts as, for requirements. A keep is a town hall too\"},\"upgradesTo\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"array\",\"items\":{\"type\":\"string\"}}],\"description\":\"Type this building can be upgraded into, where it stands; or the types, for one that may become one of several (a keep that turns into a castle or a citadel, `upgradesOf`). The first is what an upgrade is into where none is named\"},\"inventory\":{\"type\":\"number\",\"description\":\"Slots for items. Heroes have six\"},\"sells\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Items this building sells\"},\"hires\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Units it hires out to whoever has one of theirs there, by type: a tavern's heroes, a dock's boats. Their cost, and a hero only once (see `hire`). Neutral, it hires to everyone\"},\"drops\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"item\":{\"type\":\"string\"},\"chance\":{\"type\":\"number\"}},\"required\":[\"item\",\"chance\"],\"additionalProperties\":false},\"description\":\"What it may leave behind when it dies, each with its chance from 0 to 1. Or code: the items it leaves, decided as it dies (one random level-3 item; more for a stronger killer)\"},\"revives\":{\"type\":\"boolean\",\"description\":\"Brings fallen heroes back\"},\"abilities\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Ids of the abilities the unit has\"},\"cost\":{\"$ref\":\"#/definitions/Cost\"},\"supply\":{\"type\":\"number\",\"description\":\"Supply (food) it takes up for as long as it lives, and while it is being trained\"},\"supplies\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"number\"},\"description\":\"What it takes of the game's other pools of supply, by name (`GameRules.supply.pools`): `{ command: 1 }`\"},\"buildTime\":{\"type\":\"number\",\"description\":\"Seconds to build or train\"},\"supplyProvided\":{\"type\":\"number\"},\"suppliesProvided\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"number\"},\"description\":\"What it provides of the game's other pools of supply, by name (`GameRules.supply.pools`), once it stands finished\"},\"harvester\":{\"type\":\"object\",\"properties\":{\"capacity\":{\"type\":\"number\"},\"gatherTime\":{\"type\":\"number\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/ResourceKind\"},\"description\":\"The kinds of resource it gathers: all of them when left out. The undead's acolytes gold and nothing else, ghouls lumber. The `canHarvest` hook has the last word (`canHarvest`)\"}},\"required\":[\"capacity\",\"gatherTime\"],\"additionalProperties\":false},\"builds\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Building types this unit can construct\"},\"trains\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Unit types this building can train\"},\"researches\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Upgrades that can be researched here\"},\"dropOff\":{\"anyOf\":[{\"type\":\"boolean\"},{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/ResourceKind\"}}],\"description\":\"Accepts harvested resources: all of them, or only the ones listed\"},\"builtOn\":{\"$ref\":\"#/definitions/ResourceKind\",\"description\":\"Goes up on a resource of this kind, over it, of the same footprint: the undead's cursed gold mine. The resource is its owner's alone from then on (`canHarvest`); one that takes what is gathered there (`dropOff`) has its owner's workers gather at it from around it, handing in where they stand, as many at once as `gatherers` says. It goes down with the resource, which stands again where it is destroyed\"},\"gatherers\":{\"type\":\"number\",\"description\":\"Workers that gather at once at it, when it stands on a resource (`builtOn`): 5 when left out\"},\"buildsItself\":{\"type\":\"boolean\",\"description\":\"Goes up on its own once it is put down, as the undead summon theirs: the builder is free at once, and nobody helps (`GameRules.production.buildRate` of one builder, all the way). The same as `construction: 'self'`\"},\"construction\":{\"type\":\"string\",\"enum\":[\"builder\",\"self\",\"instant\"],\"description\":\"How it goes up once it is put down: with builders at it (`builder`, the more the faster, as `buildRate` says), on its own with the builder free at once (`self`, the undead's), or whole at once (`instant`: a wall piece, a ward). `builder` when left out (`self` with `buildsItself`)\"},\"resource\":{\"type\":\"object\",\"properties\":{\"kind\":{\"$ref\":\"#/definitions/ResourceKind\"},\"amount\":{\"type\":\"number\"},\"slots\":{\"type\":\"number\",\"description\":\"Workers that fit inside at once, for resources that are entered\"},\"entered\":{\"type\":\"boolean\",\"description\":\"Workers go inside to gather, like into a gold mine, instead of working on it from next to it\"},\"gatherTime\":{\"type\":\"number\",\"description\":\"Seconds a load takes, when that is not what the worker usually needs\"}},\"required\":[\"kind\",\"amount\",\"slots\"],\"additionalProperties\":false},\"hotkey\":{\"type\":\"string\",\"description\":\"Key used in the command card when building or training this type\"},\"threat\":{\"type\":\"number\",\"description\":\"Higher values are picked first when acquiring targets\"},\"invulnerable\":{\"type\":\"boolean\"},\"undying\":{\"type\":\"boolean\",\"description\":\"Takes damage like anything else but never dies of it: it is left with a little health, like something to practise on\"},\"recovers\":{\"type\":\"number\",\"description\":\"Seconds after it was last hurt that it is whole again, all at once\"},\"ignored\":{\"type\":\"boolean\",\"description\":\"Nobody picks a fight with it on their own: it is only attacked when someone is told to\"},\"fellable\":{\"type\":\"boolean\",\"description\":\"Destroyed by attacks that fell, like a tree by a catapult, whatever else it withstands\"},\"wall\":{\"type\":\"object\",\"properties\":{\"family\":{\"type\":\"string\"},\"gate\":{\"type\":\"boolean\"}},\"required\":[\"family\"],\"additionalProperties\":false,\"description\":\"A piece of wall (`withWalls`): it joins with the pieces of its family beside it, and its shape (`single`, `end`, `straight`, `corner`, `tee`, `cross`) is its look. A gate (`gate`) joins as a piece does, turned along its line, and its owner opens and shuts it for everyone (`wallGateCommand`): its look `open` or `shut`. Pieces join by their footprints, so a gate wider than a cell sits in a line of pieces of one: one of a cell lets nobody through, the ground about a wall being kept off by the radius of what walks (a gatehouse of 3 by 3 lets through what is 1 wide)\"},\"lifetime\":{\"type\":\"number\",\"description\":\"Seconds it lasts before it is gone by itself, like a ward\"},\"invisible\":{\"type\":\"object\",\"properties\":{\"after\":{\"type\":\"number\"}},\"additionalProperties\":false,\"description\":\"Enemies cannot see it, `after` seconds after it came into the world, unless something reveals it to them\"},\"hits\":{\"type\":\"number\",\"description\":\"Goes down after this many attacks and spells, whatever they deal: its health counts hits\"},\"ai\":{\"$ref\":\"#/definitions/AiHints\",\"description\":\"What the computer player takes it for (`AiHints`): its roles (`worker`, `hall`, `soldier`...), the game's words about it, what it answers. Declared where what the kit infers from the rest of the definition is wrong or cannot be known; inferred otherwise (`inferRoles` in `@v12-engine/engine/ai`)\"},\"armourType\":{\"type\":\"string\",\"description\":\"Its armour type, the column of the game's damage table (`GameRules.combat.damageTable`): `light`, `heavy`, `fortified`…\"}},\"required\":[\"id\",\"name\",\"kind\",\"radius\",\"stats\",\"sight\"],\"additionalProperties\":false},\"EntityKind\":{\"type\":\"string\",\"enum\":[\"unit\",\"building\",\"resource\"]},\"AttackDef\":{\"type\":\"object\",\"properties\":{\"stats\":{\"$ref\":\"#/definitions/AttackStats\",\"description\":\"The stats its numbers are, by id: a stat of the game's (`GameModule.stats`) for a weapon whose damage, speed or range is not the unit's first's. What changes those stats (research, items, buffs) changes this attack alone\"},\"damageType\":{\"$ref\":\"#/definitions/DamageType\",\"description\":\"Physical when left out\"},\"damagePoint\":{\"type\":\"number\",\"description\":\"Seconds from the start of the swing until damage lands (or the projectile leaves), at the attack speed the unit starts with\"},\"acquisitionRange\":{\"type\":\"number\",\"description\":\"Enemies closer than this are attacked automatically\"},\"minRange\":{\"type\":\"number\",\"description\":\"How near is too near, edge to edge: what is closer than this the attack does not hit (a siege weapon's dead zone). Another of the unit's attacks takes over there where one reaches (`GameRules.combat.pickAttack`); with none, a unit told to attack steps back out of it first, and one left to itself does not pick what is that near. None when left out\"},\"onTheMove\":{\"type\":\"boolean\",\"description\":\"Fires as the unit walks (a horse archer, a tank's turret): told to move, it hits what it passes within the attack's range without stopping or turning to it, at the attack's speed. Every other order fights as any attack does (it stops to). Not when left out\"},\"projectileSpeed\":{\"type\":\"number\",\"description\":\"Set for ranged attacks\"},\"targets\":{\"type\":\"array\",\"items\":{\"anyOf\":[{\"$ref\":\"#/definitions/EntityKind\"},{\"$ref\":\"#/definitions/Layer\"}]},\"description\":\"What this attack can hit: kinds of things, and where they are. Everything on the ground when left out. What flies is only hit by attacks that name `air`, and an attack that names `air` alone does not hit the ground\"},\"splash\":{\"type\":\"number\",\"description\":\"Radius around the point of impact in which everyone is hit, friends too: in full in the inner half, by half further out. Shots of such an attack land where they were aimed, they do not follow their target\"},\"ground\":{\"type\":\"boolean\",\"description\":\"Can be told to attack a spot on the ground\"},\"fells\":{\"type\":\"boolean\",\"description\":\"Brings down what is `fellable` in the area it hits\"},\"how\":{\"type\":\"string\",\"description\":\"How what this attack kills dies, for the screen (`'shot'`, `'crushed'`, `'burnt'`): the death's `how`, which the view shows its way (`UnitLook.death`). A word, or one decided for each blow. The `death` hook has the last say\"},\"attackType\":{\"type\":\"string\",\"description\":\"The attack's type against the target's armour type, by the game's damage table (`GameRules.combat.damageTable`): `pierce`, `siege`… Left out, the attack is multiplied by nothing\"}},\"required\":[\"damagePoint\",\"acquisitionRange\"],\"additionalProperties\":false,\"description\":\"One of the ways a unit attacks (`UnitTypeDef.attacks`): what it hits, how its blow lands. How hard, how fast and how far are stats: the unit's `attackDamage`, `attackSpeed` and `attackRange`, or stats of the game's own that the attack names (`stats`: a second weapon's)\"},\"AttackStats\":{\"type\":\"object\",\"properties\":{\"damage\":{\"$ref\":\"#/definitions/StatKey\",\"description\":\"What a hit deals, before the armour of the target. `attackDamage` when left out\"},\"speed\":{\"$ref\":\"#/definitions/StatKey\",\"description\":\"Hits a second. `attackSpeed` when left out\"},\"range\":{\"$ref\":\"#/definitions/StatKey\",\"description\":\"How far it reaches, between the edges. `attackRange` when left out\"}},\"additionalProperties\":false,\"description\":\"The stats an attack reads how hard, how fast and how far it hits from (`AttackDef.stats`): the unit's `attackDamage`, `attackSpeed` and `attackRange` for what is left out\"},\"StatKey\":{\"type\":\"string\",\"description\":\"A stat: one of the engine's (`CoreStatKey`), or one a game defined. Checked when the game is built\"},\"DamageType\":{\"type\":\"string\",\"enum\":[\"physical\",\"magic\",\"true\"],\"description\":\"Physical damage is held off by armor, magic damage by magic resist, true damage by nothing\"},\"Layer\":{\"type\":\"string\",\"enum\":[\"ground\",\"air\"],\"description\":\"Where something is: on the ground, or above it\"},\"HeroDef\":{\"type\":\"object\",\"properties\":{\"growth\":{\"$ref\":\"#/definitions/StatChange\",\"description\":\"What every level after the first adds\"},\"learns\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Abilities the hero can put points into\"},\"spells\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Abilities it has from the start and does not learn: the two spells every hero brings along. No mana, and nothing shortens their cooldown\"},\"maxLevel\":{\"type\":\"number\",\"description\":\"The level it stops at. 10 when left out\"},\"meter\":{\"type\":\"string\",\"description\":\"A buff whose stacks are shown next to its health and mana, as so many points out of as many as it can have\"},\"primary\":{\"type\":\"string\",\"enum\":[\"strength\",\"agility\",\"intelligence\"],\"description\":\"Its primary attribute, in a game with the attributes pack (`withAttributes`): each point of it adds to its attack as well. `AttributesOptions.primaryOf` says it in code\"}},\"required\":[\"growth\",\"learns\"],\"additionalProperties\":false,\"description\":\"What makes a unit a hero: it gains levels, grows with them and learns abilities\"},\"StatChange\":{\"type\":\"object\",\"properties\":{\"stats\":{\"type\":\"object\",\"properties\":{\"attackDamage\":{\"type\":\"number\",\"description\":\"What a basic attack deals, before the armor of the target\"},\"attackSpeed\":{\"type\":\"number\",\"description\":\"Attacks per second\"},\"attackRange\":{\"type\":\"number\",\"description\":\"Measured between the edges of attacker and target\"},\"health\":{\"type\":\"number\"},\"armor\":{\"type\":\"number\",\"description\":\"Against physical damage: 100 of it halve what arrives\"},\"magicResist\":{\"type\":\"number\",\"description\":\"The same against magic damage\"},\"healthRegen\":{\"type\":\"number\",\"description\":\"Per second\"},\"mana\":{\"type\":\"number\"},\"manaRegen\":{\"type\":\"number\",\"description\":\"Per second\"},\"moveSpeed\":{\"type\":\"number\",\"description\":\"Units per second. Who has none cannot move\"},\"sight\":{\"type\":\"number\",\"description\":\"How far it sees. At night the unit type says how far that is\"}},\"additionalProperties\":false},\"percent\":{\"type\":\"object\",\"properties\":{\"attackDamage\":{\"type\":\"number\",\"description\":\"What a basic attack deals, before the armor of the target\"},\"attackSpeed\":{\"type\":\"number\",\"description\":\"Attacks per second\"},\"attackRange\":{\"type\":\"number\",\"description\":\"Measured between the edges of attacker and target\"},\"health\":{\"type\":\"number\"},\"armor\":{\"type\":\"number\",\"description\":\"Against physical damage: 100 of it halve what arrives\"},\"magicResist\":{\"type\":\"number\",\"description\":\"The same against magic damage\"},\"healthRegen\":{\"type\":\"number\",\"description\":\"Per second\"},\"mana\":{\"type\":\"number\"},\"manaRegen\":{\"type\":\"number\",\"description\":\"Per second\"},\"moveSpeed\":{\"type\":\"number\",\"description\":\"Units per second. Who has none cannot move\"},\"sight\":{\"type\":\"number\",\"description\":\"How far it sees. At night the unit type says how far that is\"}},\"additionalProperties\":false}},\"additionalProperties\":false,\"description\":\"Something that changes stats: `stats` is added, `percent` makes them bigger or smaller by a part of what they are with everything added. Parts from different places add up: two times +10% are +20%.\"},\"Cost\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"number\"},\"propertyNames\":{\"description\":\"The id of a resource, as its game module defines it (`ResourceDef`)\"},\"description\":\"What something costs: an amount of each resource it takes, by resource id. What is left out costs nothing\"},\"ResourceKind\":{\"type\":\"string\",\"description\":\"The id of a resource, as its game module defines it (`ResourceDef`)\"},\"AiHints\":{\"type\":\"object\",\"properties\":{\"role\":{\"anyOf\":[{\"$ref\":\"#/definitions/AiRole\"},{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/AiRole\"}}],\"description\":\"What it is for (`AiRole`), one or several: in place of what the kit would infer\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Words of the game's own about it (`'ranged'`, `'cavalry'`), for a brain's queries (`UnitSet.withTag`). Beside the inferred ones\"},\"counters\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/CounterHint\"},\"description\":\"What it is good against: tags, roles or type ids (`['air', 'siege']`), each with a weight where a word is not enough (`{ target: 'air', weight: 1 }`). Read by the matchups (`Strength.matchup`, `odds`, `counterFor`)\"},\"strength\":{\"type\":\"number\",\"description\":\"How strong it is at full health, in place of the kit's formula (`defaultStrength`) and of any formula the brain was given: for a type whose numbers mislead (a unit that is all spells). Scaled by the health a unit has left\"},\"priority\":{\"type\":\"number\",\"description\":\"How much it is worth hitting first, in a fight: 1 when left out. Read by the micro of C2\"},\"placement\":{\"$ref\":\"#/definitions/PlacementName\",\"description\":\"Where the build plan puts it (`PlacementName`): `atEdge` for a tower, `behind` for supply, `cluster` beside its own kind. Over the brain's table; the role's default when left out\"}},\"additionalProperties\":false,\"description\":\"What a game declares of a unit type for the computer player (`UnitTypeDef.ai`), where what the kit infers from the definition is wrong or cannot be known: its roles, words of the game's own about it, what it answers, how much it is worth hitting. A declared role is in place of the inferred ones, not beside them\"},\"AiRole\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"string\",\"enum\":[\"worker\",\"gatherer\",\"topper\",\"hall\",\"supply\",\"barracks\",\"altar\",\"workshop\",\"tower\",\"shop\",\"soldier\",\"hero\",\"caster\",\"healer\",\"siege\",\"air\",\"scout\",\"detector\"]}],\"description\":\"What a unit type is for, to a computer player: the engine's roles, or a word of the game's own. A type can have several (a ghoul is a `gatherer` and a `soldier`)\\n\\n- `worker`: gathers and builds; `gatherer`: gathers (a worker is one too); `topper`: what is put up on a deposit of the money to gather it there (`builtOn`)\\n- `hall`: takes deposits and trains workers; `supply`: provides supply and trains nothing; `barracks`: trains soldiers; `altar`: trains or revives heroes; `workshop`: researches; `tower`: a building that attacks; `shop`: sells or hires\\n- `soldier`: a unit that fights and is no hero; `hero`; `caster`: fights with mana; `healer`: has an ability that heals; `siege`: attacks that splash, fell or are aimed at the ground; `air`: flies; `scout`: the fastest of the soldiers; `detector`: finds the invisible\"},\"CounterHint\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"object\",\"properties\":{\"target\":{\"type\":\"string\"},\"weight\":{\"type\":\"number\"}},\"required\":[\"target\"],\"additionalProperties\":false}],\"description\":\"What a game declares a type good against (`UnitTypeDef.ai.counters`): a tag (`'air'`), a role (`'siege'`) or a type id, as a word, or with how much it counts (`weight`, 0.5 when left out: a unit whose whole opponent it counters is worth half again)\"},\"PlacementName\":{\"type\":\"string\",\"description\":\"The kit's placement strategies, by name: `nearHome` (rings around the place, keeping the way to the mine clear), `byResource` (on the nearest deposit it is built on: a mine), `atEdge` (at the edge of the base towards the enemy, for a tower), `behind` (away from the enemy, for supply), `cluster` (beside another of its type, barracks by barracks; `cluster:<type>` beside another type), `onLayer:<layer>` (where the whole footprint is on a layer of cells the game paints: `onLayer:blight` for a race that builds on blight)\"},\"AbilityDef\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"how\":{\"type\":\"string\",\"description\":\"How what its damage kills dies, for the screen (`'burnt'`, `'frozen'`, `'blasted'`): the death's `how`, which the view shows its way. A word, or one decided for each blow: by its level, its caster, what it hit. An effect's own (`EffectDef.how`) goes over it; the `death` hook has the last say\"},\"basedOn\":{\"type\":\"string\",\"description\":\"The definition this one is a copy of, made from the data (`copyOf` in a `data.json`, the editor's Copy as new): what it is drawn, heard and pictured as where the game gives it no look of its own\"},\"description\":{\"type\":\"string\",\"description\":\"What it does, in words: a template with values in braces, worked out for the caster as it is now (\\\"Deals {damage} magic damage\\\" shows \\\"Deals 80 (50 + 60% ability power) magic damage\\\"). The values are its own (`values`), or found in its effects: `damage`, `heal`, `shield`, `amount`, `duration`, `radius`, `range`, `cooldown`, `charges`, `refill`, `mana`, `count`, `distance` (see `abilityValues`)\"},\"values\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/AbilityValueDef\"},\"description\":\"Values of its own to put in its description, by name: an amount and what of the caster scales it, or worked out in code\"},\"hotkey\":{\"type\":\"string\"},\"target\":{\"$ref\":\"#/definitions/AbilityTarget\",\"description\":\"none: happens at once. unit, point: needs a target, and the caster walks into range of it. direction: goes off from where the caster stands, towards where it was aimed. passive: always on, cannot be cast\"},\"targets\":{\"type\":\"object\",\"properties\":{\"allies\":{\"type\":\"boolean\"},\"enemies\":{\"type\":\"boolean\"},\"self\":{\"type\":\"boolean\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"}},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"}},\"heroes\":{\"type\":\"boolean\"}},\"additionalProperties\":false,\"description\":\"What may be targeted, for abilities cast on a unit\"},\"manaCost\":{\"$ref\":\"#/definitions/Scaled\"},\"healthCost\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Health it costs, as a part of all the caster can have. It cannot be cast with less than that left\"},\"cooldown\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Seconds before it can be used again\"},\"charges\":{\"type\":\"object\",\"properties\":{\"count\":{\"$ref\":\"#/definitions/Scaled\"},\"refill\":{\"$ref\":\"#/definitions/Scaled\"}},\"required\":[\"count\",\"refill\"],\"additionalProperties\":false,\"description\":\"Uses it holds, for one used so many times before it waits: `count` of them at the most, one more back every `refill` seconds while it holds fewer (shortened as the cooldown is). Each use spends one; with none left it is not ready. Its `cooldown`, where it has one, is then the time between two uses. Full when it is learned\"},\"range\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"How far away the target may be\"},\"attackRange\":{\"type\":\"boolean\",\"description\":\"The target may be as far away as the caster's attacks reach, and `range` farther\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"What kind of thing it is, for those who can keep out of its way: a buff that `avoids` one of these lets nothing of the ability happen to who carries it. `ground` is for what comes out of the ground or along it\"},\"maxLevel\":{\"type\":\"number\",\"description\":\"Levels a hero can put into it. 3 when left out\"},\"requiresLevel\":{\"type\":\"number\",\"description\":\"Hero level needed for the first level of the ability. Ultimates ask for 6\"},\"levelStep\":{\"type\":\"number\",\"description\":\"Hero levels between two levels of the ability. 2 when left out\"},\"castTime\":{\"type\":\"number\",\"description\":\"Seconds between starting to cast and the effect. The caster stands still for as long and cannot be called away: what it is told to do in the meantime, it does right after. None when left out\"},\"piercesImmunity\":{\"type\":\"boolean\",\"description\":\"Gets through a unit's immunity to spells (see `UnitTypeDef.spellImmune`), as an ultimate may. Or for the targets its function says\"},\"aim\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"shape\":{\"type\":\"string\",\"const\":\"circle\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\"}},\"required\":[\"shape\",\"radius\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"shape\":{\"type\":\"string\",\"const\":\"line\"},\"length\":{\"$ref\":\"#/definitions/Scaled\"},\"width\":{\"$ref\":\"#/definitions/Scaled\"}},\"required\":[\"shape\",\"length\",\"width\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"shape\":{\"type\":\"string\",\"const\":\"cone\"},\"length\":{\"$ref\":\"#/definitions/Scaled\"},\"angle\":{\"type\":\"number\"}},\"required\":[\"shape\",\"length\",\"angle\"],\"additionalProperties\":false}],\"description\":\"The ground it covers, when that is not what its effects say: see `aimOf`\"},\"autocast\":{\"type\":\"boolean\",\"description\":\"Units use it on their own when they have nothing else to do. The player can switch this off\"},\"charge\":{\"type\":\"string\",\"description\":\"A buff of the caster that charges the ability when it has as many stacks as it can: the ability shows it, its `charged` effects do their other thing, and casting it takes the buff away\"},\"effects\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"}},\"passive\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"What an ability that is cast does all the time besides, once it is learned: buffs, auras, `onHit`\"},\"channel\":{\"type\":\"object\",\"properties\":{\"duration\":{\"type\":\"number\"},\"every\":{\"type\":\"number\"},\"effects\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"}},\"finished\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"What it does once kept up to the end, and only then: a recall's teleport home\"},\"breaksOnDamage\":{\"type\":\"boolean\",\"description\":\"Broken off when the caster is hurt, as a recall is: any damage, from anyone\"}},\"required\":[\"duration\"],\"additionalProperties\":false,\"description\":\"Kept up once it went off: the caster stands, for `duration` seconds, doing it every `every` seconds (effects, or code), until it is done or interrupted (another order, a stun), when `interrupted` runs. An ice storm\"}},\"required\":[\"id\",\"name\",\"target\",\"effects\"],\"additionalProperties\":false},\"AbilityValueDef\":{\"type\":\"object\",\"properties\":{\"amount\":{\"$ref\":\"#/definitions/Scaled\"},\"scaling\":{\"$ref\":\"#/definitions/Scaling\"},\"unit\":{\"type\":\"string\"},\"percent\":{\"type\":\"boolean\"}},\"additionalProperties\":false,\"description\":\"How a game gives a value of an ability: an amount and what scales it, or worked out in code\"},\"Scaled\":{\"anyOf\":[{\"type\":\"number\"},{\"type\":\"array\",\"items\":{\"type\":\"number\"}}],\"description\":\"A value that can grow with the level of an ability: one number, or one per level\"},\"Scaling\":{\"type\":\"object\",\"properties\":{\"level\":{\"$ref\":\"#/definitions/Scaled\"},\"bonusAttackDamage\":{\"$ref\":\"#/definitions/Scaled\"}},\"additionalProperties\":{\"$ref\":\"#/definitions/Scaled\"},\"description\":\"What the stats of the caster add to an amount. `level` is for every level of a hero past the first, `bonusAttackDamage` for the attack damage it has on top of what its type and its levels give it\"},\"AbilityTarget\":{\"type\":\"string\",\"enum\":[\"none\",\"unit\",\"point\",\"direction\",\"passive\"],\"description\":\"How an ability is aimed\"},\"EffectDef\":{\"anyOf\":[{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"amount\":{\"$ref\":\"#/definitions/Scaled\"},\"scaling\":{\"$ref\":\"#/definitions/Scaling\",\"description\":\"What the stats of the caster add to the amount: `{ abilityPower: 0.6 }` adds 60% of its ability power. `level` is for every level of the hero past the first\"},\"damageType\":{\"$ref\":\"#/definitions/DamageType\",\"description\":\"For damage. Magic when left out\"},\"how\":{\"type\":\"string\",\"description\":\"For damage: how what it kills dies, for the screen, over the ability's own `how`\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"damage\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"amount\":{\"$ref\":\"#/definitions/Scaled\"},\"scaling\":{\"$ref\":\"#/definitions/Scaling\",\"description\":\"What the stats of the caster add to the amount: `{ abilityPower: 0.6 }` adds 60% of its ability power. `level` is for every level of the hero past the first\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"heal\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"amount\":{\"$ref\":\"#/definitions/Scaled\"},\"scaling\":{\"$ref\":\"#/definitions/Scaling\",\"description\":\"What the stats of the caster add to the amount: `{ abilityPower: 0.6 }` adds 60% of its ability power. `level` is for every level of the hero past the first\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"mana\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"duration\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Seconds the buff lasts, when that is different from its usual duration\"},\"stack\":{\"type\":\"boolean\",\"description\":\"For `buff`: adds one to how many of it the target carries instead of renewing it, up to what the buff allows\"},\"buff\":{\"type\":\"string\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"buff\"}},\"required\":[\"buff\",\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"speed\":{\"type\":\"number\",\"description\":\"For missiles: how fast they travel, in units per second, and how far\"},\"distance\":{\"$ref\":\"#/definitions/Scaled\"},\"count\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"For missiles: how many are sent, spread over an angle in radians. What one of them hit the others pass by\"},\"spread\":{\"type\":\"number\"},\"homing\":{\"type\":\"boolean\",\"description\":\"For missiles: flies after the unit the ability was cast on, and only hits that one\"},\"toPoint\":{\"type\":\"boolean\",\"description\":\"For missiles: flies to the spot the ability was aimed at and stops there, `distance` away at the most\"},\"delay\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"For missiles: seconds they take shape where they were cast before they set off. The caster is free to go\"},\"sight\":{\"type\":\"number\",\"description\":\"For missiles: how far the team of the caster sees around it\"},\"blocked\":{\"type\":\"boolean\",\"description\":\"For missiles: buildings and trees in its way stop it, without anything happening to them\"},\"pierce\":{\"type\":\"boolean\",\"description\":\"For missiles: goes on after it hit something. False for the ones that are used up by the first thing in their way\"},\"arc\":{\"type\":\"object\",\"properties\":{\"height\":{\"type\":\"number\"},\"time\":{\"type\":\"number\"}},\"required\":[\"height\"],\"additionalProperties\":false,\"description\":\"For missiles: thrown, not shot. It goes `height` up in the middle of its way and comes down at its end, `time` seconds after it set off however far that is (`speed` otherwise). It flies over everything, and what is within its `radius` where it comes down is what it hits\"},\"bounce\":{\"type\":\"object\",\"properties\":{\"count\":{\"$ref\":\"#/definitions/Scaled\"},\"range\":{\"type\":\"number\"},\"falloff\":{\"type\":\"number\"}},\"required\":[\"count\",\"range\"],\"additionalProperties\":false,\"description\":\"For missiles: where it would be done (it reached its target, was used up by what it hit, came down, or came to the end of its way) it springs on to the nearest thing it may hit within `range` that it has not hit yet, and flies after that one, `count` times. Every spring takes `falloff` of the amounts off what it does (0.25: a quarter less each time). Its `onEnd` is where it is done for good\"},\"effects\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"What happens to what is hit. For missiles: everything they pass through. For `onHit`: the target of an attack. For `dash`: what is around where it ends. For `after`: what is there when the time is up. For `zone`: what is inside, over and over\"},\"onEnd\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"For missiles that were not stopped, and for zones: what happens where and when they end\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"missile\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"buff\":{\"type\":\"string\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"aura\"}},\"required\":[\"buff\",\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"duration\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Seconds the buff lasts, when that is different from its usual duration\"},\"unit\":{\"type\":\"string\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"spawn\"}},\"required\":[\"type\",\"unit\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"buff\":{\"type\":\"string\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"toggle\"}},\"required\":[\"buff\",\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"duration\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Seconds the buff lasts, when that is different from its usual duration\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"reveal\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"whileBuff\":{\"type\":\"string\",\"description\":\"For `onHit`: only while the attacker carries this buff\"},\"effects\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"What happens to what is hit. For missiles: everything they pass through. For `onHit`: the target of an attack. For `dash`: what is around where it ends. For `after`: what is there when the time is up. For `zone`: what is inside, over and over\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"onHit\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"properties\":{\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"resetAttack\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"distance\":{\"$ref\":\"#/definitions/Scaled\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"blink\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"to\":{\"type\":\"string\",\"enum\":[\"home\",\"point\",\"target\"],\"description\":\"For `teleport`: where to, however far: the caster's side's home (`homeOf`), where it was aimed, or by its target\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"teleport\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"distance\":{\"$ref\":\"#/definitions/Scaled\"},\"leap\":{\"type\":\"number\",\"description\":\"For `dash`: how high the caster rises on the way, which makes it a leap. For whoever draws it\"},\"climb\":{\"type\":\"number\",\"description\":\"For a leap: how much higher than where it takes off the ground on its way may stand. What stands higher is a wall to it: it comes down in front of it. Over anything when left out\"},\"speed\":{\"type\":\"number\",\"description\":\"For missiles: how fast they travel, in units per second, and how far\"},\"effects\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"What happens to what is hit. For missiles: everything they pass through. For `onHit`: the target of an attack. For `dash`: what is around where it ends. For `after`: what is there when the time is up. For `zone`: what is inside, over and over\"},\"buff\":{\"type\":\"string\",\"description\":\"Id of the buff to put on the targets\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"dash\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"distance\":{\"$ref\":\"#/definitions/Scaled\"},\"speed\":{\"type\":\"number\",\"description\":\"For missiles: how fast they travel, in units per second, and how far\"},\"leap\":{\"type\":\"number\",\"description\":\"For `dash`: how high the caster rises on the way, which makes it a leap. For whoever draws it\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"knockback\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"ability\":{\"type\":\"string\",\"description\":\"For `refresh`: the ability that is ready again, when it is not the one the effect belongs to\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"refresh\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"attack\":{\"type\":\"boolean\",\"description\":\"For `onKill`: only when it was an attack of the carrier that killed\"},\"effects\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"What happens to what is hit. For missiles: everything they pass through. For `onHit`: the target of an attack. For `dash`: what is around where it ends. For `after`: what is there when the time is up. For `zone`: what is inside, over and over\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"onKill\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"effects\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"What happens to what is hit. For missiles: everything they pass through. For `onHit`: the target of an attack. For `dash`: what is around where it ends. For `after`: what is there when the time is up. For `zone`: what is inside, over and over\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"onCast\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"effects\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"What happens to what is hit. For missiles: everything they pass through. For `onHit`: the target of an attack. For `dash`: what is around where it ends. For `after`: what is there when the time is up. For `zone`: what is inside, over and over\"},\"otherwise\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"For `charged`: what happens when the ability is not charged\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"charged\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"delay\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"For missiles: seconds they take shape where they were cast before they set off. The caster is free to go\"},\"effects\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"What happens to what is hit. For missiles: everything they pass through. For `onHit`: the target of an attack. For `dash`: what is around where it ends. For `after`: what is there when the time is up. For `zone`: what is inside, over and over\"},\"impact\":{\"type\":\"string\",\"description\":\"For `after`: the name its arrival goes by for whoever draws it and plays its sound, where one ability sets off several that do not look alike: the last of a barrage. The ability's own when left out\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"after\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"duration\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Seconds the buff lasts, when that is different from its usual duration\"},\"effects\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"What happens to what is hit. For missiles: everything they pass through. For `onHit`: the target of an attack. For `dash`: what is around where it ends. For `after`: what is there when the time is up. For `zone`: what is inside, over and over\"},\"onEnd\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"For missiles that were not stopped, and for zones: what happens where and when they end\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"zone\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"duration\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Seconds the buff lasts, when that is different from its usual duration\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"detect\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"duration\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Seconds the buff lasts, when that is different from its usual duration\"},\"count\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"For missiles: how many are sent, spread over an angle in radians. What one of them hit the others pass by\"},\"unit\":{\"type\":\"string\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"raiseDead\"}},\"required\":[\"type\",\"unit\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"dealt\":{\"type\":\"number\"},\"taken\":{\"type\":\"number\"},\"count\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"For missiles: how many are sent, spread over an angle in radians. What one of them hit the others pass by\"},\"duration\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Seconds the buff lasts, when that is different from its usual duration\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"illusion\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"amount\":{\"$ref\":\"#/definitions/Scaled\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"dispel\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"sell\":{\"type\":\"string\"},\"buy\":{\"type\":\"string\"},\"rate\":{\"type\":\"number\"},\"amount\":{\"$ref\":\"#/definitions/Scaled\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"exchange\"}},\"required\":[\"amount\",\"buy\",\"rate\",\"sell\",\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"gain\":{\"$ref\":\"#/definitions/StatChange\"},\"xp\":{\"type\":\"number\"},\"levels\":{\"type\":\"number\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"tome\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"ratio\":{\"type\":\"number\"},\"amount\":{\"$ref\":\"#/definitions/Scaled\"},\"scaling\":{\"$ref\":\"#/definitions/Scaling\",\"description\":\"What the stats of the caster add to the amount: `{ abilityPower: 0.6 }` adds 60% of its ability power. `level` is for every level of the hero past the first\"},\"damageType\":{\"$ref\":\"#/definitions/DamageType\",\"description\":\"For damage. Magic when left out\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"manaBurn\"}},\"required\":[\"type\"]},{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"count\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"For missiles: how many are sent, spread over an angle in radians. What one of them hit the others pass by\"},\"ifBuff\":{\"type\":\"string\",\"description\":\"Only hits what carries this buff\"},\"only\":{\"type\":\"string\",\"const\":\"heroes\",\"description\":\"Only hits heroes\"},\"kinds\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EntityKind\"},\"description\":\"Only hits these kinds of things. Everything but resources when left out\"},\"layers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Layer\"},\"description\":\"Where what it hits has to be: on the ground, in the air. Both when left out\"},\"radius\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Makes the effect hit everything around the point of impact. Required for auras, and the size of a missile\"},\"affects\":{\"type\":\"string\",\"enum\":[\"enemies\",\"allies\",\"all\"],\"description\":\"Who an effect with a radius applies to. Follows from the kind of effect when left out\"},\"on\":{\"type\":\"string\",\"const\":\"caster\",\"description\":\"Applies to the caster, wherever the ability was aimed\"},\"type\":{\"type\":\"string\",\"const\":\"townPortal\"}},\"required\":[\"type\"]}],\"description\":\"One thing an ability does: its `type`, which picks the handler and says which fields it has (`EffectTypes`)\"},\"BuffDef\":{\"type\":\"object\",\"properties\":{\"stats\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/Scaled\"}},\"percent\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/Scaled\"}},\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"basedOn\":{\"type\":\"string\",\"description\":\"The definition this one is a copy of, made from the data (`copyOf` in a `data.json`, the editor's Copy as new): what it is drawn, heard and pictured as where the game gives it no look of its own\"},\"description\":{\"type\":\"string\"},\"duration\":{\"type\":\"number\",\"description\":\"Seconds. Lasts until removed when left out\"},\"harmful\":{\"type\":\"boolean\",\"description\":\"A debuff: by default it goes on enemies\"},\"stun\":{\"type\":\"boolean\",\"description\":\"The unit cannot do anything. A function says so for one carrier, one moment\"},\"airborne\":{\"type\":\"boolean\",\"description\":\"Thrown into the air for as long as it lasts. Comes with a stun, the rest is for the eye\"},\"down\":{\"type\":\"boolean\",\"description\":\"Knocked down: it lies on the ground for as long as it lasts, and gets up at the end. For the eye, like `airborne`\"},\"grounds\":{\"type\":\"boolean\",\"description\":\"Brings what flies down: it counts as being on the ground, for whatever is aimed at it\"},\"invisible\":{\"type\":\"boolean\",\"description\":\"Its carrier is invisible to enemies for as long as it lasts, unless they find it (`detect`): a Wind Walk. A function says so for one carrier, one moment\"},\"root\":{\"type\":\"boolean\",\"description\":\"The unit cannot move from the spot. It can still fight and cast. A function says so for one carrier, one moment\"},\"silence\":{\"type\":\"boolean\",\"description\":\"The unit cannot use its abilities. What it carries and the spells every hero has still work. A function says which abilities it keeps it from (`(ability) => ability.tags?.includes('fire')`): the screen asks it too\"},\"shield\":{\"$ref\":\"#/definitions/Scaled\",\"description\":\"Damage it takes in place of the unit, for as long as the buff lasts and there is something left of it\"},\"shieldScaling\":{\"$ref\":\"#/definitions/Scaling\",\"description\":\"What the stats of whoever gave the shield add to it\"},\"recharge\":{\"type\":\"number\",\"description\":\"The shield is as good as new once the unit has not been hurt for this many seconds\"},\"avoids\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Tags of abilities that do nothing to who carries it, for as long as it does: `ground` for someone up in the air\"},\"disarm\":{\"type\":\"boolean\",\"description\":\"Who carries it does not attack while it lasts: burrowed, or disarmed. Or while its function says\"},\"spellImmune\":{\"type\":\"boolean\",\"description\":\"Who carries it is immune to spells while it lasts, as an avatar is (see `UnitTypeDef.spellImmune`). Or while its function says\"},\"dispellable\":{\"type\":\"boolean\",\"description\":\"Whether a dispel takes it off (see the effect `dispel`). Yes when left out: no for what nothing should end early. Decided in code for each dispel: by who dispels it (`by`), its level\"},\"maxStacks\":{\"type\":\"number\",\"description\":\"How many of it one unit can carry at most. What puts it on says whether it adds one (`stack`)\"},\"onEnd\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"What happens around who carried it when its time is up, in the name of who put it there. Not when it is taken off\"},\"perStack\":{\"type\":\"boolean\",\"description\":\"Its stats count once for every stack it has (`stacks`), rather than once: \\\"each stack, -5% move speed\\\"\"},\"stacking\":{\"type\":\"string\",\"enum\":[\"strongest\",\"refresh\",\"perSource\",\"independent\"],\"description\":\"What happens when it is put on a unit that has it already. `strongest` (the default): the higher level wins, the longer among equals. `refresh`: the time starts over, the level the new one's. `perSource`: one of it for every unit that puts it on, each of its own (two poisons from two casters). `independent`: a new one every time. A function decides from the one there and the one coming: `replace` it, `renew` its time, `add` another, or `keep`\"},\"onApply\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"},\"description\":\"What happens as it is put on: effects in the name of who put it there (while they live), or code\"},\"onTick\":{\"type\":\"object\",\"properties\":{\"every\":{\"type\":\"number\"},\"effects\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/EffectDef\"}}},\"required\":[\"every\"],\"additionalProperties\":false,\"description\":\"What happens every `every` seconds while it lasts: effects in the name of who put it there (while they live), or code, which runs whoever is left\"}},\"required\":[\"id\",\"name\"],\"additionalProperties\":false,\"description\":\"Something lasting on a unit that changes what it can do\"},\"ItemDef\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"level\":{\"type\":\"number\",\"description\":\"How good it is, 1 and up (campaign plan GP3): what an item table draws by (`{ level: 3 }`)\"},\"class\":{\"type\":\"string\",\"description\":\"Its kind of item, a word of the game's (`permanent`, `charged`, `powerup`): what an item table draws by too\"},\"basedOn\":{\"type\":\"string\",\"description\":\"The definition this one is a copy of, made from the data (`copyOf` in a `data.json`, the editor's Copy as new): what it is drawn, heard and pictured as where the game gives it no look of its own\"},\"description\":{\"type\":\"string\"},\"cost\":{\"$ref\":\"#/definitions/Cost\"},\"charges\":{\"type\":\"number\",\"description\":\"Uses before it is gone. Items without charges stay\"},\"ability\":{\"type\":\"string\",\"description\":\"Id of the ability the item gives: using the item casts it, without mana\"},\"passive\":{\"type\":\"string\",\"description\":\"Id of an ability whose passive part works for whoever carries the item\"},\"from\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Items it is made of. Those the buyer carries are used up and taken off the price, which is for all of it\"},\"tier\":{\"$ref\":\"#/definitions/ItemTier\",\"description\":\"Where it goes in a shop: consumable, starter, basic, epic, legendary\"},\"bonus\":{\"$ref\":\"#/definitions/StatChange\",\"description\":\"Applies for as long as the item is carried\"},\"stock\":{\"type\":\"object\",\"properties\":{\"max\":{\"type\":\"number\"},\"restock\":{\"type\":\"number\"}},\"required\":[\"max\",\"restock\"],\"additionalProperties\":false,\"description\":\"How many a shop has at most, and the seconds it takes to get one back. As many as anyone wants when left out\"},\"hotkey\":{\"type\":\"string\"},\"powerUp\":{\"type\":\"boolean\",\"description\":\"Used as it is picked up, by walking over it or by the order, and never carried: a rune, a tome. Its ability is cast by whoever picked it. In a game with the runes pack (`withRunes`, whose `powerUp` says it in code)\"}},\"required\":[\"id\",\"name\",\"cost\"],\"additionalProperties\":false},\"ItemTier\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"string\",\"enum\":[\"consumable\",\"starter\",\"basic\",\"epic\",\"legendary\"]}],\"description\":\"Where an item is in a shop. The client's tiers, or a game's own (`GameView.shopTiers`)\"},\"UpgradeDef\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"basedOn\":{\"type\":\"string\",\"description\":\"The definition this one is a copy of, made from the data (`copyOf` in a `data.json`, the editor's Copy as new): what it is drawn, heard and pictured as where the game gives it no look of its own\"},\"description\":{\"type\":\"string\"},\"cost\":{\"$ref\":\"#/definitions/Cost\",\"description\":\"Paid for every level\"},\"researchTime\":{\"type\":\"number\",\"description\":\"Seconds per level\"},\"maxLevel\":{\"type\":\"number\"},\"requires\":{\"type\":\"array\",\"items\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"description\":\"Building types needed, one list per level. Levels past the end of the list need nothing\"},\"race\":{\"type\":\"string\",\"description\":\"The race it is of (`GameModule.races`): only its players may research it. Everyone's when left out\"},\"hotkey\":{\"type\":\"string\"},\"effects\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/UpgradeEffect\"}}},\"required\":[\"id\",\"name\",\"cost\",\"researchTime\",\"effects\"],\"additionalProperties\":false,\"description\":\"Research that improves a player's units for the rest of the game\"},\"UpgradeEffect\":{\"type\":\"object\",\"properties\":{\"stats\":{\"type\":\"object\",\"properties\":{\"attackDamage\":{\"type\":\"number\",\"description\":\"What a basic attack deals, before the armor of the target\"},\"attackSpeed\":{\"type\":\"number\",\"description\":\"Attacks per second\"},\"attackRange\":{\"type\":\"number\",\"description\":\"Measured between the edges of attacker and target\"},\"health\":{\"type\":\"number\"},\"armor\":{\"type\":\"number\",\"description\":\"Against physical damage: 100 of it halve what arrives\"},\"magicResist\":{\"type\":\"number\",\"description\":\"The same against magic damage\"},\"healthRegen\":{\"type\":\"number\",\"description\":\"Per second\"},\"mana\":{\"type\":\"number\"},\"manaRegen\":{\"type\":\"number\",\"description\":\"Per second\"},\"moveSpeed\":{\"type\":\"number\",\"description\":\"Units per second. Who has none cannot move\"},\"sight\":{\"type\":\"number\",\"description\":\"How far it sees. At night the unit type says how far that is\"}},\"additionalProperties\":false},\"percent\":{\"type\":\"object\",\"properties\":{\"attackDamage\":{\"type\":\"number\",\"description\":\"What a basic attack deals, before the armor of the target\"},\"attackSpeed\":{\"type\":\"number\",\"description\":\"Attacks per second\"},\"attackRange\":{\"type\":\"number\",\"description\":\"Measured between the edges of attacker and target\"},\"health\":{\"type\":\"number\"},\"armor\":{\"type\":\"number\",\"description\":\"Against physical damage: 100 of it halve what arrives\"},\"magicResist\":{\"type\":\"number\",\"description\":\"The same against magic damage\"},\"healthRegen\":{\"type\":\"number\",\"description\":\"Per second\"},\"mana\":{\"type\":\"number\"},\"manaRegen\":{\"type\":\"number\",\"description\":\"Per second\"},\"moveSpeed\":{\"type\":\"number\",\"description\":\"Units per second. Who has none cannot move\"},\"sight\":{\"type\":\"number\",\"description\":\"How far it sees. At night the unit type says how far that is\"}},\"additionalProperties\":false},\"units\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Unit types that benefit, or a function of the type. Everything the player owns when left out\"}},\"additionalProperties\":false,\"description\":\"What a level of research changes\"},\"RaceDef\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\",\"description\":\"What players are shown: in the lobby, on the scores\"},\"basedOn\":{\"type\":\"string\",\"description\":\"The definition this one is a copy of, made from the data (`copyOf` in a `data.json`, the editor's Copy as new): what it is drawn, heard and pictured as where the game gives it no look of its own\"},\"description\":{\"type\":\"string\",\"description\":\"What it is like, for a tooltip\"},\"icon\":{\"type\":\"string\",\"description\":\"Its picture, by the id the game's view knows icons by\"},\"resources\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"number\"},\"description\":\"What its players start with of each resource, by resource id, over what the game starts everyone with (`ResourceDef.start`). Not for a player given resources of their own (`PlayerInit.resources`, a map's)\"},\"start\":{\"$ref\":\"#/definitions/StartDef\",\"description\":\"What its players get at the start of a game on a map, in place of the game's (`GameModule.start`): its own hall and workers, as data (`StartData`: what a race made in the Data panel has, `start.hall` and the rest editable there) or as code (`StartCode`)\"},\"hooks\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/RaceHookDef\"},\"description\":\"Hook handlers of its own, run only for what is of its players (`RaceHookDef`): how it gathers, where it may build. With these a race plays differently with no test of the race in the game's code\"},\"inherits\":{\"type\":\"object\",\"properties\":{\"hooks\":{\"type\":\"boolean\"},\"onStart\":{\"type\":\"boolean\"}},\"additionalProperties\":false,\"description\":\"What a race made as a copy of another (`basedOn`: `copyOf` in a `data.json`, the editor's Copy as new) keeps of the original's code: its `hooks` and its `onStart`, each kept unless said `false` here. A copy keeps both when this is left out, so a race copied from the undead gathers at a cursed mine and builds on blight as they do; one that says `hooks: false` plays by the game's rules alone. Its `start` is data, the copy's own to change; a `start` in code is kept whole. Two toggles in the Data panel, on a copy\"}},\"required\":[\"id\",\"name\"],\"additionalProperties\":false,\"description\":\"A side a player can play, with a roster of its own: melee's humans and undead. Optional: a game that lists none (`GameModule.races`) has no race anywhere, no choice of one in the lobby and nothing kept from anyone. A unit type or an upgrade of a race (`UnitTypeDef.race`, `UpgradeDef.race`) is only its players' to make; one of none, everyone's. Each player has one (`raceOf`): chosen in the lobby (a seat setting the server offers by itself, `seatSettingsOf`), given to the world as it is made (`PlayerInit.race`, `MapWorldOptions.races`), or changed by the rules (`setRace`)\"},\"StartDef\":{\"anyOf\":[{\"$ref\":\"#/definitions/StartCode\"},{\"$ref\":\"#/definitions/StartData\"}],\"description\":\"What each player gets at the beginning of a game on a map, for players the map places nothing for: as data (`StartData`: a hall, workers and what else by unit type, editable in the Data panel) or as code (`StartCode`). What they start with of each resource is in its `ResourceDef`\"},\"StartCode\":{\"type\":\"object\",\"additionalProperties\":false,\"description\":\"A start in code: puts down a base at the start location, whatever it likes\"},\"StartData\":{\"type\":\"object\",\"properties\":{\"hall\":{\"type\":\"string\",\"description\":\"The unit type put down at the start location: a town hall\"},\"workers\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"type\",\"count\"],\"additionalProperties\":false,\"description\":\"The workers put down beside it, facing the closest gold mine\"},\"units\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"type\",\"count\"],\"additionalProperties\":false},\"description\":\"What else is put down with the workers, in the same row after them: a race's first fighter\"},\"onMine\":{\"type\":\"string\",\"description\":\"What the race builds on the closest gold mine to gather it, put down there already standing (the undead's cursed mine). With it, the closest mine is found at any distance; without, the workers face a mine within 20 cells\"},\"distance\":{\"type\":\"number\",\"description\":\"How far from the hall the workers stand: 5 cells when left out\"},\"spread\":{\"type\":\"number\",\"description\":\"The angle between two workers in the row, in radians: 0.35 when left out\"}},\"required\":[\"hall\",\"workers\"],\"additionalProperties\":false,\"description\":\"A start as data: a hall at the start location and workers on the side of the closest gold mine, as in a classic melee game (`startingBase` puts it down). Every id is a unit type of the game's (`checkRaces` says where one is not)\"},\"RaceHookDef\":{\"type\":\"object\",\"properties\":{},\"description\":\"A hook handler of a race (`RaceDef.hooks`): run only where the event is of one of the race's players. Whose an event is, is `of(event)` where it is given, or else its `player`, or else the owner of the first there is of its `entity`, `caster`, `attacker`, `worker`, `buyer`, `hero`, `producer`, `source`, `target`: the one doing it before the one it is done to (a damage is its source's)\"},\"RulesInit\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"score\":{\"$ref\":\"#/definitions/ScoreRules\"},\"hud\":{\"$ref\":\"#/definitions/HudSharing\"},\"supply\":{\"$ref\":\"#/definitions/RulesPart%3Cdef-interface-src_rules.ts-6406-7192-src_rules.ts-0-2567620292202711967698989%3E\"},\"production\":{\"$ref\":\"#/definitions/RulesPart%3Cdef-interface-src_rules.ts-7192-7817-src_rules.ts-0-256769690053192058131377%3E\"},\"economy\":{\"$ref\":\"#/definitions/RulesPart%3Cdef-interface-src_rules.ts-7817-8384-src_rules.ts-0-25676871182655808602831%3E\"},\"combat\":{\"$ref\":\"#/definitions/RulesPart%3Cdef-interface-src_rules.ts-8384-12915-src_rules.ts-0-2567620803371672015716859%3E\"},\"movement\":{\"$ref\":\"#/definitions/RulesPart%3Cdef-interface-src_rules.ts-12915-15119-src_rules.ts-0-2567619627991872051239824%3E\"},\"fog\":{\"$ref\":\"#/definitions/RulesPart%3Cdef-interface-src_rules.ts-2336-3434-src_rules.ts-0-256761516734472119914999%3E\"},\"talk\":{\"$ref\":\"#/definitions/RulesPart%3Cdef-interface-src_rules.ts-15119-16441-src_rules.ts-0-256767575302061161883220%3E\"},\"camps\":{\"$ref\":\"#/definitions/RulesPart%3Cdef-interface-src_rules.ts-22230-22604-src_rules.ts-0-25676594161992489921438%3E\"},\"heroes\":{\"$ref\":\"#/definitions/RulesPart%3Cdef-interface-src_hero.ts-953-2062-src_hero.ts-0-1454210919897661340881447%3E\"}},\"description\":\"What a game says of its rules (`GameModule.rules`), or a pack (`GamePack.rules`): any of them, a group in part (`{ supply: { max: 200 } }`), the engine's for the rest. The damage table, the score and the HUD's sharing are given whole\"},\"ScoreRules\":{\"type\":\"object\",\"properties\":{\"stats\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/ScoreStatDef\"},\"description\":\"The stats, or what makes them of the game (the engine's set makes one of each resource)\"},\"every\":{\"type\":\"number\",\"description\":\"Seconds between two points of the timeline: 10 when left out, 0 for no timeline\"},\"maxPoints\":{\"type\":\"number\",\"description\":\"Points the timeline keeps at most: past it, every other point goes and they are twice as far apart, so a long game keeps its whole story in as much room. 360 when left out\"},\"live\":{\"$ref\":\"#/definitions/ScoreAudience\",\"description\":\"Who is told a player's numbers while the game goes on: their side (`allies`) when left out. An observer is told everyone's\"},\"end\":{\"$ref\":\"#/definitions/ScoreAudience\",\"description\":\"Who is told them once the game is decided: `everyone` when left out\"},\"refresh\":{\"type\":\"number\",\"description\":\"Seconds between two tellings of the numbers to the screens while the game goes on: 1 when left out\"},\"categories\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"label\":{\"type\":\"string\"}},\"required\":[\"id\",\"label\"],\"additionalProperties\":false},\"description\":\"The tabs of the score screen, in order, with their names: those of the stats, in the order met, when left out\"}},\"required\":[\"stats\"],\"additionalProperties\":false,\"description\":\"What a game counts of its players (`GameRules.score`): the stats, how often the timeline is sampled, and who is told what while the game goes on and after it ended. `presetScore()` makes one of the engine's stats\"},\"ScoreStatDef\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"description\":\"Its own, among the game's stats: `world.score.add(player, id, …)` names it\"},\"label\":{\"type\":\"string\",\"description\":\"What it is called on a screen: its id when left out\"},\"description\":{\"type\":\"string\",\"description\":\"What the screen says it is, in a tip\"},\"category\":{\"type\":\"string\",\"description\":\"The tab of the score screen it is in: `units`, `heroes`, `resources`, `combat`, or the game's own. `other` when left out\"},\"kind\":{\"$ref\":\"#/definitions/ScoreKind\",\"description\":\"How it adds up: `sum` when left out, `last` for one that only samples\"},\"from\":{\"anyOf\":[{\"$ref\":\"#/definitions/ScoreSource\"},{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/ScoreSource\"}}],\"description\":\"The events it is counted from\"},\"byType\":{\"type\":\"boolean\",\"description\":\"Kept by what it was counted of too (`ScoreCount.type`): units killed by unit type, abilities cast by ability\"},\"timeline\":{\"type\":\"boolean\",\"description\":\"On the timeline: sampled every `ScoreRules.every` seconds, for a screen's graph\"},\"format\":{\"$ref\":\"#/definitions/ScoreFormat\",\"description\":\"How its number is written: `number` (when left out), `decimal`, `percent` (0 to 1), `time` (seconds)\"},\"hidden\":{\"type\":\"boolean\",\"description\":\"Counted, and not on the score screen: for the game's own use (an achievement, a mission's goal)\"}},\"required\":[\"id\"],\"additionalProperties\":false,\"description\":\"A number the score keeps for each player (`GameRules.score`, `defineStat`): what it is called, how it adds up, where it comes from, and how it is shown. One with no source is counted by the game's code alone (`world.score.add`)\"},\"ScoreKind\":{\"type\":\"string\",\"enum\":[\"sum\",\"count\",\"max\",\"min\",\"last\"],\"description\":\"How a stat takes what it is told: `sum` adds it up (the default), `count` adds one each time whatever the amount, `max` keeps the most it was told, `min` the least, `last` what it was told last (a gauge: the army now)\"},\"ScoreSource\":{\"type\":\"object\",\"properties\":{\"event\":{\"$ref\":\"#/definitions/EventKey%3Cany%3E\",\"description\":\"The event: one of the engine's (`entityDied`) or of the game's own (`defineEvent`)\"}},\"required\":[\"event\"],\"additionalProperties\":false,\"description\":\"Where a stat's numbers come from: an event of the world's (`'entityDied'`) or of the game's own (`defineEvent`), and what it counts of each (`scoreOn`)\"},\"EventKey<any>\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"replicate\":{\"$ref\":\"#/definitions/EventAudience%3Cany%3E\",\"description\":\"Sent to the screens of these players, in their snapshots. Only to whoever listens in the world when left out\"},\"payload\":{\"description\":\"Only there for the type of the payload\"}},\"required\":[\"id\"],\"additionalProperties\":false,\"description\":\"A typed key for an event of a game's own, made with `defineEvent`\"},\"EventAudience<any>\":{\"type\":\"string\",\"const\":\"everyone\",\"description\":\"Who of the players gets an event of a game's own on their screen (see `defineEvent`): everyone, or those a function picks, from what the event says and what the seat may know (`SessionView`: its player, `knows`, `sees`)\"},\"ScoreFormat\":{\"type\":\"string\",\"enum\":[\"number\",\"decimal\",\"percent\",\"time\"],\"description\":\"How a stat's number reads on a screen: a whole `number` (the default), a `decimal` one, a `percent` (0..1), or `time` (seconds, as 3:05). A screen's own (`GameView.hud.score.format`) goes over it\"},\"ScoreAudience\":{\"type\":\"string\",\"enum\":[\"own\",\"allies\",\"everyone\",\"none\"],\"description\":\"Who of the players is told a player's numbers: that player alone (`own`), their side (`allies`), `everyone`, or `none`; or a function of the world, who is told (`undefined` for an observer) and whose numbers they are\"},\"HudSharing\":{\"type\":\"object\",\"properties\":{\"shareBuffTimes\":{\"$ref\":\"#/definitions/HudShareRule%3C(boolean%7C%22allies%22)%3E\",\"description\":\"The seconds left of its buffs (`EntityState.buffTimes`): to everyone who sees it (`true`), to its owner and their allies (`'allies'`), or to no one (`false`, when left out)\"},\"shareAbilities\":{\"$ref\":\"#/definitions/HudShareRule%3C(%22none%22%7C%22icons%22%7C%22cooldowns%22)%3E\",\"description\":\"Its abilities (`EntityState.abilities`) to everyone who sees it, its side's or not: those it has learned and their levels (`'icons'`), with how much of their cooldowns is left too (`'cooldowns'`), or none (`'none'`, when left out: its side has them in `own` all the same)\"},\"shareXp\":{\"$ref\":\"#/definitions/HudShareRule%3Cboolean%3E\",\"description\":\"A hero's experience (`HeroSent.xp`) to everyone who sees it. Its side's alone when left out\"}},\"additionalProperties\":false,\"description\":\"What of a unit the players who see it are told beyond what they always are (`GameRules.hud`), each for the plates over it to show. What is not shared never reaches a player, so no screen can show it. A game's own data goes in a component with `replicate` (the client's `hudData` reads it)\"},\"HudShareRule<(boolean|\\\"allies\\\")>\":{\"anyOf\":[{\"type\":\"boolean\"},{\"type\":\"string\",\"const\":\"allies\"}],\"description\":\"A rule of `HudSharing`: one value for every unit, or a function deciding it for each (only heroes', only a boss's)\"},\"HudShareRule<(\\\"none\\\"|\\\"icons\\\"|\\\"cooldowns\\\")>\":{\"type\":\"string\",\"enum\":[\"none\",\"icons\",\"cooldowns\"],\"description\":\"A rule of `HudSharing`: one value for every unit, or a function deciding it for each (only heroes', only a boss's)\"},\"HudShareRule<boolean>\":{\"type\":\"boolean\",\"description\":\"A rule of `HudSharing`: one value for every unit, or a function deciding it for each (only heroes', only a boss's)\"},\"RulesPart<def-interface-src_rules.ts-6406-7192-src_rules.ts-0-2567620292202711967698989>\":{\"type\":\"object\",\"properties\":{\"max\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber1860156146%3E\"},\"pools\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/RulesPart%3Cstructure-src_rules.ts-6876-6906-src_rules.ts-6861-6907-src_rules.ts-6578-6907-src_rules.ts-6406-7192-src_rules.ts-0-25676%3E\"}}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number1860156146>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<structure-src_rules.ts-6876-6906-src_rules.ts-6861-6907-src_rules.ts-6578-6907-src_rules.ts-6406-7192-src_rules.ts-0-25676>\":{\"type\":\"object\",\"properties\":{\"name\":{\"$ref\":\"#/definitions/RulesPart%3Cstring%3E\"},\"max\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber%3E\"}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<string>\":{\"type\":\"string\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<def-interface-src_rules.ts-7192-7817-src_rules.ts-0-256769690053192058131377>\":{\"type\":\"object\",\"properties\":{\"queue\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber2067800667%3E\"},\"cancelRefund\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber1635358800%3E\"},\"constructionHealth\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber1408574317%3E\"}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number2067800667>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number1635358800>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number1408574317>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<def-interface-src_rules.ts-7817-8384-src_rules.ts-0-25676871182655808602831>\":{\"type\":\"object\",\"properties\":{\"sellPrice\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber236571108%3E\"},\"repairCost\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber385641258%3E\"},\"tradeTax\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber500452615%3E\"}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number236571108>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number385641258>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number500452615>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<def-interface-src_rules.ts-8384-12915-src_rules.ts-0-2567620803371672015716859>\":{\"type\":\"object\",\"properties\":{\"attackSpeedLimits\":{\"$ref\":\"#/definitions/RulesPart%3C%5Bnumber%2Cnumber%5D131060964%3E\"},\"corpseTime\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber202117478%3E\"},\"damageTable\":{\"type\":\"object\",\"properties\":{\"attacks\":{\"$ref\":\"#/definitions/RulesPart%3Cstring%5B%5D%3E\"},\"armours\":{\"$ref\":\"#/definitions/RulesPart%3Cstring%5B%5D%3E\"},\"table\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber%5B%5D%5B%5D%3E\"}},\"additionalProperties\":false}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<[number,number]131060964>\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},\"minItems\":2,\"maxItems\":2,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number202117478>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<string[]>\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number[][]>\":{\"type\":\"array\",\"items\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}},\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<def-interface-src_rules.ts-12915-15119-src_rules.ts-0-2567619627991872051239824>\":{\"type\":\"object\",\"properties\":{\"together\":{\"type\":\"boolean\"}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<def-interface-src_rules.ts-2336-3434-src_rules.ts-0-256761516734472119914999>\":{\"type\":\"object\",\"properties\":{\"nightSight\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber1727028879%3E\"},\"explored\":{\"type\":\"string\",\"enum\":[\"remembered\",\"nothing\"]},\"startExplored\":{\"type\":\"boolean\"},\"mask\":{\"type\":\"string\",\"enum\":[\"black\",\"dim\"]}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number1727028879>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<def-interface-src_rules.ts-15119-16441-src_rules.ts-0-256767575302061161883220>\":{\"type\":\"object\",\"properties\":{\"pings\":{\"$ref\":\"#/definitions/RulesPart%3Cstructure-src_rules.ts-15901-16099-src_rules.ts-15214-16099-src_rules.ts-15119-16441-src_rules.ts-0-25676624849888%3E\"},\"lines\":{\"$ref\":\"#/definitions/RulesPart%3Cstructure-src_rules.ts-16207-16256-src_rules.ts-16099-16256-src_rules.ts-15119-16441-src_rules.ts-0-25676381541611%3E\"},\"pauses\":{\"$ref\":\"#/definitions/RulesPart%3Cstructure-src_rules.ts-16405-16439-src_rules.ts-16256-16439-src_rules.ts-15119-16441-src_rules.ts-0-256761718446262%3E\"}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<structure-src_rules.ts-15901-16099-src_rules.ts-15214-16099-src_rules.ts-15119-16441-src_rules.ts-0-25676624849888>\":{\"type\":\"object\",\"properties\":{\"most\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber%3E\"},\"within\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber%3E\"},\"kinds\":{\"$ref\":\"#/definitions/RulesPart%3Cstring%5B%5D%3E\"},\"meanings\":{\"$ref\":\"#/definitions/RulesPart%3Cindexed-type-140537781-73964-74003-140537781-73899-74004-140537781-0-218439%3Calias-140537781-74138-74264-140537781-0-218439%3Cstring%2C(%22help%22%7C%22attack%22%7C%22defend%22%7C%22rally%22%7C%22ignore%22)%3E%3E%3E\"}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<indexed-type-140537781-73964-74003-140537781-73899-74004-140537781-0-218439<alias-140537781-74138-74264-140537781-0-218439<string,(\\\"help\\\"|\\\"attack\\\"|\\\"defend\\\"|\\\"rally\\\"|\\\"ignore\\\")>>>\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/RulesPart%3C(%22help%22%7C%22attack%22%7C%22defend%22%7C%22rally%22%7C%22ignore%22)%3E\"},\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<(\\\"help\\\"|\\\"attack\\\"|\\\"defend\\\"|\\\"rally\\\"|\\\"ignore\\\")>\":{\"type\":\"string\",\"enum\":[\"help\",\"attack\",\"defend\",\"rally\",\"ignore\"],\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<structure-src_rules.ts-16207-16256-src_rules.ts-16099-16256-src_rules.ts-15119-16441-src_rules.ts-0-25676381541611>\":{\"type\":\"object\",\"properties\":{\"most\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber%3E\"},\"within\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber%3E\"},\"length\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber%3E\"}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<structure-src_rules.ts-16405-16439-src_rules.ts-16256-16439-src_rules.ts-15119-16441-src_rules.ts-0-256761718446262>\":{\"type\":\"object\",\"properties\":{\"each\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber%3E\"},\"seconds\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber%3E\"}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<def-interface-src_rules.ts-22230-22604-src_rules.ts-0-25676594161992489921438>\":{\"type\":\"object\",\"properties\":{\"leash\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber1257883159%3E\"},\"sleep\":{\"$ref\":\"#/definitions/RulesPart%3Cboolean1244251315%3E\"},\"respawn\":{\"type\":\"number\"}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number1257883159>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<boolean1244251315>\":{\"type\":\"boolean\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<def-interface-src_hero.ts-953-2062-src_hero.ts-0-1454210919897661340881447>\":{\"type\":\"object\",\"properties\":{\"maxHeroes\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber1867899884%3E\"},\"maxHeroLevel\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber1322296163%3E\"},\"xpRange\":{\"$ref\":\"#/definitions/RulesPart%3Cnumber1756145837%3E\"}},\"additionalProperties\":false,\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number1867899884>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number1322296163>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"RulesPart<number1756145837>\":{\"type\":\"number\",\"description\":\"What is said of a group of the rules: any of it, a group in it in part as well. A function, a list and a table whole\"},\"GameData\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"removed\":{\"type\":\"object\",\"properties\":{\"units\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"abilities\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"buffs\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"items\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"upgrades\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"races\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"additionalProperties\":false},\"ai\":{\"type\":\"object\",\"properties\":{\"races\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/AiProfile\"}}},\"additionalProperties\":false,\"description\":\"How the game's computers play each race (campaign plan AR4), by race id: each a whole profile, in the place of the code's (`GameModule.ai.races`). What the editor's Data panel writes of a race's AI\"},\"rules\":{\"$ref\":\"#/definitions/DataRules\",\"description\":\"The rules as data (campaign plan RC7), laid over the game's (`DataRules`): what the Data panel's Rules writes\"},\"units\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/DataValues\"}},\"abilities\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/DataValues\"}},\"buffs\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/DataValues\"}},\"items\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/DataValues\"}},\"upgrades\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/DataValues\"}},\"races\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/DataValues\"}}},\"description\":\"The values of a game's definitions, by kind and id: what its `data.json` holds. `removed` takes definitions of the game's out, by kind (the Data panel's Delete): what still names one is the rules' check's to say (`checkRules`)\"},\"AiProfile\":{\"type\":\"object\",\"properties\":{\"economy\":{\"$ref\":\"#/definitions/EconomyOptions\",\"description\":\"The workers and the bases (`Economy`)\"},\"defence\":{\"$ref\":\"#/definitions/DefenceOptions\",\"description\":\"What it defends and how (`Defence`)\"},\"scouting\":{\"$ref\":\"#/definitions/ScoutingOptions\",\"description\":\"How often it looks and with whom (`Scouting`)\"},\"micro\":{\"$ref\":\"#/definitions/MicroOptions\",\"description\":\"How its units fight (`Micro`)\"},\"squads\":{\"$ref\":\"#/definitions/SquadsOptions\",\"description\":\"How its squads go, and the tasks they may be given (`Squads`)\"},\"team\":{\"$ref\":\"#/definitions/TeamOptions\",\"description\":\"How it plays with its team (`Team`)\"},\"waves\":{\"$ref\":\"#/definitions/WavesOptions\",\"description\":\"The waves it was given (`Waves`)\"},\"plans\":{\"$ref\":\"#/definitions/PlansOptions\",\"description\":\"What it builds, trains and researches, and in what order (`Plans`)\"},\"army\":{\"$ref\":\"#/definitions/MeleeArmyOptions\",\"description\":\"The melee army: when it attacks, how many go, how a wave goes (`melee.army`)\"},\"heroes\":{\"$ref\":\"#/definitions/MeleeHeroesOptions\",\"description\":\"The melee player's heroes: how many, and from when (`melee.heroes`)\"},\"lanes\":{\"$ref\":\"#/definitions/LanesPlayOptions\",\"description\":\"A lanes brain's play (`lanes`)\"},\"weights\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"number\"},\"description\":\"How much each goal weighs, by its id (1 when left out): the picker's (`Goals`)\"},\"flags\":{\"$ref\":\"#/definitions/AiFlags\",\"description\":\"The classic flags (see the file's comment)\"},\"brain\":{\"type\":\"string\",\"description\":\"The brain it plays with, by id (one the game lists in `GameModule.brains`): what a race's profile says (`GameAi.races`, campaign plan AR3), for a seat that names none and a map that says none. The game's first brain where nothing says\"},\"difficulty\":{\"type\":\"string\",\"description\":\"The difficulty by name: `easy`, `normal`, `hard`, `insane`, or a game's own (`GameModule.ai.difficulties`). `normal` when left out\"},\"personality\":{\"type\":\"string\",\"description\":\"The personality by name: `balanced`, `rusher`, `turtle`, `expander`, `harasser`, or a game's own. `balanced` when left out\"},\"every\":{\"type\":\"number\",\"description\":\"Seconds between thinks: its reactions\"},\"commandsPerThink\":{\"type\":\"number\",\"description\":\"Commands a think may send at the most, like a person's hands: no limit when left out\"},\"mistakes\":{\"type\":\"number\",\"description\":\"How often it slips, 0 to 1: a think's fights left alone, a wave sent early. Drawn with the brain's own numbers\"},\"vision\":{\"type\":\"string\",\"enum\":[\"fair\",\"all\"],\"description\":\"`fair`: it sees what its seat does. `all`: it sees through the fog (an observer's session), for insane only\"},\"budget\":{\"$ref\":\"#/definitions/ThinkBudgetOptions\",\"description\":\"How much of a think one tick may do (`ThinkBudgetOptions`): modules a tick, units looked at a tick, the milliseconds a module is warned over (4). Nothing sliced when left out: a game sets it where its numbers ask, at many computers\"},\"handicap\":{\"$ref\":\"#/definitions/AiHandicap\",\"description\":\"What it is given beyond the rules, each a factor (1 is none), through the game's hooks while it is seated: `income`, what its workers bring in (1.5: half again; `beforeDeposit`); `health`, the most health of everything it has (`stats`); `damage`, the damage it deals (`beforeDamage`, before armour); `buildTime`, how long what it trains, builds and researches takes (0.8: a fifth sooner; `beforeProduce`). The classic handicap is `health`\"}},\"additionalProperties\":false,\"description\":\"How a computer player plays, as plain data: the settings of its areas (`AiSettings`) and how well it plays, every field optional, layered (see the file's comment). What a difficulty, a personality and a game's defaults give\"},\"EconomyOptions\":{\"type\":\"object\",\"properties\":{\"workers\":{\"type\":\"number\",\"description\":\"Workers on the game's money at each base, where `workersPer` says nothing of it: 8 when left out\"},\"lumberjacks\":{\"type\":\"number\",\"description\":\"Workers on the other resource, at home only, where `workersPer` says nothing of it: 3 when left out\"},\"workersPer\":{\"$ref\":\"#/definitions/WorkersPer\",\"description\":\"Workers on each resource at each base (`WorkersPer`): `{ gold: 5, lumber: 3 }` at every base, or a function of the base for one that differs (home, an expansion). What it leaves out is `workers` and `lumberjacks`\"},\"saturate\":{\"type\":\"boolean\",\"description\":\"Whether a base wants no more workers on a deposit than it serves (see the file's comment): yes when left out\"},\"trainWorkers\":{\"type\":\"boolean\",\"description\":\"Whether its halls train workers up to what the bases want: yes when left out\"},\"builders\":{\"type\":\"number\",\"description\":\"Workers trained past what the bases want, to build with: a worker sent to build is off the mine meanwhile, and a mine with only as many as it serves would gather less. 1 when left out\"},\"expansions\":{\"type\":\"number\",\"description\":\"Halls to have past the first, each by a mine nobody has a hall at: the seat's `expansions`, else none\"},\"expandWhen\":{\"$ref\":\"#/definitions/AiCondition\",\"description\":\"When it may expand: a condition (`AiCondition`, as data or a function). Four minutes in and with a building that trains soldiers, when left out\"},\"workersFlee\":{\"type\":\"boolean\",\"description\":\"Whether workers a raid is on walk to a base no enemy is at, and back once theirs is clear: yes when left out\"}},\"additionalProperties\":false,\"description\":\"How a brain runs its economy (`BrainDef.economy`, the seat's `AiOptions.economy` over it, the `economy()` module's own under both). Every field has a data form a save keeps; the function forms are the brain's to give again\"},\"WorkersPer\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"number\"},\"description\":\"How many workers a base wants on each resource (`EconomyOptions.workersPer`): as numbers by resource id, or a function of the base (`index` 0 is home)\"},\"AiCondition\":{\"$ref\":\"#/definitions/AiConditionData\",\"description\":\"A condition: a question with a yes or no, as a function of the brain, or as plain data (`AiConditionData`)\"},\"AiConditionData\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"all\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/AiCondition\"}}},\"required\":[\"all\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"any\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/AiCondition\"}}},\"required\":[\"any\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"not\":{\"$ref\":\"#/definitions/AiCondition\"}},\"required\":[\"not\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"resource\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"min\":{\"type\":\"number\"},\"max\":{\"type\":\"number\"},\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}},\"required\":[\"resource\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"units\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"min\":{\"type\":\"number\"},\"max\":{\"type\":\"number\"},\"type\":{\"type\":\"string\"},\"role\":{\"$ref\":\"#/definitions/AiRole\"},\"tag\":{\"type\":\"string\"},\"of\":{\"$ref\":\"#/definitions/AiWhose\"},\"near\":{\"anyOf\":[{\"type\":\"string\"},{\"$ref\":\"#/definitions/Vec2\"}]},\"within\":{\"type\":\"number\"},\"finished\":{\"type\":\"boolean\"},\"known\":{\"type\":\"boolean\"}}}},\"required\":[\"units\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"supply\":{\"type\":\"object\",\"properties\":{\"free\":{\"$ref\":\"#/definitions/AiRange\"},\"used\":{\"$ref\":\"#/definitions/AiRange\"},\"cap\":{\"$ref\":\"#/definitions/AiRange\"}},\"additionalProperties\":false}},\"required\":[\"supply\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"time\":{\"type\":\"object\",\"properties\":{\"after\":{\"type\":\"number\"},\"before\":{\"type\":\"number\"}},\"additionalProperties\":false}},\"required\":[\"time\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"underAttack\":{\"anyOf\":[{\"type\":\"boolean\",\"const\":true},{\"type\":\"object\",\"properties\":{\"seconds\":{\"type\":\"number\"},\"near\":{\"anyOf\":[{\"type\":\"string\"},{\"$ref\":\"#/definitions/Vec2\"}]},\"reach\":{\"type\":\"number\"}},\"additionalProperties\":false}]}},\"required\":[\"underAttack\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"seen\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\"},\"role\":{\"$ref\":\"#/definitions/AiRole\"},\"within\":{\"type\":\"number\"}},\"additionalProperties\":false}},\"required\":[\"seen\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"difficulty\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"array\",\"items\":{\"type\":\"string\"}}]}},\"required\":[\"difficulty\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"told\":{\"type\":\"string\"}},\"required\":[\"told\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"chance\":{\"type\":\"number\"}},\"required\":[\"chance\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"hero\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\"},\"alive\":{\"type\":\"boolean\"},\"level\":{\"$ref\":\"#/definitions/AiRange\"}},\"additionalProperties\":false}},\"required\":[\"hero\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"wave\":{\"$ref\":\"#/definitions/AiRange\"}},\"required\":[\"wave\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"objective\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"status\":{\"anyOf\":[{\"$ref\":\"#/definitions/ObjectiveStatus\"},{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/ObjectiveStatus\"}}]}},\"required\":[\"id\",\"status\"],\"additionalProperties\":false}},\"required\":[\"objective\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"report\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"min\":{\"type\":\"number\"},\"max\":{\"type\":\"number\"},\"path\":{\"type\":\"string\"},\"equals\":{}},\"required\":[\"path\"]}},\"required\":[\"report\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"component\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"min\":{\"type\":\"number\"},\"max\":{\"type\":\"number\"},\"id\":{\"type\":\"string\"},\"entity\":{\"type\":\"string\"},\"type\":{\"type\":\"string\"},\"path\":{\"type\":\"string\"},\"equals\":{}},\"required\":[\"id\"]}},\"required\":[\"component\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"region\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"min\":{\"type\":\"number\"},\"max\":{\"type\":\"number\"},\"name\":{\"type\":\"string\"},\"of\":{\"$ref\":\"#/definitions/AiWhose\"},\"type\":{\"type\":\"string\"},\"role\":{\"$ref\":\"#/definitions/AiRole\"}},\"required\":[\"name\"]}},\"required\":[\"region\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"is\":{\"type\":\"string\"},\"args\":{\"type\":\"object\",\"additionalProperties\":{}}},\"required\":[\"is\"],\"additionalProperties\":false}],\"description\":\"A condition as plain data: what a map's file, a mission's JSON or the editor holds. Each form is one question; the table in `docs/ai.md` says what each asks\"},\"AiWhose\":{\"type\":\"string\",\"enum\":[\"mine\",\"enemy\",\"ally\"],\"description\":\"Whose units a condition counts: the player’s own (the default), its rivals’ in sight, its allies’\"},\"Vec2\":{\"type\":\"object\",\"properties\":{\"x\":{\"type\":\"number\"},\"z\":{\"type\":\"number\"}},\"required\":[\"x\",\"z\"],\"additionalProperties\":false,\"description\":\"A point on the ground plane. The simulation is 2D; height always comes from the terrain.\"},\"AiRange\":{\"type\":\"object\",\"properties\":{\"min\":{\"type\":\"number\"},\"max\":{\"type\":\"number\"}},\"additionalProperties\":false,\"description\":\"At least `min` and at most `max`, each left out for no bound: a count, an amount, a level\"},\"ObjectiveStatus\":{\"type\":\"string\",\"enum\":[\"open\",\"done\",\"failed\"]},\"DefenceOptions\":{\"type\":\"object\",\"properties\":{\"radius\":{\"type\":\"number\",\"description\":\"How near one of its buildings a rival's unit is an intruder: the seat's `defendRadius`, else 16\"},\"odds\":{\"type\":\"number\",\"description\":\"The odds of the fight (`Strength.odds`) the defenders sent against a raid are to have: two in three when left out (about four tenths stronger than the raid)\"},\"recall\":{\"type\":\"boolean\",\"description\":\"Whether a raid more than what is at home can meet brings the army home (the goal is urgent: it takes squads out, whole, once it scores past what they are on): yes when left out\"},\"towers\":{\"type\":\"number\",\"description\":\"Towers a base wants once raided `towerAfter` times: 1 when left out. 0 for none\"},\"towerAfter\":{\"type\":\"number\",\"description\":\"Raids at one base before it wants towers: 2 when left out\"},\"tower\":{\"type\":\"string\",\"description\":\"The tower to build, by type id: the cheapest of the `tower` role its workers build when left out\"},\"quiet\":{\"type\":\"number\",\"description\":\"Seconds a base stands clear before a raid on it is over: 15 when left out\"}},\"additionalProperties\":false,\"description\":\"How a brain defends its bases (`BrainDef.defence`, the seat's `AiOptions.defence` over it, the `defence()` module's own under both). Data, kept in a save\"},\"ScoutingOptions\":{\"type\":\"object\",\"properties\":{\"lookEvery\":{\"type\":\"number\",\"description\":\"Seconds a place stays out of sight before it is worth a look again: 90 when left out\"},\"scout\":{\"type\":\"boolean\",\"const\":false,\"description\":\"Where to look: `false` nowhere (the goal is left out), a function for places of the game's own each think (in place of the kit's plan). The kit's plan when left out\"},\"expansions\":{\"type\":\"boolean\",\"description\":\"Whether the mines they would expand to are looked at: yes when left out\"},\"army\":{\"type\":\"boolean\",\"description\":\"Whether where their army was last seen is looked at again: yes when left out\"},\"flyers\":{\"type\":\"boolean\",\"description\":\"Whether a flying unit with nothing to do goes before the fastest on foot: yes when left out\"},\"from\":{\"type\":\"number\",\"description\":\"Seconds into the game before the first look: 15 when left out\"}},\"additionalProperties\":false,\"description\":\"How a brain scouts (`BrainDef.scouting`, the seat's `AiOptions.scouting` over it, the `scouting()` module's own under both). Data, but for the function form of `scout`\"},\"MicroOptions\":{\"type\":\"object\",\"properties\":{\"targets\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/TargetRule\"},\"description\":\"Who is worth hitting first, as ranked rules (`TargetRule`), or a function that picks (`TargetPicker`). The kit's when left out: casters 2, heroes 1.5 (with `targetHeroes`), siege 1, healers 2, over the type's declared `UnitTypeDef.ai.priority`; among those alike, the weakest, the nearest\"},\"targetHeroes\":{\"type\":\"boolean\",\"description\":\"Whether heroes are worth hitting first (the classic Target Heroes): yes when left out\"},\"focusEvery\":{\"type\":\"number\",\"description\":\"Seconds a squad keeps hitting the rival it was told to before another is chosen: 4\"},\"kite\":{\"anyOf\":[{\"type\":\"boolean\"},{\"type\":\"string\",\"const\":\"ranged\"}],\"description\":\"Whether ranged units step back from the melee closing on them, when they are the faster: `'ranged'` (or `true`) when left out, `false` never\"},\"unitsFlee\":{\"anyOf\":[{\"type\":\"number\"},{\"type\":\"boolean\",\"const\":false}],\"description\":\"The share of its health under which a unit leaves the fight for safety (the classic Units Flee): 0.25. `false` never\"},\"heroesFlee\":{\"anyOf\":[{\"type\":\"number\"},{\"type\":\"boolean\",\"const\":false}],\"description\":\"The same for heroes (Heroes Flee): 0.35. `false` never\"},\"returnAt\":{\"type\":\"number\",\"description\":\"The share of its health at which a unit that fled is back: 0.8\"},\"removeInjuries\":{\"type\":\"boolean\",\"description\":\"Whether the hurt go home to heal between fights, not only out of one (Remove Injuries): no when left out\"},\"ignoreInjured\":{\"type\":\"boolean\",\"description\":\"Whether a unit that fled stays out until healed, however long (Ignore Injured): no when left out, so one nothing heals (or that heals too slowly to wait a minute for) comes back once safe\"},\"spells\":{\"anyOf\":[{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/SpellAim\"}},{\"type\":\"boolean\",\"const\":false}],\"description\":\"Where to use abilities, by ability id (`SpellAim`): before the ability's own `ai`, and before what the kit works out from its effects. `false` casts nothing: what is on autocast looks after itself\"},\"potions\":{\"type\":\"boolean\",\"description\":\"Whether a potion is drunk when it is needed: yes when left out\"},\"takeItems\":{\"type\":\"boolean\",\"description\":\"Whether what lies about is picked up (Take Items): yes when left out\"},\"buyItems\":{\"type\":\"boolean\",\"description\":\"Whether heroes buy from a shop in reach (Buy Items): yes when left out, where there is a shop and a list\"},\"shopping\":{\"anyOf\":[{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/ShoppingItem\"}},{\"type\":\"boolean\",\"const\":false}],\"description\":\"What a hero shops for, in order (`ShoppingItem`), or a function that names the item a hero should buy next (none for nothing). The cheapest item whose ability heals, one of it, when left out; `false` nothing\"},\"hireUnits\":{\"type\":\"boolean\",\"description\":\"Whether it hires units at a place that hires in reach (a tavern, a mercenary camp: `UnitTypeDef.hires`): yes when left out, where there is one\"},\"hiring\":{\"anyOf\":[{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/HiringItem\"}},{\"type\":\"boolean\",\"const\":false}],\"description\":\"What it hires, in order (`HiringItem`), or a function that names the unit to hire next of those a place offers (none for nothing). When left out: no hero, the strongest soldier for its price, with money to spare (150 past it) and supply free, four at the most. `false` nothing\"},\"smartArtillery\":{\"type\":\"boolean\",\"description\":\"Whether siege units shoot buildings when they may and stay behind the squad (Smart Artillery): yes when left out\"},\"heroes\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/HeroPlan\"},\"description\":\"The heroes to make, in order (`HeroPlan`: which, its skills, its items, a chance), or a function that picks the next of those that could be made. The first that can be made, and the ultimate first, when left out\"}},\"additionalProperties\":false,\"description\":\"How a brain fights (`BrainDef.micro`, the seat's `AiOptions.micro` over it, the `micro()` module's own under both): whom it hits first, whether its ranged units kite, when its units flee, how it casts, what it buys, how its siege is used, which heroes it makes. Every field has a data form a save keeps; the function forms are the brain's to give again\"},\"TargetRule\":{\"type\":\"object\",\"properties\":{\"match\":{\"type\":\"string\",\"description\":\"What it picks out: a role (`caster`, `hero`, `siege`, `worker`, `healer`), `building`, a tag (`ranged`, the game's own), or a unit type by id\"},\"weight\":{\"type\":\"number\",\"description\":\"What it adds to the worth of what it picks out: 1 when left out. Rules add up; a foe no rule names is worth 1\"},\"when\":{\"$ref\":\"#/definitions/AiCondition\",\"description\":\"When it counts: a condition (`AiCondition`, as data or a function). Always when left out\"}},\"required\":[\"match\"],\"additionalProperties\":false,\"description\":\"One rule of who is worth hitting first (`MicroOptions.targets`): what it picks out, how much it adds, when it counts. Plain data\"},\"SpellAim\":{\"not\":{},\"description\":\"Where to use an ability, in code (`MicroOptions.spells`, by ability id): where (`point`) and on whom (`target`), `null` for not now, `undefined` to leave it to what the kit works out from the effects. `foes` are the enemies within the ability's reach of the caster, `friends` its own and its allies' fighters near it\"},\"ShoppingItem\":{\"type\":\"object\",\"properties\":{\"item\":{\"type\":\"string\",\"description\":\"The item, by id\"},\"count\":{\"type\":\"number\",\"description\":\"How many of it to carry: 1 when left out\"},\"when\":{\"$ref\":\"#/definitions/AiCondition\",\"description\":\"When it is wanted: a condition. Always when left out\"}},\"required\":[\"item\"],\"additionalProperties\":false,\"description\":\"A line of a hero's shopping list (`MicroOptions.shopping`, `HeroPlan.items`): an item to carry, how many, when\"},\"HiringItem\":{\"type\":\"object\",\"properties\":{\"unit\":{\"type\":\"string\",\"description\":\"The unit type, by id: one a place that hires lists (`UnitTypeDef.hires`)\"},\"count\":{\"type\":\"number\",\"description\":\"How many of it to have: 1 when left out\"},\"when\":{\"$ref\":\"#/definitions/AiCondition\",\"description\":\"When it is wanted: a condition. Always when left out\"}},\"required\":[\"unit\"],\"additionalProperties\":false,\"description\":\"A line of what a brain hires (`MicroOptions.hiring`): a unit a place hires out, how many of it to have, when\"},\"HeroPlan\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"description\":\"The hero's unit type\"},\"skills\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"The order its skills are learned in, each level a line (`['stomp', 'holyLight', 'stomp', …]`); past the end, or left out, the ultimate as soon as it can be had and then whatever is furthest behind. Or a function: the skill to learn next of those it may, by id\"},\"items\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/ShoppingItem\"},\"description\":\"What it shops for, in place of the brain's `shopping`\"},\"chance\":{\"type\":\"number\",\"description\":\"The chance it is made at all, 0 to 1 (1 when left out): rolled once, with the brain's numbers, and remembered\"}},\"required\":[\"type\"],\"additionalProperties\":false,\"description\":\"A hero the brain means to have (`MicroOptions.heroes`): which, what it learns in what order, what it buys\"},\"SquadsOptions\":{\"type\":\"object\",\"properties\":{\"spread\":{\"type\":\"number\",\"description\":\"How far from its middle a unit may be before it counts as strayed: what a regroup brings in. Grows with the squad when left out (8, and three times the square root of its size above that: 12 for sixteen units)\"},\"regroupWhen\":{\"anyOf\":[{\"type\":\"number\"},{\"type\":\"boolean\",\"const\":false}],\"description\":\"The share of its units strayed (`spread`) at which it stops to regroup, on its way and out of a fight: 0.4. `false` never\"},\"reinforce\":{\"type\":\"boolean\",\"description\":\"Whether it makes up what it lost since its task began from the pool (the idle fighters nothing holds): no when left out\"},\"retreatAt\":{\"anyOf\":[{\"type\":\"number\"},{\"type\":\"boolean\",\"const\":false}],\"description\":\"Its odds in a fight (`Strength.odds`) under which it falls back home, away from home: 0.3. `false` fights on to the last (a mission's guards)\"},\"home\":{\"type\":\"number\",\"description\":\"How near home it fights on whatever the odds: there is nowhere to fall back to. 20 when left out\"},\"micro\":{\"type\":\"boolean\",\"description\":\"Whether the micro module (`micro()`, C2) fights for it once it is in a fight: focus fire, kiting, fleeing, siege. Yes when left out; `false` for a squad left to its units (a scout, a mission's guards on an order of the game's)\"},\"tasks\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/TaskDef\"},\"description\":\"Tasks of the game's own: registered with the brain, so that a squad on one is carried on after a load\"}},\"additionalProperties\":false,\"description\":\"How a brain's squads behave, where a squad says nothing for itself (`BrainDef.squads`, the seat's `AiOptions.squads` over it); and the tasks of the game's own (`defineTask`) a save of the brain may name\"},\"TaskDef\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"description\":\"Unique among the brain's tasks: what a save names it by. The kit's are words (`attack`); a game's prefixed (`ctf.capture`)\"},\"handles\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"retreat\",\"regroup\",\"reinforce\"]},\"description\":\"What the squads module leaves to it: a task that regroups in its own way, or retreats never\"},\"standing\":{\"type\":\"boolean\",\"description\":\"Whether it is a standing duty, going on as long as its squad has it (a hero walking a lane with its waves): never stuck, whatever its phase, for the `ends` invariant (`checkBrain`). No when left out\"},\"fighting\":{\"type\":\"boolean\",\"description\":\"Whether a squad on it is in a fight it means to win, which the micro fights for (`ai.area(Micro)`): yes when left out. No for a task that goes somewhere, looks, stands, gathers or falls back (the kit's `move`, `scout`, `hold`, `gather`, `retreat`, and the micro's own `flee`)\"}},\"required\":[\"id\"],\"additionalProperties\":false,\"description\":\"A kind of task (`defineTask`): what it sends as it begins (`start`), what it looks at each think (`think`), and the place it is about (`where`, for the squads module's regroup and reinforcements and for the report). The squads module runs it with its own regroup, reinforce and retreat around it, but for what it `handles` itself\"},\"TeamOptions\":{\"type\":\"object\",\"properties\":{\"defendUsers\":{\"type\":\"boolean\",\"description\":\"Whether it helps its allies, people and computers, where asked (the classic Defend Users): yes when left out\"},\"answerPings\":{\"type\":\"boolean\",\"description\":\"Whether it does what an ally's ping asks besides help (`GameRules.talk.pings.meanings`): a squad sent to fight at an `attack` (a push), one to hold a place at a `defend` (a hold), its squads out brought home at a `rally` (a retreat). Yes when left out\"},\"helpOdds\":{\"type\":\"number\",\"description\":\"The odds of the fight (`Strength.odds`) what it sends to help is to have: one in two when left out\"},\"askForHelp\":{\"type\":\"boolean\",\"description\":\"Whether it asks its team for help with a raid more than it can meet: yes when left out\"},\"together\":{\"type\":\"boolean\",\"description\":\"Whether its waves wait for the team's plan and go together: no when left out\"},\"gatherFor\":{\"type\":\"number\",\"description\":\"Seconds ahead of the proposal the waves of a plan go: 20 when left out\"},\"requestFor\":{\"type\":\"number\",\"description\":\"Seconds a request stands: 30 when left out\"},\"say\":{\"type\":\"boolean\",\"description\":\"Whether it says a line to its side as it sends a wave, comes to help, falls back: no when left out\"},\"lines\":{\"type\":\"object\",\"properties\":{\"attacking\":{\"type\":\"string\"},\"helping\":{\"type\":\"string\"},\"retreating\":{\"type\":\"string\"}},\"additionalProperties\":false,\"description\":\"Its lines, the game's words: `{ attacking, helping, retreating }`. Nothing is said for a line not given\"}},\"additionalProperties\":false,\"description\":\"How a brain plays with its team (`BrainDef.team`, the seat's `AiOptions.team` over it, the `team()` module's own under both). Data\"},\"WavesOptions\":{\"type\":\"object\",\"properties\":{\"groups\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/AttackGroup\"},\"description\":\"The groups waves are made of, by name\"},\"waves\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/AttackWave\"},\"description\":\"The waves, in order, or a function giving the wave whose turn it is (by its index), none for no more\"},\"train\":{\"type\":\"boolean\",\"description\":\"Whether a wave's group is trained (wants of the production plan): yes when left out. No for units that are given (a lanes game)\"},\"priority\":{\"type\":\"number\",\"description\":\"How important a wave's units are to the purse, as wants: 30 when left out (under a hall, a barracks, a hero)\"}},\"additionalProperties\":false,\"description\":\"How a brain sends its waves (`BrainDef.waves`; the seat's `AiOptions.waves` and `groups`, the data of it, go over)\"},\"AttackGroup\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"description\":\"Its name: what a wave names it by\"},\"units\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"number\"},\"description\":\"So many of each unit type or role: `{ footman: 4, rifleman: 2 }`\"},\"when\":{\"$ref\":\"#/definitions/AiCondition\",\"description\":\"Only while this holds: a wave of a group whose condition fails is skipped as its own would be\"}},\"required\":[\"id\",\"units\"],\"additionalProperties\":false,\"description\":\"What a wave is made of: so many of each unit type (by id) or role (`caster`, a game's own), by name, under a condition\"},\"AttackWave\":{\"type\":\"object\",\"properties\":{\"group\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"object\",\"additionalProperties\":{\"type\":\"number\"}}],\"description\":\"Its group: a group's name (`WavesOptions.groups`), or the units in place (`{ footman: 4 }`)\"},\"after\":{\"type\":\"number\",\"description\":\"Seconds after the wave before it (after the start, for the first) before it may go: 0 when left out\"},\"minForces\":{\"type\":\"number\",\"description\":\"The share of its group that is enough to go with, 0 to 1: all of it when left out\"},\"gatherFor\":{\"type\":\"number\",\"description\":\"Seconds it gathers at the rally before it goes, whoever is late: 15 when left out\"},\"repeat\":{\"type\":[\"boolean\",\"number\"],\"description\":\"Whether it goes again once sent: `true` for ever, a number for so many times in all. Once when left out\"},\"when\":{\"$ref\":\"#/definitions/AiCondition\",\"description\":\"Only when this holds as its turn comes: a wave whose condition fails is skipped\"},\"together\":{\"type\":\"boolean\",\"description\":\"Whether it waits for its team's attack together (`TeamOptions.together`, C5)\"},\"target\":{\"$ref\":\"#/definitions/WaveTarget\",\"description\":\"Where it goes: their base nearest home when left out\"},\"rally\":{\"anyOf\":[{\"$ref\":\"#/definitions/Vec2\"},{\"type\":\"string\"}],\"description\":\"Where it gathers: a place, or a name the map or the rules gave. A little out of home towards the target when left out\"}},\"required\":[\"group\"],\"additionalProperties\":false,\"description\":\"One wave (the classic attack wave): its group, when it goes, how much of it is enough, how long it gathers, whether it goes again, together or not, and where to\"},\"WaveTarget\":{\"anyOf\":[{\"type\":\"string\",\"const\":\"nearestBase\"},{\"type\":\"string\",\"const\":\"weakestBase\"},{\"type\":\"string\",\"const\":\"expansion\"},{\"type\":\"string\",\"const\":\"army\"},{\"$ref\":\"#/definitions/Vec2\"}],\"description\":\"Where a wave goes: their base nearest home, their weakest (the fewest towers and army seen there), an expansion of theirs, their army as last seen, a place, or a function of the brain\"},\"PlansOptions\":{\"type\":\"object\",\"properties\":{\"build\":{\"$ref\":\"#/definitions/BuildPlanOptions\"},\"production\":{\"$ref\":\"#/definitions/ProductionPlanOptions\"},\"order\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/BuildOrderItem\"},\"description\":\"What to have first, in order (`BuildOrderItem`): each item waits for the one before, and nothing else the computer would make of itself is made meanwhile (`following`); what it works out for itself once done. The seat's `AiOptions.buildOrder`\"}},\"additionalProperties\":false,\"description\":\"How a brain plans (`BrainDef.plans`): the build plan's options, the production plan's, the build order\"},\"BuildPlanOptions\":{\"type\":\"object\",\"properties\":{\"wants\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/Want\"},\"description\":\"Standing wants: what the brain always has, built anew when lost\"},\"placement\":{\"$ref\":\"#/definitions/PlacementTable\",\"description\":\"How each type's spot is chosen, by type id or by role: a name or a function. What a type declares (`UnitTypeDef.ai.placement`) goes over it\"},\"rebuild\":{\"type\":\"boolean\",\"description\":\"Whether a building lost is built again, for standing wants that do not say: yes when left out\"},\"repair\":{\"type\":\"boolean\",\"description\":\"Whether a worker is sent to mend a damaged building when no enemy reaches it: yes when left out\"},\"siteTimeout\":{\"type\":\"number\",\"description\":\"Seconds without a spot before a want that does not stand is given up: 60 when left out\"},\"within\":{\"type\":\"number\",\"description\":\"How far from home a building goes at the most, where a want does not say: 22 when left out\"}},\"additionalProperties\":false,\"description\":\"How the build plan goes about it (`BrainDef.plans.build`, the `build()` module; the data of it in `AiOptions`)\"},\"Want\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"description\":\"Its name: what the memory, the report and `drop` know it by. Made up when left out\"},\"type\":{\"type\":\"string\",\"description\":\"The unit type wanted: a building, or a unit (whose buildings the plan puts up; the production plan trains it)\"},\"within\":{\"type\":\"number\",\"description\":\"How far from there at the most: 22 when left out\"},\"priority\":{\"type\":\"number\",\"description\":\"Wants are taken most important first, and the most important one that cannot be paid for saves up: 0 when left out\"},\"spare\":{\"type\":\"number\",\"description\":\"Money to have left over after paying for it: a luxury that waits for gold to spare. 0 when left out\"},\"keep\":{\"type\":\"boolean\",\"description\":\"Whether the want stands: kept once met, and short again when what it made is lost, so that it is built anew. Wants of a brain's or a seat's options stand; one added as the game goes is dropped once met, unless it says so\"},\"rebuild\":{\"type\":\"boolean\",\"description\":\"For a standing want: whether a building lost is built again (the plan's `rebuild` when left out)\"},\"siteTimeout\":{\"type\":\"number\",\"description\":\"Seconds without a spot for it before a want that does not stand is given up: the plan's `siteTimeout` when left out\"},\"by\":{\"type\":\"string\",\"description\":\"Who wants it, for the report: `order`, `production`, `economy`, a goal's or a rule's id\"},\"count\":{\"type\":\"number\"},\"when\":{\"$ref\":\"#/definitions/AiCondition\"},\"near\":{\"anyOf\":[{\"$ref\":\"#/definitions/Vec2\"},{\"type\":\"string\"}]},\"placement\":{\"$ref\":\"#/definitions/PlacementName\"}},\"additionalProperties\":false,\"required\":[\"type\"],\"description\":\"A want with the function forms: how many as a function of the brain, a condition as code, a place found each think, a placement strategy of the game's own (`ai.plans.want`, `BuildPlanOptions.wants`)\"},\"PlacementTable\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#/definitions/PlacementName\"},\"description\":\"Which strategy a type goes by (`AiOptions.placement`, `BuildPlanOptions.placement`): by type id or by role, a name or a function\"},\"ProductionPlanOptions\":{\"type\":\"object\",\"properties\":{\"composition\":{\"$ref\":\"#/definitions/Composition\",\"description\":\"The army it wants (`Composition`), or a function of the brain giving it. Even shares of every soldier type it can train when left out\"},\"counters\":{\"type\":\"number\",\"description\":\"How much what answers the enemy's army seen pulls the shares about: 0 not at all, 1 (left out) as much as a share; a type that cannot hit what they field is not trained while they field it\"},\"train\":{\"type\":\"boolean\",\"description\":\"Whether it trains soldiers at all: yes when left out. Workers and heroes are the economy's and the hero module's\"},\"queue\":{\"type\":\"number\",\"description\":\"How many may queue at one building: 2 when left out\"},\"supplyAhead\":{\"type\":\"boolean\",\"description\":\"Whether to want one more of what provides supply when what it would train next does not fit: yes when left out\"},\"supplyMargin\":{\"type\":\"number\",\"description\":\"Supply to keep free beyond the next unit's, so that the farm is up before it is needed: 4 when left out\"},\"research\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Upgrades to research, in order, by id (the first it can is next), or a function of the brain giving the next; what the army it has uses most when left out\"},\"researchSpare\":{\"type\":\"number\",\"description\":\"Money to have left over before researching: 200 when left out (research is a luxury)\"},\"priority\":{\"type\":\"number\",\"description\":\"How important training and research are to the purse (`Purse`): 0 when left out, under every want that saves\"},\"upkeepTier\":{\"anyOf\":[{\"type\":\"number\"},{\"type\":\"boolean\",\"const\":false}],\"description\":\"The tier of the game's upkeep (`GameModule.upkeep`: 0 none, 1 the first) the composition trains no further into: a soldier that would take the supply used past it waits, and no supply is wanted past it, unless the money would lie idle (`upkeepSpare`). What is wanted outright (a wave's, a build order's) is trained all the same. `UPKEEP_TIER` (1) when left out; `false` reads no upkeep\"},\"upkeepSpare\":{\"type\":\"number\",\"description\":\"Money free past which upkeep is not read (it would lie idle else): `UPKEEP_SPARE` (600) when left out\"}},\"additionalProperties\":false,\"description\":\"How the production plan goes about it (`BrainDef.plans.production`, the `production()` module; the data of it in `AiOptions.composition` and `research`)\"},\"Composition\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"number\"},\"description\":\"The army a computer player wants, as shares by unit type id or tag (the kit's `ranged`, `melee`, `hitsAir`, a game's own): `{ footman: 2, rifleman: 1 }` is two footmen to a rifleman. A tag's share is split among the types that have it; a type named gets its own beside. Types it cannot train yet are left out until it can\"},\"BuildOrderItem\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"type\"],\"additionalProperties\":false}],\"description\":\"One item of a build order: a type to have (one of it), or a type and how many. Counts of a type add up along the order\"},\"MeleeArmyOptions\":{\"type\":\"object\",\"properties\":{\"firstAttack\":{\"type\":\"number\",\"description\":\"Seconds before its first wave: 150 when left out\"},\"attackEvery\":{\"type\":\"number\",\"description\":\"Seconds between two waves: 45 when left out\"},\"waveSize\":{\"type\":\"number\",\"description\":\"Units in its first wave, each after bringing two more: 6 when left out\"},\"goOdds\":{\"anyOf\":[{\"type\":\"number\"},{\"type\":\"boolean\",\"const\":false}],\"description\":\"The odds (`Strength.odds`) of what it has ready against their army as last seen (the last minute) under which a wave whose time has come waits and trains on rather than go: 0.4 when left out; `false` never waits\"},\"waitAtMost\":{\"type\":\"number\",\"description\":\"Seconds a wave waits past its time at the most, outmatched or short of its size: then it goes with what it has, half the wave at the least, where that would hold against what is remembered to reach the target. Twice `attackEvery` when left out. A wave that could never reach its size (the supply capped) still comes\"},\"pressOn\":{\"type\":\"boolean\",\"description\":\"Whether a wave that razed what it went for goes on to the next of their buildings it knows, while it is strong enough: yes when left out\"},\"target\":{\"type\":\"string\",\"enum\":[\"nearest\",\"weakest\"],\"description\":\"Where a wave of its own goes among their buildings it knows of: the `nearest`, or the `weakest` (the one the least of their strength is remembered to reach: an expansion before a main with towers; the nearest among those alike). `weakest` when left out\"},\"counter\":{\"type\":\"boolean\",\"description\":\"Whether a wave of theirs that died at its gate is answered at once with what is ready (half a wave at the least), their base being empty of it: yes when left out. `counterAfter` is how many of their fighters must have fallen near its bases in the last twenty seconds (3 when left out)\"},\"counterAfter\":{\"type\":\"number\"},\"pressOdds\":{\"type\":\"number\",\"description\":\"The odds against their army as last seen a wave needs to go on: 0.5 when left out\"},\"reinforce\":{\"type\":\"boolean\",\"description\":\"Whether idle fighters at home join a wave that is out, making up what it lost: yes when left out\"},\"strike\":{\"anyOf\":[{\"type\":\"boolean\",\"const\":false},{\"type\":\"object\",\"properties\":{\"awayFor\":{\"type\":\"number\"},\"odds\":{\"type\":\"number\"}},\"additionalProperties\":false}],\"description\":\"A strike while their army is away (FJ1): every sighting of their army within `awayFor` seconds (60) stands far from what a wave would go for (their base, out raiding or creeping or at a base of mine), and what is ready (half a wave at the least) would hold against what is remembered to reach it at these odds (`odds`, 0.6): the wave goes before its time. `false` for none\"},\"waveGrowth\":{\"type\":\"number\",\"description\":\"Units more in each wave than the one before: 2 when left out\"},\"waveMax\":{\"type\":\"number\",\"description\":\"The most units a wave of its own asks for: 24 when left out\"},\"raid\":{\"anyOf\":[{\"type\":\"boolean\",\"const\":false},{\"type\":\"object\",\"properties\":{\"size\":{\"type\":\"number\"},\"odds\":{\"type\":\"number\"},\"every\":{\"type\":\"number\"},\"armyAwayFor\":{\"type\":\"number\"},\"workersSeenFor\":{\"type\":\"number\"},\"retreatAt\":{\"type\":\"number\"}},\"additionalProperties\":false}],\"description\":\"Raids on their workers where their army is not (the `raid` goal): how many go (`size`, 3, the fastest), the odds against what is remembered at the base they need (`odds`, 0.6), seconds between raids (`every`, 60), seconds their army must not have been seen near the base (`armyAwayFor`, 45), how long ago their workers were seen at the most (`workersSeenFor`, 120), the odds the raiders fall back at (`retreatAt`, 0.5). `false` for no raids\"},\"harass\":{\"anyOf\":[{\"type\":\"boolean\",\"const\":false},{\"type\":\"object\",\"properties\":{\"size\":{\"type\":\"number\"},\"odds\":{\"type\":\"number\"},\"every\":{\"type\":\"number\"},\"retreatAt\":{\"type\":\"number\"}},\"additionalProperties\":false}],\"description\":\"Harassing their main before the first wave (the `harass` goal): the party (`size`, 3: the hero and the fastest), the odds it needs at their base (`odds`, 0.6), seconds between (`every`, 90), the odds it falls back at (`retreatAt`, 0.5). `false` for none\"},\"expand\":{\"anyOf\":[{\"type\":\"boolean\",\"const\":false},{\"type\":\"object\",\"properties\":{\"guards\":{\"type\":\"number\"},\"spare\":{\"type\":\"number\"}},\"additionalProperties\":false}],\"description\":\"Guards at a base past home that has no tower yet (the `expand` goal): how many (`guards`, 2), and how many fighters past a wave must stand idle for any to go (`spare`, 0). `false` for none\"},\"creep\":{\"anyOf\":[{\"type\":\"boolean\",\"const\":false},{\"type\":\"object\",\"properties\":{\"every\":{\"type\":\"number\"},\"from\":{\"type\":\"number\"},\"reach\":{\"type\":\"number\"},\"party\":{\"type\":\"number\"}},\"additionalProperties\":false}],\"description\":\"Creeping (the `creep` goal): seconds between camps (`every`, 40), seconds into the game before the first (`from`, 30), how far from home a camp may be (`reach`, 70), the fewest in a party (`party`, 3). `false` for none\"},\"surrender\":{\"anyOf\":[{\"type\":\"boolean\",\"const\":false},{\"$ref\":\"#/definitions/MeleeSurrenderOptions\"}],\"description\":\"When it gives up (the `melee.surrender` module, `MeleeSurrenderOptions`): once it has no hall, no worker and no army that could win the game back, for half a minute, when left out; a condition of the game's own (`when`), and how long it holds first (`after`). `false` plays to the last\"}},\"additionalProperties\":false,\"description\":\"How the melee player's army goes about its own waves and the goals beside them (`AiSettings.army` over the melee brain's `army(options)`). Data: a save keeps the seat's\"},\"MeleeSurrenderOptions\":{\"type\":\"object\",\"properties\":{\"when\":{\"$ref\":\"#/definitions/AiCondition\",\"description\":\"When it cannot come back, as a condition (data or a function of the brain). By default: no hall and no worker of its own, and its fighters (none at all, or) outmatched by their army as last seen in the last two minutes, odds under 0.2\"},\"after\":{\"type\":\"number\",\"description\":\"Seconds the condition holds on end before it gives up: 30 when left out\"}},\"additionalProperties\":false,\"description\":\"When the melee player gives up (`MeleeArmyOptions.surrender`): the condition, and how long it holds first\"},\"MeleeHeroesOptions\":{\"type\":\"object\",\"properties\":{\"most\":{\"type\":\"number\",\"description\":\"Heroes it has once the game is under way (`secondFrom`): one before. 2 when left out\"},\"secondFrom\":{\"type\":\"number\",\"description\":\"Seconds into the game from which it wants every hero it may have: one before. 240 when left out\"},\"priority\":{\"type\":\"number\",\"description\":\"How important a hero's price is to the purse while it is short (`Purse.reserve`): the hall, supply, the barracks, the altar, the answer to their army and another hall go before it (their wants are more important); a workshop, the next tier, a second barracks and the soldiers wait for it. 35 when left out\"}},\"additionalProperties\":false,\"description\":\"How the melee brain makes its heroes (`heroes()` of the melee brain): how many once the game is under way (the profile's `heroes`), from when, and how important their price is to the purse\"},\"LanesPlayOptions\":{\"type\":\"object\",\"properties\":{\"home\":{\"$ref\":\"#/definitions/Vec2\",\"description\":\"Where its heroes go to rest: a point, or a function. Its side's building by its side's ends of the lanes when left out (a core)\"},\"lanes\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"Only these lanes, by their path's name: every lane of the map when left out\"},\"backAt\":{\"type\":\"number\",\"description\":\"The share of its health under which a hero goes home to rest: 0.35\"},\"returnAt\":{\"type\":\"number\",\"description\":\"The share at which a resting hero goes back out: 0.85\"},\"restFor\":{\"type\":\"number\",\"description\":\"Seconds a hero rests at home at most, healed or not: 40\"},\"recall\":{\"type\":\"boolean\",\"description\":\"Whether a hero going home uses an ability that takes it there (a recall), where nothing hostile is near: yes when left out\"},\"backOdds\":{\"type\":\"number\",\"description\":\"The odds where a hero stands on its lane under which it falls back along it: 0.3\"},\"behind\":{\"type\":\"number\",\"description\":\"How many cells behind its side's wave a hero walks: 3\"},\"help\":{\"anyOf\":[{\"type\":\"boolean\",\"const\":false},{\"type\":\"object\",\"properties\":{\"reach\":{\"type\":\"number\"},\"odds\":{\"type\":\"number\"}},\"additionalProperties\":false}],\"description\":\"An ally's hero in a fight it goes to: within `reach` cells of it (30), where the odds with it are at least `odds` (0.4). `false` never\"},\"camps\":{\"anyOf\":[{\"type\":\"boolean\",\"const\":false},{\"type\":\"object\",\"properties\":{\"reach\":{\"type\":\"number\"},\"odds\":{\"type\":\"number\"}},\"additionalProperties\":false}],\"description\":\"A camp it takes with no wave to walk with: within `reach` cells (50), where the odds against it are at least `odds` (0.6). `false` never\"},\"lastHits\":{\"type\":\"boolean\",\"description\":\"Whether a hero hits the minion its next hit kills before anything else (a last hit): yes when left out\"}},\"additionalProperties\":false,\"description\":\"How a lanes brain plays (`lanesPlay(options)`, the seat's `AiOptions.lanes` over it): its heroes' rest, how far it goes for an ally or a camp, how near its wave it walks. Data, but for `units` and `home` as functions\"},\"AiFlags\":{\"type\":\"object\",\"properties\":{\"defendUsers\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Helps its allies, people and computers, where asked (the team's `help` goal)\"},\"targetHeroes\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Hits heroes first among those alike (the micro's targets)\"},\"repairStructures\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Mends damaged buildings when no enemy reaches them (the build plan's `repair`)\"},\"heroesFlee\":{\"$ref\":\"#/definitions/AiFlag%3C(boolean%7Cnumber)%3E\",\"description\":\"A hero leaves a fight under this share of its health: `true` at the share under it, `false` never (`micro.heroesFlee`)\"},\"unitsFlee\":{\"$ref\":\"#/definitions/AiFlag%3C(boolean%7Cnumber)%3E\",\"description\":\"A unit leaves a fight under this share of its health: `true` at the share under it, `false` never (`micro.unitsFlee`)\"},\"groupsFlee\":{\"$ref\":\"#/definitions/AiFlag%3C(boolean%7Cnumber)%3E\",\"description\":\"A squad falls back from a fight away from home under these odds: `true` at the odds under it, `false` never (`squads.retreatAt`)\"},\"noMercy\":{\"$ref\":\"#/definitions/AiFlag%3C(boolean%7Cnumber)%3E\",\"description\":\"Attacks before its time when the enemy's army as last seen is weak against its own: at these odds (`true`: 0.75)\"},\"ignoreInjured\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"A unit that fled stays out until healed\"},\"removeInjuries\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"The hurt go home to heal between fights\"},\"takeItems\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Picks up what lies about\"},\"buyItems\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Its heroes buy from shops in reach\"},\"allowHomeChanges\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Builds its hall again where it can, once its home falls, rather than where it first stood: yes when left out\"},\"smartArtillery\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Siege units shoot buildings and stay behind their squad\"},\"randomPaths\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Each wave takes a way of its own to the target, by the brain's numbers: straight, or round one side or the other\"},\"rebuild\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Builds again what it lost\"},\"kite\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Ranged units step back from melee they outrun\"},\"workersFlee\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Workers a raid is on walk to a safe base\"},\"say\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Says its lines to its side (the team's `say`)\"},\"scout\":{\"$ref\":\"#/definitions/AiFlag%3Cboolean%3E\",\"description\":\"Sends a unit to look at the enemy (the scouting's goal)\"}},\"additionalProperties\":false,\"description\":\"The classic RTS behaviour flags, where the meaning holds, and the kit's own beside them: each a yes or no, a number where one says how much, or a function of the brain\"},\"AiFlag<boolean>\":{\"type\":\"boolean\",\"description\":\"A flag's value: as data, or worked out by the brain as it is asked (code, not kept in a save)\"},\"AiFlag<(boolean|number)>\":{\"type\":[\"boolean\",\"number\"],\"description\":\"A flag's value: as data, or worked out by the brain as it is asked (code, not kept in a save)\"},\"ThinkBudgetOptions\":{\"type\":\"object\",\"properties\":{\"modulesPerTick\":{\"type\":\"number\",\"description\":\"Modules of a brain a think may run in one tick: a think with more goes on at the next tick, over the same picture of the game. All of them in one tick when left out\"},\"unitsPerTick\":{\"type\":\"number\",\"description\":\"Units a tick's modules may look at, as the modules say (`ai.budget.spent(n)`): a module that walks the squads or the bases stops once the tick has looked at this many, and goes on at the next tick from where it was (`later`). No limit when left out. A module that says nothing of what it looks at is never stopped by it\"},\"warnAfterMs\":{\"anyOf\":[{\"type\":\"number\"},{\"type\":\"boolean\",\"const\":false}],\"description\":\"Milliseconds a module may take before the log says so (once a module, a warning): what to measure first. A module is never cut short for it. 4 when left out; `false` for never\"}},\"additionalProperties\":false,\"description\":\"How much of a think one tick may do (`AiOptions.budget`, `AiProfile.budget`, `ComputerSeatOptions.budget`), counted in work and never in milliseconds: the same on every machine, so that a game with a sliced think is the same game everywhere. Nothing is limited where a field is left out\"},\"AiHandicap\":{\"type\":\"object\",\"properties\":{\"income\":{\"type\":\"number\",\"description\":\"What its workers bring in, through the game's `beforeDeposit` hook\"},\"health\":{\"type\":\"number\",\"description\":\"The most health of everything it has, through a `stats` hook: the classic handicap\"},\"damage\":{\"type\":\"number\",\"description\":\"The damage it deals, before armour, through a `beforeDamage` hook\"},\"buildTime\":{\"type\":\"number\",\"description\":\"How long what it trains, builds and researches takes, through a `beforeProduce` hook: under 1 is sooner\"}},\"additionalProperties\":false,\"description\":\"What a computer is given beyond the rules (`AiProfile.handicap`): each a factor, 1 (or left out) for none\"},\"DataRules\":{\"type\":\"object\",\"properties\":{\"combat\":{\"type\":\"object\",\"properties\":{\"damageTable\":{\"$ref\":\"#/definitions/DamageTable\"}},\"additionalProperties\":false},\"supply\":{\"type\":\"object\",\"properties\":{\"max\":{\"type\":\"number\"},\"pools\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"max\":{\"type\":\"number\"}},\"required\":[\"name\",\"max\"],\"additionalProperties\":false}}},\"additionalProperties\":false},\"heroes\":{\"type\":\"object\",\"properties\":{\"maxHeroes\":{\"type\":\"number\"},\"maxHeroLevel\":{\"type\":\"number\"},\"xpRange\":{\"type\":\"number\"}},\"additionalProperties\":false},\"resources\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/ResourceDef\"}},\"upkeep\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/UpkeepTier\"}}},\"additionalProperties\":false,\"description\":\"The rules a `data.json` may say (campaign plan RC7), each over the game's: the damage table whole, the supply's and the heroes' numbers, the resources and the upkeep's tiers whole (in place of `GameModule.resources`, `upkeep`)\"},\"DamageTable\":{\"type\":\"object\",\"properties\":{\"attacks\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"armours\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"table\":{\"type\":\"array\",\"items\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}}}},\"required\":[\"attacks\",\"armours\",\"table\"],\"additionalProperties\":false,\"description\":\"A damage table: what a hit of each attack type is multiplied by against each armour type (`GameRules.combat.damageTable`). `table[attack][armour]`, the rows the attacks in their order, the columns the armours in theirs\"},\"ResourceDef\":{\"type\":\"object\",\"properties\":{\"id\":{\"$ref\":\"#/definitions/ResourceKind\"},\"name\":{\"type\":\"string\"},\"start\":{\"type\":\"number\",\"description\":\"What every player starts a game with. Nothing when left out\"},\"upkeep\":{\"type\":\"boolean\",\"description\":\"What workers gather of it is cut by the upkeep of the army, see `UpkeepTier`\"}},\"required\":[\"id\",\"name\"],\"additionalProperties\":false,\"description\":\"Something players have amounts of and pay with, like gold, lumber or crystals. A game module lists its own\"},\"UpkeepTier\":{\"type\":\"object\",\"properties\":{\"above\":{\"type\":\"number\",\"description\":\"Applies when the player uses more supply than this\"},\"name\":{\"type\":\"string\"},\"income\":{\"type\":\"number\",\"description\":\"Part of what is gathered that still arrives, 0..1\"}},\"required\":[\"above\",\"name\",\"income\"],\"additionalProperties\":false,\"description\":\"A big army is paid for with income: past a certain supply, less arrives of what workers gather of the resources with `upkeep`\"},\"DataValues\":{\"type\":\"object\",\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/definitions/DataValue\"},{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/DataValue\"}},{\"$ref\":\"#/definitions/DataValues\"},{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/DataValues\"}}]},\"description\":\"Values of a definition, as its own fields nest them: `{ stats: { health: 420 }, cost: { gold: 135 } }`. What a `data.json` keeps of each definition, and what `world.redefine` is given. A list of things by place (a unit's attacks) is written as a list or as its places (`{ attacks: { 0: { damagePoint: 0.5 } } }`), each over the thing there. A list of things changed whole (an ability's effects, the units a race's start adds: a `rows` field) written as places goes over the things there, and written as a list is the list: those things, in that order, each in full\"},\"DataValue\":{\"type\":[\"number\",\"string\",\"boolean\"],\"description\":\"What a field of the data can hold: a number, words, yes or no\"}}")
|
|
2832
|
-
};
|
|
2833
|
-
//#endregion
|
|
2834
|
-
//#region src/server/packs.ts
|
|
2835
|
-
/**
|
|
2836
|
-
* The packs of content of a server, read from their files (`packs/<id>/pack.json`) in the order named: that order is
|
|
2837
|
-
* the game's, as a page's `defineGame({ packs })` is
|
|
2838
|
-
*
|
|
2839
|
-
* @beta
|
|
2840
|
-
*/
|
|
2841
|
-
function readPacks(files) {
|
|
2842
|
-
return files.map((file) => {
|
|
2843
|
-
const text = readFileSync(file, "utf8");
|
|
2844
|
-
const pack = packFromJson(text, file);
|
|
2845
|
-
const wrong = checkPackSchema(JSON.parse(text), pack_schema_default);
|
|
2846
|
-
if (wrong) throw new PackError(`${file}: ${wrong}`);
|
|
2847
|
-
return pack;
|
|
2848
|
-
});
|
|
2849
|
-
}
|
|
2850
|
-
/**
|
|
2851
|
-
* A server's game with its packs of content composed in (`readPacks`, `composeGame`): the game as is with none
|
|
2852
|
-
*
|
|
2853
|
-
* @beta
|
|
2854
|
-
*/
|
|
2855
|
-
function withPackFiles(game, files) {
|
|
2856
|
-
return files.length ? composeGame(game, readPacks(files)) : game;
|
|
2857
|
-
}
|
|
2858
|
-
//#endregion
|
|
2859
|
-
export { Doorman, GameRooms, GameServer, Lobby, PLAYER_COLOR_NAMES, Seat, WatchRelay, againstTheRest, createRatings, generatedMode, looksOfSeats, mapMode, metricsText, nodeNavigationBuilder, racesOfSeats, ratedId, readPacks, seatLooksOf, seatSettingsOf, seatsOf, startServer, startSocketServer, withPackFiles };
|
|
2860
|
-
|
|
2861
|
-
//# sourceMappingURL=server.js.map
|
|
1
|
+
import { nodeNavigationBuilder } from "./server/navigation.js";
|
|
2
|
+
import { createRatings, ratedId } from "./server/ratings.js";
|
|
3
|
+
import { Doorman } from "./server/doorman.js";
|
|
4
|
+
import { Seat } from "./server/seat.js";
|
|
5
|
+
import { seatSettingsOf } from "./server/seatSettings.js";
|
|
6
|
+
import { Lobby } from "./server/lobby.js";
|
|
7
|
+
import { metricsText } from "./server/ops.js";
|
|
8
|
+
import { startSocketServer } from "./server/websocket.js";
|
|
9
|
+
import { GameServer } from "./server/gameServer.js";
|
|
10
|
+
import { PLAYER_COLOR_NAMES, againstTheRest, generatedMode, looksOfSeats, mapMode, racesOfSeats, seatLooksOf, seatsOf } from "./server/mode.js";
|
|
11
|
+
import { startServer } from "./server/start.js";
|
|
12
|
+
import { GameRooms } from "./server/rooms.js";
|
|
13
|
+
import { WatchRelay } from "./server/relay.js";
|
|
14
|
+
import { mergePackFiles, readPacks } from "./server/packs.js";
|
|
15
|
+
export { Doorman, GameRooms, GameServer, Lobby, PLAYER_COLOR_NAMES, Seat, WatchRelay, againstTheRest, createRatings, generatedMode, looksOfSeats, mapMode, mergePackFiles, metricsText, nodeNavigationBuilder, racesOfSeats, ratedId, readPacks, seatLooksOf, seatSettingsOf, seatsOf, startServer, startSocketServer };
|