@v12-engine/engine 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/abilities/buffs.d.ts +83 -0
- package/dist/abilities/castTold.d.ts +12 -0
- package/dist/abilities/charges.d.ts +28 -0
- package/dist/abilities/dispel.d.ts +9 -0
- package/dist/abilities/effectFields.d.ts +10 -0
- package/dist/abilities/effects/buff.d.ts +8 -0
- package/dist/abilities/effects/core.d.ts +1 -0
- package/dist/abilities/effects/damage.d.ts +1 -0
- package/dist/abilities/effects/heal.d.ts +2 -0
- package/dist/abilities/effects/missile.d.ts +1 -0
- package/dist/abilities/effects/moves.d.ts +11 -0
- package/dist/abilities/effects/passives.d.ts +15 -0
- package/dist/abilities/effects/sight.d.ts +4 -0
- package/dist/abilities/effects/spawn.d.ts +2 -0
- package/dist/abilities/effects/timed.d.ts +4 -0
- package/dist/abilities/effects/trade.d.ts +1 -0
- package/dist/abilities/effects.d.ts +122 -0
- package/dist/abilities/land.d.ts +21 -0
- package/dist/abilities/scaled.d.ts +21 -0
- package/dist/abilities/text.d.ts +113 -0
- package/dist/ai/area.d.ts +53 -0
- package/dist/ai/brain.d.ts +528 -0
- package/dist/ai/budget.d.ts +67 -0
- package/dist/ai/checks.d.ts +115 -0
- package/dist/ai/computer.d.ts +166 -0
- package/dist/ai/defence.d.ts +108 -0
- package/dist/ai/define.d.ts +21 -0
- package/dist/ai/economy.d.ts +129 -0
- package/dist/ai/geometry.d.ts +35 -0
- package/dist/ai/goals.d.ts +360 -0
- package/dist/ai/index.d.ts +38 -0
- package/dist/ai/knowledge.d.ts +418 -0
- package/dist/ai/lanes.d.ts +232 -0
- package/dist/ai/melee/build.d.ts +61 -0
- package/dist/ai/melee/goals.d.ts +299 -0
- package/dist/ai/melee/heroes.d.ts +27 -0
- package/dist/ai/melee/index.d.ts +14 -0
- package/dist/ai/melee/production.d.ts +8 -0
- package/dist/ai/melee/surrender.d.ts +24 -0
- package/dist/ai/micro.d.ts +278 -0
- package/dist/ai/mission.d.ts +195 -0
- package/dist/ai/missionRules.d.ts +18 -0
- package/dist/ai/module.d.ts +116 -0
- package/dist/ai/options.d.ts +61 -0
- package/dist/ai/plans/build.d.ts +280 -0
- package/dist/ai/plans/placement.d.ts +82 -0
- package/dist/ai/plans/plans.d.ts +105 -0
- package/dist/ai/plans/production.d.ts +210 -0
- package/dist/ai/plans/purse.d.ts +73 -0
- package/dist/ai/plans/tech.d.ts +37 -0
- package/dist/ai/profiles.d.ts +204 -0
- package/dist/ai/reach.d.ts +4 -0
- package/dist/ai/report.d.ts +203 -0
- package/dist/ai/roles.d.ts +133 -0
- package/dist/ai/rules.d.ts +288 -0
- package/dist/ai/scouting.d.ts +98 -0
- package/dist/ai/settings.d.ts +73 -0
- package/dist/ai/squads.d.ts +429 -0
- package/dist/ai/strength.d.ts +167 -0
- package/dist/ai/task.d.ts +118 -0
- package/dist/ai/tasks/attack.d.ts +7 -0
- package/dist/ai/tasks/capture.d.ts +21 -0
- package/dist/ai/tasks/creep.d.ts +16 -0
- package/dist/ai/tasks/defend.d.ts +29 -0
- package/dist/ai/tasks/drop.d.ts +34 -0
- package/dist/ai/tasks/escort.d.ts +16 -0
- package/dist/ai/tasks/gather.d.ts +23 -0
- package/dist/ai/tasks/harass.d.ts +7 -0
- package/dist/ai/tasks/hold.d.ts +20 -0
- package/dist/ai/tasks/index.d.ts +32 -0
- package/dist/ai/tasks/move.d.ts +19 -0
- package/dist/ai/tasks/patrol.d.ts +8 -0
- package/dist/ai/tasks/retreat.d.ts +20 -0
- package/dist/ai/tasks/scout.d.ts +29 -0
- package/dist/ai/tasks/shared.d.ts +74 -0
- package/dist/ai/team.d.ts +155 -0
- package/dist/ai/threat.d.ts +91 -0
- package/dist/ai/view.d.ts +366 -0
- package/dist/ai/waves.d.ts +126 -0
- package/dist/ai.js +6239 -0
- package/dist/ai.js.map +1 -0
- package/dist/aiTesting.d.ts +179 -0
- package/dist/areas.d.ts +149 -0
- package/dist/attacks.d.ts +66 -0
- package/dist/attributes.d.ts +66 -0
- package/dist/brain-BvVdUhX8.js +7659 -0
- package/dist/brain-BvVdUhX8.js.map +1 -0
- package/dist/builds-C6LfgvEU.js +460 -0
- package/dist/builds-C6LfgvEU.js.map +1 -0
- package/dist/bytes.d.ts +6 -0
- package/dist/camps.d.ts +95 -0
- package/dist/capture.d.ts +131 -0
- package/dist/cellLayers.d.ts +75 -0
- package/dist/cinematics.d.ts +194 -0
- package/dist/clock.d.ts +32 -0
- package/dist/commands.d.ts +235 -0
- package/dist/components.d.ts +184 -0
- package/dist/computers.d.ts +108 -0
- package/dist/concealment.d.ts +61 -0
- package/dist/coreComponents-syuh2fPG.js +5149 -0
- package/dist/coreComponents-syuh2fPG.js.map +1 -0
- package/dist/coreComponents.d.ts +1 -0
- package/dist/dashing.d.ts +8 -0
- package/dist/debug.d.ts +36 -0
- package/dist/debug.js +3 -0
- package/dist/debugCommands-CzjrSm66.js +14739 -0
- package/dist/debugCommands-CzjrSm66.js.map +1 -0
- package/dist/debugCommands.d.ts +213 -0
- package/dist/debugging.d.ts +10 -0
- package/dist/definitions.d.ts +43 -0
- package/dist/defs.d.ts +1162 -0
- package/dist/denying.d.ts +81 -0
- package/dist/destructibles.d.ts +27 -0
- package/dist/determinism-BCymQwiE.js +518 -0
- package/dist/determinism-BCymQwiE.js.map +1 -0
- package/dist/determinism.d.ts +121 -0
- package/dist/dialog.d.ts +94 -0
- package/dist/effectRefs.d.ts +16 -0
- package/dist/effects.d.ts +80 -0
- package/dist/endings.d.ts +22 -0
- package/dist/engagement.d.ts +108 -0
- package/dist/engineEvents.d.ts +710 -0
- package/dist/entity.d.ts +409 -0
- package/dist/entityIndex.d.ts +50 -0
- package/dist/errors.d.ts +85 -0
- package/dist/events.d.ts +97 -0
- package/dist/exactMath.d.ts +63 -0
- package/dist/features.d.ts +10 -0
- package/dist/fields.d.ts +114 -0
- package/dist/fixture.d.ts +17 -0
- package/dist/fog.d.ts +110 -0
- package/dist/formation.d.ts +111 -0
- package/dist/friendlyFire.d.ts +26 -0
- package/dist/game-DiJEN-Ks.js +5800 -0
- package/dist/game-DiJEN-Ks.js.map +1 -0
- package/dist/game.d.ts +501 -0
- package/dist/gameData.d.ts +325 -0
- package/dist/gameFiles.d.ts +26 -0
- package/dist/gates.d.ts +28 -0
- package/dist/generate.d.ts +403 -0
- package/dist/generateWorker.d.ts +39 -0
- package/dist/grid.d.ts +53 -0
- package/dist/ground.d.ts +127 -0
- package/dist/groundSpeed.d.ts +22 -0
- package/dist/hero.d.ts +177 -0
- package/dist/home.d.ts +14 -0
- package/dist/hooks.d.ts +780 -0
- package/dist/hotReload.d.ts +76 -0
- package/dist/ids.d.ts +26 -0
- package/dist/index.d.ts +227 -0
- package/dist/index.js +4701 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect-D9dWHmZ0.js +491 -0
- package/dist/inspect-D9dWHmZ0.js.map +1 -0
- package/dist/inspect.d.ts +176 -0
- package/dist/inventory.d.ts +18 -0
- package/dist/items.d.ts +91 -0
- package/dist/lanes.d.ts +213 -0
- package/dist/life.d.ts +53 -0
- package/dist/local-Dkovq42k.js +1379 -0
- package/dist/local-Dkovq42k.js.map +1 -0
- package/dist/log.d.ts +140 -0
- package/dist/manaBurn.d.ts +39 -0
- package/dist/map/file.d.ts +114 -0
- package/dist/map/images.d.ts +18 -0
- package/dist/map/look.d.ts +47 -0
- package/dist/map/pathProfile.d.ts +13 -0
- package/dist/map/resize.d.ts +54 -0
- package/dist/map/schema.d.ts +10 -0
- package/dist/map/terrain.d.ts +32 -0
- package/dist/map/types.d.ts +613 -0
- package/dist/map.d.ts +7 -0
- package/dist/mapCheck/cells.d.ts +19 -0
- package/dist/mapCheck/rule.d.ts +30 -0
- package/dist/mapCheck/rules/names.d.ts +3 -0
- package/dist/mapCheck/rules/objects.d.ts +3 -0
- package/dist/mapCheck/rules/paths.d.ts +3 -0
- package/dist/mapCheck/rules/players.d.ts +3 -0
- package/dist/mapCheck/rules/races.d.ts +3 -0
- package/dist/mapCheck/rules/random.d.ts +3 -0
- package/dist/mapCheck/rules/regions.d.ts +3 -0
- package/dist/mapCheck/rules/targets.d.ts +3 -0
- package/dist/mapCheck/rules/walking.d.ts +3 -0
- package/dist/mapCheck.d.ts +23 -0
- package/dist/mapFields.d.ts +364 -0
- package/dist/mapFile.d.ts +23 -0
- package/dist/mapMeasure.d.ts +95 -0
- package/dist/mapMerge.d.ts +19 -0
- package/dist/mapObjectives.d.ts +82 -0
- package/dist/mapObjects.d.ts +1 -0
- package/dist/math.d.ts +44 -0
- package/dist/messages.d.ts +150 -0
- package/dist/missionRules-C-bGxm0Q.js +523 -0
- package/dist/missionRules-C-bGxm0Q.js.map +1 -0
- package/dist/missionRules.d.ts +179 -0
- package/dist/mods.d.ts +146 -0
- package/dist/names.d.ts +93 -0
- package/dist/navigation/agentParams.d.ts +4 -0
- package/dist/navigation/air.d.ts +89 -0
- package/dist/navigation/builds.d.ts +140 -0
- package/dist/navigation/groundAreas.d.ts +23 -0
- package/dist/navigation/navigation.d.ts +127 -0
- package/dist/navigation/recast.d.ts +214 -0
- package/dist/navigation/sidePaths.d.ts +69 -0
- package/dist/navigation/sized.d.ts +1 -0
- package/dist/navigation-worker.js +11 -0
- package/dist/navigation-worker.js.map +1 -0
- package/dist/nearest.d.ts +8 -0
- package/dist/noise.d.ts +12 -0
- package/dist/objectives.d.ts +93 -0
- package/dist/orders/basic.d.ts +96 -0
- package/dist/orders/build.d.ts +91 -0
- package/dist/orders/cast.d.ts +105 -0
- package/dist/orders/facts.d.ts +14 -0
- package/dist/orders/harvest.d.ts +94 -0
- package/dist/orders/idle.d.ts +7 -0
- package/dist/orders/items.d.ts +14 -0
- package/dist/orders/queue.d.ts +24 -0
- package/dist/pack.schema.json +6621 -0
- package/dist/packs/director/index.d.ts +81 -0
- package/dist/packs/heroes/campaign.d.ts +137 -0
- package/dist/packs/heroes/carry.d.ts +59 -0
- package/dist/packs/heroes/commands.d.ts +6 -0
- package/dist/packs/heroes/hire.d.ts +9 -0
- package/dist/packs/heroes/index.d.ts +12 -0
- package/dist/packs/heroes/making.d.ts +16 -0
- package/dist/packs/heroes/respawn.d.ts +42 -0
- package/dist/packs/moba/index.d.ts +34 -0
- package/dist/packs/pack.d.ts +84 -0
- package/dist/packs-g5thvmGn.js +3199 -0
- package/dist/packs-g5thvmGn.js.map +1 -0
- package/dist/packs.d.ts +84 -0
- package/dist/paths.d.ts +51 -0
- package/dist/plain.d.ts +17 -0
- package/dist/player.d.ts +117 -0
- package/dist/portals.d.ts +66 -0
- package/dist/prefabs.d.ts +139 -0
- package/dist/presetBuildings.d.ts +52 -0
- package/dist/presetGround.d.ts +22 -0
- package/dist/profiles-B3NS9jwk.js +335 -0
- package/dist/profiles-B3NS9jwk.js.map +1 -0
- package/dist/races.d.ts +136 -0
- package/dist/randomGroups.d.ts +2 -0
- package/dist/registry.d.ts +464 -0
- package/dist/remains.d.ts +119 -0
- package/dist/replication.d.ts +103 -0
- package/dist/rng.d.ts +17 -0
- package/dist/rules.d.ts +395 -0
- package/dist/rulesCheck.d.ts +37 -0
- package/dist/runes.d.ts +41 -0
- package/dist/save.d.ts +277 -0
- package/dist/scenario.d.ts +56 -0
- package/dist/scheduler.d.ts +67 -0
- package/dist/score.d.ts +306 -0
- package/dist/sequences.d.ts +132 -0
- package/dist/server/doorman.d.ts +21 -0
- package/dist/server/gameServer.d.ts +346 -0
- package/dist/server/index.d.ts +24 -0
- package/dist/server/lobby.d.ts +1 -0
- package/dist/server/log.d.ts +13 -0
- package/dist/server/match.d.ts +24 -0
- package/dist/server/mode.d.ts +168 -0
- package/dist/server/navigation-worker.d.ts +1 -0
- package/dist/server/navigation.d.ts +11 -0
- package/dist/server/ops.d.ts +78 -0
- package/dist/server/packs.d.ts +15 -0
- package/dist/server/ratings.d.ts +62 -0
- package/dist/server/relay.d.ts +34 -0
- package/dist/server/rooms.d.ts +162 -0
- package/dist/server/seat.d.ts +2 -0
- package/dist/server/seatSettings.d.ts +15 -0
- package/dist/server/secret.d.ts +5 -0
- package/dist/server/start.d.ts +54 -0
- package/dist/server/websocket.d.ts +74 -0
- package/dist/server.js +2861 -0
- package/dist/server.js.map +1 -0
- package/dist/session/can.d.ts +20 -0
- package/dist/session/commandReplay.d.ts +92 -0
- package/dist/session/debug.d.ts +65 -0
- package/dist/session/delay.d.ts +15 -0
- package/dist/session/delta.d.ts +98 -0
- package/dist/session/entityState.d.ts +1 -0
- package/dist/session/local.d.ts +214 -0
- package/dist/session/memory.d.ts +27 -0
- package/dist/session/protocol.d.ts +1155 -0
- package/dist/session/remote.d.ts +530 -0
- package/dist/session/replay.d.ts +114 -0
- package/dist/session/seed.d.ts +7 -0
- package/dist/session/share.d.ts +78 -0
- package/dist/session/socket.d.ts +8 -0
- package/dist/session/wire.d.ts +153 -0
- package/dist/session/worker.d.ts +227 -0
- package/dist/shared.d.ts +171 -0
- package/dist/sides.d.ts +75 -0
- package/dist/sight.d.ts +27 -0
- package/dist/simulate-oXBiw1Ff.js +167 -0
- package/dist/simulate-oXBiw1Ff.js.map +1 -0
- package/dist/simulate-worker.d.ts +1 -0
- package/dist/simulate-worker.js +28 -0
- package/dist/simulate-worker.js.map +1 -0
- package/dist/simulate.d.ts +154 -0
- package/dist/smart.d.ts +9 -0
- package/dist/spatial.d.ts +62 -0
- package/dist/spawning.d.ts +33 -0
- package/dist/stances.d.ts +76 -0
- package/dist/start.d.ts +71 -0
- package/dist/stats.d.ts +122 -0
- package/dist/summons.d.ts +63 -0
- package/dist/systems/abilities.d.ts +3 -0
- package/dist/systems/combat.d.ts +71 -0
- package/dist/systems/combatTold.d.ts +14 -0
- package/dist/systems/construction.d.ts +34 -0
- package/dist/systems/economy.d.ts +21 -0
- package/dist/systems/fog.d.ts +19 -0
- package/dist/systems/life.d.ts +3 -0
- package/dist/systems/movement.d.ts +105 -0
- package/dist/systems/orders.d.ts +7 -0
- package/dist/systems/production.d.ts +49 -0
- package/dist/systems/productionQueue.d.ts +40 -0
- package/dist/systems/victory.d.ts +14 -0
- package/dist/talk.d.ts +65 -0
- package/dist/tech.d.ts +62 -0
- package/dist/techTree.d.ts +38 -0
- package/dist/terrain.d.ts +173 -0
- package/dist/terrainSave.d.ts +29 -0
- package/dist/testing.d.ts +58 -0
- package/dist/testing.js +822 -0
- package/dist/testing.js.map +1 -0
- package/dist/tiles.d.ts +34 -0
- package/dist/timers.d.ts +146 -0
- package/dist/tomes.d.ts +48 -0
- package/dist/trade.d.ts +28 -0
- package/dist/transport.d.ts +76 -0
- package/dist/tunables.d.ts +45 -0
- package/dist/upkeep.d.ts +16 -0
- package/dist/veterancy.d.ts +80 -0
- package/dist/walls.d.ts +86 -0
- package/dist/world.d.ts +1118 -0
- package/package.json +80 -5
- package/README.md +0 -3
|
@@ -0,0 +1,1379 @@
|
|
|
1
|
+
import { Ao as assertPlainData, Fd as orderSays, Jl as checkpoint, Ki as declaredRefusal, Ol as SessionView, Vn as illusion, Xi as notReady, Zs as shieldOf, _i as decidedUnlocks, cl as fighting, gi as decidedAvailability, ii as placementCells, ll as moving, rl as deposit, ro as dashing, sd as changedStats, ti as canPlaceStructure, uc as heroSummaries, xi as techOf, yd as baseStats } from "./debugCommands-CzjrSm66.js";
|
|
2
|
+
import { Bt as minesFor, Ct as campsFor, an as shownEffects, ct as saveWorld, k as lanesOf, zt as names } from "./coreComponents-syuh2fPG.js";
|
|
3
|
+
import { i as setInspected, n as inspect, r as listWorld, t as EventLog } from "./inspect-D9dWHmZ0.js";
|
|
4
|
+
//#region src/session/protocol.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether a command is one of the engine's of a kind, narrowed to its shape (`isCommand(sent, 'order')`: its units and
|
|
7
|
+
* its order): what reads a command it did not write, a game's own (`ModCommand`) passed over
|
|
8
|
+
*
|
|
9
|
+
* @beta
|
|
10
|
+
*/
|
|
11
|
+
function isCommand(command, type) {
|
|
12
|
+
return command.type === type;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The version of what goes over the wire between a host and its players: the messages here and in `session/remote.ts`,
|
|
16
|
+
* and how deltas are made (`session/delta.ts`). The host says it in its welcome and a player says it when joining a
|
|
17
|
+
* server; a player of another version is refused with a reason, not left to fail somewhere strange. Raised by one on
|
|
18
|
+
* any change of their shape, which a game's own data (`Registry.dataHash`) does not show
|
|
19
|
+
*
|
|
20
|
+
* @beta
|
|
21
|
+
*/
|
|
22
|
+
var PROTOCOL_VERSION = 13;
|
|
23
|
+
/**
|
|
24
|
+
* What a player is told when the host's version of the wire is not its own (`PROTOCOL_VERSION`)
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
function protocolMismatch(host, player = 13) {
|
|
29
|
+
return `The host runs another version of the engine (protocol ${typeof host === "number" ? host : "unknown"}, this player's is ${player}): update the game`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Reads a message that came over a wire: an object with a `type` that is a string, or nothing. What is not JSON, or is
|
|
33
|
+
* JSON of another shape (`null`, a number, a list), is nothing, so that a peer that sends anything at all makes whoever
|
|
34
|
+
* reads it drop the message, not fall over. The other fields are still the sender's word: check each before it is used
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
function readMessage(text) {
|
|
39
|
+
let message;
|
|
40
|
+
try {
|
|
41
|
+
message = JSON.parse(text);
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) return void 0;
|
|
46
|
+
const { type } = message;
|
|
47
|
+
return typeof type === "string" ? message : void 0;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A number that is a number: not `NaN`, not infinite, not a string of digits
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/session/can.ts
|
|
57
|
+
/** A session's pace, as `SessionCan.pace`: its own `speed` and `advance` */
|
|
58
|
+
function paceOf(session) {
|
|
59
|
+
return {
|
|
60
|
+
get speed() {
|
|
61
|
+
return session.speed;
|
|
62
|
+
},
|
|
63
|
+
set speed(speed) {
|
|
64
|
+
session.speed = speed;
|
|
65
|
+
},
|
|
66
|
+
advance: (ticks) => session.advance(ticks)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** A session's saves, as `SessionCan.saves`: its own `save` and `checkpoint` */
|
|
70
|
+
function savesOf(session) {
|
|
71
|
+
return {
|
|
72
|
+
save: () => session.save(),
|
|
73
|
+
get checkpoint() {
|
|
74
|
+
return session.checkpoint;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/session/delta.ts
|
|
80
|
+
/** Rounded to `step` */
|
|
81
|
+
var round = (value, step) => Math.round(value / step) * step;
|
|
82
|
+
/**
|
|
83
|
+
* Seconds rounded up to whole ones, a hair over one (what floating point leaves over) not counted: what a plate
|
|
84
|
+
* sweeps changes once a second on the wire, not every round (the plate counts down between)
|
|
85
|
+
*/
|
|
86
|
+
var secondsUp = (value) => Math.ceil(value - 1e-6);
|
|
87
|
+
/** Positions on the wire, in 256ths of a unit, and the facing in 256ths of a turn: whole numbers, short to write */
|
|
88
|
+
var POSITION = 256;
|
|
89
|
+
var TURN = 256 / (Math.PI * 2);
|
|
90
|
+
/** An entity's numbers as they go over the wire: whole numbers where they can be. `decode` takes them back */
|
|
91
|
+
function encode(state) {
|
|
92
|
+
const out = { ...state };
|
|
93
|
+
if (out.x !== void 0) out.x = Math.round(out.x * POSITION);
|
|
94
|
+
if (out.z !== void 0) out.z = Math.round(out.z * POSITION);
|
|
95
|
+
if (out.facing !== void 0) out.facing = (Math.round(out.facing * TURN) % 256 + 256) % 256;
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function decode(state) {
|
|
99
|
+
const out = { ...state };
|
|
100
|
+
if (out.x !== void 0) out.x /= POSITION;
|
|
101
|
+
if (out.z !== void 0) out.z /= POSITION;
|
|
102
|
+
if (out.facing !== void 0) out.facing /= TURN;
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* An entity's state with its numbers rounded for the wire: positions to 1/256 of a unit (16 bits over a map of 256),
|
|
107
|
+
* the facing to a 256th of a turn, health and shield up to whole points (what is alive keeps at least one), mana down
|
|
108
|
+
* (an ability is never shown ready that the simulation would refuse), what is under way to hundredths, the seconds
|
|
109
|
+
* left of buffs and of shared cooldowns up to whole seconds
|
|
110
|
+
*
|
|
111
|
+
* @beta
|
|
112
|
+
*/
|
|
113
|
+
function quantize(state) {
|
|
114
|
+
const out = { ...state };
|
|
115
|
+
out.x = Math.round(state.x * POSITION) / POSITION;
|
|
116
|
+
out.z = Math.round(state.z * POSITION) / POSITION;
|
|
117
|
+
out.facing = (Math.round(state.facing * TURN) % 256 + 256) % 256 / TURN;
|
|
118
|
+
out.hp = Math.ceil(state.hp);
|
|
119
|
+
if (state.shield !== void 0) out.shield = Math.ceil(state.shield);
|
|
120
|
+
if (state.mana !== void 0) out.mana = Math.floor(state.mana);
|
|
121
|
+
if (state.buffTimes) out.buffTimes = state.buffTimes.map((left) => left < 0 ? left : secondsUp(left));
|
|
122
|
+
if (state.abilities?.some((ability) => ability.cooldown)) out.abilities = state.abilities.map((ability) => ability.cooldown ? {
|
|
123
|
+
...ability,
|
|
124
|
+
cooldown: secondsUp(ability.cooldown)
|
|
125
|
+
} : ability);
|
|
126
|
+
if (state.lifetime) out.lifetime = {
|
|
127
|
+
left: round(state.lifetime.left, .1),
|
|
128
|
+
total: state.lifetime.total
|
|
129
|
+
};
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Whether two values of a field are the same: the very same, or equal as data. Two flat objects (an entity's `stats`, a
|
|
134
|
+
* `lifetime`, a `leap`'s points) are compared field by field; what nests deeper as text
|
|
135
|
+
*/
|
|
136
|
+
function same(a, b) {
|
|
137
|
+
if (a === b) return true;
|
|
138
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
139
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
140
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
141
|
+
for (let i = 0; i < a.length; i++) if (!same(a[i], b[i])) return false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
const left = a;
|
|
145
|
+
const right = b;
|
|
146
|
+
for (const key in left) {
|
|
147
|
+
if (!(key in right)) return false;
|
|
148
|
+
const x = left[key];
|
|
149
|
+
const y = right[key];
|
|
150
|
+
if (x === y) continue;
|
|
151
|
+
if (typeof x !== "object" || typeof y !== "object" || x === null || y === null) return false;
|
|
152
|
+
if (!same(x, y)) return false;
|
|
153
|
+
}
|
|
154
|
+
for (const key in right) if (!(key in left)) return false;
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
/** How deep a patch goes into lists and objects: past it, what changed is sent whole */
|
|
158
|
+
var PATCH_DEPTH = 4;
|
|
159
|
+
var isObject = (value) => typeof value === "object" && value !== null;
|
|
160
|
+
/**
|
|
161
|
+
* What to send of a value that changed (`same` said so) to whoever has `before`: the parts of a list or an object
|
|
162
|
+
* that changed (`WirePatch`), or `after` whole where every part did, or where it is not the same kind of thing
|
|
163
|
+
*/
|
|
164
|
+
function patchOf(before, after, depth = PATCH_DEPTH) {
|
|
165
|
+
if (depth === 0 || !isObject(before) || !isObject(after) || Array.isArray(before) !== Array.isArray(after)) return after;
|
|
166
|
+
const parts = {};
|
|
167
|
+
let changed = 0;
|
|
168
|
+
if (Array.isArray(before)) {
|
|
169
|
+
const list = after;
|
|
170
|
+
if (list.length !== before.length) return after;
|
|
171
|
+
for (let i = 0; i < list.length; i++) {
|
|
172
|
+
if (same(before[i], list[i])) continue;
|
|
173
|
+
parts[i] = patchOf(before[i], list[i], depth - 1);
|
|
174
|
+
changed++;
|
|
175
|
+
}
|
|
176
|
+
return changed === list.length ? after : { $patch: parts };
|
|
177
|
+
}
|
|
178
|
+
const left = before;
|
|
179
|
+
const right = after;
|
|
180
|
+
let fields = 0;
|
|
181
|
+
let unset;
|
|
182
|
+
for (const key in right) {
|
|
183
|
+
if (right[key] === void 0) continue;
|
|
184
|
+
fields++;
|
|
185
|
+
if (same(left[key], right[key])) continue;
|
|
186
|
+
parts[key] = patchOf(left[key], right[key], depth - 1);
|
|
187
|
+
changed++;
|
|
188
|
+
}
|
|
189
|
+
for (const key in left) if (left[key] !== void 0 && right[key] === void 0) (unset ??= []).push(key);
|
|
190
|
+
if (changed === fields && !unset) return after;
|
|
191
|
+
return unset ? {
|
|
192
|
+
$patch: parts,
|
|
193
|
+
$unset: unset
|
|
194
|
+
} : { $patch: parts };
|
|
195
|
+
}
|
|
196
|
+
var isPatch = (value) => isObject(value) && !Array.isArray(value) && "$patch" in value;
|
|
197
|
+
/** A value a patch was made against, with the patch put in: new lists and objects where it says, the very parts it does not */
|
|
198
|
+
function patched(base, patch) {
|
|
199
|
+
const made = Array.isArray(base) ? [...base] : { ...base };
|
|
200
|
+
for (const [key, part] of Object.entries(patch.$patch)) made[key] = isPatch(part) ? patched(base[key], part) : part;
|
|
201
|
+
for (const key of patch.$unset ?? []) delete made[key];
|
|
202
|
+
return made;
|
|
203
|
+
}
|
|
204
|
+
/** Whether two entities' components say the same: each as it knows how (`Component.same`), or as text */
|
|
205
|
+
function sameComponents(registry, a, b) {
|
|
206
|
+
if (a === b) return true;
|
|
207
|
+
if (!a || !b) return false;
|
|
208
|
+
for (const id in a) if (!(id in b)) return false;
|
|
209
|
+
for (const id in b) {
|
|
210
|
+
if (!(id in a)) return false;
|
|
211
|
+
const compare = registry?.component(id)?.same;
|
|
212
|
+
if (compare ? !compare(a[id], b[id]) : !same(a[id], b[id])) return false;
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The fields of an object that changed, and those that are gone, or nothing when none did. A field named in `skip` is
|
|
218
|
+
* left to the caller (an entity's `own` part goes field by field on its own)
|
|
219
|
+
*/
|
|
220
|
+
function fieldsOf(registry, before, after, skip) {
|
|
221
|
+
let set;
|
|
222
|
+
let unset;
|
|
223
|
+
for (const key in after) {
|
|
224
|
+
if (key === skip || after[key] === void 0) continue;
|
|
225
|
+
if (key === "components" ? !sameComponents(registry, before[key], after[key]) : !same(before[key], after[key])) (set ??= {})[key] = after[key];
|
|
226
|
+
}
|
|
227
|
+
for (const key in before) if (key !== skip && before[key] !== void 0 && after[key] === void 0) (unset ??= []).push(key);
|
|
228
|
+
return set || unset ? {
|
|
229
|
+
set: set ?? {},
|
|
230
|
+
...unset ? { unset } : {}
|
|
231
|
+
} : void 0;
|
|
232
|
+
}
|
|
233
|
+
/** What changed of an entity, or nothing when nothing did. What only its owner is told goes field by field too */
|
|
234
|
+
function changeOf(registry, before, after) {
|
|
235
|
+
const ownBefore = before.own;
|
|
236
|
+
const ownAfter = after.own;
|
|
237
|
+
const fields = fieldsOf(registry, before, after, "own");
|
|
238
|
+
let own;
|
|
239
|
+
let ownPatch;
|
|
240
|
+
let unsetOwn = false;
|
|
241
|
+
if (ownBefore && ownAfter) {
|
|
242
|
+
own = fieldsOf(registry, ownBefore, ownAfter);
|
|
243
|
+
if (own) for (const key in own.set) {
|
|
244
|
+
const field = key;
|
|
245
|
+
if (!isObject(ownBefore[field])) continue;
|
|
246
|
+
const patch = patchOf(ownBefore[field], own.set[field]);
|
|
247
|
+
if (!isPatch(patch)) continue;
|
|
248
|
+
(ownPatch ??= {})[field] = patch;
|
|
249
|
+
delete own.set[field];
|
|
250
|
+
}
|
|
251
|
+
} else if (ownAfter) own = { set: ownAfter };
|
|
252
|
+
else if (ownBefore) unsetOwn = true;
|
|
253
|
+
if (!fields && !own && !unsetOwn) return void 0;
|
|
254
|
+
const unset = [...fields?.unset ?? [], ...unsetOwn ? ["own"] : []];
|
|
255
|
+
const set = fields?.set ?? {};
|
|
256
|
+
if (set.x !== void 0) set.x = Math.round(set.x * POSITION);
|
|
257
|
+
if (set.z !== void 0) set.z = Math.round(set.z * POSITION);
|
|
258
|
+
if (set.facing !== void 0) set.facing = (Math.round(set.facing * TURN) % 256 + 256) % 256;
|
|
259
|
+
const change = [after.id, set];
|
|
260
|
+
if (unset.length || own) change.push(unset.length ? unset : 0);
|
|
261
|
+
if (own) {
|
|
262
|
+
change.push(Object.keys(own.set).length || !ownPatch ? own.set : 0);
|
|
263
|
+
if (own.unset || ownPatch) change.push(own.unset ?? 0);
|
|
264
|
+
if (ownPatch) change.push(ownPatch);
|
|
265
|
+
}
|
|
266
|
+
return change;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Every part of a frame besides its tick, its entities and its events: the snapshot's parts, the engine's and a pack's
|
|
270
|
+
* (`SnapshotPart`), and the fog, the camps and the team
|
|
271
|
+
*/
|
|
272
|
+
var restOf = (frame) => {
|
|
273
|
+
const { snapshot, fog, camps, team } = frame;
|
|
274
|
+
const { tick: _tick, entities: _entities, events: _events, ...parts } = snapshot;
|
|
275
|
+
return {
|
|
276
|
+
...parts,
|
|
277
|
+
fog,
|
|
278
|
+
camps,
|
|
279
|
+
team
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
/** A frame's entities by id, worked out once: a base is diffed against round after round while the player's answer is on its way */
|
|
283
|
+
var BY_ID = /* @__PURE__ */ new WeakMap();
|
|
284
|
+
function entitiesById(frame) {
|
|
285
|
+
let byId = BY_ID.get(frame);
|
|
286
|
+
if (!byId) {
|
|
287
|
+
byId = /* @__PURE__ */ new Map();
|
|
288
|
+
for (const entity of frame.snapshot.entities) byId.set(entity.id, entity);
|
|
289
|
+
BY_ID.set(frame, byId);
|
|
290
|
+
}
|
|
291
|
+
return byId;
|
|
292
|
+
}
|
|
293
|
+
var entityDiffs = /* @__PURE__ */ new WeakMap();
|
|
294
|
+
var wholeDiffs = /* @__PURE__ */ new WeakMap();
|
|
295
|
+
function entitiesDiff(then, now, base, registry) {
|
|
296
|
+
let byBase = then && entityDiffs.get(then);
|
|
297
|
+
const known = then ? byBase?.get(now) : wholeDiffs.get(now);
|
|
298
|
+
if (known) return known;
|
|
299
|
+
const made = {
|
|
300
|
+
added: [],
|
|
301
|
+
changed: [],
|
|
302
|
+
removed: []
|
|
303
|
+
};
|
|
304
|
+
const before = base ? entitiesById(base) : void 0;
|
|
305
|
+
const seen = before && before.size ? /* @__PURE__ */ new Set() : void 0;
|
|
306
|
+
for (const entity of now) {
|
|
307
|
+
const old = before?.get(entity.id);
|
|
308
|
+
if (old) seen.add(entity.id);
|
|
309
|
+
if (!old || old.type !== entity.type) {
|
|
310
|
+
if (old) made.removed.push(old.id);
|
|
311
|
+
made.added.push(encode(entity));
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const change = changeOf(registry, old, entity);
|
|
315
|
+
if (change) made.changed.push(change);
|
|
316
|
+
}
|
|
317
|
+
if (before) {
|
|
318
|
+
for (const id of before.keys()) if (!seen.has(id)) made.removed.push(id);
|
|
319
|
+
}
|
|
320
|
+
if (then) {
|
|
321
|
+
if (!byBase) entityDiffs.set(then, byBase = /* @__PURE__ */ new WeakMap());
|
|
322
|
+
byBase.set(now, made);
|
|
323
|
+
} else wholeDiffs.set(now, made);
|
|
324
|
+
return made;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* A frame as what changed since `base`, or all of it without one. Events are the caller's to add. `registry`: the game's, whose
|
|
328
|
+
* components say how a change of theirs is found (`Component.same`); without it, as text
|
|
329
|
+
*
|
|
330
|
+
* @internal
|
|
331
|
+
*/
|
|
332
|
+
function diff(base, next, registry) {
|
|
333
|
+
const delta = {
|
|
334
|
+
tick: next.snapshot.tick,
|
|
335
|
+
base: base ? base.snapshot.tick : null,
|
|
336
|
+
added: [],
|
|
337
|
+
changed: [],
|
|
338
|
+
removed: [],
|
|
339
|
+
rest: {}
|
|
340
|
+
};
|
|
341
|
+
const { added, changed, removed } = entitiesDiff(base?.snapshot.entities, next.snapshot.entities, base, registry);
|
|
342
|
+
delta.added = added;
|
|
343
|
+
delta.changed = changed;
|
|
344
|
+
delta.removed = removed;
|
|
345
|
+
const now = restOf(next);
|
|
346
|
+
const then = base && restOf(base);
|
|
347
|
+
const keys = then ? /* @__PURE__ */ new Set([...Object.keys(now), ...Object.keys(then)]) : Object.keys(now);
|
|
348
|
+
for (const key of keys) {
|
|
349
|
+
const before = then?.[key];
|
|
350
|
+
const after = now[key];
|
|
351
|
+
if (then && same(before, after)) continue;
|
|
352
|
+
const patch = then && key !== "fog" ? patchOf(before, after) : after;
|
|
353
|
+
if (isPatch(patch)) (delta.restPatch ??= {})[key] = patch;
|
|
354
|
+
else delta.rest[key] = after;
|
|
355
|
+
}
|
|
356
|
+
return delta;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* The frame a delta makes of the one at its base (none, for a whole one). Entities that did not change are the very
|
|
360
|
+
* objects they were, so that whoever compares them by identity knows they did not. That is the frame before
|
|
361
|
+
* (`latest`) whose object is the same as what is made, which is not always the base: the base is the last frame the
|
|
362
|
+
* host heard was got, a frame or two behind, and the frames in between have objects of their own
|
|
363
|
+
*
|
|
364
|
+
* @internal
|
|
365
|
+
*/
|
|
366
|
+
function apply(base, delta, latest) {
|
|
367
|
+
const removed = new Set(delta.removed);
|
|
368
|
+
const changed = /* @__PURE__ */ new Map();
|
|
369
|
+
for (const change of delta.changed) changed.set(change[0], change);
|
|
370
|
+
const before = /* @__PURE__ */ new Map();
|
|
371
|
+
if (latest && latest !== base) for (const entity of latest.snapshot.entities) before.set(entity.id, entity);
|
|
372
|
+
/** The frame before's object for what is made, where it is the same as it; else what is made */
|
|
373
|
+
const kept = (made) => {
|
|
374
|
+
const was = before.get(made.id);
|
|
375
|
+
return was && (was === made || sameEntity(was, made)) ? was : made;
|
|
376
|
+
};
|
|
377
|
+
const entities = [];
|
|
378
|
+
for (const entity of base?.snapshot.entities ?? []) {
|
|
379
|
+
if (removed.has(entity.id)) continue;
|
|
380
|
+
const change = changed.get(entity.id);
|
|
381
|
+
if (!change) {
|
|
382
|
+
entities.push(kept(entity));
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const [, set, unset, ownSet, ownUnset, ownPatch] = change;
|
|
386
|
+
const next = {
|
|
387
|
+
...entity,
|
|
388
|
+
...decode(set)
|
|
389
|
+
};
|
|
390
|
+
for (const field of unset || []) delete next[field];
|
|
391
|
+
if (ownSet || ownUnset || ownPatch) {
|
|
392
|
+
const own = {
|
|
393
|
+
...entity.own,
|
|
394
|
+
...ownSet || {}
|
|
395
|
+
};
|
|
396
|
+
for (const field of ownUnset || []) delete own[field];
|
|
397
|
+
for (const [field, patch] of Object.entries(ownPatch ?? {})) own[field] = patched(entity.own?.[field], patch);
|
|
398
|
+
next.own = own;
|
|
399
|
+
}
|
|
400
|
+
entities.push(kept(next));
|
|
401
|
+
}
|
|
402
|
+
for (const added of delta.added) entities.push(kept(decode(added)));
|
|
403
|
+
const rest = {
|
|
404
|
+
...base ? restOf(base) : {},
|
|
405
|
+
...delta.rest
|
|
406
|
+
};
|
|
407
|
+
for (const [key, patch] of Object.entries(delta.restPatch ?? {})) rest[key] = patched(base && restOf(base)[key], patch);
|
|
408
|
+
const { fog, camps, team, ...parts } = rest;
|
|
409
|
+
return {
|
|
410
|
+
snapshot: {
|
|
411
|
+
tick: delta.tick,
|
|
412
|
+
...parts,
|
|
413
|
+
entities,
|
|
414
|
+
events: []
|
|
415
|
+
},
|
|
416
|
+
fog,
|
|
417
|
+
camps,
|
|
418
|
+
team
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Whether two states of an entity say the same, field by field
|
|
423
|
+
*
|
|
424
|
+
* @internal
|
|
425
|
+
*/
|
|
426
|
+
function sameEntity(a, b) {
|
|
427
|
+
if (a.type !== b.type) return false;
|
|
428
|
+
return fieldsOf(void 0, a, b) === void 0;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Characters of a delta's text by what they carry, added to `into`: each field of an entity, of those added and those
|
|
432
|
+
* changed together (`entity.hp`; of what only its owner is told, `entity.own.abilities`), the other parts (`rest.fog`),
|
|
433
|
+
* `events`, `removed`, and what is left (ids, brackets, the tick) as `other`, so that they add up to `text`'s length.
|
|
434
|
+
* A field is its key, its value and the comma between, as `JSON.stringify` writes them
|
|
435
|
+
*/
|
|
436
|
+
function bytesByKey(delta, text, into) {
|
|
437
|
+
let counted = 0;
|
|
438
|
+
const add = (key, characters) => {
|
|
439
|
+
into[key] = (into[key] ?? 0) + characters;
|
|
440
|
+
counted += characters;
|
|
441
|
+
};
|
|
442
|
+
const fields = (prefix, values) => {
|
|
443
|
+
for (const [key, value] of Object.entries(values)) {
|
|
444
|
+
if (value === void 0) continue;
|
|
445
|
+
if (key === "own" && prefix === "entity." && value && typeof value === "object") {
|
|
446
|
+
add("entity.own", key.length + 5);
|
|
447
|
+
fields("entity.own.", value);
|
|
448
|
+
} else add(prefix + key, key.length + 4 + JSON.stringify(value).length);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
for (const entity of delta.added) fields("entity.", entity);
|
|
452
|
+
for (const [, set, unset, ownSet, ownUnset, ownPatch] of delta.changed) {
|
|
453
|
+
fields("entity.", set);
|
|
454
|
+
if (unset) add("entity.unset", JSON.stringify(unset).length + 1);
|
|
455
|
+
if (ownSet) fields("entity.own.", ownSet);
|
|
456
|
+
if (ownUnset) add("entity.own.unset", JSON.stringify(ownUnset).length + 1);
|
|
457
|
+
if (ownPatch) fields("entity.own.", ownPatch);
|
|
458
|
+
}
|
|
459
|
+
for (const part of [delta.rest, delta.restPatch ?? {}]) for (const [key, value] of Object.entries(part)) if (value !== void 0) add(`rest.${key}`, key.length + 4 + JSON.stringify(value).length);
|
|
460
|
+
if (delta.events.length) add("events", JSON.stringify(delta.events).length);
|
|
461
|
+
if (delta.removed.length) add("removed", JSON.stringify(delta.removed).length);
|
|
462
|
+
add("other", text.length - counted);
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/session/debug.ts
|
|
466
|
+
/** Ticks between two lists of everything in the world: thousands of trees need not be listed every tick */
|
|
467
|
+
var LIST_EVERY = 10;
|
|
468
|
+
/**
|
|
469
|
+
* The debug tools' view of a world running in the same page. What it hands out is worked out again whenever a
|
|
470
|
+
* snapshot is, and at once when the client asks for something else: a remote one would wait for the answer instead
|
|
471
|
+
*/
|
|
472
|
+
var LocalDebug = class {
|
|
473
|
+
simulation;
|
|
474
|
+
inspection = null;
|
|
475
|
+
world = null;
|
|
476
|
+
target = null;
|
|
477
|
+
listedAt = -Infinity;
|
|
478
|
+
events = new EventLog();
|
|
479
|
+
constructor(simulation) {
|
|
480
|
+
this.simulation = simulation;
|
|
481
|
+
}
|
|
482
|
+
inspect(target) {
|
|
483
|
+
this.target = target;
|
|
484
|
+
this.refresh();
|
|
485
|
+
}
|
|
486
|
+
set(path, value) {
|
|
487
|
+
if (!this.target) return;
|
|
488
|
+
setInspected(this.simulation, this.target, path, value);
|
|
489
|
+
this.refresh();
|
|
490
|
+
}
|
|
491
|
+
get listing() {
|
|
492
|
+
return this.world !== null;
|
|
493
|
+
}
|
|
494
|
+
set listing(on) {
|
|
495
|
+
this.listedAt = -Infinity;
|
|
496
|
+
this.world = on ? listWorld(this.simulation) : null;
|
|
497
|
+
}
|
|
498
|
+
get logging() {
|
|
499
|
+
return this.events.attached;
|
|
500
|
+
}
|
|
501
|
+
set logging(on) {
|
|
502
|
+
if (on === this.logging) return;
|
|
503
|
+
if (on) this.events.attach(this.simulation);
|
|
504
|
+
else this.events.detach();
|
|
505
|
+
}
|
|
506
|
+
log() {
|
|
507
|
+
return this.events.entries;
|
|
508
|
+
}
|
|
509
|
+
clearLog() {
|
|
510
|
+
this.events.clear();
|
|
511
|
+
}
|
|
512
|
+
/** Works what is handed out again. The session does when a tick went by */
|
|
513
|
+
refresh() {
|
|
514
|
+
const world = this.simulation;
|
|
515
|
+
this.inspection = this.target && inspect(world, this.target);
|
|
516
|
+
if (this.world && world.tick - this.listedAt >= LIST_EVERY) {
|
|
517
|
+
this.world = listWorld(world);
|
|
518
|
+
this.listedAt = world.tick;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
dispose() {
|
|
522
|
+
this.events.detach();
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
/** What is followed, as the wire says it: an entity, a player or the world, or nothing; anything else is not */
|
|
526
|
+
function targetOf(value) {
|
|
527
|
+
if (value === null) return null;
|
|
528
|
+
if (typeof value !== "object") return void 0;
|
|
529
|
+
const target = value;
|
|
530
|
+
if (isFiniteNumber(target.entity)) return { entity: target.entity };
|
|
531
|
+
if (isFiniteNumber(target.player)) return { player: target.player };
|
|
532
|
+
if (target.world === true) return { world: true };
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Does what the debug tools of a screen asked of a world here (`DebugAsk`), each field checked first: it comes from
|
|
536
|
+
* anyone who can send anything
|
|
537
|
+
*/
|
|
538
|
+
function askDebug(view, ask) {
|
|
539
|
+
const target = targetOf(ask.inspect);
|
|
540
|
+
if (target !== void 0) view.inspect(target);
|
|
541
|
+
const set = ask.set;
|
|
542
|
+
if (set && Array.isArray(set.path) && set.path.every((key) => typeof key === "string" || isFiniteNumber(key)) && (isFiniteNumber(set.value) || typeof set.value === "boolean")) view.set(set.path, set.value);
|
|
543
|
+
if (typeof ask.listing === "boolean") view.listing = ask.listing;
|
|
544
|
+
if (typeof ask.logging === "boolean") view.logging = ask.logging;
|
|
545
|
+
if (ask.clearLog === true) view.clearLog();
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* The debug tools' view of a world elsewhere, a server's or a worker's: what it asked to follow, said over the wire
|
|
549
|
+
* (`DebugAsk`), and what came back last (`take`)
|
|
550
|
+
*/
|
|
551
|
+
var RemoteDebug = class {
|
|
552
|
+
ask;
|
|
553
|
+
inspection = null;
|
|
554
|
+
world = null;
|
|
555
|
+
listed = false;
|
|
556
|
+
logged = false;
|
|
557
|
+
target = null;
|
|
558
|
+
events = [];
|
|
559
|
+
constructor(ask) {
|
|
560
|
+
this.ask = ask;
|
|
561
|
+
}
|
|
562
|
+
inspect(target) {
|
|
563
|
+
this.target = target;
|
|
564
|
+
if (!target) this.inspection = null;
|
|
565
|
+
this.ask({ inspect: target });
|
|
566
|
+
}
|
|
567
|
+
/** Asks again for all it follows: of a world it is served from anew (a session resumed, a replay gone back) */
|
|
568
|
+
askAgain() {
|
|
569
|
+
if (this.target || this.listed || this.logged) this.ask({
|
|
570
|
+
inspect: this.target,
|
|
571
|
+
listing: this.listed,
|
|
572
|
+
logging: this.logged
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
set(path, value) {
|
|
576
|
+
this.ask({ set: {
|
|
577
|
+
path,
|
|
578
|
+
value
|
|
579
|
+
} });
|
|
580
|
+
}
|
|
581
|
+
get listing() {
|
|
582
|
+
return this.listed;
|
|
583
|
+
}
|
|
584
|
+
set listing(on) {
|
|
585
|
+
this.listed = on;
|
|
586
|
+
if (!on) this.world = null;
|
|
587
|
+
this.ask({ listing: on });
|
|
588
|
+
}
|
|
589
|
+
get logging() {
|
|
590
|
+
return this.logged;
|
|
591
|
+
}
|
|
592
|
+
set logging(on) {
|
|
593
|
+
this.logged = on;
|
|
594
|
+
this.ask({ logging: on });
|
|
595
|
+
}
|
|
596
|
+
log() {
|
|
597
|
+
return this.events;
|
|
598
|
+
}
|
|
599
|
+
clearLog() {
|
|
600
|
+
this.events = [];
|
|
601
|
+
this.ask({ clearLog: true });
|
|
602
|
+
}
|
|
603
|
+
/** What the world's side said of what is followed */
|
|
604
|
+
take(said) {
|
|
605
|
+
this.inspection = said.inspection;
|
|
606
|
+
this.world = this.listed ? said.world : null;
|
|
607
|
+
if (this.logged) this.events = said.log;
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/session/entityState.ts
|
|
612
|
+
/**
|
|
613
|
+
* What a player is told of an entity (design review E7): what everyone in sight of it is, worked out once a tick for
|
|
614
|
+
* every seat of a host (`commonEntityState`), and on it what is one player's own (`playerEntityState`). A seat
|
|
615
|
+
* (`LocalSession`) puts them in its snapshots; nothing here keeps anything between ticks
|
|
616
|
+
*/
|
|
617
|
+
/** A rule of `GameRules.hud` for one unit: its value, or what its function decides */
|
|
618
|
+
var ruleOf = (world, entity, rule) => typeof rule === "function" ? rule(world, entity) : rule;
|
|
619
|
+
/** The seconds left of a unit's buffs, in their order: -1 for one with no end, or kept up by an aura or a passive */
|
|
620
|
+
function buffTimesOf(entity) {
|
|
621
|
+
const times = new Array(entity.buffs.length);
|
|
622
|
+
for (let i = 0; i < entity.buffs.length; i++) {
|
|
623
|
+
const buff = entity.buffs[i];
|
|
624
|
+
times[i] = Number.isFinite(buff.remaining) && !buff.kept ? Math.max(buff.remaining, 0) : -1;
|
|
625
|
+
}
|
|
626
|
+
return times;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* What everyone in sight of an entity is told of it this tick, the same for every seat (`EntityState`): where it is,
|
|
630
|
+
* its health and mana, its stats where they changed, its buffs, what the rules share for the plates over it. `name`
|
|
631
|
+
* is what the map calls it, where it has a name
|
|
632
|
+
*
|
|
633
|
+
* @internal
|
|
634
|
+
*/
|
|
635
|
+
function commonEntityState(world, entity, name) {
|
|
636
|
+
const state = {
|
|
637
|
+
id: entity.id,
|
|
638
|
+
type: entity.def.id,
|
|
639
|
+
owner: entity.owner,
|
|
640
|
+
x: entity.pos.x,
|
|
641
|
+
z: entity.pos.z,
|
|
642
|
+
facing: entity.facing,
|
|
643
|
+
hp: entity.hp,
|
|
644
|
+
stats: changedStats(world, entity),
|
|
645
|
+
visible: true
|
|
646
|
+
};
|
|
647
|
+
const base = baseStats(entity.def, world.registry);
|
|
648
|
+
if (entity.maxHp !== base.health) state.maxHp = entity.maxHp;
|
|
649
|
+
if (entity.buffs.length) state.shield = shieldOf(entity) || void 0;
|
|
650
|
+
const dash = entity.get(dashing);
|
|
651
|
+
if (dash?.height) state.leap = {
|
|
652
|
+
from: dash.from,
|
|
653
|
+
to: {
|
|
654
|
+
x: dash.x,
|
|
655
|
+
z: dash.z
|
|
656
|
+
},
|
|
657
|
+
height: dash.height
|
|
658
|
+
};
|
|
659
|
+
if (entity.maxMana > 0) {
|
|
660
|
+
state.mana = entity.mana;
|
|
661
|
+
if (entity.maxMana !== base.mana) state.maxMana = entity.maxMana;
|
|
662
|
+
}
|
|
663
|
+
if (entity.buffs.length) state.buffs = entity.buffs.map((buff) => buff.id);
|
|
664
|
+
for (const buff of entity.buffs) if (buff.stacks) (state.stacks ??= {})[buff.id] = buff.stacks;
|
|
665
|
+
const { hud } = world.rules;
|
|
666
|
+
if (hud.shareBuffTimes !== void 0 && entity.buffs.length && ruleOf(world, entity, hud.shareBuffTimes) === true) state.buffTimes = buffTimesOf(entity);
|
|
667
|
+
if (hud.shareAbilities !== void 0 && entity.abilities.length) {
|
|
668
|
+
const share = ruleOf(world, entity, hud.shareAbilities);
|
|
669
|
+
if (share === "icons" || share === "cooldowns") {
|
|
670
|
+
const abilities = [];
|
|
671
|
+
for (const { id, level, cooldown } of entity.abilities) if (level >= 1) abilities.push(share === "cooldowns" ? {
|
|
672
|
+
id,
|
|
673
|
+
level,
|
|
674
|
+
cooldown
|
|
675
|
+
} : {
|
|
676
|
+
id,
|
|
677
|
+
level
|
|
678
|
+
});
|
|
679
|
+
if (abilities.length) state.abilities = abilities;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (entity.get(deposit)) state.resource = entity.get(deposit).amount;
|
|
683
|
+
if (entity.on !== void 0) state.on = entity.on;
|
|
684
|
+
if (entity.asleep) state.asleep = true;
|
|
685
|
+
if (entity.scale !== 1) state.scale = entity.scale;
|
|
686
|
+
if (entity.look) state.look = entity.look;
|
|
687
|
+
if (world.sight.isInvisible(entity)) state.unseen = true;
|
|
688
|
+
if (name !== void 0) state.name = name;
|
|
689
|
+
if (entity.expires !== null && !entity.has(illusion)) state.lifetime = {
|
|
690
|
+
left: entity.expires,
|
|
691
|
+
total: entity.lifetime
|
|
692
|
+
};
|
|
693
|
+
return state;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Whether an entity has a component replicated to its owner, or shaping itself for each player (`Component.snapshot`)
|
|
697
|
+
*
|
|
698
|
+
* @internal
|
|
699
|
+
*/
|
|
700
|
+
function hasOnePlayersPart(world, entity) {
|
|
701
|
+
for (const held of entity.heldComponents()) {
|
|
702
|
+
const component = world.registry.component(held.id);
|
|
703
|
+
if (component?.replicate === "owner" || component?.replicate && component.snapshot) return true;
|
|
704
|
+
}
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* What one player is told of an entity, on what everyone is (`shared`, `commonEntityState`): an illusion's lifetime on
|
|
709
|
+
* its own side, the components replicated to the player and what they make of themselves for them, and its own part
|
|
710
|
+
* (`own`: its orders, where it goes, its abilities) for one of the player's side. `shared` itself where there is
|
|
711
|
+
* nothing more
|
|
712
|
+
*
|
|
713
|
+
* @internal
|
|
714
|
+
*/
|
|
715
|
+
function playerEntityState(view, entity, shared) {
|
|
716
|
+
const { world, player, observer } = view;
|
|
717
|
+
let state = shared;
|
|
718
|
+
/** This player's copy of the shared state, made once something of this player's is added */
|
|
719
|
+
const mine = () => state === shared ? state = { ...shared } : state;
|
|
720
|
+
if (entity.expires !== null && entity.has(illusion) && view.isAlly(entity.owner)) mine().lifetime = {
|
|
721
|
+
left: entity.expires,
|
|
722
|
+
total: entity.lifetime
|
|
723
|
+
};
|
|
724
|
+
let seen;
|
|
725
|
+
for (const held of entity.heldComponents()) {
|
|
726
|
+
const { id } = held;
|
|
727
|
+
const component = world.registry.component(id);
|
|
728
|
+
const audience = component?.replicate;
|
|
729
|
+
if (!audience) continue;
|
|
730
|
+
const ally = view.isAlly(entity.owner);
|
|
731
|
+
if (!(audience === "everyone" || (audience === "allies" ? ally : entity.owner === player))) continue;
|
|
732
|
+
const value = entity.get(held);
|
|
733
|
+
const sent = component.snapshot ? component.snapshot(value, entity, seen ??= {
|
|
734
|
+
world,
|
|
735
|
+
player: observer ? void 0 : player,
|
|
736
|
+
own: entity.owner === player,
|
|
737
|
+
ally,
|
|
738
|
+
observer,
|
|
739
|
+
knows: view.knows,
|
|
740
|
+
isAlly: view.isAlly,
|
|
741
|
+
sees: view.sees
|
|
742
|
+
}) : structuredClone(value);
|
|
743
|
+
if (sent !== void 0) (mine().components ??= {})[id] = sent;
|
|
744
|
+
}
|
|
745
|
+
if (!view.isAlly(entity.owner)) return state;
|
|
746
|
+
const shareBuffTimes = world.rules.hud.shareBuffTimes;
|
|
747
|
+
if (shareBuffTimes !== void 0 && entity.buffs.length && !shared.buffTimes && ruleOf(world, entity, shareBuffTimes) === "allies") mine().buffTimes = buffTimesOf(entity);
|
|
748
|
+
const m = entity.get(moving);
|
|
749
|
+
let waypoints;
|
|
750
|
+
for (let i = 1; i < entity.orders.length; i++) {
|
|
751
|
+
const order = entity.orders[i];
|
|
752
|
+
const target = world.get(order.init.target);
|
|
753
|
+
const point = target && view.knows(target) ? target.pos : order.init.point;
|
|
754
|
+
if (point) (waypoints ??= []).push({
|
|
755
|
+
x: point.x,
|
|
756
|
+
z: point.z
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
const own = mine().own = {
|
|
760
|
+
...entity.owner !== player && !observer && world.mayCommand(player, entity) && { shared: true },
|
|
761
|
+
hidden: entity.hidden,
|
|
762
|
+
order: entity.orders[0]?.type ?? null,
|
|
763
|
+
build: entity.orders[0]?.type === "build" && !orderSays(world, entity, "building") ? entity.orders[0].init.unitType : void 0,
|
|
764
|
+
orders: entity.orders.length,
|
|
765
|
+
passive: entity.get(fighting)?.passive ?? false,
|
|
766
|
+
fighting: !!entity.get(fighting)?.autoTarget,
|
|
767
|
+
goal: m?.status === "moving" ? {
|
|
768
|
+
x: m.navGoal.x,
|
|
769
|
+
z: m.navGoal.z
|
|
770
|
+
} : void 0,
|
|
771
|
+
...waypoints && { waypoints }
|
|
772
|
+
};
|
|
773
|
+
if (entity.abilities.length) {
|
|
774
|
+
const hooked = world.hooks.has("canCast");
|
|
775
|
+
own.abilities = entity.abilities.map(({ id, level, cooldown, charges, refill, autocast }) => {
|
|
776
|
+
const info = {
|
|
777
|
+
id,
|
|
778
|
+
level,
|
|
779
|
+
cooldown,
|
|
780
|
+
autocast
|
|
781
|
+
};
|
|
782
|
+
if (charges !== void 0) Object.assign(info, {
|
|
783
|
+
charges,
|
|
784
|
+
refill
|
|
785
|
+
});
|
|
786
|
+
if (!hooked || level < 1) return info;
|
|
787
|
+
const ability = world.registry.ability(id);
|
|
788
|
+
const declared = declaredRefusal(world, entity, ability);
|
|
789
|
+
const decided = notReady(world, entity, ability);
|
|
790
|
+
if (declared?.key !== decided?.key) info.decided = decided ?? null;
|
|
791
|
+
return info;
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return state;
|
|
795
|
+
}
|
|
796
|
+
//#endregion
|
|
797
|
+
//#region src/session/local.ts
|
|
798
|
+
/**
|
|
799
|
+
* A seat at a world running in the same page. It does the job a server will do later:
|
|
800
|
+
* run the simulation, and hand the player only what their team is allowed to know.
|
|
801
|
+
*/
|
|
802
|
+
/** The effects of the rules a player was shown that are kept, for a stop to be told to them: the latest so many */
|
|
803
|
+
var MOST_EFFECTS = 1024;
|
|
804
|
+
/**
|
|
805
|
+
* The events a computer player's seat hears (`forComputer`): the facts a brain reads and a save keeps for it (deaths,
|
|
806
|
+
* damage, healing, pings, chat, the game's own events) and the answers to its commands. The rest is for a screen (an
|
|
807
|
+
* attack's swing, a shot, a buff, a hammer blow, a line said), and a computer's seat is spared hearing it
|
|
808
|
+
*
|
|
809
|
+
* @beta
|
|
810
|
+
*/
|
|
811
|
+
var COMPUTER_EVENTS = /* @__PURE__ */ new Set([
|
|
812
|
+
"death",
|
|
813
|
+
"damage",
|
|
814
|
+
"heal",
|
|
815
|
+
"ping",
|
|
816
|
+
"chat",
|
|
817
|
+
"game",
|
|
818
|
+
"command"
|
|
819
|
+
]);
|
|
820
|
+
var SHARED = /* @__PURE__ */ new WeakMap();
|
|
821
|
+
/**
|
|
822
|
+
* From here until `endSharedCaptures`, the sessions of the world describe each entity once between them (what
|
|
823
|
+
* everyone is told; what is one player's goes on a copy): for a host's round of sending, where every seat is captured
|
|
824
|
+
* from the same world. Not while the world changes: what was described is said again
|
|
825
|
+
*
|
|
826
|
+
* @internal
|
|
827
|
+
*/
|
|
828
|
+
function shareCaptures(world) {
|
|
829
|
+
SHARED.set(world, {
|
|
830
|
+
common: /* @__PURE__ */ new Map(),
|
|
831
|
+
sides: /* @__PURE__ */ new Map()
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
/** @internal */
|
|
835
|
+
function endSharedCaptures(world) {
|
|
836
|
+
SHARED.delete(world);
|
|
837
|
+
}
|
|
838
|
+
/** @beta */
|
|
839
|
+
var LocalSession = class {
|
|
840
|
+
world;
|
|
841
|
+
playerId;
|
|
842
|
+
terrain;
|
|
843
|
+
environment;
|
|
844
|
+
doodads;
|
|
845
|
+
regions;
|
|
846
|
+
cameras;
|
|
847
|
+
paths;
|
|
848
|
+
registry;
|
|
849
|
+
tickRate;
|
|
850
|
+
alpha = 1;
|
|
851
|
+
/** What the world says (`World.log`): a session in the page has it, and the client shows it with its own */
|
|
852
|
+
get log() {
|
|
853
|
+
return this.world.log;
|
|
854
|
+
}
|
|
855
|
+
/** Why the game ended on an error, once it has (`World.failure`) */
|
|
856
|
+
get failure() {
|
|
857
|
+
return this.world.failure;
|
|
858
|
+
}
|
|
859
|
+
/** Simulation speed multiplier, 0 pauses. A debug control, a real server sets the pace itself */
|
|
860
|
+
speed = 1;
|
|
861
|
+
accumulator = 0;
|
|
862
|
+
/** Whether the team saw the whole map at the last snapshot (the debug switch): what it saw then only is forgotten after */
|
|
863
|
+
wasRevealed = false;
|
|
864
|
+
/** What the player's team remembers, or an observer's own (`SessionView.memory`) */
|
|
865
|
+
get memory() {
|
|
866
|
+
return this.view.memory;
|
|
867
|
+
}
|
|
868
|
+
/** Ids of what made it into the snapshot being put together as in sight. Kept to be cleared, not made anew */
|
|
869
|
+
inSight = /* @__PURE__ */ new Set();
|
|
870
|
+
/** The names the map or the rules gave entities, by id, as of the snapshot being put together (`world.named`) */
|
|
871
|
+
namesNow = /* @__PURE__ */ new Map();
|
|
872
|
+
disposers = [];
|
|
873
|
+
pendingCheckpoint;
|
|
874
|
+
checkpoint;
|
|
875
|
+
debug;
|
|
876
|
+
/** Its pace, saves of its world, the debug tools' view, and what a computer player's brain reads (`SessionCan`) */
|
|
877
|
+
can = {
|
|
878
|
+
pace: paceOf(this),
|
|
879
|
+
saves: savesOf(this),
|
|
880
|
+
inspect: void 0,
|
|
881
|
+
computer: {
|
|
882
|
+
mines: () => this.mines(),
|
|
883
|
+
lanes: () => this.lanes(),
|
|
884
|
+
pendingEvents: () => this.pendingEvents(),
|
|
885
|
+
restoreEvents: (events) => this.restoreEvents(events)
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
/**
|
|
889
|
+
* Who only watches, and sees everything: every entity, the whole map, every player's resources and heroes. From
|
|
890
|
+
* `playerId`'s side all the same (who is an ally, an enemy). For a server's watchers, whose commands go nowhere
|
|
891
|
+
*/
|
|
892
|
+
observer;
|
|
893
|
+
/**
|
|
894
|
+
* Whose seat is a computer player's (`forComputer`): its snapshots leave out projectiles, missiles, corpses, reveals
|
|
895
|
+
* and the events a brain never reads, and it listens for none of those, so twelve computers cost the world less
|
|
896
|
+
*/
|
|
897
|
+
forComputer;
|
|
898
|
+
/** The seat's side of the world: whose it is, what they may know, what was told to them since the last snapshot */
|
|
899
|
+
view;
|
|
900
|
+
constructor(world, playerId, options = {}) {
|
|
901
|
+
this.world = world;
|
|
902
|
+
this.playerId = playerId;
|
|
903
|
+
this.observer = options.observer ?? false;
|
|
904
|
+
this.ownsWorld = options.ownsWorld ?? false;
|
|
905
|
+
this.forComputer = options.forComputer ?? false;
|
|
906
|
+
this.view = new SessionView(world, playerId, {
|
|
907
|
+
observer: this.observer,
|
|
908
|
+
forComputer: this.forComputer,
|
|
909
|
+
mostEffects: options.mostEffects ?? MOST_EFFECTS
|
|
910
|
+
});
|
|
911
|
+
if (options.debug) world.debugPlayers.add(playerId);
|
|
912
|
+
this.terrain = world.terrain;
|
|
913
|
+
this.environment = world.map.environment;
|
|
914
|
+
this.doodads = world.map.doodads;
|
|
915
|
+
this.regions = world.map.regions;
|
|
916
|
+
this.cameras = world.map.cameras;
|
|
917
|
+
this.paths = world.map.paths;
|
|
918
|
+
this.registry = world.registry;
|
|
919
|
+
this.tickRate = world.tickRate;
|
|
920
|
+
this.debug = new LocalDebug(world);
|
|
921
|
+
this.can.inspect = this.debug;
|
|
922
|
+
const { events } = world;
|
|
923
|
+
const { view } = this;
|
|
924
|
+
for (const replication of world.registry.replications) {
|
|
925
|
+
if (!this.hears(replication.as)) continue;
|
|
926
|
+
this.disposers.push(events.on(replication.event, (payload) => {
|
|
927
|
+
const said = replication.tell(payload, view);
|
|
928
|
+
if (said) view.events.push(said);
|
|
929
|
+
}));
|
|
930
|
+
}
|
|
931
|
+
this.disposers.push(events.on(checkpoint, ({ label }) => this.pendingCheckpoint = label));
|
|
932
|
+
if (this.hears("game")) this.disposers.push(events.watch((type, payload) => {
|
|
933
|
+
if (typeof type !== "string") return;
|
|
934
|
+
const audience = world.registry.event(type)?.replicate;
|
|
935
|
+
if (!audience) return;
|
|
936
|
+
if (audience !== "everyone" && !audience(payload, view)) return;
|
|
937
|
+
if (this.world.checks.events) assertPlainData(payload, `Event "${type}"`);
|
|
938
|
+
view.events.push({
|
|
939
|
+
type: "game",
|
|
940
|
+
event: type,
|
|
941
|
+
payload
|
|
942
|
+
});
|
|
943
|
+
}));
|
|
944
|
+
}
|
|
945
|
+
/** Whether the seat is told of events of a kind: a player's seat of all of them, a computer's of what a brain reads */
|
|
946
|
+
hears(kind) {
|
|
947
|
+
return !this.forComputer || COMPUTER_EVENTS.has(kind);
|
|
948
|
+
}
|
|
949
|
+
/** The player's team: theirs now, as the rules may move them to another (`setTeam`) */
|
|
950
|
+
get team() {
|
|
951
|
+
return this.view.team;
|
|
952
|
+
}
|
|
953
|
+
isAlly(owner) {
|
|
954
|
+
return this.view.isAlly(owner);
|
|
955
|
+
}
|
|
956
|
+
/** Whether the player's team sees a spot of the map right now: an observer sees all of it */
|
|
957
|
+
sees(x, z) {
|
|
958
|
+
return this.view.sees(x, z);
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* How many effects of the rules are kept for their stop to be told (`mostEffects`), for a test of the bound
|
|
962
|
+
*
|
|
963
|
+
* @internal
|
|
964
|
+
*/
|
|
965
|
+
get keptEffects() {
|
|
966
|
+
return shownEffects(this.view).size;
|
|
967
|
+
}
|
|
968
|
+
/** Whether the player's team can see the entity right now */
|
|
969
|
+
knows(entity) {
|
|
970
|
+
return this.view.knows(entity);
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* What everyone in sight of an entity is told of it this tick, the same for every seat: worked out once for all
|
|
974
|
+
* the sessions of a world while a host's round of sending is on (`shareCaptures`), else for this session alone
|
|
975
|
+
*/
|
|
976
|
+
common(entity) {
|
|
977
|
+
const shared = SHARED.get(this.world)?.common;
|
|
978
|
+
const known = shared?.get(entity.id);
|
|
979
|
+
if (known) return known;
|
|
980
|
+
const state = commonEntityState(this.world, entity, this.namesNow.get(entity.id));
|
|
981
|
+
shared?.set(entity.id, state);
|
|
982
|
+
return state;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* What this player is told of an entity: what everyone is (`common`, shared between the seats of a host), and on a
|
|
986
|
+
* copy of it what is this player's to know: an illusion's lifetime on its own side, the components replicated to it
|
|
987
|
+
* and what they make of themselves for it, and its own part (`own`) for an ally's
|
|
988
|
+
*/
|
|
989
|
+
describe(entity) {
|
|
990
|
+
const side = this.sideNow;
|
|
991
|
+
if (!side) return playerEntityState(this.view, entity, this.common(entity));
|
|
992
|
+
if (this.isOnePlayers(entity)) {
|
|
993
|
+
const state = playerEntityState(this.view, entity, this.common(entity));
|
|
994
|
+
const first = side.alike.get(entity.id);
|
|
995
|
+
if (!first) side.alike.set(entity.id, state);
|
|
996
|
+
else if (first !== state && sameEntity(first, state)) return first;
|
|
997
|
+
return state;
|
|
998
|
+
}
|
|
999
|
+
const known = side.all.get(entity.id);
|
|
1000
|
+
if (known) return known;
|
|
1001
|
+
const state = playerEntityState(this.view, entity, this.common(entity));
|
|
1002
|
+
side.all.set(entity.id, state);
|
|
1003
|
+
return state;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* What the seats of this one's side are told alike this round (`Round.sides`), by entity: none outside a host's round.
|
|
1007
|
+
* Set as the capture starts (`capture`)
|
|
1008
|
+
*/
|
|
1009
|
+
sideNow;
|
|
1010
|
+
/**
|
|
1011
|
+
* Which seats are told the same of an entity where nothing of it is one player's: an observer's, or a team's with the
|
|
1012
|
+
* same allies among the players (relations are a pair of players', not a team's)
|
|
1013
|
+
*/
|
|
1014
|
+
sideKey() {
|
|
1015
|
+
if (this.observer) return "observer";
|
|
1016
|
+
let key = `team ${this.team}:`;
|
|
1017
|
+
for (const player of this.world.players.keys()) key += this.isAlly(player) ? "1" : "0";
|
|
1018
|
+
return key;
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Whether what this player is told of an entity is theirs alone, not their side's: a component replicated to its owner
|
|
1022
|
+
* or shaping itself for each player (`Component.snapshot`), or control of another's unit shared with this player
|
|
1023
|
+
*/
|
|
1024
|
+
isOnePlayers(entity) {
|
|
1025
|
+
if (hasOnePlayersPart(this.world, entity)) return true;
|
|
1026
|
+
return entity.owner !== this.playerId && !this.observer && this.isAlly(entity.owner) && this.world.mayCommand(this.playerId, entity);
|
|
1027
|
+
}
|
|
1028
|
+
/** Where in the list being captured are those that may be one player's (`SideSeen.mine`): while the first seat of a side captures */
|
|
1029
|
+
mineNow;
|
|
1030
|
+
/**
|
|
1031
|
+
* What the team remembers of a structure is every teammate's to be told, not this seat's: without the parts of the
|
|
1032
|
+
* description shaped for one player (components replicated to the owner, or made by their `snapshot` for whoever is
|
|
1033
|
+
* told), which the seat that happened to see it first would otherwise give to the whole team
|
|
1034
|
+
*/
|
|
1035
|
+
forTeam(entity, state) {
|
|
1036
|
+
if (!state.components || !hasOnePlayersPart(this.world, entity)) return state;
|
|
1037
|
+
const components = {};
|
|
1038
|
+
for (const [id, part] of Object.entries(state.components)) {
|
|
1039
|
+
const component = this.world.registry.component(id);
|
|
1040
|
+
if (component?.replicate === "owner" || component?.replicate && component.snapshot) continue;
|
|
1041
|
+
components[id] = part;
|
|
1042
|
+
}
|
|
1043
|
+
return {
|
|
1044
|
+
...state,
|
|
1045
|
+
components
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
/** Adds what the player sees of an entity in sight to the snapshot, and remembers structures for when they are out of sight */
|
|
1049
|
+
see(entity, out, remembered) {
|
|
1050
|
+
const before = remembered[entity.id];
|
|
1051
|
+
const kept = entity.isResource && before?.visible && before.resource === entity.get(deposit)?.amount;
|
|
1052
|
+
const state = kept ? before : this.describe(entity);
|
|
1053
|
+
if (!kept && this.mineNow && (this.isAlly(entity.owner) || hasOnePlayersPart(this.world, entity))) this.mineNow.push(out.length);
|
|
1054
|
+
out.push(state);
|
|
1055
|
+
this.inSight.add(entity.id);
|
|
1056
|
+
if (entity.isStructure && !this.isAlly(entity.owner)) {
|
|
1057
|
+
if (state !== before) remembered[entity.id] = this.forTeam(entity, state);
|
|
1058
|
+
} else if (before) delete remembered[entity.id];
|
|
1059
|
+
}
|
|
1060
|
+
/** Throws when what was taken to be in sight is not what testing every entity gives. See `debugChecks` */
|
|
1061
|
+
checkSight() {
|
|
1062
|
+
for (const entity of this.world.entities.values()) {
|
|
1063
|
+
if (!entity.alive || this.knows(entity) === this.inSight.has(entity.id)) continue;
|
|
1064
|
+
const said = this.inSight.has(entity.id) ? "in sight" : "out of sight";
|
|
1065
|
+
throw new Error(`Snapshot for player ${this.playerId} has ${entity.def.id}#${entity.id} ${said}, it is not`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
/** The scenery the player's team sees right now */
|
|
1069
|
+
*sceneryInSight() {
|
|
1070
|
+
const { world } = this;
|
|
1071
|
+
if (this.observer || world.fog.isRevealed(this.team)) {
|
|
1072
|
+
for (const entity of world.entities.values()) if (entity.scenery) yield entity;
|
|
1073
|
+
} else yield* world.sceneryInSight.get(this.team) ?? [];
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* On a map explored from the start, what stands on it and belongs to no player (trees, mines, rocks, neutral
|
|
1077
|
+
* buildings, the creeps' own) known to the team from the first snapshot, as it stands, out of sight: the genre's melee
|
|
1078
|
+
* maps. Once for a team (`TeamMemory.startKnown`); units, and players' buildings, only once seen
|
|
1079
|
+
*/
|
|
1080
|
+
knowStart(remembered) {
|
|
1081
|
+
const { world } = this;
|
|
1082
|
+
if (this.observer || !world.fog.exploredFromStart) return;
|
|
1083
|
+
const memory = this.memory;
|
|
1084
|
+
if (memory.startKnown) return;
|
|
1085
|
+
memory.startKnown = true;
|
|
1086
|
+
for (const entity of world.entities.values()) {
|
|
1087
|
+
if (!entity.alive || !entity.isStructure || this.inSight.has(entity.id) || remembered[entity.id]) continue;
|
|
1088
|
+
const owner = world.players.get(entity.owner);
|
|
1089
|
+
if (!owner?.neutral && !owner?.creeps) continue;
|
|
1090
|
+
remembered[entity.id] = {
|
|
1091
|
+
...this.forTeam(entity, this.describe(entity)),
|
|
1092
|
+
visible: false
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
capture() {
|
|
1097
|
+
const { world } = this;
|
|
1098
|
+
const round = SHARED.get(world);
|
|
1099
|
+
if (round) {
|
|
1100
|
+
const key = this.sideKey();
|
|
1101
|
+
let side = round.sides.get(key);
|
|
1102
|
+
if (!side) round.sides.set(key, side = {
|
|
1103
|
+
all: /* @__PURE__ */ new Map(),
|
|
1104
|
+
alike: /* @__PURE__ */ new Map()
|
|
1105
|
+
});
|
|
1106
|
+
this.sideNow = side;
|
|
1107
|
+
} else this.sideNow = void 0;
|
|
1108
|
+
const remembered = this.memory.structures;
|
|
1109
|
+
this.namesNow.clear();
|
|
1110
|
+
if (world.states.has(names)) for (const [name, id] of Object.entries(world.state(names))) this.namesNow.set(id, name);
|
|
1111
|
+
const seen = this.sideNow?.seen;
|
|
1112
|
+
const entities = seen ? this.seeAsSide(seen) : this.seeAll(remembered);
|
|
1113
|
+
if (this.world.checks.sight) this.checkSight();
|
|
1114
|
+
if (seen) return this.finish(entities);
|
|
1115
|
+
this.knowStart(remembered);
|
|
1116
|
+
const revealed = this.observer || world.fog.isRevealed(this.team);
|
|
1117
|
+
if (this.wasRevealed && !revealed) {
|
|
1118
|
+
for (const [key, state] of Object.entries(remembered)) if (!this.inSight.has(Number(key)) && !world.fog.isExplored(this.team, state.x, state.z)) delete remembered[Number(key)];
|
|
1119
|
+
}
|
|
1120
|
+
this.wasRevealed = revealed;
|
|
1121
|
+
const forget = world.rules.fog?.explored === "nothing" && !revealed;
|
|
1122
|
+
for (const [key, state] of Object.entries(remembered)) {
|
|
1123
|
+
const id = Number(key);
|
|
1124
|
+
if (this.inSight.has(id)) continue;
|
|
1125
|
+
if (forget) {
|
|
1126
|
+
delete remembered[id];
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
if (!world.get(id) && this.sees(state.x, state.z)) {
|
|
1130
|
+
delete remembered[id];
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
const memory = state.visible ? {
|
|
1134
|
+
...state,
|
|
1135
|
+
visible: false
|
|
1136
|
+
} : state;
|
|
1137
|
+
if (memory !== state) remembered[id] = memory;
|
|
1138
|
+
entities.push(memory);
|
|
1139
|
+
}
|
|
1140
|
+
if (this.sideNow) this.sideNow.seen = {
|
|
1141
|
+
entities: [...entities],
|
|
1142
|
+
inSight: new Set(this.inSight),
|
|
1143
|
+
mine: this.mineNow ?? []
|
|
1144
|
+
};
|
|
1145
|
+
this.mineNow = void 0;
|
|
1146
|
+
return this.finish(entities);
|
|
1147
|
+
}
|
|
1148
|
+
/** The entities in sight, and what the side remembers out of it, as the first seat of a side (or a seat on its own) sees them */
|
|
1149
|
+
seeAll(remembered) {
|
|
1150
|
+
const { world } = this;
|
|
1151
|
+
const entities = [];
|
|
1152
|
+
this.inSight.clear();
|
|
1153
|
+
this.mineNow = this.sideNow ? [] : void 0;
|
|
1154
|
+
for (const entity of world.actors.values()) if (entity.alive && this.knows(entity)) this.see(entity, entities, remembered);
|
|
1155
|
+
for (const entity of this.sceneryInSight()) if (entity.alive && entity.scenery) this.see(entity, entities, remembered);
|
|
1156
|
+
return entities;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* What another seat of the side saw this round, as this one sees it: the same, but what may be this player's alone
|
|
1160
|
+
* described again for them. The side's memory is the first seat's to keep
|
|
1161
|
+
*/
|
|
1162
|
+
seeAsSide(seen) {
|
|
1163
|
+
const entities = [...seen.entities];
|
|
1164
|
+
this.inSight.clear();
|
|
1165
|
+
for (const id of seen.inSight) this.inSight.add(id);
|
|
1166
|
+
for (const at of seen.mine) entities[at] = this.describe(this.world.get(entities[at].id));
|
|
1167
|
+
this.wasRevealed = this.observer || this.world.fog.isRevealed(this.team);
|
|
1168
|
+
return entities;
|
|
1169
|
+
}
|
|
1170
|
+
/** The rest of a snapshot, round its entities: the players, and every feature's part (`SnapshotPart`) */
|
|
1171
|
+
finish(entities) {
|
|
1172
|
+
const { world } = this;
|
|
1173
|
+
const players = [];
|
|
1174
|
+
for (const player of world.players.values()) {
|
|
1175
|
+
const state = {
|
|
1176
|
+
id: player.id,
|
|
1177
|
+
name: player.name,
|
|
1178
|
+
team: player.team,
|
|
1179
|
+
color: player.color,
|
|
1180
|
+
neutral: player.neutral,
|
|
1181
|
+
creeps: player.creeps || void 0,
|
|
1182
|
+
...player.race !== void 0 && { race: player.race },
|
|
1183
|
+
defeated: player.defeated,
|
|
1184
|
+
relation: player.id === this.playerId ? "self" : world.areAllies(this.playerId, player.id) ? "ally" : world.areEnemies(this.playerId, player.id) ? "enemy" : "neutral"
|
|
1185
|
+
};
|
|
1186
|
+
if (state.relation === "ally" && !player.neutral && !this.observer) {
|
|
1187
|
+
const me = world.player(this.playerId);
|
|
1188
|
+
state.shared = {
|
|
1189
|
+
control: world.sides.controlOf(this.playerId).includes(player.id),
|
|
1190
|
+
vision: me.team === player.team || world.sides.visionOf(me.team).includes(player.team)
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
if (player.id === this.playerId || this.observer && !player.neutral) {
|
|
1194
|
+
state.own = {
|
|
1195
|
+
resources: { ...player.resources },
|
|
1196
|
+
supplyUsed: player.supplyUsed,
|
|
1197
|
+
supplyCap: player.supplyCap,
|
|
1198
|
+
...Object.keys(player.supplies).length > 0 && { supplies: structuredClone(player.supplies) },
|
|
1199
|
+
upkeep: world.rules.economy.upkeep(world, player.id),
|
|
1200
|
+
upgrades: Object.fromEntries(player.upgrades),
|
|
1201
|
+
researching: [...player.researching],
|
|
1202
|
+
tech: [...techOf(world, player.id)],
|
|
1203
|
+
heroes: heroSummaries(world, player.id),
|
|
1204
|
+
objectives: world.objectives.of(player.id).map((objective) => structuredClone(objective)),
|
|
1205
|
+
...world.timers.shownTo(player.id).length && { timers: world.timers.shownTo(player.id) }
|
|
1206
|
+
};
|
|
1207
|
+
if (world.hooks.has("requirement") && player.id === this.playerId) state.own.unlocks = decidedUnlocks(world, player.id);
|
|
1208
|
+
if (world.hooks.has("available") && player.id === this.playerId) {
|
|
1209
|
+
const availability = decidedAvailability(world, player.id);
|
|
1210
|
+
if (availability) state.own.availability = availability;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
players.push(state);
|
|
1214
|
+
}
|
|
1215
|
+
const { view } = this;
|
|
1216
|
+
const snapshot = { tick: world.tick };
|
|
1217
|
+
const parts = world.registry.snapshotParts;
|
|
1218
|
+
const take = (part) => {
|
|
1219
|
+
const value = part.capture(view);
|
|
1220
|
+
if (value !== void 0) snapshot[part.key] = value;
|
|
1221
|
+
};
|
|
1222
|
+
for (const part of parts) if (part.head) take(part);
|
|
1223
|
+
snapshot.entities = entities;
|
|
1224
|
+
snapshot.players = players;
|
|
1225
|
+
for (const part of parts) if (!part.head) take(part);
|
|
1226
|
+
return snapshot;
|
|
1227
|
+
}
|
|
1228
|
+
snapshot() {
|
|
1229
|
+
return this.capture();
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Moves the game on by `ticks` at the next update, however fast it goes: a step of a paused game (`speed` 0), as
|
|
1233
|
+
* an editor's Step does. Ticks asked for while it runs are taken on top of its pace
|
|
1234
|
+
*/
|
|
1235
|
+
advance(ticks = 1) {
|
|
1236
|
+
this.accumulator += this.world.dt * Math.max(0, Math.floor(ticks));
|
|
1237
|
+
}
|
|
1238
|
+
/** A save of the world as it stands (`saveWorld`): at once, as a promise for what a session elsewhere answers later */
|
|
1239
|
+
save() {
|
|
1240
|
+
try {
|
|
1241
|
+
return Promise.resolve(saveWorld(this.world));
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
return Promise.reject(error);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Runs the game on by itself, a tick at a time, as fast as the page can, until `until` holds of the world, `most`
|
|
1248
|
+
* ticks have run, the game is over, or `deadline` (`performance.now()`'s) is past: what an editor's "Play to" does a
|
|
1249
|
+
* slice a frame, its pace and `advance` aside. The ticks are the same ticks the game runs at its own pace, so the
|
|
1250
|
+
* same game comes to the same moment. What happened in them is in the next snapshot (`update`), as after a slow frame.
|
|
1251
|
+
* `until` is asked before the first tick too: what holds already runs none
|
|
1252
|
+
*/
|
|
1253
|
+
runUntil(until, options = {}) {
|
|
1254
|
+
const { world } = this;
|
|
1255
|
+
const most = options.most ?? Infinity;
|
|
1256
|
+
const deadline = options.deadline ?? Infinity;
|
|
1257
|
+
let ticks = 0;
|
|
1258
|
+
const over = () => world.failure !== null || world.winner !== null;
|
|
1259
|
+
while (!until(world)) {
|
|
1260
|
+
if (ticks >= most || over()) return this.ran(ticks, false, over());
|
|
1261
|
+
if (ticks % 8 === 7 && performance.now() > deadline) return this.ran(ticks, false, false);
|
|
1262
|
+
this.tickOnce();
|
|
1263
|
+
ticks++;
|
|
1264
|
+
}
|
|
1265
|
+
return this.ran(ticks, true, over());
|
|
1266
|
+
}
|
|
1267
|
+
/** What `runUntil` gives back, the debug tools' view brought up to date where it ran */
|
|
1268
|
+
ran(ticks, reached, over) {
|
|
1269
|
+
if (ticks) {
|
|
1270
|
+
this.debug.refresh();
|
|
1271
|
+
this.ranOn = true;
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
ticks,
|
|
1275
|
+
reached,
|
|
1276
|
+
over
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
/** Whether `runUntil` ran ticks since the last update: its next snapshot describes them, paused or not */
|
|
1280
|
+
ranOn = false;
|
|
1281
|
+
/** A tick of the world, and the checkpoint it marked saved as the world stood at its end, not at the end of the frame */
|
|
1282
|
+
tickOnce() {
|
|
1283
|
+
this.world.step();
|
|
1284
|
+
if (this.pendingCheckpoint) {
|
|
1285
|
+
this.checkpoint = {
|
|
1286
|
+
label: this.pendingCheckpoint,
|
|
1287
|
+
save: saveWorld(this.world)
|
|
1288
|
+
};
|
|
1289
|
+
this.pendingCheckpoint = void 0;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
update(dt) {
|
|
1293
|
+
const { world } = this;
|
|
1294
|
+
let stepped = this.ranOn;
|
|
1295
|
+
this.ranOn = false;
|
|
1296
|
+
this.accumulator += dt * this.speed * world.cheats.speed;
|
|
1297
|
+
while (this.accumulator >= world.dt) {
|
|
1298
|
+
this.tickOnce();
|
|
1299
|
+
this.accumulator -= world.dt;
|
|
1300
|
+
stepped = true;
|
|
1301
|
+
}
|
|
1302
|
+
this.alpha = this.speed * world.cheats.speed > 0 ? this.accumulator / world.dt : 1;
|
|
1303
|
+
if (!stepped) return this.view.events.length ? [this.capture()] : [];
|
|
1304
|
+
this.debug.refresh();
|
|
1305
|
+
return [this.capture()];
|
|
1306
|
+
}
|
|
1307
|
+
/** Sends a command. `seq` is the sender's own number for it, for a host passing on what a remote player numbered */
|
|
1308
|
+
send(command, seq = ++this.lastSeq) {
|
|
1309
|
+
this.world.issue({
|
|
1310
|
+
...command,
|
|
1311
|
+
player: this.playerId,
|
|
1312
|
+
seq
|
|
1313
|
+
});
|
|
1314
|
+
return seq;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* The number of the last command sent: a computer's seat keeps it through a save, so the commands it sends after a
|
|
1318
|
+
* load are numbered as they would have been
|
|
1319
|
+
*
|
|
1320
|
+
* @beta
|
|
1321
|
+
*/
|
|
1322
|
+
lastSeq = 0;
|
|
1323
|
+
fog() {
|
|
1324
|
+
const { fog } = this.world;
|
|
1325
|
+
const cells = fog.cells(this.team);
|
|
1326
|
+
return {
|
|
1327
|
+
width: fog.width,
|
|
1328
|
+
depth: fog.depth,
|
|
1329
|
+
version: fog.version,
|
|
1330
|
+
revealed: this.observer || fog.isRevealed(this.team) || !cells,
|
|
1331
|
+
visible: cells?.visible ?? /* @__PURE__ */ new Uint8Array(0),
|
|
1332
|
+
explored: cells?.explored ?? /* @__PURE__ */ new Uint8Array(0)
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
/** Where the game's money is mined, as the map shows it, seen or not (`GameSession.mines`, `minesFor`) */
|
|
1336
|
+
mines() {
|
|
1337
|
+
return minesFor(this.view);
|
|
1338
|
+
}
|
|
1339
|
+
/** The lanes of the map, as the map shows them (`GameSession.lanes`) */
|
|
1340
|
+
lanes() {
|
|
1341
|
+
return lanesOf(this.world);
|
|
1342
|
+
}
|
|
1343
|
+
/** The camps as the player knows them (`GameSession.camps`, `campsFor`): worked out once a tick */
|
|
1344
|
+
camps() {
|
|
1345
|
+
return campsFor(this.view);
|
|
1346
|
+
}
|
|
1347
|
+
canPlace(unitType, point) {
|
|
1348
|
+
return this.registry.units.has(unitType) && canPlaceStructure(this.world, this.registry.unit(unitType), point, this.playerId, true);
|
|
1349
|
+
}
|
|
1350
|
+
/** Which cells of a building's ground are in the way for the player: taken, or not explored by their team (`placementCells`) */
|
|
1351
|
+
placeCells(unitType, point) {
|
|
1352
|
+
if (!this.registry.units.has(unitType)) return void 0;
|
|
1353
|
+
const { world } = this;
|
|
1354
|
+
const { team } = world.player(this.playerId);
|
|
1355
|
+
return placementCells(this.registry.unit(unitType), point, (x, z) => world.grid.canPlace(x, z, 1, 1), (x, z) => world.fog.isExplored(team, x + .5, z + .5));
|
|
1356
|
+
}
|
|
1357
|
+
findPath(from, to, flying = false) {
|
|
1358
|
+
return (flying ? this.world.air : this.world.nav).findPath(from, to);
|
|
1359
|
+
}
|
|
1360
|
+
/** What happened since the last snapshot, not yet given in one: a copy, for a computer's save */
|
|
1361
|
+
pendingEvents() {
|
|
1362
|
+
return structuredClone(this.view.events);
|
|
1363
|
+
}
|
|
1364
|
+
/** Takes back what `pendingEvents` gave, ahead of what has happened since: in a game loaded from a save */
|
|
1365
|
+
restoreEvents(events) {
|
|
1366
|
+
this.view.events = [...structuredClone(events), ...this.view.events];
|
|
1367
|
+
}
|
|
1368
|
+
/** Whether the world goes with the session */
|
|
1369
|
+
ownsWorld;
|
|
1370
|
+
dispose() {
|
|
1371
|
+
for (const dispose of this.disposers.splice(0)) dispose();
|
|
1372
|
+
this.debug.dispose();
|
|
1373
|
+
if (this.ownsWorld) this.world.dispose();
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
//#endregion
|
|
1377
|
+
export { readMessage as _, RemoteDebug as a, bytesByKey as c, paceOf as d, savesOf as f, protocolMismatch as g, isFiniteNumber as h, LocalDebug as i, diff as l, isCommand as m, endSharedCaptures as n, askDebug as o, PROTOCOL_VERSION as p, shareCaptures as r, apply as s, LocalSession as t, quantize as u };
|
|
1378
|
+
|
|
1379
|
+
//# sourceMappingURL=local-Dkovq42k.js.map
|