@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.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,27 @@ 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
+ var PLATFORM_EDGE_TOLERANCE = 16;
990
+ function distanceToRectangle(point, rectangle) {
991
+ const dx = Math.max(rectangle.x - point.x, 0, point.x - rectangle.x - rectangle.width);
992
+ const dy = Math.max(rectangle.y - point.y, 0, point.y - rectangle.y - rectangle.height);
993
+ return Math.hypot(dx, dy);
994
+ }
961
995
  var Mascot = class {
962
996
  /** Creates a mascot and immediately installs its pointer handlers. */
963
997
  constructor(id, spec, dom, options, spawnOptions, callbacks) {
@@ -1004,10 +1038,13 @@ var Mascot = class {
1004
1038
  pointerId;
1005
1039
  pointerDown;
1006
1040
  lastPointer;
1041
+ activePlatformElement;
1042
+ platforms = [];
1007
1043
  /** Advances behavior, animation, physics, and rendering by one clock tick. */
1008
- tick(deltaMs, bounds) {
1044
+ tick(deltaMs, bounds, platforms = []) {
1009
1045
  if (this.destroyed) return;
1010
- const environment = this.createEnvironment(bounds);
1046
+ this.platforms = platforms;
1047
+ const environment = this.createEnvironment(bounds, platforms);
1011
1048
  if (this.state.dragging) {
1012
1049
  this.dom.render(this.domHandle, this.spec, this.state);
1013
1050
  return;
@@ -1023,12 +1060,22 @@ var Mascot = class {
1023
1060
  }
1024
1061
  if (!started) break;
1025
1062
  }
1026
- if (!this.actions.tick(deltaMs, environment, bounds)) break;
1063
+ const activeIE = environment.mascot.environment.activeIE;
1064
+ const wasOnPlatformTop = activeIE.visible && activeIE.topBorder.isOn(this.state);
1065
+ const completed = this.actions.tick(deltaMs, environment, bounds);
1066
+ const remainedNearPlatform = this.state.x >= activeIE.left - PLATFORM_EDGE_TOLERANCE && this.state.x <= activeIE.right + PLATFORM_EDGE_TOLERANCE;
1067
+ if (wasOnPlatformTop && !remainedNearPlatform && !platforms.some((platform) => isOnTop(this.state, platform))) {
1068
+ this.actions.cancel();
1069
+ this.currentBehavior = this.findFallBehavior();
1070
+ if (this.currentBehavior) this.startBehavior(this.createEnvironment(bounds, platforms));
1071
+ break;
1072
+ }
1073
+ if (!completed) break;
1027
1074
  if (this.destroyed) return;
1028
- this.currentBehavior = this.behavior.selectNext(this.createEnvironment(bounds));
1029
- if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds))) {
1075
+ this.currentBehavior = this.behavior.selectNext(this.createEnvironment(bounds, platforms));
1076
+ if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds, platforms))) {
1030
1077
  this.currentBehavior = this.findFallBehavior();
1031
- if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds))) break;
1078
+ if (!this.currentBehavior || !this.startBehavior(this.createEnvironment(bounds, platforms))) break;
1032
1079
  }
1033
1080
  deltaMs = 0;
1034
1081
  }
@@ -1059,9 +1106,11 @@ var Mascot = class {
1059
1106
  findFallBehavior() {
1060
1107
  return this.behavior.force("Fall") ?? this.behavior.force("\u843D\u4E0B\u3059\u308B");
1061
1108
  }
1062
- createEnvironment(bounds) {
1109
+ createEnvironment(bounds, platforms = this.platforms) {
1063
1110
  const workArea = environmentRectangle(bounds);
1064
1111
  const inactive = environmentRectangle({ x: -100, y: -100, width: 0, height: 0 });
1112
+ const platform = this.selectActivePlatform(platforms);
1113
+ const activeIE = platform ? { ...environmentRectangle(platform), visible: true } : { ...inactive, visible: false };
1065
1114
  return {
1066
1115
  gap: 0,
1067
1116
  maxCount: 999,
@@ -1075,11 +1124,30 @@ var Mascot = class {
1075
1124
  workArea,
1076
1125
  floor: workArea.bottomBorder,
1077
1126
  ceiling: workArea.topBorder,
1078
- activeIE: { ...inactive, visible: false }
1127
+ activeIE
1079
1128
  }
1080
1129
  }
1081
1130
  };
1082
1131
  }
1132
+ selectActivePlatform(platforms) {
1133
+ if (platforms.length === 0) {
1134
+ this.activePlatformElement = void 0;
1135
+ return void 0;
1136
+ }
1137
+ const anchor = this.state;
1138
+ const current = platforms.find((platform) => platform.element === this.activePlatformElement);
1139
+ if (current && (isOnTop(anchor, current, 2) || isOnBottom(anchor, current, 2) || isOnLeft(anchor, current, 2) || isOnRight(anchor, current, 2))) return current;
1140
+ if (this.state.vy >= 0) {
1141
+ 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];
1142
+ if (landingPlatform) {
1143
+ this.activePlatformElement = landingPlatform.element;
1144
+ return landingPlatform;
1145
+ }
1146
+ }
1147
+ 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;
1148
+ this.activePlatformElement = nearby?.element;
1149
+ return nearby;
1150
+ }
1083
1151
  installPointerHandlers() {
1084
1152
  const element = this.domHandle.element;
1085
1153
  const listen = (target, type, listener) => {
@@ -1130,6 +1198,40 @@ var Mascot = class {
1130
1198
  }
1131
1199
  };
1132
1200
 
1201
+ // src/platform.ts
1202
+ function resolvePlatformElements(source, document2, excludedRoot) {
1203
+ let elements;
1204
+ if (typeof source === "string") {
1205
+ try {
1206
+ elements = [...document2.querySelectorAll(source)];
1207
+ } catch {
1208
+ return [];
1209
+ }
1210
+ } else {
1211
+ elements = source;
1212
+ }
1213
+ const HTMLElementConstructor = document2.defaultView?.HTMLElement;
1214
+ if (!HTMLElementConstructor) return [];
1215
+ return [...new Set(elements)].filter(
1216
+ (element) => element instanceof HTMLElementConstructor && element.isConnected && (!excludedRoot || !excludedRoot.contains(element))
1217
+ );
1218
+ }
1219
+ function readPlatformRectangles(elements, workAreaRectangle) {
1220
+ const rectangles = [];
1221
+ for (const element of elements) {
1222
+ const rectangle = element.getBoundingClientRect();
1223
+ if (rectangle.width <= 0 || rectangle.height <= 0) continue;
1224
+ rectangles.push({
1225
+ element,
1226
+ x: rectangle.left - workAreaRectangle.left,
1227
+ y: rectangle.top - workAreaRectangle.top,
1228
+ width: rectangle.width,
1229
+ height: rectangle.height
1230
+ });
1231
+ }
1232
+ return rectangles;
1233
+ }
1234
+
1133
1235
  // src/sprite.ts
1134
1236
  function dataUriToBlob(source) {
1135
1237
  const match = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(source);
@@ -1210,7 +1312,8 @@ var defaults = {
1210
1312
  gravity: 2,
1211
1313
  maxDeltaTime: 100,
1212
1314
  workAreaClassName: "",
1213
- mascotClassName: ""
1315
+ mascotClassName: "",
1316
+ platforms: []
1214
1317
  };
1215
1318
  var ShimejiEngine = class {
1216
1319
  /** Creates and initializes an engine inside a host DOM element. */
@@ -1218,6 +1321,7 @@ var ShimejiEngine = class {
1218
1321
  this.container = container;
1219
1322
  if (!container) throw new TypeError("ShimejiEngine requires a container element");
1220
1323
  this.options = { ...defaults, ...options, random: options.random };
1324
+ this.platformSource = this.options.platforms;
1221
1325
  this.originalContainerPosition = container.style.position;
1222
1326
  this.adjustedContainerPosition = getComputedStyle(container).position === "static";
1223
1327
  if (this.adjustedContainerPosition) container.style.position = "relative";
@@ -1241,6 +1345,7 @@ var ShimejiEngine = class {
1241
1345
  nextMascotId = 1;
1242
1346
  destroyed = false;
1243
1347
  initialized = false;
1348
+ platformSource;
1244
1349
  /** Starts the clock and global listeners. Calling this method more than once is harmless. */
1245
1350
  initialize() {
1246
1351
  this.assertAlive();
@@ -1282,12 +1387,12 @@ var ShimejiEngine = class {
1282
1387
  this.assertAlive();
1283
1388
  const spec = this.specs.get(characterId);
1284
1389
  if (!spec) throw new Error(`Character '${characterId}' is not registered`);
1285
- const bounds = this.dom.getBounds();
1390
+ const { bounds, platforms } = this.readFrameGeometry();
1286
1391
  const random = this.options.random ?? Math.random;
1287
1392
  const spawnOptions = {
1288
1393
  ...position,
1289
1394
  x: position.x ?? bounds.x + random() * bounds.width,
1290
- y: position.y ?? bounds.y + random() * bounds.height
1395
+ y: position.y ?? bounds.y
1291
1396
  };
1292
1397
  const id = `shimeji-${this.nextMascotId++}`;
1293
1398
  const mascot = new Mascot(id, spec, this.dom, this.options, spawnOptions, {
@@ -1303,7 +1408,7 @@ var ShimejiEngine = class {
1303
1408
  error: (error) => this.events.emit("error", error)
1304
1409
  });
1305
1410
  this.mascots.set(id, mascot);
1306
- mascot.tick(0, bounds);
1411
+ mascot.tick(0, bounds, platforms);
1307
1412
  const state = mascot.snapshot();
1308
1413
  this.events.emit("spawn", state);
1309
1414
  this.emitState();
@@ -1335,6 +1440,11 @@ var ShimejiEngine = class {
1335
1440
  this.assertAlive();
1336
1441
  return this.events.on(event, listener);
1337
1442
  }
1443
+ /** Replaces the DOM elements (or selector) exposed to mascots as platforms. */
1444
+ setPlatforms(platforms) {
1445
+ this.assertAlive();
1446
+ this.platformSource = platforms;
1447
+ }
1338
1448
  /** Stops animation and timers, removes listeners and DOM, and revokes all object URLs. */
1339
1449
  destroy() {
1340
1450
  if (this.destroyed) return;
@@ -1362,14 +1472,20 @@ var ShimejiEngine = class {
1362
1472
  const rawDelta = this.lastFrameTime === void 0 ? this.options.frameDuration : timestamp - this.lastFrameTime;
1363
1473
  this.lastFrameTime = timestamp;
1364
1474
  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);
1475
+ const { bounds, platforms } = this.readFrameGeometry();
1476
+ for (const mascot of [...this.mascots.values()]) mascot.tick(delta, bounds, platforms);
1367
1477
  this.emitState();
1368
1478
  this.animationFrame = requestAnimationFrame(this.onAnimationFrame);
1369
1479
  };
1370
1480
  renderAll() {
1371
- const bounds = this.dom.getBounds();
1372
- for (const mascot of this.mascots.values()) mascot.tick(0, bounds);
1481
+ const { bounds, platforms } = this.readFrameGeometry();
1482
+ for (const mascot of this.mascots.values()) mascot.tick(0, bounds, platforms);
1483
+ }
1484
+ readFrameGeometry() {
1485
+ const workAreaRectangle = this.dom.workArea.getBoundingClientRect();
1486
+ const bounds = this.dom.getBoundsFromClientRectangle(workAreaRectangle);
1487
+ const elements = resolvePlatformElements(this.platformSource, this.container.ownerDocument, this.dom.workArea);
1488
+ return { bounds, platforms: readPlatformRectangles(elements, workAreaRectangle) };
1373
1489
  }
1374
1490
  emitState() {
1375
1491
  this.events.emit("statechange", this.getState());
@@ -1403,6 +1519,8 @@ var ShimejiEngine = class {
1403
1519
  normalizeCharacterSpec,
1404
1520
  parseActionsXml,
1405
1521
  parseBehaviorsXml,
1522
+ readPlatformRectangles,
1523
+ resolvePlatformElements,
1406
1524
  selectWeighted
1407
1525
  });
1408
1526
  //# sourceMappingURL=index.cjs.map