yuuna-engine 0.5.0 → 0.7.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/lib/index.js CHANGED
@@ -147,6 +147,12 @@ const createPlaceholderSheet = (size) => {
147
147
  }
148
148
  return canvas;
149
149
  };
150
+ // Used only when a resource declares neither `size` (see settle() below)
151
+ // nor actually loads — there's no image to measure and nothing declared
152
+ // to fall back to, so there's no way to know the intended dimensions at
153
+ // all. An arbitrary, small-but-visible size, purely so the placeholder
154
+ // still draws as *something* instead of a 0x0/NaN canvas.
155
+ const DEFAULT_PLACEHOLDER_SIZE = { width: 64, height: 64 };
150
156
  const runEngine = async (props) => {
151
157
  const runId = ++latestRunId;
152
158
  resetCanvas?.();
@@ -170,9 +176,64 @@ const runEngine = async (props) => {
170
176
  if (props.canvas?.backgroundColor !== undefined) {
171
177
  canvas.style.backgroundColor = props.canvas.backgroundColor;
172
178
  }
179
+ // The canvas's *logical* resolution — the fixed space every renderable
180
+ // position, and every mouse/touch coordinate, is expressed in. Captured
181
+ // here, before pixelRatio (below) scales the actual backing buffer past
182
+ // it, so both stay anchored to this regardless of what pixelRatio does.
183
+ const logicalWidth = canvas.width;
184
+ const logicalHeight = canvas.height;
185
+ // Scales the backing buffer beyond logicalWidth/logicalHeight so text
186
+ // and vector shapes (fillText, arc, ...) render crisply on a HiDPI
187
+ // screen — most phones — instead of the same logical-resolution buffer
188
+ // just being stretched larger by applyResize below. `true` follows the
189
+ // display's own devicePixelRatio; a number sets it explicitly; leaving
190
+ // this unset keeps today's 1x behavior. Sprites are unaffected either
191
+ // way — imageSmoothingEnabled stays false below regardless — so pixel
192
+ // art has no reason to turn this on.
193
+ const pixelRatio = props.canvas?.pixelRatio === true ? window.devicePixelRatio || 1 : props.canvas?.pixelRatio ?? 1;
194
+ if (pixelRatio !== 1) {
195
+ // Resizing the backing buffer resets the 2D context's transform (and
196
+ // everything else about its state), so context.scale below has to
197
+ // come after this, not before.
198
+ canvas.width = logicalWidth * pixelRatio;
199
+ canvas.height = logicalHeight * pixelRatio;
200
+ canvas.style.width = `${logicalWidth}px`;
201
+ canvas.style.height = `${logicalHeight}px`;
202
+ context.scale(pixelRatio, pixelRatio);
203
+ }
173
204
  // Make the canvas focusable so keyboard input is scoped to it instead of
174
205
  // leaking to the rest of the page (e.g. arrow keys scrolling the window).
175
206
  canvas.tabIndex = 0;
207
+ // Stops the browser from treating a drag/pinch on the canvas as page
208
+ // scroll/zoom — backs up the touchstart/touchmove preventDefault calls
209
+ // below for gestures (e.g. a pinch starting on the canvas) preventDefault
210
+ // alone doesn't reliably stop.
211
+ canvas.style.touchAction = "none";
212
+ // Resizes the *display* size only (CSS width/height) — canvas.width/
213
+ // height above stays the fixed logical resolution every renderable's
214
+ // position is already expressed in, so this never needs to touch any
215
+ // of that math. See RunEngineProps.canvas.resize for the mode semantics.
216
+ const applyResize = () => {
217
+ const mode = props.canvas?.resize;
218
+ if (mode === undefined || mode === "none") {
219
+ return;
220
+ }
221
+ if (mode === "stretch") {
222
+ canvas.style.width = `${window.innerWidth}px`;
223
+ canvas.style.height = `${window.innerHeight}px`;
224
+ return;
225
+ }
226
+ const scale = Math.min(window.innerWidth / canvas.width, window.innerHeight / canvas.height);
227
+ canvas.style.width = `${canvas.width * scale}px`;
228
+ canvas.style.height = `${canvas.height * scale}px`;
229
+ };
230
+ applyResize();
231
+ window.addEventListener("resize", applyResize);
232
+ const requestFullscreen = () => canvas.requestFullscreen();
233
+ // exitFullscreen() rejects with a TypeError if nothing is fullscreen —
234
+ // guarded into a no-op instead, so callers don't need to track that
235
+ // state themselves just to call this safely.
236
+ const exitFullscreen = () => window.document.fullscreenElement === null ? Promise.resolve() : window.document.exitFullscreen();
176
237
  let state = props.initialState;
177
238
  const events = [];
178
239
  // Lets a caller report something that happened outside the render loop
@@ -185,24 +246,35 @@ const runEngine = async (props) => {
185
246
  const resources = props.resources ?? {};
186
247
  const resourceById = await iterateRecordAsync(resources, async ({ value }) => new Promise((resolve) => {
187
248
  const image = new Image();
188
- const settle = (loadedImage) => {
249
+ // A single, unsliced image (1x1) if unset — only an actual
250
+ // spritesheet needs this declared.
251
+ const slices = value.slices ?? { horizontal: 1, vertical: 1 };
252
+ // sheetSize is the whole loaded sheet's pixel dimensions — value's
253
+ // declared `size` if set, otherwise whatever the image actually
254
+ // measures once it's loaded (or DEFAULT_PLACEHOLDER_SIZE if even
255
+ // that isn't available, i.e. no `size` declared *and* the image
256
+ // failed to load too).
257
+ const settle = (loadedImage, sheetSize) => {
189
258
  resolve({
190
259
  image: loadedImage,
191
260
  size: {
192
- width: value.size.width / value.slices.horizontal,
193
- height: value.size.height / value.slices.vertical,
261
+ width: sheetSize.width / slices.horizontal,
262
+ height: sheetSize.height / slices.vertical,
194
263
  },
195
- slices: value.slices,
264
+ slices,
196
265
  animations: value.animations ?? {},
197
266
  });
198
267
  };
199
268
  image.src = value.src;
200
- image.onload = () => settle(image);
269
+ image.onload = () => settle(image, value.size ?? { width: image.naturalWidth, height: image.naturalHeight });
201
270
  // Missing/failed-to-load asset (see .gitignore's dist/resources/
202
271
  // note) — a placeholder sheet, sized to match what this resource
203
272
  // declared, keeps every frame/slice/animation index the example
204
273
  // already computes valid instead of drawing nothing or throwing.
205
- image.onerror = () => settle(createPlaceholderSheet(value.size));
274
+ image.onerror = () => {
275
+ const placeholderSize = value.size ?? DEFAULT_PLACEHOLDER_SIZE;
276
+ settle(createPlaceholderSheet(placeholderSize), placeholderSize);
277
+ };
206
278
  }));
207
279
  const loadAudio = (src) => new Promise((resolve) => {
208
280
  const audio = new Audio(src);
@@ -279,9 +351,11 @@ const runEngine = async (props) => {
279
351
  };
280
352
  // A newer runEngine() call started while this one was still loading
281
353
  // resources (e.g. a spritesheet) — abandon this run instead of setting
282
- // up a second, orphaned render loop alongside the newer one.
354
+ // up a second, orphaned render loop alongside the newer one. The
355
+ // fullscreen functions are no-ops here since this run never gets far
356
+ // enough to own the canvas — the newer run's are the ones that matter.
283
357
  if (runId !== latestRunId) {
284
- return { sendEvent };
358
+ return { sendEvent, requestFullscreen: () => Promise.resolve(), exitFullscreen: () => Promise.resolve() };
285
359
  }
286
360
  context.imageSmoothingEnabled = false;
287
361
  // An offscreen 1x1 canvas used only to resolve a CSS color string (a
@@ -561,8 +635,20 @@ const runEngine = async (props) => {
561
635
  // the same conversion to line up.
562
636
  const getCanvasPosition = (ev) => {
563
637
  return {
564
- x: (ev.offsetX * canvas.width) / canvas.clientWidth,
565
- y: (ev.offsetY * canvas.height) / canvas.clientHeight,
638
+ x: (ev.offsetX * logicalWidth) / canvas.clientWidth,
639
+ y: (ev.offsetY * logicalHeight) / canvas.clientHeight,
640
+ };
641
+ };
642
+ // Touch's equivalent of getCanvasPosition — a Touch has no offsetX/Y
643
+ // (that's a MouseEvent-only convenience), so this gets there manually
644
+ // via getBoundingClientRect instead. clientX/Y and the rect are both
645
+ // viewport-relative, so their difference stays correct regardless of
646
+ // page scroll.
647
+ const getTouchPosition = (touch) => {
648
+ const rect = canvas.getBoundingClientRect();
649
+ return {
650
+ x: ((touch.clientX - rect.left) * logicalWidth) / canvas.clientWidth,
651
+ y: ((touch.clientY - rect.top) * logicalHeight) / canvas.clientHeight,
566
652
  };
567
653
  };
568
654
  const updateState = (updateFn) => {
@@ -571,8 +657,11 @@ const runEngine = async (props) => {
571
657
  const nextStateFns = Array.isArray(props.nextState) ? props.nextState : [props.nextState];
572
658
  let lastFrame = Date.now();
573
659
  let hoveredId = null;
574
- const handleClick = (ev) => {
575
- const mouse = getCanvasPosition(ev);
660
+ // Shared by the mouse "click" listener and the touch handlers below —
661
+ // firing a CLICK is the same "is whatever's currently hovered
662
+ // isClickable" check either way; only how `mouse` was determined
663
+ // differs (a real click event vs. a lifted finger).
664
+ const fireClick = (mouse) => {
576
665
  if (hoveredId === null)
577
666
  return;
578
667
  const { renderables, camera } = renderState(state);
@@ -581,6 +670,7 @@ const runEngine = async (props) => {
581
670
  events.push({ tag: "CLICK", id: hovered.id, mouse, worldMouse: toWorldPosition(mouse, camera) });
582
671
  }
583
672
  };
673
+ const handleClick = (ev) => fireClick(getCanvasPosition(ev));
584
674
  canvas.addEventListener("click", handleClick);
585
675
  const initialState = {
586
676
  keyboardState: createRecord(keyboardKeys, () => false),
@@ -610,8 +700,34 @@ const runEngine = async (props) => {
610
700
  }
611
701
  };
612
702
  canvas.addEventListener("keyup", handleKeyUp);
613
- const handleMouseMoveHover = (ev) => {
614
- const mouse = getCanvasPosition(ev);
703
+ // mouseButton state (see NextStateProps.mouseButton) — a double-buffer
704
+ // exactly like keyboardState above, just for the primary mouse button.
705
+ let previousMouseButtonState = { isPressed: false };
706
+ const currentMouseButtonState = { isPressed: false };
707
+ const handleMouseDown = (event) => {
708
+ // Left/primary button only — matching CLICK, which already only
709
+ // ever fires for it.
710
+ if (event.button === 0) {
711
+ currentMouseButtonState.isPressed = true;
712
+ }
713
+ };
714
+ canvas.addEventListener("mousedown", handleMouseDown);
715
+ const handleMouseUp = (event) => {
716
+ if (event.button === 0) {
717
+ currentMouseButtonState.isPressed = false;
718
+ }
719
+ };
720
+ // On window rather than the canvas — so releasing the button after
721
+ // having dragged off the canvas while still holding it down still
722
+ // clears isPressed, instead of leaving it stuck true forever.
723
+ window.addEventListener("mouseup", handleMouseUp);
724
+ // Shared by mousemove and the touch handlers below — updates hoveredId
725
+ // from a canvas position and fires HOVER_IN/HOVER_OUT as whatever's
726
+ // underneath it changes. Touch has no ambient hover the way a mouse
727
+ // does (nothing is "hovered" until a finger actually touches down), but
728
+ // feeding a touch's position through this the same as the mouse's is
729
+ // what lets an isHoverable renderable react to a tap/drag at all.
730
+ const updateHover = (mouse) => {
615
731
  const { renderables, camera } = renderState(state);
616
732
  const worldMouse = toWorldPosition(mouse, camera);
617
733
  const hovered = [...renderables]
@@ -628,9 +744,9 @@ const runEngine = async (props) => {
628
744
  }
629
745
  hoveredId = hovered === undefined ? null : hovered.id ?? null;
630
746
  };
747
+ const handleMouseMoveHover = (ev) => updateHover(getCanvasPosition(ev));
631
748
  canvas.addEventListener("mousemove", handleMouseMoveHover);
632
- const handleMouseMoveTracking = (ev) => {
633
- const mouse = getCanvasPosition(ev);
749
+ const updateTracking = (mouse) => {
634
750
  if (hoveredId === null) {
635
751
  return;
636
752
  }
@@ -640,13 +756,17 @@ const runEngine = async (props) => {
640
756
  events.push({ tag: "MOUSE_MOVE", mouse, worldMouse: toWorldPosition(mouse, camera), id: hovered.id });
641
757
  }
642
758
  };
759
+ const handleMouseMoveTracking = (ev) => updateTracking(getCanvasPosition(ev));
643
760
  canvas.addEventListener("mousemove", handleMouseMoveTracking);
644
761
  // No further mousemove fires once the mouse is off the canvas, so this
645
762
  // is also the only chance to report a HOVER_OUT for whatever was
646
763
  // hovered when it left — otherwise that hover would just dangle,
647
764
  // never explicitly ended.
648
- const handleMouseLeave = (ev) => {
649
- const mouse = getCanvasPosition(ev);
765
+ // Shared by mouseleave and the touch handlers below — clears whatever's
766
+ // hovered (firing HOVER_OUT for it) without a HOVER_IN taking its
767
+ // place. Returns worldMouse so callers that need it (MOUSE_LEAVE below)
768
+ // don't have to call renderState() a second time just to get it.
769
+ const clearHover = (mouse) => {
650
770
  const { renderables, camera } = renderState(state);
651
771
  const worldMouse = toWorldPosition(mouse, camera);
652
772
  if (hoveredId !== null) {
@@ -656,9 +776,79 @@ const runEngine = async (props) => {
656
776
  }
657
777
  }
658
778
  hoveredId = null;
779
+ return worldMouse;
780
+ };
781
+ const handleMouseLeave = (ev) => {
782
+ const mouse = getCanvasPosition(ev);
783
+ const worldMouse = clearHover(mouse);
659
784
  events.push({ tag: "MOUSE_LEAVE", mouse, worldMouse });
660
785
  };
661
786
  canvas.addEventListener("mouseleave", handleMouseLeave);
787
+ // Translates touch into the same HOVER_IN/HOVER_OUT/MOUSE_MOVE/CLICK
788
+ // events mouse input already produces (via updateHover/updateTracking/
789
+ // fireClick/clearHover above), so existing game code written against
790
+ // those events works on a touchscreen with no changes of its own.
791
+ // Single-touch only — touches[0]/changedTouches[0] — the same "one
792
+ // active pointer" model mouse input already assumes; a second finger is
793
+ // ignored rather than tracked as its own pointer.
794
+ //
795
+ // preventDefault on start/move keeps a drag/tap on the canvas from also
796
+ // scrolling, pinch-zooming, or triggering pull-to-refresh — the browser
797
+ // gestures a touchscreen normally reserves that space for; { passive:
798
+ // false } is what makes preventDefault actually take effect here.
799
+ const handleTouchStart = (ev) => {
800
+ ev.preventDefault();
801
+ const touch = ev.touches[0];
802
+ if (touch === undefined)
803
+ return;
804
+ currentMouseButtonState.isPressed = true;
805
+ const mouse = getTouchPosition(touch);
806
+ updateHover(mouse);
807
+ updateTracking(mouse);
808
+ };
809
+ canvas.addEventListener("touchstart", handleTouchStart, { passive: false });
810
+ const handleTouchMove = (ev) => {
811
+ ev.preventDefault();
812
+ const touch = ev.touches[0];
813
+ if (touch === undefined)
814
+ return;
815
+ const mouse = getTouchPosition(touch);
816
+ updateHover(mouse);
817
+ updateTracking(mouse);
818
+ };
819
+ canvas.addEventListener("touchmove", handleTouchMove, { passive: false });
820
+ // A lifted finger both releases (mirroring mouseup) and clicks
821
+ // (mirroring the browser's own click-after-mouseup) — touch has no
822
+ // separate "up" and "click" events of its own the way mouse does, so
823
+ // both happen here together, in that order. hover is updated once more
824
+ // first so a plain tap (touchstart immediately followed by touchend,
825
+ // with no touchmove between them to have already done this) still
826
+ // fires CLICK against whatever's actually under it; hover is then
827
+ // cleared, since nothing's left touching it once the finger lifts.
828
+ const handleTouchEnd = (ev) => {
829
+ ev.preventDefault();
830
+ currentMouseButtonState.isPressed = false;
831
+ const touch = ev.changedTouches[0];
832
+ if (touch === undefined)
833
+ return;
834
+ const mouse = getTouchPosition(touch);
835
+ updateHover(mouse);
836
+ fireClick(mouse);
837
+ clearHover(mouse);
838
+ };
839
+ canvas.addEventListener("touchend", handleTouchEnd, { passive: false });
840
+ // A cancelled touch (e.g. an incoming call interrupting the page, or
841
+ // the OS deciding it's a system gesture instead) never fires touchend —
842
+ // handled the same as lifting the finger, minus the click, since
843
+ // there's no tap to speak of once the touch itself has been cancelled.
844
+ const handleTouchCancel = (ev) => {
845
+ currentMouseButtonState.isPressed = false;
846
+ const touch = ev.changedTouches[0];
847
+ if (touch === undefined)
848
+ return;
849
+ clearHover(getTouchPosition(touch));
850
+ };
851
+ canvas.addEventListener("touchcancel", handleTouchCancel, { passive: false });
662
852
  // Tab switches are a document-level concern (visibilitychange), not
663
853
  // something that ever reaches the canvas itself the way mouse/keyboard
664
854
  // events do.
@@ -666,6 +856,17 @@ const runEngine = async (props) => {
666
856
  events.push({ tag: window.document.hidden ? "TAB_BLUR" : "TAB_FOCUS" });
667
857
  };
668
858
  window.document.addEventListener("visibilitychange", handleVisibilityChange);
859
+ // Fullscreen is a document-level concern too, and fires for every way
860
+ // fullscreen can change — the requestFullscreen()/exitFullscreen()
861
+ // above, but also things neither of those causes directly, like the
862
+ // user pressing Esc. Re-running applyResize() here (rather than relying
863
+ // solely on the "resize" listener) covers browsers that don't also fire
864
+ // a window resize when fullscreen is toggled.
865
+ const handleFullscreenChange = () => {
866
+ events.push({ tag: "FULLSCREEN_CHANGE", isFullscreen: window.document.fullscreenElement === canvas });
867
+ applyResize();
868
+ };
869
+ window.document.addEventListener("fullscreenchange", handleFullscreenChange);
669
870
  context.imageSmoothingEnabled = false;
670
871
  // Scale is a canvas transform around the renderable's anchor, applied
671
872
  // before its type-specific drawing runs below — everything drawn under
@@ -779,11 +980,17 @@ const runEngine = async (props) => {
779
980
  isJustReleased: !isPressed && previouslyPressed,
780
981
  };
781
982
  });
983
+ const mouseButton = {
984
+ isPressed: currentMouseButtonState.isPressed,
985
+ isJustPressed: currentMouseButtonState.isPressed && !previousMouseButtonState.isPressed,
986
+ isJustReleased: !currentMouseButtonState.isPressed && previousMouseButtonState.isPressed,
987
+ };
782
988
  for (const nextState of nextStateFns) {
783
989
  const result = nextState({
784
990
  state,
785
991
  event,
786
992
  keyboard,
993
+ mouseButton,
787
994
  playSound,
788
995
  playMusic,
789
996
  pauseMusic,
@@ -803,7 +1010,7 @@ const runEngine = async (props) => {
803
1010
  }
804
1011
  }
805
1012
  events.splice(0, events.length);
806
- context.clearRect(0, 0, canvas.width, canvas.height);
1013
+ context.clearRect(0, 0, logicalWidth, logicalHeight);
807
1014
  const { cursor, renderables } = renderState(state);
808
1015
  canvas.style.cursor = cursor ?? "default";
809
1016
  for (const renderable of renderables) {
@@ -836,12 +1043,11 @@ const runEngine = async (props) => {
836
1043
  continue;
837
1044
  }
838
1045
  if (renderable.type === "SPRITE") {
839
- const { opacity = 1, flipX = false, modulate, swapColors } = renderable;
1046
+ const { opacity = 1, flipX = false, frame: frameIndex = 0, modulate, swapColors } = renderable;
840
1047
  const resource = resourceById[renderable.resourceId];
841
1048
  const frame = {
842
- x: renderable.frame % resource.slices.horizontal,
843
- y: Math.floor(renderable.frame / resource.slices.horizontal) %
844
- resource.slices.vertical,
1049
+ x: frameIndex % resource.slices.horizontal,
1050
+ y: Math.floor(frameIndex / resource.slices.horizontal) % resource.slices.vertical,
845
1051
  };
846
1052
  const source = {
847
1053
  x: frame.x * resource.size.width,
@@ -861,7 +1067,7 @@ const runEngine = async (props) => {
861
1067
  let image = resource.image;
862
1068
  let imageSource = source;
863
1069
  if (swapColors !== undefined && swapColors.length > 0) {
864
- image = swappedSpriteFrame(renderable.resourceId, renderable.frame, image, imageSource, swapColors);
1070
+ image = swappedSpriteFrame(renderable.resourceId, frameIndex, image, imageSource, swapColors);
865
1071
  imageSource = { x: 0, y: 0, width: source.width, height: source.height };
866
1072
  }
867
1073
  if (modulate !== undefined) {
@@ -915,6 +1121,7 @@ const runEngine = async (props) => {
915
1121
  }
916
1122
  lastFrame = now;
917
1123
  previousState.keyboardState = { ...currentState.keyboardState };
1124
+ previousMouseButtonState = { ...currentMouseButtonState };
918
1125
  }, 0);
919
1126
  resetCanvas = () => {
920
1127
  clearInterval(intervalId);
@@ -938,15 +1145,62 @@ const runEngine = async (props) => {
938
1145
  canvas.removeEventListener("click", handleClick);
939
1146
  canvas.removeEventListener("keydown", handleKeyDown);
940
1147
  canvas.removeEventListener("keyup", handleKeyUp);
1148
+ canvas.removeEventListener("mousedown", handleMouseDown);
941
1149
  canvas.removeEventListener("mousemove", handleMouseMoveHover);
942
1150
  canvas.removeEventListener("mousemove", handleMouseMoveTracking);
943
1151
  canvas.removeEventListener("mouseleave", handleMouseLeave);
1152
+ canvas.removeEventListener("touchstart", handleTouchStart);
1153
+ canvas.removeEventListener("touchmove", handleTouchMove);
1154
+ canvas.removeEventListener("touchend", handleTouchEnd);
1155
+ canvas.removeEventListener("touchcancel", handleTouchCancel);
1156
+ // On window, not the canvas — see where it's added above for why.
1157
+ window.removeEventListener("mouseup", handleMouseUp);
944
1158
  // This one's on `document`, not the canvas — same reasoning as above,
945
1159
  // just doubly true since `document` isn't even scoped to this canvas.
946
1160
  window.document.removeEventListener("visibilitychange", handleVisibilityChange);
1161
+ window.document.removeEventListener("fullscreenchange", handleFullscreenChange);
1162
+ // Same pile-up risk as the canvas listeners above, but on `window`.
1163
+ window.removeEventListener("resize", applyResize);
947
1164
  };
948
- return { sendEvent };
1165
+ return { sendEvent, requestFullscreen, exitFullscreen };
949
1166
  };
950
1167
 
951
- export { STOP, runEngine };
1168
+ // One factory per Renderable variant — literally just `{ type: "X",
1169
+ // ...props }`, so `sprite({...})` (or `Yuuna.sprite({...})` in the
1170
+ // browser bundle/playground) reads the same as writing the object
1171
+ // literal by hand, minus needing to get `type` right yourself.
1172
+ // Deliberately not a place defaults live: besides SpriteRenderable.frame
1173
+ // (optional at the type level, see types.ts — the engine itself defaults
1174
+ // it to 0), each factory still requires whatever its Renderable type
1175
+ // still requires.
1176
+ const rectangle = (props) => ({
1177
+ type: "RECTANGLE",
1178
+ ...props,
1179
+ });
1180
+ const circle = (props) => ({
1181
+ type: "CIRCLE",
1182
+ ...props,
1183
+ });
1184
+ const text = (props) => ({
1185
+ type: "TEXT",
1186
+ ...props,
1187
+ });
1188
+ const sprite = (props) => ({
1189
+ type: "SPRITE",
1190
+ ...props,
1191
+ });
1192
+ const animatedSprite = (props) => ({
1193
+ type: "ANIMATED_SPRITE",
1194
+ ...props,
1195
+ });
1196
+ const line = (props) => ({
1197
+ type: "LINE",
1198
+ ...props,
1199
+ });
1200
+ const group = (props) => ({
1201
+ type: "GROUP",
1202
+ ...props,
1203
+ });
1204
+
1205
+ export { STOP, animatedSprite, circle, group, line, rectangle, runEngine, sprite, text };
952
1206
  //# sourceMappingURL=index.js.map
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.5.0",
3
+ "version": "0.7.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",
@@ -39,7 +39,9 @@
39
39
  "scripts": {
40
40
  "build": "node scripts/generate-playground-assets.mjs && rollup -c",
41
41
  "watch": "node scripts/generate-playground-assets.mjs && rollup -c -w",
42
- "prepublishOnly": "npm run build"
42
+ "prepublishOnly": "npm run build",
43
+ "sync-assets": "node scripts/sync-playground-assets.mjs",
44
+ "postinstall": "node -e \"const fs=require('fs');try{if(fs.existsSync('.githooks')&&fs.existsSync('.git')){require('child_process').execSync('git config core.hooksPath .githooks')}}catch(e){}\""
43
45
  },
44
46
  "devDependencies": {
45
47
  "nodemon": "^3.1.0",
@@ -1,6 +0,0 @@
1
- export declare const CONSTANTS: {
2
- WINDOW: {
3
- WIDTH: number;
4
- HEIGHT: number;
5
- };
6
- };