@lewismoten/gamemaker-sandbox-engine 0.0.0-stage → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +24 -0
  3. package/NOTICE.md +58 -0
  4. package/README.md +149 -2
  5. package/dist/assets/music.worklet-aD2x7RRM.js +466 -0
  6. package/dist/audio/cmfLoad.js +10 -0
  7. package/dist/audio/cmfPlayer.js +201 -0
  8. package/dist/audio/context.js +75 -0
  9. package/dist/audio/engine.js +173 -0
  10. package/dist/audio/mix.js +40 -0
  11. package/dist/audio/opl.js +245 -0
  12. package/dist/audio/output.js +40 -0
  13. package/dist/core/gameFiles.js +89 -0
  14. package/dist/engine/character.js +187 -0
  15. package/dist/engine/funcKeys.js +164 -0
  16. package/dist/engine/game.js +133 -0
  17. package/dist/engine/keys.js +68 -0
  18. package/dist/engine/machine.js +192 -0
  19. package/dist/engine/menus.js +135 -0
  20. package/dist/engine/monsters.js +259 -0
  21. package/dist/engine/playGain.js +10 -0
  22. package/dist/engine/playerLook.js +48 -0
  23. package/dist/engine/recorder.js +127 -0
  24. package/dist/engine/render.js +45 -0
  25. package/dist/engine/scene.js +324 -0
  26. package/dist/engine/scroll.js +79 -0
  27. package/dist/engine/showGraphic.js +107 -0
  28. package/dist/engine/slowPc.js +98 -0
  29. package/dist/engine/slowPcMusic.js +46 -0
  30. package/dist/engine/touch.js +61 -0
  31. package/dist/engine/world.js +205 -0
  32. package/dist/formats/binary.js +120 -0
  33. package/dist/formats/blocks.js +103 -0
  34. package/dist/formats/character.js +83 -0
  35. package/dist/formats/cmf.js +37 -0
  36. package/dist/formats/cmfEdit.js +154 -0
  37. package/dist/formats/cmfPreview.js +89 -0
  38. package/dist/formats/cmfSong.js +190 -0
  39. package/dist/formats/fli.js +340 -0
  40. package/dist/formats/fliFrames.js +34 -0
  41. package/dist/formats/fliLimits.js +54 -0
  42. package/dist/formats/game.js +198 -0
  43. package/dist/formats/gif.js +125 -0
  44. package/dist/formats/gifProgressive.js +99 -0
  45. package/dist/formats/graphicScripts.js +44 -0
  46. package/dist/formats/highscore.js +30 -0
  47. package/dist/formats/jpeg.js +359 -0
  48. package/dist/formats/map.js +38 -0
  49. package/dist/formats/monster.js +69 -0
  50. package/dist/formats/palette.js +58 -0
  51. package/dist/formats/recording.js +17 -0
  52. package/dist/formats/sound.js +44 -0
  53. package/dist/formats/voc.js +303 -0
  54. package/dist/gfx/colorVision.js +45 -0
  55. package/dist/gfx/cp437Glyphs.js +337 -0
  56. package/dist/gfx/font8x8.js +10 -0
  57. package/dist/gfx/screen.js +151 -0
  58. package/dist/index.js +15 -0
  59. package/dist/input/chords.js +46 -0
  60. package/dist/input/deskMouse.js +24 -0
  61. package/dist/input/keyboard.js +82 -0
  62. package/dist/input/scancodes.js +143 -0
  63. package/dist/player/fileExtensions.js +118 -0
  64. package/dist/player/patchRules.js +129 -0
  65. package/dist/player/patchTable.js +1753 -0
  66. package/dist/text/cp437.js +44 -0
  67. package/dist/types/audio/cmfLoad.d.ts +7 -0
  68. package/dist/types/audio/cmfPlayer.d.ts +69 -0
  69. package/dist/types/audio/context.d.ts +12 -0
  70. package/dist/types/audio/engine.d.ts +60 -0
  71. package/dist/types/audio/mix.d.ts +14 -0
  72. package/dist/types/audio/music.worklet.d.ts +22 -0
  73. package/dist/types/audio/opl.d.ts +37 -0
  74. package/dist/types/audio/output.d.ts +13 -0
  75. package/dist/types/core/gameFiles.d.ts +49 -0
  76. package/dist/types/engine/character.d.ts +30 -0
  77. package/dist/types/engine/funcKeys.d.ts +35 -0
  78. package/dist/types/engine/game.d.ts +57 -0
  79. package/dist/types/engine/keys.d.ts +17 -0
  80. package/dist/types/engine/machine.d.ts +103 -0
  81. package/dist/types/engine/menus.d.ts +46 -0
  82. package/dist/types/engine/monsters.d.ts +19 -0
  83. package/dist/types/engine/playGain.d.ts +11 -0
  84. package/dist/types/engine/playerLook.d.ts +44 -0
  85. package/dist/types/engine/recorder.d.ts +67 -0
  86. package/dist/types/engine/render.d.ts +8 -0
  87. package/dist/types/engine/scene.d.ts +188 -0
  88. package/dist/types/engine/scroll.d.ts +24 -0
  89. package/dist/types/engine/showGraphic.d.ts +39 -0
  90. package/dist/types/engine/slowPc.d.ts +124 -0
  91. package/dist/types/engine/slowPcMusic.d.ts +18 -0
  92. package/dist/types/engine/touch.d.ts +33 -0
  93. package/dist/types/engine/world.d.ts +237 -0
  94. package/dist/types/formats/binary.d.ts +44 -0
  95. package/dist/types/formats/blocks.d.ts +52 -0
  96. package/dist/types/formats/character.d.ts +65 -0
  97. package/dist/types/formats/cmf.d.ts +33 -0
  98. package/dist/types/formats/cmfEdit.d.ts +53 -0
  99. package/dist/types/formats/cmfPreview.d.ts +19 -0
  100. package/dist/types/formats/cmfSong.d.ts +74 -0
  101. package/dist/types/formats/fli.d.ts +56 -0
  102. package/dist/types/formats/fliFrames.d.ts +18 -0
  103. package/dist/types/formats/fliLimits.d.ts +13 -0
  104. package/dist/types/formats/game.d.ts +79 -0
  105. package/dist/types/formats/gif.d.ts +15 -0
  106. package/dist/types/formats/gifProgressive.d.ts +46 -0
  107. package/dist/types/formats/graphicScripts.d.ts +18 -0
  108. package/dist/types/formats/highscore.d.ts +10 -0
  109. package/dist/types/formats/jpeg.d.ts +20 -0
  110. package/dist/types/formats/map.d.ts +24 -0
  111. package/dist/types/formats/monster.d.ts +48 -0
  112. package/dist/types/formats/palette.d.ts +15 -0
  113. package/dist/types/formats/recording.d.ts +7 -0
  114. package/dist/types/formats/sound.d.ts +13 -0
  115. package/dist/types/formats/voc.d.ts +13 -0
  116. package/dist/types/gfx/colorVision.d.ts +26 -0
  117. package/dist/types/gfx/cp437Glyphs.d.ts +2 -0
  118. package/dist/types/gfx/font8x8.d.ts +2 -0
  119. package/dist/types/gfx/screen.d.ts +50 -0
  120. package/dist/types/index.d.ts +4 -0
  121. package/dist/types/input/chords.d.ts +22 -0
  122. package/dist/types/input/deskMouse.d.ts +14 -0
  123. package/dist/types/input/keyboard.d.ts +39 -0
  124. package/dist/types/input/scancodes.d.ts +3 -0
  125. package/dist/types/player/fileExtensions.d.ts +44 -0
  126. package/dist/types/player/patchRules.d.ts +24 -0
  127. package/dist/types/player/patchTable.d.ts +83 -0
  128. package/dist/types/text/cp437.d.ts +6 -0
  129. package/package.json +339 -4
@@ -0,0 +1,75 @@
1
+ let e, i = !1;
2
+ const r = /* @__PURE__ */ new Set(), l = ["pointerup", "touchend", "click", "keydown", "mousedown"];
3
+ function v() {
4
+ if (!(typeof window > "u"))
5
+ return window.AudioContext ?? window.webkitAudioContext;
6
+ }
7
+ function u() {
8
+ try {
9
+ const t = navigator.audioSession;
10
+ t && t.type !== "playback" && (t.type = "playback");
11
+ } catch {
12
+ }
13
+ }
14
+ function w() {
15
+ if (e) return e;
16
+ const t = v();
17
+ if (t) {
18
+ try {
19
+ u(), e = new t({ latencyHint: "interactive" });
20
+ } catch {
21
+ return;
22
+ }
23
+ return d(), e.addEventListener("statechange", () => {
24
+ e && e.state !== "running" && i && document.visibilityState === "visible" && e.resume().catch(() => {
25
+ });
26
+ }), navigator.userActivation?.isActive && s(), e;
27
+ }
28
+ }
29
+ function y() {
30
+ return i && !!e && e.state === "running";
31
+ }
32
+ function k(t) {
33
+ return r.add(t), () => r.delete(t);
34
+ }
35
+ function s() {
36
+ const t = w();
37
+ if (!t) return;
38
+ u();
39
+ const a = !i;
40
+ if (i = !0, t.state !== "running" && t.resume().catch(() => {
41
+ }), a)
42
+ try {
43
+ const n = t.createBufferSource();
44
+ n.buffer = t.createBuffer(1, 1, t.sampleRate), n.connect(t.destination), n.start(0);
45
+ } catch {
46
+ }
47
+ for (const n of [...r])
48
+ try {
49
+ n();
50
+ } catch (f) {
51
+ console.warn(f);
52
+ }
53
+ }
54
+ let c = !1;
55
+ function p(t) {
56
+ t.type === "keydown" && (t.key === "Shift" || t.key === "Meta") || t.isTrusted && (y() || s());
57
+ }
58
+ function o() {
59
+ e && i && e.state !== "running" && document.visibilityState === "visible" && e.resume().catch(() => {
60
+ });
61
+ }
62
+ function d() {
63
+ if (!(c || typeof window > "u")) {
64
+ c = !0;
65
+ for (const t of l) window.addEventListener(t, p, { capture: !0, passive: !0 });
66
+ document.addEventListener("visibilitychange", o), window.addEventListener("pageshow", o), window.addEventListener("focus", o);
67
+ }
68
+ }
69
+ typeof window < "u" && d();
70
+ export {
71
+ w as audioContext,
72
+ k as onSoundUnlocked,
73
+ y as soundUnlocked,
74
+ s as unlock
75
+ };
@@ -0,0 +1,173 @@
1
+ import { loadSong as g } from "./cmfLoad.js";
2
+ import { isVoc as p, decodeVoc as G } from "../formats/voc.js";
3
+ import { TICK_RATE as v } from "./cmfPlayer.js";
4
+ import { output as x } from "./output.js";
5
+ import { onMix as S, channelGain as m } from "./mix.js";
6
+ import { audioContext as k, unlock as E } from "./context.js";
7
+ const M = "" + new URL("../assets/music.worklet-aD2x7RRM.js", import.meta.url).href, d = 300;
8
+ let f;
9
+ class V {
10
+ ctx;
11
+ master;
12
+ /** Music and effects each go through their own gain, set by the visitor's sound settings (mix.ts). */
13
+ musicGain;
14
+ sfxGain;
15
+ /** Music and effects of a game being played also pass the Games setting (setGame). */
16
+ gameGain;
17
+ game = !1;
18
+ level = 1;
19
+ unmix;
20
+ music;
21
+ musicReady;
22
+ speaker;
23
+ voc;
24
+ vocCache = /* @__PURE__ */ new WeakMap();
25
+ musicEnabled = !0;
26
+ soundEnabled = !0;
27
+ cardEnabled = !0;
28
+ requestedSong;
29
+ loadedSong;
30
+ get soundCardEnabled() {
31
+ return this.cardEnabled;
32
+ }
33
+ /** Optional live host control: card removal pauses FM and stops digital effects, leaving the PC speaker alone. */
34
+ setSoundCard(t) {
35
+ this.cardEnabled !== t && (this.cardEnabled = t, t ? this.musicEnabled && this.requestedSong && (this.requestedSong === this.loadedSong ? this.pauseMusic(!1) : this.playSong(this.requestedSong)) : (this.stopVoc(), this.pauseMusic(!0)));
36
+ }
37
+ /**
38
+ * Starts the sound. The first time, call it from a user gesture's handler (iOS: inside
39
+ * the touchend/click itself, before any await); the page's one AudioContext (context.ts)
40
+ * is resumed there.
41
+ */
42
+ async resume() {
43
+ if (!this.ctx) {
44
+ const t = k();
45
+ if (!t) return;
46
+ this.ctx = t, this.master = t.createGain(), this.master.gain.value = 0.8, this.master.connect(x(t)), this.musicGain = t.createGain(), this.sfxGain = t.createGain(), this.gameGain = t.createGain(), this.gameGain.connect(this.master), this.musicGain.connect(this.gameGain), this.sfxGain.connect(this.gameGain);
47
+ const s = () => {
48
+ this.musicGain && (this.musicGain.gain.value = m("music")), this.sfxGain && (this.sfxGain.gain.value = m("sfx")), this.gameGain && (this.gameGain.gain.value = this.gameLevel());
49
+ };
50
+ s(), this.unmix = S(s), f ??= t.audioWorklet.addModule(M);
51
+ const e = this.master, i = this.musicGain;
52
+ this.musicReady = f.then(() => {
53
+ this.master === e && (this.music = new AudioWorkletNode(t, "gm-music", { outputChannelCount: [1] }), this.music.connect(i));
54
+ }).catch((a) => console.warn("Music unavailable:", a));
55
+ }
56
+ this.ctx.state !== "running" && (navigator.userActivation?.isActive && E(), await Promise.race([this.ctx.resume().catch(() => {
57
+ }), new Promise((t) => setTimeout(t, d))]));
58
+ }
59
+ /** Marks this engine as a game's: its sound also follows the Games setting (live). */
60
+ setGame(t) {
61
+ this.game = t, this.gameGain && (this.gameGain.gain.value = this.gameLevel());
62
+ }
63
+ /** A fixed trim (0 to 1) under the visitor's settings, for sound meant to sit quietly in the background. */
64
+ setLevel(t) {
65
+ this.level = t, this.gameGain && (this.gameGain.gain.value = this.gameLevel());
66
+ }
67
+ gameLevel() {
68
+ return (this.game ? m("games") : 1) * this.level;
69
+ }
70
+ /** Where this engine's sound leaves it (for recording just this game's sound). */
71
+ get node() {
72
+ return this.master;
73
+ }
74
+ /**
75
+ * Stops everything and lets go of the page's context (it is shared, so it stays open);
76
+ * a later resume() starts afresh.
77
+ */
78
+ dispose() {
79
+ this.stopAll(), this.music?.port.postMessage({ type: "end" }), this.unmix?.(), this.music?.disconnect(), this.musicGain?.disconnect(), this.sfxGain?.disconnect(), this.gameGain?.disconnect(), this.master?.disconnect(), this.unmix = this.music = this.musicGain = this.sfxGain = this.gameGain = this.master = this.ctx = void 0, this.musicReady = void 0;
80
+ }
81
+ get context() {
82
+ return this.ctx;
83
+ }
84
+ post(t, s = []) {
85
+ this.music?.port.postMessage(t, s);
86
+ }
87
+ /** PlaySong: returns false (silently, as the original did) for invalid songs. */
88
+ async playSong(t) {
89
+ if (this.stopSong(), !this.musicEnabled || (this.requestedSong = t, !this.cardEnabled)) return !1;
90
+ const s = g(t);
91
+ if (!s) return !1;
92
+ const e = ++this.songs, i = () => {
93
+ e !== this.songs || !this.cardEnabled || (this.loadedSong = t, this.post({ type: "load", music: s.music.slice(), instruments: s.instruments.map((n) => n.slice()) }));
94
+ }, a = this.musicReady;
95
+ return a && !this.music && await Promise.race([a, new Promise((n) => setTimeout(n, d * 3))]), this.music ? i() : a?.then(i), !0;
96
+ }
97
+ /** Counts songs asked for, so one still waiting for the worklet doesn't start after a newer one (or a stop). */
98
+ songs = 0;
99
+ stopSong() {
100
+ this.songs++, this.requestedSong = this.loadedSong = void 0, this.post({ type: "stop" });
101
+ }
102
+ /** The song stands still for `ms` (its notes keep sounding), then goes on from where it was: a slow PC's disk read. */
103
+ holdMusic(t) {
104
+ t > 0 && this.post({ type: "hold", ms: t });
105
+ }
106
+ pauseMusic(t) {
107
+ this.post({ type: "pause", paused: t });
108
+ }
109
+ /** DoSound's PC speaker path: one frequency per 1/145.6 s tick; 0 = silence. */
110
+ playSpeaker(t) {
111
+ if (!this.ctx || !this.master || !this.soundEnabled) return;
112
+ this.stopSpeaker();
113
+ const s = this.ctx.createOscillator(), e = this.ctx.createGain();
114
+ s.type = "square", e.gain.value = 0, s.connect(e).connect(this.sfxGain ?? this.master);
115
+ const i = this.ctx.currentTime + 5e-3, a = 1 / v;
116
+ for (let c = 0; c < t.length; c++) {
117
+ const o = t[c], r = i + c * a;
118
+ o >= 20 ? (s.frequency.setValueAtTime(Math.min(o, 2e4), r), e.gain.setValueAtTime(0.12, r)) : e.gain.setValueAtTime(0, r);
119
+ }
120
+ const n = i + t.length * a;
121
+ e.gain.setValueAtTime(0, n), s.start(i), s.stop(n + 0.01), this.speaker = { osc: s, gain: e }, s.onended = () => {
122
+ this.speaker?.osc === s && (this.speaker = void 0);
123
+ };
124
+ }
125
+ stopSpeaker() {
126
+ if (this.speaker) {
127
+ try {
128
+ this.speaker.osc.stop();
129
+ } catch {
130
+ }
131
+ this.speaker = void 0;
132
+ }
133
+ }
134
+ /** PlaySbVocFile: a new digital sound cuts off the previous one. */
135
+ playVoc(t) {
136
+ if (!this.cardEnabled || !this.ctx || !this.master || !t || !this.soundEnabled || !p(t)) return !1;
137
+ this.stopVoc();
138
+ let s = this.vocCache.get(t);
139
+ if (!s) {
140
+ const i = G(t);
141
+ if (!i.samples.length) return !1;
142
+ s = y(this.ctx, i.samples, i.sampleRate), this.vocCache.set(t, s);
143
+ }
144
+ const e = this.ctx.createBufferSource();
145
+ return e.buffer = s, e.connect(this.sfxGain ?? this.master), e.start(), this.voc = e, !0;
146
+ }
147
+ stopVoc() {
148
+ try {
149
+ this.voc?.stop();
150
+ } catch {
151
+ }
152
+ this.voc = void 0;
153
+ }
154
+ stopAll() {
155
+ this.stopSong(), this.stopSpeaker(), this.stopVoc();
156
+ }
157
+ }
158
+ function y(h, t, s) {
159
+ try {
160
+ const e = h.createBuffer(1, t.length, s);
161
+ return e.copyToChannel(t, 0), e;
162
+ } catch {
163
+ const e = h.sampleRate, i = Math.max(1, Math.floor(t.length * e / s)), a = h.createBuffer(1, i, e), n = a.getChannelData(0), c = s / e;
164
+ for (let o = 0; o < i; o++) {
165
+ const r = o * c, u = Math.floor(r), l = r - u;
166
+ n[o] = (t[u] ?? 0) * (1 - l) + (t[u + 1] ?? t[u] ?? 0) * l;
167
+ }
168
+ return a;
169
+ }
170
+ }
171
+ export {
172
+ V as AudioEngine
173
+ };
@@ -0,0 +1,40 @@
1
+ const s = "gm-sound-mix", l = ["music", "voice", "sfx", "games"], m = () => ({
2
+ music: { on: !0, volume: 100 },
3
+ voice: { on: !0, volume: 100 },
4
+ sfx: { on: !0, volume: 100 },
5
+ games: { on: !0, volume: 100 }
6
+ });
7
+ function r() {
8
+ const o = m();
9
+ try {
10
+ const u = JSON.parse(localStorage.getItem(s) ?? "null");
11
+ if (u && typeof u == "object")
12
+ for (const n of l) {
13
+ const e = u[n];
14
+ typeof e?.on == "boolean" && (o[n].on = e.on), typeof e?.volume == "number" && e.volume >= 0 && e.volume <= 100 && (o[n].volume = Math.round(e.volume));
15
+ }
16
+ } catch {
17
+ }
18
+ return o;
19
+ }
20
+ let t = r();
21
+ const c = /* @__PURE__ */ new Set(), a = () => t, i = (o) => t[o].on ? t[o].volume / 100 : 0;
22
+ function f(o, u) {
23
+ const n = { ...t[o], ...u };
24
+ n.volume = Math.max(0, Math.min(100, Math.round(n.volume))), t = { ...t, [o]: n };
25
+ try {
26
+ localStorage.setItem(s, JSON.stringify(t));
27
+ } catch {
28
+ }
29
+ for (const e of [...c]) e(t);
30
+ }
31
+ function v(o) {
32
+ return c.add(o), () => c.delete(o);
33
+ }
34
+ export {
35
+ l as CHANNELS,
36
+ i as channelGain,
37
+ a as getMix,
38
+ v as onMix,
39
+ f as setChannel
40
+ };
@@ -0,0 +1,245 @@
1
+ const y = [0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 12, 12, 15, 15], u = [0, 9, 12, 13.875, 15, 16.125, 16.875, 17.625, 18, 18.75, 19.125, 19.5, 19.875, 20.25, 20.625, 21], w = [0, 0.5, 0.25, 1], v = 49716, l = 96, D = [0, 1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 16, 17, 18, 19, 20, 21];
2
+ class m {
3
+ am = !1;
4
+ vib = !1;
5
+ sustainHold = !1;
6
+ ksr = !1;
7
+ mult = 1;
8
+ ksl = 0;
9
+ tl = 0;
10
+ ar = 0;
11
+ dr = 0;
12
+ sl = 0;
13
+ rr = 0;
14
+ wave = 0;
15
+ // Runtime state. envDb is the envelope attenuation (0 = full, SILENT_DB = off);
16
+ // out/prevOut keep the last two modulator outputs for feedback.
17
+ phase = 0;
18
+ // cycles
19
+ stage = 0;
20
+ envDb = l;
21
+ out = 0;
22
+ prevOut = 0;
23
+ keyOn() {
24
+ this.stage = 1, this.phase = 0;
25
+ }
26
+ keyOff() {
27
+ this.stage !== 0 && (this.stage = 4);
28
+ }
29
+ /** Rate in "rate units" (0..63) including key scale rate offset. */
30
+ rate(s, t) {
31
+ return s === 0 ? 0 : Math.min(63, s * 4 + (this.ksr ? t : t >> 2));
32
+ }
33
+ stepEnvelope(s, t) {
34
+ switch (this.stage) {
35
+ case 1: {
36
+ const e = this.rate(this.ar, t);
37
+ if (e === 0) return;
38
+ if (e >= 60)
39
+ this.envDb = 0;
40
+ else {
41
+ const a = 2.826 / Math.pow(2, (e - 4) / 4);
42
+ this.envDb -= (this.envDb + 8) * (s / a) * 3.2;
43
+ }
44
+ this.envDb <= 0 && (this.envDb = 0, this.stage = 2);
45
+ return;
46
+ }
47
+ case 2: {
48
+ const e = this.sl === 15 ? 93 : this.sl * 3;
49
+ this.envDb += this.decayDbPerSecond(this.dr, t) * s, this.envDb >= e && (this.envDb = e, this.stage = 3);
50
+ return;
51
+ }
52
+ // EG-TYP set (sustainHold) holds the level until key off; otherwise the
53
+ // percussive envelope keeps falling at the release rate.
54
+ case 3:
55
+ this.sustainHold || (this.envDb += this.decayDbPerSecond(this.rr, t) * s);
56
+ break;
57
+ case 4:
58
+ this.envDb += this.decayDbPerSecond(this.rr, t) * s;
59
+ break;
60
+ default:
61
+ return;
62
+ }
63
+ this.envDb >= l && (this.envDb = l, this.stage = 0);
64
+ }
65
+ decayDbPerSecond(s, t) {
66
+ const e = this.rate(s, t);
67
+ return e === 0 ? 0 : l / (39.28 / Math.pow(2, (e - 4) / 4));
68
+ }
69
+ /** Waveform value at phase (cycles): 0 sine, 1 half sine, 2 absolute sine, 3 pulse (quarter) sine. */
70
+ wave01(s) {
71
+ const t = s - Math.floor(s), e = Math.sin(t * 2 * Math.PI);
72
+ switch (this.wave) {
73
+ case 1:
74
+ return t < 0.5 ? e : 0;
75
+ case 2:
76
+ return Math.abs(e);
77
+ case 3:
78
+ return t < 0.25 || t >= 0.5 && t < 0.75 ? Math.abs(e) : 0;
79
+ default:
80
+ return e;
81
+ }
82
+ }
83
+ }
84
+ class S {
85
+ fnum = 0;
86
+ block = 0;
87
+ key = !1;
88
+ feedback = 0;
89
+ additive = !1;
90
+ ops = [new m(), new m()];
91
+ /** Hz = F-number * clock / 2^(20 - block); block is the octave. */
92
+ get freq() {
93
+ return this.fnum * v / Math.pow(2, 20 - this.block);
94
+ }
95
+ /** Key scale rate offset: block * 2 + bit 9 (or 8) of the F-number. */
96
+ rof(s) {
97
+ return this.block * 2 + (this.fnum >> (s ? 8 : 9) & 1);
98
+ }
99
+ /** KSL attenuation: table value minus 6 dB per octave below block 7, scaled by the KSL setting. */
100
+ kslDb(s) {
101
+ return s.ksl ? Math.max(0, u[this.fnum >> 6] - 6 * (7 - this.block)) * w[s.ksl] : 0;
102
+ }
103
+ }
104
+ class M {
105
+ sampleRate;
106
+ channels = Array.from({ length: 9 }, () => new S());
107
+ regs = new Uint8Array(256);
108
+ waveSelect = !1;
109
+ noteSel = !1;
110
+ amDepth = !1;
111
+ vibDepth = !1;
112
+ rhythm = !1;
113
+ time = 0;
114
+ noise = 1;
115
+ constructor(s) {
116
+ this.sampleRate = s;
117
+ }
118
+ reset() {
119
+ for (let s = 0; s < 256; s++) this.write(s, 0);
120
+ for (const s of this.channels)
121
+ for (const t of s.ops)
122
+ t.stage = 0, t.envDb = l;
123
+ }
124
+ read(s) {
125
+ return this.regs[s & 255];
126
+ }
127
+ write(s, t) {
128
+ if (s &= 255, t &= 255, this.regs[s] = t, s === 1) {
129
+ this.waveSelect = (t & 32) !== 0;
130
+ return;
131
+ }
132
+ if (s === 8) {
133
+ this.noteSel = (t & 64) !== 0;
134
+ return;
135
+ }
136
+ if (s === 189) {
137
+ this.writeRhythm(t);
138
+ return;
139
+ }
140
+ const e = s & 224;
141
+ if (e >= 32 && e <= 128 || e === 224) {
142
+ const n = D.indexOf(s & 31);
143
+ if (n < 0) return;
144
+ const i = this.channels[Math.floor(n / 6) * 3 + n % 3].ops[Math.floor(n % 6 / 3)];
145
+ switch (e) {
146
+ case 32:
147
+ i.am = (t & 128) !== 0, i.vib = (t & 64) !== 0, i.sustainHold = (t & 32) !== 0, i.ksr = (t & 16) !== 0, i.mult = y[t & 15];
148
+ break;
149
+ case 64:
150
+ i.ksl = t >> 6, i.tl = t & 63;
151
+ break;
152
+ case 96:
153
+ i.ar = t >> 4, i.dr = t & 15;
154
+ break;
155
+ case 128:
156
+ i.sl = t >> 4, i.rr = t & 15;
157
+ break;
158
+ case 224:
159
+ i.wave = this.waveSelect ? t & 3 : 0;
160
+ break;
161
+ }
162
+ return;
163
+ }
164
+ const a = s & 15;
165
+ if (a > 8) return;
166
+ const h = this.channels[a];
167
+ if (e === 160 && s < 176)
168
+ h.fnum = h.fnum & 768 | t;
169
+ else if (s >= 176 && s <= 184) {
170
+ h.fnum = h.fnum & 255 | (t & 3) << 8, h.block = t >> 2 & 7;
171
+ const n = (t & 32) !== 0;
172
+ if (n && !h.key) for (const o of h.ops) o.keyOn();
173
+ if (!n && h.key) for (const o of h.ops) o.keyOff();
174
+ h.key = n;
175
+ } else s >= 192 && s <= 200 && (h.feedback = t >> 1 & 7, h.additive = (t & 1) !== 0);
176
+ }
177
+ rhythmKeys = 0;
178
+ /**
179
+ * Register 0xBD: bit 7 tremolo depth, bit 6 vibrato depth, bit 5 rhythm mode, bits 0-4 the
180
+ * drum keys. In rhythm mode channels 6-8 become the bass drum (both operators of channel 6),
181
+ * hi-hat and snare (channel 7) and tom-tom and cymbal (channel 8).
182
+ */
183
+ writeRhythm(s) {
184
+ this.amDepth = (s & 128) !== 0, this.vibDepth = (s & 64) !== 0, this.rhythm = (s & 32) !== 0;
185
+ const t = this.rhythm ? s & 31 : 0, e = t ^ this.rhythmKeys, [a, h, n] = [this.channels[6], this.channels[7], this.channels[8]], o = (i, r) => {
186
+ if (e & i)
187
+ for (const c of r) t & i ? c.keyOn() : c.keyOff();
188
+ };
189
+ o(16, a.ops), o(8, [h.ops[1]]), o(4, [n.ops[0]]), o(2, [n.ops[1]]), o(1, [h.ops[0]]), this.rhythmKeys = t;
190
+ }
191
+ /** Renders mono samples into out (added to existing content). */
192
+ render(s, t = 0.12) {
193
+ const e = 1 / this.sampleRate;
194
+ for (let a = 0; a < s.length; a++) {
195
+ this.time += e;
196
+ const h = (1 - Math.cos(this.time * 3.7 * 2 * Math.PI)) / 2 * (this.amDepth ? 4.8 : 1), n = Math.sin(this.time * 6.1 * 2 * Math.PI) * (this.vibDepth ? 14 : 7), o = Math.pow(2, n / 1200);
197
+ this.noise = this.noise >> 1 | ((this.noise >> 0 ^ this.noise >> 14) & 1) << 22;
198
+ const i = this.noise & 1 ? 1 : -1;
199
+ let r = 0;
200
+ const c = this.rhythm ? 6 : 9;
201
+ for (let f = 0; f < c; f++) r += this.channelSample(this.channels[f], e, h, o);
202
+ this.rhythm && (r += this.rhythmSample(e, h, o, i)), s[a] += r * t;
203
+ }
204
+ }
205
+ /** Linear amplitude from the summed attenuation: envelope + TL (0.75 dB steps) + KSL + tremolo. */
206
+ opLevel(s, t, e) {
207
+ const a = t.envDb + t.tl * 0.75 + s.kslDb(t) + (t.am ? e : 0);
208
+ return a >= l ? 0 : Math.pow(10, -a / 20);
209
+ }
210
+ advance(s, t, e, a) {
211
+ t.phase += s.freq * t.mult * (t.vib ? a : 1) * e, t.phase > 1e6 && (t.phase -= Math.floor(t.phase)), t.stepEnvelope(e, s.rof(this.noteSel));
212
+ }
213
+ channelSample(s, t, e, a) {
214
+ const [h, n] = s.ops;
215
+ if (h.stage === 0 && n.stage === 0) return 0;
216
+ this.advance(s, h, t, a), this.advance(s, n, t, a);
217
+ const o = s.feedback ? (h.out + h.prevOut) * Math.pow(2, s.feedback - 7) : 0, i = h.wave01(h.phase + o) * this.opLevel(s, h, e);
218
+ h.prevOut = h.out, h.out = i;
219
+ const r = this.opLevel(s, n, e);
220
+ return s.additive ? i + n.wave01(n.phase) * r : n.wave01(n.phase + i * 4) * r;
221
+ }
222
+ /**
223
+ * Approximates the five drums. The bass drum is a normal FM channel (at double level); the
224
+ * hi-hat, snare and cymbal mix operator tone with LFSR noise, and the tom-tom is a plain tone.
225
+ * The mixing weights are this port's approximation, not the chip's exact phase logic.
226
+ */
227
+ rhythmSample(s, t, e, a) {
228
+ const [h, n, o] = [this.channels[6], this.channels[7], this.channels[8]];
229
+ let i = this.channelSample(h, s, t, e) * 2;
230
+ const [r, c] = n.ops, [f, p] = o.ops;
231
+ for (const [d, k] of [
232
+ [n, r],
233
+ [n, c],
234
+ [o, f],
235
+ [o, p]
236
+ ])
237
+ this.advance(d, k, s, e);
238
+ return r.stage !== 0 && (i += a * 0.5 * this.opLevel(n, r, t) * (r.wave01(r.phase) > 0 ? 1 : 0.6) * 2), c.stage !== 0 && (i += (c.wave01(c.phase) * 0.6 + a * 0.4) * this.opLevel(n, c, t) * 2), f.stage !== 0 && (i += f.wave01(f.phase) * this.opLevel(o, f, t) * 2), p.stage !== 0 && (i += (p.wave01(p.phase) > 0 ? 0.5 : -0.5) * (a > 0 ? 1 : 0.7) * this.opLevel(o, p, t) * 2), i;
239
+ }
240
+ }
241
+ const O = [0, 1, 2, 8, 9, 10, 16, 17, 18];
242
+ export {
243
+ O as MODULATOR_OFFSET,
244
+ M as Opl2
245
+ };
@@ -0,0 +1,40 @@
1
+ const a = "gm-volume", u = /* @__PURE__ */ new Map();
2
+ function s() {
3
+ try {
4
+ const t = Number(localStorage.getItem(a));
5
+ return localStorage.getItem(a) !== null && t >= 0 && t <= 1 ? t : 1;
6
+ } catch {
7
+ return 1;
8
+ }
9
+ }
10
+ let o = s();
11
+ const i = () => o;
12
+ function g(t) {
13
+ o = Math.max(0, Math.min(1, t));
14
+ for (const [e, n] of u) n.gain.setTargetAtTime(o, e.currentTime, 0.02);
15
+ try {
16
+ localStorage.setItem(a, String(o));
17
+ } catch {
18
+ }
19
+ }
20
+ const c = /* @__PURE__ */ new Map();
21
+ function m(t) {
22
+ let e = c.get(t);
23
+ if (!e) {
24
+ const n = t.createGain();
25
+ n.gain.value = o, n.connect(t.destination), u.set(t, n), e = t.createGain(), e.connect(n), c.set(t, e);
26
+ for (const l of r) l(t, e);
27
+ }
28
+ return e;
29
+ }
30
+ const r = /* @__PURE__ */ new Set();
31
+ function f(t) {
32
+ for (const [e, n] of c) t(e, n);
33
+ return r.add(t), () => r.delete(t);
34
+ }
35
+ export {
36
+ i as getVolume,
37
+ m as output,
38
+ g as setVolume,
39
+ f as tapOutputs
40
+ };
@@ -0,0 +1,89 @@
1
+ class h {
2
+ files = /* @__PURE__ */ new Map();
3
+ /** Notified after any change. */
4
+ onChange;
5
+ /** Set while files are still arriving: the game waits for one it needs (engine/machine.ts, need). */
6
+ stream;
7
+ /** Asked before set or delete changes a file; throws to refuse (a game opened to look inside, editor/core/readOnly.ts). */
8
+ guard;
9
+ constructor(t) {
10
+ if (t) for (const [s, e] of t) this.files.set(i(s), e);
11
+ }
12
+ get(t) {
13
+ return t ? this.files.get(i(t)) ?? this.files.get(o(t)) : void 0;
14
+ }
15
+ has(t) {
16
+ return !!t && (this.files.has(i(t)) || this.files.has(o(t)));
17
+ }
18
+ /** The stored name a reference opens (see cdName), or undefined when there is none. */
19
+ resolve(t) {
20
+ if (!t) return;
21
+ const s = i(t);
22
+ if (this.files.has(s)) return s;
23
+ const e = o(t);
24
+ return this.files.has(e) ? e : void 0;
25
+ }
26
+ set(t, s) {
27
+ const e = i(t);
28
+ this.guard?.(e), this.files.set(e, s), this.onChange?.(e);
29
+ }
30
+ /**
31
+ * A copy of these files to change freely. While files are still arriving here, the copy
32
+ * gets them too as it needs them (its own changes stay).
33
+ */
34
+ copy() {
35
+ const t = new h(this.entries());
36
+ if (!this.stream) return t;
37
+ const s = (e) => {
38
+ for (const a of e) {
39
+ const r = this.resolve(a);
40
+ r && !t.has(r) && t.put(r, this.get(r));
41
+ }
42
+ };
43
+ return t.stream = {
44
+ need: (e) => (s(e), this.stream?.need(e)?.then(() => s(e))),
45
+ want: (e) => this.stream?.want(e)
46
+ }, t;
47
+ }
48
+ /** Adds a file that arrived (not one the game wrote): onChange is not told, and a file already here stays. */
49
+ put(t, s) {
50
+ const e = i(t);
51
+ this.files.has(e) || this.files.set(e, s);
52
+ }
53
+ delete(t) {
54
+ const s = i(t);
55
+ this.guard?.(s), this.files.delete(s), this.onChange?.(s);
56
+ }
57
+ names() {
58
+ return [...this.files.keys()].sort();
59
+ }
60
+ entries() {
61
+ return this.names().map((t) => [t, this.files.get(t)]);
62
+ }
63
+ byExtension(t) {
64
+ const s = t.startsWith(".") ? t : `.${t}`;
65
+ return this.names().filter((e) => e.endsWith(s));
66
+ }
67
+ get size() {
68
+ return this.files.size;
69
+ }
70
+ /** The .gam file to play: the one named like the folder, else the first. */
71
+ mainGame(t) {
72
+ const s = this.byExtension("gam");
73
+ return t && s.includes(i(t)) ? i(t) : s[0];
74
+ }
75
+ }
76
+ function i(n) {
77
+ const t = (n.replace(/\\/g, "/").split("/").pop() ?? n).trim().toLowerCase(), s = t.indexOf("."), e = s < 0 ? "" : t.slice(s + 1, s + 4).trim();
78
+ return e ? `${t.slice(0, Math.min(s, 8))}.${e}` : t.slice(0, s < 0 ? 8 : Math.min(s, 8));
79
+ }
80
+ function o(n) {
81
+ return i(n).replace(/[^a-z0-9_.]/g, "_");
82
+ }
83
+ const c = /\.(exe|com|bat|drv|ico|bak|obj|map\.bak)$/i;
84
+ export {
85
+ h as GameFiles,
86
+ c as IGNORED_FILE,
87
+ o as cdName,
88
+ i as normalize
89
+ };