@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.cjs CHANGED
@@ -39,6 +39,8 @@ __export(index_exports, {
39
39
  normalizeCharacterSpec: () => normalizeCharacterSpec,
40
40
  parseActionsXml: () => parseActionsXml,
41
41
  parseBehaviorsXml: () => parseBehaviorsXml,
42
+ readPlatformRectangles: () => readPlatformRectangles,
43
+ resolvePlatformElements: () => resolvePlatformElements,
42
44
  selectWeighted: () => selectWeighted
43
45
  });
44
46
  module.exports = __toCommonJS(index_exports);
@@ -74,7 +76,10 @@ var DomManager = class {
74
76
  }
75
77
  /** Returns the current local work-area rectangle. */
76
78
  getBounds() {
77
- const rectangle = this.workArea.getBoundingClientRect();
79
+ return this.getBoundsFromClientRectangle(this.workArea.getBoundingClientRect());
80
+ }
81
+ /** Converts a previously-read work-area client rectangle into local bounds. */
82
+ getBoundsFromClientRectangle(rectangle) {
78
83
  const width = rectangle.width || this.container.clientWidth || window.innerWidth;
79
84
  const height = rectangle.height || this.container.clientHeight || window.innerHeight;
80
85
  return { x: 0, y: 0, width, height };
@@ -642,7 +647,8 @@ var BehaviorController = class {
642
647
  isOnAnyBoundary(environment) {
643
648
  const anchor = environment.mascot.anchor;
644
649
  const area = environment.mascot.environment.workArea;
645
- return area.topBorder.isOn(anchor) || area.leftBorder.isOn(anchor) || area.rightBorder.isOn(anchor) || area.bottomBorder.isOn(anchor);
650
+ const activeIE = environment.mascot.environment.activeIE;
651
+ 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));
646
652
  }
647
653
  };
648
654
 
@@ -662,17 +668,25 @@ function isOnLeft(point, rectangle, tolerance = 1) {
662
668
  function isOnRight(point, rectangle, tolerance = 1) {
663
669
  return point.y >= rectangle.y - tolerance && point.y <= rectangle.y + rectangle.height + tolerance && Math.abs(point.x - rectangle.x - rectangle.width) <= tolerance;
664
670
  }
665
- function isOnBorder(state, bounds, border) {
671
+ function isOnBorder(state, bounds, border, platform) {
666
672
  if (!border) return true;
667
- if (border === "Floor") return isOnBottom(state, bounds);
668
- if (border === "Ceiling") return isOnTop(state, bounds);
669
- return isOnLeft(state, bounds) || isOnRight(state, bounds);
673
+ if (border === "Floor") return isOnBottom(state, bounds) || platform !== void 0 && isOnTop(state, platform);
674
+ if (border === "Ceiling") return isOnTop(state, bounds) || platform !== void 0 && isOnBottom(state, platform);
675
+ return isOnLeft(state, bounds) || isOnRight(state, bounds) || platform !== void 0 && (isOnLeft(state, platform) || isOnRight(state, platform));
670
676
  }
671
- function applyGravity(state, bounds, frameScale, gravity, resistanceX = 0.05, resistanceY = 0.01) {
672
- state.x = clamp(state.x + state.vx * frameScale, bounds.x, bounds.x + bounds.width);
673
- state.y = clamp(state.y + state.vy * frameScale, bounds.y, bounds.y + bounds.height);
677
+ function applyGravity(state, bounds, frameScale, gravity, resistanceX = 0.05, resistanceY = 0.01, platform) {
678
+ const previousY = state.y;
679
+ const nextX = clamp(state.x + state.vx * frameScale, bounds.x, bounds.x + bounds.width);
680
+ const nextY = clamp(state.y + state.vy * frameScale, bounds.y, bounds.y + bounds.height);
681
+ state.x = nextX;
682
+ state.y = nextY;
674
683
  state.vx *= Math.max(0, 1 - resistanceX * frameScale);
675
684
  state.vy = state.vy * Math.max(0, 1 - resistanceY * frameScale) + gravity * frameScale;
685
+ if (platform && nextY >= previousY && previousY <= platform.y && nextY >= platform.y && nextX >= platform.x && nextX <= platform.x + platform.width) {
686
+ state.y = platform.y;
687
+ state.vy = 0;
688
+ return true;
689
+ }
676
690
  if (state.y >= bounds.y + bounds.height) {
677
691
  state.y = bounds.y + bounds.height;
678
692
  state.vy = 0;
@@ -709,7 +723,8 @@ function evaluateAction(definition, environment) {
709
723
  const initialVx = numeric(definition.initialVx, scopedEnvironment);
710
724
  let lookRight = environment.mascot.lookRight;
711
725
  if (definition.borderType === "Wall") {
712
- lookRight = environment.mascot.environment.workArea.rightBorder.isOn(environment.mascot.anchor);
726
+ const { activeIE, workArea } = environment.mascot.environment;
727
+ lookRight = workArea.rightBorder.isOn(environment.mascot.anchor) || activeIE.visible && activeIE.leftBorder.isOn(environment.mascot.anchor);
713
728
  } else if (definition.type === "Move" || definition.embedType === "Jump" || definition.embedType === "WalkWithIE") {
714
729
  if (targetX !== void 0) lookRight = targetX > environment.mascot.anchor.x;
715
730
  } else if (definition.embedType === "Fall" || definition.embedType === "FallWithIE") {
@@ -837,7 +852,15 @@ var LeafRuntime = class {
837
852
  case "Fall":
838
853
  case "FallWithIE":
839
854
  case "Thrown":
840
- return applyGravity(this.state, bounds, frameScale, this.action.gravity ?? this.options.gravity, this.action.resistanceX, this.action.resistanceY);
855
+ return applyGravity(
856
+ this.state,
857
+ bounds,
858
+ frameScale,
859
+ this.action.gravity ?? this.options.gravity,
860
+ this.action.resistanceX,
861
+ this.action.resistanceY,
862
+ environment.mascot.environment.activeIE.visible ? environment.mascot.environment.activeIE : void 0
863
+ );
841
864
  case "Jump": {
842
865
  const target = { x: this.action.targetX ?? this.state.x, y: this.action.targetY ?? this.state.y };
843
866
  return moveToward(this.state, target, this.action.velocity ?? 20, frameScale);
@@ -918,7 +941,8 @@ var ActionExecutor = class {
918
941
  createRuntime(definition, environment, references) {
919
942
  if (definition.condition && !evaluateExpression(definition.condition, environment, false)) return new CompleteRuntime();
920
943
  const bounds = environment.mascot.environment.workArea;
921
- if (!isOnBorder(this.state, bounds, definition.borderType)) return new CompleteRuntime();
944
+ const activeIE = environment.mascot.environment.activeIE;
945
+ if (!isOnBorder(this.state, bounds, definition.borderType, activeIE.visible ? activeIE : void 0)) return new CompleteRuntime();
922
946
  if (definition.type === "Reference") {
923
947
  if (!definition.name || references.has(definition.name)) return new CompleteRuntime();
924
948
  const referenced = this.spec.actions.find((action) => action.name === definition.name);
@@ -947,17 +971,26 @@ function environmentRectangle(bounds) {
947
971
  const top = bounds.y;
948
972
  const bottom = bounds.y + bounds.height;
949
973
  return {
950
- ...bounds,
974
+ x: bounds.x,
975
+ y: bounds.y,
976
+ width: bounds.width,
977
+ height: bounds.height,
951
978
  left,
952
979
  right,
953
980
  top,
954
981
  bottom,
955
- topBorder: edge((point) => Math.abs(point.y - top) <= 1),
956
- leftBorder: edge((point) => Math.abs(point.x - left) <= 1),
957
- rightBorder: edge((point) => Math.abs(point.x - right) <= 1),
958
- bottomBorder: edge((point) => Math.abs(point.y - bottom) <= 1)
982
+ topBorder: edge((point) => isOnTop(point, bounds)),
983
+ leftBorder: edge((point) => isOnLeft(point, bounds)),
984
+ rightBorder: edge((point) => isOnRight(point, bounds)),
985
+ bottomBorder: edge((point) => isOnBottom(point, bounds))
959
986
  };
960
987
  }
988
+ var PLATFORM_NEARBY_DISTANCE = 400;
989
+ function distanceToRectangle(point, rectangle) {
990
+ const dx = Math.max(rectangle.x - point.x, 0, point.x - rectangle.x - rectangle.width);
991
+ const dy = Math.max(rectangle.y - point.y, 0, point.y - rectangle.y - rectangle.height);
992
+ return Math.hypot(dx, dy);
993
+ }
961
994
  var Mascot = class {
962
995
  /** Creates a mascot and immediately installs its pointer handlers. */
963
996
  constructor(id, spec, dom, options, spawnOptions, callbacks) {
@@ -1004,10 +1037,13 @@ var Mascot = class {
1004
1037
  pointerId;
1005
1038
  pointerDown;
1006
1039
  lastPointer;
1040
+ activePlatformElement;
1041
+ platforms = [];
1007
1042
  /** Advances behavior, animation, physics, and rendering by one clock tick. */
1008
- tick(deltaMs, bounds) {
1043
+ tick(deltaMs, bounds, platforms = []) {
1009
1044
  if (this.destroyed) return;
1010
- const environment = this.createEnvironment(bounds);
1045
+ this.platforms = platforms;
1046
+ const environment = this.createEnvironment(bounds, platforms);
1011
1047
  if (this.state.dragging) {
1012
1048
  this.dom.render(this.domHandle, this.spec, this.state);
1013
1049
  return;
@@ -1023,12 +1059,20 @@ var Mascot = class {
1023
1059
  }
1024
1060
  if (!started) break;
1025
1061
  }
1026
- if (!this.actions.tick(deltaMs, environment, bounds)) break;
1062
+ const wasOnPlatformTop = environment.mascot.environment.activeIE.visible && environment.mascot.environment.activeIE.topBorder.isOn(this.state);
1063
+ const completed = this.actions.tick(deltaMs, environment, bounds);
1064
+ if (wasOnPlatformTop && !platforms.some((platform) => isOnTop(this.state, platform))) {
1065
+ this.actions.cancel();
1066
+ this.currentBehavior = this.findFallBehavior();
1067
+ if (this.currentBehavior) this.startBehavior(this.createEnvironment(bounds, platforms));
1068
+ break;
1069
+ }
1070
+ if (!completed) break;
1027
1071
  if (this.destroyed) return;
1028
- this.currentBehavior = this.behavior.selectNext(this.createEnvironment(bounds));
1029
- if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds))) {
1072
+ this.currentBehavior = this.behavior.selectNext(this.createEnvironment(bounds, platforms));
1073
+ if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds, platforms))) {
1030
1074
  this.currentBehavior = this.findFallBehavior();
1031
- if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds))) break;
1075
+ if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds, platforms))) break;
1032
1076
  }
1033
1077
  deltaMs = 0;
1034
1078
  }
@@ -1059,9 +1103,11 @@ var Mascot = class {
1059
1103
  findFallBehavior() {
1060
1104
  return this.behavior.force("Fall") ?? this.behavior.force("\u843D\u4E0B\u3059\u308B");
1061
1105
  }
1062
- createEnvironment(bounds) {
1106
+ createEnvironment(bounds, platforms = this.platforms) {
1063
1107
  const workArea = environmentRectangle(bounds);
1064
1108
  const inactive = environmentRectangle({ x: -100, y: -100, width: 0, height: 0 });
1109
+ const platform = this.selectActivePlatform(platforms);
1110
+ const activeIE = platform ? { ...environmentRectangle(platform), visible: true } : { ...inactive, visible: false };
1065
1111
  return {
1066
1112
  gap: 0,
1067
1113
  maxCount: 999,
@@ -1075,11 +1121,30 @@ var Mascot = class {
1075
1121
  workArea,
1076
1122
  floor: workArea.bottomBorder,
1077
1123
  ceiling: workArea.topBorder,
1078
- activeIE: { ...inactive, visible: false }
1124
+ activeIE
1079
1125
  }
1080
1126
  }
1081
1127
  };
1082
1128
  }
1129
+ selectActivePlatform(platforms) {
1130
+ if (platforms.length === 0) {
1131
+ this.activePlatformElement = void 0;
1132
+ return void 0;
1133
+ }
1134
+ const anchor = this.state;
1135
+ const current = platforms.find((platform) => platform.element === this.activePlatformElement);
1136
+ if (current && (isOnTop(anchor, current, 2) || isOnBottom(anchor, current, 2) || isOnLeft(anchor, current, 2) || isOnRight(anchor, current, 2))) return current;
1137
+ if (this.state.vy >= 0) {
1138
+ 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];
1139
+ if (landingPlatform) {
1140
+ this.activePlatformElement = landingPlatform.element;
1141
+ return landingPlatform;
1142
+ }
1143
+ }
1144
+ 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;
1145
+ this.activePlatformElement = nearby?.element;
1146
+ return nearby;
1147
+ }
1083
1148
  installPointerHandlers() {
1084
1149
  const element = this.domHandle.element;
1085
1150
  const listen = (target, type, listener) => {
@@ -1130,6 +1195,40 @@ var Mascot = class {
1130
1195
  }
1131
1196
  };
1132
1197
 
1198
+ // src/platform.ts
1199
+ function resolvePlatformElements(source, document2, excludedRoot) {
1200
+ let elements;
1201
+ if (typeof source === "string") {
1202
+ try {
1203
+ elements = [...document2.querySelectorAll(source)];
1204
+ } catch {
1205
+ return [];
1206
+ }
1207
+ } else {
1208
+ elements = source;
1209
+ }
1210
+ const HTMLElementConstructor = document2.defaultView?.HTMLElement;
1211
+ if (!HTMLElementConstructor) return [];
1212
+ return [...new Set(elements)].filter(
1213
+ (element) => element instanceof HTMLElementConstructor && element.isConnected && (!excludedRoot || !excludedRoot.contains(element))
1214
+ );
1215
+ }
1216
+ function readPlatformRectangles(elements, workAreaRectangle) {
1217
+ const rectangles = [];
1218
+ for (const element of elements) {
1219
+ const rectangle = element.getBoundingClientRect();
1220
+ if (rectangle.width <= 0 || rectangle.height <= 0) continue;
1221
+ rectangles.push({
1222
+ element,
1223
+ x: rectangle.left - workAreaRectangle.left,
1224
+ y: rectangle.top - workAreaRectangle.top,
1225
+ width: rectangle.width,
1226
+ height: rectangle.height
1227
+ });
1228
+ }
1229
+ return rectangles;
1230
+ }
1231
+
1133
1232
  // src/sprite.ts
1134
1233
  function dataUriToBlob(source) {
1135
1234
  const match = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(source);
@@ -1210,7 +1309,8 @@ var defaults = {
1210
1309
  gravity: 2,
1211
1310
  maxDeltaTime: 100,
1212
1311
  workAreaClassName: "",
1213
- mascotClassName: ""
1312
+ mascotClassName: "",
1313
+ platforms: []
1214
1314
  };
1215
1315
  var ShimejiEngine = class {
1216
1316
  /** Creates and initializes an engine inside a host DOM element. */
@@ -1218,6 +1318,7 @@ var ShimejiEngine = class {
1218
1318
  this.container = container;
1219
1319
  if (!container) throw new TypeError("ShimejiEngine requires a container element");
1220
1320
  this.options = { ...defaults, ...options, random: options.random };
1321
+ this.platformSource = this.options.platforms;
1221
1322
  this.originalContainerPosition = container.style.position;
1222
1323
  this.adjustedContainerPosition = getComputedStyle(container).position === "static";
1223
1324
  if (this.adjustedContainerPosition) container.style.position = "relative";
@@ -1241,6 +1342,7 @@ var ShimejiEngine = class {
1241
1342
  nextMascotId = 1;
1242
1343
  destroyed = false;
1243
1344
  initialized = false;
1345
+ platformSource;
1244
1346
  /** Starts the clock and global listeners. Calling this method more than once is harmless. */
1245
1347
  initialize() {
1246
1348
  this.assertAlive();
@@ -1282,12 +1384,12 @@ var ShimejiEngine = class {
1282
1384
  this.assertAlive();
1283
1385
  const spec = this.specs.get(characterId);
1284
1386
  if (!spec) throw new Error(`Character '${characterId}' is not registered`);
1285
- const bounds = this.dom.getBounds();
1387
+ const { bounds, platforms } = this.readFrameGeometry();
1286
1388
  const random = this.options.random ?? Math.random;
1287
1389
  const spawnOptions = {
1288
1390
  ...position,
1289
1391
  x: position.x ?? bounds.x + random() * bounds.width,
1290
- y: position.y ?? bounds.y + random() * bounds.height
1392
+ y: position.y ?? bounds.y
1291
1393
  };
1292
1394
  const id = `shimeji-${this.nextMascotId++}`;
1293
1395
  const mascot = new Mascot(id, spec, this.dom, this.options, spawnOptions, {
@@ -1303,7 +1405,7 @@ var ShimejiEngine = class {
1303
1405
  error: (error) => this.events.emit("error", error)
1304
1406
  });
1305
1407
  this.mascots.set(id, mascot);
1306
- mascot.tick(0, bounds);
1408
+ mascot.tick(0, bounds, platforms);
1307
1409
  const state = mascot.snapshot();
1308
1410
  this.events.emit("spawn", state);
1309
1411
  this.emitState();
@@ -1335,6 +1437,11 @@ var ShimejiEngine = class {
1335
1437
  this.assertAlive();
1336
1438
  return this.events.on(event, listener);
1337
1439
  }
1440
+ /** Replaces the DOM elements (or selector) exposed to mascots as platforms. */
1441
+ setPlatforms(platforms) {
1442
+ this.assertAlive();
1443
+ this.platformSource = platforms;
1444
+ }
1338
1445
  /** Stops animation and timers, removes listeners and DOM, and revokes all object URLs. */
1339
1446
  destroy() {
1340
1447
  if (this.destroyed) return;
@@ -1362,14 +1469,20 @@ var ShimejiEngine = class {
1362
1469
  const rawDelta = this.lastFrameTime === void 0 ? this.options.frameDuration : timestamp - this.lastFrameTime;
1363
1470
  this.lastFrameTime = timestamp;
1364
1471
  const delta = Math.max(0, Math.min(rawDelta, this.options.maxDeltaTime));
1365
- const bounds = this.dom.getBounds();
1366
- for (const mascot of [...this.mascots.values()]) mascot.tick(delta, bounds);
1472
+ const { bounds, platforms } = this.readFrameGeometry();
1473
+ for (const mascot of [...this.mascots.values()]) mascot.tick(delta, bounds, platforms);
1367
1474
  this.emitState();
1368
1475
  this.animationFrame = requestAnimationFrame(this.onAnimationFrame);
1369
1476
  };
1370
1477
  renderAll() {
1371
- const bounds = this.dom.getBounds();
1372
- for (const mascot of this.mascots.values()) mascot.tick(0, bounds);
1478
+ const { bounds, platforms } = this.readFrameGeometry();
1479
+ for (const mascot of this.mascots.values()) mascot.tick(0, bounds, platforms);
1480
+ }
1481
+ readFrameGeometry() {
1482
+ const workAreaRectangle = this.dom.workArea.getBoundingClientRect();
1483
+ const bounds = this.dom.getBoundsFromClientRectangle(workAreaRectangle);
1484
+ const elements = resolvePlatformElements(this.platformSource, this.container.ownerDocument, this.dom.workArea);
1485
+ return { bounds, platforms: readPlatformRectangles(elements, workAreaRectangle) };
1373
1486
  }
1374
1487
  emitState() {
1375
1488
  this.events.emit("statechange", this.getState());
@@ -1403,6 +1516,8 @@ var ShimejiEngine = class {
1403
1516
  normalizeCharacterSpec,
1404
1517
  parseActionsXml,
1405
1518
  parseBehaviorsXml,
1519
+ readPlatformRectangles,
1520
+ resolvePlatformElements,
1406
1521
  selectWeighted
1407
1522
  });
1408
1523
  //# sourceMappingURL=index.cjs.map