@datamoc/mw_games 0.7.0 → 0.7.2
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 +964 -0
- package/README.md +43 -3
- package/dist/actors/Affix.d.ts +7 -1
- package/dist/actors/Affix.d.ts.map +1 -1
- package/dist/actors/Affix.js +4 -2
- package/dist/actors/Affix.js.map +1 -1
- package/dist/actors/StatBlock.d.ts +20 -0
- package/dist/actors/StatBlock.d.ts.map +1 -1
- package/dist/actors/StatBlock.js +14 -3
- package/dist/actors/StatBlock.js.map +1 -1
- package/dist/actors/index.d.ts +2 -2
- package/dist/actors/index.d.ts.map +1 -1
- package/dist/actors/index.js +1 -1
- package/dist/actors/index.js.map +1 -1
- package/dist/ai/index.d.ts +118 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +203 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/lua.d.ts +47 -0
- package/dist/ai/lua.d.ts.map +1 -0
- package/dist/ai/lua.js +195 -0
- package/dist/ai/lua.js.map +1 -0
- package/dist/ai/search.d.ts +59 -0
- package/dist/ai/search.d.ts.map +1 -0
- package/dist/ai/search.js +100 -0
- package/dist/ai/search.js.map +1 -0
- package/dist/board/Engine.d.ts +14 -0
- package/dist/board/Engine.d.ts.map +1 -1
- package/dist/board/Engine.js +33 -45
- package/dist/board/Engine.js.map +1 -1
- package/dist/board/index.d.ts +1 -1
- package/dist/board/index.d.ts.map +1 -1
- package/dist/board/index.js +1 -1
- package/dist/board/index.js.map +1 -1
- package/dist/core/Csv.d.ts +10 -0
- package/dist/core/Csv.d.ts.map +1 -1
- package/dist/core/Csv.js +34 -3
- package/dist/core/Csv.js.map +1 -1
- package/dist/core/Entity.d.ts +2 -2
- package/dist/core/Entity.d.ts.map +1 -1
- package/dist/core/Entity.js +18 -4
- package/dist/core/Entity.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mw_games.global.js +97 -97
- package/dist/mwl/actors.d.ts +3 -1
- package/dist/mwl/actors.d.ts.map +1 -1
- package/dist/mwl/actors.js +15 -2
- package/dist/mwl/actors.js.map +1 -1
- package/dist/mwl/catalog.d.ts.map +1 -1
- package/dist/mwl/catalog.js +16 -9
- package/dist/mwl/catalog.js.map +1 -1
- package/dist/mwl/compiler.d.ts +20 -0
- package/dist/mwl/compiler.d.ts.map +1 -1
- package/dist/mwl/compiler.js +76 -3
- package/dist/mwl/compiler.js.map +1 -1
- package/dist/mwl/conditions.d.ts +9 -0
- package/dist/mwl/conditions.d.ts.map +1 -0
- package/dist/mwl/conditions.js +162 -0
- package/dist/mwl/conditions.js.map +1 -0
- package/dist/mwl/content.d.ts +61 -0
- package/dist/mwl/content.d.ts.map +1 -1
- package/dist/mwl/content.js +95 -34
- package/dist/mwl/content.js.map +1 -1
- package/dist/mwl/expression.d.ts.map +1 -1
- package/dist/mwl/expression.js +16 -6
- package/dist/mwl/expression.js.map +1 -1
- package/dist/mwl/fengari.d.ts +8 -0
- package/dist/mwl/fengari.d.ts.map +1 -0
- package/dist/mwl/fengari.js +136 -0
- package/dist/mwl/fengari.js.map +1 -0
- package/dist/mwl/hooks.d.ts +1 -1
- package/dist/mwl/hooks.d.ts.map +1 -1
- package/dist/mwl/hooks.js.map +1 -1
- package/dist/mwl/index.d.ts +16 -7
- package/dist/mwl/index.d.ts.map +1 -1
- package/dist/mwl/index.js +7 -3
- package/dist/mwl/index.js.map +1 -1
- package/dist/mwl/persistence.d.ts.map +1 -1
- package/dist/mwl/persistence.js +10 -2
- package/dist/mwl/persistence.js.map +1 -1
- package/dist/mwl/readers.d.ts +18 -0
- package/dist/mwl/readers.d.ts.map +1 -0
- package/dist/mwl/readers.js +58 -0
- package/dist/mwl/readers.js.map +1 -0
- package/dist/mwl/report.d.ts +9 -0
- package/dist/mwl/report.d.ts.map +1 -0
- package/dist/mwl/report.js +36 -0
- package/dist/mwl/report.js.map +1 -0
- package/dist/mwl/runtime.d.ts +65 -2
- package/dist/mwl/runtime.d.ts.map +1 -1
- package/dist/mwl/runtime.js +245 -26
- package/dist/mwl/runtime.js.map +1 -1
- package/dist/mwl/schema.d.ts +13 -1
- package/dist/mwl/schema.d.ts.map +1 -1
- package/dist/mwl/schema.js +222 -7
- package/dist/mwl/schema.js.map +1 -1
- package/dist/mwl/scripts.d.ts +15 -0
- package/dist/mwl/scripts.d.ts.map +1 -0
- package/dist/mwl/scripts.js +23 -0
- package/dist/mwl/scripts.js.map +1 -0
- package/dist/mwl/utils.d.ts +8 -0
- package/dist/mwl/utils.d.ts.map +1 -0
- package/dist/mwl/utils.js +37 -0
- package/dist/mwl/utils.js.map +1 -0
- package/dist/two-d/render/Types2D.d.ts +19 -4
- package/dist/two-d/render/Types2D.d.ts.map +1 -1
- package/dist/two-d/render/Types2D.js +25 -0
- package/dist/two-d/render/Types2D.js.map +1 -1
- package/dist/two-d/render/index.d.ts +2 -2
- package/dist/two-d/render/index.d.ts.map +1 -1
- package/dist/two-d/render/index.js +1 -1
- package/dist/two-d/render/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +27 -6
- package/tools/extract-html.d.mts +18 -0
- package/tools/extract-html.mjs +235 -0
- package/tools/mwl.mjs +86 -16
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Read a compiled node with one shared coercion and diagnostic policy. */
|
|
2
|
+
export function readAttributes(node, fields) {
|
|
3
|
+
const value = {};
|
|
4
|
+
const diagnostics = [];
|
|
5
|
+
for (const [name, spec] of Object.entries(fields)) {
|
|
6
|
+
const source = spec.source ?? name;
|
|
7
|
+
const raw = node.attributes[source];
|
|
8
|
+
if (raw === undefined) {
|
|
9
|
+
if (spec.required)
|
|
10
|
+
diagnostics.push(diagnostic('MWL_REQUIRED_FIELD', `missing required attribute "${source}"`, node));
|
|
11
|
+
else if (spec.default !== undefined)
|
|
12
|
+
value[name] = spec.default;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const converted = coerce(raw, spec.type);
|
|
16
|
+
if (converted === undefined)
|
|
17
|
+
diagnostics.push(diagnostic('MWL_FIELD_TYPE', `invalid ${spec.type} attribute "${source}": "${raw}"`, node));
|
|
18
|
+
else
|
|
19
|
+
value[name] = converted;
|
|
20
|
+
}
|
|
21
|
+
return { value: value, diagnostics };
|
|
22
|
+
}
|
|
23
|
+
/** Collect children without making every game adapter repeat this filter. */
|
|
24
|
+
export function readChildren(node, tag, read) {
|
|
25
|
+
const values = [];
|
|
26
|
+
const diagnostics = [];
|
|
27
|
+
for (const child of node.children) {
|
|
28
|
+
if (child.tag !== tag)
|
|
29
|
+
continue;
|
|
30
|
+
const result = read(child);
|
|
31
|
+
values.push(result.value);
|
|
32
|
+
diagnostics.push(...result.diagnostics);
|
|
33
|
+
}
|
|
34
|
+
return { value: values, diagnostics };
|
|
35
|
+
}
|
|
36
|
+
function coerce(raw, type) {
|
|
37
|
+
if (type === 'string')
|
|
38
|
+
return raw;
|
|
39
|
+
if (type === 'id')
|
|
40
|
+
return /^[A-Za-z_][\w.-]*$/.test(raw) ? raw : undefined;
|
|
41
|
+
if (type === 'number')
|
|
42
|
+
return Number.isFinite(Number(raw)) ? Number(raw) : undefined;
|
|
43
|
+
if (type === 'integer')
|
|
44
|
+
return /^-?\d+$/.test(raw) ? Number(raw) : undefined;
|
|
45
|
+
if (type === 'boolean')
|
|
46
|
+
return raw === 'true' || raw === 'yes' ? true : raw === 'false' || raw === 'no' ? false : undefined;
|
|
47
|
+
const parts = raw
|
|
48
|
+
.split(',')
|
|
49
|
+
.map((part) => part.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
if (type === 'id-list')
|
|
52
|
+
return parts.every((part) => /^[A-Za-z_][\w.-]*$/.test(part)) ? parts : undefined;
|
|
53
|
+
return parts.every((part) => Number.isFinite(Number(part))) ? parts.map(Number) : undefined;
|
|
54
|
+
}
|
|
55
|
+
function diagnostic(code, message, node) {
|
|
56
|
+
return { code, message, location: node.location ?? { file: '<mwl>', line: 1, column: 1 } };
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=readers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readers.js","sourceRoot":"","sources":["../../src/mwl/readers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MwlCompiledGame } from './compiler.ts';
|
|
2
|
+
export interface MwlContentReport {
|
|
3
|
+
readonly tags: Readonly<Record<string, number>>;
|
|
4
|
+
readonly opaqueTags: readonly string[];
|
|
5
|
+
readonly danglingReferences: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
/** Summarise a compiled catalog for parity checks and CI output. */
|
|
8
|
+
export declare function contentReport(game: MwlCompiledGame): MwlContentReport;
|
|
9
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/mwl/report.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAIrD,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/C;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,gBAAgB,CA8BrE"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { schema01 } from './schema.js';
|
|
2
|
+
import { flattenNodes } from './utils.js';
|
|
3
|
+
/** Summarise a compiled catalog for parity checks and CI output. */
|
|
4
|
+
export function contentReport(game) {
|
|
5
|
+
const nodes = flattenNodes(game.roots);
|
|
6
|
+
const tags = {};
|
|
7
|
+
const ids = new Set();
|
|
8
|
+
for (const node of nodes) {
|
|
9
|
+
tags[node.tag] = (tags[node.tag] ?? 0) + 1;
|
|
10
|
+
if (node.attributes.id)
|
|
11
|
+
ids.add(node.attributes.id);
|
|
12
|
+
}
|
|
13
|
+
const knownTags = new Set(Object.keys(schema01));
|
|
14
|
+
const opaqueTags = nodes
|
|
15
|
+
.filter((node) => !knownTags.has(node.tag))
|
|
16
|
+
.map((node) => node.tag)
|
|
17
|
+
.filter((tag, index, all) => all.indexOf(tag) === index)
|
|
18
|
+
.sort();
|
|
19
|
+
const references = new Set();
|
|
20
|
+
for (const node of nodes) {
|
|
21
|
+
for (const [key, value] of Object.entries(node.attributes)) {
|
|
22
|
+
if (/^(?:ability|item|terrain|unit|unit_type|weapon)(?:_id)?$/.test(key))
|
|
23
|
+
for (const reference of value
|
|
24
|
+
.split(',')
|
|
25
|
+
.map((part) => part.trim())
|
|
26
|
+
.filter(Boolean))
|
|
27
|
+
references.add(reference);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
tags: Object.fromEntries(Object.entries(tags).sort(([a], [b]) => a.localeCompare(b))),
|
|
32
|
+
opaqueTags,
|
|
33
|
+
danglingReferences: [...references].filter((reference) => !ids.has(reference)).sort(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/mwl/report.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAQ1C,oEAAoE;AACpE,MAAM,UAAU,aAAa,CAAC,IAAqB;IAClD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvC,MAAM,IAAI,GAA2B,EAAE,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,IAAI,CAAC,UAAU,CAAC,EAAE;YAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;IACrD,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,KAAK;SACtB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;SAC1C,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC;SACvB,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC;SACvD,IAAI,EAAE,CAAC;IACT,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5D,IAAI,0DAA0D,CAAC,IAAI,CAAC,GAAG,CAAC;gBACvE,KAAK,MAAM,SAAS,IAAI,KAAK;qBAC3B,KAAK,CAAC,GAAG,CAAC;qBACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;qBAC1B,MAAM,CAAC,OAAO,CAAC;oBAChB,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7B,CAAC;IACF,CAAC;IACD,OAAO;QACN,IAAI,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;QACrF,UAAU;QACV,kBAAkB,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE;KACrF,CAAC;AACH,CAAC"}
|
package/dist/mwl/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MwlCompiledGame } from './compiler.ts';
|
|
1
|
+
import type { MwlCompiledGame, MwlCompiledNode } from './compiler.ts';
|
|
2
2
|
import type { AiHook, CommandHook, GeneratorHook, MigrationHook, ModifierHook, PredicateHook } from './hooks.ts';
|
|
3
3
|
import { type MwlPersistenceOptions } from './persistence.ts';
|
|
4
4
|
export interface MwlMapStart {
|
|
@@ -44,6 +44,30 @@ export interface MwlWorld {
|
|
|
44
44
|
/** the current schedule entry, by id */
|
|
45
45
|
timeOfDay?: string;
|
|
46
46
|
scheduleIndex?: number;
|
|
47
|
+
/** ids of one-shot events that already fired, so a restore keeps them spent */
|
|
48
|
+
firedEvents?: string[];
|
|
49
|
+
pendingDialogue?: {
|
|
50
|
+
id: string;
|
|
51
|
+
choices: readonly MwlDialogueChoice[];
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** One `[message]`: the text plus whatever a game needs to show it. */
|
|
55
|
+
export interface MwlMessage {
|
|
56
|
+
readonly text: string;
|
|
57
|
+
readonly speaker?: string;
|
|
58
|
+
readonly portrait?: string;
|
|
59
|
+
readonly side?: number;
|
|
60
|
+
/** choices waiting on the player; answer with `answerDialogue` */
|
|
61
|
+
readonly choices?: readonly MwlDialogueChoice[];
|
|
62
|
+
/** identifies the pending dialogue this message's choices belong to */
|
|
63
|
+
readonly dialogueId?: string;
|
|
64
|
+
}
|
|
65
|
+
/** One offered choice inside a `dialogue`: its label and the event answering runs. */
|
|
66
|
+
export interface MwlDialogueChoice {
|
|
67
|
+
readonly text: string;
|
|
68
|
+
readonly event?: string;
|
|
69
|
+
/** inline branch commands, used when a choice does not name a separate event */
|
|
70
|
+
readonly branch?: readonly MwlCompiledNode[];
|
|
47
71
|
}
|
|
48
72
|
/** The hook implementations a game provides to the runtime. */
|
|
49
73
|
export interface MwlHookRegistry {
|
|
@@ -56,7 +80,8 @@ export interface MwlHookRegistry {
|
|
|
56
80
|
}
|
|
57
81
|
export interface MwlRuntimeOptions {
|
|
58
82
|
readonly world?: MwlWorld;
|
|
59
|
-
|
|
83
|
+
/** called for every `[message]` command, with its speaker and portrait */
|
|
84
|
+
readonly onMessage?: (message: MwlMessage) => void;
|
|
60
85
|
/** resolve a `[map] file=` reference to its text */
|
|
61
86
|
readonly resolveMap?: (file: string) => string;
|
|
62
87
|
readonly hooks?: MwlHookRegistry;
|
|
@@ -129,8 +154,43 @@ export declare class MwlRuntime {
|
|
|
129
154
|
private readonly persistence;
|
|
130
155
|
private readonly unitTypes;
|
|
131
156
|
private schedule;
|
|
157
|
+
private pendingDialogue;
|
|
158
|
+
private dialogueCounter;
|
|
132
159
|
constructor(game: MwlCompiledGame, options?: MwlRuntimeOptions);
|
|
133
160
|
run(trigger: string): void;
|
|
161
|
+
/** Fire one named event through the same filter, claim, and execution path as a trigger. */
|
|
162
|
+
fireEvent(id: string): boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Fire the `on=moveto` events for a unit that has just arrived somewhere.
|
|
165
|
+
* The runtime's own `[move]` command calls this, and so does a game whose
|
|
166
|
+
* engine moved the unit: write the new position into `world.units` first,
|
|
167
|
+
* then call this with the unit id.
|
|
168
|
+
*
|
|
169
|
+
* An event matches when `x`/`y` (when given) are the unit's position, and
|
|
170
|
+
* `unit`/`side` (when given) are the unit's. A moveto event fires once by
|
|
171
|
+
* default; pass `once=false` on the event to let it fire every time.
|
|
172
|
+
*/
|
|
173
|
+
fireMoveto(id: string): void;
|
|
174
|
+
/** A `moveto` event is a story beat: it fires once unless told otherwise. */
|
|
175
|
+
private claimEvent;
|
|
176
|
+
private eventFiltersMatch;
|
|
177
|
+
private executeEvent;
|
|
178
|
+
/**
|
|
179
|
+
* Deliver a `dialogue`: its `message`/`say` lines first, then one message
|
|
180
|
+
* carrying the offered `choice`s. A choice names the event answering runs;
|
|
181
|
+
* the game answers later with `answerDialogue`, so content keeps any
|
|
182
|
+
* follow-up commands inside the referenced events rather than after the
|
|
183
|
+
* dialogue. Choices gated by `variable`/`equals` are skipped unless the
|
|
184
|
+
* world variable matches.
|
|
185
|
+
*/
|
|
186
|
+
private showDialogue;
|
|
187
|
+
private showSay;
|
|
188
|
+
/**
|
|
189
|
+
* Answer a pending dialogue: run the chosen choice's event and clear the
|
|
190
|
+
* pending state. Returns false when there is no such pending dialogue or
|
|
191
|
+
* choice; throws when the referenced event does not exist (content error).
|
|
192
|
+
*/
|
|
193
|
+
answerDialogue(dialogueId: string, choiceIndex: number): boolean;
|
|
134
194
|
/**
|
|
135
195
|
* Evaluate `[objectives]` now and return the resulting status. A game whose
|
|
136
196
|
* own engine resolved the actions (real movement, combat) calls this after
|
|
@@ -150,6 +210,9 @@ export declare class MwlRuntime {
|
|
|
150
210
|
private executeNode;
|
|
151
211
|
private runHook;
|
|
152
212
|
private applyMove;
|
|
213
|
+
/** Deliver a `[message]` to the game, with speaker and portrait when given. */
|
|
214
|
+
private showMessage;
|
|
215
|
+
private setVariable;
|
|
153
216
|
private spawnUnit;
|
|
154
217
|
private killUnit;
|
|
155
218
|
private addGold;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/mwl/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/mwl/runtime.ts"],"names":[],"mappings":"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"}
|
package/dist/mwl/runtime.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { decodeSave, encodeSave } from './persistence.js';
|
|
2
|
+
import { evaluateExpression } from './expression.js';
|
|
3
|
+
import { integerAttribute, requiredAttribute } from './utils.js';
|
|
4
|
+
const required = requiredAttribute;
|
|
5
|
+
const integer = (node, attribute, fallback) => integerAttribute(node, attribute, fallback) ?? fallback;
|
|
2
6
|
export function createWorld() {
|
|
3
7
|
return {
|
|
4
8
|
variables: {},
|
|
@@ -54,6 +58,8 @@ export class MwlRuntime {
|
|
|
54
58
|
constructor(game, options = {}) {
|
|
55
59
|
this.unitTypes = new Map();
|
|
56
60
|
this.schedule = [];
|
|
61
|
+
this.pendingDialogue = null;
|
|
62
|
+
this.dialogueCounter = 0;
|
|
57
63
|
this.game = game;
|
|
58
64
|
this.world = options.world ?? createWorld();
|
|
59
65
|
this.onMessage = options.onMessage;
|
|
@@ -70,19 +76,183 @@ export class MwlRuntime {
|
|
|
70
76
|
for (const event of this.nodes('event')) {
|
|
71
77
|
if ((event.attributes.on ?? event.attributes.trigger) !== trigger)
|
|
72
78
|
continue;
|
|
73
|
-
|
|
74
|
-
if (condition && this.world.variables[condition.attributes.variable] !== condition.attributes.equals)
|
|
79
|
+
if (!this.eventFiltersMatch(event))
|
|
75
80
|
continue;
|
|
76
|
-
|
|
77
|
-
if (filter && !this.filterMatches(filter))
|
|
81
|
+
if (!this.claimEvent(event))
|
|
78
82
|
continue;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
this.executeEvent(event);
|
|
84
|
+
}
|
|
85
|
+
this.checkObjectives();
|
|
86
|
+
}
|
|
87
|
+
/** Fire one named event through the same filter, claim, and execution path as a trigger. */
|
|
88
|
+
fireEvent(id) {
|
|
89
|
+
const event = this.nodes('event').find((candidate) => candidate.attributes.id === id);
|
|
90
|
+
if (!event || !this.eventFiltersMatch(event) || !this.claimEvent(event))
|
|
91
|
+
return false;
|
|
92
|
+
this.executeEvent(event);
|
|
93
|
+
this.checkObjectives();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Fire the `on=moveto` events for a unit that has just arrived somewhere.
|
|
98
|
+
* The runtime's own `[move]` command calls this, and so does a game whose
|
|
99
|
+
* engine moved the unit: write the new position into `world.units` first,
|
|
100
|
+
* then call this with the unit id.
|
|
101
|
+
*
|
|
102
|
+
* An event matches when `x`/`y` (when given) are the unit's position, and
|
|
103
|
+
* `unit`/`side` (when given) are the unit's. A moveto event fires once by
|
|
104
|
+
* default; pass `once=false` on the event to let it fire every time.
|
|
105
|
+
*/
|
|
106
|
+
fireMoveto(id) {
|
|
107
|
+
const unit = this.world.units[id];
|
|
108
|
+
if (!unit || !unit.alive)
|
|
109
|
+
return;
|
|
110
|
+
for (const event of this.nodes('event')) {
|
|
111
|
+
if ((event.attributes.on ?? event.attributes.trigger) !== 'moveto')
|
|
112
|
+
continue;
|
|
113
|
+
if (event.attributes.x !== undefined && !coordinateMatches(event.attributes.x, unit.x))
|
|
114
|
+
continue;
|
|
115
|
+
if (event.attributes.y !== undefined && !coordinateMatches(event.attributes.y, unit.y))
|
|
116
|
+
continue;
|
|
117
|
+
if (event.attributes.unit !== undefined && event.attributes.unit !== id)
|
|
118
|
+
continue;
|
|
119
|
+
if (event.attributes.side !== undefined && integer(event, 'side', Number.NaN) !== unit.side)
|
|
120
|
+
continue;
|
|
121
|
+
if (!this.eventFiltersMatch(event))
|
|
122
|
+
continue;
|
|
123
|
+
if (!this.claimEvent(event))
|
|
124
|
+
continue;
|
|
125
|
+
this.executeEvent(event);
|
|
126
|
+
}
|
|
127
|
+
this.checkObjectives();
|
|
128
|
+
}
|
|
129
|
+
/** A `moveto` event is a story beat: it fires once unless told otherwise. */
|
|
130
|
+
claimEvent(event) {
|
|
131
|
+
const once = event.attributes.once === undefined ? event.attributes.on === 'moveto' : event.attributes.once !== 'false';
|
|
132
|
+
if (!once)
|
|
133
|
+
return true;
|
|
134
|
+
const key = event.attributes.id ??
|
|
135
|
+
`${event.attributes.on ?? event.attributes.trigger ?? 'event'}@${event.location?.line ?? 0}`;
|
|
136
|
+
const fired = (this.world.firedEvents ??= []);
|
|
137
|
+
if (fired.includes(key))
|
|
138
|
+
return false;
|
|
139
|
+
fired.push(key);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
eventFiltersMatch(event) {
|
|
143
|
+
const condition = event.children.find((child) => child.tag === 'condition');
|
|
144
|
+
if (condition && !sameValue(this.world.variables[condition.attributes.variable], condition.attributes.equals))
|
|
145
|
+
return false;
|
|
146
|
+
const filter = event.children.find((child) => child.tag === 'filter');
|
|
147
|
+
return !filter || this.filterMatches(filter);
|
|
148
|
+
}
|
|
149
|
+
executeEvent(event) {
|
|
150
|
+
for (const command of event.children.filter((child) => child.tag !== 'condition' && child.tag !== 'filter')) {
|
|
151
|
+
if (command.tag === 'say') {
|
|
152
|
+
this.showSay(command);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (command.tag === 'dialogue') {
|
|
156
|
+
this.showDialogue(command);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
this.executeNode(command);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Deliver a `dialogue`: its `message`/`say` lines first, then one message
|
|
164
|
+
* carrying the offered `choice`s. A choice names the event answering runs;
|
|
165
|
+
* the game answers later with `answerDialogue`, so content keeps any
|
|
166
|
+
* follow-up commands inside the referenced events rather than after the
|
|
167
|
+
* dialogue. Choices gated by `variable`/`equals` are skipped unless the
|
|
168
|
+
* world variable matches.
|
|
169
|
+
*/
|
|
170
|
+
showDialogue(node) {
|
|
171
|
+
let last = null;
|
|
172
|
+
for (const child of node.children) {
|
|
173
|
+
if (child.tag === 'message') {
|
|
174
|
+
this.showMessage(child.attributes);
|
|
175
|
+
const side = Number.parseInt(child.attributes.side ?? '', 10);
|
|
176
|
+
last = {
|
|
177
|
+
text: child.attributes.text ?? child.attributes.value ?? '',
|
|
178
|
+
...(child.attributes.speaker === undefined ? {} : { speaker: child.attributes.speaker }),
|
|
179
|
+
...(child.attributes.portrait === undefined ? {} : { portrait: child.attributes.portrait }),
|
|
180
|
+
...(Number.isFinite(side) ? { side } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
else if (child.tag === 'say') {
|
|
184
|
+
this.showSay(child);
|
|
185
|
+
last = { text: child.attributes.text ?? '', speaker: child.attributes.speaker };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const choices = [];
|
|
189
|
+
for (const child of node.children) {
|
|
190
|
+
if (child.tag !== 'choice' || !child.attributes.text)
|
|
191
|
+
continue;
|
|
192
|
+
if (child.attributes.variable !== undefined &&
|
|
193
|
+
!sameValue(this.world.variables[child.attributes.variable], child.attributes.equals))
|
|
194
|
+
continue;
|
|
195
|
+
const branches = child.children.filter((branch) => branch.tag === 'branch');
|
|
196
|
+
if (branches.length) {
|
|
197
|
+
for (const branch of branches) {
|
|
198
|
+
choices.push({
|
|
199
|
+
text: branch.attributes.text ?? child.attributes.text,
|
|
200
|
+
branch: branch.children,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (child.attributes.event)
|
|
205
|
+
choices.push({ text: child.attributes.text, event: child.attributes.event });
|
|
206
|
+
}
|
|
207
|
+
if (choices.length === 0)
|
|
208
|
+
return;
|
|
209
|
+
this.dialogueCounter += 1;
|
|
210
|
+
const id = `dialogue-${this.dialogueCounter}`;
|
|
211
|
+
this.pendingDialogue = { id, choices };
|
|
212
|
+
this.world.pendingDialogue = { id, choices };
|
|
213
|
+
this.onMessage?.({
|
|
214
|
+
text: last?.text ?? '',
|
|
215
|
+
...(last?.speaker === undefined ? {} : { speaker: last.speaker }),
|
|
216
|
+
choices,
|
|
217
|
+
dialogueId: id,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
showSay(node) {
|
|
221
|
+
if (!this.onMessage)
|
|
222
|
+
return;
|
|
223
|
+
this.onMessage({
|
|
224
|
+
text: node.attributes.text ?? '',
|
|
225
|
+
...(node.attributes.speaker === undefined ? {} : { speaker: node.attributes.speaker }),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Answer a pending dialogue: run the chosen choice's event and clear the
|
|
230
|
+
* pending state. Returns false when there is no such pending dialogue or
|
|
231
|
+
* choice; throws when the referenced event does not exist (content error).
|
|
232
|
+
*/
|
|
233
|
+
answerDialogue(dialogueId, choiceIndex) {
|
|
234
|
+
const pending = this.pendingDialogue;
|
|
235
|
+
if (!pending || pending.id !== dialogueId)
|
|
236
|
+
return false;
|
|
237
|
+
const choice = pending.choices[choiceIndex];
|
|
238
|
+
if (!choice)
|
|
239
|
+
return false;
|
|
240
|
+
this.pendingDialogue = null;
|
|
241
|
+
delete this.world.pendingDialogue;
|
|
242
|
+
if (choice.event) {
|
|
243
|
+
const event = this.nodes('event').find((candidate) => candidate.attributes.id === choice.event);
|
|
244
|
+
if (!event)
|
|
245
|
+
throw new Error(`MWL dialogue choice points at unknown event: ${choice.event}`);
|
|
246
|
+
if (!this.claimEvent(event))
|
|
247
|
+
return true;
|
|
248
|
+
this.executeEvent(event);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
for (const command of choice.branch ?? [])
|
|
83
252
|
this.executeNode(command);
|
|
84
253
|
}
|
|
85
254
|
this.checkObjectives();
|
|
255
|
+
return true;
|
|
86
256
|
}
|
|
87
257
|
/**
|
|
88
258
|
* Evaluate `[objectives]` now and return the resulting status. A game whose
|
|
@@ -111,6 +281,15 @@ export class MwlRuntime {
|
|
|
111
281
|
this.world.map = restored.map ?? null;
|
|
112
282
|
this.world.timeOfDay = restored.timeOfDay ?? this.schedule[0] ?? '';
|
|
113
283
|
this.world.scheduleIndex = restored.scheduleIndex ?? 0;
|
|
284
|
+
// One-shot events stay spent across a load.
|
|
285
|
+
this.world.firedEvents = [...(restored.firedEvents ?? [])];
|
|
286
|
+
this.world.pendingDialogue = restored.pendingDialogue;
|
|
287
|
+
this.pendingDialogue = restored.pendingDialogue
|
|
288
|
+
? { id: restored.pendingDialogue.id, choices: [...restored.pendingDialogue.choices] }
|
|
289
|
+
: null;
|
|
290
|
+
const dialogueNumber = restored.pendingDialogue?.id.match(/^dialogue-(\d+)$/);
|
|
291
|
+
if (dialogueNumber)
|
|
292
|
+
this.dialogueCounter = Math.max(this.dialogueCounter, Number(dialogueNumber[1]));
|
|
114
293
|
}
|
|
115
294
|
loadUnitTypes() {
|
|
116
295
|
for (const node of this.nodes('unit_type')) {
|
|
@@ -225,9 +404,10 @@ export class MwlRuntime {
|
|
|
225
404
|
if (!name)
|
|
226
405
|
throw new Error('MWL command is missing name');
|
|
227
406
|
switch (name) {
|
|
228
|
-
case 'message':
|
|
229
|
-
this.
|
|
407
|
+
case 'message': {
|
|
408
|
+
this.showMessage(node.attributes);
|
|
230
409
|
break;
|
|
410
|
+
}
|
|
231
411
|
case 'spawn':
|
|
232
412
|
this.spawnUnit(node.attributes.type ?? '', node.attributes.side === undefined ? undefined : integer(node, 'side', 0), integer(node, 'x', 0), integer(node, 'y', 0), node.attributes.id ?? node.attributes.target, node.attributes.hp === undefined ? undefined : integer(node, 'hp', 1));
|
|
233
413
|
break;
|
|
@@ -247,8 +427,7 @@ export class MwlRuntime {
|
|
|
247
427
|
this.addGold(required(node, 'side'), integer(node, 'delta', integer(node, 'amount', 0)));
|
|
248
428
|
break;
|
|
249
429
|
case 'set_variable':
|
|
250
|
-
this.
|
|
251
|
-
node.attributes.value ?? '';
|
|
430
|
+
this.setVariable(node.tag === 'command' ? required(node, 'target') : required(node, 'name'), node.attributes.value ?? '');
|
|
252
431
|
break;
|
|
253
432
|
case 'end_turn':
|
|
254
433
|
this.endTurn();
|
|
@@ -303,6 +482,33 @@ export class MwlRuntime {
|
|
|
303
482
|
}
|
|
304
483
|
unit.x = x;
|
|
305
484
|
unit.y = y;
|
|
485
|
+
this.fireMoveto(id);
|
|
486
|
+
}
|
|
487
|
+
/** Deliver a `[message]` to the game, with speaker and portrait when given. */
|
|
488
|
+
showMessage(attributes) {
|
|
489
|
+
if (!this.onMessage)
|
|
490
|
+
return;
|
|
491
|
+
const side = Number.parseInt(attributes.side ?? '', 10);
|
|
492
|
+
const message = {
|
|
493
|
+
text: attributes.text ?? attributes.value ?? '',
|
|
494
|
+
...(attributes.speaker === undefined ? {} : { speaker: attributes.speaker }),
|
|
495
|
+
...(attributes.portrait === undefined ? {} : { portrait: attributes.portrait }),
|
|
496
|
+
...(Number.isFinite(side) ? { side } : {}),
|
|
497
|
+
};
|
|
498
|
+
this.onMessage(message);
|
|
499
|
+
}
|
|
500
|
+
setVariable(name, raw) {
|
|
501
|
+
const numeric = Number(raw);
|
|
502
|
+
if (raw.trim() !== '' && Number.isFinite(numeric)) {
|
|
503
|
+
this.world.variables[name] = numeric;
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const context = Object.fromEntries(Object.entries(this.world.variables).filter((entry) => typeof entry[1] === 'number'));
|
|
507
|
+
if (/[+*/^()]|\s-\s/.test(raw) || Object.prototype.hasOwnProperty.call(context, raw.trim())) {
|
|
508
|
+
this.world.variables[name] = evaluateExpression(raw, context);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
this.world.variables[name] = raw;
|
|
306
512
|
}
|
|
307
513
|
spawnUnit(type, side, x, y, id, hp) {
|
|
308
514
|
const stats = type ? this.unitTypes.get(type) : undefined;
|
|
@@ -349,15 +555,16 @@ export class MwlRuntime {
|
|
|
349
555
|
}
|
|
350
556
|
}
|
|
351
557
|
filterMatches(node) {
|
|
352
|
-
|
|
353
|
-
|
|
558
|
+
return Object.entries(this.world.units).some(([id, unit]) => unit.alive &&
|
|
559
|
+
(node.attributes.unit === undefined || id === node.attributes.unit) &&
|
|
560
|
+
(node.attributes.side === undefined || unit.side === Number(node.attributes.side)) &&
|
|
354
561
|
(node.attributes.type === undefined || unit.type === node.attributes.type) &&
|
|
355
562
|
(node.attributes.x === undefined || unit.x === Number(node.attributes.x)) &&
|
|
356
563
|
(node.attributes.y === undefined || unit.y === Number(node.attributes.y)));
|
|
357
564
|
}
|
|
358
565
|
conditionMatches(node) {
|
|
359
566
|
const condition = node.children.find((child) => child.tag === 'condition');
|
|
360
|
-
return !condition || this.world.variables[condition.attributes.variable]
|
|
567
|
+
return (!condition || sameValue(this.world.variables[condition.attributes.variable], condition.attributes.equals));
|
|
361
568
|
}
|
|
362
569
|
conditionMet(node) {
|
|
363
570
|
const condition = node.attributes.condition ?? '';
|
|
@@ -382,7 +589,9 @@ export class MwlRuntime {
|
|
|
382
589
|
case 'unit_at': {
|
|
383
590
|
const x = optionalInteger(node, 'x');
|
|
384
591
|
const y = optionalInteger(node, 'y');
|
|
592
|
+
const targetSide = optionalInteger(node, 'side_filter') ?? side;
|
|
385
593
|
return Object.values(this.world.units).some((unit) => unit.alive &&
|
|
594
|
+
(targetSide === undefined || unit.side === targetSide) &&
|
|
386
595
|
(node.attributes.type === undefined || unit.type === node.attributes.type) &&
|
|
387
596
|
unit.x === x &&
|
|
388
597
|
unit.y === y);
|
|
@@ -431,7 +640,7 @@ export class MwlRuntime {
|
|
|
431
640
|
setVariable: (name, value) => {
|
|
432
641
|
this.world.variables[name] = value;
|
|
433
642
|
},
|
|
434
|
-
message: (
|
|
643
|
+
message: (speaker, text) => this.onMessage?.({ text, ...(speaker ? { speaker } : {}) }),
|
|
435
644
|
endTurn: () => this.endTurn(),
|
|
436
645
|
win: (_side) => {
|
|
437
646
|
this.world.status = 'won';
|
|
@@ -499,16 +708,6 @@ function requireUnit(world, id) {
|
|
|
499
708
|
throw new Error(`MWL unit is not alive: ${id}`);
|
|
500
709
|
return unit;
|
|
501
710
|
}
|
|
502
|
-
function required(node, attribute) {
|
|
503
|
-
const value = node.attributes[attribute];
|
|
504
|
-
if (!value)
|
|
505
|
-
throw new Error(`MWL ${node.tag} is missing ${attribute}`);
|
|
506
|
-
return value;
|
|
507
|
-
}
|
|
508
|
-
function integer(node, attribute, fallback) {
|
|
509
|
-
const value = node.attributes[attribute];
|
|
510
|
-
return value === undefined ? fallback : Number.parseInt(value, 10);
|
|
511
|
-
}
|
|
512
711
|
function optionalInteger(node, attribute) {
|
|
513
712
|
const value = node.attributes[attribute];
|
|
514
713
|
if (value === undefined || value === '')
|
|
@@ -516,4 +715,24 @@ function optionalInteger(node, attribute) {
|
|
|
516
715
|
const parsed = Number.parseInt(value, 10);
|
|
517
716
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
518
717
|
}
|
|
718
|
+
function sameValue(value, expected) {
|
|
719
|
+
if (value === undefined || expected === undefined)
|
|
720
|
+
return value === expected;
|
|
721
|
+
const numeric = Number(expected);
|
|
722
|
+
return typeof value === 'number' && expected.trim() !== '' && Number.isFinite(numeric)
|
|
723
|
+
? value === numeric
|
|
724
|
+
: value === expected;
|
|
725
|
+
}
|
|
726
|
+
function coordinateMatches(specification, coordinate) {
|
|
727
|
+
return specification.split(',').some((part) => {
|
|
728
|
+
const value = part.trim();
|
|
729
|
+
const range = value.match(/^(-?\d+)\s*-\s*(-?\d+)$/);
|
|
730
|
+
if (range) {
|
|
731
|
+
const from = Number(range[1]);
|
|
732
|
+
const to = Number(range[2]);
|
|
733
|
+
return coordinate >= Math.min(from, to) && coordinate <= Math.max(from, to);
|
|
734
|
+
}
|
|
735
|
+
return /^-?\d+$/.test(value) && Number(value) === coordinate;
|
|
736
|
+
});
|
|
737
|
+
}
|
|
519
738
|
//# sourceMappingURL=runtime.js.map
|