@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/README.md +21 -3
- package/dist/gleam.d.ts +526 -62
- package/dist/gleam.esm.js +358 -81
- package/dist/gleam.esm.js.map +3 -3
- package/dist/gleam.js +358 -81
- package/dist/gleam.js.map +3 -3
- package/dist/gleam.min.js +7 -7
- package/dist/gleam.min.js.map +4 -4
- package/package.json +14 -12
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
3442
|
+
var DEADZONE = 0.25;
|
|
3176
3443
|
var Controller = class {
|
|
3177
|
-
constructor(
|
|
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
|
-
|
|
3220
|
-
|
|
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
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
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
|
-
*
|
|
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,
|