@cosmoledo/gleam 1.0.0 → 1.0.2

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/gleam.esm.js CHANGED
@@ -84,6 +84,9 @@ function urlBasename(path) {
84
84
 
85
85
  // src/core/EventSystem.ts
86
86
  var _EventSystem = class _EventSystem {
87
+ /**
88
+ * Register a listener for `eventName`. Returns a dispose function — the primary teardown path. Multiple disposers (returned, `once`, `signal.abort`) are idempotent. Use {@link EventSystemOptions} for `once` and `signal` behavior.
89
+ */
87
90
  static addEventListener(eventName, callback, options = {}) {
88
91
  if (options.signal?.aborted) {
89
92
  return function dispose2() {
@@ -118,6 +121,7 @@ var _EventSystem = class _EventSystem {
118
121
  }
119
122
  return dispose;
120
123
  }
124
+ /** Synchronously fire `eventName` with the typed payload. Listeners are invoked in registration order; nested dispatches and self-disposing listeners are handled safely. */
121
125
  static dispatchEvent(eventName, ...params) {
122
126
  const bucket = this.eventListener[eventName];
123
127
  if (!bucket) {
@@ -165,21 +169,25 @@ var MEDIA_ERROR_CODES = {
165
169
  };
166
170
  var AudioBase = class {
167
171
  constructor(enabled = true) {
172
+ /** Registered audio elements keyed by name. Subclasses read this; mutate via {@link register}. */
168
173
  __publicField(this, "songs", /* @__PURE__ */ new Map());
169
174
  __publicField(this, "_enabled");
170
175
  __publicField(this, "registered", false);
171
176
  this._enabled = enabled;
172
177
  EventSystem.addEventListener("gameloopStopped", () => this.stop());
173
178
  }
179
+ /** Whether playback is permitted. Setting to `false` invokes {@link stop} immediately. */
174
180
  get enabled() {
175
181
  return this._enabled;
176
182
  }
183
+ /** Setting to `false` invokes {@link stop} immediately; subclasses (notably {@link Music}) may re-start playback when flipped back to `true`. */
177
184
  set enabled(value) {
178
185
  this._enabled = value;
179
186
  if (!value) {
180
187
  this.stop();
181
188
  }
182
189
  }
190
+ /** Load and register one or more audio files. Each entry may be a bare URL string (the file's basename becomes the name) or a {@link RegisterData} object. Per-song volume falls back to `defaultVolume`. **Call once per instance** — throws on a second invocation, on non-finite volume, or on volume outside `[0, 1]`. Load failures are logged to `console.error` but don't throw. */
183
191
  register(defaultVolume = 1, ...songs) {
184
192
  this.throwOnBadVolume(defaultVolume, "defaultVolume");
185
193
  if (this.registered) {
@@ -208,6 +216,7 @@ var AudioBase = class {
208
216
  this.songs.set(song.name, audio);
209
217
  });
210
218
  }
219
+ /** Base hook called when {@link enabled} flips to `false` and on `"gameloopStopped"`. The default is a no-op; {@link Sound} and {@link Music} override it to cut playback. Subclass overrides should call `super.stop()`. */
211
220
  stop() {
212
221
  }
213
222
  throwOnBadVolume(volume, name) {
@@ -286,7 +295,7 @@ var EASINGS = {
286
295
  "ease-in": easeIn,
287
296
  "ease-in-out": easeInOut,
288
297
  "ease-out": easeOut,
289
- "linear": linear
298
+ linear
290
299
  };
291
300
 
292
301
  // src/utilities/Array.ts
@@ -337,18 +346,24 @@ var Music = class extends AudioBase {
337
346
  __publicField(this, "next", null);
338
347
  __publicField(this, "fadeCancel", null);
339
348
  }
349
+ /** `true` while a fade is in progress OR the current track is actively playing. */
340
350
  get isPlaying() {
341
351
  return !!this.fadeCancel || this.current instanceof window.Audio && !this.current.paused;
342
352
  }
353
+ /** Whether music playback is permitted (inherited from {@link AudioBase}). */
343
354
  get enabled() {
344
355
  return super.enabled;
345
356
  }
357
+ /** Flipping from `false` to `true` while no music is playing auto-starts a fade-in to a random track. */
346
358
  set enabled(value) {
347
359
  super.enabled = value;
348
360
  if (value && !this.isPlaying) {
349
361
  this.fade();
350
362
  }
351
363
  }
364
+ /**
365
+ * Cross-fade to `name` (or a random unplayed track when `null`) over `fadeTime` ms. `easing.cur` controls the outgoing track's volume curve, `easing.next` the incoming one. Cancels any in-progress fade. No-op when disabled. Throws on `fadeTime <= 0`, an empty registry, or an unknown `name`. When the new track ends, the next fade fires automatically — call {@link stop} to break the cycle.
366
+ */
352
367
  fade(name = null, fadeTime = 1e3, easing = {
353
368
  cur: "ease-in",
354
369
  next: "ease-out"
@@ -425,6 +440,7 @@ var Music = class extends AudioBase {
425
440
  }
426
441
  });
427
442
  }
443
+ /** Stop everything immediately: cancels any in-flight fade, halts current and next tracks, and breaks the auto-cycle chain. Restart via {@link fade} or by flipping {@link enabled}. */
428
444
  stop() {
429
445
  super.stop();
430
446
  this.fadeCancel?.();
@@ -456,6 +472,7 @@ var Sound = class extends AudioBase {
456
472
  super(...arguments);
457
473
  __publicField(this, "currentSounds", []);
458
474
  }
475
+ /** Play the registered sound `name` once. Returns a promise that resolves when playback starts (or immediately if `enabled` is `false`) and rejects on autoplay/permission errors. Throws synchronously if no sounds are registered or `name` is unknown. Each call allocates a clone, so concurrent plays of the same name overlap. */
459
476
  play(name) {
460
477
  if (this.songs.size === 0) {
461
478
  throw new Error("No sounds registered!");
@@ -481,6 +498,7 @@ var Sound = class extends AudioBase {
481
498
  throw err;
482
499
  });
483
500
  }
501
+ /** Stop and forget every currently-playing clone. Also calls the base-class teardown. */
484
502
  stop() {
485
503
  super.stop();
486
504
  this.currentSounds.forEach((audio) => audio.stop());
@@ -640,6 +658,7 @@ var Color = class _Color {
640
658
  __publicField(this, "_alpha", 1);
641
659
  this.set(r, g, b, a);
642
660
  }
661
+ /** Parse `#rgb`, `#rgba`, `#rrggbb`, or `#rrggbbaa` (case-insensitive, `#` optional). Throws on any other shape or on non-hex characters. */
643
662
  static fromHex(hex) {
644
663
  let cleanHex = hex.replace("#", "").toUpperCase();
645
664
  if (!/^[0-9A-F]+$/.test(cleanHex)) {
@@ -660,6 +679,7 @@ var Color = class _Color {
660
679
  }
661
680
  return new _Color(r, g, b);
662
681
  }
682
+ /** Build from HSL(A). `h` in **degrees** (wraps mod 360), `s`/`l` in percent `[0, 100]`, `a` in `[0, 1]`. The degree convention matches CSS — note that {@link hueRotate} uses radians instead. */
663
683
  static fromHSL(h, s, l, a = 1) {
664
684
  const hNorm = wrapValue(h, 0, 360) / 360;
665
685
  const sNorm = s / 100;
@@ -676,18 +696,23 @@ var Color = class _Color {
676
696
  }
677
697
  return new _Color(r * 255, g * 255, b * 255, a);
678
698
  }
699
+ /** Red channel, `[0, 255]`. Read-only; mutate via {@link set} or any chainable transform. */
679
700
  get r() {
680
701
  return this._r;
681
702
  }
703
+ /** Green channel, `[0, 255]`. Read-only; mutate via {@link set} or any chainable transform. */
682
704
  get g() {
683
705
  return this._g;
684
706
  }
707
+ /** Blue channel, `[0, 255]`. Read-only; mutate via {@link set} or any chainable transform. */
685
708
  get b() {
686
709
  return this._b;
687
710
  }
711
+ /** Alpha channel, `[0, 1]`. Read-only; mutate via {@link set} (pass the fourth arg). */
688
712
  get alpha() {
689
713
  return this._alpha;
690
714
  }
715
+ /** Primary mutator — every other transform on this class routes through it. Clamps `r`/`g`/`b` to `[0, 255]` and `a` to `[0, 1]`; alpha is snapped to exact `0` or `1` when within `approxEqual` tolerance so equality checks stay clean. Returns `this` for chaining. */
691
716
  set(r, g, b, a) {
692
717
  this._r = clamp(r, 0, 255);
693
718
  this._g = clamp(g, 0, 255);
@@ -698,6 +723,7 @@ var Color = class _Color {
698
723
  }
699
724
  return this;
700
725
  }
726
+ /** Apply a 3×3 RGB color matrix in row-major order (`m1..m9`). Alpha is unchanged. Used by {@link grayscale}, {@link hueRotate}, {@link saturate}, {@link sepia}. Mutates and returns `this`. */
701
727
  applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9) {
702
728
  return this.set(
703
729
  this.r * m1 + this.g * m2 + this.b * m3,
@@ -705,9 +731,11 @@ var Color = class _Color {
705
731
  this.r * m7 + this.g * m8 + this.b * m9
706
732
  );
707
733
  }
734
+ /** Multiply each channel by `factor`. `factor < 1` darkens, `factor > 1` brightens (clamped at 255). Mutates and returns `this`. */
708
735
  brightness(factor) {
709
736
  return this.set(this.r * factor, this.g * factor, this.b * factor);
710
737
  }
738
+ /** Push each channel away from `127.5` (the midtone) by `factor`. `factor < 1` flattens contrast, `> 1` increases it, `0` collapses every channel to gray. Mutates and returns `this`. */
711
739
  contrast(factor) {
712
740
  const midtone = 127.5;
713
741
  return this.set(
@@ -716,6 +744,7 @@ var Color = class _Color {
716
744
  midtone + (this.b - midtone) * factor
717
745
  );
718
746
  }
747
+ /** Desaturate via the standard luminance-preserving matrix. `value` in `[0, 1]`: `0` is a no-op, `1` is full grayscale. Mutates and returns `this`. */
719
748
  grayscale(value = 1) {
720
749
  const m1 = 0.2126 + 0.7874 * (1 - value);
721
750
  const m2 = 0.7152 - 0.7152 * (1 - value);
@@ -728,6 +757,7 @@ var Color = class _Color {
728
757
  const m9 = 0.0722 + 0.9278 * (1 - value);
729
758
  return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
730
759
  }
760
+ /** Rotate hue by `radians` (use `Math.PI / 2` etc.). Unlike {@link fromHSL}, this takes radians, not degrees. Mutates and returns `this`. */
731
761
  hueRotate(radians) {
732
762
  const cos = Math.cos(radians);
733
763
  const sin = Math.sin(radians);
@@ -742,6 +772,7 @@ var Color = class _Color {
742
772
  const m9 = 0.072 + cos * 0.928 + sin * 0.072;
743
773
  return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
744
774
  }
775
+ /** Interpolate each channel toward its inverse (`255 - c`). `factor` in `[0, 1]`: `0` is unchanged, `1` is fully inverted. Mutates and returns `this`. */
745
776
  invert(factor = 1) {
746
777
  return this.set(
747
778
  this.r * (1 - factor) + (255 - this.r) * factor,
@@ -749,6 +780,7 @@ var Color = class _Color {
749
780
  this.b * (1 - factor) + (255 - this.b) * factor
750
781
  );
751
782
  }
783
+ /** Linear blend toward `other`. `amount` in `[0, 1]`: `0` keeps `this`, `1` becomes `other`. Mixes alpha too. Mutates and returns `this`. */
752
784
  mix(other, amount) {
753
785
  const inv = 1 - amount;
754
786
  return this.set(
@@ -758,6 +790,7 @@ var Color = class _Color {
758
790
  this.alpha * inv + other.alpha * amount
759
791
  );
760
792
  }
793
+ /** Round each RGB channel to the nearest integer. Alpha is untouched. Mutates and returns `this`. */
761
794
  round() {
762
795
  return this.set(
763
796
  Math.round(this.r),
@@ -765,6 +798,7 @@ var Color = class _Color {
765
798
  Math.round(this.b)
766
799
  );
767
800
  }
801
+ /** Saturation matrix. `value` typically in `[0, 2]`: `0` desaturates to grayscale (same as `grayscale(1)`), `1` is a no-op, `> 1` oversaturates. Mutates and returns `this`. */
768
802
  saturate(value = 1) {
769
803
  const m1 = 0.213 + 0.787 * value;
770
804
  const m2 = 0.715 - 0.715 * value;
@@ -777,6 +811,7 @@ var Color = class _Color {
777
811
  const m9 = 0.072 + 0.928 * value;
778
812
  return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
779
813
  }
814
+ /** Sepia matrix. `value` in `[0, 1]`: `0` is unchanged, `1` is full sepia. Mutates and returns `this`. */
780
815
  sepia(value = 1) {
781
816
  const m1 = 0.393 + 0.607 * (1 - value);
782
817
  const m2 = 0.769 - 0.769 * (1 - value);
@@ -789,6 +824,7 @@ var Color = class _Color {
789
824
  const m9 = 0.131 + 0.869 * (1 - value);
790
825
  return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
791
826
  }
827
+ /** Tint or shade. `percent` in `[-1, 1]`: negative shades toward black, positive tints toward white, magnitude is the amount. Mutates and returns `this`. */
792
828
  shade(percent) {
793
829
  const target = percent < 0 ? 0 : 255;
794
830
  const p = Math.abs(percent);
@@ -798,6 +834,7 @@ var Color = class _Color {
798
834
  this.b + (target - this.b) * p
799
835
  );
800
836
  }
837
+ /** CSS hex string. `#rrggbb` when alpha is exactly `1`, `#rrggbbaa` otherwise. Channels are rounded. */
801
838
  toHex() {
802
839
  const r = Math.round(this.r).toString(16).padStart(2, "0");
803
840
  const g = Math.round(this.g).toString(16).padStart(2, "0");
@@ -809,6 +846,7 @@ var Color = class _Color {
809
846
  const a = Math.round(this.alpha * 255);
810
847
  return `${rgb}${a.toString(16).padStart(2, "0")}`;
811
848
  }
849
+ /** CSS HSL string. `hsl(h, s%, l%)` when alpha is exactly `1`, `hsla(...)` otherwise. Hue is in degrees. */
812
850
  toHSL() {
813
851
  const { h, s, l } = this.toHSLObject();
814
852
  const cssH = Math.round(h);
@@ -816,6 +854,7 @@ var Color = class _Color {
816
854
  const cssL = Math.round(l);
817
855
  return this.alpha === 1 ? `hsl(${cssH}, ${cssS}%, ${cssL}%)` : `hsla(${cssH}, ${cssS}%, ${cssL}%, ${this.alpha.toFixed(2)})`;
818
856
  }
857
+ /** HSL(A) components as numbers — see {@link HSLObject}. Use when you need to compute against the values rather than render them as a string. */
819
858
  toHSLObject() {
820
859
  const r = this.r / 255;
821
860
  const g = this.g / 255;
@@ -843,15 +882,18 @@ var Color = class _Color {
843
882
  }
844
883
  return { h: h * 360, s: s * 100, l: l * 100, a: this.alpha };
845
884
  }
885
+ /** CSS RGB string. `rgb(r, g, b)` when alpha is exactly `1`, `rgba(r, g, b, a)` otherwise. Channels are rounded. */
846
886
  toRGB() {
847
887
  const r = Math.round(this.r);
848
888
  const g = Math.round(this.g);
849
889
  const b = Math.round(this.b);
850
890
  return this.alpha === 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${this.alpha.toFixed(2)})`;
851
891
  }
892
+ /** New `Color` with the same channels. */
852
893
  clone() {
853
894
  return new _Color(this.r, this.g, this.b, this.alpha);
854
895
  }
896
+ /** Approximate equality (within `approxEqual` tolerance). Pass `compareAlpha: false` to ignore the alpha channel. */
855
897
  equals(other, compareAlpha = true) {
856
898
  return approxEqual(this.r, other.r) && approxEqual(this.g, other.g) && approxEqual(this.b, other.b) && (!compareAlpha || approxEqual(this.alpha, other.alpha));
857
899
  }
@@ -993,12 +1035,14 @@ var Rect = class _Rect {
993
1035
  __publicField(this, "sideIsDirty", true);
994
1036
  this.set(x, y, w, h);
995
1037
  }
1038
+ /** Build from an `HTMLElement` (via `getBoundingClientRect`) or a `DOMRect`. */
996
1039
  static fromBoundingClientRect(rect) {
997
1040
  if (rect instanceof HTMLElement) {
998
1041
  rect = rect.getBoundingClientRect();
999
1042
  }
1000
1043
  return new _Rect(rect.left, rect.top, rect.width, rect.height);
1001
1044
  }
1045
+ /** Axis-aligned bounding box of a polygon's points. Throws if the polygon has no points. */
1002
1046
  static fromPolygon(polygon) {
1003
1047
  if (polygon.points.length === 0) {
1004
1048
  throw new Error("Supplied polygon has no points!");
@@ -1023,34 +1067,43 @@ var Rect = class _Rect {
1023
1067
  });
1024
1068
  return new _Rect(minX, minY, maxX - minX, maxY - minY);
1025
1069
  }
1070
+ /** Height. */
1026
1071
  get h() {
1027
1072
  return this._h;
1028
1073
  }
1074
+ /** Height. */
1029
1075
  set h(value) {
1030
1076
  this._h = value;
1031
1077
  this.sideIsDirty = true;
1032
1078
  }
1079
+ /** Width. */
1033
1080
  get w() {
1034
1081
  return this._w;
1035
1082
  }
1083
+ /** Width. */
1036
1084
  set w(value) {
1037
1085
  this._w = value;
1038
1086
  this.sideIsDirty = true;
1039
1087
  }
1088
+ /** Top-left x. */
1040
1089
  get x() {
1041
1090
  return this._x;
1042
1091
  }
1092
+ /** Top-left x. */
1043
1093
  set x(value) {
1044
1094
  this._x = value;
1045
1095
  this.sideIsDirty = true;
1046
1096
  }
1097
+ /** Top-left y. */
1047
1098
  get y() {
1048
1099
  return this._y;
1049
1100
  }
1101
+ /** Top-left y. */
1050
1102
  set y(value) {
1051
1103
  this._y = value;
1052
1104
  this.sideIsDirty = true;
1053
1105
  }
1106
+ /** Derived sides/center/halfSize. Lazily recomputed after any `x`/`y`/`w`/`h` change. */
1054
1107
  get sides() {
1055
1108
  if (this.sideIsDirty) {
1056
1109
  this._sides = {
@@ -1066,6 +1119,7 @@ var Rect = class _Rect {
1066
1119
  }
1067
1120
  return this._sides;
1068
1121
  }
1122
+ /** Grow on every side by `delta` (`x`/`y` shift in, `w`/`h` grow by `2*delta`). Pass a negative value to shrink. Mutates and returns `this`. */
1069
1123
  inflate(delta) {
1070
1124
  this.x -= delta;
1071
1125
  this.y -= delta;
@@ -1074,12 +1128,16 @@ var Rect = class _Rect {
1074
1128
  this.sideIsDirty = true;
1075
1129
  return this;
1076
1130
  }
1131
+ /** Round `x` and `y` to the nearest integer. `w`/`h` are unchanged. Mutates and returns `this`. */
1077
1132
  round() {
1078
1133
  this.x = Math.round(this.x);
1079
1134
  this.y = Math.round(this.y);
1080
1135
  this.sideIsDirty = true;
1081
1136
  return this;
1082
1137
  }
1138
+ /**
1139
+ * Replace fields. The first arg may be a `Vector4` (sets all four), a `Vector2` (sets `x`/`y` only, unless explicit `w`/`h` follow), or `x` as a number with separate `y`/`w`/`h`. Mutates and returns `this`.
1140
+ */
1083
1141
  set(x = 0, y = 0, w, h) {
1084
1142
  if (typeof x === "number") {
1085
1143
  this.x = x;
@@ -1101,15 +1159,19 @@ var Rect = class _Rect {
1101
1159
  this.sideIsDirty = true;
1102
1160
  return this;
1103
1161
  }
1162
+ /** AABB-vs-AABB overlap test (inclusive of touching edges). */
1104
1163
  collide(rect) {
1105
1164
  return this.x <= rect.x + rect.w && this.x + this.w >= rect.x && this.y <= rect.y + rect.h && this.y + this.h >= rect.y;
1106
1165
  }
1166
+ /** `true` when `rect` is fully inside `this`. */
1107
1167
  collideFull(rect) {
1108
1168
  return rect.x + rect.w <= this.x + this.w && rect.x >= this.x && rect.y >= this.y && rect.y + rect.h <= this.y + this.h;
1109
1169
  }
1170
+ /** `true` when `vec` lies inside `this` (inclusive of edges). */
1110
1171
  collidePoint(vec) {
1111
1172
  return this.x <= vec.x && vec.x <= this.x + this.w && this.y <= vec.y && vec.y <= this.y + this.h;
1112
1173
  }
1174
+ /** Side of `this` that `rect` overlaps from, or `"none"` if disjoint. Useful for picking a bounce axis. */
1113
1175
  collideSide(rect) {
1114
1176
  const dx = this.x + this.w * 0.5 - (rect.x + rect.w * 0.5);
1115
1177
  const dy = this.y + this.h * 0.5 - (rect.y + rect.h * 0.5);
@@ -1127,18 +1189,23 @@ var Rect = class _Rect {
1127
1189
  }
1128
1190
  return collision;
1129
1191
  }
1192
+ /** Top-left corner as a new `Vec2`. */
1130
1193
  pos() {
1131
1194
  return new Vec2(this.x, this.y);
1132
1195
  }
1196
+ /** Width and height as a new `Vec2`. */
1133
1197
  size() {
1134
1198
  return new Vec2(this.w, this.h);
1135
1199
  }
1200
+ /** Debug string like `"Rect [x: 0, y: 0, w: 10, h: 20]"`. */
1136
1201
  toString() {
1137
1202
  return `Rect [x: ${this.x}, y: ${this.y}, w: ${this.w}, h: ${this.h}]`;
1138
1203
  }
1204
+ /** New `Rect` with the same values. */
1139
1205
  clone() {
1140
1206
  return new _Rect(this.x, this.y, this.w, this.h);
1141
1207
  }
1208
+ /** Approximate equality. Pass `withSize: false` to compare position only. */
1142
1209
  equals(other, withSize = true) {
1143
1210
  let output = approxEqual(this.x, other.x) && approxEqual(this.y, other.y);
1144
1211
  if (output && withSize) {
@@ -1170,25 +1237,31 @@ var warnNonFinite = throttle(
1170
1237
  );
1171
1238
  var Vec2 = class _Vec2 {
1172
1239
  constructor(x = 0, y) {
1240
+ /** Horizontal component. */
1173
1241
  __publicField(this, "x", 0);
1242
+ /** Vertical component. */
1174
1243
  __publicField(this, "y", 0);
1175
1244
  this.calculate(Operation.Equal, x, y);
1176
1245
  }
1246
+ /** Unit vector at angle `rad` (radians), scaled per-axis. `scaleY` defaults to `scaleX`. */
1177
1247
  static fromAngle(rad, scaleX = 1, scaleY = scaleX) {
1178
1248
  return new _Vec2(Math.cos(rad) * scaleX, Math.sin(rad) * scaleY);
1179
1249
  }
1180
1250
  set(x, y) {
1181
1251
  return this.calculate(Operation.Equal, x, y);
1182
1252
  }
1253
+ /** Set each component to its absolute value. Mutates and returns `this`. */
1183
1254
  abs() {
1184
1255
  return this.map(Math.abs);
1185
1256
  }
1186
1257
  add(x, y) {
1187
1258
  return this.calculate(Operation.Add, x, y);
1188
1259
  }
1260
+ /** Round each component up. Mutates and returns `this`. */
1189
1261
  ceil() {
1190
1262
  return this.map(Math.ceil);
1191
1263
  }
1264
+ /** Clamp each axis to its `[min, max]` range. `y` defaults to `x`. Mutates and returns `this`. */
1192
1265
  clamp(x, y = x) {
1193
1266
  this.x = clamp(this.x, x[0], x[1]);
1194
1267
  this.y = clamp(this.y, y[0], y[1]);
@@ -1197,9 +1270,19 @@ var Vec2 = class _Vec2 {
1197
1270
  div(x, y) {
1198
1271
  return this.calculate(Operation.Div, x, y);
1199
1272
  }
1273
+ /** Round each component down. Mutates and returns `this`. */
1200
1274
  floor() {
1201
1275
  return this.map(Math.floor);
1202
1276
  }
1277
+ /**
1278
+ * Apply `callback` to each component (`index` is `0` for x, `1` for y). Mutates and returns `this`.
1279
+ *
1280
+ * @example
1281
+ * ```ts
1282
+ * new Vec2(3.6, -2.1).map(Math.trunc); // Vec2 { x: 3, y: -2 }
1283
+ * new Vec2(2, 5).map((v, i) => v * (i + 1)); // Vec2 { x: 2, y: 10 }
1284
+ * ```
1285
+ */
1203
1286
  map(callback) {
1204
1287
  this.x = callback(this.x, 0);
1205
1288
  this.y = callback(this.y, 1);
@@ -1211,9 +1294,11 @@ var Vec2 = class _Vec2 {
1211
1294
  mult(x, y) {
1212
1295
  return this.calculate(Operation.Mult, x, y);
1213
1296
  }
1297
+ /** Flip the sign of both components (same as `mult(-1)`). Mutates and returns `this`. */
1214
1298
  negate() {
1215
1299
  return this.mult(-1);
1216
1300
  }
1301
+ /** Scale to unit length. Zero-length vectors are left untouched and warn (throttled). Mutates and returns `this`. */
1217
1302
  normalize() {
1218
1303
  const length = this.length();
1219
1304
  if (approxEqual(length, 0)) {
@@ -1222,6 +1307,7 @@ var Vec2 = class _Vec2 {
1222
1307
  }
1223
1308
  return this.map((value) => value / length);
1224
1309
  }
1310
+ /** Scale so `|x| + |y| === 1`. Zero-length vectors are left untouched. Mutates and returns `this`. */
1225
1311
  normalizeManhattan() {
1226
1312
  const length = this.lengthManhattan();
1227
1313
  if (approxEqual(length, 0)) {
@@ -1232,6 +1318,7 @@ var Vec2 = class _Vec2 {
1232
1318
  rem(x, y) {
1233
1319
  return this.calculate(Operation.Rem, x, y);
1234
1320
  }
1321
+ /** Round each component to the nearest integer. Mutates and returns `this`. */
1235
1322
  round() {
1236
1323
  this.x = Math.round(this.x);
1237
1324
  this.y = Math.round(this.y);
@@ -1240,38 +1327,48 @@ var Vec2 = class _Vec2 {
1240
1327
  sub(x, y) {
1241
1328
  return this.calculate(Operation.Sub, x, y);
1242
1329
  }
1330
+ /** Angle in radians. No arg: angle of `this` from origin. With `other`: angle from `this` toward `other`. */
1243
1331
  angle(other) {
1244
1332
  if (!other) {
1245
1333
  return Math.atan2(this.y, this.x);
1246
1334
  }
1247
1335
  return Math.atan2(other.y - this.y, other.x - this.x);
1248
1336
  }
1337
+ /** Euclidean distance to `other`. */
1249
1338
  distance(other) {
1250
1339
  return Math.sqrt(
1251
1340
  Math.pow(other.x - this.x, 2) + Math.pow(other.y - this.y, 2)
1252
1341
  );
1253
1342
  }
1343
+ /** Manhattan distance (`|dx| + |dy|`) to `other`. */
1254
1344
  distanceManhattan(other) {
1255
1345
  return Math.abs(other.x - this.x) + Math.abs(other.y - this.y);
1256
1346
  }
1347
+ /** Dot product with `other`. */
1257
1348
  dotProduct(other) {
1258
1349
  return this.x * other.x + this.y * other.y;
1259
1350
  }
1351
+ /** `true` when both components are finite (rules out `NaN` and `±Infinity`). */
1260
1352
  isValid() {
1261
1353
  return Number.isFinite(this.x) && Number.isFinite(this.y);
1262
1354
  }
1355
+ /** Euclidean magnitude (`sqrt(x² + y²)`). */
1263
1356
  length() {
1264
1357
  return Math.sqrt(this.x * this.x + this.y * this.y);
1265
1358
  }
1359
+ /** Manhattan magnitude (`|x| + |y|`). */
1266
1360
  lengthManhattan() {
1267
1361
  return Math.abs(this.x) + Math.abs(this.y);
1268
1362
  }
1363
+ /** Larger of the two components. */
1269
1364
  max() {
1270
1365
  return Math.max(this.x, this.y);
1271
1366
  }
1367
+ /** Smaller of the two components. */
1272
1368
  min() {
1273
1369
  return Math.min(this.x, this.y);
1274
1370
  }
1371
+ /** Tuple `[x, y]`. */
1275
1372
  toArray() {
1276
1373
  return [this.x, this.y];
1277
1374
  }
@@ -1281,9 +1378,11 @@ var Vec2 = class _Vec2 {
1281
1378
  toRectAddSize(width, height) {
1282
1379
  return this.concat(false, width, height);
1283
1380
  }
1381
+ /** Debug string like `"Vec2 [x: 1, y: 2]"`. */
1284
1382
  toString() {
1285
1383
  return `Vec2 [x: ${this.x}, y: ${this.y}]`;
1286
1384
  }
1385
+ /** New `Vec2` with the same components. */
1287
1386
  clone() {
1288
1387
  return new _Vec2(this.x, this.y);
1289
1388
  }
@@ -1363,9 +1462,11 @@ var Vec2 = class _Vec2 {
1363
1462
  // src/core/Settings.ts
1364
1463
  var LOCAL_STORAGE_KEY = "gleam";
1365
1464
  var Settings = class {
1465
+ /** Read-only view of the persisted localStorage blob. Writes go through {@link setLocalStorage}. */
1366
1466
  static get localStorage() {
1367
1467
  return this._localStorage;
1368
1468
  }
1469
+ /** One-time setup — called by `Game`'s constructor with the overrides passed to `super()`. Validates {@link fps}, loads the persisted localStorage blob, derives `language` from `navigator.language`, and wires the close-warning handler if {@link warnBeforeClose}. Throws if called twice or if `fps` isn't a finite positive number. */
1369
1470
  static init(overrides, game) {
1370
1471
  if (this.initialized) {
1371
1472
  throw new Error("Settings.init called twice");
@@ -1405,6 +1506,7 @@ var Settings = class {
1405
1506
  }
1406
1507
  this.initialized = true;
1407
1508
  }
1509
+ /** Typed setter for the persisted localStorage blob. Writes both in-memory and to actual `localStorage` (under a single JSON key — `"gleam"`). The only supported way to mutate persisted state. */
1408
1510
  static setLocalStorage(key, value) {
1409
1511
  this._localStorage[key] = value;
1410
1512
  localStorage.setItem(
@@ -1413,16 +1515,27 @@ var Settings = class {
1413
1515
  );
1414
1516
  }
1415
1517
  };
1518
+ /** Enable smoothing on the main canvas context. Default `false` for crisp pixel art. */
1416
1519
  __publicField(Settings, "antialias", false);
1520
+ /** Start the gameloop automatically after `init()` resolves. Disable to drive `gameloop.startLoop()` manually. Default `true`. */
1417
1521
  __publicField(Settings, "autoloop", true);
1522
+ /** CSS color used when {@link useClearRect} is `false`. Default `"#444"`. */
1418
1523
  __publicField(Settings, "backgroundColor", "#444");
1524
+ /** Debug mode: assigns the `Game` instance to `window.game` and lets {@link Keyboard} Escape stop the loop. Default `false`. */
1419
1525
  __publicField(Settings, "debug", false);
1526
+ /** Skip the per-frame canvas clear. Use for trail/decay effects where you manage clearing yourself. Default `false`. */
1420
1527
  __publicField(Settings, "doNotClear", false);
1528
+ /** Stretch the main canvas to fill the window on resize while preserving its aspect ratio. Default `true`. */
1421
1529
  __publicField(Settings, "enableResize", true);
1530
+ /** Default font family for `canman.setFontSize`. Default `"Arial"`. */
1422
1531
  __publicField(Settings, "font", "Arial");
1532
+ /** **Seconds per fixed step**, not frames per second — `1 / 60` = 60 Hz, `1 / 30` = 30 Hz. Must be finite and `> 0` or {@link init} throws. */
1423
1533
  __publicField(Settings, "fps", 1 / 60);
1534
+ /** Callback invoked from the `beforeunload` handler when {@link warnBeforeClose} is `true`. Useful for "are you sure?" autosave logic. */
1424
1535
  __publicField(Settings, "triedToClose");
1536
+ /** Clear the canvas with `clearRect` (transparent) when `true`, or `fillRect` with {@link backgroundColor} when `false`. Default `true`. */
1425
1537
  __publicField(Settings, "useClearRect", true);
1538
+ /** Show a browser "are you sure?" dialog on tab close. Required for {@link triedToClose} to fire. Default `false`. */
1426
1539
  __publicField(Settings, "warnBeforeClose", false);
1427
1540
  __publicField(Settings, "initialized", false);
1428
1541
  // Only mutated via `setLocalStorage` (typed) and via the localStorage
@@ -2015,19 +2128,20 @@ function getUsedColors(image, pixelAmount = 1, removeLowerThan = 0, removeHigher
2015
2128
  // src/content/Animator.ts
2016
2129
  var _Animator = class _Animator {
2017
2130
  /**
2018
- * We store rendered images in a cache.
2019
- * So if you have the same entity multiple times, the images get computed once and then shared.
2020
- * This reduces overhead and frees time up for other important computations.
2021
- *
2022
- * `namespace` is the cache key prefix for these rendered images.
2023
- * So pass a different key for different Animators; otherwise, you will see wrong images.
2131
+ * Bind to `entity` and stamp `namespace` as the prefix for cache keys (`${namespace}.${animationName}`). **Use distinct namespaces for animators whose sprite sets differ** — sharing a namespace across mismatched sprite sets serves the wrong cached frames.
2024
2132
  */
2025
2133
  constructor(entity, namespace) {
2134
+ /** When `false`, {@link update} is a no-op. Set automatically to `false` after a single-frame animation finishes and via {@link reset} when no default animation exists. */
2026
2135
  __publicField(this, "active", true);
2136
+ /** Current rendered frame (after flip processing). Pulled from the cache by {@link setImage} every time the frame advances. */
2027
2137
  __publicField(this, "image");
2138
+ /** Index of the current frame within the current animation's `sprites` array. */
2028
2139
  __publicField(this, "imageId", 0);
2140
+ /** Flip the rendered sprite horizontally. Caches a separate "flipped" bucket so toggling is cheap. */
2029
2141
  __publicField(this, "lookLeft", false);
2142
+ /** One-shot callback that fires when the current animation's last frame finishes. Cleared after firing. */
2030
2143
  __publicField(this, "onEnd");
2144
+ /** Current sprite's `(width, height)`. Updated by {@link setImage}. */
2031
2145
  __publicField(this, "size", new Vec2());
2032
2146
  __publicField(this, "animations", []);
2033
2147
  __publicField(this, "currentAnimation", 0);
@@ -2055,9 +2169,11 @@ var _Animator = class _Animator {
2055
2169
  }
2056
2170
  });
2057
2171
  }
2172
+ /** The currently-playing animation. `undefined` if no animations have been added yet. */
2058
2173
  get current() {
2059
2174
  return this.animations[this.currentAnimation];
2060
2175
  }
2176
+ /** Blit the current cached frame at `entity.pos + offset`, shifted left by `size.x` to compensate for the 2×-wide cache canvas. */
2061
2177
  draw(context, offset = new Vec2()) {
2062
2178
  context.drawImage(
2063
2179
  this.image,
@@ -2065,6 +2181,7 @@ var _Animator = class _Animator {
2065
2181
  this.entity.pos.y + offset.y
2066
2182
  );
2067
2183
  }
2184
+ /** Advance the timer; when it crosses `current.timing`, step to the next frame, fire any `onFrame[index]` callback, and on rollover fire `onEnd` and queue `lastPlayed` (set by {@link playOnce}). No-op when {@link active} is `false`. */
2068
2185
  update(dt) {
2069
2186
  if (!this.active) {
2070
2187
  return;
@@ -2108,6 +2225,7 @@ var _Animator = class _Animator {
2108
2225
  this.setImage();
2109
2226
  }
2110
2227
  }
2228
+ /** Register a new animation. Logs an error (but still registers) if `defaultAnim` is true while another default already exists, or if `name` collides with an existing animation. Auto-plays the new animation when `defaultAnim` is `true`. */
2111
2229
  add(name, sprites, timing, defaultAnim = false) {
2112
2230
  if (defaultAnim && this.animations.some((anim) => anim.default)) {
2113
2231
  console.error("Only one default animation allowed!");
@@ -2125,6 +2243,7 @@ var _Animator = class _Animator {
2125
2243
  this.play(name);
2126
2244
  }
2127
2245
  }
2246
+ /** Convenience wrapper around {@link add} that takes a packed {@link SpriteAnimation}. `defaultAnim` is OR'd with `anim.default`. */
2128
2247
  addAnimation(anim, defaultAnim = false) {
2129
2248
  this.add(
2130
2249
  anim.name,
@@ -2133,6 +2252,7 @@ var _Animator = class _Animator {
2133
2252
  anim.default || defaultAnim
2134
2253
  );
2135
2254
  }
2255
+ /** Draw the current frame rotated by `angle` radians around a sprite-relative pivot (75% width, 50% height). Uses `setTransform` and resets the transform on exit. */
2136
2256
  drawRotated(context, angle, offset = new Vec2()) {
2137
2257
  const x = this.entity.pos.x + offset.x;
2138
2258
  const y = this.entity.pos.y + offset.y;
@@ -2143,6 +2263,7 @@ var _Animator = class _Animator {
2143
2263
  context.drawImage(this.image, -w, -h);
2144
2264
  context.setTransform(1, 0, 0, 1, 0, 0);
2145
2265
  }
2266
+ /** Switch to the named animation, rewinding timer and frame index. Optionally register `onEnd` (fires after the last frame) and `onFrame` (frame-indexed callbacks). Any previous {@link Animator.onEnd} fires before the new one is set. Throws if `name` isn't registered. */
2146
2267
  play(name, onEnd, onFrame) {
2147
2268
  const index = this.animations.findIndex(
2148
2269
  (anim) => anim.name === name
@@ -2165,6 +2286,7 @@ var _Animator = class _Animator {
2165
2286
  }
2166
2287
  this.active = true;
2167
2288
  }
2289
+ /** {@link play} only if `name` isn't already the current animation. Returns `true` if it started a new playback, `false` if it was already playing. */
2168
2290
  playIfNot(name, onEnd, onFrame) {
2169
2291
  if (!this.isPlaying(name)) {
2170
2292
  this.play(name, onEnd, onFrame);
@@ -2172,6 +2294,7 @@ var _Animator = class _Animator {
2172
2294
  }
2173
2295
  return false;
2174
2296
  }
2297
+ /** Queue an animation to play once the current one finishes. Pass `undefined` to cancel the queue. Used internally by {@link playOnce}. */
2175
2298
  playNextOnce(name) {
2176
2299
  this.lastPlayed = name;
2177
2300
  }
@@ -2182,9 +2305,11 @@ var _Animator = class _Animator {
2182
2305
  this.lastPlayed = this.current?.name;
2183
2306
  this.play(name, onEnd, onFrame);
2184
2307
  }
2308
+ /** Randomize the frame timer to a value in `[0, current.timing)`. Useful when spawning many instances of the same animation to break phase lockstep. */
2185
2309
  randomTimer() {
2186
2310
  this.timer = randomBetweenFloat(0, this.current.timing);
2187
2311
  }
2312
+ /** Drop every registered animation and clear this instance's cached frames. {@link active} resets to `true`; pending callbacks are cleared. */
2188
2313
  removeAllAnimations() {
2189
2314
  this.animations.length = 0;
2190
2315
  this.active = true;
@@ -2192,6 +2317,7 @@ var _Animator = class _Animator {
2192
2317
  this.onFrame = void 0;
2193
2318
  _Animator.clearSpriteCache(this.namespace);
2194
2319
  }
2320
+ /** Switch back to the default animation if one was registered (marked via `defaultAnim`); otherwise just stop animating ({@link active} = `false`). */
2195
2321
  reset() {
2196
2322
  const defaultAnim = this.animations.find(
2197
2323
  (anim) => anim.default
@@ -2203,6 +2329,7 @@ var _Animator = class _Animator {
2203
2329
  this.active = false;
2204
2330
  }
2205
2331
  }
2332
+ /** `true` when `name` matches the currently-playing animation. */
2206
2333
  isPlaying(name) {
2207
2334
  return this.current && this.current.name === name;
2208
2335
  }
@@ -2241,15 +2368,63 @@ var _Animator = class _Animator {
2241
2368
  __publicField(_Animator, "spriteCache", /* @__PURE__ */ new Map());
2242
2369
  var Animator = _Animator;
2243
2370
 
2371
+ // src/content/ControllerCursor.ts
2372
+ var ControllerCursor = class {
2373
+ /**
2374
+ * @param controller Gamepad input source.
2375
+ * @param crosshair Drawn at each cursor position via `CanvasRenderingContext2D.drawImage`. The image's top-left is the draw origin — center the visible reticle inside the image, or offset the anchors to compensate.
2376
+ * @param sticks Anchor positions, one per stick to track. Cloned at construction so caller mutation is harmless.
2377
+ * @param range Max pixel deflection from anchor at full stick. Default `80`.
2378
+ */
2379
+ constructor(controller, crosshair, sticks, range = 80) {
2380
+ __publicField(this, "controller");
2381
+ __publicField(this, "crosshair");
2382
+ __publicField(this, "range");
2383
+ __publicField(this, "sticks", []);
2384
+ this.controller = controller;
2385
+ this.crosshair = crosshair;
2386
+ this.range = range;
2387
+ sticks.forEach(
2388
+ (stick) => this.sticks.push({ pos: stick.clone(), offset: new Vec2() })
2389
+ );
2390
+ }
2391
+ /** Draw a crosshair at `anchor + offset` for each tracked stick. */
2392
+ draw(context) {
2393
+ this.sticks.forEach((stick) => {
2394
+ context.drawImage(
2395
+ this.crosshair,
2396
+ stick.pos.x + stick.offset.x,
2397
+ stick.pos.y + stick.offset.y
2398
+ );
2399
+ });
2400
+ }
2401
+ /** Pull fresh stick state via {@link Controller.poll} and smooth each crosshair's offset toward `stickAxis * range`. Frame-rate independent — 50 ms to cover half the remaining distance. */
2402
+ update(dt) {
2403
+ const alpha = 1 - Math.pow(0.5, dt / 0.05);
2404
+ this.controller.poll().forEach((axi, index) => {
2405
+ const offset = this.sticks[index].offset;
2406
+ offset.x += (axi.x * this.range - offset.x) * alpha;
2407
+ offset.y += (axi.y * this.range - offset.y) * alpha;
2408
+ });
2409
+ }
2410
+ };
2411
+
2244
2412
  // src/content/Particle.ts
2245
2413
  var Particle = class {
2246
2414
  constructor(pos, color, size = 2) {
2415
+ /** CSS color string passed to `context.fillStyle` in {@link draw}. */
2247
2416
  __publicField(this, "color");
2417
+ /** Accumulated time (seconds) since spawn or last {@link resetLifetime}. */
2248
2418
  __publicField(this, "lifetime", 0);
2419
+ /** Lifetime cap in seconds, randomized to `[0.5, 1.5]` at construction. */
2249
2420
  __publicField(this, "maxLifeTime");
2421
+ /** Top-left position. Cloned from the constructor arg so the caller's `Vec2` isn't aliased. */
2250
2422
  __publicField(this, "pos");
2423
+ /** Circle radius (pixels). */
2251
2424
  __publicField(this, "size");
2425
+ /** Velocity in px/s. Seeded randomly by the constructor (random angle, random magnitude per axis). */
2252
2426
  __publicField(this, "vel");
2427
+ /** Backing storage for the {@link rect} getter. Subclasses can read it; the public-facing accessor is {@link rect}. */
2253
2428
  __publicField(this, "_rect");
2254
2429
  this.pos = pos.clone();
2255
2430
  this.color = color;
@@ -2262,12 +2437,15 @@ var Particle = class {
2262
2437
  );
2263
2438
  this.maxLifeTime = 0.5 + Math.random();
2264
2439
  }
2440
+ /** `false` once {@link lifetime} reaches {@link maxLifeTime}. Owners typically filter dead particles out of their list each frame, or call {@link resetLifetime} to recycle. */
2265
2441
  get alive() {
2266
2442
  return this.lifetime < this.maxLifeTime;
2267
2443
  }
2444
+ /** Read-only AABB tracking `pos` and the particle's `size`. Recomputed each {@link update}. */
2268
2445
  get rect() {
2269
2446
  return this._rect;
2270
2447
  }
2448
+ /** Fill a circle at `pos + offset` using {@link color}. `offset` is useful for shifting by a camera/world transform without mutating `pos`. */
2271
2449
  draw(context, offset = new Vec2()) {
2272
2450
  context.fillStyle = this.color;
2273
2451
  context.drawCircle(
@@ -2279,12 +2457,14 @@ var Particle = class {
2279
2457
  "fill"
2280
2458
  );
2281
2459
  }
2460
+ /** Integrate lifetime, position, and bounding rect. */
2282
2461
  update(dt) {
2283
2462
  this.lifetime += dt;
2284
2463
  this.pos.x += this.vel.x * dt;
2285
2464
  this.pos.y += this.vel.y * dt;
2286
2465
  this._rect.set(this.pos.x, this.pos.y);
2287
2466
  }
2467
+ /** Recycle a dead particle by subtracting `maxLifeTime` from `lifetime` — preserves any overshoot so a pool of pre-allocated particles can stay phase-stable across loops. Note this doesn't re-randomize `vel` or `pos`; mutate those externally if you want a fresh trajectory. */
2288
2468
  resetLifetime() {
2289
2469
  this.lifetime -= this.maxLifeTime;
2290
2470
  }
@@ -2293,14 +2473,23 @@ var Particle = class {
2293
2473
  // src/content/Projectile.ts
2294
2474
  var Projectile = class {
2295
2475
  constructor(pos, image, vel = new Vec2()) {
2476
+ /** Seconds after which {@link alive} flips to `false`. Defaults to `Infinity` — no natural expiry. */
2296
2477
  __publicField(this, "maxLifetime", Infinity);
2478
+ /** Caller-supplied data. Typed via the class generic so consumers can read `projectile.payload` without casting. */
2297
2479
  __publicField(this, "payload");
2480
+ /** Magnitude multiplier applied to `vel` each update: `pos += vel * speed * dt`. Pass a unit-length `vel` to make this read as "pixels per second". */
2298
2481
  __publicField(this, "speed", 1200);
2482
+ /** Current pre-rotated sprite. Re-baked by {@link rebuildRotation} from the un-rotated `originalImage`. */
2299
2483
  __publicField(this, "image");
2484
+ /** Accumulated time (seconds). Drives the {@link alive} check against {@link maxLifetime}. */
2300
2485
  __publicField(this, "lifetime", 0);
2486
+ /** Top-left position. Cloned from the constructor arg so the caller's `Vec2` isn't aliased. */
2301
2487
  __publicField(this, "pos");
2488
+ /** Current sprite rotation in radians, kept in sync with `vel` by {@link rebuildRotation}. */
2302
2489
  __publicField(this, "rotation", 0);
2490
+ /** Velocity direction vector. Multiplied by {@link speed} each update — pass a unit vector for `speed`-as-px-per-second semantics. Cloned from the constructor arg. */
2303
2491
  __publicField(this, "vel");
2492
+ /** Backing storage for the {@link rect} getter. Subclasses can read it; mutate via `pos`/{@link rebuildRotation} instead of touching it directly. */
2304
2493
  __publicField(this, "_rect");
2305
2494
  __publicField(this, "originalImage");
2306
2495
  this.pos = pos.clone();
@@ -2309,12 +2498,15 @@ var Projectile = class {
2309
2498
  this._rect = pos.toRectAddSize(image.width, image.height);
2310
2499
  this.rebuildRotation();
2311
2500
  }
2501
+ /** `false` once {@link lifetime} reaches {@link maxLifetime}. Owners typically filter dead projectiles out of their list each frame. */
2312
2502
  get alive() {
2313
2503
  return this.lifetime < this.maxLifetime;
2314
2504
  }
2505
+ /** Read-only AABB tracking `pos` and the (rotated) image size. Recomputed in {@link update} and {@link rebuildRotation}. */
2315
2506
  get rect() {
2316
2507
  return this._rect;
2317
2508
  }
2509
+ /** Blit the pre-rotated image at `pos + offset`. `offset` is useful for shifting by a camera/world transform without mutating `pos`. */
2318
2510
  draw(context, offset = new Vec2()) {
2319
2511
  context.drawImage(
2320
2512
  this.image,
@@ -2322,6 +2514,7 @@ var Projectile = class {
2322
2514
  this.pos.y + offset.y
2323
2515
  );
2324
2516
  }
2517
+ /** Integrate motion and advance lifetime. Doesn't re-bake the rotation — call {@link rebuildRotation} after mutating `vel`. */
2325
2518
  update(dt) {
2326
2519
  this.lifetime += dt;
2327
2520
  this.pos.x += this.vel.x * this.speed * dt;
@@ -2329,8 +2522,7 @@ var Projectile = class {
2329
2522
  this._rect.set(this.pos.x, this.pos.y);
2330
2523
  }
2331
2524
  /**
2332
- * Allocates a fresh rotated canvas on each call — no internal cache.
2333
- * Caching by quantized rotation could be a feature when projectiles need to re-aim every tick (homing/seeking).
2525
+ * Re-bake the sprite to match the current `vel` direction (rotation = `atan2(vel.y, vel.x)`) and update `rect` to the new bounds. Allocates a fresh rotated canvas every call — no internal cache, so heavy re-aiming (homing/seeking) is a candidate for adding quantized caching.
2334
2526
  */
2335
2527
  rebuildRotation() {
2336
2528
  this.rotation = Math.atan2(this.vel.y, this.vel.x);
@@ -2338,6 +2530,7 @@ var Projectile = class {
2338
2530
  this._rect.w = this.image.width;
2339
2531
  this._rect.h = this.image.height;
2340
2532
  }
2533
+ /** Force {@link alive} to `false` immediately (sets `lifetime` past `maxLifetime`). Use when the projectile should die on collision/impact, not from natural expiry. */
2341
2534
  remove() {
2342
2535
  this.lifetime = this.maxLifetime * 2;
2343
2536
  }
@@ -2345,40 +2538,56 @@ var Projectile = class {
2345
2538
 
2346
2539
  // src/core/CanvasManager.ts
2347
2540
  var CANVAS_TYPES = {
2541
+ /** Catch-all tag for canvases without a specific role. */
2348
2542
  ANY: /* @__PURE__ */ Symbol("any"),
2543
+ /** Generic placeholder tag — distinct from `ANY` so consumers can differentiate. */
2349
2544
  DEFAULT: /* @__PURE__ */ Symbol("default"),
2545
+ /** Background canvas (drawn behind the main one). */
2350
2546
  BACKGROUND: /* @__PURE__ */ Symbol("background"),
2547
+ /** Primary render target. Exactly one canvas must be registered with this type before {@link CanvasManager.finishSetup}. */
2351
2548
  MAIN: /* @__PURE__ */ Symbol("main")
2352
2549
  };
2353
2550
  var CanvasManager = class {
2354
2551
  constructor() {
2552
+ /** Cached `getBoundingClientRect()` of the main canvas. Refreshed in {@link resize}. Used to map pointer client coords into canvas space. */
2355
2553
  __publicField(this, "canvasBoundingClientRect");
2554
+ /** Registry of every {@link setupCanvas}-registered canvas, keyed by selector. */
2356
2555
  __publicField(this, "canvasHolder", {});
2556
+ /** Display-to-buffer scale factor after the last {@link resize} (`displayWidth / bufferWidth`). `1` until the first resize. */
2357
2557
  __publicField(this, "ratio", 1);
2558
+ /** Display (CSS-pixel) size of the main canvas after the last {@link resize}. Independent of the buffer dimensions in {@link width}/{@link height}. */
2358
2559
  __publicField(this, "resizedSize", new Vec2());
2359
2560
  __publicField(this, "mainHolder");
2360
2561
  }
2562
+ /** Main canvas element (the one registered with `CANVAS_TYPES.MAIN`). */
2361
2563
  get canvas() {
2362
2564
  return this.mainHolder.canvas;
2363
2565
  }
2566
+ /** Main canvas 2D rendering context. */
2364
2567
  get canvasContext() {
2365
2568
  return this.mainHolder.context;
2366
2569
  }
2570
+ /** Main canvas **buffer** height (the drawing surface, not the CSS display size). */
2367
2571
  get height() {
2368
2572
  return this.canvas.height;
2369
2573
  }
2574
+ /** Main canvas **buffer** height (the drawing surface, not the CSS display size). */
2370
2575
  set height(height) {
2371
2576
  this.canvas.height = height;
2372
2577
  }
2578
+ /** Main canvas buffer dimensions as a new `Vec2`. */
2373
2579
  get size() {
2374
2580
  return new Vec2(this.width, this.height);
2375
2581
  }
2582
+ /** Main canvas **buffer** width (the drawing surface, not the CSS display size). */
2376
2583
  get width() {
2377
2584
  return this.canvas.width;
2378
2585
  }
2586
+ /** Main canvas **buffer** width (the drawing surface, not the CSS display size). */
2379
2587
  set width(width) {
2380
2588
  this.canvas.width = width;
2381
2589
  }
2590
+ /** Finalize the canvas registry. Called once by `Game.preInit()`. Validates that exactly one `CANVAS_TYPES.MAIN` canvas is registered and that its buffer is non-zero, caches its bounding rect, and wires the `"resized"` listener if `Settings.enableResize`. Throws on duplicate calls or invalid registry state. */
2382
2591
  finishSetup() {
2383
2592
  if (this.mainHolder) {
2384
2593
  throw new Error("Already set up.");
@@ -2401,6 +2610,7 @@ var CanvasManager = class {
2401
2610
  EventSystem.addEventListener("resized", () => this.resize());
2402
2611
  }
2403
2612
  }
2613
+ /** Rescale every opt-in canvas (`holder.resize === true`) to fit the window while preserving its buffer aspect ratio. Updates `style.width`/`style.height` only — buffer dimensions don't change. Refreshes {@link canvasBoundingClientRect}, {@link resizedSize}, and {@link ratio} from the main canvas. */
2404
2614
  resize() {
2405
2615
  const windowRatio = window.innerHeight / window.innerWidth;
2406
2616
  Object.values(this.canvasHolder).forEach((ch) => {
@@ -2426,10 +2636,12 @@ var CanvasManager = class {
2426
2636
  });
2427
2637
  this.canvasBoundingClientRect = this.canvas.getBoundingClientRect();
2428
2638
  }
2639
+ /** Set the main context's `font` to `${size}px "${font}"`. Defaults the family to `Settings.font`. */
2429
2640
  setFontSize(size, font = Settings.font) {
2430
2641
  this.canvasContext.font = `${size}px "${font}"`;
2431
2642
  }
2432
- setupCanvas(canvasType, selector, resize = true) {
2643
+ /** Register a canvas at `selector` with the given role tag. Initializes its context (`fillStyle`/`strokeStyle` = white, font = `12px Arial`) and returns the {@link CanvasHolder}. `resize` defaults to `Settings.enableResize`. Throws if the selector doesn't match an element or has already been registered. */
2644
+ setupCanvas(canvasType, selector, resize = Settings.enableResize) {
2433
2645
  if (!document.querySelector(selector)) {
2434
2646
  throw new Error("Canvas '" + selector + "' does not exist!");
2435
2647
  }
@@ -2458,6 +2670,7 @@ var MAX_DT_SECONDS = 0.25;
2458
2670
  var MAX_STEPS_PER_FRAME = 5;
2459
2671
  var Gameloop = class {
2460
2672
  constructor(game) {
2673
+ /** Simulation time in milliseconds. Advances by `Settings.fps * 1000` per update step, so it reflects simulated time, not wall-clock — paused/dropped frames don't add. Use this for time-driven spawning, animations, etc. */
2461
2674
  __publicField(this, "levelTime", 0);
2462
2675
  __publicField(this, "_isLooping", false);
2463
2676
  __publicField(this, "accumulator", 0);
@@ -2465,9 +2678,11 @@ var Gameloop = class {
2465
2678
  __publicField(this, "stop", false);
2466
2679
  this.game = game;
2467
2680
  }
2681
+ /** `true` while the rAF callback is registered. Goes `false` only after the final frame fires the `"gameloopStopped"` event. */
2468
2682
  get isLooping() {
2469
2683
  return this._isLooping;
2470
2684
  }
2685
+ /** Begin the rAF loop. Throws if {@link stopLoop} was called but teardown hasn't completed yet — wait for the `"gameloopStopped"` event before restarting. */
2471
2686
  startLoop() {
2472
2687
  if (this._isLooping && this.stop) {
2473
2688
  throw new Error(
@@ -2477,6 +2692,7 @@ var Gameloop = class {
2477
2692
  this.stop = false;
2478
2693
  this.looper();
2479
2694
  }
2695
+ /** Request that the loop stop on its next tick. Asynchronous — the loop tears down on the following frame and dispatches `"gameloopStopped"` when done. */
2480
2696
  stopLoop() {
2481
2697
  this.stop = true;
2482
2698
  }
@@ -2534,53 +2750,98 @@ var Gameloop = class {
2534
2750
 
2535
2751
  // src/input/Keyboard.ts
2536
2752
  var KEYBOARD_KEYS = {
2753
+ /** Digit row `0`. */
2537
2754
  KEY_0: "Digit0",
2755
+ /** Digit row `1`. */
2538
2756
  KEY_1: "Digit1",
2757
+ /** Digit row `2`. */
2539
2758
  KEY_2: "Digit2",
2759
+ /** Digit row `3`. */
2540
2760
  KEY_3: "Digit3",
2761
+ /** Digit row `4`. */
2541
2762
  KEY_4: "Digit4",
2763
+ /** Digit row `5`. */
2542
2764
  KEY_5: "Digit5",
2765
+ /** Digit row `6`. */
2543
2766
  KEY_6: "Digit6",
2767
+ /** Digit row `7`. */
2544
2768
  KEY_7: "Digit7",
2769
+ /** Digit row `8`. */
2545
2770
  KEY_8: "Digit8",
2771
+ /** Digit row `9`. */
2546
2772
  KEY_9: "Digit9",
2773
+ /** Letter `A`. */
2547
2774
  KEY_A: "KeyA",
2775
+ /** Letter `B`. */
2548
2776
  KEY_B: "KeyB",
2777
+ /** Letter `C`. */
2549
2778
  KEY_C: "KeyC",
2779
+ /** Letter `D`. */
2550
2780
  KEY_D: "KeyD",
2781
+ /** Down arrow. */
2551
2782
  KEY_DOWN: "ArrowDown",
2783
+ /** Letter `E`. */
2552
2784
  KEY_E: "KeyE",
2785
+ /** Enter / Return. */
2553
2786
  KEY_ENTER: "Enter",
2787
+ /** Escape. Note: in `Settings.debug` mode this stops the gameloop. */
2554
2788
  KEY_ESCAPE: "Escape",
2789
+ /** Letter `F`. */
2555
2790
  KEY_F: "KeyF",
2791
+ /** Letter `G`. */
2556
2792
  KEY_G: "KeyG",
2793
+ /** Letter `H`. */
2557
2794
  KEY_H: "KeyH",
2795
+ /** Letter `I`. */
2558
2796
  KEY_I: "KeyI",
2797
+ /** Letter `J`. */
2559
2798
  KEY_J: "KeyJ",
2799
+ /** Letter `K`. */
2560
2800
  KEY_K: "KeyK",
2801
+ /** Letter `L`. */
2561
2802
  KEY_L: "KeyL",
2803
+ /** Left arrow. */
2562
2804
  KEY_LEFT: "ArrowLeft",
2805
+ /** Letter `M`. */
2563
2806
  KEY_M: "KeyM",
2807
+ /** Letter `N`. */
2564
2808
  KEY_N: "KeyN",
2809
+ /** Letter `O`. */
2565
2810
  KEY_O: "KeyO",
2811
+ /** Letter `P`. */
2566
2812
  KEY_P: "KeyP",
2813
+ /** Letter `Q`. */
2567
2814
  KEY_Q: "KeyQ",
2815
+ /** Letter `R`. */
2568
2816
  KEY_R: "KeyR",
2817
+ /** Right arrow. */
2569
2818
  KEY_RIGHT: "ArrowRight",
2819
+ /** Letter `S`. */
2570
2820
  KEY_S: "KeyS",
2821
+ /** Space bar. */
2571
2822
  KEY_SPACE: "Space",
2823
+ /** Letter `T`. */
2572
2824
  KEY_T: "KeyT",
2825
+ /** Tab. */
2573
2826
  KEY_TAB: "Tab",
2827
+ /** Letter `U`. */
2574
2828
  KEY_U: "KeyU",
2829
+ /** Up arrow. */
2575
2830
  KEY_UP: "ArrowUp",
2831
+ /** Letter `V`. */
2576
2832
  KEY_V: "KeyV",
2833
+ /** Letter `W`. */
2577
2834
  KEY_W: "KeyW",
2835
+ /** Letter `X`. */
2578
2836
  KEY_X: "KeyX",
2837
+ /** Letter `Y`. */
2579
2838
  KEY_Y: "KeyY",
2839
+ /** Letter `Z`. */
2580
2840
  KEY_Z: "KeyZ"
2581
2841
  };
2582
2842
  var Keyboard = class {
2583
2843
  constructor(game) {
2844
+ /** Live map of `KeyboardEvent.code` → pressed state. Codes only appear after the key has been touched at least once; missing codes read as `undefined` (use {@link isPressed} for a safe `boolean`). */
2584
2845
  __publicField(this, "keys", {});
2585
2846
  const keyEvent = (event) => {
2586
2847
  const code = event.code;
@@ -2601,14 +2862,17 @@ var Keyboard = class {
2601
2862
  window.addEventListener("blur", () => this.reset(), false);
2602
2863
  EventSystem.addEventListener("gameloopStopped", () => this.reset());
2603
2864
  }
2865
+ /** Mark every tracked key as released. Called automatically on `window` blur and on `gameloopStopped`. */
2604
2866
  reset() {
2605
2867
  for (const key in this.keys) {
2606
2868
  this.keys[key] = false;
2607
2869
  }
2608
2870
  }
2871
+ /** Mark a single key as released — used to consume a press so subsequent ticks don't re-trigger one-shot actions while the key is still held. */
2609
2872
  stopPress(code) {
2610
2873
  this.keys[code] = false;
2611
2874
  }
2875
+ /** `true` when `code` is currently held. Safe for untouched keys (returns `false` rather than `undefined`). */
2612
2876
  isPressed(code) {
2613
2877
  return !!this.keys[code];
2614
2878
  }
@@ -2616,20 +2880,32 @@ var Keyboard = class {
2616
2880
 
2617
2881
  // src/input/Pointer.ts
2618
2882
  var POINTER_KEYS = {
2883
+ /** Primary button (left for right-handers). */
2619
2884
  LEFT: 0,
2885
+ /** Middle button / wheel click. */
2620
2886
  MIDDLE: 1,
2887
+ /** Secondary button (right for right-handers). */
2621
2888
  RIGHT: 2,
2889
+ /** "Back" side button (browser back). */
2622
2890
  PREV: 3,
2891
+ /** "Forward" side button. */
2623
2892
  FORWARD: 4
2624
2893
  };
2625
2894
  var Pointer = class {
2626
2895
  constructor(game) {
2896
+ /** Dirty bit set to `true` on every move and never cleared by the engine — flip it back to `false` after reading to detect "moved since last check". */
2627
2897
  __publicField(this, "hasMoved", false);
2898
+ /** Last raw `PointerEvent` received. `null` until any pointer event fires. Use for properties not surfaced as Vec2/booleans (pressure, pointerType, etc.). */
2628
2899
  __publicField(this, "lastEvent", null);
2900
+ /** Viewport-space coordinates (`event.clientX/Y` — CSS pixels relative to the page). */
2629
2901
  __publicField(this, "posReal", new Vec2());
2902
+ /** Previous tick's {@link posReal}. Subtract for a per-frame delta. */
2630
2903
  __publicField(this, "posRealLast", new Vec2());
2904
+ /** Canvas-space coordinates, mapped from the bounding rect into the main canvas's pixel buffer and clamped to its size. This is the position to use for in-game logic. */
2631
2905
  __publicField(this, "posScaled", new Vec2());
2906
+ /** Previous tick's {@link posScaled}. */
2632
2907
  __publicField(this, "posScaledLast", new Vec2());
2908
+ /** Per-button pressed state. Index with {@link POINTER_KEYS} (e.g. `pressed[POINTER_KEYS.LEFT]`). Sparse — unindexed entries are `undefined`, not `false`. */
2633
2909
  __publicField(this, "pressed", []);
2634
2910
  __publicField(this, "game");
2635
2911
  this.game = game;
@@ -2662,6 +2938,7 @@ var Pointer = class {
2662
2938
  false
2663
2939
  );
2664
2940
  }
2941
+ /** Clear all pressed-button state. Called automatically on `window` blur so held buttons don't stay "pressed" forever when focus is lost. */
2665
2942
  reset() {
2666
2943
  this.pressed.length = 0;
2667
2944
  }
@@ -3002,9 +3279,13 @@ defineMethod(
3002
3279
  // src/core/Game.ts
3003
3280
  var Game = class {
3004
3281
  constructor(settingOverrides = {}) {
3282
+ /** Canvas registry + 2D context exposure. Register canvases here from the constructor (`canman.setupCanvas(CANVAS_TYPES.MAIN, "#game")`) before calling {@link preInit}. */
3005
3283
  __publicField(this, "canman", new CanvasManager());
3284
+ /** The fixed-step driver. Started automatically by `preInit` when `Settings.autoloop` is `true`. */
3006
3285
  __publicField(this, "gameloop");
3286
+ /** Live keyboard state. See {@link Keyboard}. */
3007
3287
  __publicField(this, "keyboard");
3288
+ /** Live pointer (mouse / pen / touch) state. See {@link Pointer}. */
3008
3289
  __publicField(this, "pointer");
3009
3290
  __publicField(this, "initialized", false);
3010
3291
  Settings.init(settingOverrides, this);
@@ -3013,17 +3294,21 @@ var Game = class {
3013
3294
  this.keyboard = new Keyboard(this);
3014
3295
  this.pointer = new Pointer(this);
3015
3296
  }
3297
+ /** Render the current frame. Called by {@link Gameloop} after the canvas is cleared. Subclasses must override — the default throws. */
3016
3298
  draw(_context) {
3017
3299
  throw new Error("Override draw function!");
3018
3300
  }
3301
+ /** Advance the simulation by `dt` seconds (= `Settings.fps`). Called by {@link Gameloop} zero-or-more times per frame depending on real-time accumulation. Subclasses must override — the default throws. */
3019
3302
  update(_dt) {
3020
3303
  throw new Error("Override update function!");
3021
3304
  }
3305
+ /** One-time setup hook (assets, world build) invoked by {@link preInit}. Can be `async`; the loop waits for it to resolve before starting. **Do not call directly** — kick off via {@link preInit} from the constructor. Subclasses must override — the default throws. */
3022
3306
  async init() {
3023
3307
  throw new Error(
3024
3308
  "Override init() and start the game via preInit() \u2014 do not call init() directly."
3025
3309
  );
3026
3310
  }
3311
+ /** Finalise engine wiring and start the loop. Call once from the subclass constructor *after* registering canvases. Steps: `canman.finishSetup()` → install debounced `window.resize` → reset `gameloop.levelTime` → `await this.init()` (if `doInit`) → dispatch `"resized"` → start the loop if `Settings.autoloop`. Throws if called twice. Pass `doInit: false` to skip the `init()` await (useful for tests). */
3027
3312
  async preInit(doInit = true) {
3028
3313
  if (this.initialized) {
3029
3314
  throw new Error(
@@ -3050,8 +3335,11 @@ var Game = class {
3050
3335
 
3051
3336
  // src/effects/Screenshake.ts
3052
3337
  var SHAKE_TYPES = {
3338
+ /** ~0.33 s wobble combining a small random rotation with a blur fall-off. */
3053
3339
  NORMAL: {
3340
+ /** Decay rate — see {@link ShakeType.step}. */
3054
3341
  step: 3,
3342
+ /** Per-frame mutator — see {@link ShakeType.update}. */
3055
3343
  update(updateCss, time) {
3056
3344
  const tr = `rotate(${randomBetweenFloat(-2, 2) * time}deg)`;
3057
3345
  updateCss("transform", tr);
@@ -3059,8 +3347,11 @@ var SHAKE_TYPES = {
3059
3347
  updateCss("filter", `blur(${time * 5}px)`);
3060
3348
  }
3061
3349
  },
3350
+ /** ~0.07 s impact: blur-only fall-off, no rotation. */
3062
3351
  FAST: {
3352
+ /** Decay rate — see {@link ShakeType.step}. */
3063
3353
  step: 15,
3354
+ /** Per-frame mutator — see {@link ShakeType.update}. */
3064
3355
  update(updateCss, time) {
3065
3356
  updateCss("filter", `blur(${time * 3}px)`);
3066
3357
  }
@@ -3073,7 +3364,7 @@ var Screenshake = class {
3073
3364
  __publicField(this, "style");
3074
3365
  this.style = element.style;
3075
3366
  }
3076
- /** Best-effort style restore: each key the shake writes is snapshotted on first write and rewritten when the shake ends or is disposed. */
3367
+ /** Start a shake of the given `shakeType`. Returns a dispose function that stops the shake early and restores every CSS key it touched, or `null` if a shake is already active on this instance. Auto-stops and restores when the timer reaches zero. */
3077
3368
  shake(shakeType = SHAKE_TYPES.NORMAL) {
3078
3369
  if (this.isShaking) {
3079
3370
  return null;
@@ -3111,72 +3402,48 @@ var Screenshake = class {
3111
3402
  }
3112
3403
  };
3113
3404
 
3114
- // src/input/ControllerCursor.ts
3115
- var SPEED = 600;
3116
- var CROSSHAIR;
3117
- (async () => CROSSHAIR = await loadImage(
3118
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAilBMVEUAAAAaGhoZGRkZGRkbGxsZGRkaGhoWFhYgICAZGRkZGRkZGRkZGRkZGRkZGRkYGBgZGRkZGRkbGxsaGhoZGRkZGRkZGRkZGRkZGRkaGhoZGRkZGRkaGhoZGRkZGRkZGRkYGBgZGRkZGRkbGxsZGRkYGBgZGRkYGBgaGhoYGBgYGBgYGBgZGRkaGhqj+rsBAAAALXRSTlMA9fvsBINBCwfhZTnS2sByMcUZE8oe1TUqubeppJOLhlDyeiPOrp57X19dSkdeSFeiAAAELElEQVR42sSY2XaqQBBFD20jozKJI3E2Tun//72bKgmXZBmjLbT7KQ9J2HSdrqoFtMiCw2I2SUKvI0THC5PJbHEIMhjBOc/7HXWVTn9+dtAmbtdOhLqJSOyui3YYvXXUXXTeRmicfN9TNaxtYUf+shtbVtxd+pFdbC1Vo7fPm335Xe3Z0/VyI1FiWSiRm+V6WrPYNXcMwbSq8KCqcCXwMyWDKiXTAE3Q7auSMEpB/C7ApFGoSvpdPEs6q5IVA/hbgImrvM5SPIOMVorxfBe4U4BxfU8xq0hCmyAsM+1L4BEBlvd7ZeV0oyBtoQjvJIEHBRh58hQhbKlV/Uv4xNwBNAQYZy4uYdRIwsclRkkMaAowcXKJ8AceQy4UYfmApkCFbyli8VAZ3EIRwzGeF8B4qIjCxd04E0W8O2hCAM67IiYO7iQb8PEfgQYEmCOXYZDhLvIexyZAcwIIONK9/K735+d7YzQpgLHHBtkdBRtw/HI0K4B8yFVw/sz/pKxWswJVsiYubiILfv8MzQsg4zMoJG6x4PrnaEMAOedgcbP/cv7HaEcAY74LN7pySr9gBWhLAIFFL5j+GgCef0e0J4Ajz8bfYmBz/0WbAuCubOMqgaAL4LQr4NBVEMHVAoQUgDHaFcCYYhBeK0KkPvHRtgB8ek505QbQ/pugfQEktCun+MmMahObEIgpazP8oKs+mcOEAOb0rC6+Qy3Ac8wIOB41A3wjIKkTzAjgRE8LUGdK24I0JSBp55mixuhyBY0IVFdxhP/sKAHSnICkFOxQkeseAAtoH0GOL/Y0JF2TAi4N/j2+oEy8waQA3ij13yIYmxWIOYY1mxBmBRDSqdfqEZkWiDh31RgQqWmBVNBAqDaxAUwLYFDtZgn9ZF7A/to/HD4L8wJceQfAmVZB17yAS8vhuVwPpjAvwBN4Xq4i61cIrMu1hLrA8hUCS+oEQKY+2bxCYENPzhBQBuUrBKTFi9lBKbXFKwSwVUod+JNE8RqBgj9XzKgPGheoeuEMEx6FuoyEGAFPDMQJTwIfeuR9/t6QQw+fp0Go3wbcoWKGrn4jCOHpj6K1WtmWZa/UWn8ceejo74OhiCmEsQj198IOBDUiPZSwLPpjof0PlBL/mrd3GwiBIAiiAgRYCOcMfOzNP70TpNBCr4kAaX8zPVX+B6IluJ4luKIlSDbhOpZzms5lrMEmTI7hfoz3O/bgGEYX0f17s/07uYjCq3ib5y27iv1jxJ9jXpDwkowXpbws940Jb814c8rbcx5Q+IiGh1Q8puNBpY9qeVjt43o+sOAjGz+04mM7P7jko1s/vObjew8wcITDQywc4/Egk0e5OMzmcT4PNHqkk0OtHuv1YLNHuz3c7vF+Lzh4xcNLLgWajxedvOpVILt53a9AeCxQPguk1wLtt0B8blC/C+T3L/T/P3gDDpik2UVvAAAAAElFTkSuQmCC"
3119
- ))();
3120
- var ControllerCursor = class {
3121
- constructor(controller, game, axisId) {
3122
- __publicField(this, "axisId");
3123
- __publicField(this, "controller");
3124
- __publicField(this, "game");
3125
- __publicField(this, "pos");
3126
- this.controller = controller;
3127
- this.game = game;
3128
- this.axisId = axisId;
3129
- this.pos = game.canman.size.mult(0.5);
3130
- }
3131
- get centerPos() {
3132
- return this.pos.clone().add(CROSSHAIR.width * 0.5, CROSSHAIR.height * 0.5);
3133
- }
3134
- draw(context) {
3135
- context.drawImage(CROSSHAIR, this.pos.x, this.pos.y);
3136
- }
3137
- update(dt) {
3138
- const step = this.controller.stick(this.axisId).mult(SPEED * dt);
3139
- if (step.length() === 0) {
3140
- return;
3141
- }
3142
- this.pos.x = clamp(
3143
- this.pos.x + step.x,
3144
- 0,
3145
- this.game.canman.width - CROSSHAIR.width
3146
- );
3147
- this.pos.y = clamp(
3148
- this.pos.y + step.y,
3149
- 0,
3150
- this.game.canman.height - CROSSHAIR.height
3151
- );
3152
- }
3153
- };
3154
-
3155
3405
  // src/input/Controller.ts
3156
3406
  var CONTROLLER_KEYS = {
3407
+ /** Bottom face button — `A` on Xbox, `×` on PlayStation. */
3157
3408
  A: 0,
3409
+ /** Right face button — `B` on Xbox, `○` on PlayStation. */
3158
3410
  B: 1,
3411
+ /** Left face button — `X` on Xbox, `□` on PlayStation. */
3159
3412
  X: 2,
3413
+ /** Top face button — `Y` on Xbox, `△` on PlayStation. */
3160
3414
  Y: 3,
3415
+ /** Left bumper / shoulder. */
3161
3416
  LB: 4,
3417
+ /** Right bumper / shoulder. */
3162
3418
  RB: 5,
3419
+ /** Left trigger. The digital pressed-state lives here; the analog value is on the underlying `Gamepad.buttons[6].value`. */
3163
3420
  LT: 6,
3421
+ /** Right trigger. The digital pressed-state lives here; the analog value is on the underlying `Gamepad.buttons[7].value`. */
3164
3422
  RT: 7,
3423
+ /** Back / Select / Share. */
3165
3424
  SELECT: 8,
3425
+ /** Start / Options / Menu. */
3166
3426
  START: 9,
3427
+ /** Left stick click (L3). */
3167
3428
  LEFT_STICK: 10,
3429
+ /** Right stick click (R3). */
3168
3430
  RIGHT_STICK: 11,
3431
+ /** D-pad up. */
3169
3432
  UP: 12,
3433
+ /** D-pad down. */
3170
3434
  DOWN: 13,
3435
+ /** D-pad left. */
3171
3436
  LEFT: 14,
3437
+ /** D-pad right. */
3172
3438
  RIGHT: 15,
3439
+ /** Guide / Home / PS button. Not exposed by all browsers. */
3173
3440
  GUIDE: 16
3174
3441
  };
3175
- var AXIS_THRESHOLD = 0.3;
3442
+ var DEADZONE = 0.25;
3176
3443
  var Controller = class {
3177
- constructor(game) {
3444
+ constructor() {
3445
+ /** Pressed-state per button, indexed in the same order as the underlying `Gamepad.buttons`. Index with {@link CONTROLLER_KEYS}. Updated by {@link poll}; empty until the first non-cached poll. */
3178
3446
  __publicField(this, "buttons", []);
3179
- __publicField(this, "cursors", []);
3180
3447
  __publicField(this, "axes", []);
3181
3448
  __publicField(this, "index", -1);
3182
3449
  __publicField(this, "lastTime", 0);
@@ -3184,11 +3451,11 @@ var Controller = class {
3184
3451
  console.error("Controller not supported!");
3185
3452
  return;
3186
3453
  }
3187
- for (let i = 0; i < 2; i++) {
3188
- this.cursors.push(new ControllerCursor(this, game, i));
3189
- }
3190
3454
  window.addEventListener("gamepadconnected", (event) => {
3191
3455
  this.index = event.gamepad.index;
3456
+ for (let i = 0; i < event.gamepad.axes.length / 2; i++) {
3457
+ this.axes.push(new Vec2());
3458
+ }
3192
3459
  console.log("Gamepad connected:", event.gamepad.id);
3193
3460
  EventSystem.dispatchEvent(
3194
3461
  "inputControllerConnected",
@@ -3205,6 +3472,7 @@ var Controller = class {
3205
3472
  "Our Gamepad was disconnected:",
3206
3473
  event.gamepad.index
3207
3474
  );
3475
+ this.reset();
3208
3476
  EventSystem.dispatchEvent("inputControllerDisconnected");
3209
3477
  } else {
3210
3478
  console.log(
@@ -3216,32 +3484,35 @@ var Controller = class {
3216
3484
  );
3217
3485
  window.addEventListener("blur", () => this.reset(), false);
3218
3486
  }
3219
- draw(context) {
3220
- if (this.index < 0) {
3221
- return;
3222
- }
3223
- this.cursors.forEach((cursor) => cursor.draw(context));
3224
- }
3225
- update(dt) {
3226
- this.cursors.forEach((cursor) => cursor.update(dt));
3487
+ /** Read the current gamepad state and return one {@link Vec2} per stick pair with a circular deadzone applied (`0.25` inner radius, output magnitude clamped to `[0, 1]`). The returned array (and each `Vec2` in it) is reused across calls — clone if you need to retain. Also refreshes {@link buttons}. Returns the cached array unchanged when the gamepad timestamp hasn't advanced. */
3488
+ poll() {
3227
3489
  const gp = this.getGamepad();
3228
3490
  if (!gp || this.lastTime === gp.timestamp) {
3229
- return;
3491
+ return this.axes;
3230
3492
  }
3231
3493
  this.lastTime = gp.timestamp;
3232
3494
  this.buttons = gp.buttons.map(
3233
3495
  (button) => button.pressed
3234
3496
  );
3235
- this.axes.length = 0;
3236
- const axes = gp.axes;
3237
- for (let i = 0; i + 1 < axes.length; i += 2) {
3238
- this.axes.push(new Vec2(axes[i], axes[i + 1]));
3497
+ for (let i = 0; i < this.axes.length; i++) {
3498
+ this.axes[i].set(gp.axes[i * 2], gp.axes[i * 2 + 1]);
3499
+ const mag = this.axes[i].length();
3500
+ if (mag < DEADZONE) {
3501
+ this.axes[i].set(0, 0);
3502
+ } else {
3503
+ this.axes[i].mult(
3504
+ (Math.min(1, mag) - DEADZONE) / (1 - DEADZONE) / mag
3505
+ );
3506
+ }
3239
3507
  }
3508
+ return this.axes;
3240
3509
  }
3510
+ /** Clear {@link buttons} and the cached stick axes. Called automatically on `window` blur and on gamepad disconnect. */
3241
3511
  reset() {
3242
3512
  this.buttons.length = 0;
3243
3513
  this.axes.length = 0;
3244
3514
  }
3515
+ /** Trigger a 400 ms full-strength dual-rumble pulse. Returns `false` when no gamepad is connected or the pad has no `vibrationActuator`; `true` when the effect was dispatched. */
3245
3516
  vibrate() {
3246
3517
  const gp = this.getGamepad();
3247
3518
  if (!gp) {
@@ -3258,14 +3529,6 @@ var Controller = class {
3258
3529
  }
3259
3530
  return !!vibrator;
3260
3531
  }
3261
- stick(index) {
3262
- if (this.index < 0 || index >= this.axes.length) {
3263
- return new Vec2();
3264
- }
3265
- return this.axes[index].clone().map((value) => threshold(value, AXIS_THRESHOLD)).map(
3266
- (value) => Math.sign(value) * map(Math.abs(value), AXIS_THRESHOLD, 1, 0, 1)
3267
- );
3268
- }
3269
3532
  getGamepad() {
3270
3533
  return this.index < 0 ? null : navigator.getGamepads()[this.index] ?? null;
3271
3534
  }
@@ -3304,8 +3567,9 @@ var Polygon = class _Polygon {
3304
3567
  this.addPoints(...points);
3305
3568
  }
3306
3569
  /**
3307
- * `angle` is the simplification threshold in radians: vertices whose turn
3308
- * angle wraps to within ±`angle` of straight are dropped.
3570
+ * Trace an outline around the opaque pixels of `canvas` via four directional sweeps (top, right, bottom, left). `detail` is the pixel stride (≥ 2) between scanline samples — higher = faster but coarser. `angle` is the simplification threshold in radians: vertices whose turn angle wraps to within ±`angle` of straight are dropped. Throws if fewer than 3 vertices survive.
3571
+ *
3572
+ * **Convex shapes only.** The sweep ignores anything an outer-perimeter ray can't reach: holes (donuts), inward bays (a "C" opening sideways), or any row/column with multiple disjoint opaque spans. For those inputs the result is either a broken polygon or simply the outer hull, and {@link Polygon.collide} relies on convexity anyway.
3309
3573
  */
3310
3574
  static fromCanvas(canvas, detail, angle) {
3311
3575
  detail = Math.max(2, detail);
@@ -3407,6 +3671,7 @@ var Polygon = class _Polygon {
3407
3671
  }
3408
3672
  return new _Polygon(...points);
3409
3673
  }
3674
+ /** Regular convex polygon with `edges` vertices, inscribed in a bounding box of `size` (number = square). */
3410
3675
  static fromEdges(edges, size) {
3411
3676
  const s = size instanceof Vec2 ? size : new Vec2(size, size);
3412
3677
  const rad = Math.min(s.x, s.y) * 0.5;
@@ -3427,6 +3692,7 @@ var Polygon = class _Polygon {
3427
3692
  }
3428
3693
  return new _Polygon(...points);
3429
3694
  }
3695
+ /** Polygon from the four corners of `rect`. */
3430
3696
  static fromRect(rect) {
3431
3697
  return new _Polygon(
3432
3698
  new Vec2(rect.x, rect.y),
@@ -3435,12 +3701,15 @@ var Polygon = class _Polygon {
3435
3701
  new Vec2(rect.x, rect.y + rect.h)
3436
3702
  );
3437
3703
  }
3704
+ /** Centroid (mean of vertex positions). Recomputed whenever the vertex set changes. */
3438
3705
  get center() {
3439
3706
  return this._center;
3440
3707
  }
3708
+ /** Read-only view of the current vertex list. Use `addPoint`/`offset`/`rotate` to mutate. */
3441
3709
  get points() {
3442
3710
  return this._points;
3443
3711
  }
3712
+ /** Stroke the polygon to `context`, shifted by `offset`. Coordinates are truncated to integers (via `| 0`) for crisp lines. */
3444
3713
  draw(context, offset = new Vec2()) {
3445
3714
  if (this._points.length === 0) {
3446
3715
  return;
@@ -3459,21 +3728,25 @@ var Polygon = class _Polygon {
3459
3728
  context.closePath();
3460
3729
  context.stroke();
3461
3730
  }
3731
+ /** Append a single vertex at `(x, y)`. Mutates and returns `this`. */
3462
3732
  addPoint(x, y) {
3463
3733
  this._points.push(new Vec2(x, y));
3464
3734
  this.update();
3465
3735
  return this;
3466
3736
  }
3737
+ /** Append cloned copies of every passed vertex. Mutates and returns `this`. */
3467
3738
  addPoints(...points) {
3468
3739
  points.forEach((point) => this._points.push(point.clone()));
3469
3740
  this.update();
3470
3741
  return this;
3471
3742
  }
3743
+ /** Translate every vertex by `(x, y)`. Mutates and returns `this`. */
3472
3744
  offset(x = 0, y = 0) {
3473
3745
  this._points.forEach((point) => point.add(x, y));
3474
3746
  this.update();
3475
3747
  return this;
3476
3748
  }
3749
+ /** Rotate by `angle` radians around `pos` (defaults to the centroid). Mutates and returns `this`. */
3477
3750
  rotate(angle, pos = this.center) {
3478
3751
  if (!angle) {
3479
3752
  return this;
@@ -3488,6 +3761,7 @@ var Polygon = class _Polygon {
3488
3761
  this.update();
3489
3762
  return this;
3490
3763
  }
3764
+ /** SAT collision against `otherPolygon`. **Both polygons must be convex** — SAT silently misses collisions for concave shapes. Pass a non-zero `velocity` to also compute whether the polygons would intersect after that displacement. Warns and returns a no-collision result if either polygon has zero edges. */
3491
3765
  collide(otherPolygon, velocity = new Vec2()) {
3492
3766
  const result = {
3493
3767
  intersect: true,
@@ -3555,6 +3829,7 @@ var Polygon = class _Polygon {
3555
3829
  }
3556
3830
  return result;
3557
3831
  }
3832
+ /** New `Polygon` with the same vertices. */
3558
3833
  clone() {
3559
3834
  return new _Polygon(...this._points);
3560
3835
  }
@@ -3711,6 +3986,8 @@ export {
3711
3986
  Gameloop,
3712
3987
  KEYBOARD_KEYS,
3713
3988
  Keyboard,
3989
+ MAX_DT_SECONDS,
3990
+ MAX_STEPS_PER_FRAME,
3714
3991
  Music,
3715
3992
  POINTER_KEYS,
3716
3993
  Particle,