@react-shimeji/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1362 @@
1
+ // src/dom.ts
2
+ var DomManager = class {
3
+ /** Creates an isolated work area inside the supplied host element. */
4
+ constructor(container, sprites, workAreaClassName) {
5
+ this.container = container;
6
+ this.sprites = sprites;
7
+ const workArea = document.createElement("div");
8
+ workArea.dataset.reactShimejiWorkArea = "true";
9
+ if (workAreaClassName) workArea.className = workAreaClassName;
10
+ Object.assign(workArea.style, {
11
+ position: "absolute",
12
+ inset: "0",
13
+ width: "100%",
14
+ height: "100%",
15
+ overflow: "hidden",
16
+ pointerEvents: "none",
17
+ zIndex: "2147483643"
18
+ });
19
+ this.workArea = workArea;
20
+ this.ensureMounted();
21
+ }
22
+ container;
23
+ sprites;
24
+ /** Generated element that contains all mascots. */
25
+ workArea;
26
+ /** Reattaches the work area if application code temporarily removed it. */
27
+ ensureMounted() {
28
+ if (this.workArea.parentElement !== this.container) this.container.appendChild(this.workArea);
29
+ }
30
+ /** Returns the current local work-area rectangle. */
31
+ getBounds() {
32
+ const rectangle = this.workArea.getBoundingClientRect();
33
+ const width = rectangle.width || this.container.clientWidth || window.innerWidth;
34
+ const height = rectangle.height || this.container.clientHeight || window.innerHeight;
35
+ return { x: 0, y: 0, width, height };
36
+ }
37
+ /** Creates a mascot node and acquires its spritesheet resource. */
38
+ createMascot(spec, mascotId, mascotClassName) {
39
+ const spriteLease = this.sprites.acquire(spec.spritesheet);
40
+ const element = document.createElement("div");
41
+ element.dataset.shimejiId = mascotId;
42
+ if (mascotClassName) element.className = mascotClassName;
43
+ Object.assign(element.style, { position: "absolute", left: "0", top: "0", width: "0", height: "0", pointerEvents: "auto", touchAction: "none", userSelect: "none", willChange: "transform" });
44
+ const spriteElement = document.createElement("div");
45
+ Object.assign(spriteElement.style, { position: "absolute", left: "0", top: "0", backgroundRepeat: "no-repeat", transformOrigin: "center center", pointerEvents: "none" });
46
+ element.appendChild(spriteElement);
47
+ this.workArea.appendChild(element);
48
+ return { element, spriteElement, spriteLease };
49
+ }
50
+ /** Paints one mascot state into its existing DOM nodes. */
51
+ render(handle, spec, state) {
52
+ const sprite = this.sprites.resolve(spec, handle.spriteLease, state.sprite);
53
+ handle.element.style.transform = `translate3d(${state.x - state.anchorX}px, ${state.y - state.anchorY}px, 0)`;
54
+ handle.spriteElement.style.left = "0";
55
+ handle.spriteElement.style.top = "0";
56
+ handle.spriteElement.style.transform = `scaleX(${state.lookRight ? -1 : 1})`;
57
+ if (!sprite) return;
58
+ handle.spriteElement.style.backgroundImage = `url("${sprite.url.replaceAll('"', '\\"')}")`;
59
+ if (sprite.rectangle) {
60
+ handle.spriteElement.style.width = `${sprite.rectangle.width}px`;
61
+ handle.spriteElement.style.height = `${sprite.rectangle.height}px`;
62
+ handle.element.style.width = `${sprite.rectangle.width}px`;
63
+ handle.element.style.height = `${sprite.rectangle.height}px`;
64
+ handle.spriteElement.style.backgroundPosition = `${-sprite.rectangle.x}px ${-sprite.rectangle.y}px`;
65
+ handle.spriteElement.style.backgroundSize = "auto";
66
+ } else {
67
+ handle.spriteElement.style.backgroundPosition = "0 0";
68
+ handle.spriteElement.style.backgroundSize = "contain";
69
+ handle.spriteElement.style.width = "128px";
70
+ handle.spriteElement.style.height = "128px";
71
+ const definition = Object.values(spec.sprites).find((candidate) => typeof candidate === "object" && "url" in candidate && candidate.url === sprite.url);
72
+ if (typeof definition === "object" && "width" in definition && definition.width !== void 0) handle.spriteElement.style.width = `${definition.width}px`;
73
+ if (typeof definition === "object" && "height" in definition && definition.height !== void 0) handle.spriteElement.style.height = `${definition.height}px`;
74
+ handle.element.style.width = handle.spriteElement.style.width;
75
+ handle.element.style.height = handle.spriteElement.style.height;
76
+ }
77
+ }
78
+ /** Removes one mascot node and releases its temporary image URL. */
79
+ removeMascot(handle) {
80
+ handle.element.remove();
81
+ handle.spriteLease.release();
82
+ }
83
+ /** Removes the work area owned by this manager. */
84
+ destroy() {
85
+ this.workArea.remove();
86
+ }
87
+ };
88
+
89
+ // src/loader.ts
90
+ var actionTypeNames = {
91
+ Sequence: "Sequence",
92
+ Select: "Select",
93
+ Reference: "Reference",
94
+ Stay: "Stay",
95
+ Animate: "Animate",
96
+ Move: "Move",
97
+ Embedded: "Embedded",
98
+ \u8907\u5408: "Sequence",
99
+ \u9078\u629E: "Select",
100
+ \u53C2\u7167: "Reference",
101
+ \u9759\u6B62: "Stay",
102
+ \u56FA\u5B9A: "Animate",
103
+ \u79FB\u52D5: "Move",
104
+ \u7D44\u307F\u8FBC\u307F: "Embedded"
105
+ };
106
+ var borderTypeNames = { Floor: "Floor", Wall: "Wall", Ceiling: "Ceiling", \u5730\u9762: "Floor", \u58C1: "Wall", \u5929\u4E95: "Ceiling" };
107
+ function parseJson(value, label) {
108
+ if (typeof value !== "string") return value;
109
+ try {
110
+ return JSON.parse(value);
111
+ } catch (error) {
112
+ throw new TypeError(`Invalid ${label}: ${error instanceof Error ? error.message : String(error)}`);
113
+ }
114
+ }
115
+ function attribute(element, ...names) {
116
+ for (const name of names) {
117
+ const value = element.getAttribute(name);
118
+ if (value !== null) return value;
119
+ }
120
+ return void 0;
121
+ }
122
+ function directChildren(element, ...names) {
123
+ const accepted = new Set(names);
124
+ return Array.from(element.children).filter((child) => accepted.has(child.localName) || accepted.has(child.tagName));
125
+ }
126
+ function parsePoint(value, fallback = { x: 0, y: 0 }) {
127
+ if (!value) return fallback;
128
+ const [x = fallback.x, y = fallback.y] = value.split(",").map(Number);
129
+ return { x: Number.isFinite(x) ? x : fallback.x, y: Number.isFinite(y) ? y : fallback.y };
130
+ }
131
+ function requireDomParser() {
132
+ if (typeof DOMParser === "undefined") throw new Error("XML character packs require the browser DOMParser API; use pre-parsed JSON in non-browser environments");
133
+ return new DOMParser();
134
+ }
135
+ function parseDocument(xml, label) {
136
+ const document2 = requireDomParser().parseFromString(xml.replace(/^\uFEFF/, ""), "application/xml");
137
+ const error = document2.querySelector("parsererror");
138
+ if (error) throw new TypeError(`Invalid ${label}: ${error.textContent?.trim() ?? "XML parse error"}`);
139
+ return document2;
140
+ }
141
+ function actionProperty(element, ...names) {
142
+ return attribute(element, ...names);
143
+ }
144
+ function parseAnimation(element) {
145
+ const poses = directChildren(element, "Pose", "\u30DD\u30FC\u30BA").map((pose) => ({
146
+ sprite: attribute(pose, "Image", "\u753B\u50CF") ?? "/shime1.png",
147
+ anchor: parsePoint(attribute(pose, "Anchor", "\u57FA\u6E96\u5EA7\u6A19"), { x: 64, y: 128 }),
148
+ velocity: parsePoint(attribute(pose, "Velocity", "\u79FB\u52D5\u901F\u5EA6")),
149
+ duration: Number(attribute(pose, "Duration", "\u9577\u3055") ?? 1)
150
+ }));
151
+ const condition = attribute(element, "Condition", "\u6761\u4EF6");
152
+ return { poses, ...condition !== void 0 && { condition } };
153
+ }
154
+ function parseActionElement(element) {
155
+ const isReference = element.localName === "ActionReference" || element.localName === "\u52D5\u4F5C\u53C2\u7167";
156
+ const rawType = isReference ? "Reference" : attribute(element, "Type", "\u7A2E\u985E") ?? (directChildren(element, "Action", "\u52D5\u4F5C", "ActionReference", "\u52D5\u4F5C\u53C2\u7167").length ? "Sequence" : "Animate");
157
+ const className = attribute(element, "Class", "\u30AF\u30E9\u30B9");
158
+ const embedType = className?.split(".").at(-1);
159
+ const type = actionTypeNames[rawType] ?? (embedType ? "Embedded" : "Animate");
160
+ const name = attribute(element, "Name", "\u540D\u524D");
161
+ const condition = actionProperty(element, "Condition", "\u6761\u4EF6");
162
+ const borderRaw = actionProperty(element, "BorderType", "Border", "\u67A0");
163
+ const actions = directChildren(element, "Action", "\u52D5\u4F5C", "ActionReference", "\u52D5\u4F5C\u53C2\u7167").map(parseActionElement);
164
+ const animations = directChildren(element, "Animation", "\u30A2\u30CB\u30E1\u30FC\u30B7\u30E7\u30F3").map(parseAnimation);
165
+ const result = {
166
+ type,
167
+ ...name !== void 0 && { name },
168
+ ...embedType !== void 0 && { embedType },
169
+ ...condition !== void 0 && { condition },
170
+ ...borderRaw !== void 0 && borderTypeNames[borderRaw] !== void 0 && { borderType: borderTypeNames[borderRaw] },
171
+ ...actions.length > 0 && { actions },
172
+ ...animations.length > 0 && { animations }
173
+ };
174
+ const properties = [
175
+ ["duration", actionProperty(element, "Duration", "\u9577\u3055")],
176
+ ["gap", actionProperty(element, "Gap", "\u9593\u9694")],
177
+ ["targetX", actionProperty(element, "TargetX", "\u76EE\u7684\u5730X")],
178
+ ["targetY", actionProperty(element, "TargetY", "\u76EE\u7684\u5730Y")],
179
+ ["velocity", actionProperty(element, "Velocity", "\u901F\u5EA6")],
180
+ ["x", actionProperty(element, "X", "\u5909\u4F4DX")],
181
+ ["y", actionProperty(element, "Y", "\u5909\u4F4DY")],
182
+ ["initialVx", actionProperty(element, "InitialVX", "InitialVx", "\u521D\u901FX")],
183
+ ["initialVy", actionProperty(element, "InitialVY", "InitialVy", "\u521D\u901FY")],
184
+ ["resistanceX", actionProperty(element, "ResistanceX", "\u7A7A\u6C17\u62B5\u6297X")],
185
+ ["resistanceY", actionProperty(element, "ResistanceY", "\u7A7A\u6C17\u62B5\u6297Y")],
186
+ ["gravity", actionProperty(element, "Gravity", "\u91CD\u529B")],
187
+ ["bornX", actionProperty(element, "BornX", "\u8A95\u751FX")],
188
+ ["bornY", actionProperty(element, "BornY", "\u8A95\u751FY")],
189
+ ["bornBehavior", actionProperty(element, "BornBehavior", "\u8A95\u751F\u6642\u306E\u884C\u52D5")],
190
+ ["ieOffsetX", actionProperty(element, "IEOffsetX", "IE\u306E\u7AEFX")],
191
+ ["ieOffsetY", actionProperty(element, "IEOffsetY", "IE\u306E\u7AEFY")],
192
+ ["lookRight", actionProperty(element, "LookRight", "\u53F3\u5411\u304D")]
193
+ ];
194
+ for (const [key, value] of properties) if (value !== void 0) result[key] = value;
195
+ const loop = actionProperty(element, "Loop", "\u7E70\u308A\u8FD4\u3057");
196
+ if (loop !== void 0) result.loop = loop.toLowerCase() === "true";
197
+ return result;
198
+ }
199
+ function parseActionsXml(xml) {
200
+ const document2 = parseDocument(xml, "actions.xml");
201
+ const lists = Array.from(document2.getElementsByTagNameNS("*", "ActionList")).concat(Array.from(document2.getElementsByTagNameNS("*", "\u52D5\u4F5C\u30EA\u30B9\u30C8")));
202
+ const roots = lists.length ? lists : [document2.documentElement];
203
+ return roots.flatMap((list) => directChildren(list, "Action", "\u52D5\u4F5C").map(parseActionElement));
204
+ }
205
+ function parseBehaviorElement(element, inheritedConditions, groupIndex) {
206
+ const condition = attribute(element, "Condition", "\u6761\u4EF6");
207
+ const conditions = [...inheritedConditions, ...condition ? [condition] : []];
208
+ const nextList = directChildren(element, "NextBehaviorList", "NextBehavior", "\u6B21\u306E\u884C\u52D5\u30EA\u30B9\u30C8")[0];
209
+ const nextBehaviors = nextList ? directChildren(nextList, "Behavior", "\u884C\u52D5", "BehaviorReference", "BehaviorReferance", "\u884C\u52D5\u53C2\u7167").map((child) => parseBehaviorElement(child, conditions, 0)) : [];
210
+ const reference = element.localName === "BehaviorReference" || element.localName === "BehaviorReferance" || element.localName === "\u884C\u52D5\u53C2\u7167";
211
+ return {
212
+ type: reference ? "Reference" : "Behavior",
213
+ name: attribute(element, "Name", "\u540D\u524D") ?? "",
214
+ frequency: Number(attribute(element, "Frequency", "\u983B\u5EA6") ?? 0),
215
+ conditions,
216
+ nextBehaviors,
217
+ groupIndex,
218
+ hidden: (attribute(element, "Hidden", "\u975E\u8868\u793A") ?? "false").toLowerCase() === "true"
219
+ };
220
+ }
221
+ function parseBehaviorsXml(xml) {
222
+ const document2 = parseDocument(xml, "behaviors.xml");
223
+ const lists = Array.from(document2.getElementsByTagNameNS("*", "BehaviorList")).concat(Array.from(document2.getElementsByTagNameNS("*", "\u884C\u52D5\u30EA\u30B9\u30C8")));
224
+ const root = lists[0] ?? document2.documentElement;
225
+ const behaviors = [];
226
+ let groupIndex = 0;
227
+ for (const child of Array.from(root.children)) {
228
+ if (child.localName === "Condition" || child.localName === "\u6761\u4EF6") {
229
+ groupIndex += 1;
230
+ const condition = attribute(child, "Condition", "\u6761\u4EF6");
231
+ const inherited = condition ? [condition] : [];
232
+ behaviors.push(...directChildren(child, "Behavior", "\u884C\u52D5", "BehaviorReference", "BehaviorReferance", "\u884C\u52D5\u53C2\u7167").map((element) => parseBehaviorElement(element, inherited, groupIndex)));
233
+ } else if (["Behavior", "\u884C\u52D5", "BehaviorReference", "BehaviorReferance", "\u884C\u52D5\u53C2\u7167"].includes(child.localName)) {
234
+ behaviors.push(parseBehaviorElement(child, [], 0));
235
+ }
236
+ }
237
+ return behaviors;
238
+ }
239
+ function isCharacterSpec(value) {
240
+ if (!value || typeof value !== "object") return false;
241
+ const candidate = value;
242
+ return typeof candidate.id === "string" && Array.isArray(candidate.actions) && Array.isArray(candidate.behaviors) && typeof candidate.sprites === "object" && candidate.sprites !== null && (typeof candidate.spritesheet === "string" || typeof Blob !== "undefined" && candidate.spritesheet instanceof Blob);
243
+ }
244
+ function normalizeCharacterSpec(input) {
245
+ let candidate = input;
246
+ if (typeof candidate === "string") candidate = parseJson(candidate, "character JSON");
247
+ if (!candidate || typeof candidate !== "object") throw new TypeError("Character data must be an object");
248
+ const record = candidate;
249
+ if (record.configuration) {
250
+ const configuration = typeof record.configuration === "string" ? parseJson(record.configuration, "configuration") : record.configuration;
251
+ if (typeof configuration === "object") candidate = { ...configuration, ...record };
252
+ }
253
+ const pack = candidate;
254
+ const id = typeof pack.id === "string" ? pack.id : typeof pack.metadata?.shimeji === "string" ? pack.metadata.shimeji : void 0;
255
+ if (!id) throw new TypeError("Character specification requires an id");
256
+ if (pack.actions === void 0 || pack.behaviors === void 0 || pack.sprites === void 0 || pack.spritesheet === void 0) throw new TypeError(`Character '${id}' is missing actions, behaviors, sprites, or spritesheet`);
257
+ const actions = typeof pack.actions === "string" && pack.actions.trimStart().startsWith("<") ? parseActionsXml(pack.actions) : parseJson(pack.actions, "actions");
258
+ const behaviors = typeof pack.behaviors === "string" && pack.behaviors.trimStart().startsWith("<") ? parseBehaviorsXml(pack.behaviors) : parseJson(pack.behaviors, "behaviors");
259
+ const sprites = parseJson(pack.sprites, "sprites");
260
+ const spec = {
261
+ id,
262
+ spritesheet: pack.spritesheet,
263
+ sprites,
264
+ actions,
265
+ behaviors,
266
+ ...typeof pack.name === "string" && { name: pack.name },
267
+ ...pack.metadata !== void 0 && { metadata: pack.metadata }
268
+ };
269
+ if (!isCharacterSpec(spec)) throw new TypeError(`Character '${id}' could not be normalized`);
270
+ return spec;
271
+ }
272
+ async function loadCharacter(source) {
273
+ if (typeof source !== "string" && !(source instanceof URL)) return normalizeCharacterSpec(source);
274
+ const url = source instanceof URL ? source : new URL(source, typeof document === "undefined" ? "http://localhost/" : document.baseURI);
275
+ const response = await fetch(url);
276
+ if (!response.ok) throw new Error(`Unable to load character '${url}': ${response.status} ${response.statusText}`);
277
+ const spec = normalizeCharacterSpec(await response.json());
278
+ if (typeof spec.spritesheet === "string" && !/^(?:data:|blob:)/.test(spec.spritesheet)) {
279
+ spec.spritesheet = new URL(spec.spritesheet, url).toString();
280
+ }
281
+ return spec;
282
+ }
283
+
284
+ // src/behavior.ts
285
+ var forbiddenProperties = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
286
+ var functions = {
287
+ abs: Math.abs,
288
+ acos: Math.acos,
289
+ acosh: Math.acosh,
290
+ asin: Math.asin,
291
+ asinh: Math.asinh,
292
+ atan: Math.atan,
293
+ atan2: Math.atan2,
294
+ atanh: Math.atanh,
295
+ cbrt: Math.cbrt,
296
+ ceil: Math.ceil,
297
+ cos: Math.cos,
298
+ cosh: Math.cosh,
299
+ exp: Math.exp,
300
+ expm1: Math.expm1,
301
+ floor: Math.floor,
302
+ hypot: Math.hypot,
303
+ log: Math.log,
304
+ log1p: Math.log1p,
305
+ log2: Math.log2,
306
+ log10: Math.log10,
307
+ max: Math.max,
308
+ min: Math.min,
309
+ pow: Math.pow,
310
+ random: (maximum = 1) => Math.random() * maximum,
311
+ round: Math.round,
312
+ sign: Math.sign,
313
+ sin: Math.sin,
314
+ sinh: Math.sinh,
315
+ sqrt: Math.sqrt,
316
+ tan: Math.tan,
317
+ tanh: Math.tanh,
318
+ trunc: Math.trunc
319
+ };
320
+ var constants = { E: Math.E, PI: Math.PI };
321
+ function normalizeExpression(source) {
322
+ return source.trim().replace(/^(?:#|\$)\{/, "").replace(/\}$/, "").replace(/Math\.(random|min|max|abs|floor|ceil|round|sqrt|pow|sin|cos|tan|asin|acos|atan|sinh|cosh|tanh|asinh|acosh|atanh|cbrt|log|log2|log10|exp|expm1|log1p|trunc|sign|hypot|atan2)/g, "$1").replace(/Math\.(PI|E)\b/g, "$1").replace(/Mascot\./gi, "mascot.").replace(/TargetX|目的地X/gi, "targetX").replace(/TargetY|目的地Y/gi, "targetY").replace(/FootX|足X/gi, "footX").replace(/FootY|足Y/gi, "footY").replace(/MaxCount/gi, "maxCount").replace(/Gap/gi, "gap").replace(/\band\b/gi, "&&").replace(/\bor\b/gi, "||").replace(/\bnot\b/gi, "!");
323
+ }
324
+ function tokenize(source) {
325
+ const tokens = [];
326
+ let index = 0;
327
+ while (index < source.length) {
328
+ const rest = source.slice(index);
329
+ const whitespace = /^\s+/.exec(rest);
330
+ if (whitespace) {
331
+ index += whitespace[0].length;
332
+ continue;
333
+ }
334
+ const number = /^(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/i.exec(rest);
335
+ if (number) {
336
+ tokens.push({ kind: "number", value: number[0] });
337
+ index += number[0].length;
338
+ continue;
339
+ }
340
+ const identifier = /^[A-Za-z_$\u0080-\uFFFF][\w$\u0080-\uFFFF]*/u.exec(rest);
341
+ if (identifier) {
342
+ tokens.push({ kind: "identifier", value: identifier[0] });
343
+ index += identifier[0].length;
344
+ continue;
345
+ }
346
+ const operator = /^(?:===|!==|==|!=|<=|>=|&&|\|\||[+\-*/%^<>!?:.,()])/.exec(rest);
347
+ if (!operator) throw new SyntaxError(`Unexpected token at ${index}`);
348
+ const value = operator[0];
349
+ tokens.push({ kind: value === "(" || value === ")" || value === "," || value === "." ? "punctuation" : "operator", value });
350
+ index += value.length;
351
+ }
352
+ tokens.push({ kind: "eof", value: "" });
353
+ return tokens;
354
+ }
355
+ var Parser = class {
356
+ constructor(tokens) {
357
+ this.tokens = tokens;
358
+ }
359
+ tokens;
360
+ index = 0;
361
+ parse() {
362
+ const node = this.parseConditional();
363
+ if (this.peek().kind !== "eof") throw new SyntaxError(`Unexpected '${this.peek().value}'`);
364
+ return node;
365
+ }
366
+ peek() {
367
+ return this.tokens[this.index] ?? { kind: "eof", value: "" };
368
+ }
369
+ take(value) {
370
+ const token = this.peek();
371
+ if (value !== void 0 && token.value !== value) throw new SyntaxError(`Expected '${value}'`);
372
+ this.index += 1;
373
+ return token;
374
+ }
375
+ match(...values) {
376
+ if (!values.includes(this.peek().value)) return false;
377
+ this.index += 1;
378
+ return true;
379
+ }
380
+ parseConditional() {
381
+ const test = this.parseOr();
382
+ if (!this.match("?")) return test;
383
+ const consequent = this.parseConditional();
384
+ this.take(":");
385
+ return { kind: "conditional", test, consequent, alternate: this.parseConditional() };
386
+ }
387
+ parseOr() {
388
+ return this.binary(() => this.parseAnd(), ["||"]);
389
+ }
390
+ parseAnd() {
391
+ return this.binary(() => this.parseEquality(), ["&&"]);
392
+ }
393
+ parseEquality() {
394
+ return this.binary(() => this.parseComparison(), ["==", "===", "!=", "!=="]);
395
+ }
396
+ parseComparison() {
397
+ return this.binary(() => this.parseAdditive(), ["<", "<=", ">", ">="]);
398
+ }
399
+ parseAdditive() {
400
+ return this.binary(() => this.parseMultiplicative(), ["+", "-"]);
401
+ }
402
+ parseMultiplicative() {
403
+ return this.binary(() => this.parsePower(), ["*", "/", "%"]);
404
+ }
405
+ parsePower() {
406
+ return this.binary(() => this.parseUnary(), ["^"]);
407
+ }
408
+ binary(next, operators) {
409
+ let left = next();
410
+ while (operators.includes(this.peek().value)) {
411
+ const operator = this.take().value;
412
+ left = { kind: "binary", operator, left, right: next() };
413
+ }
414
+ return left;
415
+ }
416
+ parseUnary() {
417
+ if (["!", "+", "-"].includes(this.peek().value)) {
418
+ return { kind: "unary", operator: this.take().value, argument: this.parseUnary() };
419
+ }
420
+ return this.parsePostfix();
421
+ }
422
+ parsePostfix() {
423
+ let node = this.parsePrimary();
424
+ for (; ; ) {
425
+ if (this.match(".")) {
426
+ const property = this.take();
427
+ if (property.kind !== "identifier" || forbiddenProperties.has(property.value)) throw new SyntaxError("Unsafe member access");
428
+ node = { kind: "member", object: node, property: property.value };
429
+ } else if (this.match("(")) {
430
+ const args = [];
431
+ if (!this.match(")")) {
432
+ do {
433
+ args.push(this.parseConditional());
434
+ } while (this.match(","));
435
+ this.take(")");
436
+ }
437
+ node = { kind: "call", callee: node, args };
438
+ } else return node;
439
+ }
440
+ }
441
+ parsePrimary() {
442
+ const token = this.take();
443
+ if (token.kind === "number") return { kind: "literal", value: Number(token.value) };
444
+ if (token.kind === "identifier") {
445
+ if (token.value === "true" || token.value === "false") return { kind: "literal", value: token.value === "true" };
446
+ return { kind: "identifier", name: token.value };
447
+ }
448
+ if (token.value === "(") {
449
+ const node = this.parseConditional();
450
+ this.take(")");
451
+ return node;
452
+ }
453
+ throw new SyntaxError(`Unexpected '${token.value}'`);
454
+ }
455
+ };
456
+ function resolveMember(node, scope) {
457
+ const owner = evaluateNode(node.object, scope);
458
+ if (typeof owner !== "object" && typeof owner !== "function" || owner === null) return { owner, value: void 0 };
459
+ if (forbiddenProperties.has(node.property)) return { owner, value: void 0 };
460
+ return { owner, value: owner[node.property] };
461
+ }
462
+ function evaluateNode(node, scope) {
463
+ switch (node.kind) {
464
+ case "literal":
465
+ return node.value;
466
+ case "identifier":
467
+ return Object.hasOwn(scope, node.name) ? scope[node.name] : functions[node.name] ?? constants[node.name];
468
+ case "member":
469
+ return resolveMember(node, scope).value;
470
+ case "call": {
471
+ const member = node.callee.kind === "member" ? resolveMember(node.callee, scope) : void 0;
472
+ const callable = member?.value ?? evaluateNode(node.callee, scope);
473
+ if (typeof callable !== "function") throw new TypeError("Expression value is not callable");
474
+ return callable.apply(member?.owner, node.args.map((argument) => evaluateNode(argument, scope)));
475
+ }
476
+ case "unary": {
477
+ const value = evaluateNode(node.argument, scope);
478
+ if (node.operator === "!") return !value;
479
+ if (node.operator === "+") return Number(value);
480
+ return -Number(value);
481
+ }
482
+ case "conditional":
483
+ return evaluateNode(node.test, scope) ? evaluateNode(node.consequent, scope) : evaluateNode(node.alternate, scope);
484
+ case "binary": {
485
+ if (node.operator === "&&") return Boolean(evaluateNode(node.left, scope)) && Boolean(evaluateNode(node.right, scope));
486
+ if (node.operator === "||") return Boolean(evaluateNode(node.left, scope)) || Boolean(evaluateNode(node.right, scope));
487
+ const left = evaluateNode(node.left, scope);
488
+ const right = evaluateNode(node.right, scope);
489
+ switch (node.operator) {
490
+ case "+":
491
+ return Number(left) + Number(right);
492
+ case "-":
493
+ return Number(left) - Number(right);
494
+ case "*":
495
+ return Number(left) * Number(right);
496
+ case "/":
497
+ return Number(left) / Number(right);
498
+ case "%":
499
+ return Number(left) % Number(right);
500
+ case "^":
501
+ return Math.pow(Number(left), Number(right));
502
+ case "==":
503
+ case "===":
504
+ return left === right;
505
+ case "!=":
506
+ case "!==":
507
+ return left !== right;
508
+ case "<":
509
+ return Number(left) < Number(right);
510
+ case "<=":
511
+ return Number(left) <= Number(right);
512
+ case ">":
513
+ return Number(left) > Number(right);
514
+ case ">=":
515
+ return Number(left) >= Number(right);
516
+ default:
517
+ return false;
518
+ }
519
+ }
520
+ }
521
+ }
522
+ var expressionCache = /* @__PURE__ */ new Map();
523
+ function evaluateExpression(expression, environment, fallback) {
524
+ if (expression === void 0) return fallback;
525
+ if (typeof expression !== "string") return expression;
526
+ try {
527
+ const normalized = normalizeExpression(expression);
528
+ let ast = expressionCache.get(normalized);
529
+ if (!ast) {
530
+ ast = new Parser(tokenize(normalized)).parse();
531
+ expressionCache.set(normalized, ast);
532
+ }
533
+ const result = evaluateNode(ast, environment);
534
+ if (typeof fallback === "boolean") return Boolean(result);
535
+ const numericResult = Number(result);
536
+ return Number.isNaN(numericResult) ? fallback : numericResult;
537
+ } catch {
538
+ return fallback;
539
+ }
540
+ }
541
+ function conditionsMatch(conditions, environment) {
542
+ return conditions.every((condition) => evaluateExpression(condition, environment, false));
543
+ }
544
+ function selectWeighted(items, weight, random = Math.random) {
545
+ const weighted = items.map((item) => ({ item, weight: Math.max(0, weight(item)) }));
546
+ const total = weighted.reduce((sum, entry) => sum + entry.weight, 0);
547
+ if (total <= 0) return void 0;
548
+ let cursor = random() * total;
549
+ for (const entry of weighted) {
550
+ cursor -= entry.weight;
551
+ if (cursor < 0) return entry.item;
552
+ }
553
+ return weighted.at(-1)?.item;
554
+ }
555
+ var BehaviorController = class {
556
+ /** Creates a behavior selector for a normalized character specification. */
557
+ constructor(spec, random = Math.random) {
558
+ this.spec = spec;
559
+ this.random = random;
560
+ }
561
+ spec;
562
+ random;
563
+ previous;
564
+ /** Selects an initial behavior, honoring an explicit requested name when possible. */
565
+ selectInitial(environment, requestedName) {
566
+ if (requestedName) {
567
+ const requested = this.spec.behaviors.find((behavior) => behavior.name === requestedName);
568
+ if (requested && conditionsMatch(requested.conditions, environment)) return this.previous = this.resolve(requested);
569
+ }
570
+ const fall = this.findFallBehavior();
571
+ if (fall && !this.isOnAnyBoundary(environment)) return this.previous = fall;
572
+ return this.previous = this.choose(this.spec.behaviors, environment);
573
+ }
574
+ /** Selects the weighted transition following the current behavior. */
575
+ selectNext(environment) {
576
+ const pool = this.previous?.nextBehaviors.length ? this.previous.nextBehaviors : this.spec.behaviors;
577
+ return this.previous = this.choose(pool, environment) ?? this.findFallBehavior();
578
+ }
579
+ /** Replaces selection history so an external interaction can force a behavior. */
580
+ force(name) {
581
+ const behavior = this.spec.behaviors.find((candidate) => candidate.name === name);
582
+ return this.previous = behavior ? this.resolve(behavior) : void 0;
583
+ }
584
+ choose(pool, environment) {
585
+ const applicable = pool.filter((behavior) => conditionsMatch(behavior.conditions, environment));
586
+ const chosen = selectWeighted(applicable, (behavior) => behavior.frequency, this.random);
587
+ return chosen ? this.resolve(chosen) : void 0;
588
+ }
589
+ resolve(behavior) {
590
+ if (behavior.type !== "Reference") return behavior;
591
+ const target = this.spec.behaviors.find((candidate) => candidate.type === "Behavior" && candidate.name === behavior.name);
592
+ return target ? { ...target, ...behavior, type: "Behavior", nextBehaviors: target.nextBehaviors } : { ...behavior, type: "Behavior" };
593
+ }
594
+ findFallBehavior() {
595
+ return this.spec.behaviors.find((behavior) => behavior.name === "Fall" || behavior.name === "\u843D\u4E0B\u3059\u308B");
596
+ }
597
+ isOnAnyBoundary(environment) {
598
+ const anchor = environment.mascot.anchor;
599
+ const area = environment.mascot.environment.workArea;
600
+ return area.topBorder.isOn(anchor) || area.leftBorder.isOn(anchor) || area.rightBorder.isOn(anchor) || area.bottomBorder.isOn(anchor);
601
+ }
602
+ };
603
+
604
+ // src/physics.ts
605
+ function clamp(value, minimum, maximum) {
606
+ return Math.min(Math.max(value, minimum), maximum);
607
+ }
608
+ function isOnTop(point, rectangle, tolerance = 1) {
609
+ return point.x >= rectangle.x - tolerance && point.x <= rectangle.x + rectangle.width + tolerance && Math.abs(point.y - rectangle.y) <= tolerance;
610
+ }
611
+ function isOnBottom(point, rectangle, tolerance = 1) {
612
+ return point.x >= rectangle.x - tolerance && point.x <= rectangle.x + rectangle.width + tolerance && Math.abs(point.y - rectangle.y - rectangle.height) <= tolerance;
613
+ }
614
+ function isOnLeft(point, rectangle, tolerance = 1) {
615
+ return point.y >= rectangle.y - tolerance && point.y <= rectangle.y + rectangle.height + tolerance && Math.abs(point.x - rectangle.x) <= tolerance;
616
+ }
617
+ function isOnRight(point, rectangle, tolerance = 1) {
618
+ return point.y >= rectangle.y - tolerance && point.y <= rectangle.y + rectangle.height + tolerance && Math.abs(point.x - rectangle.x - rectangle.width) <= tolerance;
619
+ }
620
+ function isOnBorder(state, bounds, border) {
621
+ if (!border) return true;
622
+ if (border === "Floor") return isOnBottom(state, bounds);
623
+ if (border === "Ceiling") return isOnTop(state, bounds);
624
+ return isOnLeft(state, bounds) || isOnRight(state, bounds);
625
+ }
626
+ function applyGravity(state, bounds, frameScale, gravity, resistanceX = 0.05, resistanceY = 0.01) {
627
+ state.x = clamp(state.x + state.vx * frameScale, bounds.x, bounds.x + bounds.width);
628
+ state.y = clamp(state.y + state.vy * frameScale, bounds.y, bounds.y + bounds.height);
629
+ state.vx *= Math.max(0, 1 - resistanceX * frameScale);
630
+ state.vy = state.vy * Math.max(0, 1 - resistanceY * frameScale) + gravity * frameScale;
631
+ if (state.y >= bounds.y + bounds.height) {
632
+ state.y = bounds.y + bounds.height;
633
+ state.vy = 0;
634
+ return true;
635
+ }
636
+ if (state.x <= bounds.x || state.x >= bounds.x + bounds.width) {
637
+ state.vx = 0;
638
+ return true;
639
+ }
640
+ return false;
641
+ }
642
+ function moveToward(state, target, speed, frameScale) {
643
+ const dx = target.x - state.x;
644
+ const dy = target.y - state.y;
645
+ const distance = Math.hypot(dx, dy);
646
+ if (distance <= Math.max(1e-3, speed * frameScale)) {
647
+ state.x = target.x;
648
+ state.y = target.y;
649
+ return true;
650
+ }
651
+ state.x += dx / distance * speed * frameScale;
652
+ state.y += dy / distance * speed * frameScale;
653
+ return false;
654
+ }
655
+
656
+ // src/action.ts
657
+ function numeric(value, environment) {
658
+ return value === void 0 ? void 0 : evaluateExpression(value, environment, 0);
659
+ }
660
+ function evaluateAction(definition, environment) {
661
+ const gap = numeric(definition.gap, environment) ?? 0;
662
+ const scopedEnvironment = { ...environment, gap };
663
+ const targetX = numeric(definition.targetX, scopedEnvironment);
664
+ const initialVx = numeric(definition.initialVx, scopedEnvironment);
665
+ let lookRight = environment.mascot.lookRight;
666
+ if (definition.borderType === "Wall") {
667
+ lookRight = environment.mascot.environment.workArea.rightBorder.isOn(environment.mascot.anchor);
668
+ } else if (definition.type === "Move" || definition.embedType === "Jump" || definition.embedType === "WalkWithIE") {
669
+ if (targetX !== void 0) lookRight = targetX > environment.mascot.anchor.x;
670
+ } else if (definition.embedType === "Fall" || definition.embedType === "FallWithIE") {
671
+ if (initialVx !== void 0 && initialVx !== 0) lookRight = initialVx > 0;
672
+ } else if (definition.embedType === "Look") {
673
+ lookRight = definition.lookRight === void 0 ? !environment.mascot.lookRight : typeof definition.lookRight === "boolean" ? definition.lookRight : evaluateExpression(definition.lookRight, scopedEnvironment, environment.mascot.lookRight);
674
+ }
675
+ const duration = numeric(definition.duration, scopedEnvironment);
676
+ const targetY = numeric(definition.targetY, scopedEnvironment);
677
+ const velocity = numeric(definition.velocity, scopedEnvironment);
678
+ const x = numeric(definition.x, scopedEnvironment);
679
+ const y = numeric(definition.y, scopedEnvironment);
680
+ const initialVy = numeric(definition.initialVy, scopedEnvironment);
681
+ const resistanceX = numeric(definition.resistanceX, scopedEnvironment);
682
+ const resistanceY = numeric(definition.resistanceY, scopedEnvironment);
683
+ const gravity = numeric(definition.gravity, scopedEnvironment);
684
+ const bornX = numeric(definition.bornX, scopedEnvironment);
685
+ const bornY = numeric(definition.bornY, scopedEnvironment);
686
+ return {
687
+ definition,
688
+ lookRight,
689
+ ...duration !== void 0 && { duration },
690
+ ...targetX !== void 0 && { targetX },
691
+ ...targetY !== void 0 && { targetY },
692
+ ...velocity !== void 0 && { velocity },
693
+ ...x !== void 0 && { x },
694
+ ...y !== void 0 && { y },
695
+ ...initialVx !== void 0 && { initialVx },
696
+ ...initialVy !== void 0 && { initialVy },
697
+ ...resistanceX !== void 0 && { resistanceX },
698
+ ...resistanceY !== void 0 && { resistanceY },
699
+ ...gravity !== void 0 && { gravity },
700
+ ...bornX !== void 0 && { bornX },
701
+ ...bornY !== void 0 && { bornY }
702
+ };
703
+ }
704
+ var SequenceRuntime = class {
705
+ constructor(definitions, factory, loop) {
706
+ this.definitions = definitions;
707
+ this.factory = factory;
708
+ this.loop = loop;
709
+ }
710
+ definitions;
711
+ factory;
712
+ loop;
713
+ index = 0;
714
+ child;
715
+ tick(deltaMs, environment, bounds) {
716
+ for (let guard = 0; guard < 32; guard += 1) {
717
+ const definition = this.definitions[this.index];
718
+ if (!definition) {
719
+ if (!this.loop || this.definitions.length === 0) return true;
720
+ this.index = 0;
721
+ continue;
722
+ }
723
+ this.child ??= this.factory(definition, environment);
724
+ if (!this.child.tick(deltaMs, environment, bounds)) return false;
725
+ this.child = void 0;
726
+ this.index += 1;
727
+ deltaMs = 0;
728
+ }
729
+ return false;
730
+ }
731
+ };
732
+ var CompleteRuntime = class {
733
+ tick() {
734
+ return true;
735
+ }
736
+ };
737
+ var LeafRuntime = class {
738
+ constructor(action, state, options, callbacks, environment) {
739
+ this.action = action;
740
+ this.state = state;
741
+ this.options = options;
742
+ this.callbacks = callbacks;
743
+ const animationEnvironment = { ...environment, ...action.targetX !== void 0 && { targetX: action.targetX }, ...action.targetY !== void 0 && { targetY: action.targetY } };
744
+ this.animation = action.definition.animations?.find((animation) => !animation.condition || evaluateExpression(animation.condition, animationEnvironment, false)) ?? action.definition.animations?.find((animation) => !animation.condition) ?? action.definition.animations?.[0];
745
+ this.poseDuration = this.animation?.poses.reduce((sum, pose) => sum + Math.max(0, pose.duration), 0) ?? 0;
746
+ }
747
+ action;
748
+ state;
749
+ options;
750
+ callbacks;
751
+ elapsedMs = 0;
752
+ started = false;
753
+ spawned = false;
754
+ animation;
755
+ poseDuration;
756
+ tick(deltaMs, environment, bounds) {
757
+ const frameScale = deltaMs / this.options.frameDuration;
758
+ if (!this.started) {
759
+ this.started = true;
760
+ this.state.lookRight = this.action.lookRight;
761
+ this.initialize();
762
+ }
763
+ this.elapsedMs += deltaMs;
764
+ const pose = this.currentPose();
765
+ if (pose) this.applyPose(pose, frameScale, bounds);
766
+ const { definition } = this.action;
767
+ if (definition.type === "Embedded") return this.tickEmbedded(frameScale, environment, bounds);
768
+ if (definition.type === "Move" && (this.action.targetX !== void 0 || this.action.targetY !== void 0)) {
769
+ const reachedX = this.action.targetX === void 0 || Math.abs(this.state.x - this.action.targetX) < 0.5;
770
+ const reachedY = this.action.targetY === void 0 || Math.abs(this.state.y - this.action.targetY) < 0.5;
771
+ return reachedX && reachedY;
772
+ }
773
+ return this.elapsedMs >= this.durationMs();
774
+ }
775
+ initialize() {
776
+ const type = this.action.definition.embedType;
777
+ if (type === "Fall" || type === "FallWithIE") {
778
+ this.state.vx = this.action.initialVx ?? this.state.vx;
779
+ this.state.vy = this.action.initialVy ?? this.state.vy;
780
+ }
781
+ if (type === "Offset") {
782
+ this.state.x += this.action.x ?? 0;
783
+ this.state.y += this.action.y ?? 0;
784
+ }
785
+ if (type === "Reboot") {
786
+ this.state.vx = 0;
787
+ this.state.vy = 0;
788
+ }
789
+ }
790
+ tickEmbedded(frameScale, environment, bounds) {
791
+ switch (this.action.definition.embedType) {
792
+ case "Fall":
793
+ case "FallWithIE":
794
+ case "Thrown":
795
+ return applyGravity(this.state, bounds, frameScale, this.action.gravity ?? this.options.gravity, this.action.resistanceX, this.action.resistanceY);
796
+ case "Jump": {
797
+ const target = { x: this.action.targetX ?? this.state.x, y: this.action.targetY ?? this.state.y };
798
+ return moveToward(this.state, target, this.action.velocity ?? 20, frameScale);
799
+ }
800
+ case "Breed":
801
+ if (!this.spawned && this.elapsedMs >= this.durationMs()) {
802
+ this.spawned = true;
803
+ const child = { x: this.state.x + (this.action.bornX ?? 0), y: this.state.y + (this.action.bornY ?? 0) };
804
+ this.callbacks.spawn(this.action.definition.bornBehavior ? { ...child, behaviorName: this.action.definition.bornBehavior } : child);
805
+ return true;
806
+ }
807
+ return false;
808
+ case "Exit":
809
+ this.callbacks.remove();
810
+ return true;
811
+ case "Reboot":
812
+ this.state.x = bounds.x + 128 + Math.max(0, bounds.width - 256) * Math.random();
813
+ this.state.y = bounds.y + 128 + Math.max(0, bounds.height - 256) * Math.random();
814
+ return true;
815
+ case "Offset":
816
+ case "Look":
817
+ return true;
818
+ case "Dragged":
819
+ return false;
820
+ default:
821
+ return this.elapsedMs >= this.durationMs();
822
+ }
823
+ }
824
+ currentPose() {
825
+ if (!this.animation?.poses.length || this.poseDuration <= 0) return void 0;
826
+ let cursor = this.elapsedMs / this.options.frameDuration % this.poseDuration;
827
+ for (const pose of this.animation.poses) {
828
+ cursor -= pose.duration;
829
+ if (cursor < 0) return pose;
830
+ }
831
+ return this.animation.poses.at(-1);
832
+ }
833
+ applyPose(pose, frameScale, bounds) {
834
+ this.state.sprite = pose.sprite;
835
+ this.state.anchorX = pose.anchor.x;
836
+ this.state.anchorY = pose.anchor.y;
837
+ const direction = this.state.lookRight ? -1 : 1;
838
+ this.state.x = clamp(this.state.x + pose.velocity.x * direction * frameScale, bounds.x, bounds.x + bounds.width);
839
+ this.state.y = clamp(this.state.y + pose.velocity.y * frameScale, bounds.y, bounds.y + bounds.height);
840
+ }
841
+ durationMs() {
842
+ const frames = this.action.duration ?? this.poseDuration;
843
+ return Math.max(this.options.frameDuration, frames * this.options.frameDuration);
844
+ }
845
+ };
846
+ var ActionExecutor = class {
847
+ /** Creates an executor bound to one mascot's mutable internal state. */
848
+ constructor(spec, state, options, callbacks) {
849
+ this.spec = spec;
850
+ this.state = state;
851
+ this.options = options;
852
+ this.callbacks = callbacks;
853
+ }
854
+ spec;
855
+ state;
856
+ options;
857
+ callbacks;
858
+ runtime;
859
+ /** Starts the action whose name matches a selected behavior. */
860
+ start(actionName, environment) {
861
+ const definition = this.spec.actions.find((action) => action.name === actionName);
862
+ this.runtime = definition ? this.createRuntime(definition, environment, /* @__PURE__ */ new Set()) : void 0;
863
+ return this.runtime !== void 0;
864
+ }
865
+ /** Advances the current action and returns true when it has completed. */
866
+ tick(deltaMs, environment, bounds) {
867
+ return this.runtime?.tick(deltaMs, environment, bounds) ?? true;
868
+ }
869
+ /** Cancels the current action tree. */
870
+ cancel() {
871
+ this.runtime = void 0;
872
+ }
873
+ createRuntime(definition, environment, references) {
874
+ if (definition.condition && !evaluateExpression(definition.condition, environment, false)) return new CompleteRuntime();
875
+ const bounds = environment.mascot.environment.workArea;
876
+ if (!isOnBorder(this.state, bounds, definition.borderType)) return new CompleteRuntime();
877
+ if (definition.type === "Reference") {
878
+ if (!definition.name || references.has(definition.name)) return new CompleteRuntime();
879
+ const referenced = this.spec.actions.find((action) => action.name === definition.name);
880
+ if (!referenced) return new CompleteRuntime();
881
+ const nextReferences = new Set(references).add(definition.name);
882
+ return this.createRuntime({ ...referenced, ...definition, type: referenced.type }, environment, nextReferences);
883
+ }
884
+ if (definition.type === "Sequence") {
885
+ return new SequenceRuntime(definition.actions ?? [], (child, nextEnvironment) => this.createRuntime(child, nextEnvironment, new Set(references)), definition.loop === true);
886
+ }
887
+ if (definition.type === "Select") {
888
+ const child = definition.actions?.find((candidate) => !candidate.condition || evaluateExpression(candidate.condition, environment, false));
889
+ return child ? this.createRuntime(child, environment, new Set(references)) : new CompleteRuntime();
890
+ }
891
+ return new LeafRuntime(evaluateAction(definition, environment), this.state, this.options, this.callbacks, environment);
892
+ }
893
+ };
894
+
895
+ // src/mascot.ts
896
+ function edge(predicate) {
897
+ return { isOn: predicate };
898
+ }
899
+ function environmentRectangle(bounds) {
900
+ const left = bounds.x;
901
+ const right = bounds.x + bounds.width;
902
+ const top = bounds.y;
903
+ const bottom = bounds.y + bounds.height;
904
+ return {
905
+ ...bounds,
906
+ left,
907
+ right,
908
+ top,
909
+ bottom,
910
+ topBorder: edge((point) => Math.abs(point.y - top) <= 1),
911
+ leftBorder: edge((point) => Math.abs(point.x - left) <= 1),
912
+ rightBorder: edge((point) => Math.abs(point.x - right) <= 1),
913
+ bottomBorder: edge((point) => Math.abs(point.y - bottom) <= 1)
914
+ };
915
+ }
916
+ var Mascot = class {
917
+ /** Creates a mascot and immediately installs its pointer handlers. */
918
+ constructor(id, spec, dom, options, spawnOptions, callbacks) {
919
+ this.id = id;
920
+ this.spec = spec;
921
+ this.dom = dom;
922
+ this.callbacks = callbacks;
923
+ const initialPose = spec.actions.flatMap((action) => action.animations ?? []).flatMap((animation) => animation.poses)[0];
924
+ this.state = {
925
+ id,
926
+ characterId: spec.id,
927
+ x: spawnOptions.x ?? 0,
928
+ y: spawnOptions.y ?? 0,
929
+ vx: spawnOptions.vx ?? 0,
930
+ vy: spawnOptions.vy ?? 0,
931
+ sprite: initialPose?.sprite ?? Object.keys(spec.sprites)[0] ?? "",
932
+ anchorX: initialPose?.anchor.x ?? 64,
933
+ anchorY: initialPose?.anchor.y ?? 128,
934
+ lookRight: spawnOptions.lookRight ?? false,
935
+ behaviorName: spawnOptions.behaviorName ?? "Fall",
936
+ dragging: false
937
+ };
938
+ this.domHandle = dom.createMascot(spec, id, options.mascotClassName || void 0);
939
+ this.behavior = new BehaviorController(spec, options.random);
940
+ this.actions = new ActionExecutor(spec, this.state, options, {
941
+ spawn: (position) => this.callbacks.spawn(this.spec.id, position),
942
+ remove: () => this.callbacks.remove(this.id)
943
+ });
944
+ this.installPointerHandlers();
945
+ }
946
+ id;
947
+ spec;
948
+ dom;
949
+ callbacks;
950
+ /** Mutable internal state; callers should consume snapshots from the engine. */
951
+ state;
952
+ domHandle;
953
+ behavior;
954
+ actions;
955
+ disposers = [];
956
+ currentBehavior;
957
+ destroyed = false;
958
+ dragOffset = { x: 0, y: 0 };
959
+ pointerId;
960
+ pointerDown;
961
+ lastPointer;
962
+ /** Advances behavior, animation, physics, and rendering by one clock tick. */
963
+ tick(deltaMs, bounds) {
964
+ if (this.destroyed) return;
965
+ const environment = this.createEnvironment(bounds);
966
+ if (this.state.dragging) {
967
+ this.dom.render(this.domHandle, this.spec, this.state);
968
+ return;
969
+ }
970
+ try {
971
+ for (let guard = 0; guard < 8; guard += 1) {
972
+ if (!this.currentBehavior) {
973
+ this.currentBehavior = this.behavior.selectInitial(environment, this.state.behaviorName);
974
+ let started = this.startBehavior(environment);
975
+ if (!started) {
976
+ this.currentBehavior = this.findFallBehavior();
977
+ started = this.startBehavior(environment);
978
+ }
979
+ if (!started) break;
980
+ }
981
+ if (!this.actions.tick(deltaMs, environment, bounds)) break;
982
+ if (this.destroyed) return;
983
+ this.currentBehavior = this.behavior.selectNext(this.createEnvironment(bounds));
984
+ if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds))) {
985
+ this.currentBehavior = this.findFallBehavior();
986
+ if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds))) break;
987
+ }
988
+ deltaMs = 0;
989
+ }
990
+ } catch (error) {
991
+ this.callbacks.error(error instanceof Error ? error : new Error(String(error)));
992
+ this.currentBehavior = void 0;
993
+ this.actions.cancel();
994
+ }
995
+ this.dom.render(this.domHandle, this.spec, this.state);
996
+ }
997
+ /** Returns a detached snapshot safe for application code to retain. */
998
+ snapshot() {
999
+ return { ...this.state };
1000
+ }
1001
+ /** Removes listeners, DOM nodes, and object URLs owned by this mascot. */
1002
+ destroy() {
1003
+ if (this.destroyed) return;
1004
+ this.destroyed = true;
1005
+ this.actions.cancel();
1006
+ for (const dispose of this.disposers.splice(0)) dispose();
1007
+ this.dom.removeMascot(this.domHandle);
1008
+ }
1009
+ startBehavior(environment) {
1010
+ if (!this.currentBehavior) return false;
1011
+ this.state.behaviorName = this.currentBehavior.name;
1012
+ return this.actions.start(this.currentBehavior.name, environment);
1013
+ }
1014
+ findFallBehavior() {
1015
+ return this.behavior.force("Fall") ?? this.behavior.force("\u843D\u4E0B\u3059\u308B");
1016
+ }
1017
+ createEnvironment(bounds) {
1018
+ const workArea = environmentRectangle(bounds);
1019
+ const inactive = environmentRectangle({ x: -100, y: -100, width: 0, height: 0 });
1020
+ return {
1021
+ gap: 0,
1022
+ maxCount: 999,
1023
+ mascot: {
1024
+ totalCount: this.callbacks.count(),
1025
+ anchor: { x: this.state.x, y: this.state.y },
1026
+ lookRight: this.state.lookRight,
1027
+ environment: {
1028
+ cursor: this.callbacks.pointer(),
1029
+ screen: { width: window.innerWidth, height: window.innerHeight },
1030
+ workArea,
1031
+ floor: workArea.bottomBorder,
1032
+ ceiling: workArea.topBorder,
1033
+ activeIE: { ...inactive, visible: false }
1034
+ }
1035
+ }
1036
+ };
1037
+ }
1038
+ installPointerHandlers() {
1039
+ const element = this.domHandle.element;
1040
+ const listen = (target, type, listener) => {
1041
+ target.addEventListener(type, listener);
1042
+ this.disposers.push(() => target.removeEventListener(type, listener));
1043
+ };
1044
+ listen(element, "pointerdown", (event) => {
1045
+ const pointerEvent = event;
1046
+ if (pointerEvent.button !== 0) return;
1047
+ event.preventDefault();
1048
+ const bounds = this.dom.workArea.getBoundingClientRect();
1049
+ const point = { x: pointerEvent.clientX - bounds.left, y: pointerEvent.clientY - bounds.top };
1050
+ this.pointerId = pointerEvent.pointerId;
1051
+ this.pointerDown = point;
1052
+ this.lastPointer = point;
1053
+ this.dragOffset = { x: this.state.x - point.x, y: this.state.y - point.y };
1054
+ this.state.dragging = true;
1055
+ this.actions.cancel();
1056
+ this.currentBehavior = this.behavior.force("Dragged") ?? this.behavior.force("\u30C9\u30E9\u30C3\u30B0\u3055\u308C\u308B");
1057
+ if (this.currentBehavior) this.state.behaviorName = this.currentBehavior.name;
1058
+ element.setPointerCapture?.(pointerEvent.pointerId);
1059
+ });
1060
+ listen(document, "pointermove", (event) => {
1061
+ const pointerEvent = event;
1062
+ if (!this.state.dragging || pointerEvent.pointerId !== this.pointerId) return;
1063
+ const bounds = this.dom.workArea.getBoundingClientRect();
1064
+ const point = { x: pointerEvent.clientX - bounds.left, y: pointerEvent.clientY - bounds.top };
1065
+ const previous = this.lastPointer ?? point;
1066
+ this.state.vx = (point.x - previous.x) * 0.8;
1067
+ this.state.vy = (point.y - previous.y) * 0.8;
1068
+ this.state.x = point.x + this.dragOffset.x;
1069
+ this.state.y = point.y + this.dragOffset.y;
1070
+ this.lastPointer = point;
1071
+ });
1072
+ listen(document, "pointerup", (event) => {
1073
+ const pointerEvent = event;
1074
+ if (!this.state.dragging || pointerEvent.pointerId !== this.pointerId) return;
1075
+ this.state.dragging = false;
1076
+ const moved = this.pointerDown ? Math.hypot(this.lastPointer.x - this.pointerDown.x, this.lastPointer.y - this.pointerDown.y) : 0;
1077
+ this.pointerId = void 0;
1078
+ this.currentBehavior = this.behavior.force("Thrown") ?? this.behavior.force("\u6295\u3052\u3089\u308C\u308B") ?? this.findFallBehavior();
1079
+ if (this.currentBehavior) {
1080
+ this.state.behaviorName = this.currentBehavior.name;
1081
+ this.actions.start(this.currentBehavior.name, this.createEnvironment(this.dom.getBounds()));
1082
+ }
1083
+ if (moved < 4) this.callbacks.click(this.snapshot());
1084
+ });
1085
+ }
1086
+ };
1087
+
1088
+ // src/sprite.ts
1089
+ function dataUriToBlob(source) {
1090
+ const match = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(source);
1091
+ if (!match) throw new TypeError("Invalid image data URI");
1092
+ const mimeType = match[1] ?? "application/octet-stream";
1093
+ const encoded = match[3] ?? "";
1094
+ const binary = match[2] ? atob(encoded) : decodeURIComponent(encoded);
1095
+ const bytes = new Uint8Array(binary.length);
1096
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
1097
+ return new Blob([bytes], { type: mimeType });
1098
+ }
1099
+ var SpriteManager = class {
1100
+ leases = /* @__PURE__ */ new Set();
1101
+ /** Creates a separately releasable spritesheet lease for a mascot. */
1102
+ acquire(source) {
1103
+ let url = typeof source === "string" ? source : "";
1104
+ let owned = false;
1105
+ if (typeof URL.createObjectURL === "function" && (typeof source !== "string" || source.startsWith("data:image"))) {
1106
+ try {
1107
+ url = URL.createObjectURL(typeof source === "string" ? dataUriToBlob(source) : source);
1108
+ owned = true;
1109
+ } catch {
1110
+ if (typeof source !== "string") throw new Error("The current environment cannot create a URL for the spritesheet Blob");
1111
+ }
1112
+ }
1113
+ let released = false;
1114
+ const lease = {
1115
+ url,
1116
+ release: () => {
1117
+ if (released) return;
1118
+ released = true;
1119
+ this.leases.delete(lease);
1120
+ if (owned) URL.revokeObjectURL(url);
1121
+ }
1122
+ };
1123
+ this.leases.add(lease);
1124
+ return lease;
1125
+ }
1126
+ /** Resolves a sprite key using a lease and the character's sprite map. */
1127
+ resolve(spec, lease, spriteName) {
1128
+ const key = Object.keys(spec.sprites).find((candidate) => candidate.toLowerCase() === spriteName.toLowerCase());
1129
+ const sprite = key ? spec.sprites[key] : void 0;
1130
+ if (typeof sprite === "string") return { url: sprite };
1131
+ if (sprite && "url" in sprite && typeof sprite.url === "string" && !("x" in sprite)) return { url: sprite.url };
1132
+ if (sprite && "x" in sprite) return { url: sprite.url ?? lease.url, rectangle: sprite };
1133
+ if (/^(?:data:|blob:|https?:|\/)/.test(spriteName)) return { url: spriteName };
1134
+ return void 0;
1135
+ }
1136
+ /** Revokes every object URL that has not already been released. */
1137
+ destroy() {
1138
+ for (const lease of [...this.leases]) lease.release();
1139
+ }
1140
+ };
1141
+ function isIndividualSprite(sprite) {
1142
+ return typeof sprite === "object" && "url" in sprite && !("x" in sprite);
1143
+ }
1144
+
1145
+ // src/engine.ts
1146
+ var EventEmitter = class {
1147
+ listeners = /* @__PURE__ */ new Map();
1148
+ on(event, listener) {
1149
+ const listeners = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
1150
+ listeners.add(listener);
1151
+ this.listeners.set(event, listeners);
1152
+ return () => {
1153
+ listeners.delete(listener);
1154
+ };
1155
+ }
1156
+ emit(event, payload) {
1157
+ for (const listener of [...this.listeners.get(event) ?? []]) listener(payload);
1158
+ }
1159
+ clear() {
1160
+ this.listeners.clear();
1161
+ }
1162
+ };
1163
+ var defaults = {
1164
+ frameDuration: 40,
1165
+ gravity: 2,
1166
+ maxDeltaTime: 100,
1167
+ workAreaClassName: "",
1168
+ mascotClassName: ""
1169
+ };
1170
+ var ShimejiEngine = class {
1171
+ /** Creates and initializes an engine inside a host DOM element. */
1172
+ constructor(container, options = {}) {
1173
+ this.container = container;
1174
+ if (!container) throw new TypeError("ShimejiEngine requires a container element");
1175
+ this.options = { ...defaults, ...options, random: options.random };
1176
+ this.originalContainerPosition = container.style.position;
1177
+ this.adjustedContainerPosition = getComputedStyle(container).position === "static";
1178
+ if (this.adjustedContainerPosition) container.style.position = "relative";
1179
+ this.dom = new DomManager(container, this.sprites, this.options.workAreaClassName || void 0);
1180
+ this.initialize();
1181
+ }
1182
+ container;
1183
+ specs = /* @__PURE__ */ new Map();
1184
+ mascots = /* @__PURE__ */ new Map();
1185
+ sprites = new SpriteManager();
1186
+ dom;
1187
+ events = new EventEmitter();
1188
+ disposers = [];
1189
+ intervals = /* @__PURE__ */ new Set();
1190
+ options;
1191
+ originalContainerPosition;
1192
+ adjustedContainerPosition;
1193
+ pointer = { x: 0, y: 0, dx: 0, dy: 0 };
1194
+ animationFrame;
1195
+ lastFrameTime;
1196
+ nextMascotId = 1;
1197
+ destroyed = false;
1198
+ initialized = false;
1199
+ /** Starts the clock and global listeners. Calling this method more than once is harmless. */
1200
+ initialize() {
1201
+ this.assertAlive();
1202
+ if (this.initialized) return;
1203
+ this.initialized = true;
1204
+ this.listen(document, "pointermove", (event) => {
1205
+ const pointerEvent = event;
1206
+ const rectangle = this.dom.workArea.getBoundingClientRect();
1207
+ const x = pointerEvent.clientX - rectangle.left;
1208
+ const y = pointerEvent.clientY - rectangle.top;
1209
+ this.pointer = { x, y, dx: x - this.pointer.x, dy: y - this.pointer.y };
1210
+ });
1211
+ this.listen(window, "resize", () => this.renderAll());
1212
+ const maintenance = window.setInterval(() => this.dom.ensureMounted(), 2e3);
1213
+ this.intervals.add(maintenance);
1214
+ this.animationFrame = requestAnimationFrame(this.onAnimationFrame);
1215
+ }
1216
+ /** Registers or replaces a parsed or legacy character specification. */
1217
+ registerCharacter(spec) {
1218
+ this.assertAlive();
1219
+ const normalized = normalizeCharacterSpec(spec);
1220
+ this.specs.set(normalized.id, normalized);
1221
+ return normalized.id;
1222
+ }
1223
+ /** Unregisters a character and optionally removes all of its live mascots. */
1224
+ unregisterCharacter(characterId, removeMascots = true) {
1225
+ this.assertAlive();
1226
+ if (removeMascots) {
1227
+ for (const state of this.getState()) if (state.characterId === characterId) this.remove(state.id);
1228
+ }
1229
+ return this.specs.delete(characterId);
1230
+ }
1231
+ /** Returns identifiers for all currently registered characters. */
1232
+ getCharacterIds() {
1233
+ return [...this.specs.keys()];
1234
+ }
1235
+ /** Creates a mascot from a registered character and returns its instance id. */
1236
+ spawn(characterId, position = {}) {
1237
+ this.assertAlive();
1238
+ const spec = this.specs.get(characterId);
1239
+ if (!spec) throw new Error(`Character '${characterId}' is not registered`);
1240
+ const bounds = this.dom.getBounds();
1241
+ const random = this.options.random ?? Math.random;
1242
+ const spawnOptions = {
1243
+ ...position,
1244
+ x: position.x ?? bounds.x + random() * bounds.width,
1245
+ y: position.y ?? bounds.y + random() * bounds.height
1246
+ };
1247
+ const id = `shimeji-${this.nextMascotId++}`;
1248
+ const mascot = new Mascot(id, spec, this.dom, this.options, spawnOptions, {
1249
+ pointer: () => ({ ...this.pointer }),
1250
+ count: () => this.mascots.size,
1251
+ spawn: (nextCharacterId, nextPosition) => {
1252
+ if (!this.destroyed) this.spawn(nextCharacterId, nextPosition);
1253
+ },
1254
+ remove: (mascotId) => {
1255
+ if (!this.destroyed) this.remove(mascotId);
1256
+ },
1257
+ click: (state2) => this.events.emit("click", state2),
1258
+ error: (error) => this.events.emit("error", error)
1259
+ });
1260
+ this.mascots.set(id, mascot);
1261
+ mascot.tick(0, bounds);
1262
+ const state = mascot.snapshot();
1263
+ this.events.emit("spawn", state);
1264
+ this.emitState();
1265
+ return id;
1266
+ }
1267
+ /** Removes one mascot and all resources associated with it. */
1268
+ remove(mascotId) {
1269
+ this.assertAlive();
1270
+ const mascot = this.mascots.get(mascotId);
1271
+ if (!mascot) return false;
1272
+ const state = mascot.snapshot();
1273
+ this.mascots.delete(mascotId);
1274
+ mascot.destroy();
1275
+ this.events.emit("remove", state);
1276
+ this.emitState();
1277
+ return true;
1278
+ }
1279
+ /** Removes every live mascot while leaving registered character specs available. */
1280
+ removeAll() {
1281
+ this.assertAlive();
1282
+ for (const id of [...this.mascots.keys()]) this.remove(id);
1283
+ }
1284
+ /** Returns detached snapshots of every live mascot. */
1285
+ getState() {
1286
+ return [...this.mascots.values()].map((mascot) => mascot.snapshot());
1287
+ }
1288
+ /** Subscribes to a typed engine event and returns an unsubscribe function. */
1289
+ on(event, listener) {
1290
+ this.assertAlive();
1291
+ return this.events.on(event, listener);
1292
+ }
1293
+ /** Stops animation and timers, removes listeners and DOM, and revokes all object URLs. */
1294
+ destroy() {
1295
+ if (this.destroyed) return;
1296
+ for (const mascot of this.mascots.values()) mascot.destroy();
1297
+ this.mascots.clear();
1298
+ if (this.animationFrame !== void 0) cancelAnimationFrame(this.animationFrame);
1299
+ this.animationFrame = void 0;
1300
+ for (const interval of this.intervals) window.clearInterval(interval);
1301
+ this.intervals.clear();
1302
+ for (const dispose of this.disposers.splice(0)) dispose();
1303
+ this.dom.destroy();
1304
+ this.sprites.destroy();
1305
+ this.specs.clear();
1306
+ this.events.clear();
1307
+ if (this.adjustedContainerPosition && this.container.style.position === "relative") this.container.style.position = this.originalContainerPosition;
1308
+ this.destroyed = true;
1309
+ this.initialized = false;
1310
+ }
1311
+ /** Returns whether this engine has completed permanent teardown. */
1312
+ isDestroyed() {
1313
+ return this.destroyed;
1314
+ }
1315
+ onAnimationFrame = (timestamp) => {
1316
+ if (this.destroyed) return;
1317
+ const rawDelta = this.lastFrameTime === void 0 ? this.options.frameDuration : timestamp - this.lastFrameTime;
1318
+ this.lastFrameTime = timestamp;
1319
+ const delta = Math.max(0, Math.min(rawDelta, this.options.maxDeltaTime));
1320
+ const bounds = this.dom.getBounds();
1321
+ for (const mascot of [...this.mascots.values()]) mascot.tick(delta, bounds);
1322
+ this.emitState();
1323
+ this.animationFrame = requestAnimationFrame(this.onAnimationFrame);
1324
+ };
1325
+ renderAll() {
1326
+ const bounds = this.dom.getBounds();
1327
+ for (const mascot of this.mascots.values()) mascot.tick(0, bounds);
1328
+ }
1329
+ emitState() {
1330
+ this.events.emit("statechange", this.getState());
1331
+ }
1332
+ listen(target, type, listener) {
1333
+ target.addEventListener(type, listener);
1334
+ this.disposers.push(() => target.removeEventListener(type, listener));
1335
+ }
1336
+ assertAlive() {
1337
+ if (this.destroyed) throw new Error("ShimejiEngine has been destroyed");
1338
+ }
1339
+ };
1340
+ export {
1341
+ ActionExecutor,
1342
+ BehaviorController,
1343
+ ShimejiEngine,
1344
+ SpriteManager,
1345
+ applyGravity,
1346
+ clamp,
1347
+ conditionsMatch,
1348
+ evaluateExpression,
1349
+ isIndividualSprite,
1350
+ isOnBorder,
1351
+ isOnBottom,
1352
+ isOnLeft,
1353
+ isOnRight,
1354
+ isOnTop,
1355
+ loadCharacter,
1356
+ moveToward,
1357
+ normalizeCharacterSpec,
1358
+ parseActionsXml,
1359
+ parseBehaviorsXml,
1360
+ selectWeighted
1361
+ };
1362
+ //# sourceMappingURL=index.js.map