@react-shimeji/core 0.2.3 → 0.3.0

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