yuuna-engine 0.8.0 → 0.10.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/README.md CHANGED
@@ -84,7 +84,7 @@ runEngine<GameState>({
84
84
  `CIRCLE`, `TEXT`, `SPRITE`, `ANIMATED_SPRITE`, `LINE`, and `GROUP`. Give
85
85
  one an `id` plus `isClickable`/`isHoverable` to make it interactive.
86
86
  - **Events** — `nextState` receives one `GameEvent` per call: `TIME`,
87
- `CLICK`, `HOVER_IN`, `HOVER_OUT`, `MOUSE_MOVE`, `MOUSE_LEAVE`,
87
+ `CLICK`, `RIGHT_CLICK`, `HOVER_IN`, `HOVER_OUT`, `MOUSE_MOVE`, `MOUSE_LEAVE`,
88
88
  `MUSIC_END`, or a `CUSTOM` event of a type you define yourself, for
89
89
  reporting things like an async `fetch()` resolving back into your
90
90
  state machine.
@@ -117,6 +117,18 @@ export type ClickEvent = {
117
117
  y: number;
118
118
  };
119
119
  };
120
+ export type RightClickEvent = {
121
+ tag: "RIGHT_CLICK";
122
+ id?: string;
123
+ mouse: {
124
+ x: number;
125
+ y: number;
126
+ };
127
+ worldMouse: {
128
+ x: number;
129
+ y: number;
130
+ };
131
+ };
120
132
  export type HoverInEvent = {
121
133
  tag: "HOVER_IN";
122
134
  id?: string;
@@ -178,7 +190,7 @@ export type FullscreenChangeEvent = {
178
190
  tag: "FULLSCREEN_CHANGE";
179
191
  isFullscreen: boolean;
180
192
  };
181
- export type GameEvent = TimeEvent | ClickEvent | HoverInEvent | HoverOutEvent | MouseMoveEvent | MouseLeaveEvent | TabBlurEvent | TabFocusEvent | MusicEndEvent | FullscreenChangeEvent;
193
+ export type GameEvent = TimeEvent | ClickEvent | RightClickEvent | HoverInEvent | HoverOutEvent | MouseMoveEvent | MouseLeaveEvent | TabBlurEvent | TabFocusEvent | MusicEndEvent | FullscreenChangeEvent;
182
194
  export type CustomGameEvent<Custom> = {
183
195
  tag: "CUSTOM";
184
196
  event: Custom;
@@ -196,6 +208,11 @@ export type NextStateProps<State, Custom = never> = {
196
208
  isJustPressed: boolean;
197
209
  isJustReleased: boolean;
198
210
  };
211
+ rightMouseButton: {
212
+ isPressed: boolean;
213
+ isJustPressed: boolean;
214
+ isJustReleased: boolean;
215
+ };
199
216
  playSound: (id: string) => void;
200
217
  playMusic: (id: string) => void;
201
218
  pauseMusic: () => void;
@@ -240,6 +257,7 @@ export type RunEngineProps<State, Custom = never> = {
240
257
  height?: number;
241
258
  backgroundColor?: string;
242
259
  resize?: "none" | "fit" | "stretch";
260
+ disableContextMenu?: boolean;
243
261
  pixelRatio?: number | true;
244
262
  };
245
263
  camera?: (state: State) => {
@@ -248,6 +266,7 @@ export type RunEngineProps<State, Custom = never> = {
248
266
  zoom: number;
249
267
  };
250
268
  timeScale?: (state: State) => number;
269
+ maxFps?: (state: State) => number;
251
270
  };
252
271
  export type RunEngineFunction = <State, Custom = never>(props: RunEngineProps<State, Custom>) => Promise<{
253
272
  sendEvent: (event: Custom) => void;
package/lib/index.cjs CHANGED
@@ -680,6 +680,9 @@ const runEngine = async (props) => {
680
680
  };
681
681
  const nextStateFns = Array.isArray(props.nextState) ? props.nextState : [props.nextState];
682
682
  let lastFrame = Date.now();
683
+ // When the next tick is due under RunEngineProps.maxFps — only read
684
+ // or advanced while a cap is set (see the top of the loop below).
685
+ let nextTickAt = 0;
683
686
  let hoveredId = null;
684
687
  // How much this tick's simulated time is scaled by (see
685
688
  // RunEngineProps.timeScale) — set once per tick, right before that
@@ -689,21 +692,33 @@ const runEngine = async (props) => {
689
692
  // one tick rather than each recomputing props.timeScale(state)
690
693
  // separately (state may itself have just changed this same tick).
691
694
  let currentTimeScale = 1;
692
- // Shared by the mouse "click" listener and the touch handlers below —
693
- // firing a CLICK is the same "is whatever's currently hovered
694
- // isClickable" check either way; only how `mouse` was determined
695
- // differs (a real click event vs. a lifted finger).
696
- const fireClick = (mouse) => {
695
+ // Shared by the mouse "click"/"contextmenu" listeners and the touch
696
+ // handlers below — firing a CLICK (or RIGHT_CLICK) is the same "is
697
+ // whatever's currently hovered isClickable" check either way; only how
698
+ // `mouse` was determined differs (a real click event vs. a lifted
699
+ // finger), and which button it was.
700
+ const fireClick = (mouse, tag = "CLICK") => {
697
701
  if (hoveredId === null)
698
702
  return;
699
703
  const { renderables, camera } = renderState(state);
700
704
  const hovered = renderables.find((e) => e.id === hoveredId);
701
705
  if (hovered !== undefined && hovered.isClickable) {
702
- events.push({ tag: "CLICK", id: hovered.id, mouse, worldMouse: toWorldPosition(mouse, camera) });
706
+ events.push({ tag, id: hovered.id, mouse, worldMouse: toWorldPosition(mouse, camera) });
703
707
  }
704
708
  };
705
709
  const handleClick = (ev) => fireClick(getCanvasPosition(ev));
706
710
  canvas.addEventListener("click", handleClick);
711
+ // contextmenu rather than mouseup with button 2 — it's the browser's
712
+ // own "secondary click" (Ctrl+click on macOS included), the same way
713
+ // "click" above is its primary one, and it's the event that has to be
714
+ // cancelled to keep the browser's menu from opening anyway.
715
+ const handleContextMenu = (ev) => {
716
+ if (props.canvas?.disableContextMenu ?? true) {
717
+ ev.preventDefault();
718
+ }
719
+ fireClick(getCanvasPosition(ev), "RIGHT_CLICK");
720
+ };
721
+ canvas.addEventListener("contextmenu", handleContextMenu);
707
722
  const initialState = {
708
723
  keyboardState: createRecord(keyboardKeys, () => false),
709
724
  };
@@ -732,22 +747,30 @@ const runEngine = async (props) => {
732
747
  }
733
748
  };
734
749
  canvas.addEventListener("keyup", handleKeyUp);
735
- // mouseButton state (see NextStateProps.mouseButton) — a double-buffer
736
- // exactly like keyboardState above, just for the primary mouse button.
750
+ // mouseButton/rightMouseButton state (see NextStateProps.mouseButton)
751
+ // — double-buffers exactly like keyboardState above, one per button.
737
752
  let previousMouseButtonState = { isPressed: false };
738
753
  const currentMouseButtonState = { isPressed: false };
754
+ let previousRightMouseButtonState = { isPressed: false };
755
+ const currentRightMouseButtonState = { isPressed: false };
739
756
  const handleMouseDown = (event) => {
740
- // Left/primary button only — matching CLICK, which already only
741
- // ever fires for it.
757
+ // Primary and secondary buttons only — matching CLICK and
758
+ // RIGHT_CLICK respectively. Middle/back/forward stay ignored.
742
759
  if (event.button === 0) {
743
760
  currentMouseButtonState.isPressed = true;
744
761
  }
762
+ else if (event.button === 2) {
763
+ currentRightMouseButtonState.isPressed = true;
764
+ }
745
765
  };
746
766
  canvas.addEventListener("mousedown", handleMouseDown);
747
767
  const handleMouseUp = (event) => {
748
768
  if (event.button === 0) {
749
769
  currentMouseButtonState.isPressed = false;
750
770
  }
771
+ else if (event.button === 2) {
772
+ currentRightMouseButtonState.isPressed = false;
773
+ }
751
774
  };
752
775
  // On window rather than the canvas — so releasing the button after
753
776
  // having dragged off the canvas while still holding it down still
@@ -1002,6 +1025,27 @@ const runEngine = async (props) => {
1002
1025
  const intervalId = setInterval(() => {
1003
1026
  const now = Date.now();
1004
1027
  const rawDelta = now - lastFrame;
1028
+ // Skipped rather than delayed (see RunEngineProps.maxFps) — returning
1029
+ // before anything else runs leaves `events`, keyboard/mouse state and
1030
+ // lastFrame all untouched, so whatever happened in the meantime is
1031
+ // still delivered by the next tick that does run, and its TIME delta
1032
+ // covers this skipped one too.
1033
+ const maxFps = props.maxFps?.(state);
1034
+ if (maxFps !== undefined && maxFps > 0) {
1035
+ if (now < nextTickAt) {
1036
+ return;
1037
+ }
1038
+ // Scheduled from the previous target rather than from `now`: the
1039
+ // interval only fires every ~4ms, so "at least 1000/maxFps since
1040
+ // the last tick" would always round late (a 60 cap landing on
1041
+ // 20ms ticks, i.e. 50 FPS), while this lets the early and late
1042
+ // ticks average out to the cap itself. Falling more than a whole
1043
+ // interval behind (a hitch, a backgrounded tab, the cap having
1044
+ // just been turned on) restarts the schedule from now instead of
1045
+ // bursting through every missed tick to catch up.
1046
+ const interval = 1000 / maxFps;
1047
+ nextTickAt = now - nextTickAt > interval ? now + interval : nextTickAt + interval;
1048
+ }
1005
1049
  // Read from state as this tick starts (i.e. still last tick's own
1006
1050
  // result) — resolveAnimatedSprite (below, during this same tick's
1007
1051
  // render pass) reads this same currentTimeScale rather than calling
@@ -1024,12 +1068,18 @@ const runEngine = async (props) => {
1024
1068
  isJustPressed: currentMouseButtonState.isPressed && !previousMouseButtonState.isPressed,
1025
1069
  isJustReleased: !currentMouseButtonState.isPressed && previousMouseButtonState.isPressed,
1026
1070
  };
1071
+ const rightMouseButton = {
1072
+ isPressed: currentRightMouseButtonState.isPressed,
1073
+ isJustPressed: currentRightMouseButtonState.isPressed && !previousRightMouseButtonState.isPressed,
1074
+ isJustReleased: !currentRightMouseButtonState.isPressed && previousRightMouseButtonState.isPressed,
1075
+ };
1027
1076
  for (const nextState of nextStateFns) {
1028
1077
  const result = nextState({
1029
1078
  state,
1030
1079
  event,
1031
1080
  keyboard,
1032
1081
  mouseButton,
1082
+ rightMouseButton,
1033
1083
  playSound,
1034
1084
  playMusic,
1035
1085
  pauseMusic,
@@ -1161,6 +1211,7 @@ const runEngine = async (props) => {
1161
1211
  lastFrame = now;
1162
1212
  previousState.keyboardState = { ...currentState.keyboardState };
1163
1213
  previousMouseButtonState = { ...currentMouseButtonState };
1214
+ previousRightMouseButtonState = { ...currentRightMouseButtonState };
1164
1215
  }, 0);
1165
1216
  resetCanvas = () => {
1166
1217
  clearInterval(intervalId);
@@ -1182,6 +1233,7 @@ const runEngine = async (props) => {
1182
1233
  // slow enough device or long enough playground session, that pile-up
1183
1234
  // is what "clicks stop working" actually looks like.
1184
1235
  canvas.removeEventListener("click", handleClick);
1236
+ canvas.removeEventListener("contextmenu", handleContextMenu);
1185
1237
  canvas.removeEventListener("keydown", handleKeyDown);
1186
1238
  canvas.removeEventListener("keyup", handleKeyUp);
1187
1239
  canvas.removeEventListener("mousedown", handleMouseDown);
package/lib/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/lib/index.js CHANGED
@@ -678,6 +678,9 @@ const runEngine = async (props) => {
678
678
  };
679
679
  const nextStateFns = Array.isArray(props.nextState) ? props.nextState : [props.nextState];
680
680
  let lastFrame = Date.now();
681
+ // When the next tick is due under RunEngineProps.maxFps — only read
682
+ // or advanced while a cap is set (see the top of the loop below).
683
+ let nextTickAt = 0;
681
684
  let hoveredId = null;
682
685
  // How much this tick's simulated time is scaled by (see
683
686
  // RunEngineProps.timeScale) — set once per tick, right before that
@@ -687,21 +690,33 @@ const runEngine = async (props) => {
687
690
  // one tick rather than each recomputing props.timeScale(state)
688
691
  // separately (state may itself have just changed this same tick).
689
692
  let currentTimeScale = 1;
690
- // Shared by the mouse "click" listener and the touch handlers below —
691
- // firing a CLICK is the same "is whatever's currently hovered
692
- // isClickable" check either way; only how `mouse` was determined
693
- // differs (a real click event vs. a lifted finger).
694
- const fireClick = (mouse) => {
693
+ // Shared by the mouse "click"/"contextmenu" listeners and the touch
694
+ // handlers below — firing a CLICK (or RIGHT_CLICK) is the same "is
695
+ // whatever's currently hovered isClickable" check either way; only how
696
+ // `mouse` was determined differs (a real click event vs. a lifted
697
+ // finger), and which button it was.
698
+ const fireClick = (mouse, tag = "CLICK") => {
695
699
  if (hoveredId === null)
696
700
  return;
697
701
  const { renderables, camera } = renderState(state);
698
702
  const hovered = renderables.find((e) => e.id === hoveredId);
699
703
  if (hovered !== undefined && hovered.isClickable) {
700
- events.push({ tag: "CLICK", id: hovered.id, mouse, worldMouse: toWorldPosition(mouse, camera) });
704
+ events.push({ tag, id: hovered.id, mouse, worldMouse: toWorldPosition(mouse, camera) });
701
705
  }
702
706
  };
703
707
  const handleClick = (ev) => fireClick(getCanvasPosition(ev));
704
708
  canvas.addEventListener("click", handleClick);
709
+ // contextmenu rather than mouseup with button 2 — it's the browser's
710
+ // own "secondary click" (Ctrl+click on macOS included), the same way
711
+ // "click" above is its primary one, and it's the event that has to be
712
+ // cancelled to keep the browser's menu from opening anyway.
713
+ const handleContextMenu = (ev) => {
714
+ if (props.canvas?.disableContextMenu ?? true) {
715
+ ev.preventDefault();
716
+ }
717
+ fireClick(getCanvasPosition(ev), "RIGHT_CLICK");
718
+ };
719
+ canvas.addEventListener("contextmenu", handleContextMenu);
705
720
  const initialState = {
706
721
  keyboardState: createRecord(keyboardKeys, () => false),
707
722
  };
@@ -730,22 +745,30 @@ const runEngine = async (props) => {
730
745
  }
731
746
  };
732
747
  canvas.addEventListener("keyup", handleKeyUp);
733
- // mouseButton state (see NextStateProps.mouseButton) — a double-buffer
734
- // exactly like keyboardState above, just for the primary mouse button.
748
+ // mouseButton/rightMouseButton state (see NextStateProps.mouseButton)
749
+ // — double-buffers exactly like keyboardState above, one per button.
735
750
  let previousMouseButtonState = { isPressed: false };
736
751
  const currentMouseButtonState = { isPressed: false };
752
+ let previousRightMouseButtonState = { isPressed: false };
753
+ const currentRightMouseButtonState = { isPressed: false };
737
754
  const handleMouseDown = (event) => {
738
- // Left/primary button only — matching CLICK, which already only
739
- // ever fires for it.
755
+ // Primary and secondary buttons only — matching CLICK and
756
+ // RIGHT_CLICK respectively. Middle/back/forward stay ignored.
740
757
  if (event.button === 0) {
741
758
  currentMouseButtonState.isPressed = true;
742
759
  }
760
+ else if (event.button === 2) {
761
+ currentRightMouseButtonState.isPressed = true;
762
+ }
743
763
  };
744
764
  canvas.addEventListener("mousedown", handleMouseDown);
745
765
  const handleMouseUp = (event) => {
746
766
  if (event.button === 0) {
747
767
  currentMouseButtonState.isPressed = false;
748
768
  }
769
+ else if (event.button === 2) {
770
+ currentRightMouseButtonState.isPressed = false;
771
+ }
749
772
  };
750
773
  // On window rather than the canvas — so releasing the button after
751
774
  // having dragged off the canvas while still holding it down still
@@ -1000,6 +1023,27 @@ const runEngine = async (props) => {
1000
1023
  const intervalId = setInterval(() => {
1001
1024
  const now = Date.now();
1002
1025
  const rawDelta = now - lastFrame;
1026
+ // Skipped rather than delayed (see RunEngineProps.maxFps) — returning
1027
+ // before anything else runs leaves `events`, keyboard/mouse state and
1028
+ // lastFrame all untouched, so whatever happened in the meantime is
1029
+ // still delivered by the next tick that does run, and its TIME delta
1030
+ // covers this skipped one too.
1031
+ const maxFps = props.maxFps?.(state);
1032
+ if (maxFps !== undefined && maxFps > 0) {
1033
+ if (now < nextTickAt) {
1034
+ return;
1035
+ }
1036
+ // Scheduled from the previous target rather than from `now`: the
1037
+ // interval only fires every ~4ms, so "at least 1000/maxFps since
1038
+ // the last tick" would always round late (a 60 cap landing on
1039
+ // 20ms ticks, i.e. 50 FPS), while this lets the early and late
1040
+ // ticks average out to the cap itself. Falling more than a whole
1041
+ // interval behind (a hitch, a backgrounded tab, the cap having
1042
+ // just been turned on) restarts the schedule from now instead of
1043
+ // bursting through every missed tick to catch up.
1044
+ const interval = 1000 / maxFps;
1045
+ nextTickAt = now - nextTickAt > interval ? now + interval : nextTickAt + interval;
1046
+ }
1003
1047
  // Read from state as this tick starts (i.e. still last tick's own
1004
1048
  // result) — resolveAnimatedSprite (below, during this same tick's
1005
1049
  // render pass) reads this same currentTimeScale rather than calling
@@ -1022,12 +1066,18 @@ const runEngine = async (props) => {
1022
1066
  isJustPressed: currentMouseButtonState.isPressed && !previousMouseButtonState.isPressed,
1023
1067
  isJustReleased: !currentMouseButtonState.isPressed && previousMouseButtonState.isPressed,
1024
1068
  };
1069
+ const rightMouseButton = {
1070
+ isPressed: currentRightMouseButtonState.isPressed,
1071
+ isJustPressed: currentRightMouseButtonState.isPressed && !previousRightMouseButtonState.isPressed,
1072
+ isJustReleased: !currentRightMouseButtonState.isPressed && previousRightMouseButtonState.isPressed,
1073
+ };
1025
1074
  for (const nextState of nextStateFns) {
1026
1075
  const result = nextState({
1027
1076
  state,
1028
1077
  event,
1029
1078
  keyboard,
1030
1079
  mouseButton,
1080
+ rightMouseButton,
1031
1081
  playSound,
1032
1082
  playMusic,
1033
1083
  pauseMusic,
@@ -1159,6 +1209,7 @@ const runEngine = async (props) => {
1159
1209
  lastFrame = now;
1160
1210
  previousState.keyboardState = { ...currentState.keyboardState };
1161
1211
  previousMouseButtonState = { ...currentMouseButtonState };
1212
+ previousRightMouseButtonState = { ...currentRightMouseButtonState };
1162
1213
  }, 0);
1163
1214
  resetCanvas = () => {
1164
1215
  clearInterval(intervalId);
@@ -1180,6 +1231,7 @@ const runEngine = async (props) => {
1180
1231
  // slow enough device or long enough playground session, that pile-up
1181
1232
  // is what "clicks stop working" actually looks like.
1182
1233
  canvas.removeEventListener("click", handleClick);
1234
+ canvas.removeEventListener("contextmenu", handleContextMenu);
1183
1235
  canvas.removeEventListener("keydown", handleKeyDown);
1184
1236
  canvas.removeEventListener("keyup", handleKeyUp);
1185
1237
  canvas.removeEventListener("mousedown", handleMouseDown);
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yuuna-engine",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "A lightweight, state-machine-based TypeScript game engine for quick prototypes. Runs directly in the browser.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.cjs",