@react-shimeji/core 0.1.0 → 0.2.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.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,26 @@ 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
+ function distanceToRectangle(point, rectangle) {
943
+ const dx = Math.max(rectangle.x - point.x, 0, point.x - rectangle.x - rectangle.width);
944
+ const dy = Math.max(rectangle.y - point.y, 0, point.y - rectangle.y - rectangle.height);
945
+ return Math.hypot(dx, dy);
946
+ }
916
947
  var Mascot = class {
917
948
  /** Creates a mascot and immediately installs its pointer handlers. */
918
949
  constructor(id, spec, dom, options, spawnOptions, callbacks) {
@@ -959,10 +990,13 @@ var Mascot = class {
959
990
  pointerId;
960
991
  pointerDown;
961
992
  lastPointer;
993
+ activePlatformElement;
994
+ platforms = [];
962
995
  /** Advances behavior, animation, physics, and rendering by one clock tick. */
963
- tick(deltaMs, bounds) {
996
+ tick(deltaMs, bounds, platforms = []) {
964
997
  if (this.destroyed) return;
965
- const environment = this.createEnvironment(bounds);
998
+ this.platforms = platforms;
999
+ const environment = this.createEnvironment(bounds, platforms);
966
1000
  if (this.state.dragging) {
967
1001
  this.dom.render(this.domHandle, this.spec, this.state);
968
1002
  return;
@@ -978,12 +1012,20 @@ var Mascot = class {
978
1012
  }
979
1013
  if (!started) break;
980
1014
  }
981
- if (!this.actions.tick(deltaMs, environment, bounds)) break;
1015
+ const wasOnPlatformTop = environment.mascot.environment.activeIE.visible && environment.mascot.environment.activeIE.topBorder.isOn(this.state);
1016
+ const completed = this.actions.tick(deltaMs, environment, bounds);
1017
+ if (wasOnPlatformTop && !platforms.some((platform) => isOnTop(this.state, platform))) {
1018
+ this.actions.cancel();
1019
+ this.currentBehavior = this.findFallBehavior();
1020
+ if (this.currentBehavior) this.startBehavior(this.createEnvironment(bounds, platforms));
1021
+ break;
1022
+ }
1023
+ if (!completed) break;
982
1024
  if (this.destroyed) return;
983
- this.currentBehavior = this.behavior.selectNext(this.createEnvironment(bounds));
984
- if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds))) {
1025
+ this.currentBehavior = this.behavior.selectNext(this.createEnvironment(bounds, platforms));
1026
+ if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds, platforms))) {
985
1027
  this.currentBehavior = this.findFallBehavior();
986
- if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds))) break;
1028
+ if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds, platforms))) break;
987
1029
  }
988
1030
  deltaMs = 0;
989
1031
  }
@@ -1014,9 +1056,11 @@ var Mascot = class {
1014
1056
  findFallBehavior() {
1015
1057
  return this.behavior.force("Fall") ?? this.behavior.force("\u843D\u4E0B\u3059\u308B");
1016
1058
  }
1017
- createEnvironment(bounds) {
1059
+ createEnvironment(bounds, platforms = this.platforms) {
1018
1060
  const workArea = environmentRectangle(bounds);
1019
1061
  const inactive = environmentRectangle({ x: -100, y: -100, width: 0, height: 0 });
1062
+ const platform = this.selectActivePlatform(platforms);
1063
+ const activeIE = platform ? { ...environmentRectangle(platform), visible: true } : { ...inactive, visible: false };
1020
1064
  return {
1021
1065
  gap: 0,
1022
1066
  maxCount: 999,
@@ -1030,11 +1074,30 @@ var Mascot = class {
1030
1074
  workArea,
1031
1075
  floor: workArea.bottomBorder,
1032
1076
  ceiling: workArea.topBorder,
1033
- activeIE: { ...inactive, visible: false }
1077
+ activeIE
1034
1078
  }
1035
1079
  }
1036
1080
  };
1037
1081
  }
1082
+ selectActivePlatform(platforms) {
1083
+ if (platforms.length === 0) {
1084
+ this.activePlatformElement = void 0;
1085
+ return void 0;
1086
+ }
1087
+ const anchor = this.state;
1088
+ const current = platforms.find((platform) => platform.element === this.activePlatformElement);
1089
+ if (current && (isOnTop(anchor, current, 2) || isOnBottom(anchor, current, 2) || isOnLeft(anchor, current, 2) || isOnRight(anchor, current, 2))) return current;
1090
+ if (this.state.vy >= 0) {
1091
+ 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];
1092
+ if (landingPlatform) {
1093
+ this.activePlatformElement = landingPlatform.element;
1094
+ return landingPlatform;
1095
+ }
1096
+ }
1097
+ 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;
1098
+ this.activePlatformElement = nearby?.element;
1099
+ return nearby;
1100
+ }
1038
1101
  installPointerHandlers() {
1039
1102
  const element = this.domHandle.element;
1040
1103
  const listen = (target, type, listener) => {
@@ -1085,6 +1148,40 @@ var Mascot = class {
1085
1148
  }
1086
1149
  };
1087
1150
 
1151
+ // src/platform.ts
1152
+ function resolvePlatformElements(source, document2, excludedRoot) {
1153
+ let elements;
1154
+ if (typeof source === "string") {
1155
+ try {
1156
+ elements = [...document2.querySelectorAll(source)];
1157
+ } catch {
1158
+ return [];
1159
+ }
1160
+ } else {
1161
+ elements = source;
1162
+ }
1163
+ const HTMLElementConstructor = document2.defaultView?.HTMLElement;
1164
+ if (!HTMLElementConstructor) return [];
1165
+ return [...new Set(elements)].filter(
1166
+ (element) => element instanceof HTMLElementConstructor && element.isConnected && (!excludedRoot || !excludedRoot.contains(element))
1167
+ );
1168
+ }
1169
+ function readPlatformRectangles(elements, workAreaRectangle) {
1170
+ const rectangles = [];
1171
+ for (const element of elements) {
1172
+ const rectangle = element.getBoundingClientRect();
1173
+ if (rectangle.width <= 0 || rectangle.height <= 0) continue;
1174
+ rectangles.push({
1175
+ element,
1176
+ x: rectangle.left - workAreaRectangle.left,
1177
+ y: rectangle.top - workAreaRectangle.top,
1178
+ width: rectangle.width,
1179
+ height: rectangle.height
1180
+ });
1181
+ }
1182
+ return rectangles;
1183
+ }
1184
+
1088
1185
  // src/sprite.ts
1089
1186
  function dataUriToBlob(source) {
1090
1187
  const match = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(source);
@@ -1165,7 +1262,8 @@ var defaults = {
1165
1262
  gravity: 2,
1166
1263
  maxDeltaTime: 100,
1167
1264
  workAreaClassName: "",
1168
- mascotClassName: ""
1265
+ mascotClassName: "",
1266
+ platforms: []
1169
1267
  };
1170
1268
  var ShimejiEngine = class {
1171
1269
  /** Creates and initializes an engine inside a host DOM element. */
@@ -1173,6 +1271,7 @@ var ShimejiEngine = class {
1173
1271
  this.container = container;
1174
1272
  if (!container) throw new TypeError("ShimejiEngine requires a container element");
1175
1273
  this.options = { ...defaults, ...options, random: options.random };
1274
+ this.platformSource = this.options.platforms;
1176
1275
  this.originalContainerPosition = container.style.position;
1177
1276
  this.adjustedContainerPosition = getComputedStyle(container).position === "static";
1178
1277
  if (this.adjustedContainerPosition) container.style.position = "relative";
@@ -1196,6 +1295,7 @@ var ShimejiEngine = class {
1196
1295
  nextMascotId = 1;
1197
1296
  destroyed = false;
1198
1297
  initialized = false;
1298
+ platformSource;
1199
1299
  /** Starts the clock and global listeners. Calling this method more than once is harmless. */
1200
1300
  initialize() {
1201
1301
  this.assertAlive();
@@ -1237,12 +1337,12 @@ var ShimejiEngine = class {
1237
1337
  this.assertAlive();
1238
1338
  const spec = this.specs.get(characterId);
1239
1339
  if (!spec) throw new Error(`Character '${characterId}' is not registered`);
1240
- const bounds = this.dom.getBounds();
1340
+ const { bounds, platforms } = this.readFrameGeometry();
1241
1341
  const random = this.options.random ?? Math.random;
1242
1342
  const spawnOptions = {
1243
1343
  ...position,
1244
1344
  x: position.x ?? bounds.x + random() * bounds.width,
1245
- y: position.y ?? bounds.y + random() * bounds.height
1345
+ y: position.y ?? bounds.y
1246
1346
  };
1247
1347
  const id = `shimeji-${this.nextMascotId++}`;
1248
1348
  const mascot = new Mascot(id, spec, this.dom, this.options, spawnOptions, {
@@ -1258,7 +1358,7 @@ var ShimejiEngine = class {
1258
1358
  error: (error) => this.events.emit("error", error)
1259
1359
  });
1260
1360
  this.mascots.set(id, mascot);
1261
- mascot.tick(0, bounds);
1361
+ mascot.tick(0, bounds, platforms);
1262
1362
  const state = mascot.snapshot();
1263
1363
  this.events.emit("spawn", state);
1264
1364
  this.emitState();
@@ -1290,6 +1390,11 @@ var ShimejiEngine = class {
1290
1390
  this.assertAlive();
1291
1391
  return this.events.on(event, listener);
1292
1392
  }
1393
+ /** Replaces the DOM elements (or selector) exposed to mascots as platforms. */
1394
+ setPlatforms(platforms) {
1395
+ this.assertAlive();
1396
+ this.platformSource = platforms;
1397
+ }
1293
1398
  /** Stops animation and timers, removes listeners and DOM, and revokes all object URLs. */
1294
1399
  destroy() {
1295
1400
  if (this.destroyed) return;
@@ -1317,14 +1422,20 @@ var ShimejiEngine = class {
1317
1422
  const rawDelta = this.lastFrameTime === void 0 ? this.options.frameDuration : timestamp - this.lastFrameTime;
1318
1423
  this.lastFrameTime = timestamp;
1319
1424
  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);
1425
+ const { bounds, platforms } = this.readFrameGeometry();
1426
+ for (const mascot of [...this.mascots.values()]) mascot.tick(delta, bounds, platforms);
1322
1427
  this.emitState();
1323
1428
  this.animationFrame = requestAnimationFrame(this.onAnimationFrame);
1324
1429
  };
1325
1430
  renderAll() {
1326
- const bounds = this.dom.getBounds();
1327
- for (const mascot of this.mascots.values()) mascot.tick(0, bounds);
1431
+ const { bounds, platforms } = this.readFrameGeometry();
1432
+ for (const mascot of this.mascots.values()) mascot.tick(0, bounds, platforms);
1433
+ }
1434
+ readFrameGeometry() {
1435
+ const workAreaRectangle = this.dom.workArea.getBoundingClientRect();
1436
+ const bounds = this.dom.getBoundsFromClientRectangle(workAreaRectangle);
1437
+ const elements = resolvePlatformElements(this.platformSource, this.container.ownerDocument, this.dom.workArea);
1438
+ return { bounds, platforms: readPlatformRectangles(elements, workAreaRectangle) };
1328
1439
  }
1329
1440
  emitState() {
1330
1441
  this.events.emit("statechange", this.getState());
@@ -1357,6 +1468,8 @@ export {
1357
1468
  normalizeCharacterSpec,
1358
1469
  parseActionsXml,
1359
1470
  parseBehaviorsXml,
1471
+ readPlatformRectangles,
1472
+ resolvePlatformElements,
1360
1473
  selectWeighted
1361
1474
  };
1362
1475
  //# sourceMappingURL=index.js.map