@cosmoledo/gleam 1.0.1 → 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.js CHANGED
@@ -37,6 +37,8 @@ var Gleam = (() => {
37
37
  Gameloop: () => Gameloop,
38
38
  KEYBOARD_KEYS: () => KEYBOARD_KEYS,
39
39
  Keyboard: () => Keyboard,
40
+ MAX_DT_SECONDS: () => MAX_DT_SECONDS,
41
+ MAX_STEPS_PER_FRAME: () => MAX_STEPS_PER_FRAME,
40
42
  Music: () => Music,
41
43
  POINTER_KEYS: () => POINTER_KEYS,
42
44
  Particle: () => Particle,
@@ -207,6 +209,9 @@ var Gleam = (() => {
207
209
 
208
210
  // src/core/EventSystem.ts
209
211
  var _EventSystem = class _EventSystem {
212
+ /**
213
+ * 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.
214
+ */
210
215
  static addEventListener(eventName, callback, options = {}) {
211
216
  if (options.signal?.aborted) {
212
217
  return function dispose2() {
@@ -241,6 +246,7 @@ var Gleam = (() => {
241
246
  }
242
247
  return dispose;
243
248
  }
249
+ /** Synchronously fire `eventName` with the typed payload. Listeners are invoked in registration order; nested dispatches and self-disposing listeners are handled safely. */
244
250
  static dispatchEvent(eventName, ...params) {
245
251
  const bucket = this.eventListener[eventName];
246
252
  if (!bucket) {
@@ -288,21 +294,25 @@ var Gleam = (() => {
288
294
  };
289
295
  var AudioBase = class {
290
296
  constructor(enabled = true) {
297
+ /** Registered audio elements keyed by name. Subclasses read this; mutate via {@link register}. */
291
298
  __publicField(this, "songs", /* @__PURE__ */ new Map());
292
299
  __publicField(this, "_enabled");
293
300
  __publicField(this, "registered", false);
294
301
  this._enabled = enabled;
295
302
  EventSystem.addEventListener("gameloopStopped", () => this.stop());
296
303
  }
304
+ /** Whether playback is permitted. Setting to `false` invokes {@link stop} immediately. */
297
305
  get enabled() {
298
306
  return this._enabled;
299
307
  }
308
+ /** Setting to `false` invokes {@link stop} immediately; subclasses (notably {@link Music}) may re-start playback when flipped back to `true`. */
300
309
  set enabled(value) {
301
310
  this._enabled = value;
302
311
  if (!value) {
303
312
  this.stop();
304
313
  }
305
314
  }
315
+ /** 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. */
306
316
  register(defaultVolume = 1, ...songs) {
307
317
  this.throwOnBadVolume(defaultVolume, "defaultVolume");
308
318
  if (this.registered) {
@@ -331,6 +341,7 @@ var Gleam = (() => {
331
341
  this.songs.set(song.name, audio);
332
342
  });
333
343
  }
344
+ /** 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()`. */
334
345
  stop() {
335
346
  }
336
347
  throwOnBadVolume(volume, name) {
@@ -409,7 +420,7 @@ var Gleam = (() => {
409
420
  "ease-in": easeIn,
410
421
  "ease-in-out": easeInOut,
411
422
  "ease-out": easeOut,
412
- "linear": linear
423
+ linear
413
424
  };
414
425
 
415
426
  // src/utilities/Array.ts
@@ -460,18 +471,24 @@ var Gleam = (() => {
460
471
  __publicField(this, "next", null);
461
472
  __publicField(this, "fadeCancel", null);
462
473
  }
474
+ /** `true` while a fade is in progress OR the current track is actively playing. */
463
475
  get isPlaying() {
464
476
  return !!this.fadeCancel || this.current instanceof window.Audio && !this.current.paused;
465
477
  }
478
+ /** Whether music playback is permitted (inherited from {@link AudioBase}). */
466
479
  get enabled() {
467
480
  return super.enabled;
468
481
  }
482
+ /** Flipping from `false` to `true` while no music is playing auto-starts a fade-in to a random track. */
469
483
  set enabled(value) {
470
484
  super.enabled = value;
471
485
  if (value && !this.isPlaying) {
472
486
  this.fade();
473
487
  }
474
488
  }
489
+ /**
490
+ * 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.
491
+ */
475
492
  fade(name = null, fadeTime = 1e3, easing = {
476
493
  cur: "ease-in",
477
494
  next: "ease-out"
@@ -548,6 +565,7 @@ var Gleam = (() => {
548
565
  }
549
566
  });
550
567
  }
568
+ /** 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}. */
551
569
  stop() {
552
570
  super.stop();
553
571
  this.fadeCancel?.();
@@ -579,6 +597,7 @@ var Gleam = (() => {
579
597
  super(...arguments);
580
598
  __publicField(this, "currentSounds", []);
581
599
  }
600
+ /** 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. */
582
601
  play(name) {
583
602
  if (this.songs.size === 0) {
584
603
  throw new Error("No sounds registered!");
@@ -604,6 +623,7 @@ var Gleam = (() => {
604
623
  throw err;
605
624
  });
606
625
  }
626
+ /** Stop and forget every currently-playing clone. Also calls the base-class teardown. */
607
627
  stop() {
608
628
  super.stop();
609
629
  this.currentSounds.forEach((audio) => audio.stop());
@@ -763,6 +783,7 @@ var Gleam = (() => {
763
783
  __publicField(this, "_alpha", 1);
764
784
  this.set(r, g, b, a);
765
785
  }
786
+ /** Parse `#rgb`, `#rgba`, `#rrggbb`, or `#rrggbbaa` (case-insensitive, `#` optional). Throws on any other shape or on non-hex characters. */
766
787
  static fromHex(hex) {
767
788
  let cleanHex = hex.replace("#", "").toUpperCase();
768
789
  if (!/^[0-9A-F]+$/.test(cleanHex)) {
@@ -783,6 +804,7 @@ var Gleam = (() => {
783
804
  }
784
805
  return new _Color(r, g, b);
785
806
  }
807
+ /** 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. */
786
808
  static fromHSL(h, s, l, a = 1) {
787
809
  const hNorm = wrapValue(h, 0, 360) / 360;
788
810
  const sNorm = s / 100;
@@ -799,18 +821,23 @@ var Gleam = (() => {
799
821
  }
800
822
  return new _Color(r * 255, g * 255, b * 255, a);
801
823
  }
824
+ /** Red channel, `[0, 255]`. Read-only; mutate via {@link set} or any chainable transform. */
802
825
  get r() {
803
826
  return this._r;
804
827
  }
828
+ /** Green channel, `[0, 255]`. Read-only; mutate via {@link set} or any chainable transform. */
805
829
  get g() {
806
830
  return this._g;
807
831
  }
832
+ /** Blue channel, `[0, 255]`. Read-only; mutate via {@link set} or any chainable transform. */
808
833
  get b() {
809
834
  return this._b;
810
835
  }
836
+ /** Alpha channel, `[0, 1]`. Read-only; mutate via {@link set} (pass the fourth arg). */
811
837
  get alpha() {
812
838
  return this._alpha;
813
839
  }
840
+ /** 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. */
814
841
  set(r, g, b, a) {
815
842
  this._r = clamp(r, 0, 255);
816
843
  this._g = clamp(g, 0, 255);
@@ -821,6 +848,7 @@ var Gleam = (() => {
821
848
  }
822
849
  return this;
823
850
  }
851
+ /** 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`. */
824
852
  applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9) {
825
853
  return this.set(
826
854
  this.r * m1 + this.g * m2 + this.b * m3,
@@ -828,9 +856,11 @@ var Gleam = (() => {
828
856
  this.r * m7 + this.g * m8 + this.b * m9
829
857
  );
830
858
  }
859
+ /** Multiply each channel by `factor`. `factor < 1` darkens, `factor > 1` brightens (clamped at 255). Mutates and returns `this`. */
831
860
  brightness(factor) {
832
861
  return this.set(this.r * factor, this.g * factor, this.b * factor);
833
862
  }
863
+ /** 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`. */
834
864
  contrast(factor) {
835
865
  const midtone = 127.5;
836
866
  return this.set(
@@ -839,6 +869,7 @@ var Gleam = (() => {
839
869
  midtone + (this.b - midtone) * factor
840
870
  );
841
871
  }
872
+ /** Desaturate via the standard luminance-preserving matrix. `value` in `[0, 1]`: `0` is a no-op, `1` is full grayscale. Mutates and returns `this`. */
842
873
  grayscale(value = 1) {
843
874
  const m1 = 0.2126 + 0.7874 * (1 - value);
844
875
  const m2 = 0.7152 - 0.7152 * (1 - value);
@@ -851,6 +882,7 @@ var Gleam = (() => {
851
882
  const m9 = 0.0722 + 0.9278 * (1 - value);
852
883
  return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
853
884
  }
885
+ /** Rotate hue by `radians` (use `Math.PI / 2` etc.). Unlike {@link fromHSL}, this takes radians, not degrees. Mutates and returns `this`. */
854
886
  hueRotate(radians) {
855
887
  const cos = Math.cos(radians);
856
888
  const sin = Math.sin(radians);
@@ -865,6 +897,7 @@ var Gleam = (() => {
865
897
  const m9 = 0.072 + cos * 0.928 + sin * 0.072;
866
898
  return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
867
899
  }
900
+ /** Interpolate each channel toward its inverse (`255 - c`). `factor` in `[0, 1]`: `0` is unchanged, `1` is fully inverted. Mutates and returns `this`. */
868
901
  invert(factor = 1) {
869
902
  return this.set(
870
903
  this.r * (1 - factor) + (255 - this.r) * factor,
@@ -872,6 +905,7 @@ var Gleam = (() => {
872
905
  this.b * (1 - factor) + (255 - this.b) * factor
873
906
  );
874
907
  }
908
+ /** Linear blend toward `other`. `amount` in `[0, 1]`: `0` keeps `this`, `1` becomes `other`. Mixes alpha too. Mutates and returns `this`. */
875
909
  mix(other, amount) {
876
910
  const inv = 1 - amount;
877
911
  return this.set(
@@ -881,6 +915,7 @@ var Gleam = (() => {
881
915
  this.alpha * inv + other.alpha * amount
882
916
  );
883
917
  }
918
+ /** Round each RGB channel to the nearest integer. Alpha is untouched. Mutates and returns `this`. */
884
919
  round() {
885
920
  return this.set(
886
921
  Math.round(this.r),
@@ -888,6 +923,7 @@ var Gleam = (() => {
888
923
  Math.round(this.b)
889
924
  );
890
925
  }
926
+ /** 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`. */
891
927
  saturate(value = 1) {
892
928
  const m1 = 0.213 + 0.787 * value;
893
929
  const m2 = 0.715 - 0.715 * value;
@@ -900,6 +936,7 @@ var Gleam = (() => {
900
936
  const m9 = 0.072 + 0.928 * value;
901
937
  return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
902
938
  }
939
+ /** Sepia matrix. `value` in `[0, 1]`: `0` is unchanged, `1` is full sepia. Mutates and returns `this`. */
903
940
  sepia(value = 1) {
904
941
  const m1 = 0.393 + 0.607 * (1 - value);
905
942
  const m2 = 0.769 - 0.769 * (1 - value);
@@ -912,6 +949,7 @@ var Gleam = (() => {
912
949
  const m9 = 0.131 + 0.869 * (1 - value);
913
950
  return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
914
951
  }
952
+ /** Tint or shade. `percent` in `[-1, 1]`: negative shades toward black, positive tints toward white, magnitude is the amount. Mutates and returns `this`. */
915
953
  shade(percent) {
916
954
  const target = percent < 0 ? 0 : 255;
917
955
  const p = Math.abs(percent);
@@ -921,6 +959,7 @@ var Gleam = (() => {
921
959
  this.b + (target - this.b) * p
922
960
  );
923
961
  }
962
+ /** CSS hex string. `#rrggbb` when alpha is exactly `1`, `#rrggbbaa` otherwise. Channels are rounded. */
924
963
  toHex() {
925
964
  const r = Math.round(this.r).toString(16).padStart(2, "0");
926
965
  const g = Math.round(this.g).toString(16).padStart(2, "0");
@@ -932,6 +971,7 @@ var Gleam = (() => {
932
971
  const a = Math.round(this.alpha * 255);
933
972
  return `${rgb}${a.toString(16).padStart(2, "0")}`;
934
973
  }
974
+ /** CSS HSL string. `hsl(h, s%, l%)` when alpha is exactly `1`, `hsla(...)` otherwise. Hue is in degrees. */
935
975
  toHSL() {
936
976
  const { h, s, l } = this.toHSLObject();
937
977
  const cssH = Math.round(h);
@@ -939,6 +979,7 @@ var Gleam = (() => {
939
979
  const cssL = Math.round(l);
940
980
  return this.alpha === 1 ? `hsl(${cssH}, ${cssS}%, ${cssL}%)` : `hsla(${cssH}, ${cssS}%, ${cssL}%, ${this.alpha.toFixed(2)})`;
941
981
  }
982
+ /** HSL(A) components as numbers — see {@link HSLObject}. Use when you need to compute against the values rather than render them as a string. */
942
983
  toHSLObject() {
943
984
  const r = this.r / 255;
944
985
  const g = this.g / 255;
@@ -966,15 +1007,18 @@ var Gleam = (() => {
966
1007
  }
967
1008
  return { h: h * 360, s: s * 100, l: l * 100, a: this.alpha };
968
1009
  }
1010
+ /** CSS RGB string. `rgb(r, g, b)` when alpha is exactly `1`, `rgba(r, g, b, a)` otherwise. Channels are rounded. */
969
1011
  toRGB() {
970
1012
  const r = Math.round(this.r);
971
1013
  const g = Math.round(this.g);
972
1014
  const b = Math.round(this.b);
973
1015
  return this.alpha === 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${this.alpha.toFixed(2)})`;
974
1016
  }
1017
+ /** New `Color` with the same channels. */
975
1018
  clone() {
976
1019
  return new _Color(this.r, this.g, this.b, this.alpha);
977
1020
  }
1021
+ /** Approximate equality (within `approxEqual` tolerance). Pass `compareAlpha: false` to ignore the alpha channel. */
978
1022
  equals(other, compareAlpha = true) {
979
1023
  return approxEqual(this.r, other.r) && approxEqual(this.g, other.g) && approxEqual(this.b, other.b) && (!compareAlpha || approxEqual(this.alpha, other.alpha));
980
1024
  }
@@ -1116,12 +1160,14 @@ var Gleam = (() => {
1116
1160
  __publicField(this, "sideIsDirty", true);
1117
1161
  this.set(x, y, w, h);
1118
1162
  }
1163
+ /** Build from an `HTMLElement` (via `getBoundingClientRect`) or a `DOMRect`. */
1119
1164
  static fromBoundingClientRect(rect) {
1120
1165
  if (rect instanceof HTMLElement) {
1121
1166
  rect = rect.getBoundingClientRect();
1122
1167
  }
1123
1168
  return new _Rect(rect.left, rect.top, rect.width, rect.height);
1124
1169
  }
1170
+ /** Axis-aligned bounding box of a polygon's points. Throws if the polygon has no points. */
1125
1171
  static fromPolygon(polygon) {
1126
1172
  if (polygon.points.length === 0) {
1127
1173
  throw new Error("Supplied polygon has no points!");
@@ -1146,34 +1192,43 @@ var Gleam = (() => {
1146
1192
  });
1147
1193
  return new _Rect(minX, minY, maxX - minX, maxY - minY);
1148
1194
  }
1195
+ /** Height. */
1149
1196
  get h() {
1150
1197
  return this._h;
1151
1198
  }
1199
+ /** Height. */
1152
1200
  set h(value) {
1153
1201
  this._h = value;
1154
1202
  this.sideIsDirty = true;
1155
1203
  }
1204
+ /** Width. */
1156
1205
  get w() {
1157
1206
  return this._w;
1158
1207
  }
1208
+ /** Width. */
1159
1209
  set w(value) {
1160
1210
  this._w = value;
1161
1211
  this.sideIsDirty = true;
1162
1212
  }
1213
+ /** Top-left x. */
1163
1214
  get x() {
1164
1215
  return this._x;
1165
1216
  }
1217
+ /** Top-left x. */
1166
1218
  set x(value) {
1167
1219
  this._x = value;
1168
1220
  this.sideIsDirty = true;
1169
1221
  }
1222
+ /** Top-left y. */
1170
1223
  get y() {
1171
1224
  return this._y;
1172
1225
  }
1226
+ /** Top-left y. */
1173
1227
  set y(value) {
1174
1228
  this._y = value;
1175
1229
  this.sideIsDirty = true;
1176
1230
  }
1231
+ /** Derived sides/center/halfSize. Lazily recomputed after any `x`/`y`/`w`/`h` change. */
1177
1232
  get sides() {
1178
1233
  if (this.sideIsDirty) {
1179
1234
  this._sides = {
@@ -1189,6 +1244,7 @@ var Gleam = (() => {
1189
1244
  }
1190
1245
  return this._sides;
1191
1246
  }
1247
+ /** 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`. */
1192
1248
  inflate(delta) {
1193
1249
  this.x -= delta;
1194
1250
  this.y -= delta;
@@ -1197,12 +1253,16 @@ var Gleam = (() => {
1197
1253
  this.sideIsDirty = true;
1198
1254
  return this;
1199
1255
  }
1256
+ /** Round `x` and `y` to the nearest integer. `w`/`h` are unchanged. Mutates and returns `this`. */
1200
1257
  round() {
1201
1258
  this.x = Math.round(this.x);
1202
1259
  this.y = Math.round(this.y);
1203
1260
  this.sideIsDirty = true;
1204
1261
  return this;
1205
1262
  }
1263
+ /**
1264
+ * 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`.
1265
+ */
1206
1266
  set(x = 0, y = 0, w, h) {
1207
1267
  if (typeof x === "number") {
1208
1268
  this.x = x;
@@ -1224,15 +1284,19 @@ var Gleam = (() => {
1224
1284
  this.sideIsDirty = true;
1225
1285
  return this;
1226
1286
  }
1287
+ /** AABB-vs-AABB overlap test (inclusive of touching edges). */
1227
1288
  collide(rect) {
1228
1289
  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;
1229
1290
  }
1291
+ /** `true` when `rect` is fully inside `this`. */
1230
1292
  collideFull(rect) {
1231
1293
  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;
1232
1294
  }
1295
+ /** `true` when `vec` lies inside `this` (inclusive of edges). */
1233
1296
  collidePoint(vec) {
1234
1297
  return this.x <= vec.x && vec.x <= this.x + this.w && this.y <= vec.y && vec.y <= this.y + this.h;
1235
1298
  }
1299
+ /** Side of `this` that `rect` overlaps from, or `"none"` if disjoint. Useful for picking a bounce axis. */
1236
1300
  collideSide(rect) {
1237
1301
  const dx = this.x + this.w * 0.5 - (rect.x + rect.w * 0.5);
1238
1302
  const dy = this.y + this.h * 0.5 - (rect.y + rect.h * 0.5);
@@ -1250,18 +1314,23 @@ var Gleam = (() => {
1250
1314
  }
1251
1315
  return collision;
1252
1316
  }
1317
+ /** Top-left corner as a new `Vec2`. */
1253
1318
  pos() {
1254
1319
  return new Vec2(this.x, this.y);
1255
1320
  }
1321
+ /** Width and height as a new `Vec2`. */
1256
1322
  size() {
1257
1323
  return new Vec2(this.w, this.h);
1258
1324
  }
1325
+ /** Debug string like `"Rect [x: 0, y: 0, w: 10, h: 20]"`. */
1259
1326
  toString() {
1260
1327
  return `Rect [x: ${this.x}, y: ${this.y}, w: ${this.w}, h: ${this.h}]`;
1261
1328
  }
1329
+ /** New `Rect` with the same values. */
1262
1330
  clone() {
1263
1331
  return new _Rect(this.x, this.y, this.w, this.h);
1264
1332
  }
1333
+ /** Approximate equality. Pass `withSize: false` to compare position only. */
1265
1334
  equals(other, withSize = true) {
1266
1335
  let output = approxEqual(this.x, other.x) && approxEqual(this.y, other.y);
1267
1336
  if (output && withSize) {
@@ -1293,25 +1362,31 @@ var Gleam = (() => {
1293
1362
  );
1294
1363
  var Vec2 = class _Vec2 {
1295
1364
  constructor(x = 0, y) {
1365
+ /** Horizontal component. */
1296
1366
  __publicField(this, "x", 0);
1367
+ /** Vertical component. */
1297
1368
  __publicField(this, "y", 0);
1298
1369
  this.calculate(Operation.Equal, x, y);
1299
1370
  }
1371
+ /** Unit vector at angle `rad` (radians), scaled per-axis. `scaleY` defaults to `scaleX`. */
1300
1372
  static fromAngle(rad, scaleX = 1, scaleY = scaleX) {
1301
1373
  return new _Vec2(Math.cos(rad) * scaleX, Math.sin(rad) * scaleY);
1302
1374
  }
1303
1375
  set(x, y) {
1304
1376
  return this.calculate(Operation.Equal, x, y);
1305
1377
  }
1378
+ /** Set each component to its absolute value. Mutates and returns `this`. */
1306
1379
  abs() {
1307
1380
  return this.map(Math.abs);
1308
1381
  }
1309
1382
  add(x, y) {
1310
1383
  return this.calculate(Operation.Add, x, y);
1311
1384
  }
1385
+ /** Round each component up. Mutates and returns `this`. */
1312
1386
  ceil() {
1313
1387
  return this.map(Math.ceil);
1314
1388
  }
1389
+ /** Clamp each axis to its `[min, max]` range. `y` defaults to `x`. Mutates and returns `this`. */
1315
1390
  clamp(x, y = x) {
1316
1391
  this.x = clamp(this.x, x[0], x[1]);
1317
1392
  this.y = clamp(this.y, y[0], y[1]);
@@ -1320,9 +1395,19 @@ var Gleam = (() => {
1320
1395
  div(x, y) {
1321
1396
  return this.calculate(Operation.Div, x, y);
1322
1397
  }
1398
+ /** Round each component down. Mutates and returns `this`. */
1323
1399
  floor() {
1324
1400
  return this.map(Math.floor);
1325
1401
  }
1402
+ /**
1403
+ * Apply `callback` to each component (`index` is `0` for x, `1` for y). Mutates and returns `this`.
1404
+ *
1405
+ * @example
1406
+ * ```ts
1407
+ * new Vec2(3.6, -2.1).map(Math.trunc); // Vec2 { x: 3, y: -2 }
1408
+ * new Vec2(2, 5).map((v, i) => v * (i + 1)); // Vec2 { x: 2, y: 10 }
1409
+ * ```
1410
+ */
1326
1411
  map(callback) {
1327
1412
  this.x = callback(this.x, 0);
1328
1413
  this.y = callback(this.y, 1);
@@ -1334,9 +1419,11 @@ var Gleam = (() => {
1334
1419
  mult(x, y) {
1335
1420
  return this.calculate(Operation.Mult, x, y);
1336
1421
  }
1422
+ /** Flip the sign of both components (same as `mult(-1)`). Mutates and returns `this`. */
1337
1423
  negate() {
1338
1424
  return this.mult(-1);
1339
1425
  }
1426
+ /** Scale to unit length. Zero-length vectors are left untouched and warn (throttled). Mutates and returns `this`. */
1340
1427
  normalize() {
1341
1428
  const length = this.length();
1342
1429
  if (approxEqual(length, 0)) {
@@ -1345,6 +1432,7 @@ var Gleam = (() => {
1345
1432
  }
1346
1433
  return this.map((value) => value / length);
1347
1434
  }
1435
+ /** Scale so `|x| + |y| === 1`. Zero-length vectors are left untouched. Mutates and returns `this`. */
1348
1436
  normalizeManhattan() {
1349
1437
  const length = this.lengthManhattan();
1350
1438
  if (approxEqual(length, 0)) {
@@ -1355,6 +1443,7 @@ var Gleam = (() => {
1355
1443
  rem(x, y) {
1356
1444
  return this.calculate(Operation.Rem, x, y);
1357
1445
  }
1446
+ /** Round each component to the nearest integer. Mutates and returns `this`. */
1358
1447
  round() {
1359
1448
  this.x = Math.round(this.x);
1360
1449
  this.y = Math.round(this.y);
@@ -1363,38 +1452,48 @@ var Gleam = (() => {
1363
1452
  sub(x, y) {
1364
1453
  return this.calculate(Operation.Sub, x, y);
1365
1454
  }
1455
+ /** Angle in radians. No arg: angle of `this` from origin. With `other`: angle from `this` toward `other`. */
1366
1456
  angle(other) {
1367
1457
  if (!other) {
1368
1458
  return Math.atan2(this.y, this.x);
1369
1459
  }
1370
1460
  return Math.atan2(other.y - this.y, other.x - this.x);
1371
1461
  }
1462
+ /** Euclidean distance to `other`. */
1372
1463
  distance(other) {
1373
1464
  return Math.sqrt(
1374
1465
  Math.pow(other.x - this.x, 2) + Math.pow(other.y - this.y, 2)
1375
1466
  );
1376
1467
  }
1468
+ /** Manhattan distance (`|dx| + |dy|`) to `other`. */
1377
1469
  distanceManhattan(other) {
1378
1470
  return Math.abs(other.x - this.x) + Math.abs(other.y - this.y);
1379
1471
  }
1472
+ /** Dot product with `other`. */
1380
1473
  dotProduct(other) {
1381
1474
  return this.x * other.x + this.y * other.y;
1382
1475
  }
1476
+ /** `true` when both components are finite (rules out `NaN` and `±Infinity`). */
1383
1477
  isValid() {
1384
1478
  return Number.isFinite(this.x) && Number.isFinite(this.y);
1385
1479
  }
1480
+ /** Euclidean magnitude (`sqrt(x² + y²)`). */
1386
1481
  length() {
1387
1482
  return Math.sqrt(this.x * this.x + this.y * this.y);
1388
1483
  }
1484
+ /** Manhattan magnitude (`|x| + |y|`). */
1389
1485
  lengthManhattan() {
1390
1486
  return Math.abs(this.x) + Math.abs(this.y);
1391
1487
  }
1488
+ /** Larger of the two components. */
1392
1489
  max() {
1393
1490
  return Math.max(this.x, this.y);
1394
1491
  }
1492
+ /** Smaller of the two components. */
1395
1493
  min() {
1396
1494
  return Math.min(this.x, this.y);
1397
1495
  }
1496
+ /** Tuple `[x, y]`. */
1398
1497
  toArray() {
1399
1498
  return [this.x, this.y];
1400
1499
  }
@@ -1404,9 +1503,11 @@ var Gleam = (() => {
1404
1503
  toRectAddSize(width, height) {
1405
1504
  return this.concat(false, width, height);
1406
1505
  }
1506
+ /** Debug string like `"Vec2 [x: 1, y: 2]"`. */
1407
1507
  toString() {
1408
1508
  return `Vec2 [x: ${this.x}, y: ${this.y}]`;
1409
1509
  }
1510
+ /** New `Vec2` with the same components. */
1410
1511
  clone() {
1411
1512
  return new _Vec2(this.x, this.y);
1412
1513
  }
@@ -1486,9 +1587,11 @@ var Gleam = (() => {
1486
1587
  // src/core/Settings.ts
1487
1588
  var LOCAL_STORAGE_KEY = "gleam";
1488
1589
  var Settings = class {
1590
+ /** Read-only view of the persisted localStorage blob. Writes go through {@link setLocalStorage}. */
1489
1591
  static get localStorage() {
1490
1592
  return this._localStorage;
1491
1593
  }
1594
+ /** 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. */
1492
1595
  static init(overrides, game) {
1493
1596
  if (this.initialized) {
1494
1597
  throw new Error("Settings.init called twice");
@@ -1528,6 +1631,7 @@ var Gleam = (() => {
1528
1631
  }
1529
1632
  this.initialized = true;
1530
1633
  }
1634
+ /** 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. */
1531
1635
  static setLocalStorage(key, value) {
1532
1636
  this._localStorage[key] = value;
1533
1637
  localStorage.setItem(
@@ -1536,16 +1640,27 @@ var Gleam = (() => {
1536
1640
  );
1537
1641
  }
1538
1642
  };
1643
+ /** Enable smoothing on the main canvas context. Default `false` for crisp pixel art. */
1539
1644
  __publicField(Settings, "antialias", false);
1645
+ /** Start the gameloop automatically after `init()` resolves. Disable to drive `gameloop.startLoop()` manually. Default `true`. */
1540
1646
  __publicField(Settings, "autoloop", true);
1647
+ /** CSS color used when {@link useClearRect} is `false`. Default `"#444"`. */
1541
1648
  __publicField(Settings, "backgroundColor", "#444");
1649
+ /** Debug mode: assigns the `Game` instance to `window.game` and lets {@link Keyboard} Escape stop the loop. Default `false`. */
1542
1650
  __publicField(Settings, "debug", false);
1651
+ /** Skip the per-frame canvas clear. Use for trail/decay effects where you manage clearing yourself. Default `false`. */
1543
1652
  __publicField(Settings, "doNotClear", false);
1653
+ /** Stretch the main canvas to fill the window on resize while preserving its aspect ratio. Default `true`. */
1544
1654
  __publicField(Settings, "enableResize", true);
1655
+ /** Default font family for `canman.setFontSize`. Default `"Arial"`. */
1545
1656
  __publicField(Settings, "font", "Arial");
1657
+ /** **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. */
1546
1658
  __publicField(Settings, "fps", 1 / 60);
1659
+ /** Callback invoked from the `beforeunload` handler when {@link warnBeforeClose} is `true`. Useful for "are you sure?" autosave logic. */
1547
1660
  __publicField(Settings, "triedToClose");
1661
+ /** Clear the canvas with `clearRect` (transparent) when `true`, or `fillRect` with {@link backgroundColor} when `false`. Default `true`. */
1548
1662
  __publicField(Settings, "useClearRect", true);
1663
+ /** Show a browser "are you sure?" dialog on tab close. Required for {@link triedToClose} to fire. Default `false`. */
1549
1664
  __publicField(Settings, "warnBeforeClose", false);
1550
1665
  __publicField(Settings, "initialized", false);
1551
1666
  // Only mutated via `setLocalStorage` (typed) and via the localStorage
@@ -2138,19 +2253,20 @@ var Gleam = (() => {
2138
2253
  // src/content/Animator.ts
2139
2254
  var _Animator = class _Animator {
2140
2255
  /**
2141
- * We store rendered images in a cache.
2142
- * So if you have the same entity multiple times, the images get computed once and then shared.
2143
- * This reduces overhead and frees time up for other important computations.
2144
- *
2145
- * `namespace` is the cache key prefix for these rendered images.
2146
- * So pass a different key for different Animators; otherwise, you will see wrong images.
2256
+ * 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.
2147
2257
  */
2148
2258
  constructor(entity, namespace) {
2259
+ /** 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. */
2149
2260
  __publicField(this, "active", true);
2261
+ /** Current rendered frame (after flip processing). Pulled from the cache by {@link setImage} every time the frame advances. */
2150
2262
  __publicField(this, "image");
2263
+ /** Index of the current frame within the current animation's `sprites` array. */
2151
2264
  __publicField(this, "imageId", 0);
2265
+ /** Flip the rendered sprite horizontally. Caches a separate "flipped" bucket so toggling is cheap. */
2152
2266
  __publicField(this, "lookLeft", false);
2267
+ /** One-shot callback that fires when the current animation's last frame finishes. Cleared after firing. */
2153
2268
  __publicField(this, "onEnd");
2269
+ /** Current sprite's `(width, height)`. Updated by {@link setImage}. */
2154
2270
  __publicField(this, "size", new Vec2());
2155
2271
  __publicField(this, "animations", []);
2156
2272
  __publicField(this, "currentAnimation", 0);
@@ -2178,9 +2294,11 @@ var Gleam = (() => {
2178
2294
  }
2179
2295
  });
2180
2296
  }
2297
+ /** The currently-playing animation. `undefined` if no animations have been added yet. */
2181
2298
  get current() {
2182
2299
  return this.animations[this.currentAnimation];
2183
2300
  }
2301
+ /** Blit the current cached frame at `entity.pos + offset`, shifted left by `size.x` to compensate for the 2×-wide cache canvas. */
2184
2302
  draw(context, offset = new Vec2()) {
2185
2303
  context.drawImage(
2186
2304
  this.image,
@@ -2188,6 +2306,7 @@ var Gleam = (() => {
2188
2306
  this.entity.pos.y + offset.y
2189
2307
  );
2190
2308
  }
2309
+ /** 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`. */
2191
2310
  update(dt) {
2192
2311
  if (!this.active) {
2193
2312
  return;
@@ -2231,6 +2350,7 @@ var Gleam = (() => {
2231
2350
  this.setImage();
2232
2351
  }
2233
2352
  }
2353
+ /** 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`. */
2234
2354
  add(name, sprites, timing, defaultAnim = false) {
2235
2355
  if (defaultAnim && this.animations.some((anim) => anim.default)) {
2236
2356
  console.error("Only one default animation allowed!");
@@ -2248,6 +2368,7 @@ var Gleam = (() => {
2248
2368
  this.play(name);
2249
2369
  }
2250
2370
  }
2371
+ /** Convenience wrapper around {@link add} that takes a packed {@link SpriteAnimation}. `defaultAnim` is OR'd with `anim.default`. */
2251
2372
  addAnimation(anim, defaultAnim = false) {
2252
2373
  this.add(
2253
2374
  anim.name,
@@ -2256,6 +2377,7 @@ var Gleam = (() => {
2256
2377
  anim.default || defaultAnim
2257
2378
  );
2258
2379
  }
2380
+ /** 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. */
2259
2381
  drawRotated(context, angle, offset = new Vec2()) {
2260
2382
  const x = this.entity.pos.x + offset.x;
2261
2383
  const y = this.entity.pos.y + offset.y;
@@ -2266,6 +2388,7 @@ var Gleam = (() => {
2266
2388
  context.drawImage(this.image, -w, -h);
2267
2389
  context.setTransform(1, 0, 0, 1, 0, 0);
2268
2390
  }
2391
+ /** 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. */
2269
2392
  play(name, onEnd, onFrame) {
2270
2393
  const index = this.animations.findIndex(
2271
2394
  (anim) => anim.name === name
@@ -2288,6 +2411,7 @@ var Gleam = (() => {
2288
2411
  }
2289
2412
  this.active = true;
2290
2413
  }
2414
+ /** {@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. */
2291
2415
  playIfNot(name, onEnd, onFrame) {
2292
2416
  if (!this.isPlaying(name)) {
2293
2417
  this.play(name, onEnd, onFrame);
@@ -2295,6 +2419,7 @@ var Gleam = (() => {
2295
2419
  }
2296
2420
  return false;
2297
2421
  }
2422
+ /** Queue an animation to play once the current one finishes. Pass `undefined` to cancel the queue. Used internally by {@link playOnce}. */
2298
2423
  playNextOnce(name) {
2299
2424
  this.lastPlayed = name;
2300
2425
  }
@@ -2305,9 +2430,11 @@ var Gleam = (() => {
2305
2430
  this.lastPlayed = this.current?.name;
2306
2431
  this.play(name, onEnd, onFrame);
2307
2432
  }
2433
+ /** Randomize the frame timer to a value in `[0, current.timing)`. Useful when spawning many instances of the same animation to break phase lockstep. */
2308
2434
  randomTimer() {
2309
2435
  this.timer = randomBetweenFloat(0, this.current.timing);
2310
2436
  }
2437
+ /** Drop every registered animation and clear this instance's cached frames. {@link active} resets to `true`; pending callbacks are cleared. */
2311
2438
  removeAllAnimations() {
2312
2439
  this.animations.length = 0;
2313
2440
  this.active = true;
@@ -2315,6 +2442,7 @@ var Gleam = (() => {
2315
2442
  this.onFrame = void 0;
2316
2443
  _Animator.clearSpriteCache(this.namespace);
2317
2444
  }
2445
+ /** Switch back to the default animation if one was registered (marked via `defaultAnim`); otherwise just stop animating ({@link active} = `false`). */
2318
2446
  reset() {
2319
2447
  const defaultAnim = this.animations.find(
2320
2448
  (anim) => anim.default
@@ -2326,6 +2454,7 @@ var Gleam = (() => {
2326
2454
  this.active = false;
2327
2455
  }
2328
2456
  }
2457
+ /** `true` when `name` matches the currently-playing animation. */
2329
2458
  isPlaying(name) {
2330
2459
  return this.current && this.current.name === name;
2331
2460
  }
@@ -2364,15 +2493,63 @@ var Gleam = (() => {
2364
2493
  __publicField(_Animator, "spriteCache", /* @__PURE__ */ new Map());
2365
2494
  var Animator = _Animator;
2366
2495
 
2496
+ // src/content/ControllerCursor.ts
2497
+ var ControllerCursor = class {
2498
+ /**
2499
+ * @param controller Gamepad input source.
2500
+ * @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.
2501
+ * @param sticks Anchor positions, one per stick to track. Cloned at construction so caller mutation is harmless.
2502
+ * @param range Max pixel deflection from anchor at full stick. Default `80`.
2503
+ */
2504
+ constructor(controller, crosshair, sticks, range = 80) {
2505
+ __publicField(this, "controller");
2506
+ __publicField(this, "crosshair");
2507
+ __publicField(this, "range");
2508
+ __publicField(this, "sticks", []);
2509
+ this.controller = controller;
2510
+ this.crosshair = crosshair;
2511
+ this.range = range;
2512
+ sticks.forEach(
2513
+ (stick) => this.sticks.push({ pos: stick.clone(), offset: new Vec2() })
2514
+ );
2515
+ }
2516
+ /** Draw a crosshair at `anchor + offset` for each tracked stick. */
2517
+ draw(context) {
2518
+ this.sticks.forEach((stick) => {
2519
+ context.drawImage(
2520
+ this.crosshair,
2521
+ stick.pos.x + stick.offset.x,
2522
+ stick.pos.y + stick.offset.y
2523
+ );
2524
+ });
2525
+ }
2526
+ /** 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. */
2527
+ update(dt) {
2528
+ const alpha = 1 - Math.pow(0.5, dt / 0.05);
2529
+ this.controller.poll().forEach((axi, index) => {
2530
+ const offset = this.sticks[index].offset;
2531
+ offset.x += (axi.x * this.range - offset.x) * alpha;
2532
+ offset.y += (axi.y * this.range - offset.y) * alpha;
2533
+ });
2534
+ }
2535
+ };
2536
+
2367
2537
  // src/content/Particle.ts
2368
2538
  var Particle = class {
2369
2539
  constructor(pos, color, size = 2) {
2540
+ /** CSS color string passed to `context.fillStyle` in {@link draw}. */
2370
2541
  __publicField(this, "color");
2542
+ /** Accumulated time (seconds) since spawn or last {@link resetLifetime}. */
2371
2543
  __publicField(this, "lifetime", 0);
2544
+ /** Lifetime cap in seconds, randomized to `[0.5, 1.5]` at construction. */
2372
2545
  __publicField(this, "maxLifeTime");
2546
+ /** Top-left position. Cloned from the constructor arg so the caller's `Vec2` isn't aliased. */
2373
2547
  __publicField(this, "pos");
2548
+ /** Circle radius (pixels). */
2374
2549
  __publicField(this, "size");
2550
+ /** Velocity in px/s. Seeded randomly by the constructor (random angle, random magnitude per axis). */
2375
2551
  __publicField(this, "vel");
2552
+ /** Backing storage for the {@link rect} getter. Subclasses can read it; the public-facing accessor is {@link rect}. */
2376
2553
  __publicField(this, "_rect");
2377
2554
  this.pos = pos.clone();
2378
2555
  this.color = color;
@@ -2385,12 +2562,15 @@ var Gleam = (() => {
2385
2562
  );
2386
2563
  this.maxLifeTime = 0.5 + Math.random();
2387
2564
  }
2565
+ /** `false` once {@link lifetime} reaches {@link maxLifeTime}. Owners typically filter dead particles out of their list each frame, or call {@link resetLifetime} to recycle. */
2388
2566
  get alive() {
2389
2567
  return this.lifetime < this.maxLifeTime;
2390
2568
  }
2569
+ /** Read-only AABB tracking `pos` and the particle's `size`. Recomputed each {@link update}. */
2391
2570
  get rect() {
2392
2571
  return this._rect;
2393
2572
  }
2573
+ /** Fill a circle at `pos + offset` using {@link color}. `offset` is useful for shifting by a camera/world transform without mutating `pos`. */
2394
2574
  draw(context, offset = new Vec2()) {
2395
2575
  context.fillStyle = this.color;
2396
2576
  context.drawCircle(
@@ -2402,12 +2582,14 @@ var Gleam = (() => {
2402
2582
  "fill"
2403
2583
  );
2404
2584
  }
2585
+ /** Integrate lifetime, position, and bounding rect. */
2405
2586
  update(dt) {
2406
2587
  this.lifetime += dt;
2407
2588
  this.pos.x += this.vel.x * dt;
2408
2589
  this.pos.y += this.vel.y * dt;
2409
2590
  this._rect.set(this.pos.x, this.pos.y);
2410
2591
  }
2592
+ /** 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. */
2411
2593
  resetLifetime() {
2412
2594
  this.lifetime -= this.maxLifeTime;
2413
2595
  }
@@ -2416,14 +2598,23 @@ var Gleam = (() => {
2416
2598
  // src/content/Projectile.ts
2417
2599
  var Projectile = class {
2418
2600
  constructor(pos, image, vel = new Vec2()) {
2601
+ /** Seconds after which {@link alive} flips to `false`. Defaults to `Infinity` — no natural expiry. */
2419
2602
  __publicField(this, "maxLifetime", Infinity);
2603
+ /** Caller-supplied data. Typed via the class generic so consumers can read `projectile.payload` without casting. */
2420
2604
  __publicField(this, "payload");
2605
+ /** Magnitude multiplier applied to `vel` each update: `pos += vel * speed * dt`. Pass a unit-length `vel` to make this read as "pixels per second". */
2421
2606
  __publicField(this, "speed", 1200);
2607
+ /** Current pre-rotated sprite. Re-baked by {@link rebuildRotation} from the un-rotated `originalImage`. */
2422
2608
  __publicField(this, "image");
2609
+ /** Accumulated time (seconds). Drives the {@link alive} check against {@link maxLifetime}. */
2423
2610
  __publicField(this, "lifetime", 0);
2611
+ /** Top-left position. Cloned from the constructor arg so the caller's `Vec2` isn't aliased. */
2424
2612
  __publicField(this, "pos");
2613
+ /** Current sprite rotation in radians, kept in sync with `vel` by {@link rebuildRotation}. */
2425
2614
  __publicField(this, "rotation", 0);
2615
+ /** 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. */
2426
2616
  __publicField(this, "vel");
2617
+ /** Backing storage for the {@link rect} getter. Subclasses can read it; mutate via `pos`/{@link rebuildRotation} instead of touching it directly. */
2427
2618
  __publicField(this, "_rect");
2428
2619
  __publicField(this, "originalImage");
2429
2620
  this.pos = pos.clone();
@@ -2432,12 +2623,15 @@ var Gleam = (() => {
2432
2623
  this._rect = pos.toRectAddSize(image.width, image.height);
2433
2624
  this.rebuildRotation();
2434
2625
  }
2626
+ /** `false` once {@link lifetime} reaches {@link maxLifetime}. Owners typically filter dead projectiles out of their list each frame. */
2435
2627
  get alive() {
2436
2628
  return this.lifetime < this.maxLifetime;
2437
2629
  }
2630
+ /** Read-only AABB tracking `pos` and the (rotated) image size. Recomputed in {@link update} and {@link rebuildRotation}. */
2438
2631
  get rect() {
2439
2632
  return this._rect;
2440
2633
  }
2634
+ /** Blit the pre-rotated image at `pos + offset`. `offset` is useful for shifting by a camera/world transform without mutating `pos`. */
2441
2635
  draw(context, offset = new Vec2()) {
2442
2636
  context.drawImage(
2443
2637
  this.image,
@@ -2445,6 +2639,7 @@ var Gleam = (() => {
2445
2639
  this.pos.y + offset.y
2446
2640
  );
2447
2641
  }
2642
+ /** Integrate motion and advance lifetime. Doesn't re-bake the rotation — call {@link rebuildRotation} after mutating `vel`. */
2448
2643
  update(dt) {
2449
2644
  this.lifetime += dt;
2450
2645
  this.pos.x += this.vel.x * this.speed * dt;
@@ -2452,8 +2647,7 @@ var Gleam = (() => {
2452
2647
  this._rect.set(this.pos.x, this.pos.y);
2453
2648
  }
2454
2649
  /**
2455
- * Allocates a fresh rotated canvas on each call — no internal cache.
2456
- * Caching by quantized rotation could be a feature when projectiles need to re-aim every tick (homing/seeking).
2650
+ * 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.
2457
2651
  */
2458
2652
  rebuildRotation() {
2459
2653
  this.rotation = Math.atan2(this.vel.y, this.vel.x);
@@ -2461,6 +2655,7 @@ var Gleam = (() => {
2461
2655
  this._rect.w = this.image.width;
2462
2656
  this._rect.h = this.image.height;
2463
2657
  }
2658
+ /** Force {@link alive} to `false` immediately (sets `lifetime` past `maxLifetime`). Use when the projectile should die on collision/impact, not from natural expiry. */
2464
2659
  remove() {
2465
2660
  this.lifetime = this.maxLifetime * 2;
2466
2661
  }
@@ -2468,40 +2663,56 @@ var Gleam = (() => {
2468
2663
 
2469
2664
  // src/core/CanvasManager.ts
2470
2665
  var CANVAS_TYPES = {
2666
+ /** Catch-all tag for canvases without a specific role. */
2471
2667
  ANY: /* @__PURE__ */ Symbol("any"),
2668
+ /** Generic placeholder tag — distinct from `ANY` so consumers can differentiate. */
2472
2669
  DEFAULT: /* @__PURE__ */ Symbol("default"),
2670
+ /** Background canvas (drawn behind the main one). */
2473
2671
  BACKGROUND: /* @__PURE__ */ Symbol("background"),
2672
+ /** Primary render target. Exactly one canvas must be registered with this type before {@link CanvasManager.finishSetup}. */
2474
2673
  MAIN: /* @__PURE__ */ Symbol("main")
2475
2674
  };
2476
2675
  var CanvasManager = class {
2477
2676
  constructor() {
2677
+ /** Cached `getBoundingClientRect()` of the main canvas. Refreshed in {@link resize}. Used to map pointer client coords into canvas space. */
2478
2678
  __publicField(this, "canvasBoundingClientRect");
2679
+ /** Registry of every {@link setupCanvas}-registered canvas, keyed by selector. */
2479
2680
  __publicField(this, "canvasHolder", {});
2681
+ /** Display-to-buffer scale factor after the last {@link resize} (`displayWidth / bufferWidth`). `1` until the first resize. */
2480
2682
  __publicField(this, "ratio", 1);
2683
+ /** Display (CSS-pixel) size of the main canvas after the last {@link resize}. Independent of the buffer dimensions in {@link width}/{@link height}. */
2481
2684
  __publicField(this, "resizedSize", new Vec2());
2482
2685
  __publicField(this, "mainHolder");
2483
2686
  }
2687
+ /** Main canvas element (the one registered with `CANVAS_TYPES.MAIN`). */
2484
2688
  get canvas() {
2485
2689
  return this.mainHolder.canvas;
2486
2690
  }
2691
+ /** Main canvas 2D rendering context. */
2487
2692
  get canvasContext() {
2488
2693
  return this.mainHolder.context;
2489
2694
  }
2695
+ /** Main canvas **buffer** height (the drawing surface, not the CSS display size). */
2490
2696
  get height() {
2491
2697
  return this.canvas.height;
2492
2698
  }
2699
+ /** Main canvas **buffer** height (the drawing surface, not the CSS display size). */
2493
2700
  set height(height) {
2494
2701
  this.canvas.height = height;
2495
2702
  }
2703
+ /** Main canvas buffer dimensions as a new `Vec2`. */
2496
2704
  get size() {
2497
2705
  return new Vec2(this.width, this.height);
2498
2706
  }
2707
+ /** Main canvas **buffer** width (the drawing surface, not the CSS display size). */
2499
2708
  get width() {
2500
2709
  return this.canvas.width;
2501
2710
  }
2711
+ /** Main canvas **buffer** width (the drawing surface, not the CSS display size). */
2502
2712
  set width(width) {
2503
2713
  this.canvas.width = width;
2504
2714
  }
2715
+ /** 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. */
2505
2716
  finishSetup() {
2506
2717
  if (this.mainHolder) {
2507
2718
  throw new Error("Already set up.");
@@ -2524,6 +2735,7 @@ var Gleam = (() => {
2524
2735
  EventSystem.addEventListener("resized", () => this.resize());
2525
2736
  }
2526
2737
  }
2738
+ /** 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. */
2527
2739
  resize() {
2528
2740
  const windowRatio = window.innerHeight / window.innerWidth;
2529
2741
  Object.values(this.canvasHolder).forEach((ch) => {
@@ -2549,10 +2761,12 @@ var Gleam = (() => {
2549
2761
  });
2550
2762
  this.canvasBoundingClientRect = this.canvas.getBoundingClientRect();
2551
2763
  }
2764
+ /** Set the main context's `font` to `${size}px "${font}"`. Defaults the family to `Settings.font`. */
2552
2765
  setFontSize(size, font = Settings.font) {
2553
2766
  this.canvasContext.font = `${size}px "${font}"`;
2554
2767
  }
2555
- setupCanvas(canvasType, selector, resize = true) {
2768
+ /** 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. */
2769
+ setupCanvas(canvasType, selector, resize = Settings.enableResize) {
2556
2770
  if (!document.querySelector(selector)) {
2557
2771
  throw new Error("Canvas '" + selector + "' does not exist!");
2558
2772
  }
@@ -2581,6 +2795,7 @@ var Gleam = (() => {
2581
2795
  var MAX_STEPS_PER_FRAME = 5;
2582
2796
  var Gameloop = class {
2583
2797
  constructor(game) {
2798
+ /** 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. */
2584
2799
  __publicField(this, "levelTime", 0);
2585
2800
  __publicField(this, "_isLooping", false);
2586
2801
  __publicField(this, "accumulator", 0);
@@ -2588,9 +2803,11 @@ var Gleam = (() => {
2588
2803
  __publicField(this, "stop", false);
2589
2804
  this.game = game;
2590
2805
  }
2806
+ /** `true` while the rAF callback is registered. Goes `false` only after the final frame fires the `"gameloopStopped"` event. */
2591
2807
  get isLooping() {
2592
2808
  return this._isLooping;
2593
2809
  }
2810
+ /** Begin the rAF loop. Throws if {@link stopLoop} was called but teardown hasn't completed yet — wait for the `"gameloopStopped"` event before restarting. */
2594
2811
  startLoop() {
2595
2812
  if (this._isLooping && this.stop) {
2596
2813
  throw new Error(
@@ -2600,6 +2817,7 @@ var Gleam = (() => {
2600
2817
  this.stop = false;
2601
2818
  this.looper();
2602
2819
  }
2820
+ /** Request that the loop stop on its next tick. Asynchronous — the loop tears down on the following frame and dispatches `"gameloopStopped"` when done. */
2603
2821
  stopLoop() {
2604
2822
  this.stop = true;
2605
2823
  }
@@ -2657,53 +2875,98 @@ var Gleam = (() => {
2657
2875
 
2658
2876
  // src/input/Keyboard.ts
2659
2877
  var KEYBOARD_KEYS = {
2878
+ /** Digit row `0`. */
2660
2879
  KEY_0: "Digit0",
2880
+ /** Digit row `1`. */
2661
2881
  KEY_1: "Digit1",
2882
+ /** Digit row `2`. */
2662
2883
  KEY_2: "Digit2",
2884
+ /** Digit row `3`. */
2663
2885
  KEY_3: "Digit3",
2886
+ /** Digit row `4`. */
2664
2887
  KEY_4: "Digit4",
2888
+ /** Digit row `5`. */
2665
2889
  KEY_5: "Digit5",
2890
+ /** Digit row `6`. */
2666
2891
  KEY_6: "Digit6",
2892
+ /** Digit row `7`. */
2667
2893
  KEY_7: "Digit7",
2894
+ /** Digit row `8`. */
2668
2895
  KEY_8: "Digit8",
2896
+ /** Digit row `9`. */
2669
2897
  KEY_9: "Digit9",
2898
+ /** Letter `A`. */
2670
2899
  KEY_A: "KeyA",
2900
+ /** Letter `B`. */
2671
2901
  KEY_B: "KeyB",
2902
+ /** Letter `C`. */
2672
2903
  KEY_C: "KeyC",
2904
+ /** Letter `D`. */
2673
2905
  KEY_D: "KeyD",
2906
+ /** Down arrow. */
2674
2907
  KEY_DOWN: "ArrowDown",
2908
+ /** Letter `E`. */
2675
2909
  KEY_E: "KeyE",
2910
+ /** Enter / Return. */
2676
2911
  KEY_ENTER: "Enter",
2912
+ /** Escape. Note: in `Settings.debug` mode this stops the gameloop. */
2677
2913
  KEY_ESCAPE: "Escape",
2914
+ /** Letter `F`. */
2678
2915
  KEY_F: "KeyF",
2916
+ /** Letter `G`. */
2679
2917
  KEY_G: "KeyG",
2918
+ /** Letter `H`. */
2680
2919
  KEY_H: "KeyH",
2920
+ /** Letter `I`. */
2681
2921
  KEY_I: "KeyI",
2922
+ /** Letter `J`. */
2682
2923
  KEY_J: "KeyJ",
2924
+ /** Letter `K`. */
2683
2925
  KEY_K: "KeyK",
2926
+ /** Letter `L`. */
2684
2927
  KEY_L: "KeyL",
2928
+ /** Left arrow. */
2685
2929
  KEY_LEFT: "ArrowLeft",
2930
+ /** Letter `M`. */
2686
2931
  KEY_M: "KeyM",
2932
+ /** Letter `N`. */
2687
2933
  KEY_N: "KeyN",
2934
+ /** Letter `O`. */
2688
2935
  KEY_O: "KeyO",
2936
+ /** Letter `P`. */
2689
2937
  KEY_P: "KeyP",
2938
+ /** Letter `Q`. */
2690
2939
  KEY_Q: "KeyQ",
2940
+ /** Letter `R`. */
2691
2941
  KEY_R: "KeyR",
2942
+ /** Right arrow. */
2692
2943
  KEY_RIGHT: "ArrowRight",
2944
+ /** Letter `S`. */
2693
2945
  KEY_S: "KeyS",
2946
+ /** Space bar. */
2694
2947
  KEY_SPACE: "Space",
2948
+ /** Letter `T`. */
2695
2949
  KEY_T: "KeyT",
2950
+ /** Tab. */
2696
2951
  KEY_TAB: "Tab",
2952
+ /** Letter `U`. */
2697
2953
  KEY_U: "KeyU",
2954
+ /** Up arrow. */
2698
2955
  KEY_UP: "ArrowUp",
2956
+ /** Letter `V`. */
2699
2957
  KEY_V: "KeyV",
2958
+ /** Letter `W`. */
2700
2959
  KEY_W: "KeyW",
2960
+ /** Letter `X`. */
2701
2961
  KEY_X: "KeyX",
2962
+ /** Letter `Y`. */
2702
2963
  KEY_Y: "KeyY",
2964
+ /** Letter `Z`. */
2703
2965
  KEY_Z: "KeyZ"
2704
2966
  };
2705
2967
  var Keyboard = class {
2706
2968
  constructor(game) {
2969
+ /** 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`). */
2707
2970
  __publicField(this, "keys", {});
2708
2971
  const keyEvent = (event) => {
2709
2972
  const code = event.code;
@@ -2724,14 +2987,17 @@ var Gleam = (() => {
2724
2987
  window.addEventListener("blur", () => this.reset(), false);
2725
2988
  EventSystem.addEventListener("gameloopStopped", () => this.reset());
2726
2989
  }
2990
+ /** Mark every tracked key as released. Called automatically on `window` blur and on `gameloopStopped`. */
2727
2991
  reset() {
2728
2992
  for (const key in this.keys) {
2729
2993
  this.keys[key] = false;
2730
2994
  }
2731
2995
  }
2996
+ /** 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. */
2732
2997
  stopPress(code) {
2733
2998
  this.keys[code] = false;
2734
2999
  }
3000
+ /** `true` when `code` is currently held. Safe for untouched keys (returns `false` rather than `undefined`). */
2735
3001
  isPressed(code) {
2736
3002
  return !!this.keys[code];
2737
3003
  }
@@ -2739,20 +3005,32 @@ var Gleam = (() => {
2739
3005
 
2740
3006
  // src/input/Pointer.ts
2741
3007
  var POINTER_KEYS = {
3008
+ /** Primary button (left for right-handers). */
2742
3009
  LEFT: 0,
3010
+ /** Middle button / wheel click. */
2743
3011
  MIDDLE: 1,
3012
+ /** Secondary button (right for right-handers). */
2744
3013
  RIGHT: 2,
3014
+ /** "Back" side button (browser back). */
2745
3015
  PREV: 3,
3016
+ /** "Forward" side button. */
2746
3017
  FORWARD: 4
2747
3018
  };
2748
3019
  var Pointer = class {
2749
3020
  constructor(game) {
3021
+ /** 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". */
2750
3022
  __publicField(this, "hasMoved", false);
3023
+ /** Last raw `PointerEvent` received. `null` until any pointer event fires. Use for properties not surfaced as Vec2/booleans (pressure, pointerType, etc.). */
2751
3024
  __publicField(this, "lastEvent", null);
3025
+ /** Viewport-space coordinates (`event.clientX/Y` — CSS pixels relative to the page). */
2752
3026
  __publicField(this, "posReal", new Vec2());
3027
+ /** Previous tick's {@link posReal}. Subtract for a per-frame delta. */
2753
3028
  __publicField(this, "posRealLast", new Vec2());
3029
+ /** 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. */
2754
3030
  __publicField(this, "posScaled", new Vec2());
3031
+ /** Previous tick's {@link posScaled}. */
2755
3032
  __publicField(this, "posScaledLast", new Vec2());
3033
+ /** Per-button pressed state. Index with {@link POINTER_KEYS} (e.g. `pressed[POINTER_KEYS.LEFT]`). Sparse — unindexed entries are `undefined`, not `false`. */
2756
3034
  __publicField(this, "pressed", []);
2757
3035
  __publicField(this, "game");
2758
3036
  this.game = game;
@@ -2785,6 +3063,7 @@ var Gleam = (() => {
2785
3063
  false
2786
3064
  );
2787
3065
  }
3066
+ /** Clear all pressed-button state. Called automatically on `window` blur so held buttons don't stay "pressed" forever when focus is lost. */
2788
3067
  reset() {
2789
3068
  this.pressed.length = 0;
2790
3069
  }
@@ -3125,9 +3404,13 @@ var Gleam = (() => {
3125
3404
  // src/core/Game.ts
3126
3405
  var Game = class {
3127
3406
  constructor(settingOverrides = {}) {
3407
+ /** Canvas registry + 2D context exposure. Register canvases here from the constructor (`canman.setupCanvas(CANVAS_TYPES.MAIN, "#game")`) before calling {@link preInit}. */
3128
3408
  __publicField(this, "canman", new CanvasManager());
3409
+ /** The fixed-step driver. Started automatically by `preInit` when `Settings.autoloop` is `true`. */
3129
3410
  __publicField(this, "gameloop");
3411
+ /** Live keyboard state. See {@link Keyboard}. */
3130
3412
  __publicField(this, "keyboard");
3413
+ /** Live pointer (mouse / pen / touch) state. See {@link Pointer}. */
3131
3414
  __publicField(this, "pointer");
3132
3415
  __publicField(this, "initialized", false);
3133
3416
  Settings.init(settingOverrides, this);
@@ -3136,17 +3419,21 @@ var Gleam = (() => {
3136
3419
  this.keyboard = new Keyboard(this);
3137
3420
  this.pointer = new Pointer(this);
3138
3421
  }
3422
+ /** Render the current frame. Called by {@link Gameloop} after the canvas is cleared. Subclasses must override — the default throws. */
3139
3423
  draw(_context) {
3140
3424
  throw new Error("Override draw function!");
3141
3425
  }
3426
+ /** 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. */
3142
3427
  update(_dt) {
3143
3428
  throw new Error("Override update function!");
3144
3429
  }
3430
+ /** 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. */
3145
3431
  async init() {
3146
3432
  throw new Error(
3147
3433
  "Override init() and start the game via preInit() \u2014 do not call init() directly."
3148
3434
  );
3149
3435
  }
3436
+ /** 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). */
3150
3437
  async preInit(doInit = true) {
3151
3438
  if (this.initialized) {
3152
3439
  throw new Error(
@@ -3173,8 +3460,11 @@ var Gleam = (() => {
3173
3460
 
3174
3461
  // src/effects/Screenshake.ts
3175
3462
  var SHAKE_TYPES = {
3463
+ /** ~0.33 s wobble combining a small random rotation with a blur fall-off. */
3176
3464
  NORMAL: {
3465
+ /** Decay rate — see {@link ShakeType.step}. */
3177
3466
  step: 3,
3467
+ /** Per-frame mutator — see {@link ShakeType.update}. */
3178
3468
  update(updateCss, time) {
3179
3469
  const tr = `rotate(${randomBetweenFloat(-2, 2) * time}deg)`;
3180
3470
  updateCss("transform", tr);
@@ -3182,8 +3472,11 @@ var Gleam = (() => {
3182
3472
  updateCss("filter", `blur(${time * 5}px)`);
3183
3473
  }
3184
3474
  },
3475
+ /** ~0.07 s impact: blur-only fall-off, no rotation. */
3185
3476
  FAST: {
3477
+ /** Decay rate — see {@link ShakeType.step}. */
3186
3478
  step: 15,
3479
+ /** Per-frame mutator — see {@link ShakeType.update}. */
3187
3480
  update(updateCss, time) {
3188
3481
  updateCss("filter", `blur(${time * 3}px)`);
3189
3482
  }
@@ -3196,7 +3489,7 @@ var Gleam = (() => {
3196
3489
  __publicField(this, "style");
3197
3490
  this.style = element.style;
3198
3491
  }
3199
- /** Best-effort style restore: each key the shake writes is snapshotted on first write and rewritten when the shake ends or is disposed. */
3492
+ /** 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. */
3200
3493
  shake(shakeType = SHAKE_TYPES.NORMAL) {
3201
3494
  if (this.isShaking) {
3202
3495
  return null;
@@ -3234,72 +3527,48 @@ var Gleam = (() => {
3234
3527
  }
3235
3528
  };
3236
3529
 
3237
- // src/input/ControllerCursor.ts
3238
- var SPEED = 600;
3239
- var CROSSHAIR;
3240
- (async () => CROSSHAIR = await loadImage(
3241
- "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"
3242
- ))();
3243
- var ControllerCursor = class {
3244
- constructor(controller, game, axisId) {
3245
- __publicField(this, "axisId");
3246
- __publicField(this, "controller");
3247
- __publicField(this, "game");
3248
- __publicField(this, "pos");
3249
- this.controller = controller;
3250
- this.game = game;
3251
- this.axisId = axisId;
3252
- this.pos = game.canman.size.mult(0.5);
3253
- }
3254
- get centerPos() {
3255
- return this.pos.clone().add(CROSSHAIR.width * 0.5, CROSSHAIR.height * 0.5);
3256
- }
3257
- draw(context) {
3258
- context.drawImage(CROSSHAIR, this.pos.x, this.pos.y);
3259
- }
3260
- update(dt) {
3261
- const step = this.controller.stick(this.axisId).mult(SPEED * dt);
3262
- if (step.length() === 0) {
3263
- return;
3264
- }
3265
- this.pos.x = clamp(
3266
- this.pos.x + step.x,
3267
- 0,
3268
- this.game.canman.width - CROSSHAIR.width
3269
- );
3270
- this.pos.y = clamp(
3271
- this.pos.y + step.y,
3272
- 0,
3273
- this.game.canman.height - CROSSHAIR.height
3274
- );
3275
- }
3276
- };
3277
-
3278
3530
  // src/input/Controller.ts
3279
3531
  var CONTROLLER_KEYS = {
3532
+ /** Bottom face button — `A` on Xbox, `×` on PlayStation. */
3280
3533
  A: 0,
3534
+ /** Right face button — `B` on Xbox, `○` on PlayStation. */
3281
3535
  B: 1,
3536
+ /** Left face button — `X` on Xbox, `□` on PlayStation. */
3282
3537
  X: 2,
3538
+ /** Top face button — `Y` on Xbox, `△` on PlayStation. */
3283
3539
  Y: 3,
3540
+ /** Left bumper / shoulder. */
3284
3541
  LB: 4,
3542
+ /** Right bumper / shoulder. */
3285
3543
  RB: 5,
3544
+ /** Left trigger. The digital pressed-state lives here; the analog value is on the underlying `Gamepad.buttons[6].value`. */
3286
3545
  LT: 6,
3546
+ /** Right trigger. The digital pressed-state lives here; the analog value is on the underlying `Gamepad.buttons[7].value`. */
3287
3547
  RT: 7,
3548
+ /** Back / Select / Share. */
3288
3549
  SELECT: 8,
3550
+ /** Start / Options / Menu. */
3289
3551
  START: 9,
3552
+ /** Left stick click (L3). */
3290
3553
  LEFT_STICK: 10,
3554
+ /** Right stick click (R3). */
3291
3555
  RIGHT_STICK: 11,
3556
+ /** D-pad up. */
3292
3557
  UP: 12,
3558
+ /** D-pad down. */
3293
3559
  DOWN: 13,
3560
+ /** D-pad left. */
3294
3561
  LEFT: 14,
3562
+ /** D-pad right. */
3295
3563
  RIGHT: 15,
3564
+ /** Guide / Home / PS button. Not exposed by all browsers. */
3296
3565
  GUIDE: 16
3297
3566
  };
3298
- var AXIS_THRESHOLD = 0.3;
3567
+ var DEADZONE = 0.25;
3299
3568
  var Controller = class {
3300
- constructor(game) {
3569
+ constructor() {
3570
+ /** 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. */
3301
3571
  __publicField(this, "buttons", []);
3302
- __publicField(this, "cursors", []);
3303
3572
  __publicField(this, "axes", []);
3304
3573
  __publicField(this, "index", -1);
3305
3574
  __publicField(this, "lastTime", 0);
@@ -3307,11 +3576,11 @@ var Gleam = (() => {
3307
3576
  console.error("Controller not supported!");
3308
3577
  return;
3309
3578
  }
3310
- for (let i = 0; i < 2; i++) {
3311
- this.cursors.push(new ControllerCursor(this, game, i));
3312
- }
3313
3579
  window.addEventListener("gamepadconnected", (event) => {
3314
3580
  this.index = event.gamepad.index;
3581
+ for (let i = 0; i < event.gamepad.axes.length / 2; i++) {
3582
+ this.axes.push(new Vec2());
3583
+ }
3315
3584
  console.log("Gamepad connected:", event.gamepad.id);
3316
3585
  EventSystem.dispatchEvent(
3317
3586
  "inputControllerConnected",
@@ -3328,6 +3597,7 @@ var Gleam = (() => {
3328
3597
  "Our Gamepad was disconnected:",
3329
3598
  event.gamepad.index
3330
3599
  );
3600
+ this.reset();
3331
3601
  EventSystem.dispatchEvent("inputControllerDisconnected");
3332
3602
  } else {
3333
3603
  console.log(
@@ -3339,32 +3609,35 @@ var Gleam = (() => {
3339
3609
  );
3340
3610
  window.addEventListener("blur", () => this.reset(), false);
3341
3611
  }
3342
- draw(context) {
3343
- if (this.index < 0) {
3344
- return;
3345
- }
3346
- this.cursors.forEach((cursor) => cursor.draw(context));
3347
- }
3348
- update(dt) {
3349
- this.cursors.forEach((cursor) => cursor.update(dt));
3612
+ /** 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. */
3613
+ poll() {
3350
3614
  const gp = this.getGamepad();
3351
3615
  if (!gp || this.lastTime === gp.timestamp) {
3352
- return;
3616
+ return this.axes;
3353
3617
  }
3354
3618
  this.lastTime = gp.timestamp;
3355
3619
  this.buttons = gp.buttons.map(
3356
3620
  (button) => button.pressed
3357
3621
  );
3358
- this.axes.length = 0;
3359
- const axes = gp.axes;
3360
- for (let i = 0; i + 1 < axes.length; i += 2) {
3361
- this.axes.push(new Vec2(axes[i], axes[i + 1]));
3622
+ for (let i = 0; i < this.axes.length; i++) {
3623
+ this.axes[i].set(gp.axes[i * 2], gp.axes[i * 2 + 1]);
3624
+ const mag = this.axes[i].length();
3625
+ if (mag < DEADZONE) {
3626
+ this.axes[i].set(0, 0);
3627
+ } else {
3628
+ this.axes[i].mult(
3629
+ (Math.min(1, mag) - DEADZONE) / (1 - DEADZONE) / mag
3630
+ );
3631
+ }
3362
3632
  }
3633
+ return this.axes;
3363
3634
  }
3635
+ /** Clear {@link buttons} and the cached stick axes. Called automatically on `window` blur and on gamepad disconnect. */
3364
3636
  reset() {
3365
3637
  this.buttons.length = 0;
3366
3638
  this.axes.length = 0;
3367
3639
  }
3640
+ /** 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. */
3368
3641
  vibrate() {
3369
3642
  const gp = this.getGamepad();
3370
3643
  if (!gp) {
@@ -3381,14 +3654,6 @@ var Gleam = (() => {
3381
3654
  }
3382
3655
  return !!vibrator;
3383
3656
  }
3384
- stick(index) {
3385
- if (this.index < 0 || index >= this.axes.length) {
3386
- return new Vec2();
3387
- }
3388
- return this.axes[index].clone().map((value) => threshold(value, AXIS_THRESHOLD)).map(
3389
- (value) => Math.sign(value) * map(Math.abs(value), AXIS_THRESHOLD, 1, 0, 1)
3390
- );
3391
- }
3392
3657
  getGamepad() {
3393
3658
  return this.index < 0 ? null : navigator.getGamepads()[this.index] ?? null;
3394
3659
  }
@@ -3427,8 +3692,9 @@ var Gleam = (() => {
3427
3692
  this.addPoints(...points);
3428
3693
  }
3429
3694
  /**
3430
- * `angle` is the simplification threshold in radians: vertices whose turn
3431
- * angle wraps to within ±`angle` of straight are dropped.
3695
+ * 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.
3696
+ *
3697
+ * **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.
3432
3698
  */
3433
3699
  static fromCanvas(canvas, detail, angle) {
3434
3700
  detail = Math.max(2, detail);
@@ -3530,6 +3796,7 @@ var Gleam = (() => {
3530
3796
  }
3531
3797
  return new _Polygon(...points);
3532
3798
  }
3799
+ /** Regular convex polygon with `edges` vertices, inscribed in a bounding box of `size` (number = square). */
3533
3800
  static fromEdges(edges, size) {
3534
3801
  const s = size instanceof Vec2 ? size : new Vec2(size, size);
3535
3802
  const rad = Math.min(s.x, s.y) * 0.5;
@@ -3550,6 +3817,7 @@ var Gleam = (() => {
3550
3817
  }
3551
3818
  return new _Polygon(...points);
3552
3819
  }
3820
+ /** Polygon from the four corners of `rect`. */
3553
3821
  static fromRect(rect) {
3554
3822
  return new _Polygon(
3555
3823
  new Vec2(rect.x, rect.y),
@@ -3558,12 +3826,15 @@ var Gleam = (() => {
3558
3826
  new Vec2(rect.x, rect.y + rect.h)
3559
3827
  );
3560
3828
  }
3829
+ /** Centroid (mean of vertex positions). Recomputed whenever the vertex set changes. */
3561
3830
  get center() {
3562
3831
  return this._center;
3563
3832
  }
3833
+ /** Read-only view of the current vertex list. Use `addPoint`/`offset`/`rotate` to mutate. */
3564
3834
  get points() {
3565
3835
  return this._points;
3566
3836
  }
3837
+ /** Stroke the polygon to `context`, shifted by `offset`. Coordinates are truncated to integers (via `| 0`) for crisp lines. */
3567
3838
  draw(context, offset = new Vec2()) {
3568
3839
  if (this._points.length === 0) {
3569
3840
  return;
@@ -3582,21 +3853,25 @@ var Gleam = (() => {
3582
3853
  context.closePath();
3583
3854
  context.stroke();
3584
3855
  }
3856
+ /** Append a single vertex at `(x, y)`. Mutates and returns `this`. */
3585
3857
  addPoint(x, y) {
3586
3858
  this._points.push(new Vec2(x, y));
3587
3859
  this.update();
3588
3860
  return this;
3589
3861
  }
3862
+ /** Append cloned copies of every passed vertex. Mutates and returns `this`. */
3590
3863
  addPoints(...points) {
3591
3864
  points.forEach((point) => this._points.push(point.clone()));
3592
3865
  this.update();
3593
3866
  return this;
3594
3867
  }
3868
+ /** Translate every vertex by `(x, y)`. Mutates and returns `this`. */
3595
3869
  offset(x = 0, y = 0) {
3596
3870
  this._points.forEach((point) => point.add(x, y));
3597
3871
  this.update();
3598
3872
  return this;
3599
3873
  }
3874
+ /** Rotate by `angle` radians around `pos` (defaults to the centroid). Mutates and returns `this`. */
3600
3875
  rotate(angle, pos = this.center) {
3601
3876
  if (!angle) {
3602
3877
  return this;
@@ -3611,6 +3886,7 @@ var Gleam = (() => {
3611
3886
  this.update();
3612
3887
  return this;
3613
3888
  }
3889
+ /** 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. */
3614
3890
  collide(otherPolygon, velocity = new Vec2()) {
3615
3891
  const result = {
3616
3892
  intersect: true,
@@ -3678,6 +3954,7 @@ var Gleam = (() => {
3678
3954
  }
3679
3955
  return result;
3680
3956
  }
3957
+ /** New `Polygon` with the same vertices. */
3681
3958
  clone() {
3682
3959
  return new _Polygon(...this._points);
3683
3960
  }