@react-shimeji/core 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,67 +1,82 @@
1
1
  // src/dom.ts
2
2
  var DomManager = class {
3
- /** Uses the supplied element only as a mount point for independent mascots. */
3
+ /** Uses the supplied element as the containing block and clipping boundary. */
4
4
  constructor(container, sprites) {
5
5
  this.container = container;
6
6
  this.sprites = sprites;
7
+ const view = container.ownerDocument.defaultView;
8
+ const computedStyle = view?.getComputedStyle(container);
9
+ if (!computedStyle?.position || computedStyle.position === "static") this.applyContainerStyle("position", "relative");
10
+ if (computedStyle?.overflowX === "visible" || !computedStyle?.overflowX) {
11
+ this.applyContainerStyle("overflowX", "clip");
12
+ }
7
13
  }
8
14
  container;
9
15
  sprites;
10
16
  handles = /* @__PURE__ */ new Set();
17
+ restoreContainerStyles = [];
11
18
  /** Reattaches mascot elements if application code temporarily removed them. */
12
19
  ensureMounted() {
13
- const target = this.container.ownerDocument.body;
14
20
  for (const handle of this.handles) {
15
- if (handle.element.parentElement !== target) target.appendChild(handle.element);
21
+ if (handle.element.parentElement !== this.container) this.container.appendChild(handle.element);
16
22
  }
17
23
  }
18
- /** Returns viewport bounds because fixed mascots use viewport coordinates. */
24
+ /** Returns bounds in the container-local coordinate system. */
19
25
  getBounds() {
20
- const view = this.container.ownerDocument.defaultView ?? window;
21
- return { x: 0, y: 0, width: view.innerWidth, height: view.innerHeight };
26
+ return { x: 0, y: 0, width: this.container.clientWidth, height: this.container.clientHeight };
27
+ }
28
+ /** Converts a viewport client coordinate into container-local coordinates. */
29
+ toLocalPoint(clientX, clientY) {
30
+ const rectangle = this.container.getBoundingClientRect();
31
+ return { x: clientX - rectangle.left, y: clientY - rectangle.top };
22
32
  }
23
33
  /** Creates a mascot node and acquires its spritesheet resource. */
24
34
  createMascot(spec, mascotId, mascotClassName) {
25
35
  const spriteLease = this.sprites.acquire(spec.spritesheet);
26
- const element = document.createElement("div");
36
+ const element = this.container.ownerDocument.createElement("div");
27
37
  element.dataset.shimejiId = mascotId;
38
+ element.setAttribute("aria-hidden", "true");
28
39
  if (mascotClassName) element.className = mascotClassName;
29
- Object.assign(element.style, { position: "fixed", left: "0", top: "0", width: "0", height: "0", pointerEvents: "none", zIndex: "9999", userSelect: "none", willChange: "transform" });
30
- const spriteElement = document.createElement("div");
40
+ Object.assign(element.style, { position: "absolute", left: "0", top: "0", width: "0", height: "0", pointerEvents: "none", zIndex: "9999", userSelect: "none", willChange: "transform" });
41
+ const spriteElement = this.container.ownerDocument.createElement("div");
31
42
  Object.assign(spriteElement.style, { position: "absolute", left: "0", top: "0", backgroundRepeat: "no-repeat", transformOrigin: "center center", pointerEvents: "auto", touchAction: "none", userSelect: "none" });
32
43
  element.appendChild(spriteElement);
33
44
  const handle = { element, spriteElement, spriteLease };
34
45
  this.handles.add(handle);
35
- this.container.ownerDocument.body.appendChild(element);
46
+ this.container.appendChild(element);
36
47
  return handle;
37
48
  }
38
49
  /** Paints one mascot state into its existing DOM nodes. */
39
50
  render(handle, spec, state) {
40
51
  const sprite = this.sprites.resolve(spec, handle.spriteLease, state.sprite);
41
- handle.element.style.transform = `translate3d(${state.x - state.anchorX}px, ${state.y - state.anchorY}px, 0)`;
42
52
  handle.spriteElement.style.left = "0";
43
53
  handle.spriteElement.style.top = "0";
44
54
  handle.spriteElement.style.transform = `scaleX(${state.lookRight ? -1 : 1})`;
45
- if (!sprite) return;
55
+ if (!sprite) {
56
+ handle.element.style.transform = `translate3d(${state.x - state.anchorX}px, ${state.y - state.anchorY}px, 0)`;
57
+ return;
58
+ }
59
+ let width = 128;
60
+ let height = 128;
46
61
  handle.spriteElement.style.backgroundImage = `url("${sprite.url.replaceAll('"', '\\"')}")`;
47
62
  if (sprite.rectangle) {
48
- handle.spriteElement.style.width = `${sprite.rectangle.width}px`;
49
- handle.spriteElement.style.height = `${sprite.rectangle.height}px`;
50
- handle.element.style.width = `${sprite.rectangle.width}px`;
51
- handle.element.style.height = `${sprite.rectangle.height}px`;
63
+ width = sprite.rectangle.width;
64
+ height = sprite.rectangle.height;
52
65
  handle.spriteElement.style.backgroundPosition = `${-sprite.rectangle.x}px ${-sprite.rectangle.y}px`;
53
66
  handle.spriteElement.style.backgroundSize = "auto";
54
67
  } else {
55
68
  handle.spriteElement.style.backgroundPosition = "0 0";
56
69
  handle.spriteElement.style.backgroundSize = "contain";
57
- handle.spriteElement.style.width = "128px";
58
- handle.spriteElement.style.height = "128px";
59
70
  const definition = Object.values(spec.sprites).find((candidate) => typeof candidate === "object" && "url" in candidate && candidate.url === sprite.url);
60
- if (typeof definition === "object" && "width" in definition && definition.width !== void 0) handle.spriteElement.style.width = `${definition.width}px`;
61
- if (typeof definition === "object" && "height" in definition && definition.height !== void 0) handle.spriteElement.style.height = `${definition.height}px`;
62
- handle.element.style.width = handle.spriteElement.style.width;
63
- handle.element.style.height = handle.spriteElement.style.height;
71
+ if (typeof definition === "object" && "width" in definition && definition.width !== void 0) width = definition.width;
72
+ if (typeof definition === "object" && "height" in definition && definition.height !== void 0) height = definition.height;
64
73
  }
74
+ handle.spriteElement.style.width = `${width}px`;
75
+ handle.spriteElement.style.height = `${height}px`;
76
+ handle.element.style.width = `${width}px`;
77
+ handle.element.style.height = `${height}px`;
78
+ const anchorX = state.lookRight ? width - state.anchorX : state.anchorX;
79
+ handle.element.style.transform = `translate3d(${state.x - anchorX}px, ${state.y - state.anchorY}px, 0)`;
65
80
  }
66
81
  /** Removes one mascot node and releases its temporary image URL. */
67
82
  removeMascot(handle) {
@@ -77,6 +92,14 @@ var DomManager = class {
77
92
  /** Removes every mascot element and releases its temporary image URL. */
78
93
  destroy() {
79
94
  for (const handle of [...this.handles]) this.removeMascot(handle);
95
+ for (const restore of this.restoreContainerStyles.splice(0).reverse()) restore();
96
+ }
97
+ applyContainerStyle(property, value) {
98
+ const previous = this.container.style[property];
99
+ this.container.style[property] = value;
100
+ this.restoreContainerStyles.push(() => {
101
+ if (this.container.style[property] === value) this.container.style[property] = previous;
102
+ });
80
103
  }
81
104
  };
82
105
 
@@ -89,6 +112,9 @@ var actionTypeNames = {
89
112
  Animate: "Animate",
90
113
  Move: "Move",
91
114
  Embedded: "Embedded",
115
+ Composite: "Sequence",
116
+ Fixed: "Animate",
117
+ Pause: "Stay",
92
118
  \u8907\u5408: "Sequence",
93
119
  \u9078\u629E: "Select",
94
120
  \u53C2\u7167: "Reference",
@@ -138,12 +164,13 @@ function actionProperty(element, ...names) {
138
164
  function parseAnimation(element) {
139
165
  const poses = directChildren(element, "Pose", "\u30DD\u30FC\u30BA").map((pose) => ({
140
166
  sprite: attribute(pose, "Image", "\u753B\u50CF") ?? "/shime1.png",
141
- anchor: parsePoint(attribute(pose, "Anchor", "\u57FA\u6E96\u5EA7\u6A19"), { x: 64, y: 128 }),
167
+ anchor: parsePoint(attribute(pose, "ImageAnchor", "Anchor", "\u57FA\u6E96\u5EA7\u6A19"), { x: 64, y: 128 }),
142
168
  velocity: parsePoint(attribute(pose, "Velocity", "\u79FB\u52D5\u901F\u5EA6")),
143
169
  duration: Number(attribute(pose, "Duration", "\u9577\u3055") ?? 1)
144
170
  }));
145
171
  const condition = attribute(element, "Condition", "\u6761\u4EF6");
146
- return { poses, ...condition !== void 0 && { condition } };
172
+ const turn = (attribute(element, "IsTurn", "Turn") ?? "false").toLowerCase() === "true";
173
+ return { poses, ...condition !== void 0 && { condition }, ...turn && { turn } };
147
174
  }
148
175
  function parseActionElement(element) {
149
176
  const isReference = element.localName === "ActionReference" || element.localName === "\u52D5\u4F5C\u53C2\u7167";
@@ -167,20 +194,26 @@ function parseActionElement(element) {
167
194
  };
168
195
  const properties = [
169
196
  ["duration", actionProperty(element, "Duration", "\u9577\u3055")],
170
- ["gap", actionProperty(element, "Gap", "\u9593\u9694")],
197
+ ["gap", actionProperty(element, "Gap", "\u9593\u9694", "\u305A\u308C")],
171
198
  ["targetX", actionProperty(element, "TargetX", "\u76EE\u7684\u5730X")],
172
199
  ["targetY", actionProperty(element, "TargetY", "\u76EE\u7684\u5730Y")],
173
- ["velocity", actionProperty(element, "Velocity", "\u901F\u5EA6")],
200
+ ["velocity", actionProperty(element, "VelocityParam", "Velocity", "\u901F\u5EA6")],
174
201
  ["x", actionProperty(element, "X", "\u5909\u4F4DX")],
175
202
  ["y", actionProperty(element, "Y", "\u5909\u4F4DY")],
203
+ ["offsetX", actionProperty(element, "OffsetX", "\u7AEFX")],
204
+ ["offsetY", actionProperty(element, "OffsetY", "\u7AEFY")],
205
+ ["offsetType", actionProperty(element, "OffsetType")],
176
206
  ["initialVx", actionProperty(element, "InitialVX", "InitialVx", "\u521D\u901FX")],
177
207
  ["initialVy", actionProperty(element, "InitialVY", "InitialVy", "\u521D\u901FY")],
178
- ["resistanceX", actionProperty(element, "ResistanceX", "\u7A7A\u6C17\u62B5\u6297X")],
179
- ["resistanceY", actionProperty(element, "ResistanceY", "\u7A7A\u6C17\u62B5\u6297Y")],
208
+ ["resistanceX", actionProperty(element, "RegistanceX", "ResistanceX", "\u7A7A\u6C17\u62B5\u6297X")],
209
+ ["resistanceY", actionProperty(element, "RegistanceY", "ResistanceY", "\u7A7A\u6C17\u62B5\u6297Y")],
180
210
  ["gravity", actionProperty(element, "Gravity", "\u91CD\u529B")],
181
- ["bornX", actionProperty(element, "BornX", "\u8A95\u751FX")],
182
- ["bornY", actionProperty(element, "BornY", "\u8A95\u751FY")],
183
- ["bornBehavior", actionProperty(element, "BornBehavior", "\u8A95\u751F\u6642\u306E\u884C\u52D5")],
211
+ ["bornX", actionProperty(element, "BornX", "\u8A95\u751FX", "\u751F\u307E\u308C\u308B\u5834\u6240X")],
212
+ ["bornY", actionProperty(element, "BornY", "\u8A95\u751FY", "\u751F\u307E\u308C\u308B\u5834\u6240Y")],
213
+ ["bornBehavior", actionProperty(element, "BornBehavior", "BornBehaviour", "\u8A95\u751F\u6642\u306E\u884C\u52D5", "\u751F\u307E\u308C\u305F\u6642\u306E\u884C\u52D5")],
214
+ ["bornMascot", actionProperty(element, "BornMascot")],
215
+ ["bornCount", actionProperty(element, "BornCount")],
216
+ ["bornInterval", actionProperty(element, "BornInterval")],
184
217
  ["ieOffsetX", actionProperty(element, "IEOffsetX", "IE\u306E\u7AEFX")],
185
218
  ["ieOffsetY", actionProperty(element, "IEOffsetY", "IE\u306E\u7AEFY")],
186
219
  ["lookRight", actionProperty(element, "LookRight", "\u53F3\u5411\u304D")]
@@ -196,18 +229,33 @@ function parseActionsXml(xml) {
196
229
  const roots = lists.length ? lists : [document2.documentElement];
197
230
  return roots.flatMap((list) => directChildren(list, "Action", "\u52D5\u4F5C").map(parseActionElement));
198
231
  }
232
+ function parseNextBehaviors(element, inheritedConditions) {
233
+ const behaviors = [];
234
+ for (const child of Array.from(element.children)) {
235
+ if (child.localName === "Condition" || child.localName === "\u6761\u4EF6") {
236
+ const condition = attribute(child, "Condition", "\u6761\u4EF6");
237
+ behaviors.push(...parseNextBehaviors(child, [...inheritedConditions, ...condition ? [condition] : []]));
238
+ } else if (["Behavior", "\u884C\u52D5", "BehaviorReference", "BehaviorReferance", "\u884C\u52D5\u53C2\u7167"].includes(child.localName)) {
239
+ behaviors.push(parseBehaviorElement(child, inheritedConditions, 0));
240
+ }
241
+ }
242
+ return behaviors;
243
+ }
199
244
  function parseBehaviorElement(element, inheritedConditions, groupIndex) {
200
245
  const condition = attribute(element, "Condition", "\u6761\u4EF6");
201
246
  const conditions = [...inheritedConditions, ...condition ? [condition] : []];
202
247
  const nextList = directChildren(element, "NextBehaviorList", "NextBehavior", "\u6B21\u306E\u884C\u52D5\u30EA\u30B9\u30C8")[0];
203
- const nextBehaviors = nextList ? directChildren(nextList, "Behavior", "\u884C\u52D5", "BehaviorReference", "BehaviorReferance", "\u884C\u52D5\u53C2\u7167").map((child) => parseBehaviorElement(child, conditions, 0)) : [];
248
+ const nextBehaviors = nextList ? parseNextBehaviors(nextList, []) : [];
204
249
  const reference = element.localName === "BehaviorReference" || element.localName === "BehaviorReferance" || element.localName === "\u884C\u52D5\u53C2\u7167";
250
+ const actionName = attribute(element, "Action", "\u52D5\u4F5C");
205
251
  return {
206
252
  type: reference ? "Reference" : "Behavior",
207
253
  name: attribute(element, "Name", "\u540D\u524D") ?? "",
208
254
  frequency: Number(attribute(element, "Frequency", "\u983B\u5EA6") ?? 0),
209
255
  conditions,
210
256
  nextBehaviors,
257
+ ...nextList && { nextAdditive: (attribute(nextList, "Add", "\u8FFD\u52A0") ?? "true").toLowerCase() === "true" },
258
+ ...actionName !== void 0 && { actionName },
211
259
  groupIndex,
212
260
  hidden: (attribute(element, "Hidden", "\u975E\u8868\u793A") ?? "false").toLowerCase() === "true"
213
261
  };
@@ -313,7 +361,7 @@ var functions = {
313
361
  };
314
362
  var constants = { E: Math.E, PI: Math.PI };
315
363
  function normalizeExpression(source) {
316
- 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, "!");
364
+ 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(/VelocityX|速度X/gi, "velocityX").replace(/VelocityY|速度Y/gi, "velocityY").replace(/MaxCount/gi, "maxCount").replace(/Gap|ずれ/gi, "gap").replace(/\band\b/gi, "&&").replace(/\bor\b/gi, "||").replace(/\bnot\b/gi, "!");
317
365
  }
318
366
  function tokenize(source) {
319
367
  const tokens = [];
@@ -514,7 +562,7 @@ function evaluateNode(node, scope) {
514
562
  }
515
563
  }
516
564
  var expressionCache = /* @__PURE__ */ new Map();
517
- function evaluateExpression(expression, environment, fallback) {
565
+ function evaluateExpression(expression, environment, fallback, random = Math.random) {
518
566
  if (expression === void 0) return fallback;
519
567
  if (typeof expression !== "string") return expression;
520
568
  try {
@@ -524,7 +572,7 @@ function evaluateExpression(expression, environment, fallback) {
524
572
  ast = new Parser(tokenize(normalized)).parse();
525
573
  expressionCache.set(normalized, ast);
526
574
  }
527
- const result = evaluateNode(ast, environment);
575
+ const result = evaluateNode(ast, { ...environment, random: (maximum = 1) => random() * Number(maximum) });
528
576
  if (typeof fallback === "boolean") return Boolean(result);
529
577
  const numericResult = Number(result);
530
578
  return Number.isNaN(numericResult) ? fallback : numericResult;
@@ -532,8 +580,8 @@ function evaluateExpression(expression, environment, fallback) {
532
580
  return fallback;
533
581
  }
534
582
  }
535
- function conditionsMatch(conditions, environment) {
536
- return conditions.every((condition) => evaluateExpression(condition, environment, false));
583
+ function conditionsMatch(conditions, environment, random = Math.random) {
584
+ return conditions.every((condition) => evaluateExpression(condition, environment, false, random));
537
585
  }
538
586
  function selectWeighted(items, weight, random = Math.random) {
539
587
  const weighted = items.map((item) => ({ item, weight: Math.max(0, weight(item)) }));
@@ -555,62 +603,76 @@ var BehaviorController = class {
555
603
  spec;
556
604
  random;
557
605
  previous;
606
+ fallbackSelected = false;
558
607
  /** Selects an initial behavior, honoring an explicit requested name when possible. */
559
608
  selectInitial(environment, requestedName) {
609
+ this.fallbackSelected = false;
560
610
  if (requestedName) {
561
611
  const requested = this.spec.behaviors.find((behavior) => behavior.name === requestedName);
562
- if (requested && conditionsMatch(requested.conditions, environment)) return this.previous = this.resolve(requested);
612
+ if (requested) return this.previous = this.resolve(requested);
563
613
  }
564
- const fall = this.findFallBehavior();
565
- if (fall && !this.isOnAnyBoundary(environment)) return this.previous = fall;
566
614
  return this.previous = this.choose(this.spec.behaviors, environment);
567
615
  }
568
616
  /** Selects the weighted transition following the current behavior. */
569
617
  selectNext(environment) {
570
- const pool = this.previous?.nextBehaviors.length ? this.previous.nextBehaviors : this.spec.behaviors;
571
- return this.previous = this.choose(pool, environment) ?? this.findFallBehavior();
618
+ const next = this.previous?.nextBehaviors ?? [];
619
+ const pool = this.previous && this.previous.nextAdditive === false ? next : [...this.spec.behaviors, ...next];
620
+ const selected = this.choose(pool, environment);
621
+ this.fallbackSelected = selected === void 0;
622
+ return this.previous = selected ?? this.findFallBehavior();
623
+ }
624
+ /** Whether the most recent transition had no effective weighted candidate. */
625
+ usedFallback() {
626
+ return this.fallbackSelected;
572
627
  }
573
628
  /** Replaces selection history so an external interaction can force a behavior. */
574
629
  force(name) {
630
+ this.fallbackSelected = false;
575
631
  const behavior = this.spec.behaviors.find((candidate) => candidate.name === name);
576
632
  return this.previous = behavior ? this.resolve(behavior) : void 0;
577
633
  }
578
634
  choose(pool, environment) {
579
- const applicable = pool.filter((behavior) => conditionsMatch(behavior.conditions, environment));
635
+ const applicable = pool.filter((behavior) => conditionsMatch(behavior.conditions, environment, this.random));
580
636
  const chosen = selectWeighted(applicable, (behavior) => behavior.frequency, this.random);
581
637
  return chosen ? this.resolve(chosen) : void 0;
582
638
  }
583
639
  resolve(behavior) {
584
640
  if (behavior.type !== "Reference") return behavior;
585
641
  const target = this.spec.behaviors.find((candidate) => candidate.type === "Behavior" && candidate.name === behavior.name);
586
- return target ? { ...target, ...behavior, type: "Behavior", nextBehaviors: target.nextBehaviors } : { ...behavior, type: "Behavior" };
642
+ return target ? {
643
+ ...target,
644
+ ...behavior,
645
+ type: "Behavior",
646
+ nextBehaviors: target.nextBehaviors,
647
+ ...behavior.actionName !== void 0 ? { actionName: behavior.actionName } : target.actionName !== void 0 ? { actionName: target.actionName } : {},
648
+ ...target.nextAdditive !== void 0 && { nextAdditive: target.nextAdditive }
649
+ } : { ...behavior, type: "Behavior" };
587
650
  }
588
651
  findFallBehavior() {
589
652
  return this.spec.behaviors.find((behavior) => behavior.name === "Fall" || behavior.name === "\u843D\u4E0B\u3059\u308B");
590
653
  }
591
- isOnAnyBoundary(environment) {
592
- const anchor = environment.mascot.anchor;
593
- const area = environment.mascot.environment.workArea;
594
- const activeIE = environment.mascot.environment.activeIE;
595
- return area.topBorder.isOn(anchor) || area.leftBorder.isOn(anchor) || area.rightBorder.isOn(anchor) || area.bottomBorder.isOn(anchor) || activeIE.visible && (activeIE.topBorder.isOn(anchor) || activeIE.leftBorder.isOn(anchor) || activeIE.rightBorder.isOn(anchor) || activeIE.bottomBorder.isOn(anchor));
596
- }
597
654
  };
598
655
 
599
656
  // src/physics.ts
657
+ var BORDER_TOLERANCE = 1 - Number.EPSILON / 2;
658
+ function isWithinSpan(value, minimum, maximum, tolerance) {
659
+ const distance = value < minimum ? minimum - value : value > maximum ? value - maximum : 0;
660
+ return distance <= tolerance;
661
+ }
600
662
  function clamp(value, minimum, maximum) {
601
663
  return Math.min(Math.max(value, minimum), maximum);
602
664
  }
603
- function isOnTop(point, rectangle, tolerance = 1) {
604
- return point.x >= rectangle.x - tolerance && point.x <= rectangle.x + rectangle.width + tolerance && Math.abs(point.y - rectangle.y) <= tolerance;
665
+ function isOnTop(point, rectangle, tolerance = BORDER_TOLERANCE) {
666
+ return isWithinSpan(point.x, rectangle.x, rectangle.x + rectangle.width, tolerance) && Math.abs(point.y - rectangle.y) <= tolerance;
605
667
  }
606
- function isOnBottom(point, rectangle, tolerance = 1) {
607
- return point.x >= rectangle.x - tolerance && point.x <= rectangle.x + rectangle.width + tolerance && Math.abs(point.y - rectangle.y - rectangle.height) <= tolerance;
668
+ function isOnBottom(point, rectangle, tolerance = BORDER_TOLERANCE) {
669
+ return isWithinSpan(point.x, rectangle.x, rectangle.x + rectangle.width, tolerance) && Math.abs(point.y - rectangle.y - rectangle.height) <= tolerance;
608
670
  }
609
- function isOnLeft(point, rectangle, tolerance = 1) {
610
- return point.y >= rectangle.y - tolerance && point.y <= rectangle.y + rectangle.height + tolerance && Math.abs(point.x - rectangle.x) <= tolerance;
671
+ function isOnLeft(point, rectangle, tolerance = BORDER_TOLERANCE) {
672
+ return isWithinSpan(point.y, rectangle.y, rectangle.y + rectangle.height, tolerance) && Math.abs(point.x - rectangle.x) <= tolerance;
611
673
  }
612
- function isOnRight(point, rectangle, tolerance = 1) {
613
- return point.y >= rectangle.y - tolerance && point.y <= rectangle.y + rectangle.height + tolerance && Math.abs(point.x - rectangle.x - rectangle.width) <= tolerance;
674
+ function isOnRight(point, rectangle, tolerance = BORDER_TOLERANCE) {
675
+ return isWithinSpan(point.y, rectangle.y, rectangle.y + rectangle.height, tolerance) && Math.abs(point.x - rectangle.x - rectangle.width) <= tolerance;
614
676
  }
615
677
  function isOnBorder(state, bounds, border, platform) {
616
678
  if (!border) return true;
@@ -618,29 +680,46 @@ function isOnBorder(state, bounds, border, platform) {
618
680
  if (border === "Ceiling") return isOnTop(state, bounds) || platform !== void 0 && isOnBottom(state, platform);
619
681
  return isOnLeft(state, bounds) || isOnRight(state, bounds) || platform !== void 0 && (isOnLeft(state, platform) || isOnRight(state, platform));
620
682
  }
621
- function applyGravity(state, bounds, frameScale, gravity, resistanceX = 0.05, resistanceY = 0.01, platform) {
622
- const previousY = state.y;
623
- const nextX = clamp(state.x + state.vx * frameScale, bounds.x, bounds.x + bounds.width);
624
- const nextY = clamp(state.y + state.vy * frameScale, bounds.y, bounds.y + bounds.height);
625
- state.x = nextX;
626
- state.y = nextY;
627
- state.vx *= Math.max(0, 1 - resistanceX * frameScale);
628
- state.vy = state.vy * Math.max(0, 1 - resistanceY * frameScale) + gravity * frameScale;
629
- if (platform && nextY >= previousY && previousY <= platform.y && nextY >= platform.y && nextX >= platform.x && nextX <= platform.x + platform.width) {
630
- state.y = platform.y;
631
- state.vy = 0;
632
- return true;
633
- }
634
- if (state.y >= bounds.y + bounds.height) {
635
- state.y = bounds.y + bounds.height;
636
- state.vy = 0;
637
- return true;
638
- }
639
- if (state.x <= bounds.x || state.x >= bounds.x + bounds.width) {
640
- state.vx = 0;
641
- return true;
683
+ function isOnFloor(point, bounds, platforms = []) {
684
+ return platforms.some((platform) => isOnTop(point, platform)) || isOnBottom(point, bounds);
685
+ }
686
+ function isOnWall(point, bounds, lookRight, platforms = []) {
687
+ return lookRight ? platforms.some((platform) => isOnLeft(point, platform)) || isOnRight(point, bounds) : platforms.some((platform) => isOnRight(point, platform)) || isOnLeft(point, bounds);
688
+ }
689
+ function applyGravity(state, bounds, frameScale, gravity, resistanceX = 0.05, resistanceY = 0.1, platform) {
690
+ const platforms = platform ? [platform] : [];
691
+ const steps = Math.max(1, Math.round(frameScale));
692
+ let stopped = false;
693
+ for (let frame = 0; frame < steps && !stopped; frame += 1) {
694
+ state.vx -= state.vx * resistanceX;
695
+ state.vy = state.vy - state.vy * resistanceY + gravity;
696
+ const dx = Math.trunc(state.vx);
697
+ const dy = Math.trunc(state.vy);
698
+ const divisions = Math.max(1, Math.abs(dx), Math.abs(dy));
699
+ const start = { x: state.x, y: state.y };
700
+ for (let index = 0; index <= divisions; index += 1) {
701
+ const x = start.x + Math.trunc(dx * index / divisions);
702
+ const y = start.y + Math.trunc(dy * index / divisions);
703
+ state.x = x;
704
+ state.y = y;
705
+ if (dy > 0) {
706
+ for (let offset = -80; offset <= 0; offset += 1) {
707
+ state.y = y + offset;
708
+ if (isOnFloor(state, bounds, platforms)) {
709
+ stopped = true;
710
+ break;
711
+ }
712
+ }
713
+ if (stopped) break;
714
+ state.y = y;
715
+ }
716
+ if (isOnWall(state, bounds, state.lookRight, platforms)) {
717
+ stopped = true;
718
+ break;
719
+ }
720
+ }
642
721
  }
643
- return false;
722
+ return stopped;
644
723
  }
645
724
  function moveToward(state, target, speed, frameScale) {
646
725
  const dx = target.x - state.x;
@@ -657,251 +736,832 @@ function moveToward(state, target, speed, frameScale) {
657
736
  }
658
737
 
659
738
  // src/action.ts
660
- function numeric(value, environment) {
661
- return value === void 0 ? void 0 : evaluateExpression(value, environment, 0);
739
+ function expressionIsPerFrame(value) {
740
+ return typeof value === "string" && value.trimStart().startsWith("#{");
662
741
  }
663
- function evaluateAction(definition, environment) {
664
- const gap = numeric(definition.gap, environment) ?? 0;
665
- const scopedEnvironment = { ...environment, gap };
666
- const targetX = numeric(definition.targetX, scopedEnvironment);
667
- const initialVx = numeric(definition.initialVx, scopedEnvironment);
668
- let lookRight = environment.mascot.lookRight;
669
- if (definition.borderType === "Wall") {
670
- const { activeIE, workArea } = environment.mascot.environment;
671
- lookRight = workArea.rightBorder.isOn(environment.mascot.anchor) || activeIE.visible && activeIE.leftBorder.isOn(environment.mascot.anchor);
672
- } else if (definition.type === "Move" || definition.embedType === "Jump" || definition.embedType === "WalkWithIE") {
673
- if (targetX !== void 0) lookRight = targetX > environment.mascot.anchor.x;
674
- } else if (definition.embedType === "Fall" || definition.embedType === "FallWithIE") {
675
- if (initialVx !== void 0 && initialVx !== 0) lookRight = initialVx > 0;
676
- } else if (definition.embedType === "Look") {
677
- lookRight = definition.lookRight === void 0 ? !environment.mascot.lookRight : typeof definition.lookRight === "boolean" ? definition.lookRight : evaluateExpression(definition.lookRight, scopedEnvironment, environment.mascot.lookRight);
678
- }
679
- const duration = numeric(definition.duration, scopedEnvironment);
680
- const targetY = numeric(definition.targetY, scopedEnvironment);
681
- const velocity = numeric(definition.velocity, scopedEnvironment);
682
- const x = numeric(definition.x, scopedEnvironment);
683
- const y = numeric(definition.y, scopedEnvironment);
684
- const initialVy = numeric(definition.initialVy, scopedEnvironment);
685
- const resistanceX = numeric(definition.resistanceX, scopedEnvironment);
686
- const resistanceY = numeric(definition.resistanceY, scopedEnvironment);
687
- const gravity = numeric(definition.gravity, scopedEnvironment);
688
- const bornX = numeric(definition.bornX, scopedEnvironment);
689
- const bornY = numeric(definition.bornY, scopedEnvironment);
690
- return {
691
- definition,
692
- lookRight,
693
- ...duration !== void 0 && { duration },
694
- ...targetX !== void 0 && { targetX },
695
- ...targetY !== void 0 && { targetY },
696
- ...velocity !== void 0 && { velocity },
697
- ...x !== void 0 && { x },
698
- ...y !== void 0 && { y },
699
- ...initialVx !== void 0 && { initialVx },
700
- ...initialVy !== void 0 && { initialVy },
701
- ...resistanceX !== void 0 && { resistanceX },
702
- ...resistanceY !== void 0 && { resistanceY },
703
- ...gravity !== void 0 && { gravity },
704
- ...bornX !== void 0 && { bornX },
705
- ...bornY !== void 0 && { bornY }
706
- };
742
+ var ActionValues = class {
743
+ constructor(random) {
744
+ this.random = random;
745
+ }
746
+ random;
747
+ actionCache = /* @__PURE__ */ new Map();
748
+ frameCache = /* @__PURE__ */ new Map();
749
+ init() {
750
+ this.actionCache.clear();
751
+ this.frameCache.clear();
752
+ }
753
+ initFrame() {
754
+ this.frameCache.clear();
755
+ }
756
+ number(key, value, environment, fallback) {
757
+ if (value === void 0) return fallback;
758
+ if (typeof value === "number") return value;
759
+ const cache = expressionIsPerFrame(value) ? this.frameCache : this.actionCache;
760
+ const cached = cache.get(key);
761
+ if (typeof cached === "number") return cached;
762
+ const result = evaluateExpression(value, environment, fallback, this.random);
763
+ cache.set(key, result);
764
+ return result;
765
+ }
766
+ boolean(key, value, environment, fallback) {
767
+ if (value === void 0) return fallback;
768
+ if (typeof value === "boolean") return value;
769
+ const cache = expressionIsPerFrame(value) ? this.frameCache : this.actionCache;
770
+ const cached = cache.get(key);
771
+ if (typeof cached === "boolean") return cached;
772
+ const result = evaluateExpression(value, environment, fallback, this.random);
773
+ cache.set(key, result);
774
+ return result;
775
+ }
776
+ };
777
+ var RuntimeBase = class {
778
+ constructor(definition, random) {
779
+ this.definition = definition;
780
+ this.values = new ActionValues(random);
781
+ }
782
+ definition;
783
+ time = 0;
784
+ values;
785
+ init(context) {
786
+ this.time = 0;
787
+ this.values.init();
788
+ this.onInit(context);
789
+ }
790
+ hasNext(context) {
791
+ return this.baseHasNext(context) && this.hasMore(context);
792
+ }
793
+ step(context) {
794
+ this.values.initFrame();
795
+ const result = this.tick(context);
796
+ this.time += 1;
797
+ return result;
798
+ }
799
+ onInit(_context) {
800
+ }
801
+ hasMore(_context) {
802
+ return true;
803
+ }
804
+ baseHasNext(context) {
805
+ const condition = this.values.boolean("condition", this.definition.condition, context.environment, true);
806
+ const duration = Math.trunc(this.values.number("duration", this.definition.duration, context.environment, Number.POSITIVE_INFINITY));
807
+ return condition && this.time < duration;
808
+ }
809
+ };
810
+ var TrackedBorder = class {
811
+ constructor(side, source, context) {
812
+ this.side = side;
813
+ this.source = source;
814
+ this.previous = this.rectangle(context);
815
+ }
816
+ side;
817
+ source;
818
+ previous;
819
+ move(point, context) {
820
+ const current = this.rectangle(context);
821
+ const previous = this.previous;
822
+ this.previous = current;
823
+ if (!current || !previous) return point;
824
+ if (this.side === "left" || this.side === "right") {
825
+ if (previous.height === 0) return point;
826
+ const next2 = {
827
+ x: point.x + this.coordinate(current) - this.coordinate(previous),
828
+ y: Math.trunc((point.y - previous.y) * current.height / previous.height + current.y)
829
+ };
830
+ return Math.abs(next2.x - point.x) >= 80 || Math.abs(next2.y - point.y) >= 80 ? point : next2;
831
+ }
832
+ if (previous.width === 0) return point;
833
+ const next = {
834
+ // FloorCeiling.java performs integer division before applying the
835
+ // mascot's relative offset along a resized border.
836
+ x: (point.x - previous.x) * Math.trunc(current.width / previous.width) + current.x,
837
+ y: point.y + this.coordinate(current) - this.coordinate(previous)
838
+ };
839
+ return Math.abs(next.x - point.x) >= 80 || next.y - point.y > 20 || next.y - point.y < -80 ? point : next;
840
+ }
841
+ isOn(point, context) {
842
+ const rectangle = this.rectangle(context);
843
+ if (!rectangle) return false;
844
+ switch (this.side) {
845
+ case "top":
846
+ return isOnTop(point, rectangle);
847
+ case "bottom":
848
+ return isOnBottom(point, rectangle);
849
+ case "left":
850
+ return isOnLeft(point, rectangle);
851
+ case "right":
852
+ return isOnRight(point, rectangle);
853
+ }
854
+ }
855
+ coordinate(rectangle) {
856
+ switch (this.side) {
857
+ case "top":
858
+ return rectangle.y;
859
+ case "bottom":
860
+ return rectangle.y + rectangle.height;
861
+ case "left":
862
+ return rectangle.x;
863
+ case "right":
864
+ return rectangle.x + rectangle.width;
865
+ }
866
+ }
867
+ rectangle(context) {
868
+ if (this.source === "work-area") return context.bounds;
869
+ if (!this.source) return void 0;
870
+ return context.platforms.find((platform) => platform.element === this.source);
871
+ }
872
+ };
873
+ function selectBorder(type, state, context) {
874
+ if (type === "Floor") {
875
+ const platform2 = context.platforms.find((candidate) => isOnTop(state, candidate));
876
+ if (platform2) return new TrackedBorder("top", platform2.element, context);
877
+ if (isOnBottom(state, context.bounds)) return new TrackedBorder("bottom", "work-area", context);
878
+ return new TrackedBorder("bottom", void 0, context);
879
+ }
880
+ if (type === "Ceiling") {
881
+ const platform2 = context.platforms.find((candidate) => isOnBottom(state, candidate));
882
+ if (platform2) return new TrackedBorder("bottom", platform2.element, context);
883
+ if (isOnTop(state, context.bounds)) return new TrackedBorder("top", "work-area", context);
884
+ return new TrackedBorder("top", void 0, context);
885
+ }
886
+ if (state.lookRight) {
887
+ const platform2 = context.platforms.find((candidate) => isOnLeft(state, candidate));
888
+ if (platform2) return new TrackedBorder("left", platform2.element, context);
889
+ if (isOnRight(state, context.bounds)) return new TrackedBorder("right", "work-area", context);
890
+ return new TrackedBorder("right", void 0, context);
891
+ }
892
+ const platform = context.platforms.find((candidate) => isOnRight(state, candidate));
893
+ if (platform) return new TrackedBorder("right", platform.element, context);
894
+ if (isOnLeft(state, context.bounds)) return new TrackedBorder("left", "work-area", context);
895
+ return new TrackedBorder("left", void 0, context);
707
896
  }
708
- var SequenceRuntime = class {
709
- constructor(definitions, factory, loop) {
710
- this.definitions = definitions;
711
- this.factory = factory;
712
- this.loop = loop;
897
+ var AnimatedRuntime = class extends RuntimeBase {
898
+ constructor(definition, state, random) {
899
+ super(definition, random);
900
+ this.state = state;
713
901
  }
714
- definitions;
715
- factory;
716
- loop;
717
- index = 0;
718
- child;
719
- tick(deltaMs, environment, bounds) {
720
- for (let guard = 0; guard < 32; guard += 1) {
721
- const definition = this.definitions[this.index];
722
- if (!definition) {
723
- if (!this.loop || this.definitions.length === 0) return true;
724
- this.index = 0;
725
- continue;
726
- }
727
- this.child ??= this.factory(definition, environment);
728
- if (!this.child.tick(deltaMs, environment, bounds)) return false;
729
- this.child = void 0;
730
- this.index += 1;
731
- deltaMs = 0;
902
+ state;
903
+ border;
904
+ onInit(context) {
905
+ this.border = this.definition.borderType ? selectBorder(this.definition.borderType, this.state, context) : void 0;
906
+ }
907
+ animation(context, turn) {
908
+ const scoped = this.scopedEnvironment(context.environment);
909
+ return this.definition.animations?.find((animation, index) => (turn === void 0 || Boolean(animation.turn) === turn) && this.values.boolean(`animation-${index}`, animation.condition, scoped, true));
910
+ }
911
+ animationDuration(context, turn) {
912
+ return this.animation(context, turn)?.poses.reduce((sum, pose) => sum + Math.max(0, pose.duration), 0) ?? 0;
913
+ }
914
+ applyBorder(context) {
915
+ if (!this.border) return "running";
916
+ const moved = this.border.move(this.state, context);
917
+ this.state.x = moved.x;
918
+ this.state.y = moved.y;
919
+ return this.border.isOn(this.state, context) ? "running" : "lost-ground";
920
+ }
921
+ applyAnimation(context, turn) {
922
+ const animation = this.animation(context, turn);
923
+ const pose = animation && poseAt(animation, this.time);
924
+ if (pose) applyPose(this.state, pose);
925
+ }
926
+ scopedEnvironment(environment) {
927
+ const gap = this.values.number("gap", this.definition.gap, environment, 0);
928
+ const withGap = { ...environment, gap };
929
+ const targetX = this.definition.targetX === void 0 ? void 0 : this.values.number("targetX", this.definition.targetX, withGap, 0);
930
+ const targetY = this.definition.targetY === void 0 ? void 0 : this.values.number("targetY", this.definition.targetY, withGap, 0);
931
+ return { ...withGap, ...targetX !== void 0 && { targetX }, ...targetY !== void 0 && { targetY } };
932
+ }
933
+ };
934
+ function poseAt(animation, time) {
935
+ const duration = animation.poses.reduce((sum, pose) => sum + Math.max(0, pose.duration), 0);
936
+ if (duration <= 0) return void 0;
937
+ let cursor = time % duration;
938
+ for (const pose of animation.poses) {
939
+ cursor -= Math.max(0, pose.duration);
940
+ if (cursor < 0) return pose;
941
+ }
942
+ return animation.poses.at(-1);
943
+ }
944
+ function applyPose(state, pose) {
945
+ state.sprite = pose.sprite;
946
+ state.anchorX = pose.anchor.x;
947
+ state.anchorY = pose.anchor.y;
948
+ state.x += (state.lookRight ? -1 : 1) * pose.velocity.x;
949
+ state.y += pose.velocity.y;
950
+ }
951
+ var StayRuntime = class extends AnimatedRuntime {
952
+ tick(context) {
953
+ const border = this.applyBorder(context);
954
+ if (border === "lost-ground") return border;
955
+ this.applyAnimation(context);
956
+ return "running";
957
+ }
958
+ };
959
+ var AnimateRuntime = class extends StayRuntime {
960
+ hasMore(context) {
961
+ return this.time < this.animationDuration(context);
962
+ }
963
+ };
964
+ var MoveRuntime = class extends AnimatedRuntime {
965
+ turning = false;
966
+ hasTurningAnimation = false;
967
+ onInit(context) {
968
+ super.onInit(context);
969
+ this.turning = false;
970
+ this.hasTurningAnimation = this.definition.animations?.some((animation) => animation.turn) ?? false;
971
+ }
972
+ hasMore(context) {
973
+ const scoped = this.scopedEnvironment(context.environment);
974
+ const targetX = this.targetX(scoped);
975
+ const targetY = this.targetY(scoped);
976
+ const reached = targetX !== void 0 && this.state.x === targetX || targetY !== void 0 && this.state.y === targetY;
977
+ return !reached || this.turning;
978
+ }
979
+ tick(context) {
980
+ const border = this.applyBorder(context);
981
+ if (border === "lost-ground") return border;
982
+ const scoped = this.scopedEnvironment(context.environment);
983
+ const targetX = this.targetX(scoped);
984
+ const targetY = this.targetY(scoped);
985
+ let down = false;
986
+ if (targetX !== void 0 && this.state.x !== targetX) {
987
+ const nextLookRight = this.state.x < targetX;
988
+ this.turning = this.hasTurningAnimation && (this.turning || nextLookRight !== this.state.lookRight);
989
+ this.state.lookRight = nextLookRight;
732
990
  }
733
- return false;
991
+ if (targetY !== void 0) down = this.state.y < targetY;
992
+ if (this.turning && this.time >= this.animationDuration(context, true)) this.turning = false;
993
+ this.applyAnimation(context, this.turning);
994
+ if (targetX !== void 0 && (this.state.lookRight && this.state.x >= targetX || !this.state.lookRight && this.state.x <= targetX)) this.state.x = targetX;
995
+ if (targetY !== void 0 && (down && this.state.y >= targetY || !down && this.state.y <= targetY)) this.state.y = targetY;
996
+ return "running";
997
+ }
998
+ targetX(environment) {
999
+ return this.definition.targetX === void 0 ? void 0 : Math.trunc(this.values.number("targetX", this.definition.targetX, environment, 0));
1000
+ }
1001
+ targetY(environment) {
1002
+ return this.definition.targetY === void 0 ? void 0 : Math.trunc(this.values.number("targetY", this.definition.targetY, environment, 0));
734
1003
  }
735
1004
  };
736
- var CompleteRuntime = class {
1005
+ var MoveWithTurnRuntime = class extends MoveRuntime {
1006
+ onInit(context) {
1007
+ super.onInit(context);
1008
+ this.hasTurningAnimation = (this.definition.animations?.length ?? 0) >= 2;
1009
+ }
1010
+ animation(context, turn) {
1011
+ const animations = this.definition.animations ?? [];
1012
+ if (turn) return animations.at(-1);
1013
+ const scoped = this.scopedEnvironment(context.environment);
1014
+ return animations.slice(0, -1).find((candidate, index) => this.values.boolean(`animation-${index}`, candidate.condition, scoped, true));
1015
+ }
1016
+ };
1017
+ var TurnRuntime = class extends AnimatedRuntime {
1018
+ turning = false;
1019
+ hasMore(context) {
1020
+ const desired = this.values.boolean("lookRight", this.definition.lookRight, context.environment, !this.state.lookRight);
1021
+ this.turning ||= desired !== this.state.lookRight;
1022
+ return this.turning && this.time < this.animationDuration(context);
1023
+ }
1024
+ tick(context) {
1025
+ this.state.lookRight = this.values.boolean("lookRight", this.definition.lookRight, context.environment, !this.state.lookRight);
1026
+ const border = this.applyBorder(context);
1027
+ if (border === "lost-ground") return border;
1028
+ this.applyAnimation(context);
1029
+ return "running";
1030
+ }
1031
+ };
1032
+ var InstantRuntime = class extends RuntimeBase {
1033
+ constructor(definition, state, random, operation) {
1034
+ super(definition, random);
1035
+ this.state = state;
1036
+ this.operation = operation;
1037
+ }
1038
+ state;
1039
+ operation;
1040
+ onInit(context) {
1041
+ if (!this.baseHasNext(context)) return;
1042
+ if (this.operation === "look") {
1043
+ this.state.lookRight = this.values.boolean("lookRight", this.definition.lookRight, context.environment, !this.state.lookRight);
1044
+ } else if (this.operation === "offset") {
1045
+ this.state.x += Math.trunc(this.values.number("x", this.definition.x, context.environment, 0));
1046
+ this.state.y += Math.trunc(this.values.number("y", this.definition.y, context.environment, 0));
1047
+ }
1048
+ }
1049
+ hasMore() {
1050
+ return false;
1051
+ }
737
1052
  tick() {
738
- return true;
1053
+ return "running";
739
1054
  }
740
1055
  };
741
- var LeafRuntime = class {
742
- constructor(action, state, options, callbacks, environment) {
743
- this.action = action;
1056
+ var JumpRuntime = class extends RuntimeBase {
1057
+ constructor(definition, state, random) {
1058
+ super(definition, random);
744
1059
  this.state = state;
745
- this.options = options;
746
- this.callbacks = callbacks;
747
- const animationEnvironment = { ...environment, ...action.targetX !== void 0 && { targetX: action.targetX }, ...action.targetY !== void 0 && { targetY: action.targetY } };
748
- 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];
749
- this.poseDuration = this.animation?.poses.reduce((sum, pose) => sum + Math.max(0, pose.duration), 0) ?? 0;
750
1060
  }
751
- action;
752
1061
  state;
753
- options;
754
- callbacks;
755
- elapsedMs = 0;
756
- started = false;
757
- spawned = false;
758
- animation;
759
- poseDuration;
760
- tick(deltaMs, environment, bounds) {
761
- const frameScale = deltaMs / this.options.frameDuration;
762
- if (!this.started) {
763
- this.started = true;
764
- this.state.lookRight = this.action.lookRight;
765
- this.initialize();
1062
+ hasMore(context) {
1063
+ return this.distance(context).distance !== 0;
1064
+ }
1065
+ tick(context) {
1066
+ const { targetX, targetY, distanceX, distanceY, distance } = this.distance(context);
1067
+ this.state.lookRight = this.state.x < targetX;
1068
+ const velocity = this.values.number("velocity", this.definition.velocity, context.environment, 20);
1069
+ if (distance !== 0) {
1070
+ this.state.vx = velocity * distanceX / distance;
1071
+ this.state.vy = velocity * distanceY / distance;
1072
+ this.state.x += Math.trunc(this.state.vx);
1073
+ this.state.y += Math.trunc(this.state.vy);
1074
+ const environment = { ...context.environment, targetX, targetY };
1075
+ const animation = this.definition.animations?.find((candidate, index) => this.values.boolean(`animation-${index}`, candidate.condition, environment, true));
1076
+ const pose = animation && poseAt(animation, this.time);
1077
+ if (pose) applyPose(this.state, pose);
766
1078
  }
767
- this.elapsedMs += deltaMs;
768
- const pose = this.currentPose();
769
- if (pose) this.applyPose(pose, frameScale, bounds);
770
- const { definition } = this.action;
771
- if (definition.type === "Embedded") return this.tickEmbedded(frameScale, environment, bounds);
772
- if (definition.type === "Move" && (this.action.targetX !== void 0 || this.action.targetY !== void 0)) {
773
- const reachedX = this.action.targetX === void 0 || Math.abs(this.state.x - this.action.targetX) < 0.5;
774
- const reachedY = this.action.targetY === void 0 || Math.abs(this.state.y - this.action.targetY) < 0.5;
775
- return reachedX && reachedY;
1079
+ if (distance <= velocity) {
1080
+ this.state.x = targetX;
1081
+ this.state.y = targetY;
776
1082
  }
777
- return this.elapsedMs >= this.durationMs();
1083
+ return "running";
778
1084
  }
779
- initialize() {
780
- const type = this.action.definition.embedType;
781
- if (type === "Fall" || type === "FallWithIE") {
782
- this.state.vx = this.action.initialVx ?? this.state.vx;
783
- this.state.vy = this.action.initialVy ?? this.state.vy;
784
- }
785
- if (type === "Offset") {
786
- this.state.x += this.action.x ?? 0;
787
- this.state.y += this.action.y ?? 0;
1085
+ distance(context) {
1086
+ const targetX = Math.trunc(this.values.number("targetX", this.definition.targetX, context.environment, 0));
1087
+ const targetY = Math.trunc(this.values.number("targetY", this.definition.targetY, context.environment, 0));
1088
+ const distanceX = targetX - this.state.x;
1089
+ const distanceY = targetY - this.state.y - Math.abs(distanceX) / 2;
1090
+ return { targetX, targetY, distanceX, distanceY, distance: Math.hypot(distanceX, distanceY) };
1091
+ }
1092
+ };
1093
+ var FallRuntime = class extends RuntimeBase {
1094
+ constructor(definition, state, random, defaultGravity) {
1095
+ super(definition, random);
1096
+ this.state = state;
1097
+ this.defaultGravity = defaultGravity;
1098
+ }
1099
+ state;
1100
+ defaultGravity;
1101
+ modX = 0;
1102
+ modY = 0;
1103
+ onInit(context) {
1104
+ this.modX = 0;
1105
+ this.modY = 0;
1106
+ this.state.vx = Math.trunc(this.values.number("initialVx", this.definition.initialVx, context.environment, 0));
1107
+ this.state.vy = Math.trunc(this.values.number("initialVy", this.definition.initialVy, context.environment, 0));
1108
+ }
1109
+ hasMore(context) {
1110
+ return !isOnFloor(this.state, context.bounds, context.platforms) && !isOnWall(this.state, context.bounds, this.state.lookRight, context.platforms);
1111
+ }
1112
+ tick(context) {
1113
+ if (this.state.vx !== 0) this.state.lookRight = this.state.vx > 0;
1114
+ const resistanceX = this.values.number("resistanceX", this.definition.resistanceX, context.environment, 0.05);
1115
+ const resistanceY = this.values.number("resistanceY", this.definition.resistanceY, context.environment, 0.1);
1116
+ const gravity = this.values.number("gravity", this.definition.gravity, context.environment, this.defaultGravity);
1117
+ this.state.vx -= this.state.vx * resistanceX;
1118
+ this.state.vy = this.state.vy - this.state.vy * resistanceY + gravity;
1119
+ this.modX += this.state.vx % 1;
1120
+ this.modY += this.state.vy % 1;
1121
+ const dx = Math.trunc(this.state.vx) + Math.trunc(this.modX);
1122
+ const dy = Math.trunc(this.state.vy) + Math.trunc(this.modY);
1123
+ this.modX %= 1;
1124
+ this.modY %= 1;
1125
+ const divisions = Math.max(1, Math.abs(dx), Math.abs(dy));
1126
+ const start = { x: this.state.x, y: this.state.y };
1127
+ let stopped = false;
1128
+ for (let index = 0; index <= divisions; index += 1) {
1129
+ const x = start.x + Math.trunc(dx * index / divisions);
1130
+ const y = start.y + Math.trunc(dy * index / divisions);
1131
+ this.state.x = x;
1132
+ this.state.y = y;
1133
+ if (dy > 0) {
1134
+ for (let offset = -80; offset <= 0; offset += 1) {
1135
+ this.state.y = y + offset;
1136
+ if (isOnFloor(this.state, context.bounds, context.platforms)) {
1137
+ stopped = true;
1138
+ break;
1139
+ }
1140
+ }
1141
+ if (stopped) break;
1142
+ this.state.y = y;
1143
+ }
1144
+ if (isOnWall(this.state, context.bounds, this.state.lookRight, context.platforms)) break;
788
1145
  }
789
- if (type === "Reboot") {
790
- this.state.vx = 0;
791
- this.state.vy = 0;
1146
+ const fallEnvironment = { ...context.environment, velocityX: this.state.vx, velocityY: this.state.vy };
1147
+ const animation = this.definition.animations?.find((candidate, index) => this.values.boolean(`animation-${index}`, candidate.condition, fallEnvironment, true));
1148
+ const pose = animation && poseAt(animation, this.time);
1149
+ if (pose) applyPose(this.state, pose);
1150
+ return "running";
1151
+ }
1152
+ };
1153
+ function matchingActivePlatform(context) {
1154
+ const active = context.environment.mascot.environment.activeIE;
1155
+ if (!active.visible) return void 0;
1156
+ return context.platforms.find((platform) => Math.abs(platform.x - active.x) < 1e-3 && Math.abs(platform.y - active.y) < 1e-3 && Math.abs(platform.width - active.width) < 1e-3 && Math.abs(platform.height - active.height) < 1e-3);
1157
+ }
1158
+ function carryRelation(state, platform, offsetX, offsetY) {
1159
+ const grip = {
1160
+ x: state.x + (state.lookRight ? -offsetX : offsetX),
1161
+ y: state.y + offsetY
1162
+ };
1163
+ return isOnBottom(grip, platform) && (state.lookRight ? isOnLeft(grip, platform) : isOnRight(grip, platform));
1164
+ }
1165
+ function carriedPlatformPosition(state, platform, offsetX, offsetY) {
1166
+ return state.lookRight ? { x: state.x - offsetX, y: state.y + offsetY - platform.height } : { x: state.x + offsetX - platform.width, y: state.y + offsetY - platform.height };
1167
+ }
1168
+ var CarryFallRuntime = class extends FallRuntime {
1169
+ constructor(definition, state, random, gravity, callbacks) {
1170
+ super(definition, state, random, gravity);
1171
+ this.callbacks = callbacks;
1172
+ }
1173
+ callbacks;
1174
+ element;
1175
+ onInit(context) {
1176
+ super.onInit(context);
1177
+ this.element = matchingActivePlatform(context)?.element;
1178
+ }
1179
+ tick(context) {
1180
+ const platform = context.platforms.find((candidate) => candidate.element === this.element);
1181
+ const offsetX = Math.trunc(this.values.number("ieOffsetX", this.definition.ieOffsetX, context.environment, 0));
1182
+ const offsetY = Math.trunc(this.values.number("ieOffsetY", this.definition.ieOffsetY, context.environment, 0));
1183
+ if (!platform || !carryRelation(this.state, platform, offsetX, offsetY)) return "lost-ground";
1184
+ const result = super.tick(context);
1185
+ this.callbacks.movePlatform?.(platform.element, carriedPlatformPosition(this.state, platform, offsetX, offsetY));
1186
+ return result;
1187
+ }
1188
+ };
1189
+ var CarryMoveRuntime = class extends MoveRuntime {
1190
+ constructor(definition, state, random, callbacks) {
1191
+ super(definition, state, random);
1192
+ this.callbacks = callbacks;
1193
+ }
1194
+ callbacks;
1195
+ element;
1196
+ onInit(context) {
1197
+ super.onInit(context);
1198
+ this.element = matchingActivePlatform(context)?.element;
1199
+ }
1200
+ tick(context) {
1201
+ const platform = context.platforms.find((candidate) => candidate.element === this.element);
1202
+ const offsetX = Math.trunc(this.values.number("ieOffsetX", this.definition.ieOffsetX, context.environment, 0));
1203
+ const offsetY = Math.trunc(this.values.number("ieOffsetY", this.definition.ieOffsetY, context.environment, 0));
1204
+ if (!platform || !carryRelation(this.state, platform, offsetX, offsetY)) return "lost-ground";
1205
+ const result = super.tick(context);
1206
+ this.callbacks.movePlatform?.(platform.element, carriedPlatformPosition(this.state, platform, offsetX, offsetY));
1207
+ return result;
1208
+ }
1209
+ };
1210
+ var ThrowPlatformRuntime = class extends AnimateRuntime {
1211
+ constructor(definition, state, random, callbacks) {
1212
+ super(definition, state, random);
1213
+ this.callbacks = callbacks;
1214
+ }
1215
+ callbacks;
1216
+ element;
1217
+ onInit(context) {
1218
+ super.onInit(context);
1219
+ this.element = matchingActivePlatform(context)?.element;
1220
+ }
1221
+ tick(context) {
1222
+ const result = super.tick(context);
1223
+ const platform = context.platforms.find((candidate) => candidate.element === this.element);
1224
+ if (platform) {
1225
+ const vx = Math.trunc(this.values.number("initialVx", this.definition.initialVx, context.environment, 32));
1226
+ const vy = Math.trunc(this.values.number("initialVy", this.definition.initialVy, context.environment, -10));
1227
+ const gravity = this.values.number("gravity", this.definition.gravity, context.environment, 0.5);
1228
+ this.callbacks.movePlatform?.(platform.element, {
1229
+ x: platform.x + (this.state.lookRight ? vx : -vx),
1230
+ y: platform.y + vy + Math.trunc(this.time * gravity)
1231
+ });
792
1232
  }
1233
+ return result;
793
1234
  }
794
- tickEmbedded(frameScale, environment, bounds) {
795
- switch (this.action.definition.embedType) {
796
- case "Fall":
797
- case "FallWithIE":
798
- case "Thrown":
799
- return applyGravity(
800
- this.state,
801
- bounds,
802
- frameScale,
803
- this.action.gravity ?? this.options.gravity,
804
- this.action.resistanceX,
805
- this.action.resistanceY,
806
- environment.mascot.environment.activeIE.visible ? environment.mascot.environment.activeIE : void 0
807
- );
808
- case "Jump": {
809
- const target = { x: this.action.targetX ?? this.state.x, y: this.action.targetY ?? this.state.y };
810
- return moveToward(this.state, target, this.action.velocity ?? 20, frameScale);
811
- }
812
- case "Breed":
813
- if (!this.spawned && this.elapsedMs >= this.durationMs()) {
814
- this.spawned = true;
815
- const child = { x: this.state.x + (this.action.bornX ?? 0), y: this.state.y + (this.action.bornY ?? 0) };
816
- this.callbacks.spawn(this.action.definition.bornBehavior ? { ...child, behaviorName: this.action.definition.bornBehavior } : child);
817
- return true;
818
- }
819
- return false;
820
- case "Exit":
821
- this.callbacks.remove();
822
- return true;
823
- case "Reboot":
824
- this.state.x = bounds.x + 128 + Math.max(0, bounds.width - 256) * Math.random();
825
- this.state.y = bounds.y + 128 + Math.max(0, bounds.height - 256) * Math.random();
826
- return true;
827
- case "Offset":
828
- case "Look":
829
- return true;
830
- case "Dragged":
831
- return false;
832
- default:
833
- return this.elapsedMs >= this.durationMs();
1235
+ };
1236
+ function breed(definition, state, values, context, callbacks) {
1237
+ const bornX = Math.trunc(values.number("bornX", definition.bornX, context.environment, 0));
1238
+ const bornY = Math.trunc(values.number("bornY", definition.bornY, context.environment, 0));
1239
+ const count = Math.trunc(values.number("bornCount", definition.bornCount, context.environment, 1));
1240
+ if (count < 1) throw new RangeError("BornCount must be positive");
1241
+ for (let index = 0; index < count; index += 1) {
1242
+ callbacks.spawn({
1243
+ x: state.x + (state.lookRight ? -bornX : bornX),
1244
+ y: state.y + bornY,
1245
+ lookRight: state.lookRight,
1246
+ ...definition.bornBehavior && { behaviorName: definition.bornBehavior }
1247
+ }, definition.bornMascot);
1248
+ }
1249
+ }
1250
+ var BreedRuntime = class extends AnimateRuntime {
1251
+ constructor(definition, state, random, callbacks) {
1252
+ super(definition, state, random);
1253
+ this.callbacks = callbacks;
1254
+ }
1255
+ callbacks;
1256
+ spawned = false;
1257
+ tick(context) {
1258
+ const result = super.tick(context);
1259
+ if (result === "lost-ground") return result;
1260
+ const duration = this.animationDuration(context);
1261
+ if (!this.spawned && this.time === duration - 1) {
1262
+ this.spawned = true;
1263
+ breed(this.definition, this.state, this.values, context, this.callbacks);
834
1264
  }
1265
+ return result;
1266
+ }
1267
+ };
1268
+ var BreedMoveRuntime = class extends MoveRuntime {
1269
+ constructor(definition, state, random, callbacks) {
1270
+ super(definition, state, random);
1271
+ this.callbacks = callbacks;
1272
+ }
1273
+ callbacks;
1274
+ tick(context) {
1275
+ const result = super.tick(context);
1276
+ if (result === "lost-ground") return result;
1277
+ const interval = Math.trunc(this.values.number("bornInterval", this.definition.bornInterval, context.environment, 1));
1278
+ if (interval < 1) throw new RangeError("BornInterval must be positive");
1279
+ if (this.time % interval === 0 && !this.turning) breed(this.definition, this.state, this.values, context, this.callbacks);
1280
+ return result;
1281
+ }
1282
+ };
1283
+ var BreedJumpRuntime = class extends JumpRuntime {
1284
+ constructor(definition, breedState, random, callbacks) {
1285
+ super(definition, breedState, random);
1286
+ this.breedState = breedState;
1287
+ this.callbacks = callbacks;
835
1288
  }
836
- currentPose() {
837
- if (!this.animation?.poses.length || this.poseDuration <= 0) return void 0;
838
- let cursor = this.elapsedMs / this.options.frameDuration % this.poseDuration;
839
- for (const pose of this.animation.poses) {
840
- cursor -= pose.duration;
841
- if (cursor < 0) return pose;
1289
+ breedState;
1290
+ callbacks;
1291
+ tick(context) {
1292
+ const result = super.tick(context);
1293
+ const interval = Math.trunc(this.values.number("bornInterval", this.definition.bornInterval, context.environment, 1));
1294
+ if (interval < 1) throw new RangeError("BornInterval must be positive");
1295
+ if (this.time % interval === 0) breed(this.definition, this.breedState, this.values, context, this.callbacks);
1296
+ return result;
1297
+ }
1298
+ };
1299
+ var DraggedRuntime = class extends RuntimeBase {
1300
+ constructor(definition, state, random, spec) {
1301
+ super(definition, random);
1302
+ this.state = state;
1303
+ this.spec = spec;
1304
+ }
1305
+ state;
1306
+ spec;
1307
+ footX = 0;
1308
+ footDx = 0;
1309
+ timeToRegist = 250;
1310
+ onInit(context) {
1311
+ this.footDx = 0;
1312
+ this.timeToRegist = 250;
1313
+ this.footX = context.environment.mascot.environment.cursor.x + this.offsetX(context);
1314
+ }
1315
+ hasMore() {
1316
+ return this.time < this.timeToRegist;
1317
+ }
1318
+ tick(context) {
1319
+ this.state.lookRight = false;
1320
+ this.state.dragging = true;
1321
+ const cursor = context.environment.mascot.environment.cursor;
1322
+ const offsetX = this.offsetX(context);
1323
+ const offsetY = this.offsetY(context);
1324
+ if (Math.abs(cursor.x - this.state.x + offsetX) >= 5) this.time = 0;
1325
+ this.footDx = (this.footDx + (cursor.x - this.footX) * 0.1) * 0.8;
1326
+ this.footX += this.footDx;
1327
+ const environment = { ...context.environment, footX: this.footX };
1328
+ const animation = this.definition.animations?.find((candidate, index) => this.values.boolean(`animation-${index}`, candidate.condition, environment, true));
1329
+ const pose = animation && poseAt(animation, this.time);
1330
+ if (pose) applyPose(this.state, pose);
1331
+ this.state.x = cursor.x + offsetX;
1332
+ this.state.y = cursor.y + offsetY;
1333
+ if (this.time === this.timeToRegist - 1 && this.values.number(`regist-${this.time}`, "#{Math.random()}", environment, 0) >= 0.1) this.timeToRegist += 1;
1334
+ return "running";
1335
+ }
1336
+ offsetX(context) {
1337
+ const offset = Math.trunc(this.values.number("offsetX", this.definition.offsetX, context.environment, 0));
1338
+ return this.definition.offsetType === "Origin" ? -offset + this.spriteCenter().x : offset;
1339
+ }
1340
+ offsetY(context) {
1341
+ const offset = Math.trunc(this.values.number("offsetY", this.definition.offsetY, context.environment, 120));
1342
+ return this.definition.offsetType === "Origin" ? -offset + this.spriteCenter().y : offset;
1343
+ }
1344
+ spriteCenter() {
1345
+ const sprite = this.spec.sprites[this.state.sprite];
1346
+ const width = typeof sprite === "object" ? sprite.width ?? 128 : 128;
1347
+ return { x: this.state.lookRight ? width - this.state.anchorX : this.state.anchorX, y: this.state.anchorY };
1348
+ }
1349
+ };
1350
+ var RegistRuntime = class extends AnimatedRuntime {
1351
+ constructor(definition, state, random, spec) {
1352
+ super(definition, state, random);
1353
+ this.spec = spec;
1354
+ }
1355
+ spec;
1356
+ hasMore(context) {
1357
+ const cursor = context.environment.mascot.environment.cursor;
1358
+ const rawOffset = Math.trunc(this.values.number("offsetX", this.definition.offsetX, context.environment, 0));
1359
+ const sprite = this.spec.sprites[this.state.sprite];
1360
+ const width = typeof sprite === "object" ? sprite.width ?? 128 : 128;
1361
+ const centerX = this.state.lookRight ? width - this.state.anchorX : this.state.anchorX;
1362
+ const offsetX = this.definition.offsetType === "Origin" ? -rawOffset + centerX : rawOffset;
1363
+ return Math.abs(cursor.x - this.state.x + offsetX) < 5;
1364
+ }
1365
+ tick(context) {
1366
+ this.state.dragging = true;
1367
+ this.applyAnimation(context);
1368
+ if (this.time + 1 >= this.animationDuration(context)) {
1369
+ this.state.lookRight = this.values.number(`look-${this.time}`, "#{Math.random()}", context.environment, 0) < 0.5;
1370
+ return "lost-ground";
842
1371
  }
843
- return this.animation.poses.at(-1);
1372
+ return "running";
844
1373
  }
845
- applyPose(pose, frameScale, bounds) {
846
- this.state.sprite = pose.sprite;
847
- this.state.anchorX = pose.anchor.x;
848
- this.state.anchorY = pose.anchor.y;
849
- const direction = this.state.lookRight ? -1 : 1;
850
- this.state.x = clamp(this.state.x + pose.velocity.x * direction * frameScale, bounds.x, bounds.x + bounds.width);
851
- this.state.y = clamp(this.state.y + pose.velocity.y * frameScale, bounds.y, bounds.y + bounds.height);
1374
+ };
1375
+ var SelfDestructRuntime = class extends AnimateRuntime {
1376
+ constructor(definition, state, random, callbacks) {
1377
+ super(definition, state, random);
1378
+ this.callbacks = callbacks;
1379
+ }
1380
+ callbacks;
1381
+ tick(context) {
1382
+ const result = super.tick(context);
1383
+ if (this.time === this.animationDuration(context) - 1) this.callbacks.remove();
1384
+ return result;
1385
+ }
1386
+ };
1387
+ var ComplexRuntime = class extends RuntimeBase {
1388
+ constructor(definition, random, factory, selectOnly) {
1389
+ super(definition, random);
1390
+ this.factory = factory;
1391
+ this.selectOnly = selectOnly;
852
1392
  }
853
- durationMs() {
854
- const frames = this.action.duration ?? this.poseDuration;
855
- return Math.max(this.options.frameDuration, frames * this.options.frameDuration);
1393
+ factory;
1394
+ selectOnly;
1395
+ index = 0;
1396
+ child;
1397
+ selectionMade = false;
1398
+ onInit(context) {
1399
+ this.index = 0;
1400
+ this.child = void 0;
1401
+ this.selectionMade = false;
1402
+ if (this.baseHasNext(context)) this.seek(context);
1403
+ }
1404
+ hasMore(context) {
1405
+ if (!this.selectOnly) this.seek(context);
1406
+ return this.child?.hasNext(context) ?? false;
1407
+ }
1408
+ tick(context) {
1409
+ return this.child?.hasNext(context) ? this.child.step(context) : "running";
1410
+ }
1411
+ seek(context) {
1412
+ const definitions = this.definition.actions ?? [];
1413
+ if (definitions.length === 0) return;
1414
+ for (let guard = 0; guard <= definitions.length; guard += 1) {
1415
+ if (this.child?.hasNext(context)) {
1416
+ this.selectionMade = true;
1417
+ return;
1418
+ }
1419
+ if (this.selectOnly && this.selectionMade) {
1420
+ this.child = void 0;
1421
+ return;
1422
+ }
1423
+ if (this.index >= definitions.length) {
1424
+ if (this.definition.loop !== true) {
1425
+ this.child = void 0;
1426
+ return;
1427
+ }
1428
+ this.index = 0;
1429
+ }
1430
+ const definition = definitions[this.index++];
1431
+ if (!definition) {
1432
+ this.child = void 0;
1433
+ return;
1434
+ }
1435
+ this.child = this.factory(definition, context);
1436
+ this.child.init(context);
1437
+ }
1438
+ this.child = void 0;
856
1439
  }
857
1440
  };
858
1441
  var ActionExecutor = class {
859
- /** Creates an executor bound to one mascot's mutable internal state. */
860
1442
  constructor(spec, state, options, callbacks) {
861
1443
  this.spec = spec;
862
1444
  this.state = state;
863
1445
  this.options = options;
864
1446
  this.callbacks = callbacks;
1447
+ this.random = options.random ?? Math.random;
865
1448
  }
866
1449
  spec;
867
1450
  state;
868
1451
  options;
869
1452
  callbacks;
870
1453
  runtime;
1454
+ accumulator = 0;
1455
+ random;
871
1456
  /** Starts the action whose name matches a selected behavior. */
872
- start(actionName, environment) {
1457
+ start(actionName, environment, _preserveLookRight = false, bounds = environment.mascot.environment.workArea, platforms = []) {
873
1458
  const definition = this.spec.actions.find((action) => action.name === actionName);
874
- this.runtime = definition ? this.createRuntime(definition, environment, /* @__PURE__ */ new Set()) : void 0;
875
- return this.runtime !== void 0;
1459
+ this.accumulator = 0;
1460
+ if (!definition) {
1461
+ this.runtime = void 0;
1462
+ return false;
1463
+ }
1464
+ const context = { environment, bounds, platforms };
1465
+ this.runtime = this.createRuntime(definition, context, /* @__PURE__ */ new Set());
1466
+ this.runtime.init(context);
1467
+ return true;
876
1468
  }
877
- /** Advances the current action and returns true when it has completed. */
878
- tick(deltaMs, environment, bounds) {
879
- return this.runtime?.tick(deltaMs, environment, bounds) ?? true;
1469
+ /** Advances by elapsed milliseconds and reports completion. */
1470
+ tick(deltaMs, environment, bounds, platforms = []) {
1471
+ this.accumulator += Math.max(0, deltaMs);
1472
+ let result = this.runtime?.hasNext({ environment, bounds, platforms }) ? "running" : "complete";
1473
+ while (this.accumulator >= this.options.frameDuration && result === "running") {
1474
+ this.accumulator -= this.options.frameDuration;
1475
+ result = this.step(environment, bounds, platforms);
1476
+ }
1477
+ return result !== "running";
1478
+ }
1479
+ /** Advances exactly one legacy frame. */
1480
+ step(environment, bounds, platforms = []) {
1481
+ const context = { environment, bounds, platforms };
1482
+ if (!this.runtime?.hasNext(context)) return "complete";
1483
+ const result = this.runtime.step(context);
1484
+ if (result === "lost-ground") return result;
1485
+ return this.runtime.hasNext(context) ? "running" : "complete";
1486
+ }
1487
+ /** Returns whether the current action can execute another legacy frame. */
1488
+ hasNext(environment, bounds, platforms = []) {
1489
+ return this.runtime?.hasNext({ environment, bounds, platforms }) ?? false;
1490
+ }
1491
+ /** Retained for source compatibility with the previous collision API. */
1492
+ consumeViewportWallCollision() {
1493
+ return false;
880
1494
  }
881
1495
  /** Cancels the current action tree. */
882
1496
  cancel() {
883
1497
  this.runtime = void 0;
1498
+ this.accumulator = 0;
884
1499
  }
885
- createRuntime(definition, environment, references) {
886
- if (definition.condition && !evaluateExpression(definition.condition, environment, false)) return new CompleteRuntime();
887
- const bounds = environment.mascot.environment.workArea;
888
- const activeIE = environment.mascot.environment.activeIE;
889
- if (!isOnBorder(this.state, bounds, definition.borderType, activeIE.visible ? activeIE : void 0)) return new CompleteRuntime();
1500
+ createRuntime(definition, context, references) {
890
1501
  if (definition.type === "Reference") {
891
- if (!definition.name || references.has(definition.name)) return new CompleteRuntime();
1502
+ if (!definition.name || references.has(definition.name)) return new InstantRuntime(definition, this.state, this.random, "noop");
892
1503
  const referenced = this.spec.actions.find((action) => action.name === definition.name);
893
- if (!referenced) return new CompleteRuntime();
894
- const nextReferences = new Set(references).add(definition.name);
895
- return this.createRuntime({ ...referenced, ...definition, type: referenced.type }, environment, nextReferences);
1504
+ if (!referenced) return new InstantRuntime(definition, this.state, this.random, "noop");
1505
+ return this.createRuntime({ ...referenced, ...definition, type: referenced.type, name: definition.name }, context, new Set(references).add(definition.name));
896
1506
  }
897
- if (definition.type === "Sequence") {
898
- return new SequenceRuntime(definition.actions ?? [], (child, nextEnvironment) => this.createRuntime(child, nextEnvironment, new Set(references)), definition.loop === true);
1507
+ if (definition.type === "Sequence" || definition.type === "Select") {
1508
+ return new ComplexRuntime(definition, this.random, (child, nextContext) => this.createRuntime(child, nextContext, new Set(references)), definition.type === "Select");
899
1509
  }
900
- if (definition.type === "Select") {
901
- const child = definition.actions?.find((candidate) => !candidate.condition || evaluateExpression(candidate.condition, environment, false));
902
- return child ? this.createRuntime(child, environment, new Set(references)) : new CompleteRuntime();
1510
+ if (definition.type === "Stay") return new StayRuntime(definition, this.state, this.random);
1511
+ if (definition.type === "Animate") return new AnimateRuntime(definition, this.state, this.random);
1512
+ if (definition.type === "Move") return new MoveRuntime(definition, this.state, this.random);
1513
+ switch (definition.embedType) {
1514
+ case "Fall":
1515
+ return new FallRuntime(definition, this.state, this.random, this.options.gravity);
1516
+ case "FallWithIE":
1517
+ return new CarryFallRuntime(definition, this.state, this.random, this.options.gravity, this.callbacks);
1518
+ case "Jump":
1519
+ case "ComplexJump":
1520
+ case "ScanJump":
1521
+ case "BroadcastJump":
1522
+ return new JumpRuntime(definition, this.state, this.random);
1523
+ case "WalkWithIE":
1524
+ return new CarryMoveRuntime(definition, this.state, this.random, this.callbacks);
1525
+ case "MoveWithTurn":
1526
+ return new MoveWithTurnRuntime(definition, this.state, this.random);
1527
+ case "ComplexMove":
1528
+ case "ScanMove":
1529
+ case "BroadcastMove":
1530
+ return new MoveRuntime(definition, this.state, this.random);
1531
+ case "Turn":
1532
+ return new TurnRuntime(definition, this.state, this.random);
1533
+ case "Look":
1534
+ return new InstantRuntime(definition, this.state, this.random, "look");
1535
+ case "Offset":
1536
+ return new InstantRuntime(definition, this.state, this.random, "offset");
1537
+ case "Mute":
1538
+ case "Reboot":
1539
+ return new InstantRuntime(definition, this.state, this.random, "noop");
1540
+ case "Breed":
1541
+ return new BreedRuntime(definition, this.state, this.random, this.callbacks);
1542
+ case "BreedMove":
1543
+ return new BreedMoveRuntime(definition, this.state, this.random, this.callbacks);
1544
+ case "BreedJump":
1545
+ return new BreedJumpRuntime(definition, this.state, this.random, this.callbacks);
1546
+ case "ThrowIE":
1547
+ return new ThrowPlatformRuntime(definition, this.state, this.random, this.callbacks);
1548
+ case "SelfDestruct":
1549
+ case "Exit":
1550
+ return new SelfDestructRuntime(definition, this.state, this.random, this.callbacks);
1551
+ case "Dragged":
1552
+ return new DraggedRuntime(definition, this.state, this.random, this.spec);
1553
+ case "Regist":
1554
+ return new RegistRuntime(definition, this.state, this.random, this.spec);
1555
+ case "Broadcast":
1556
+ case "Interact":
1557
+ case "ScanInteract":
1558
+ case "Transform":
1559
+ return new AnimateRuntime(definition, this.state, this.random);
1560
+ case "BroadcastStay":
1561
+ return new StayRuntime(definition, this.state, this.random);
1562
+ default:
1563
+ return new StayRuntime(definition, this.state, this.random);
903
1564
  }
904
- return new LeafRuntime(evaluateAction(definition, environment), this.state, this.options, this.callbacks, environment);
905
1565
  }
906
1566
  };
907
1567
 
@@ -930,7 +1590,6 @@ function environmentRectangle(bounds) {
930
1590
  };
931
1591
  }
932
1592
  var PLATFORM_NEARBY_DISTANCE = 400;
933
- var PLATFORM_EDGE_TOLERANCE = 16;
934
1593
  function distanceToRectangle(point, rectangle) {
935
1594
  const dx = Math.max(rectangle.x - point.x, 0, point.x - rectangle.x - rectangle.width);
936
1595
  const dy = Math.max(rectangle.y - point.y, 0, point.y - rectangle.y - rectangle.height);
@@ -959,10 +1618,14 @@ var Mascot = class {
959
1618
  dragging: false
960
1619
  };
961
1620
  this.domHandle = dom.createMascot(spec, id, options.mascotClassName || void 0);
1621
+ this.frameDuration = options.frameDuration;
1622
+ this.random = options.random ?? Math.random;
1623
+ this.forceInitialFall = spawnOptions.behaviorName === void 0;
962
1624
  this.behavior = new BehaviorController(spec, options.random);
963
1625
  this.actions = new ActionExecutor(spec, this.state, options, {
964
- spawn: (position) => this.callbacks.spawn(this.spec.id, position),
965
- remove: () => this.callbacks.remove(this.id)
1626
+ spawn: (position, characterId) => this.callbacks.spawn(characterId ?? this.spec.id, position),
1627
+ remove: () => this.callbacks.remove(this.id),
1628
+ ...this.callbacks.movePlatform && { movePlatform: this.callbacks.movePlatform }
966
1629
  });
967
1630
  this.installPointerHandlers();
968
1631
  }
@@ -984,44 +1647,20 @@ var Mascot = class {
984
1647
  lastPointer;
985
1648
  activePlatformElement;
986
1649
  platforms = [];
1650
+ accumulatedMs = 0;
1651
+ frameDuration;
1652
+ random;
1653
+ forceInitialFall;
987
1654
  /** Advances behavior, animation, physics, and rendering by one clock tick. */
988
1655
  tick(deltaMs, bounds, platforms = []) {
989
1656
  if (this.destroyed) return;
990
1657
  this.platforms = platforms;
991
- const environment = this.createEnvironment(bounds, platforms);
992
- if (this.state.dragging) {
993
- this.dom.render(this.domHandle, this.spec, this.state);
994
- return;
995
- }
996
1658
  try {
997
- for (let guard = 0; guard < 8; guard += 1) {
998
- if (!this.currentBehavior) {
999
- this.currentBehavior = this.behavior.selectInitial(environment, this.state.behaviorName);
1000
- let started = this.startBehavior(environment);
1001
- if (!started) {
1002
- this.currentBehavior = this.findFallBehavior();
1003
- started = this.startBehavior(environment);
1004
- }
1005
- if (!started) break;
1006
- }
1007
- const activeIE = environment.mascot.environment.activeIE;
1008
- const wasOnPlatformTop = activeIE.visible && activeIE.topBorder.isOn(this.state);
1009
- const completed = this.actions.tick(deltaMs, environment, bounds);
1010
- const remainedNearPlatform = this.state.x >= activeIE.left - PLATFORM_EDGE_TOLERANCE && this.state.x <= activeIE.right + PLATFORM_EDGE_TOLERANCE;
1011
- if (wasOnPlatformTop && !remainedNearPlatform && !platforms.some((platform) => isOnTop(this.state, platform))) {
1012
- this.actions.cancel();
1013
- this.currentBehavior = this.findFallBehavior();
1014
- if (this.currentBehavior) this.startBehavior(this.createEnvironment(bounds, platforms));
1015
- break;
1016
- }
1017
- if (!completed) break;
1018
- if (this.destroyed) return;
1019
- this.currentBehavior = this.behavior.selectNext(this.createEnvironment(bounds, platforms));
1020
- if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds, platforms))) {
1021
- this.currentBehavior = this.findFallBehavior();
1022
- if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds, platforms))) break;
1023
- }
1024
- deltaMs = 0;
1659
+ this.ensureBehavior(bounds, platforms, true);
1660
+ this.accumulatedMs += Math.max(0, deltaMs);
1661
+ while (this.accumulatedMs >= this.frameDuration && !this.destroyed) {
1662
+ this.accumulatedMs -= this.frameDuration;
1663
+ this.legacyTick(bounds, platforms);
1025
1664
  }
1026
1665
  } catch (error) {
1027
1666
  this.callbacks.error(error instanceof Error ? error : new Error(String(error)));
@@ -1042,14 +1681,68 @@ var Mascot = class {
1042
1681
  for (const dispose of this.disposers.splice(0)) dispose();
1043
1682
  this.dom.removeMascot(this.domHandle);
1044
1683
  }
1045
- startBehavior(environment) {
1684
+ startBehavior(environment, bounds, platforms) {
1046
1685
  if (!this.currentBehavior) return false;
1047
1686
  this.state.behaviorName = this.currentBehavior.name;
1048
- return this.actions.start(this.currentBehavior.name, environment);
1687
+ return this.actions.start(this.currentBehavior.actionName ?? this.currentBehavior.name, environment, false, bounds, platforms);
1688
+ }
1689
+ ensureBehavior(bounds, platforms, initial) {
1690
+ for (let guard = 0; guard < 32 && !this.destroyed; guard += 1) {
1691
+ const environment = this.createEnvironment(bounds, platforms);
1692
+ if (!this.currentBehavior) {
1693
+ this.currentBehavior = initial ? this.forceInitialFall ? this.findFallBehavior() ?? this.behavior.selectInitial(environment) : this.behavior.selectInitial(environment, this.state.behaviorName) : this.selectNextBehavior(environment, bounds);
1694
+ initial = false;
1695
+ if (!this.currentBehavior) this.currentBehavior = this.findFallBehavior();
1696
+ if (!this.currentBehavior || !this.startBehavior(environment, bounds, platforms)) return;
1697
+ }
1698
+ if (this.actions.hasNext(environment, bounds, platforms)) return;
1699
+ this.currentBehavior = this.selectNextBehavior(environment, bounds) ?? this.findFallBehavior();
1700
+ if (!this.currentBehavior) return;
1701
+ if (!this.startBehavior(this.createEnvironment(bounds, platforms), bounds, platforms)) return;
1702
+ }
1703
+ }
1704
+ legacyTick(bounds, platforms) {
1705
+ this.ensureBehavior(bounds, platforms, false);
1706
+ if (!this.currentBehavior) return;
1707
+ const result = this.actions.step(this.createEnvironment(bounds, platforms), bounds, platforms);
1708
+ if (this.destroyed) return;
1709
+ if (result === "lost-ground") {
1710
+ this.state.dragging = false;
1711
+ this.actions.cancel();
1712
+ this.currentBehavior = this.findFallBehavior();
1713
+ if (this.currentBehavior) this.startBehavior(this.createEnvironment(bounds, platforms), bounds, platforms);
1714
+ } else if (result === "complete") {
1715
+ this.currentBehavior = this.selectNextBehavior(this.createEnvironment(bounds, platforms), bounds);
1716
+ if (this.currentBehavior) this.startBehavior(this.createEnvironment(bounds, platforms), bounds, platforms);
1717
+ this.ensureBehavior(bounds, platforms, false);
1718
+ } else if (this.isOutsideVisibleBounds(bounds)) {
1719
+ this.state.x = Math.trunc(bounds.x + this.random() * bounds.width);
1720
+ this.state.y = bounds.y - 256;
1721
+ this.actions.cancel();
1722
+ this.currentBehavior = this.findFallBehavior();
1723
+ if (this.currentBehavior) this.startBehavior(this.createEnvironment(bounds, platforms), bounds, platforms);
1724
+ }
1725
+ }
1726
+ isOutsideVisibleBounds(bounds) {
1727
+ const sprite = this.spec.sprites[this.state.sprite];
1728
+ const width = typeof sprite === "object" && "width" in sprite && sprite.width !== void 0 ? sprite.width : 128;
1729
+ const height = typeof sprite === "object" && "height" in sprite && sprite.height !== void 0 ? sprite.height : 128;
1730
+ const anchorX = this.state.lookRight ? width - this.state.anchorX : this.state.anchorX;
1731
+ const left = this.state.x - anchorX;
1732
+ const top = this.state.y - this.state.anchorY;
1733
+ return left + width <= bounds.x || bounds.x + bounds.width <= left || bounds.y + bounds.height <= top;
1049
1734
  }
1050
1735
  findFallBehavior() {
1051
1736
  return this.behavior.force("Fall") ?? this.behavior.force("\u843D\u4E0B\u3059\u308B");
1052
1737
  }
1738
+ selectNextBehavior(environment, bounds) {
1739
+ const selected = this.behavior.selectNext(environment);
1740
+ if (this.behavior.usedFallback()) {
1741
+ this.state.x = Math.trunc(bounds.x + this.random() * bounds.width);
1742
+ this.state.y = bounds.y - 256;
1743
+ }
1744
+ return selected;
1745
+ }
1053
1746
  createEnvironment(bounds, platforms = this.platforms) {
1054
1747
  const workArea = environmentRectangle(bounds);
1055
1748
  const inactive = environmentRectangle({ x: -100, y: -100, width: 0, height: 0 });
@@ -1064,10 +1757,10 @@ var Mascot = class {
1064
1757
  lookRight: this.state.lookRight,
1065
1758
  environment: {
1066
1759
  cursor: this.callbacks.pointer(),
1067
- screen: { width: window.innerWidth, height: window.innerHeight },
1760
+ screen: workArea,
1068
1761
  workArea,
1069
- floor: workArea.bottomBorder,
1070
- ceiling: workArea.topBorder,
1762
+ floor: edge((point) => isOnFloor(point, bounds, platforms)),
1763
+ ceiling: edge((point) => isOnTop(point, bounds) || platforms.some((candidate) => isOnBottom(point, candidate))),
1071
1764
  activeIE
1072
1765
  }
1073
1766
  }
@@ -1094,6 +1787,7 @@ var Mascot = class {
1094
1787
  }
1095
1788
  installPointerHandlers() {
1096
1789
  const element = this.domHandle.spriteElement;
1790
+ const document2 = element.ownerDocument;
1097
1791
  const listen = (target, type, listener) => {
1098
1792
  target.addEventListener(type, listener);
1099
1793
  this.disposers.push(() => target.removeEventListener(type, listener));
@@ -1102,29 +1796,29 @@ var Mascot = class {
1102
1796
  const pointerEvent = event;
1103
1797
  if (pointerEvent.button !== 0) return;
1104
1798
  event.preventDefault();
1105
- const point = { x: pointerEvent.clientX, y: pointerEvent.clientY };
1799
+ const point = this.dom.toLocalPoint(pointerEvent.clientX, pointerEvent.clientY);
1106
1800
  this.pointerId = pointerEvent.pointerId;
1107
1801
  this.pointerDown = point;
1108
1802
  this.lastPointer = point;
1109
1803
  this.dragOffset = { x: this.state.x - point.x, y: this.state.y - point.y };
1110
1804
  this.state.dragging = true;
1111
- this.actions.cancel();
1112
1805
  this.currentBehavior = this.behavior.force("Dragged") ?? this.behavior.force("\u30C9\u30E9\u30C3\u30B0\u3055\u308C\u308B");
1113
- if (this.currentBehavior) this.state.behaviorName = this.currentBehavior.name;
1806
+ if (this.currentBehavior) this.startBehavior(this.createEnvironment(this.dom.getBounds()), this.dom.getBounds(), this.platforms);
1114
1807
  element.setPointerCapture?.(pointerEvent.pointerId);
1115
1808
  });
1116
- listen(document, "pointermove", (event) => {
1809
+ listen(document2, "pointermove", (event) => {
1117
1810
  const pointerEvent = event;
1118
1811
  if (!this.state.dragging || pointerEvent.pointerId !== this.pointerId) return;
1119
- const point = { x: pointerEvent.clientX, y: pointerEvent.clientY };
1812
+ const point = this.dom.toLocalPoint(pointerEvent.clientX, pointerEvent.clientY);
1120
1813
  const previous = this.lastPointer ?? point;
1814
+ const bounds = this.dom.getBounds();
1121
1815
  this.state.vx = (point.x - previous.x) * 0.8;
1122
1816
  this.state.vy = (point.y - previous.y) * 0.8;
1123
1817
  this.state.x = point.x + this.dragOffset.x;
1124
1818
  this.state.y = point.y + this.dragOffset.y;
1125
1819
  this.lastPointer = point;
1126
1820
  });
1127
- listen(document, "pointerup", (event) => {
1821
+ listen(document2, "pointerup", (event) => {
1128
1822
  const pointerEvent = event;
1129
1823
  if (!this.state.dragging || pointerEvent.pointerId !== this.pointerId) return;
1130
1824
  this.state.dragging = false;
@@ -1132,8 +1826,8 @@ var Mascot = class {
1132
1826
  this.pointerId = void 0;
1133
1827
  this.currentBehavior = this.behavior.force("Thrown") ?? this.behavior.force("\u6295\u3052\u3089\u308C\u308B") ?? this.findFallBehavior();
1134
1828
  if (this.currentBehavior) {
1135
- this.state.behaviorName = this.currentBehavior.name;
1136
- this.actions.start(this.currentBehavior.name, this.createEnvironment(this.dom.getBounds()));
1829
+ const bounds = this.dom.getBounds();
1830
+ this.startBehavior(this.createEnvironment(bounds), bounds, this.platforms);
1137
1831
  }
1138
1832
  if (moved < 4) this.callbacks.click(this.snapshot());
1139
1833
  });
@@ -1141,21 +1835,23 @@ var Mascot = class {
1141
1835
  };
1142
1836
 
1143
1837
  // src/platform.ts
1144
- function resolvePlatformElements(source, document2, excludedRoot) {
1838
+ function resolvePlatformElements(source, root, excludedRoot) {
1145
1839
  let elements;
1146
1840
  if (typeof source === "string") {
1147
1841
  try {
1148
- elements = [...document2.querySelectorAll(source)];
1842
+ elements = [...root.querySelectorAll(source)];
1149
1843
  } catch {
1150
1844
  return [];
1151
1845
  }
1152
1846
  } else {
1153
1847
  elements = source;
1154
1848
  }
1849
+ const document2 = root.nodeType === 9 ? root : root.ownerDocument;
1850
+ if (!document2) return [];
1155
1851
  const HTMLElementConstructor = document2.defaultView?.HTMLElement;
1156
1852
  if (!HTMLElementConstructor) return [];
1157
1853
  return [...new Set(elements)].filter(
1158
- (element) => element instanceof HTMLElementConstructor && element.isConnected && (!excludedRoot || !excludedRoot.contains(element))
1854
+ (element) => element instanceof HTMLElementConstructor && element.isConnected && root.contains(element) && (!excludedRoot || !excludedRoot.contains(element))
1159
1855
  );
1160
1856
  }
1161
1857
  function readPlatformRectangles(elements, workAreaRectangle) {
@@ -1283,21 +1979,26 @@ var ShimejiEngine = class {
1283
1979
  destroyed = false;
1284
1980
  initialized = false;
1285
1981
  platformSource;
1286
- /** Starts the clock and global listeners. Calling this method more than once is harmless. */
1982
+ additionalPlatformElements = [];
1983
+ movedPlatforms = /* @__PURE__ */ new Map();
1984
+ platformRectangles = /* @__PURE__ */ new Map();
1985
+ /** Starts the clock and container-aware listeners. Calling this method more than once is harmless. */
1287
1986
  initialize() {
1288
1987
  this.assertAlive();
1289
1988
  if (this.initialized) return;
1290
1989
  this.initialized = true;
1291
- this.listen(document, "pointermove", (event) => {
1990
+ const document2 = this.container.ownerDocument;
1991
+ const view = document2.defaultView;
1992
+ if (!view) throw new Error("ShimejiEngine requires a container connected to a window");
1993
+ this.listen(document2, "pointermove", (event) => {
1292
1994
  const pointerEvent = event;
1293
- const x = pointerEvent.clientX;
1294
- const y = pointerEvent.clientY;
1995
+ const { x, y } = this.dom.toLocalPoint(pointerEvent.clientX, pointerEvent.clientY);
1295
1996
  this.pointer = { x, y, dx: x - this.pointer.x, dy: y - this.pointer.y };
1296
1997
  });
1297
- this.listen(window, "resize", () => this.renderAll());
1298
- const maintenance = window.setInterval(() => this.dom.ensureMounted(), 2e3);
1998
+ this.listen(view, "resize", () => this.renderAll());
1999
+ const maintenance = view.setInterval(() => this.dom.ensureMounted(), 2e3);
1299
2000
  this.intervals.add(maintenance);
1300
- this.animationFrame = requestAnimationFrame(this.onAnimationFrame);
2001
+ this.animationFrame = view.requestAnimationFrame(this.onAnimationFrame);
1301
2002
  }
1302
2003
  /** Registers or replaces a parsed or legacy character specification. */
1303
2004
  registerCharacter(spec) {
@@ -1325,10 +2026,14 @@ var ShimejiEngine = class {
1325
2026
  if (!spec) throw new Error(`Character '${characterId}' is not registered`);
1326
2027
  const { bounds, platforms } = this.readFrameGeometry();
1327
2028
  const random = this.options.random ?? Math.random;
2029
+ const randomX = Math.trunc(bounds.x + random() * bounds.width);
2030
+ const spawnInset = Math.min(2, bounds.width / 2);
1328
2031
  const spawnOptions = {
1329
2032
  ...position,
1330
- x: position.x ?? bounds.x + random() * bounds.width,
1331
- y: position.y ?? bounds.y
2033
+ // Fall treats the wall in the facing direction as ground. Keep implicit
2034
+ // spawns clear of both side-wall tolerances so even random() === 0 falls.
2035
+ x: position.x ?? Math.min(Math.max(randomX, bounds.x + spawnInset), bounds.x + bounds.width - spawnInset),
2036
+ y: position.y ?? bounds.y + 2
1332
2037
  };
1333
2038
  const id = `shimeji-${this.nextMascotId++}`;
1334
2039
  const mascot = new Mascot(id, spec, this.dom, this.options, spawnOptions, {
@@ -1340,6 +2045,7 @@ var ShimejiEngine = class {
1340
2045
  remove: (mascotId) => {
1341
2046
  if (!this.destroyed) this.remove(mascotId);
1342
2047
  },
2048
+ movePlatform: (element, point) => this.movePlatform(element, point),
1343
2049
  click: (state2) => this.events.emit("click", state2),
1344
2050
  error: (error) => this.events.emit("error", error)
1345
2051
  });
@@ -1376,22 +2082,27 @@ var ShimejiEngine = class {
1376
2082
  this.assertAlive();
1377
2083
  return this.events.on(event, listener);
1378
2084
  }
1379
- /** Replaces the DOM elements (or selector) exposed to mascots as platforms. */
1380
- setPlatforms(platforms) {
2085
+ /** Replaces the primary platform source and any additional registered elements. */
2086
+ setPlatforms(platforms, additionalPlatforms = []) {
1381
2087
  this.assertAlive();
1382
2088
  this.platformSource = platforms;
2089
+ this.additionalPlatformElements = additionalPlatforms;
1383
2090
  }
1384
2091
  /** Stops animation and timers, removes listeners and DOM, and revokes all object URLs. */
1385
2092
  destroy() {
1386
2093
  if (this.destroyed) return;
1387
2094
  for (const mascot of this.mascots.values()) mascot.destroy();
1388
2095
  this.mascots.clear();
1389
- if (this.animationFrame !== void 0) cancelAnimationFrame(this.animationFrame);
2096
+ const view = this.container.ownerDocument.defaultView;
2097
+ if (this.animationFrame !== void 0) view?.cancelAnimationFrame(this.animationFrame);
1390
2098
  this.animationFrame = void 0;
1391
- for (const interval of this.intervals) window.clearInterval(interval);
2099
+ for (const interval of this.intervals) view?.clearInterval(interval);
1392
2100
  this.intervals.clear();
1393
2101
  for (const dispose of this.disposers.splice(0)) dispose();
1394
2102
  this.dom.destroy();
2103
+ for (const [element, movement] of this.movedPlatforms) element.style.transform = movement.originalTransform;
2104
+ this.movedPlatforms.clear();
2105
+ this.platformRectangles.clear();
1395
2106
  this.sprites.destroy();
1396
2107
  this.specs.clear();
1397
2108
  this.events.clear();
@@ -1410,7 +2121,7 @@ var ShimejiEngine = class {
1410
2121
  const { bounds, platforms } = this.readFrameGeometry();
1411
2122
  for (const mascot of [...this.mascots.values()]) mascot.tick(delta, bounds, platforms);
1412
2123
  this.emitState();
1413
- this.animationFrame = requestAnimationFrame(this.onAnimationFrame);
2124
+ this.animationFrame = this.container.ownerDocument.defaultView?.requestAnimationFrame(this.onAnimationFrame);
1414
2125
  };
1415
2126
  renderAll() {
1416
2127
  const { bounds, platforms } = this.readFrameGeometry();
@@ -1418,8 +2129,26 @@ var ShimejiEngine = class {
1418
2129
  }
1419
2130
  readFrameGeometry() {
1420
2131
  const bounds = this.dom.getBounds();
1421
- const elements = resolvePlatformElements(this.platformSource, this.container.ownerDocument).filter((element) => !this.dom.owns(element));
1422
- return { bounds, platforms: readPlatformRectangles(elements, { left: 0, top: 0 }) };
2132
+ const elements = [.../* @__PURE__ */ new Set([
2133
+ ...resolvePlatformElements(this.platformSource, this.container),
2134
+ ...resolvePlatformElements(this.additionalPlatformElements, this.container)
2135
+ ])].filter((element) => !this.dom.owns(element));
2136
+ const platforms = readPlatformRectangles(elements, this.container.getBoundingClientRect());
2137
+ this.platformRectangles.clear();
2138
+ for (const platform of platforms) this.platformRectangles.set(platform.element, platform);
2139
+ return { bounds, platforms };
2140
+ }
2141
+ movePlatform(element, point) {
2142
+ const rectangle = this.platformRectangles.get(element);
2143
+ if (!rectangle) return;
2144
+ const movement = this.movedPlatforms.get(element) ?? { originalTransform: element.style.transform, x: 0, y: 0 };
2145
+ movement.x += point.x - rectangle.x;
2146
+ movement.y += point.y - rectangle.y;
2147
+ const translate = `translate(${movement.x}px, ${movement.y}px)`;
2148
+ element.style.transform = movement.originalTransform ? `${movement.originalTransform} ${translate}` : translate;
2149
+ rectangle.x = point.x;
2150
+ rectangle.y = point.y;
2151
+ this.movedPlatforms.set(element, movement);
1423
2152
  }
1424
2153
  emitState() {
1425
2154
  this.events.emit("statechange", this.getState());