@datamoc/mw_games 0.7.6 → 0.7.7
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/CHANGELOG.md +270 -0
- package/dist/actors/Inventory.d.ts +7 -1
- package/dist/actors/Inventory.d.ts.map +1 -1
- package/dist/actors/Inventory.js.map +1 -1
- package/dist/actors/craft.d.ts +35 -14
- package/dist/actors/craft.d.ts.map +1 -1
- package/dist/actors/craft.js +56 -24
- package/dist/actors/craft.js.map +1 -1
- package/dist/actors/index.d.ts +1 -1
- package/dist/actors/index.d.ts.map +1 -1
- package/dist/ai/Heuristics.d.ts +232 -0
- package/dist/ai/Heuristics.d.ts.map +1 -0
- package/dist/ai/Heuristics.js +298 -0
- package/dist/ai/Heuristics.js.map +1 -0
- package/dist/ai/index.d.ts +4 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +2 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/score.d.ts +126 -0
- package/dist/ai/score.d.ts.map +1 -0
- package/dist/ai/score.js +121 -0
- package/dist/ai/score.js.map +1 -0
- package/dist/assets/loader.d.ts +18 -4
- package/dist/assets/loader.d.ts.map +1 -1
- package/dist/assets/loader.js +40 -11
- package/dist/assets/loader.js.map +1 -1
- package/dist/audio/Positional.d.ts +125 -0
- package/dist/audio/Positional.d.ts.map +1 -0
- package/dist/audio/Positional.js +135 -0
- package/dist/audio/Positional.js.map +1 -0
- package/dist/audio/Sound.d.ts +7 -1
- package/dist/audio/Sound.d.ts.map +1 -1
- package/dist/audio/Sound.js +8 -2
- package/dist/audio/Sound.js.map +1 -1
- package/dist/audio/index.d.ts +2 -0
- package/dist/audio/index.d.ts.map +1 -1
- package/dist/audio/index.js +1 -0
- package/dist/audio/index.js.map +1 -1
- package/dist/battle/AttackDialog.d.ts +75 -0
- package/dist/battle/AttackDialog.d.ts.map +1 -0
- package/dist/battle/AttackDialog.js +115 -0
- package/dist/battle/AttackDialog.js.map +1 -0
- package/dist/battle/AttackPreview.d.ts +70 -0
- package/dist/battle/AttackPreview.d.ts.map +1 -0
- package/dist/battle/AttackPreview.js +78 -0
- package/dist/battle/AttackPreview.js.map +1 -0
- package/dist/battle/BattleStats.d.ts +45 -0
- package/dist/battle/BattleStats.d.ts.map +1 -0
- package/dist/battle/BattleStats.js +63 -0
- package/dist/battle/BattleStats.js.map +1 -0
- package/dist/battle/UnitSelector.d.ts +72 -0
- package/dist/battle/UnitSelector.d.ts.map +1 -0
- package/dist/battle/UnitSelector.js +117 -0
- package/dist/battle/UnitSelector.js.map +1 -0
- package/dist/battle/Whiteboard.d.ts +50 -0
- package/dist/battle/Whiteboard.d.ts.map +1 -0
- package/dist/battle/Whiteboard.js +94 -0
- package/dist/battle/Whiteboard.js.map +1 -0
- package/dist/battle/index.d.ts +10 -0
- package/dist/battle/index.d.ts.map +1 -1
- package/dist/battle/index.js +5 -0
- package/dist/battle/index.js.map +1 -1
- package/dist/board/FogOfWar.d.ts +32 -2
- package/dist/board/FogOfWar.d.ts.map +1 -1
- package/dist/board/FogOfWar.js +70 -6
- package/dist/board/FogOfWar.js.map +1 -1
- package/dist/core/Achievements.d.ts +40 -7
- package/dist/core/Achievements.d.ts.map +1 -1
- package/dist/core/Achievements.js +51 -7
- package/dist/core/Achievements.js.map +1 -1
- package/dist/core/Hex.d.ts +26 -6
- package/dist/core/Hex.d.ts.map +1 -1
- package/dist/core/Hex.js +40 -13
- package/dist/core/Hex.js.map +1 -1
- package/dist/core/Input.d.ts +36 -0
- package/dist/core/Input.d.ts.map +1 -1
- package/dist/core/Input.js +63 -1
- package/dist/core/Input.js.map +1 -1
- package/dist/core/Random.d.ts +93 -0
- package/dist/core/Random.d.ts.map +1 -1
- package/dist/core/Random.js +177 -0
- package/dist/core/Random.js.map +1 -1
- package/dist/core/SyncGuard.d.ts +57 -0
- package/dist/core/SyncGuard.d.ts.map +1 -0
- package/dist/core/SyncGuard.js +116 -0
- package/dist/core/SyncGuard.js.map +1 -0
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/i18n/Po.d.ts +51 -0
- package/dist/i18n/Po.d.ts.map +1 -0
- package/dist/i18n/Po.js +176 -0
- package/dist/i18n/Po.js.map +1 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/index.js.map +1 -1
- package/dist/mw_games.global.js +104 -98
- package/dist/mwl/MapFile.d.ts +41 -0
- package/dist/mwl/MapFile.d.ts.map +1 -0
- package/dist/mwl/MapFile.js +43 -0
- package/dist/mwl/MapFile.js.map +1 -0
- package/dist/mwl/campaign.d.ts +51 -0
- package/dist/mwl/campaign.d.ts.map +1 -0
- package/dist/mwl/campaign.js +58 -0
- package/dist/mwl/campaign.js.map +1 -0
- package/dist/mwl/carryover.d.ts +79 -0
- package/dist/mwl/carryover.d.ts.map +1 -0
- package/dist/mwl/carryover.js +63 -0
- package/dist/mwl/carryover.js.map +1 -0
- package/dist/mwl/content.d.ts +10 -0
- package/dist/mwl/content.d.ts.map +1 -1
- package/dist/mwl/content.js +8 -0
- package/dist/mwl/content.js.map +1 -1
- package/dist/mwl/hooks.d.ts +6 -5
- package/dist/mwl/hooks.d.ts.map +1 -1
- package/dist/mwl/hooks.js.map +1 -1
- package/dist/mwl/index.d.ts +8 -2
- package/dist/mwl/index.d.ts.map +1 -1
- package/dist/mwl/index.js +4 -1
- package/dist/mwl/index.js.map +1 -1
- package/dist/mwl/runtime.d.ts +140 -7
- package/dist/mwl/runtime.d.ts.map +1 -1
- package/dist/mwl/runtime.js +427 -33
- package/dist/mwl/runtime.js.map +1 -1
- package/dist/mwl/schema.d.ts.map +1 -1
- package/dist/mwl/schema.js +97 -18
- package/dist/mwl/schema.js.map +1 -1
- package/dist/roguelike/MultiTurnBeam.d.ts +50 -12
- package/dist/roguelike/MultiTurnBeam.d.ts.map +1 -1
- package/dist/roguelike/MultiTurnBeam.js +100 -53
- package/dist/roguelike/MultiTurnBeam.js.map +1 -1
- package/dist/roguelike/Targeting.d.ts +94 -0
- package/dist/roguelike/Targeting.d.ts.map +1 -1
- package/dist/roguelike/Targeting.js +123 -0
- package/dist/roguelike/Targeting.js.map +1 -1
- package/dist/roguelike/index.d.ts +3 -1
- package/dist/roguelike/index.d.ts.map +1 -1
- package/dist/roguelike/index.js +1 -0
- package/dist/roguelike/index.js.map +1 -1
- package/dist/simulation/CampaignSave.d.ts +68 -0
- package/dist/simulation/CampaignSave.d.ts.map +1 -0
- package/dist/simulation/CampaignSave.js +54 -0
- package/dist/simulation/CampaignSave.js.map +1 -0
- package/dist/simulation/EventPresentation.d.ts +111 -0
- package/dist/simulation/EventPresentation.d.ts.map +1 -0
- package/dist/simulation/EventPresentation.js +120 -0
- package/dist/simulation/EventPresentation.js.map +1 -0
- package/dist/simulation/index.d.ts +4 -0
- package/dist/simulation/index.d.ts.map +1 -1
- package/dist/simulation/index.js +2 -0
- package/dist/simulation/index.js.map +1 -1
- package/dist/two-d/pixi-interop.d.ts +18 -6
- package/dist/two-d/pixi-interop.d.ts.map +1 -1
- package/dist/two-d/pixi-interop.js +18 -6
- package/dist/two-d/pixi-interop.js.map +1 -1
- package/dist/two-d/render/AnimatedSprite.d.ts +74 -7
- package/dist/two-d/render/AnimatedSprite.d.ts.map +1 -1
- package/dist/two-d/render/AnimatedSprite.js +88 -26
- package/dist/two-d/render/AnimatedSprite.js.map +1 -1
- package/dist/two-d/render/Camera.d.ts +64 -4
- package/dist/two-d/render/Camera.d.ts.map +1 -1
- package/dist/two-d/render/Camera.js +132 -11
- package/dist/two-d/render/Camera.js.map +1 -1
- package/dist/two-d/render/Halo.d.ts +58 -0
- package/dist/two-d/render/Halo.d.ts.map +1 -0
- package/dist/two-d/render/Halo.js +48 -0
- package/dist/two-d/render/Halo.js.map +1 -0
- package/dist/two-d/render/ImageModifiers.d.ts +174 -0
- package/dist/two-d/render/ImageModifiers.d.ts.map +1 -1
- package/dist/two-d/render/ImageModifiers.js +438 -1
- package/dist/two-d/render/ImageModifiers.js.map +1 -1
- package/dist/two-d/render/LightningArc.d.ts +57 -0
- package/dist/two-d/render/LightningArc.d.ts.map +1 -0
- package/dist/two-d/render/LightningArc.js +91 -0
- package/dist/two-d/render/LightningArc.js.map +1 -0
- package/dist/two-d/render/PaletteRemap.d.ts +135 -0
- package/dist/two-d/render/PaletteRemap.d.ts.map +1 -0
- package/dist/two-d/render/PaletteRemap.js +181 -0
- package/dist/two-d/render/PaletteRemap.js.map +1 -0
- package/dist/two-d/render/Particles.d.ts +26 -0
- package/dist/two-d/render/Particles.d.ts.map +1 -1
- package/dist/two-d/render/Particles.js +23 -2
- package/dist/two-d/render/Particles.js.map +1 -1
- package/dist/two-d/render/SpriteAttachment.d.ts +55 -0
- package/dist/two-d/render/SpriteAttachment.d.ts.map +1 -0
- package/dist/two-d/render/SpriteAttachment.js +60 -0
- package/dist/two-d/render/SpriteAttachment.js.map +1 -0
- package/dist/two-d/render/TerrainGraphics.d.ts +142 -0
- package/dist/two-d/render/TerrainGraphics.d.ts.map +1 -0
- package/dist/two-d/render/TerrainGraphics.js +161 -0
- package/dist/two-d/render/TerrainGraphics.js.map +1 -0
- package/dist/two-d/render/index.d.ts +13 -3
- package/dist/two-d/render/index.d.ts.map +1 -1
- package/dist/two-d/render/index.js +6 -1
- package/dist/two-d/render/index.js.map +1 -1
- package/dist/two-d/stage/StoryScreen.d.ts +110 -0
- package/dist/two-d/stage/StoryScreen.d.ts.map +1 -0
- package/dist/two-d/stage/StoryScreen.js +178 -0
- package/dist/two-d/stage/StoryScreen.js.map +1 -0
- package/dist/two-d/stage/index.d.ts +2 -0
- package/dist/two-d/stage/index.d.ts.map +1 -1
- package/dist/two-d/stage/index.js +1 -0
- package/dist/two-d/stage/index.js.map +1 -1
- package/dist/two-d/ui/Checkbox.d.ts +44 -0
- package/dist/two-d/ui/Checkbox.d.ts.map +1 -0
- package/dist/two-d/ui/Checkbox.js +89 -0
- package/dist/two-d/ui/Checkbox.js.map +1 -0
- package/dist/two-d/ui/DataTable.d.ts +79 -0
- package/dist/two-d/ui/DataTable.d.ts.map +1 -0
- package/dist/two-d/ui/DataTable.js +164 -0
- package/dist/two-d/ui/DataTable.js.map +1 -0
- package/dist/two-d/ui/Dropdown.d.ts +72 -0
- package/dist/two-d/ui/Dropdown.d.ts.map +1 -0
- package/dist/two-d/ui/Dropdown.js +126 -0
- package/dist/two-d/ui/Dropdown.js.map +1 -0
- package/dist/two-d/ui/FloatingText.d.ts +43 -4
- package/dist/two-d/ui/FloatingText.d.ts.map +1 -1
- package/dist/two-d/ui/FloatingText.js +52 -5
- package/dist/two-d/ui/FloatingText.js.map +1 -1
- package/dist/two-d/ui/FloatingTextStack.d.ts +86 -13
- package/dist/two-d/ui/FloatingTextStack.d.ts.map +1 -1
- package/dist/two-d/ui/FloatingTextStack.js +89 -18
- package/dist/two-d/ui/FloatingTextStack.js.map +1 -1
- package/dist/two-d/ui/Layout.d.ts +105 -0
- package/dist/two-d/ui/Layout.d.ts.map +1 -0
- package/dist/two-d/ui/Layout.js +174 -0
- package/dist/two-d/ui/Layout.js.map +1 -0
- package/dist/two-d/ui/ScrollBox.d.ts +67 -0
- package/dist/two-d/ui/ScrollBox.d.ts.map +1 -0
- package/dist/two-d/ui/ScrollBox.js +127 -0
- package/dist/two-d/ui/ScrollBox.js.map +1 -0
- package/dist/two-d/ui/Skins.d.ts +59 -0
- package/dist/two-d/ui/Skins.d.ts.map +1 -0
- package/dist/two-d/ui/Skins.js +77 -0
- package/dist/two-d/ui/Skins.js.map +1 -0
- package/dist/two-d/ui/Slider.d.ts +91 -0
- package/dist/two-d/ui/Slider.d.ts.map +1 -0
- package/dist/two-d/ui/Slider.js +170 -0
- package/dist/two-d/ui/Slider.js.map +1 -0
- package/dist/two-d/ui/Spinner.d.ts +67 -0
- package/dist/two-d/ui/Spinner.d.ts.map +1 -0
- package/dist/two-d/ui/Spinner.js +137 -0
- package/dist/two-d/ui/Spinner.js.map +1 -0
- package/dist/two-d/ui/TabbedList.d.ts +118 -0
- package/dist/two-d/ui/TabbedList.d.ts.map +1 -0
- package/dist/two-d/ui/TabbedList.js +221 -0
- package/dist/two-d/ui/TabbedList.js.map +1 -0
- package/dist/two-d/ui/TextModel.d.ts +65 -0
- package/dist/two-d/ui/TextModel.d.ts.map +1 -0
- package/dist/two-d/ui/TextModel.js +126 -0
- package/dist/two-d/ui/TextModel.js.map +1 -0
- package/dist/two-d/ui/TreeView.d.ts +68 -0
- package/dist/two-d/ui/TreeView.d.ts.map +1 -0
- package/dist/two-d/ui/TreeView.js +133 -0
- package/dist/two-d/ui/TreeView.js.map +1 -0
- package/dist/two-d/ui/Window.d.ts +13 -0
- package/dist/two-d/ui/Window.d.ts.map +1 -1
- package/dist/two-d/ui/Window.js +23 -0
- package/dist/two-d/ui/Window.js.map +1 -1
- package/dist/two-d/ui/WindowStack.d.ts.map +1 -1
- package/dist/two-d/ui/WindowStack.js +2 -0
- package/dist/two-d/ui/WindowStack.js.map +1 -1
- package/dist/two-d/ui/index.d.ts +27 -3
- package/dist/two-d/ui/index.d.ts.map +1 -1
- package/dist/two-d/ui/index.js +14 -2
- package/dist/two-d/ui/index.js.map +1 -1
- package/dist/two-d/ui/markup.d.ts +127 -0
- package/dist/two-d/ui/markup.d.ts.map +1 -0
- package/dist/two-d/ui/markup.js +322 -0
- package/dist/two-d/ui/markup.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/world/SideTurns.d.ts +115 -0
- package/dist/world/SideTurns.d.ts.map +1 -0
- package/dist/world/SideTurns.js +138 -0
- package/dist/world/SideTurns.js.map +1 -0
- package/dist/world/index.d.ts +2 -0
- package/dist/world/index.d.ts.map +1 -1
- package/dist/world/index.js +1 -0
- package/dist/world/index.js.map +1 -1
- package/package.json +2 -1
package/dist/mwl/runtime.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { decodeSave, encodeSave } from './persistence.js';
|
|
2
2
|
import { evaluateCondition } from './conditions.js';
|
|
3
3
|
import { evaluateExpression } from './expression.js';
|
|
4
|
-
import { integerAttribute, requiredAttribute } from './utils.js';
|
|
4
|
+
import { booleanAttribute, integerAttribute, numberAttribute, requiredAttribute } from './utils.js';
|
|
5
|
+
import { endLevelCarryover } from './carryover.js';
|
|
5
6
|
const required = requiredAttribute;
|
|
6
7
|
const integer = (node, attribute, fallback) => integerAttribute(node, attribute, fallback) ?? fallback;
|
|
7
8
|
export function createWorld() {
|
|
@@ -13,6 +14,7 @@ export function createWorld() {
|
|
|
13
14
|
gold: {},
|
|
14
15
|
turn: 1,
|
|
15
16
|
status: 'playing',
|
|
17
|
+
sideStatus: {},
|
|
16
18
|
map: null,
|
|
17
19
|
timeOfDay: '',
|
|
18
20
|
scheduleIndex: 0,
|
|
@@ -35,10 +37,11 @@ export function parseTerrain(text) {
|
|
|
35
37
|
for (let y = 0; y < height; y++) {
|
|
36
38
|
for (let x = 0; x < width; x++) {
|
|
37
39
|
const token = rows[y]?.[x] ?? '_off^_usr';
|
|
38
|
-
|
|
40
|
+
//`<side> <code>`: the side is the id `[side]` declares, so a named side works here too
|
|
41
|
+
const keep = /^([A-Za-z0-9_][\w.-]*)\s+(\S.*)$/.exec(token);
|
|
39
42
|
if (keep) {
|
|
40
43
|
codes.push(keep[2]);
|
|
41
|
-
(starts[
|
|
44
|
+
(starts[keep[1]] ??= []).push({ x, y });
|
|
42
45
|
}
|
|
43
46
|
else
|
|
44
47
|
codes.push(token);
|
|
@@ -73,6 +76,8 @@ export class MwlRuntime {
|
|
|
73
76
|
this.loadInitialContent();
|
|
74
77
|
this.loadInitialUnits();
|
|
75
78
|
this.loadLeaders();
|
|
79
|
+
//roles filter over the units, so this runs once they and the leaders are in the world
|
|
80
|
+
this.loadScenarioExtras();
|
|
76
81
|
}
|
|
77
82
|
run(trigger) {
|
|
78
83
|
for (const event of this.nodes('event')) {
|
|
@@ -118,7 +123,7 @@ export class MwlRuntime {
|
|
|
118
123
|
continue;
|
|
119
124
|
if (event.attributes.unit !== undefined && event.attributes.unit !== id)
|
|
120
125
|
continue;
|
|
121
|
-
if (event.attributes.side !== undefined &&
|
|
126
|
+
if (event.attributes.side !== undefined && event.attributes.side !== unit.side)
|
|
122
127
|
continue;
|
|
123
128
|
if (!this.eventFiltersMatch(event))
|
|
124
129
|
continue;
|
|
@@ -188,12 +193,13 @@ export class MwlRuntime {
|
|
|
188
193
|
for (const child of node.children) {
|
|
189
194
|
if (child.tag === 'message') {
|
|
190
195
|
this.showMessage(child.attributes);
|
|
191
|
-
const side = Number.parseInt(child.attributes.side ?? '', 10);
|
|
192
196
|
last = {
|
|
193
197
|
text: child.attributes.text ?? child.attributes.value ?? '',
|
|
194
198
|
...(child.attributes.speaker === undefined ? {} : { speaker: child.attributes.speaker }),
|
|
195
199
|
...(child.attributes.portrait === undefined ? {} : { portrait: child.attributes.portrait }),
|
|
196
|
-
...(
|
|
200
|
+
...(child.attributes.side === undefined || child.attributes.side === ''
|
|
201
|
+
? {}
|
|
202
|
+
: { side: child.attributes.side }),
|
|
197
203
|
};
|
|
198
204
|
}
|
|
199
205
|
else if (child.tag === 'say') {
|
|
@@ -294,11 +300,19 @@ export class MwlRuntime {
|
|
|
294
300
|
Object.assign(this.world.gold, restored.gold ?? {});
|
|
295
301
|
this.world.turn = restored.turn;
|
|
296
302
|
this.world.status = restored.status;
|
|
303
|
+
this.world.sideStatus = { ...(restored.sideStatus ?? {}) };
|
|
297
304
|
this.world.map = restored.map ?? null;
|
|
298
305
|
this.world.timeOfDay = restored.timeOfDay ?? this.schedule[0] ?? '';
|
|
299
306
|
this.world.scheduleIndex = restored.scheduleIndex ?? 0;
|
|
300
307
|
// One-shot events stay spent across a load.
|
|
301
308
|
this.world.firedEvents = [...(restored.firedEvents ?? [])];
|
|
309
|
+
// Scenario data a command may have changed, restored wholesale rather than merged: a load
|
|
310
|
+
// is that save's world, not this scenario's with a few fields over it.
|
|
311
|
+
this.world.villages = restored.villages ? structuredClone(restored.villages) : undefined;
|
|
312
|
+
this.world.clearedShroud = restored.clearedShroud ? structuredClone(restored.clearedShroud) : undefined;
|
|
313
|
+
this.world.roles = restored.roles ? structuredClone(restored.roles) : undefined;
|
|
314
|
+
this.world.objects = restored.objects ? structuredClone(restored.objects) : undefined;
|
|
315
|
+
this.world.story = restored.story ? structuredClone(restored.story) : undefined;
|
|
302
316
|
this.world.pendingDialogue = restored.pendingDialogue;
|
|
303
317
|
this.pendingDialogue = restored.pendingDialogue
|
|
304
318
|
? { id: restored.pendingDialogue.id, choices: [...restored.pendingDialogue.choices] }
|
|
@@ -337,6 +351,22 @@ export class MwlRuntime {
|
|
|
337
351
|
entry.controller = side.attributes.controller;
|
|
338
352
|
if (side.attributes.recruit !== undefined)
|
|
339
353
|
entry.recruit = side.attributes.recruit;
|
|
354
|
+
//only what was written, so a world stays as small as the content that made it
|
|
355
|
+
if (side.attributes.team_name !== undefined)
|
|
356
|
+
entry.teamName = side.attributes.team_name;
|
|
357
|
+
if (side.attributes.user_team_name !== undefined)
|
|
358
|
+
entry.userTeamName = side.attributes.user_team_name;
|
|
359
|
+
if (side.attributes.share_vision !== undefined)
|
|
360
|
+
entry.shareVision = side.attributes.share_vision;
|
|
361
|
+
if (side.attributes.flag !== undefined)
|
|
362
|
+
entry.flag = side.attributes.flag;
|
|
363
|
+
if (side.attributes.village_gold !== undefined)
|
|
364
|
+
entry.villageGold = integer(side, 'village_gold', 0);
|
|
365
|
+
for (const key of ['heal', 'fog', 'shroud', 'hidden']) {
|
|
366
|
+
const written = side.attributes[key];
|
|
367
|
+
if (written !== undefined)
|
|
368
|
+
entry[key] = written === 'yes' || written === 'true';
|
|
369
|
+
}
|
|
340
370
|
this.world.sides[required(side, 'id')] = entry;
|
|
341
371
|
}
|
|
342
372
|
for (const map of this.nodes('map')) {
|
|
@@ -346,6 +376,54 @@ export class MwlRuntime {
|
|
|
346
376
|
this.world.map = this.buildMap(id, map);
|
|
347
377
|
}
|
|
348
378
|
}
|
|
379
|
+
/** Direct children of `[game]` with a tag, as opposed to `nodes` which searches the whole tree. */
|
|
380
|
+
topLevelNodes(tag) {
|
|
381
|
+
const game = this.game.roots.find((root) => root.tag === 'game');
|
|
382
|
+
return (game?.children ?? this.game.roots).filter((root) => root.tag === tag);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Scenario-level data that is not a side, a map or a unit: `[object]` placements, the `[story]`
|
|
386
|
+
* beats, and the `[role]` table. Kept as plain data on the world - the framework sequences and
|
|
387
|
+
* serialises it, and the game decides what an object does, what a story beat looks like, and
|
|
388
|
+
* what a role is for.
|
|
389
|
+
*/
|
|
390
|
+
loadScenarioExtras() {
|
|
391
|
+
for (const object of this.topLevelNodes('object')) {
|
|
392
|
+
const x = integer(object, 'x', 0);
|
|
393
|
+
const y = integer(object, 'y', 0);
|
|
394
|
+
const entry = { x, y };
|
|
395
|
+
if (object.attributes.id !== undefined)
|
|
396
|
+
entry.id = object.attributes.id;
|
|
397
|
+
if (object.attributes.name !== undefined)
|
|
398
|
+
entry.name = object.attributes.name;
|
|
399
|
+
if (object.attributes.image !== undefined)
|
|
400
|
+
entry.image = object.attributes.image;
|
|
401
|
+
if (object.attributes.side !== undefined)
|
|
402
|
+
entry.side = object.attributes.side;
|
|
403
|
+
(this.world.objects ??= []).push(entry);
|
|
404
|
+
}
|
|
405
|
+
for (const story of this.topLevelNodes('story')) {
|
|
406
|
+
const text = story.attributes.text ?? story.attributes.value;
|
|
407
|
+
if (text === undefined)
|
|
408
|
+
continue;
|
|
409
|
+
const entry = { text };
|
|
410
|
+
if (story.attributes.title !== undefined)
|
|
411
|
+
entry.title = story.attributes.title;
|
|
412
|
+
if (story.attributes.image !== undefined)
|
|
413
|
+
entry.image = story.attributes.image;
|
|
414
|
+
if (story.attributes.music !== undefined)
|
|
415
|
+
entry.music = story.attributes.music;
|
|
416
|
+
(this.world.story ??= []).push(entry);
|
|
417
|
+
}
|
|
418
|
+
for (const role of this.topLevelNodes('role')) {
|
|
419
|
+
const name = role.attributes.role ?? role.attributes.name;
|
|
420
|
+
if (name === undefined)
|
|
421
|
+
continue;
|
|
422
|
+
(this.world.roles ??= {})[name] = Object.entries(this.world.units)
|
|
423
|
+
.filter(([id, unit]) => unit.alive && unitMatchesFilter(unit, id, role.attributes))
|
|
424
|
+
.map(([id]) => id);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
349
427
|
buildMap(id, node) {
|
|
350
428
|
let text = node.attributes.terrain;
|
|
351
429
|
if (!text && node.attributes.file && this.resolveMap)
|
|
@@ -356,7 +434,7 @@ export class MwlRuntime {
|
|
|
356
434
|
for (const start of node.children) {
|
|
357
435
|
if (start.tag !== 'start')
|
|
358
436
|
continue;
|
|
359
|
-
const side =
|
|
437
|
+
const side = start.attributes.side;
|
|
360
438
|
const x = optionalInteger(start, 'x');
|
|
361
439
|
const y = optionalInteger(start, 'y');
|
|
362
440
|
if (side !== undefined && x !== undefined && y !== undefined)
|
|
@@ -380,7 +458,7 @@ export class MwlRuntime {
|
|
|
380
458
|
entry.moves = stats.movement;
|
|
381
459
|
}
|
|
382
460
|
if (unit.attributes.side !== undefined)
|
|
383
|
-
entry.side =
|
|
461
|
+
entry.side = unit.attributes.side;
|
|
384
462
|
this.world.units[id] = entry;
|
|
385
463
|
}
|
|
386
464
|
}
|
|
@@ -389,24 +467,24 @@ export class MwlRuntime {
|
|
|
389
467
|
const leader = side.attributes.leader;
|
|
390
468
|
if (!leader)
|
|
391
469
|
continue;
|
|
392
|
-
const
|
|
393
|
-
if (
|
|
470
|
+
const sideId = side.attributes.id;
|
|
471
|
+
if (!sideId)
|
|
394
472
|
continue;
|
|
395
|
-
const starts = this.world.map?.starts[
|
|
473
|
+
const starts = this.world.map?.starts[sideId];
|
|
396
474
|
if (!starts?.length)
|
|
397
475
|
continue;
|
|
398
476
|
const stats = this.unitTypes.get(leader);
|
|
399
477
|
for (const start of starts) {
|
|
400
478
|
if (this.unitAt(start.x, start.y))
|
|
401
479
|
continue;
|
|
402
|
-
const id = `${leader} ${
|
|
480
|
+
const id = `${leader} ${sideId} (${start.x},${start.y})`;
|
|
403
481
|
this.world.units[id] = {
|
|
404
482
|
hp: stats?.hitpoints ?? 1,
|
|
405
483
|
x: start.x,
|
|
406
484
|
y: start.y,
|
|
407
485
|
alive: true,
|
|
408
486
|
type: leader,
|
|
409
|
-
side:
|
|
487
|
+
side: sideId,
|
|
410
488
|
moves: stats?.movement ?? 0,
|
|
411
489
|
};
|
|
412
490
|
}
|
|
@@ -425,14 +503,104 @@ export class MwlRuntime {
|
|
|
425
503
|
break;
|
|
426
504
|
}
|
|
427
505
|
case 'spawn':
|
|
428
|
-
this.spawnUnit(node.attributes.type ?? '', node.attributes.side
|
|
506
|
+
this.spawnUnit(node.attributes.type ?? '', node.attributes.side, integer(node, 'x', 0), integer(node, 'y', 0), node.attributes.id ?? node.attributes.target, node.attributes.hp === undefined ? undefined : integer(node, 'hp', 1));
|
|
429
507
|
break;
|
|
430
508
|
case 'move':
|
|
431
509
|
this.applyMove(node.attributes.unit ?? required(node, 'target'), integer(node, 'x', 0), integer(node, 'y', 0));
|
|
432
510
|
break;
|
|
433
|
-
case 'kill':
|
|
434
|
-
|
|
511
|
+
case 'kill': {
|
|
512
|
+
//a named unit that is not there is a content error; a filter that matches nobody is not,
|
|
513
|
+
//which is what WML's own `[kill]` does. An empty filter matches every unit, there as
|
|
514
|
+
//here, which is worth knowing before writing one.
|
|
515
|
+
const named = node.attributes.unit ?? node.attributes.target;
|
|
516
|
+
if (named === undefined)
|
|
517
|
+
killMatching(this.world, node.attributes);
|
|
518
|
+
else
|
|
519
|
+
this.killUnit(named);
|
|
435
520
|
break;
|
|
521
|
+
}
|
|
522
|
+
case 'fire_event': {
|
|
523
|
+
const id = node.attributes.id ?? node.attributes.name;
|
|
524
|
+
if (!id)
|
|
525
|
+
throw new Error('[fire_event] requires id');
|
|
526
|
+
this.fireEvent(id);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case 'store_unit': {
|
|
530
|
+
const variable = node.attributes.variable ?? node.attributes.name ?? required(node, 'variable');
|
|
531
|
+
this.setVariableAt(variable, this.matchingUnits(node).map(([id, unit]) => ({ id, ...unitSnapshot(unit) })));
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
case 'unstore_unit': {
|
|
535
|
+
const variable = required(node, 'variable');
|
|
536
|
+
for (const entry of this.storedUnits(variable))
|
|
537
|
+
this.restoreUnit(entry, {});
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
case 'recall': {
|
|
541
|
+
const stored = this.storedUnits(required(node, 'variable'));
|
|
542
|
+
const wanted = node.attributes.id ?? node.attributes.unit;
|
|
543
|
+
const entry = wanted === undefined ? stored[0] : stored.find((candidate) => candidate.id === wanted);
|
|
544
|
+
if (!entry)
|
|
545
|
+
throw new Error(`[recall] found no stored unit${wanted === undefined ? '' : ` named ${wanted}`}`);
|
|
546
|
+
const placement = {};
|
|
547
|
+
if (node.attributes.x !== undefined)
|
|
548
|
+
placement.x = integer(node, 'x', 0);
|
|
549
|
+
if (node.attributes.y !== undefined)
|
|
550
|
+
placement.y = integer(node, 'y', 0);
|
|
551
|
+
if (node.attributes.side !== undefined)
|
|
552
|
+
placement.side = node.attributes.side;
|
|
553
|
+
this.restoreUnit(entry, placement);
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
case 'modify_unit': {
|
|
557
|
+
//WML separates which units (`[filter]`) from what changes (`[set]`); a bare
|
|
558
|
+
//`[modify_unit] hp=5` with no `[set]` changes every unit it matches
|
|
559
|
+
const changes = node.children.find((child) => child.tag === 'set')?.attributes ?? node.attributes;
|
|
560
|
+
for (const [, unit] of this.matchingUnits(node))
|
|
561
|
+
applyUnitChanges(unit, changes);
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case 'heal_unit': {
|
|
565
|
+
const amount = optionalInteger(node, 'amount');
|
|
566
|
+
const absolute = optionalInteger(node, 'hp');
|
|
567
|
+
if (amount === undefined && absolute === undefined)
|
|
568
|
+
throw new Error('[heal_unit] requires amount or hp');
|
|
569
|
+
for (const [, unit] of this.matchingUnits(node))
|
|
570
|
+
unit.hp = absolute ?? unit.hp + (amount ?? 0);
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
case 'set_terrain':
|
|
574
|
+
this.setTerrain(integer(node, 'x', 0), integer(node, 'y', 0), required(node, 'terrain'));
|
|
575
|
+
break;
|
|
576
|
+
case 'capture_village': {
|
|
577
|
+
const x = integer(node, 'x', 0);
|
|
578
|
+
const y = integer(node, 'y', 0);
|
|
579
|
+
const entry = { x, y, side: required(node, 'side') };
|
|
580
|
+
if (node.attributes.name !== undefined)
|
|
581
|
+
entry.name = node.attributes.name;
|
|
582
|
+
(this.world.villages ??= {})[`${x},${y}`] = entry;
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case 'clear_shroud': {
|
|
586
|
+
const side = required(node, 'side');
|
|
587
|
+
const x = integer(node, 'x', 0);
|
|
588
|
+
const y = integer(node, 'y', 0);
|
|
589
|
+
const radius = integer(node, 'radius', 1);
|
|
590
|
+
const cleared = new Set(this.world.clearedShroud?.[side] ?? []);
|
|
591
|
+
for (let dy = -radius; dy <= radius; dy++)
|
|
592
|
+
for (let dx = -radius; dx <= radius; dx++)
|
|
593
|
+
cleared.add(`${x + dx},${y + dy}`);
|
|
594
|
+
(this.world.clearedShroud ??= {})[side] = [...cleared];
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
case 'role': {
|
|
598
|
+
const name = node.attributes.role ?? node.attributes.name;
|
|
599
|
+
if (!name)
|
|
600
|
+
throw new Error('[role] requires role');
|
|
601
|
+
(this.world.roles ??= {})[name] = this.matchingUnits(node).map(([id]) => id);
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
436
604
|
case 'attack':
|
|
437
605
|
this.attack(node.attributes.defender ?? node.attributes.target ?? required(node, 'target'), integer(node, 'amount', 0));
|
|
438
606
|
break;
|
|
@@ -490,11 +658,25 @@ export class MwlRuntime {
|
|
|
490
658
|
this.endTurn();
|
|
491
659
|
break;
|
|
492
660
|
case 'win':
|
|
661
|
+
this.markSideResult(node.attributes.side, 'won');
|
|
493
662
|
this.world.status = 'won';
|
|
494
663
|
break;
|
|
495
664
|
case 'lose':
|
|
665
|
+
this.markSideResult(node.attributes.side, 'lost');
|
|
496
666
|
this.world.status = 'lost';
|
|
497
667
|
break;
|
|
668
|
+
case 'endlevel': {
|
|
669
|
+
const result = node.attributes.result === 'defeat' ? 'defeat' : 'victory';
|
|
670
|
+
this.world.status = result === 'defeat' ? 'lost' : 'won';
|
|
671
|
+
this.world.carryover = endLevelCarryover(this.world, endLevelSide(this.world, node.attributes.side), {
|
|
672
|
+
result,
|
|
673
|
+
bonus: integerAttribute(node, 'bonus'),
|
|
674
|
+
carryoverPercentage: numberAttribute(node, 'carryover_percentage'),
|
|
675
|
+
carryoverAdd: booleanAttribute(node, 'carryover_add'),
|
|
676
|
+
nextScenario: node.attributes.next_scenario ?? null,
|
|
677
|
+
});
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
498
680
|
case 'if':
|
|
499
681
|
if (!this.nodeConditionMatches(node))
|
|
500
682
|
break;
|
|
@@ -545,12 +727,11 @@ export class MwlRuntime {
|
|
|
545
727
|
showMessage(attributes) {
|
|
546
728
|
if (!this.onMessage)
|
|
547
729
|
return;
|
|
548
|
-
const side = Number.parseInt(attributes.side ?? '', 10);
|
|
549
730
|
const message = {
|
|
550
731
|
text: this.interpolate(attributes.text ?? attributes.value ?? ''),
|
|
551
732
|
...(attributes.speaker === undefined ? {} : { speaker: attributes.speaker }),
|
|
552
733
|
...(attributes.portrait === undefined ? {} : { portrait: attributes.portrait }),
|
|
553
|
-
...(
|
|
734
|
+
...(attributes.side === undefined || attributes.side === '' ? {} : { side: attributes.side }),
|
|
554
735
|
};
|
|
555
736
|
this.onMessage(message);
|
|
556
737
|
}
|
|
@@ -625,7 +806,7 @@ export class MwlRuntime {
|
|
|
625
806
|
}
|
|
626
807
|
spawnUnit(type, side, x, y, id, hp) {
|
|
627
808
|
const stats = type ? this.unitTypes.get(type) : undefined;
|
|
628
|
-
const key = id ?? `${type || 'unit'}#${side ??
|
|
809
|
+
const key = id ?? `${type || 'unit'}#${side ?? ''}@${x},${y}`;
|
|
629
810
|
this.world.units[key] = {
|
|
630
811
|
hp: hp ?? stats?.hitpoints ?? 1,
|
|
631
812
|
x,
|
|
@@ -642,6 +823,54 @@ export class MwlRuntime {
|
|
|
642
823
|
throw new Error(`MWL unit is not alive: ${id}`);
|
|
643
824
|
unit.alive = false;
|
|
644
825
|
}
|
|
826
|
+
/**
|
|
827
|
+
* Live units a command's filter selects: its `[filter]` child when it has one, otherwise its
|
|
828
|
+
* own attributes, read by the same `unitMatchesFilter` an event filter and `[kill]` use. An
|
|
829
|
+
* empty filter matches every live unit, the rule `[kill]` already documents.
|
|
830
|
+
*/
|
|
831
|
+
matchingUnits(node) {
|
|
832
|
+
const filter = node.children.find((child) => child.tag === 'filter');
|
|
833
|
+
const attributes = filter?.attributes ?? node.attributes;
|
|
834
|
+
return Object.entries(this.world.units).filter(([id, unit]) => unit.alive && unitMatchesFilter(unit, id, attributes));
|
|
835
|
+
}
|
|
836
|
+
/** The unit snapshots `[store_unit]` wrote into a world variable, or a named content error. */
|
|
837
|
+
storedUnits(variable) {
|
|
838
|
+
const stored = this.world.variables[variable];
|
|
839
|
+
if (!Array.isArray(stored))
|
|
840
|
+
throw new Error(`MWL variable ${variable} is not a stored unit list; store one first`);
|
|
841
|
+
return stored.filter((entry) => !!entry && typeof entry === 'object' && !Array.isArray(entry));
|
|
842
|
+
}
|
|
843
|
+
/** Writes a stored snapshot back into the world, applying any placement overrides. */
|
|
844
|
+
restoreUnit(entry, placement) {
|
|
845
|
+
const id = typeof entry.id === 'string' ? entry.id : undefined;
|
|
846
|
+
if (!id)
|
|
847
|
+
throw new Error('a stored unit snapshot is missing its id');
|
|
848
|
+
const unit = {
|
|
849
|
+
hp: typeof entry.hp === 'number' ? entry.hp : 1,
|
|
850
|
+
x: placement.x ?? (typeof entry.x === 'number' ? entry.x : 0),
|
|
851
|
+
y: placement.y ?? (typeof entry.y === 'number' ? entry.y : 0),
|
|
852
|
+
alive: entry.alive !== false,
|
|
853
|
+
};
|
|
854
|
+
if (typeof entry.type === 'string')
|
|
855
|
+
unit.type = entry.type;
|
|
856
|
+
const side = placement.side ?? (typeof entry.side === 'string' ? entry.side : undefined);
|
|
857
|
+
if (side !== undefined)
|
|
858
|
+
unit.side = side;
|
|
859
|
+
if (typeof entry.moves === 'number')
|
|
860
|
+
unit.moves = entry.moves;
|
|
861
|
+
this.world.units[id] = unit;
|
|
862
|
+
}
|
|
863
|
+
/** Changes one cell of the primary map, keeping the parsed grid and its size in step. */
|
|
864
|
+
setTerrain(x, y, terrain) {
|
|
865
|
+
const map = this.world.map;
|
|
866
|
+
if (!map)
|
|
867
|
+
throw new Error('[set_terrain] needs a loaded map');
|
|
868
|
+
if (x < 0 || y < 0 || x >= map.width || y >= map.height)
|
|
869
|
+
throw new Error(`[set_terrain] is outside the map: ${x},${y}`);
|
|
870
|
+
const codes = [...map.codes];
|
|
871
|
+
codes[y * map.width + x] = terrain;
|
|
872
|
+
this.world.map = { ...map, codes };
|
|
873
|
+
}
|
|
645
874
|
addGold(side, delta) {
|
|
646
875
|
this.world.gold[side] = (this.world.gold[side] ?? 0) + delta;
|
|
647
876
|
}
|
|
@@ -650,6 +879,22 @@ export class MwlRuntime {
|
|
|
650
879
|
this.advanceSchedule();
|
|
651
880
|
this.resetMoves();
|
|
652
881
|
this.run('turn');
|
|
882
|
+
this.checkTimeOver();
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Wesnoth's "time over", as a framework native: once the scenario's `turn_limit` has passed,
|
|
886
|
+
* `time_over` becomes a variable content can test and an event trigger it can answer. The
|
|
887
|
+
* framework reports it rather than ending anything, because a turn limit is usually a defeat and
|
|
888
|
+
* occasionally the whole point of the scenario ("hold out until then"), so what it means is
|
|
889
|
+
* content's business - `[endlevel]` is right there for it. Fired once, and derived from the turn
|
|
890
|
+
* counter rather than kept in step with it.
|
|
891
|
+
*/
|
|
892
|
+
checkTimeOver() {
|
|
893
|
+
const limit = integerAttribute(this.game.roots[0], 'turn_limit');
|
|
894
|
+
if (limit === undefined || this.world.turn <= limit || this.world.variables.time_over === 'yes')
|
|
895
|
+
return;
|
|
896
|
+
this.world.variables.time_over = 'yes';
|
|
897
|
+
this.run('time_over');
|
|
653
898
|
}
|
|
654
899
|
advanceSchedule() {
|
|
655
900
|
if (!this.schedule.length)
|
|
@@ -721,12 +966,17 @@ export class MwlRuntime {
|
|
|
721
966
|
return true;
|
|
722
967
|
});
|
|
723
968
|
}
|
|
724
|
-
|
|
969
|
+
/**
|
|
970
|
+
* Whether one objective's condition holds. `defaultSide` is the side whose own
|
|
971
|
+
* `[victory]`/`[defeat]` node this is, so a condition that names no side (a side losing
|
|
972
|
+
* when its own units are gone) reads as that side rather than as nobody.
|
|
973
|
+
*/
|
|
974
|
+
conditionMet(node, defaultSide) {
|
|
725
975
|
const condition = node.attributes.condition ?? '';
|
|
726
|
-
const side =
|
|
976
|
+
const side = node.attributes.side ?? defaultSide;
|
|
727
977
|
switch (condition) {
|
|
728
978
|
case 'units_dead': {
|
|
729
|
-
const target =
|
|
979
|
+
const target = node.attributes.side_filter ?? side;
|
|
730
980
|
if (target === undefined)
|
|
731
981
|
return false;
|
|
732
982
|
return !Object.values(this.world.units).some((unit) => unit.alive && unit.side === target);
|
|
@@ -737,14 +987,13 @@ export class MwlRuntime {
|
|
|
737
987
|
}
|
|
738
988
|
case 'gold_at_least': {
|
|
739
989
|
const gold = optionalInteger(node, 'gold');
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
(this.world.gold[node.attributes.side] ?? 0) >= gold);
|
|
990
|
+
const goldSide = node.attributes.side ?? defaultSide;
|
|
991
|
+
return gold !== undefined && goldSide !== undefined && (this.world.gold[goldSide] ?? 0) >= gold;
|
|
743
992
|
}
|
|
744
993
|
case 'unit_at': {
|
|
745
994
|
const x = optionalInteger(node, 'x');
|
|
746
995
|
const y = optionalInteger(node, 'y');
|
|
747
|
-
const targetSide =
|
|
996
|
+
const targetSide = node.attributes.side_filter ?? side;
|
|
748
997
|
return Object.values(this.world.units).some((unit) => unit.alive &&
|
|
749
998
|
(targetSide === undefined || unit.side === targetSide) &&
|
|
750
999
|
(node.attributes.type === undefined || unit.type === node.attributes.type) &&
|
|
@@ -765,15 +1014,60 @@ export class MwlRuntime {
|
|
|
765
1014
|
return false;
|
|
766
1015
|
}
|
|
767
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Evaluates the scenario-wide `[objectives]` conditions first, exactly as before, then each
|
|
1019
|
+
* side's own `[victory]`/`[defeat]` children. The scenario ends on the first side whose own
|
|
1020
|
+
* condition fires, recording that side in `world.sideStatus`; a side's victory outranks
|
|
1021
|
+
* another side's defeat when both fire in the same evaluation, which is the same precedence
|
|
1022
|
+
* the scenario-wide pair has always had. A side condition with no `side`/`side_filter` reads
|
|
1023
|
+
* as that side's own, so "this side loses when it has no units left" is written without
|
|
1024
|
+
* repeating the id.
|
|
1025
|
+
*/
|
|
768
1026
|
checkObjectives() {
|
|
769
1027
|
if (this.world.status !== 'playing')
|
|
770
1028
|
return;
|
|
771
|
-
|
|
1029
|
+
// a `[victory]`/`[defeat]` under a `[side]` is that side's own, never the scenario's
|
|
1030
|
+
const sideConditions = new Set();
|
|
1031
|
+
const sides = this.nodes('side');
|
|
1032
|
+
for (const side of sides) {
|
|
1033
|
+
for (const child of side.children) {
|
|
1034
|
+
if (child.tag === 'victory' || child.tag === 'defeat')
|
|
1035
|
+
sideConditions.add(child);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (this.nodes('victory').some((node) => !sideConditions.has(node) && this.conditionMet(node))) {
|
|
772
1039
|
this.world.status = 'won';
|
|
773
1040
|
return;
|
|
774
1041
|
}
|
|
775
|
-
if (this.nodes('defeat').some((node) => this.conditionMet(node)))
|
|
1042
|
+
if (this.nodes('defeat').some((node) => !sideConditions.has(node) && this.conditionMet(node))) {
|
|
776
1043
|
this.world.status = 'lost';
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
let sideWon = false;
|
|
1047
|
+
let sideLost = false;
|
|
1048
|
+
for (const side of sides) {
|
|
1049
|
+
const id = side.attributes.id;
|
|
1050
|
+
if (!id || (this.world.sideStatus?.[id] ?? 'playing') !== 'playing')
|
|
1051
|
+
continue;
|
|
1052
|
+
if (side.children.some((child) => child.tag === 'victory' && this.conditionMet(child, id))) {
|
|
1053
|
+
this.markSideResult(id, 'won');
|
|
1054
|
+
sideWon = true;
|
|
1055
|
+
}
|
|
1056
|
+
else if (side.children.some((child) => child.tag === 'defeat' && this.conditionMet(child, id))) {
|
|
1057
|
+
this.markSideResult(id, 'lost');
|
|
1058
|
+
sideLost = true;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (sideWon)
|
|
1062
|
+
this.world.status = 'won';
|
|
1063
|
+
else if (sideLost)
|
|
1064
|
+
this.world.status = 'lost';
|
|
1065
|
+
}
|
|
1066
|
+
/** Records one side's own result; the scenario status is set by whichever side fired first. */
|
|
1067
|
+
markSideResult(side, result) {
|
|
1068
|
+
if (side === undefined || side === '')
|
|
1069
|
+
return;
|
|
1070
|
+
(this.world.sideStatus ??= {})[side] = result;
|
|
777
1071
|
}
|
|
778
1072
|
worldView() {
|
|
779
1073
|
return {
|
|
@@ -791,16 +1085,18 @@ export class MwlRuntime {
|
|
|
791
1085
|
},
|
|
792
1086
|
spawn: (type, side, x, y) => this.spawnUnit(type, side, x, y),
|
|
793
1087
|
kill: (unit) => this.killUnit(unit),
|
|
794
|
-
gold: (side, delta) => this.addGold(
|
|
1088
|
+
gold: (side, delta) => this.addGold(side, delta),
|
|
795
1089
|
setVariable: (name, value) => {
|
|
796
1090
|
this.world.variables[name] = value;
|
|
797
1091
|
},
|
|
798
1092
|
message: (speaker, text) => this.onMessage?.({ text, ...(speaker ? { speaker } : {}) }),
|
|
799
1093
|
endTurn: () => this.endTurn(),
|
|
800
|
-
win: (
|
|
1094
|
+
win: (side) => {
|
|
1095
|
+
this.markSideResult(side, 'won');
|
|
801
1096
|
this.world.status = 'won';
|
|
802
1097
|
},
|
|
803
|
-
lose: (
|
|
1098
|
+
lose: (side) => {
|
|
1099
|
+
this.markSideResult(side, 'lost');
|
|
804
1100
|
this.world.status = 'lost';
|
|
805
1101
|
},
|
|
806
1102
|
};
|
|
@@ -844,25 +1140,123 @@ export function execute(world, command) {
|
|
|
844
1140
|
world.units[command.target] = { hp: command.hp, x: command.x, y: command.y, alive: true };
|
|
845
1141
|
break;
|
|
846
1142
|
case 'kill':
|
|
847
|
-
|
|
1143
|
+
if (command.target !== undefined)
|
|
1144
|
+
requireUnit(world, command.target).alive = false;
|
|
1145
|
+
else
|
|
1146
|
+
killMatching(world, command.filter ?? {});
|
|
848
1147
|
break;
|
|
849
1148
|
case 'end_turn':
|
|
850
1149
|
world.turn++;
|
|
851
1150
|
break;
|
|
852
1151
|
case 'win':
|
|
1152
|
+
if (command.side !== undefined && command.side !== '')
|
|
1153
|
+
(world.sideStatus ??= {})[command.side] = 'won';
|
|
853
1154
|
world.status = 'won';
|
|
854
1155
|
break;
|
|
855
1156
|
case 'lose':
|
|
1157
|
+
if (command.side !== undefined && command.side !== '')
|
|
1158
|
+
(world.sideStatus ??= {})[command.side] = 'lost';
|
|
856
1159
|
world.status = 'lost';
|
|
857
1160
|
break;
|
|
1161
|
+
case 'endlevel':
|
|
1162
|
+
world.status = command.result === 'defeat' ? 'lost' : 'won';
|
|
1163
|
+
world.carryover = endLevelCarryover(world, command.side ?? null, command);
|
|
1164
|
+
break;
|
|
858
1165
|
}
|
|
859
1166
|
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Which side an `[endlevel]` carries: the one it names, or the human-controlled side when it names
|
|
1169
|
+
* none. `null` when there is no such side, which is not an error - a scenario can end with nobody
|
|
1170
|
+
* to carry.
|
|
1171
|
+
*/
|
|
1172
|
+
function endLevelSide(world, named) {
|
|
1173
|
+
const id = named ?? Object.entries(world.sides).find(([, side]) => side.controller === 'human')?.[0];
|
|
1174
|
+
if (!id || !(id in world.sides))
|
|
1175
|
+
return null;
|
|
1176
|
+
return { id };
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* The sides a scenario's content says see together, as groups ready for `FactionFog.share`.
|
|
1180
|
+
*
|
|
1181
|
+
* Wesnoth groups sides into teams with `[side] team_name`, and `share_vision` says whether a side
|
|
1182
|
+
* shares what it sees with its team. `none` does not; `all` and `shroud` are treated alike here
|
|
1183
|
+
* because `FactionFog` shares sight and memory as one thing rather than keeping them apart, and a
|
|
1184
|
+
* fog that separated them would be the place to tell the two apart. A side with no `team_name`
|
|
1185
|
+
* shares with nobody, and a team of one is not returned, since sharing with nobody is what it
|
|
1186
|
+
* already does.
|
|
1187
|
+
*
|
|
1188
|
+
* @example
|
|
1189
|
+
* ```ts
|
|
1190
|
+
* import { sideVisionGroups, type MwlWorld } from '@datamoc/mw_games/mwl';
|
|
1191
|
+
*
|
|
1192
|
+
* declare const world: MwlWorld;
|
|
1193
|
+
*
|
|
1194
|
+
* // sides 1 and 2 both wrote team_name=north, and side 3 asked for share_vision=none
|
|
1195
|
+
* console.log(sideVisionGroups(world)); // [['1', '2']]
|
|
1196
|
+
* ```
|
|
1197
|
+
*/
|
|
1198
|
+
export function sideVisionGroups(world) {
|
|
1199
|
+
const teams = new Map();
|
|
1200
|
+
for (const [id, side] of Object.entries(world.sides)) {
|
|
1201
|
+
if (!side.teamName || side.shareVision === 'none')
|
|
1202
|
+
continue;
|
|
1203
|
+
const group = teams.get(side.teamName);
|
|
1204
|
+
if (group)
|
|
1205
|
+
group.push(id);
|
|
1206
|
+
else
|
|
1207
|
+
teams.set(side.teamName, [id]);
|
|
1208
|
+
}
|
|
1209
|
+
return [...teams.values()].filter((group) => group.length > 1);
|
|
1210
|
+
}
|
|
860
1211
|
function requireUnit(world, id) {
|
|
861
1212
|
const unit = world.units[id];
|
|
862
1213
|
if (!unit || !unit.alive)
|
|
863
1214
|
throw new Error(`MWL unit is not alive: ${id}`);
|
|
864
1215
|
return unit;
|
|
865
1216
|
}
|
|
1217
|
+
/** Kills every alive unit a `[kill]` filter matches, and says how many it killed. */
|
|
1218
|
+
function killMatching(world, attributes) {
|
|
1219
|
+
let killed = 0;
|
|
1220
|
+
for (const [id, unit] of Object.entries(world.units)) {
|
|
1221
|
+
if (!unit.alive || !unitMatchesFilter(unit, id, attributes))
|
|
1222
|
+
continue;
|
|
1223
|
+
unit.alive = false;
|
|
1224
|
+
killed++;
|
|
1225
|
+
}
|
|
1226
|
+
return killed;
|
|
1227
|
+
}
|
|
1228
|
+
/** One unit as `[store_unit]` writes it: only the fields it has, so a variable stays small. */
|
|
1229
|
+
function unitSnapshot(unit) {
|
|
1230
|
+
const snapshot = { hp: unit.hp, x: unit.x, y: unit.y, alive: unit.alive };
|
|
1231
|
+
if (unit.type !== undefined)
|
|
1232
|
+
snapshot.type = unit.type;
|
|
1233
|
+
if (unit.side !== undefined)
|
|
1234
|
+
snapshot.side = unit.side;
|
|
1235
|
+
if (unit.moves !== undefined)
|
|
1236
|
+
snapshot.moves = unit.moves;
|
|
1237
|
+
return snapshot;
|
|
1238
|
+
}
|
|
1239
|
+
/** Applies `[modify_unit]`'s chosen attributes to one unit, ignoring anything it did not name. */
|
|
1240
|
+
function applyUnitChanges(unit, changes) {
|
|
1241
|
+
const hp = finiteNumber(changes.hp);
|
|
1242
|
+
if (hp !== undefined)
|
|
1243
|
+
unit.hp = hp;
|
|
1244
|
+
const moves = finiteNumber(changes.moves);
|
|
1245
|
+
if (moves !== undefined)
|
|
1246
|
+
unit.moves = moves;
|
|
1247
|
+
if (changes.type !== undefined)
|
|
1248
|
+
unit.type = changes.type;
|
|
1249
|
+
if (changes.side !== undefined)
|
|
1250
|
+
unit.side = changes.side;
|
|
1251
|
+
if (changes.alive !== undefined)
|
|
1252
|
+
unit.alive = changes.alive === 'yes' || changes.alive === 'true';
|
|
1253
|
+
}
|
|
1254
|
+
function finiteNumber(value) {
|
|
1255
|
+
if (value === undefined || value === '')
|
|
1256
|
+
return undefined;
|
|
1257
|
+
const parsed = Number(value);
|
|
1258
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1259
|
+
}
|
|
866
1260
|
function optionalInteger(node, attribute) {
|
|
867
1261
|
const value = node.attributes[attribute];
|
|
868
1262
|
if (value === undefined || value === '')
|
|
@@ -922,7 +1316,7 @@ function unitMatchesFilter(unit, id, attributes) {
|
|
|
922
1316
|
.filter(Boolean);
|
|
923
1317
|
const key = attributes.unit ?? attributes.id;
|
|
924
1318
|
return ((key === undefined || id === key) &&
|
|
925
|
-
(attributes.side === undefined || unit.side ===
|
|
1319
|
+
(attributes.side === undefined || unit.side === attributes.side) &&
|
|
926
1320
|
(attributes.type === undefined || unit.type === attributes.type) &&
|
|
927
1321
|
(excluded.length === 0 || !excluded.includes(unit.type ?? '')) &&
|
|
928
1322
|
(attributes.x === undefined || unit.x === Number(attributes.x)) &&
|