@react-shimeji/core 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -223,6 +223,8 @@ interface ShimejiEngineOptions {
223
223
  workAreaClassName?: string;
224
224
  /** CSS class added to each generated mascot element. */
225
225
  mascotClassName?: string;
226
+ /** DOM elements, or a selector for elements, that mascots can use as platforms. */
227
+ platforms?: string | readonly HTMLElement[];
226
228
  /** Optional deterministic random-number source. */
227
229
  random?: () => number;
228
230
  }
@@ -269,7 +271,7 @@ interface MascotEnvironment {
269
271
  floor: EnvironmentEdge;
270
272
  /** Alias for the work-area top edge. */
271
273
  ceiling: EnvironmentEdge;
272
- /** Inactive compatibility rectangle for page-element actions. */
274
+ /** Most relevant DOM platform for page-element actions. */
273
275
  activeIE: EnvironmentRectangle & {
274
276
  visible: boolean;
275
277
  };
@@ -332,6 +334,7 @@ declare class ShimejiEngine {
332
334
  private nextMascotId;
333
335
  private destroyed;
334
336
  private initialized;
337
+ private platformSource;
335
338
  /** Creates and initializes an engine inside a host DOM element. */
336
339
  constructor(container: HTMLElement, options?: ShimejiEngineOptions);
337
340
  /** Starts the clock and global listeners. Calling this method more than once is harmless. */
@@ -352,12 +355,15 @@ declare class ShimejiEngine {
352
355
  getState(): MascotState[];
353
356
  /** Subscribes to a typed engine event and returns an unsubscribe function. */
354
357
  on<K extends keyof ShimejiEngineEventMap>(event: K, listener: ShimejiEventListener<K>): () => void;
358
+ /** Replaces the DOM elements (or selector) exposed to mascots as platforms. */
359
+ setPlatforms(platforms: string | readonly HTMLElement[]): void;
355
360
  /** Stops animation and timers, removes listeners and DOM, and revokes all object URLs. */
356
361
  destroy(): void;
357
362
  /** Returns whether this engine has completed permanent teardown. */
358
363
  isDestroyed(): boolean;
359
364
  private readonly onAnimationFrame;
360
365
  private renderAll;
366
+ private readFrameGeometry;
361
367
  private emitState;
362
368
  private listen;
363
369
  private assertAlive;
@@ -446,12 +452,21 @@ declare function isOnLeft(point: Point, rectangle: Rectangle, tolerance?: number
446
452
  /** Returns whether a point lies on the right edge of a rectangle. */
447
453
  declare function isOnRight(point: Point, rectangle: Rectangle, tolerance?: number): boolean;
448
454
  /** Returns whether an anchor satisfies an action's boundary requirement. */
449
- declare function isOnBorder(state: MascotState, bounds: Rectangle, border: "Floor" | "Wall" | "Ceiling" | undefined): boolean;
455
+ declare function isOnBorder(state: MascotState, bounds: Rectangle, border: "Floor" | "Wall" | "Ceiling" | undefined, platform?: Rectangle): boolean;
450
456
  /** Advances ballistic motion and clamps the mascot to the work-area boundaries. */
451
- declare function applyGravity(state: MascotState, bounds: Rectangle, frameScale: number, gravity: number, resistanceX?: number, resistanceY?: number): boolean;
457
+ declare function applyGravity(state: MascotState, bounds: Rectangle, frameScale: number, gravity: number, resistanceX?: number, resistanceY?: number, platform?: Rectangle): boolean;
452
458
  /** Moves a mascot toward a target without overshooting it. */
453
459
  declare function moveToward(state: MascotState, target: Point, speed: number, frameScale: number): boolean;
454
460
 
461
+ /** A platform rectangle paired with the DOM element that produced it. */
462
+ interface PlatformRectangle extends Rectangle {
463
+ element: HTMLElement;
464
+ }
465
+ /** Resolves a platform option into DOM elements, excluding engine-owned nodes. */
466
+ declare function resolvePlatformElements(source: string | readonly HTMLElement[], document: Document, excludedRoot?: HTMLElement): HTMLElement[];
467
+ /** Reads platform bounds once and converts viewport coordinates to work-area coordinates. */
468
+ declare function readPlatformRectangles(elements: readonly HTMLElement[], workAreaRectangle: Pick<DOMRect, "left" | "top">): PlatformRectangle[];
469
+
455
470
  /** A resolved image and optional atlas crop for one sprite frame. */
456
471
  interface ResolvedSprite {
457
472
  /** Browser-loadable image URL. */
@@ -508,4 +523,4 @@ interface MascotCallbacks {
508
523
  error(error: Error): void;
509
524
  }
510
525
 
511
- export { type ActionDefinition, ActionExecutor, type ActionExecutorCallbacks, type ActionExecutorOptions, type ActionType, type AnimationDefinition, BehaviorController, type BehaviorDefinition, type BehaviorType, type BorderType, type CharacterMetadata, type CharacterSource, type CharacterSpec, type EnvironmentEdge, type EnvironmentRectangle, type IndividualSprite, type LegacyCharacterPack, type MascotCallbacks, type MascotDomHandle, type MascotEnvironment, type MascotState, type Point, type Pose, type Rectangle, type ResolvedSprite, ShimejiEngine, type ShimejiEngineEventMap, type ShimejiEngineOptions, type ShimejiEventListener, type SpawnOptions, type SpriteLease, SpriteManager, type SpriteMap, type SpriteRectangle, applyGravity, clamp, conditionsMatch, evaluateExpression, isIndividualSprite, isOnBorder, isOnBottom, isOnLeft, isOnRight, isOnTop, loadCharacter, moveToward, normalizeCharacterSpec, parseActionsXml, parseBehaviorsXml, selectWeighted };
526
+ export { type ActionDefinition, ActionExecutor, type ActionExecutorCallbacks, type ActionExecutorOptions, type ActionType, type AnimationDefinition, BehaviorController, type BehaviorDefinition, type BehaviorType, type BorderType, type CharacterMetadata, type CharacterSource, type CharacterSpec, type EnvironmentEdge, type EnvironmentRectangle, type IndividualSprite, type LegacyCharacterPack, type MascotCallbacks, type MascotDomHandle, type MascotEnvironment, type MascotState, type PlatformRectangle, type Point, type Pose, type Rectangle, type ResolvedSprite, ShimejiEngine, type ShimejiEngineEventMap, type ShimejiEngineOptions, type ShimejiEventListener, type SpawnOptions, type SpriteLease, SpriteManager, type SpriteMap, type SpriteRectangle, applyGravity, clamp, conditionsMatch, evaluateExpression, isIndividualSprite, isOnBorder, isOnBottom, isOnLeft, isOnRight, isOnTop, loadCharacter, moveToward, normalizeCharacterSpec, parseActionsXml, parseBehaviorsXml, readPlatformRectangles, resolvePlatformElements, selectWeighted };
package/dist/index.d.ts CHANGED
@@ -223,6 +223,8 @@ interface ShimejiEngineOptions {
223
223
  workAreaClassName?: string;
224
224
  /** CSS class added to each generated mascot element. */
225
225
  mascotClassName?: string;
226
+ /** DOM elements, or a selector for elements, that mascots can use as platforms. */
227
+ platforms?: string | readonly HTMLElement[];
226
228
  /** Optional deterministic random-number source. */
227
229
  random?: () => number;
228
230
  }
@@ -269,7 +271,7 @@ interface MascotEnvironment {
269
271
  floor: EnvironmentEdge;
270
272
  /** Alias for the work-area top edge. */
271
273
  ceiling: EnvironmentEdge;
272
- /** Inactive compatibility rectangle for page-element actions. */
274
+ /** Most relevant DOM platform for page-element actions. */
273
275
  activeIE: EnvironmentRectangle & {
274
276
  visible: boolean;
275
277
  };
@@ -332,6 +334,7 @@ declare class ShimejiEngine {
332
334
  private nextMascotId;
333
335
  private destroyed;
334
336
  private initialized;
337
+ private platformSource;
335
338
  /** Creates and initializes an engine inside a host DOM element. */
336
339
  constructor(container: HTMLElement, options?: ShimejiEngineOptions);
337
340
  /** Starts the clock and global listeners. Calling this method more than once is harmless. */
@@ -352,12 +355,15 @@ declare class ShimejiEngine {
352
355
  getState(): MascotState[];
353
356
  /** Subscribes to a typed engine event and returns an unsubscribe function. */
354
357
  on<K extends keyof ShimejiEngineEventMap>(event: K, listener: ShimejiEventListener<K>): () => void;
358
+ /** Replaces the DOM elements (or selector) exposed to mascots as platforms. */
359
+ setPlatforms(platforms: string | readonly HTMLElement[]): void;
355
360
  /** Stops animation and timers, removes listeners and DOM, and revokes all object URLs. */
356
361
  destroy(): void;
357
362
  /** Returns whether this engine has completed permanent teardown. */
358
363
  isDestroyed(): boolean;
359
364
  private readonly onAnimationFrame;
360
365
  private renderAll;
366
+ private readFrameGeometry;
361
367
  private emitState;
362
368
  private listen;
363
369
  private assertAlive;
@@ -446,12 +452,21 @@ declare function isOnLeft(point: Point, rectangle: Rectangle, tolerance?: number
446
452
  /** Returns whether a point lies on the right edge of a rectangle. */
447
453
  declare function isOnRight(point: Point, rectangle: Rectangle, tolerance?: number): boolean;
448
454
  /** Returns whether an anchor satisfies an action's boundary requirement. */
449
- declare function isOnBorder(state: MascotState, bounds: Rectangle, border: "Floor" | "Wall" | "Ceiling" | undefined): boolean;
455
+ declare function isOnBorder(state: MascotState, bounds: Rectangle, border: "Floor" | "Wall" | "Ceiling" | undefined, platform?: Rectangle): boolean;
450
456
  /** Advances ballistic motion and clamps the mascot to the work-area boundaries. */
451
- declare function applyGravity(state: MascotState, bounds: Rectangle, frameScale: number, gravity: number, resistanceX?: number, resistanceY?: number): boolean;
457
+ declare function applyGravity(state: MascotState, bounds: Rectangle, frameScale: number, gravity: number, resistanceX?: number, resistanceY?: number, platform?: Rectangle): boolean;
452
458
  /** Moves a mascot toward a target without overshooting it. */
453
459
  declare function moveToward(state: MascotState, target: Point, speed: number, frameScale: number): boolean;
454
460
 
461
+ /** A platform rectangle paired with the DOM element that produced it. */
462
+ interface PlatformRectangle extends Rectangle {
463
+ element: HTMLElement;
464
+ }
465
+ /** Resolves a platform option into DOM elements, excluding engine-owned nodes. */
466
+ declare function resolvePlatformElements(source: string | readonly HTMLElement[], document: Document, excludedRoot?: HTMLElement): HTMLElement[];
467
+ /** Reads platform bounds once and converts viewport coordinates to work-area coordinates. */
468
+ declare function readPlatformRectangles(elements: readonly HTMLElement[], workAreaRectangle: Pick<DOMRect, "left" | "top">): PlatformRectangle[];
469
+
455
470
  /** A resolved image and optional atlas crop for one sprite frame. */
456
471
  interface ResolvedSprite {
457
472
  /** Browser-loadable image URL. */
@@ -508,4 +523,4 @@ interface MascotCallbacks {
508
523
  error(error: Error): void;
509
524
  }
510
525
 
511
- export { type ActionDefinition, ActionExecutor, type ActionExecutorCallbacks, type ActionExecutorOptions, type ActionType, type AnimationDefinition, BehaviorController, type BehaviorDefinition, type BehaviorType, type BorderType, type CharacterMetadata, type CharacterSource, type CharacterSpec, type EnvironmentEdge, type EnvironmentRectangle, type IndividualSprite, type LegacyCharacterPack, type MascotCallbacks, type MascotDomHandle, type MascotEnvironment, type MascotState, type Point, type Pose, type Rectangle, type ResolvedSprite, ShimejiEngine, type ShimejiEngineEventMap, type ShimejiEngineOptions, type ShimejiEventListener, type SpawnOptions, type SpriteLease, SpriteManager, type SpriteMap, type SpriteRectangle, applyGravity, clamp, conditionsMatch, evaluateExpression, isIndividualSprite, isOnBorder, isOnBottom, isOnLeft, isOnRight, isOnTop, loadCharacter, moveToward, normalizeCharacterSpec, parseActionsXml, parseBehaviorsXml, selectWeighted };
526
+ export { type ActionDefinition, ActionExecutor, type ActionExecutorCallbacks, type ActionExecutorOptions, type ActionType, type AnimationDefinition, BehaviorController, type BehaviorDefinition, type BehaviorType, type BorderType, type CharacterMetadata, type CharacterSource, type CharacterSpec, type EnvironmentEdge, type EnvironmentRectangle, type IndividualSprite, type LegacyCharacterPack, type MascotCallbacks, type MascotDomHandle, type MascotEnvironment, type MascotState, type PlatformRectangle, type Point, type Pose, type Rectangle, type ResolvedSprite, ShimejiEngine, type ShimejiEngineEventMap, type ShimejiEngineOptions, type ShimejiEventListener, type SpawnOptions, type SpriteLease, SpriteManager, type SpriteMap, type SpriteRectangle, applyGravity, clamp, conditionsMatch, evaluateExpression, isIndividualSprite, isOnBorder, isOnBottom, isOnLeft, isOnRight, isOnTop, loadCharacter, moveToward, normalizeCharacterSpec, parseActionsXml, parseBehaviorsXml, readPlatformRectangles, resolvePlatformElements, selectWeighted };
package/dist/index.js CHANGED
@@ -29,7 +29,10 @@ var DomManager = class {
29
29
  }
30
30
  /** Returns the current local work-area rectangle. */
31
31
  getBounds() {
32
- const rectangle = this.workArea.getBoundingClientRect();
32
+ return this.getBoundsFromClientRectangle(this.workArea.getBoundingClientRect());
33
+ }
34
+ /** Converts a previously-read work-area client rectangle into local bounds. */
35
+ getBoundsFromClientRectangle(rectangle) {
33
36
  const width = rectangle.width || this.container.clientWidth || window.innerWidth;
34
37
  const height = rectangle.height || this.container.clientHeight || window.innerHeight;
35
38
  return { x: 0, y: 0, width, height };
@@ -597,7 +600,8 @@ var BehaviorController = class {
597
600
  isOnAnyBoundary(environment) {
598
601
  const anchor = environment.mascot.anchor;
599
602
  const area = environment.mascot.environment.workArea;
600
- return area.topBorder.isOn(anchor) || area.leftBorder.isOn(anchor) || area.rightBorder.isOn(anchor) || area.bottomBorder.isOn(anchor);
603
+ const activeIE = environment.mascot.environment.activeIE;
604
+ 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));
601
605
  }
602
606
  };
603
607
 
@@ -617,17 +621,25 @@ function isOnLeft(point, rectangle, tolerance = 1) {
617
621
  function isOnRight(point, rectangle, tolerance = 1) {
618
622
  return point.y >= rectangle.y - tolerance && point.y <= rectangle.y + rectangle.height + tolerance && Math.abs(point.x - rectangle.x - rectangle.width) <= tolerance;
619
623
  }
620
- function isOnBorder(state, bounds, border) {
624
+ function isOnBorder(state, bounds, border, platform) {
621
625
  if (!border) return true;
622
- if (border === "Floor") return isOnBottom(state, bounds);
623
- if (border === "Ceiling") return isOnTop(state, bounds);
624
- return isOnLeft(state, bounds) || isOnRight(state, bounds);
626
+ if (border === "Floor") return isOnBottom(state, bounds) || platform !== void 0 && isOnTop(state, platform);
627
+ if (border === "Ceiling") return isOnTop(state, bounds) || platform !== void 0 && isOnBottom(state, platform);
628
+ return isOnLeft(state, bounds) || isOnRight(state, bounds) || platform !== void 0 && (isOnLeft(state, platform) || isOnRight(state, platform));
625
629
  }
626
- function applyGravity(state, bounds, frameScale, gravity, resistanceX = 0.05, resistanceY = 0.01) {
627
- state.x = clamp(state.x + state.vx * frameScale, bounds.x, bounds.x + bounds.width);
628
- state.y = clamp(state.y + state.vy * frameScale, bounds.y, bounds.y + bounds.height);
630
+ function applyGravity(state, bounds, frameScale, gravity, resistanceX = 0.05, resistanceY = 0.01, platform) {
631
+ const previousY = state.y;
632
+ const nextX = clamp(state.x + state.vx * frameScale, bounds.x, bounds.x + bounds.width);
633
+ const nextY = clamp(state.y + state.vy * frameScale, bounds.y, bounds.y + bounds.height);
634
+ state.x = nextX;
635
+ state.y = nextY;
629
636
  state.vx *= Math.max(0, 1 - resistanceX * frameScale);
630
637
  state.vy = state.vy * Math.max(0, 1 - resistanceY * frameScale) + gravity * frameScale;
638
+ if (platform && nextY >= previousY && previousY <= platform.y && nextY >= platform.y && nextX >= platform.x && nextX <= platform.x + platform.width) {
639
+ state.y = platform.y;
640
+ state.vy = 0;
641
+ return true;
642
+ }
631
643
  if (state.y >= bounds.y + bounds.height) {
632
644
  state.y = bounds.y + bounds.height;
633
645
  state.vy = 0;
@@ -664,7 +676,8 @@ function evaluateAction(definition, environment) {
664
676
  const initialVx = numeric(definition.initialVx, scopedEnvironment);
665
677
  let lookRight = environment.mascot.lookRight;
666
678
  if (definition.borderType === "Wall") {
667
- lookRight = environment.mascot.environment.workArea.rightBorder.isOn(environment.mascot.anchor);
679
+ const { activeIE, workArea } = environment.mascot.environment;
680
+ lookRight = workArea.rightBorder.isOn(environment.mascot.anchor) || activeIE.visible && activeIE.leftBorder.isOn(environment.mascot.anchor);
668
681
  } else if (definition.type === "Move" || definition.embedType === "Jump" || definition.embedType === "WalkWithIE") {
669
682
  if (targetX !== void 0) lookRight = targetX > environment.mascot.anchor.x;
670
683
  } else if (definition.embedType === "Fall" || definition.embedType === "FallWithIE") {
@@ -792,7 +805,15 @@ var LeafRuntime = class {
792
805
  case "Fall":
793
806
  case "FallWithIE":
794
807
  case "Thrown":
795
- return applyGravity(this.state, bounds, frameScale, this.action.gravity ?? this.options.gravity, this.action.resistanceX, this.action.resistanceY);
808
+ return applyGravity(
809
+ this.state,
810
+ bounds,
811
+ frameScale,
812
+ this.action.gravity ?? this.options.gravity,
813
+ this.action.resistanceX,
814
+ this.action.resistanceY,
815
+ environment.mascot.environment.activeIE.visible ? environment.mascot.environment.activeIE : void 0
816
+ );
796
817
  case "Jump": {
797
818
  const target = { x: this.action.targetX ?? this.state.x, y: this.action.targetY ?? this.state.y };
798
819
  return moveToward(this.state, target, this.action.velocity ?? 20, frameScale);
@@ -873,7 +894,8 @@ var ActionExecutor = class {
873
894
  createRuntime(definition, environment, references) {
874
895
  if (definition.condition && !evaluateExpression(definition.condition, environment, false)) return new CompleteRuntime();
875
896
  const bounds = environment.mascot.environment.workArea;
876
- if (!isOnBorder(this.state, bounds, definition.borderType)) return new CompleteRuntime();
897
+ const activeIE = environment.mascot.environment.activeIE;
898
+ if (!isOnBorder(this.state, bounds, definition.borderType, activeIE.visible ? activeIE : void 0)) return new CompleteRuntime();
877
899
  if (definition.type === "Reference") {
878
900
  if (!definition.name || references.has(definition.name)) return new CompleteRuntime();
879
901
  const referenced = this.spec.actions.find((action) => action.name === definition.name);
@@ -902,17 +924,27 @@ function environmentRectangle(bounds) {
902
924
  const top = bounds.y;
903
925
  const bottom = bounds.y + bounds.height;
904
926
  return {
905
- ...bounds,
927
+ x: bounds.x,
928
+ y: bounds.y,
929
+ width: bounds.width,
930
+ height: bounds.height,
906
931
  left,
907
932
  right,
908
933
  top,
909
934
  bottom,
910
- topBorder: edge((point) => Math.abs(point.y - top) <= 1),
911
- leftBorder: edge((point) => Math.abs(point.x - left) <= 1),
912
- rightBorder: edge((point) => Math.abs(point.x - right) <= 1),
913
- bottomBorder: edge((point) => Math.abs(point.y - bottom) <= 1)
935
+ topBorder: edge((point) => isOnTop(point, bounds)),
936
+ leftBorder: edge((point) => isOnLeft(point, bounds)),
937
+ rightBorder: edge((point) => isOnRight(point, bounds)),
938
+ bottomBorder: edge((point) => isOnBottom(point, bounds))
914
939
  };
915
940
  }
941
+ var PLATFORM_NEARBY_DISTANCE = 400;
942
+ var PLATFORM_EDGE_TOLERANCE = 16;
943
+ function distanceToRectangle(point, rectangle) {
944
+ const dx = Math.max(rectangle.x - point.x, 0, point.x - rectangle.x - rectangle.width);
945
+ const dy = Math.max(rectangle.y - point.y, 0, point.y - rectangle.y - rectangle.height);
946
+ return Math.hypot(dx, dy);
947
+ }
916
948
  var Mascot = class {
917
949
  /** Creates a mascot and immediately installs its pointer handlers. */
918
950
  constructor(id, spec, dom, options, spawnOptions, callbacks) {
@@ -959,10 +991,13 @@ var Mascot = class {
959
991
  pointerId;
960
992
  pointerDown;
961
993
  lastPointer;
994
+ activePlatformElement;
995
+ platforms = [];
962
996
  /** Advances behavior, animation, physics, and rendering by one clock tick. */
963
- tick(deltaMs, bounds) {
997
+ tick(deltaMs, bounds, platforms = []) {
964
998
  if (this.destroyed) return;
965
- const environment = this.createEnvironment(bounds);
999
+ this.platforms = platforms;
1000
+ const environment = this.createEnvironment(bounds, platforms);
966
1001
  if (this.state.dragging) {
967
1002
  this.dom.render(this.domHandle, this.spec, this.state);
968
1003
  return;
@@ -978,12 +1013,22 @@ var Mascot = class {
978
1013
  }
979
1014
  if (!started) break;
980
1015
  }
981
- if (!this.actions.tick(deltaMs, environment, bounds)) break;
1016
+ const activeIE = environment.mascot.environment.activeIE;
1017
+ const wasOnPlatformTop = activeIE.visible && activeIE.topBorder.isOn(this.state);
1018
+ const completed = this.actions.tick(deltaMs, environment, bounds);
1019
+ const remainedNearPlatform = this.state.x >= activeIE.left - PLATFORM_EDGE_TOLERANCE && this.state.x <= activeIE.right + PLATFORM_EDGE_TOLERANCE;
1020
+ if (wasOnPlatformTop && !remainedNearPlatform && !platforms.some((platform) => isOnTop(this.state, platform))) {
1021
+ this.actions.cancel();
1022
+ this.currentBehavior = this.findFallBehavior();
1023
+ if (this.currentBehavior) this.startBehavior(this.createEnvironment(bounds, platforms));
1024
+ break;
1025
+ }
1026
+ if (!completed) break;
982
1027
  if (this.destroyed) return;
983
- this.currentBehavior = this.behavior.selectNext(this.createEnvironment(bounds));
984
- if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds))) {
1028
+ this.currentBehavior = this.behavior.selectNext(this.createEnvironment(bounds, platforms));
1029
+ if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds, platforms))) {
985
1030
  this.currentBehavior = this.findFallBehavior();
986
- if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds))) break;
1031
+ if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds, platforms))) break;
987
1032
  }
988
1033
  deltaMs = 0;
989
1034
  }
@@ -1014,9 +1059,11 @@ var Mascot = class {
1014
1059
  findFallBehavior() {
1015
1060
  return this.behavior.force("Fall") ?? this.behavior.force("\u843D\u4E0B\u3059\u308B");
1016
1061
  }
1017
- createEnvironment(bounds) {
1062
+ createEnvironment(bounds, platforms = this.platforms) {
1018
1063
  const workArea = environmentRectangle(bounds);
1019
1064
  const inactive = environmentRectangle({ x: -100, y: -100, width: 0, height: 0 });
1065
+ const platform = this.selectActivePlatform(platforms);
1066
+ const activeIE = platform ? { ...environmentRectangle(platform), visible: true } : { ...inactive, visible: false };
1020
1067
  return {
1021
1068
  gap: 0,
1022
1069
  maxCount: 999,
@@ -1030,11 +1077,30 @@ var Mascot = class {
1030
1077
  workArea,
1031
1078
  floor: workArea.bottomBorder,
1032
1079
  ceiling: workArea.topBorder,
1033
- activeIE: { ...inactive, visible: false }
1080
+ activeIE
1034
1081
  }
1035
1082
  }
1036
1083
  };
1037
1084
  }
1085
+ selectActivePlatform(platforms) {
1086
+ if (platforms.length === 0) {
1087
+ this.activePlatformElement = void 0;
1088
+ return void 0;
1089
+ }
1090
+ const anchor = this.state;
1091
+ const current = platforms.find((platform) => platform.element === this.activePlatformElement);
1092
+ if (current && (isOnTop(anchor, current, 2) || isOnBottom(anchor, current, 2) || isOnLeft(anchor, current, 2) || isOnRight(anchor, current, 2))) return current;
1093
+ if (this.state.vy >= 0) {
1094
+ const landingPlatform = platforms.filter((platform) => anchor.x >= platform.x && anchor.x <= platform.x + platform.width && platform.y >= anchor.y - 1).sort((left, right) => left.y - right.y)[0];
1095
+ if (landingPlatform) {
1096
+ this.activePlatformElement = landingPlatform.element;
1097
+ return landingPlatform;
1098
+ }
1099
+ }
1100
+ const nearby = [...platforms].map((platform) => ({ platform, distance: distanceToRectangle(anchor, platform) })).filter(({ distance }) => distance <= PLATFORM_NEARBY_DISTANCE).sort((left, right) => left.distance - right.distance)[0]?.platform;
1101
+ this.activePlatformElement = nearby?.element;
1102
+ return nearby;
1103
+ }
1038
1104
  installPointerHandlers() {
1039
1105
  const element = this.domHandle.element;
1040
1106
  const listen = (target, type, listener) => {
@@ -1085,6 +1151,40 @@ var Mascot = class {
1085
1151
  }
1086
1152
  };
1087
1153
 
1154
+ // src/platform.ts
1155
+ function resolvePlatformElements(source, document2, excludedRoot) {
1156
+ let elements;
1157
+ if (typeof source === "string") {
1158
+ try {
1159
+ elements = [...document2.querySelectorAll(source)];
1160
+ } catch {
1161
+ return [];
1162
+ }
1163
+ } else {
1164
+ elements = source;
1165
+ }
1166
+ const HTMLElementConstructor = document2.defaultView?.HTMLElement;
1167
+ if (!HTMLElementConstructor) return [];
1168
+ return [...new Set(elements)].filter(
1169
+ (element) => element instanceof HTMLElementConstructor && element.isConnected && (!excludedRoot || !excludedRoot.contains(element))
1170
+ );
1171
+ }
1172
+ function readPlatformRectangles(elements, workAreaRectangle) {
1173
+ const rectangles = [];
1174
+ for (const element of elements) {
1175
+ const rectangle = element.getBoundingClientRect();
1176
+ if (rectangle.width <= 0 || rectangle.height <= 0) continue;
1177
+ rectangles.push({
1178
+ element,
1179
+ x: rectangle.left - workAreaRectangle.left,
1180
+ y: rectangle.top - workAreaRectangle.top,
1181
+ width: rectangle.width,
1182
+ height: rectangle.height
1183
+ });
1184
+ }
1185
+ return rectangles;
1186
+ }
1187
+
1088
1188
  // src/sprite.ts
1089
1189
  function dataUriToBlob(source) {
1090
1190
  const match = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(source);
@@ -1165,7 +1265,8 @@ var defaults = {
1165
1265
  gravity: 2,
1166
1266
  maxDeltaTime: 100,
1167
1267
  workAreaClassName: "",
1168
- mascotClassName: ""
1268
+ mascotClassName: "",
1269
+ platforms: []
1169
1270
  };
1170
1271
  var ShimejiEngine = class {
1171
1272
  /** Creates and initializes an engine inside a host DOM element. */
@@ -1173,6 +1274,7 @@ var ShimejiEngine = class {
1173
1274
  this.container = container;
1174
1275
  if (!container) throw new TypeError("ShimejiEngine requires a container element");
1175
1276
  this.options = { ...defaults, ...options, random: options.random };
1277
+ this.platformSource = this.options.platforms;
1176
1278
  this.originalContainerPosition = container.style.position;
1177
1279
  this.adjustedContainerPosition = getComputedStyle(container).position === "static";
1178
1280
  if (this.adjustedContainerPosition) container.style.position = "relative";
@@ -1196,6 +1298,7 @@ var ShimejiEngine = class {
1196
1298
  nextMascotId = 1;
1197
1299
  destroyed = false;
1198
1300
  initialized = false;
1301
+ platformSource;
1199
1302
  /** Starts the clock and global listeners. Calling this method more than once is harmless. */
1200
1303
  initialize() {
1201
1304
  this.assertAlive();
@@ -1237,12 +1340,12 @@ var ShimejiEngine = class {
1237
1340
  this.assertAlive();
1238
1341
  const spec = this.specs.get(characterId);
1239
1342
  if (!spec) throw new Error(`Character '${characterId}' is not registered`);
1240
- const bounds = this.dom.getBounds();
1343
+ const { bounds, platforms } = this.readFrameGeometry();
1241
1344
  const random = this.options.random ?? Math.random;
1242
1345
  const spawnOptions = {
1243
1346
  ...position,
1244
1347
  x: position.x ?? bounds.x + random() * bounds.width,
1245
- y: position.y ?? bounds.y + random() * bounds.height
1348
+ y: position.y ?? bounds.y
1246
1349
  };
1247
1350
  const id = `shimeji-${this.nextMascotId++}`;
1248
1351
  const mascot = new Mascot(id, spec, this.dom, this.options, spawnOptions, {
@@ -1258,7 +1361,7 @@ var ShimejiEngine = class {
1258
1361
  error: (error) => this.events.emit("error", error)
1259
1362
  });
1260
1363
  this.mascots.set(id, mascot);
1261
- mascot.tick(0, bounds);
1364
+ mascot.tick(0, bounds, platforms);
1262
1365
  const state = mascot.snapshot();
1263
1366
  this.events.emit("spawn", state);
1264
1367
  this.emitState();
@@ -1290,6 +1393,11 @@ var ShimejiEngine = class {
1290
1393
  this.assertAlive();
1291
1394
  return this.events.on(event, listener);
1292
1395
  }
1396
+ /** Replaces the DOM elements (or selector) exposed to mascots as platforms. */
1397
+ setPlatforms(platforms) {
1398
+ this.assertAlive();
1399
+ this.platformSource = platforms;
1400
+ }
1293
1401
  /** Stops animation and timers, removes listeners and DOM, and revokes all object URLs. */
1294
1402
  destroy() {
1295
1403
  if (this.destroyed) return;
@@ -1317,14 +1425,20 @@ var ShimejiEngine = class {
1317
1425
  const rawDelta = this.lastFrameTime === void 0 ? this.options.frameDuration : timestamp - this.lastFrameTime;
1318
1426
  this.lastFrameTime = timestamp;
1319
1427
  const delta = Math.max(0, Math.min(rawDelta, this.options.maxDeltaTime));
1320
- const bounds = this.dom.getBounds();
1321
- for (const mascot of [...this.mascots.values()]) mascot.tick(delta, bounds);
1428
+ const { bounds, platforms } = this.readFrameGeometry();
1429
+ for (const mascot of [...this.mascots.values()]) mascot.tick(delta, bounds, platforms);
1322
1430
  this.emitState();
1323
1431
  this.animationFrame = requestAnimationFrame(this.onAnimationFrame);
1324
1432
  };
1325
1433
  renderAll() {
1326
- const bounds = this.dom.getBounds();
1327
- for (const mascot of this.mascots.values()) mascot.tick(0, bounds);
1434
+ const { bounds, platforms } = this.readFrameGeometry();
1435
+ for (const mascot of this.mascots.values()) mascot.tick(0, bounds, platforms);
1436
+ }
1437
+ readFrameGeometry() {
1438
+ const workAreaRectangle = this.dom.workArea.getBoundingClientRect();
1439
+ const bounds = this.dom.getBoundsFromClientRectangle(workAreaRectangle);
1440
+ const elements = resolvePlatformElements(this.platformSource, this.container.ownerDocument, this.dom.workArea);
1441
+ return { bounds, platforms: readPlatformRectangles(elements, workAreaRectangle) };
1328
1442
  }
1329
1443
  emitState() {
1330
1444
  this.events.emit("statechange", this.getState());
@@ -1357,6 +1471,8 @@ export {
1357
1471
  normalizeCharacterSpec,
1358
1472
  parseActionsXml,
1359
1473
  parseBehaviorsXml,
1474
+ readPlatformRectangles,
1475
+ resolvePlatformElements,
1360
1476
  selectWeighted
1361
1477
  };
1362
1478
  //# sourceMappingURL=index.js.map