@jjlmoya/utils-games-development 1.54.0 → 1.56.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 (65) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +7 -1
  3. package/src/index.ts +2 -0
  4. package/src/tests/contrast_legibility.test.ts +291 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/itchioGameTester/itchio-game-tester.css +3 -3
  8. package/src/tool/localizationSanitizer/localization-sanitizer.css +17 -15
  9. package/src/tool/pixelArtPaletteSwapper/app-client.ts +229 -0
  10. package/src/tool/pixelArtPaletteSwapper/bibliography.astro +6 -0
  11. package/src/tool/pixelArtPaletteSwapper/bibliography.ts +16 -0
  12. package/src/tool/pixelArtPaletteSwapper/component.astro +153 -0
  13. package/src/tool/pixelArtPaletteSwapper/entry.ts +28 -0
  14. package/src/tool/pixelArtPaletteSwapper/i18n/de.ts +233 -0
  15. package/src/tool/pixelArtPaletteSwapper/i18n/en.ts +233 -0
  16. package/src/tool/pixelArtPaletteSwapper/i18n/es.ts +233 -0
  17. package/src/tool/pixelArtPaletteSwapper/i18n/fr.ts +233 -0
  18. package/src/tool/pixelArtPaletteSwapper/i18n/id.ts +233 -0
  19. package/src/tool/pixelArtPaletteSwapper/i18n/it.ts +233 -0
  20. package/src/tool/pixelArtPaletteSwapper/i18n/ja.ts +225 -0
  21. package/src/tool/pixelArtPaletteSwapper/i18n/ko.ts +225 -0
  22. package/src/tool/pixelArtPaletteSwapper/i18n/nl.ts +233 -0
  23. package/src/tool/pixelArtPaletteSwapper/i18n/pl.ts +233 -0
  24. package/src/tool/pixelArtPaletteSwapper/i18n/pt.ts +233 -0
  25. package/src/tool/pixelArtPaletteSwapper/i18n/ru.ts +233 -0
  26. package/src/tool/pixelArtPaletteSwapper/i18n/sv.ts +233 -0
  27. package/src/tool/pixelArtPaletteSwapper/i18n/tr.ts +233 -0
  28. package/src/tool/pixelArtPaletteSwapper/i18n/zh.ts +225 -0
  29. package/src/tool/pixelArtPaletteSwapper/index.ts +12 -0
  30. package/src/tool/pixelArtPaletteSwapper/logic.test.ts +43 -0
  31. package/src/tool/pixelArtPaletteSwapper/logic.ts +171 -0
  32. package/src/tool/pixelArtPaletteSwapper/pixel-art-palette-swapper.css +728 -0
  33. package/src/tool/pixelArtPaletteSwapper/seo.astro +15 -0
  34. package/src/tool/pixelArtPaletteSwapper/ui.ts +35 -0
  35. package/src/tool/retroSfxGenerator/bibliography.astro +6 -0
  36. package/src/tool/retroSfxGenerator/bibliography.ts +16 -0
  37. package/src/tool/retroSfxGenerator/client.ts +263 -0
  38. package/src/tool/retroSfxGenerator/component.astro +146 -0
  39. package/src/tool/retroSfxGenerator/entry.ts +27 -0
  40. package/src/tool/retroSfxGenerator/i18n/de.ts +211 -0
  41. package/src/tool/retroSfxGenerator/i18n/en.ts +224 -0
  42. package/src/tool/retroSfxGenerator/i18n/es.ts +211 -0
  43. package/src/tool/retroSfxGenerator/i18n/fr.ts +211 -0
  44. package/src/tool/retroSfxGenerator/i18n/id.ts +211 -0
  45. package/src/tool/retroSfxGenerator/i18n/it.ts +211 -0
  46. package/src/tool/retroSfxGenerator/i18n/ja.ts +211 -0
  47. package/src/tool/retroSfxGenerator/i18n/ko.ts +211 -0
  48. package/src/tool/retroSfxGenerator/i18n/nl.ts +211 -0
  49. package/src/tool/retroSfxGenerator/i18n/pl.ts +211 -0
  50. package/src/tool/retroSfxGenerator/i18n/pt.ts +211 -0
  51. package/src/tool/retroSfxGenerator/i18n/ru.ts +211 -0
  52. package/src/tool/retroSfxGenerator/i18n/sv.ts +211 -0
  53. package/src/tool/retroSfxGenerator/i18n/tr.ts +211 -0
  54. package/src/tool/retroSfxGenerator/i18n/zh.ts +211 -0
  55. package/src/tool/retroSfxGenerator/index.ts +11 -0
  56. package/src/tool/retroSfxGenerator/logic.test.ts +61 -0
  57. package/src/tool/retroSfxGenerator/logic.ts +214 -0
  58. package/src/tool/retroSfxGenerator/retro-sfx-generator.css +435 -0
  59. package/src/tool/retroSfxGenerator/seo.astro +15 -0
  60. package/src/tool/retroSfxGenerator/ui.ts +41 -0
  61. package/src/tool/saveFileEditor/game-save-file-editor.css +5 -5
  62. package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +5 -5
  63. package/src/tool/steamBbcodeTranslator/steam-bbcode-translator.css +8 -8
  64. package/src/tool/steamCapsuleGenerator/steam-capsule-generator.css +6 -6
  65. package/src/tools.ts +4 -0
@@ -0,0 +1,35 @@
1
+ export interface PixelArtPaletteSwapperUI extends Record<string, string> {
2
+ uploadTitle: string;
3
+ uploadHint: string;
4
+ chooseImage: string;
5
+ replaceImage: string;
6
+ paletteTitle: string;
7
+ gameBoyPalette: string;
8
+ nesPalette: string;
9
+ pico8Palette: string;
10
+ commodore64Palette: string;
11
+ dawnBringerPalette: string;
12
+ customPalette: string;
13
+ customPaletteHint: string;
14
+ applyCustomPalette: string;
15
+ resetCustomPalette: string;
16
+ sourcePreview: string;
17
+ resultPreview: string;
18
+ waitingForImage: string;
19
+ uploadToPreview: string;
20
+ resultEmpty: string;
21
+ downloadPng: string;
22
+ downloadDisabled: string;
23
+ colorCount: string;
24
+ mappedCount: string;
25
+ imageSize: string;
26
+ paletteCount: string;
27
+ preserveAlpha: string;
28
+ zoomLabel: string;
29
+ processing: string;
30
+ invalidPalette: string;
31
+ invalidImage: string;
32
+ readyStatus: string;
33
+ dropActive: string;
34
+ mappedSummary: string;
35
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { bibliographyEntries } from './bibliography';
4
+ ---
5
+
6
+ <SharedBibliography links={bibliographyEntries} />
@@ -0,0 +1,16 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliographyEntries: BibliographyEntry[] = [
4
+ {
5
+ name: 'W3C Web Audio API Recommendation',
6
+ url: 'https://www.w3.org/TR/webaudio/',
7
+ },
8
+ {
9
+ name: 'MDN OscillatorNode Web API Reference',
10
+ url: 'https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode',
11
+ },
12
+ {
13
+ name: 'Microsoft RIFF WAVE Audio File Format',
14
+ url: 'https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html',
15
+ },
16
+ ];
@@ -0,0 +1,263 @@
1
+ import {
2
+ calculateEndFrequency,
3
+ createDefaultParams,
4
+ encodeWav,
5
+ formatDuration,
6
+ formatFrequency,
7
+ generateSfx,
8
+ getPresetParams,
9
+ type SfxParams,
10
+ type SfxPreset,
11
+ type SfxWaveform,
12
+ } from './logic';
13
+
14
+ const root = document.querySelector<HTMLElement>('#rsfx-root');
15
+
16
+ if (root) {
17
+ const canvas = root.querySelector<HTMLCanvasElement>('#rsfx-waveform');
18
+ const status = root.querySelector<HTMLElement>('#rsfx-status');
19
+ const readout = root.querySelector<HTMLElement>('#rsfx-readout');
20
+ const led = root.querySelector<HTMLElement>('#rsfx-led');
21
+ const frequencyReadout = root.querySelector<HTMLElement>('#rsfx-frequency-readout');
22
+ const durationReadout = root.querySelector<HTMLElement>('#rsfx-duration-readout');
23
+ const waveformSelect = root.querySelector<HTMLSelectElement>('#rsfx-waveform-select');
24
+ const playButton = root.querySelector<HTMLButtonElement>('#rsfx-play');
25
+ const stopButton = root.querySelector<HTMLButtonElement>('#rsfx-stop');
26
+ const downloadButton = root.querySelector<HTMLButtonElement>('#rsfx-download');
27
+ const randomizeButton = root.querySelector<HTMLButtonElement>('#rsfx-randomize');
28
+ const resetButton = root.querySelector<HTMLButtonElement>('#rsfx-reset');
29
+ const presetButtons = Array.from(root.querySelectorAll<HTMLButtonElement>('[data-preset]'));
30
+ const statusReady = root.dataset.statusReady ?? 'Ready';
31
+ const statusPlaying = root.dataset.statusPlaying ?? 'Playing';
32
+ const statusStopped = root.dataset.statusStopped ?? 'Stopped';
33
+ const statusDownloaded = root.dataset.statusDownloaded ?? 'Downloaded';
34
+ const statusAudioBlocked = root.dataset.statusAudioBlocked ?? 'Audio playback is unavailable';
35
+ const statusGenerating = root.dataset.statusGenerating ?? 'Generating';
36
+ const generatedLabel = root.dataset.generatedLabel ?? 'Generated';
37
+ const frequencyLabel = root.dataset.frequencyLabel ?? 'Frequency';
38
+ const frequencyEndLabel = root.dataset.frequencyEndLabel ?? 'End frequency';
39
+ const durationLabel = root.dataset.durationLabel ?? 'Duration';
40
+ const activePresetButton = presetButtons.find((button) => button.classList.contains('is-active'));
41
+ let currentPreset = (activePresetButton?.dataset.preset as SfxPreset | undefined) ?? 'explosion';
42
+ let params = createDefaultParams();
43
+ let generated = generateSfx(params);
44
+ let audioContext: AudioContext | undefined;
45
+ let source: AudioBufferSourceNode | undefined;
46
+
47
+ const input = (id: string): HTMLInputElement | null => root.querySelector<HTMLInputElement>(id);
48
+ const output = (id: string): HTMLOutputElement | null => root.querySelector<HTMLOutputElement>(id);
49
+
50
+ function drawCenterLine(ctx: CanvasRenderingContext2D, width: number, height: number, color: string): void {
51
+ ctx.shadowBlur = 0;
52
+ ctx.strokeStyle = color;
53
+ ctx.globalAlpha = 0.45;
54
+ ctx.beginPath();
55
+ ctx.moveTo(0, height / 2);
56
+ ctx.lineTo(width, height / 2);
57
+ ctx.stroke();
58
+ ctx.globalAlpha = 1;
59
+ }
60
+
61
+ function drawSamplesPath(ctx: CanvasRenderingContext2D, samples: Float32Array, width: number, height: number): void {
62
+ ctx.beginPath();
63
+ const step = Math.max(1, Math.floor(samples.length / width));
64
+ for (let x = 0; x < width; x += 1) {
65
+ const index = Math.min(samples.length - 1, x * step);
66
+ const sample = samples[index] ?? 0;
67
+ const y = height / 2 - sample * height * 0.38;
68
+ if (x === 0) {
69
+ ctx.moveTo(x, y);
70
+ } else {
71
+ ctx.lineTo(x, y);
72
+ }
73
+ }
74
+ ctx.stroke();
75
+ }
76
+
77
+ function drawWaveform(): void {
78
+ if (!canvas || !root) return;
79
+ const rect = canvas.getBoundingClientRect();
80
+ const ratio = window.devicePixelRatio || 1;
81
+ const width = Math.max(320, Math.round(rect.width * ratio));
82
+ const height = Math.max(160, Math.round(rect.height * ratio));
83
+ canvas.width = width;
84
+ canvas.height = height;
85
+ const context = canvas.getContext('2d');
86
+ if (!context) return;
87
+ const styles = getComputedStyle(root);
88
+ const waveformColor = styles.getPropertyValue('--rsfx-waveform').trim() || '#72f2c8';
89
+ const waveformGlow = styles.getPropertyValue('--rsfx-waveform-glow').trim() || 'rgba(114, 242, 200, 0.75)';
90
+ context.clearRect(0, 0, width, height);
91
+ context.lineWidth = Math.max(1, ratio * 1.5);
92
+ context.strokeStyle = waveformColor;
93
+ context.shadowColor = waveformGlow;
94
+ context.shadowBlur = 8 * ratio;
95
+ drawSamplesPath(context, generated.samples, width, height);
96
+ drawCenterLine(context, width, height, waveformGlow);
97
+ }
98
+
99
+ function setStatus(value: string): void {
100
+ if (status) status.textContent = value;
101
+ }
102
+
103
+ function setOutputValue(id: string, value: string): void {
104
+ const target = output(id);
105
+ if (target) target.value = value;
106
+ }
107
+
108
+ function updateOutputReadouts(): void {
109
+ setOutputValue('#rsfx-frequency-value', formatFrequency(params.frequency));
110
+ setOutputValue('#rsfx-sweep-value', `${Math.round(params.sweep * 100)}%`);
111
+ setOutputValue('#rsfx-duration-value', formatDuration(params.duration));
112
+ setOutputValue('#rsfx-decay-value', `${Math.round(params.decay * 100)}%`);
113
+ setOutputValue('#rsfx-vibrato-value', `${Math.round(params.vibrato * 100)}%`);
114
+ setOutputValue('#rsfx-lowpass-value', formatFrequency(params.lowpass));
115
+ setOutputValue('#rsfx-highpass-value', formatFrequency(params.highpass));
116
+ setOutputValue('#rsfx-noise-mix-value', `${Math.round(params.noiseMix * 100)}%`);
117
+ }
118
+
119
+ function renderReadouts(): void {
120
+ updateOutputReadouts();
121
+ if (readout) readout.textContent = `${generatedLabel} · ${params.waveform}`;
122
+ if (frequencyReadout) frequencyReadout.textContent = `${frequencyLabel} ${formatFrequency(params.frequency)} → ${frequencyEndLabel} ${formatFrequency(params.frequencyEnd)}`;
123
+ if (durationReadout) durationReadout.textContent = `${durationLabel} ${formatDuration(params.duration)}`;
124
+ }
125
+
126
+ function syncInputs(): void {
127
+ const values: Record<string, string> = {
128
+ '#rsfx-frequency': String(Math.round(params.frequency)),
129
+ '#rsfx-sweep': String(Math.round(params.sweep * 100)),
130
+ '#rsfx-duration': params.duration.toFixed(2),
131
+ '#rsfx-decay': String(Math.round(params.decay * 100)),
132
+ '#rsfx-vibrato': String(Math.round(params.vibrato * 100)),
133
+ '#rsfx-lowpass': String(Math.round(params.lowpass)),
134
+ '#rsfx-highpass': String(Math.round(params.highpass)),
135
+ '#rsfx-noise-mix': String(Math.round(params.noiseMix * 100)),
136
+ };
137
+ Object.entries(values).forEach(([selector, value]) => {
138
+ const element = input(selector);
139
+ if (element) element.value = value;
140
+ });
141
+ if (waveformSelect) waveformSelect.value = params.waveform;
142
+ renderReadouts();
143
+ }
144
+
145
+ function regenerate(nextParams: SfxParams, nextStatus = statusReady): void {
146
+ setStatus(statusGenerating);
147
+ params = { ...nextParams, frequencyEnd: calculateEndFrequency(nextParams.frequency, nextParams.sweep) };
148
+ generated = generateSfx(params);
149
+ syncInputs();
150
+ drawWaveform();
151
+ setStatus(nextStatus);
152
+ }
153
+
154
+ function parseInputValue(selector: string, fallback: number, divisor = 1): number {
155
+ const raw = input(selector)?.value;
156
+ if (raw === undefined) return fallback;
157
+ const val = Number(raw) / divisor;
158
+ return Number.isFinite(val) ? val : fallback;
159
+ }
160
+
161
+ function readParams(): SfxParams {
162
+ const frequency = parseInputValue('#rsfx-frequency', params.frequency);
163
+ const sweep = parseInputValue('#rsfx-sweep', params.sweep * 100, 100);
164
+ const duration = parseInputValue('#rsfx-duration', params.duration);
165
+ const decay = parseInputValue('#rsfx-decay', params.decay * 100, 100);
166
+ const vibrato = parseInputValue('#rsfx-vibrato', params.vibrato * 100, 100);
167
+ const lowpass = parseInputValue('#rsfx-lowpass', params.lowpass);
168
+ const highpass = parseInputValue('#rsfx-highpass', params.highpass);
169
+ const noiseMix = parseInputValue('#rsfx-noise-mix', params.noiseMix * 100, 100);
170
+ const waveform = (waveformSelect?.value ?? params.waveform) as SfxWaveform;
171
+ return {
172
+ ...params,
173
+ waveform,
174
+ frequency,
175
+ frequencyEnd: calculateEndFrequency(frequency, sweep),
176
+ duration,
177
+ decay,
178
+ sweep,
179
+ vibrato,
180
+ lowpass,
181
+ highpass,
182
+ noiseMix,
183
+ };
184
+ }
185
+
186
+ function stopAudio(): void {
187
+ if (source) {
188
+ source.stop();
189
+ source.disconnect();
190
+ source = undefined;
191
+ }
192
+ if (led) led.classList.remove('is-on');
193
+ setStatus(statusStopped);
194
+ }
195
+
196
+ async function playAudio(): Promise<void> {
197
+ try {
198
+ if (!audioContext) audioContext = new AudioContext();
199
+ await audioContext.resume();
200
+ stopAudio();
201
+ const buffer = audioContext.createBuffer(1, generated.samples.length, generated.sampleRate);
202
+ buffer.getChannelData(0).set(generated.samples);
203
+ source = audioContext.createBufferSource();
204
+ source.buffer = buffer;
205
+ source.connect(audioContext.destination);
206
+ source.onended = () => {
207
+ source = undefined;
208
+ if (led) led.classList.remove('is-on');
209
+ setStatus(statusReady);
210
+ };
211
+ source.start();
212
+ if (led) led.classList.add('is-on');
213
+ setStatus(statusPlaying);
214
+ } catch {
215
+ setStatus(statusAudioBlocked);
216
+ }
217
+ }
218
+
219
+ function downloadWav(): void {
220
+ const wav = encodeWav(generated.samples, generated.sampleRate);
221
+ const url = URL.createObjectURL(new Blob([wav.buffer as ArrayBuffer], { type: 'audio/wav' }));
222
+ const anchor = document.createElement('a');
223
+ anchor.href = url;
224
+ anchor.download = `retro-${params.waveform}-${Math.round(params.frequency)}hz.wav`;
225
+ anchor.click();
226
+ URL.revokeObjectURL(url);
227
+ setStatus(statusDownloaded);
228
+ }
229
+
230
+ function bindControls(): void {
231
+ const ids = ['#rsfx-frequency', '#rsfx-sweep', '#rsfx-duration', '#rsfx-decay', '#rsfx-vibrato', '#rsfx-lowpass', '#rsfx-highpass', '#rsfx-noise-mix'];
232
+ ids.forEach((selector) => {
233
+ input(selector)?.addEventListener('input', () => regenerate(readParams()));
234
+ });
235
+ waveformSelect?.addEventListener('change', () => regenerate(readParams()));
236
+ presetButtons.forEach((button) => {
237
+ button.addEventListener('click', () => {
238
+ const preset = button.dataset.preset as SfxPreset;
239
+ currentPreset = preset;
240
+ presetButtons.forEach((item) => item.classList.toggle('is-active', item === button));
241
+ regenerate(getPresetParams(preset));
242
+ });
243
+ });
244
+ playButton?.addEventListener('click', () => void playAudio());
245
+ stopButton?.addEventListener('click', stopAudio);
246
+ downloadButton?.addEventListener('click', downloadWav);
247
+ randomizeButton?.addEventListener('click', () => {
248
+ regenerate(getPresetParams(currentPreset));
249
+ void playAudio();
250
+ });
251
+ resetButton?.addEventListener('click', () => {
252
+ currentPreset = 'explosion';
253
+ presetButtons.forEach((item) => item.classList.toggle('is-active', item.dataset.preset === 'explosion'));
254
+ regenerate(createDefaultParams());
255
+ });
256
+ window.addEventListener('resize', drawWaveform);
257
+ }
258
+
259
+ syncInputs();
260
+ bindControls();
261
+ drawWaveform();
262
+ void canvas?.getContext('2d');
263
+ }
@@ -0,0 +1,146 @@
1
+ ---
2
+ import type { RetroSfxGeneratorUI } from './ui';
3
+ import './retro-sfx-generator.css';
4
+
5
+ interface Props {
6
+ ui: RetroSfxGeneratorUI;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ ---
11
+
12
+ <div
13
+ class="rsfx-app"
14
+ id="rsfx-root"
15
+ data-status-ready={ui.statusReady}
16
+ data-status-playing={ui.statusPlaying}
17
+ data-status-stopped={ui.statusStopped}
18
+ data-status-downloaded={ui.statusDownloaded}
19
+ data-status-audio-blocked={ui.statusAudioBlocked}
20
+ data-status-generating={ui.statusGenerating}
21
+ data-waveform-preview-label={ui.waveformPreviewLabel}
22
+ data-generated-label={ui.generatedLabel}
23
+ data-frequency-label={ui.frequencyLabel}
24
+ data-frequency-end-label={ui.frequencyEndLabel}
25
+ data-duration-label={ui.durationLabel}
26
+ data-mono-wav-hint={ui.monoWavHint}
27
+ >
28
+ <section class="rsfx-scope-panel" aria-labelledby="rsfx-scope-heading">
29
+ <div class="rsfx-scope-topline">
30
+ <div>
31
+ <span class="rsfx-kicker" id="rsfx-scope-heading">{ui.waveformPreviewLabel}</span>
32
+ <span class="rsfx-scope-readout" id="rsfx-readout">{ui.generatedLabel}</span>
33
+ </div>
34
+ <span class="rsfx-scope-led" id="rsfx-led" aria-hidden="true"></span>
35
+ </div>
36
+ <div class="rsfx-scope-screen">
37
+ <canvas id="rsfx-waveform" class="rsfx-waveform" aria-label={ui.waveformPreviewLabel}></canvas>
38
+ <div class="rsfx-scope-grid" aria-hidden="true"></div>
39
+ </div>
40
+ <div class="rsfx-scope-footer">
41
+ <span id="rsfx-frequency-readout">{ui.frequencyLabel}</span>
42
+ <span id="rsfx-duration-readout">{ui.durationLabel}</span>
43
+ <span>{ui.monoWavHint}</span>
44
+ </div>
45
+ </section>
46
+
47
+ <section class="rsfx-preset-strip" aria-labelledby="rsfx-preset-heading">
48
+ <div class="rsfx-section-label" id="rsfx-preset-heading">{ui.presetLabel}</div>
49
+ <div class="rsfx-preset-grid">
50
+ <button type="button" class="rsfx-preset is-active" data-preset="explosion" aria-label={ui.presetExplosion}>
51
+ <span class="rsfx-preset-glyph">FX</span><span>{ui.presetExplosion}</span>
52
+ </button>
53
+ <button type="button" class="rsfx-preset" data-preset="laser" aria-label={ui.presetLaser}>
54
+ <span class="rsfx-preset-glyph">LZR</span><span>{ui.presetLaser}</span>
55
+ </button>
56
+ <button type="button" class="rsfx-preset" data-preset="jump" aria-label={ui.presetJump}>
57
+ <span class="rsfx-preset-glyph">JMP</span><span>{ui.presetJump}</span>
58
+ </button>
59
+ <button type="button" class="rsfx-preset" data-preset="coin" aria-label={ui.presetCoin}>
60
+ <span class="rsfx-preset-glyph">COIN</span><span>{ui.presetCoin}</span>
61
+ </button>
62
+ <button type="button" class="rsfx-preset" data-preset="powerUp" aria-label={ui.presetPowerUp}>
63
+ <span class="rsfx-preset-glyph">UP</span><span>{ui.presetPowerUp}</span>
64
+ </button>
65
+ </div>
66
+ <p class="rsfx-hint">{ui.presetHint}</p>
67
+ </section>
68
+
69
+ <section class="rsfx-control-board" aria-label={ui.toneSection}>
70
+ <div class="rsfx-control-section">
71
+ <div class="rsfx-section-label">{ui.toneSection}</div>
72
+ <div class="rsfx-waveform-picker">
73
+ <label class="rsfx-control-label" for="rsfx-waveform-select">{ui.waveformLabel}</label>
74
+ <select id="rsfx-waveform-select" class="rsfx-select" aria-label={ui.waveformLabel}>
75
+ <option value="square">{ui.waveformSquare}</option>
76
+ <option value="sawtooth">{ui.waveformSawtooth}</option>
77
+ <option value="sine">{ui.waveformSine}</option>
78
+ <option value="triangle">{ui.waveformTriangle}</option>
79
+ <option value="noise">{ui.waveformNoise}</option>
80
+ </select>
81
+ </div>
82
+ <div class="rsfx-slider-grid">
83
+ <label class="rsfx-slider-control" for="rsfx-frequency">
84
+ <span><span>{ui.frequencyLabel}</span><output id="rsfx-frequency-value">0 Hz</output></span>
85
+ <input id="rsfx-frequency" type="range" min="60" max="8000" step="10" value="1200" aria-label={ui.frequencyLabel} />
86
+ </label>
87
+ <label class="rsfx-slider-control" for="rsfx-sweep">
88
+ <span><span>{ui.sweepLabel}</span><output id="rsfx-sweep-value">0%</output></span>
89
+ <input id="rsfx-sweep" type="range" min="-100" max="100" step="1" value="-70" aria-label={ui.sweepLabel} />
90
+ </label>
91
+ </div>
92
+ </div>
93
+
94
+ <div class="rsfx-control-section">
95
+ <div class="rsfx-section-label">{ui.dynamicsSection}</div>
96
+ <div class="rsfx-slider-grid">
97
+ <label class="rsfx-slider-control" for="rsfx-duration">
98
+ <span><span>{ui.durationLabel}</span><output id="rsfx-duration-value">0.32 s</output></span>
99
+ <input id="rsfx-duration" type="range" min="0.08" max="2.5" step="0.01" value="0.32" aria-label={ui.durationLabel} />
100
+ </label>
101
+ <label class="rsfx-slider-control" for="rsfx-decay">
102
+ <span><span>{ui.decayLabel}</span><output id="rsfx-decay-value">0%</output></span>
103
+ <input id="rsfx-decay" type="range" min="0" max="100" step="1" value="56" aria-label={ui.decayLabel} />
104
+ </label>
105
+ <label class="rsfx-slider-control" for="rsfx-vibrato">
106
+ <span><span>{ui.vibratoLabel}</span><output id="rsfx-vibrato-value">0%</output></span>
107
+ <input id="rsfx-vibrato" type="range" min="0" max="100" step="1" value="3" aria-label={ui.vibratoLabel} />
108
+ </label>
109
+ </div>
110
+ </div>
111
+
112
+ <div class="rsfx-control-section">
113
+ <div class="rsfx-section-label">{ui.filterSection}</div>
114
+ <div class="rsfx-slider-grid">
115
+ <label class="rsfx-slider-control" for="rsfx-lowpass">
116
+ <span><span>{ui.lowpassLabel}</span><output id="rsfx-lowpass-value">0 kHz</output></span>
117
+ <input id="rsfx-lowpass" type="range" min="400" max="12000" step="100" value="9000" aria-label={ui.lowpassLabel} />
118
+ </label>
119
+ <label class="rsfx-slider-control" for="rsfx-highpass">
120
+ <span><span>{ui.highpassLabel}</span><output id="rsfx-highpass-value">0 Hz</output></span>
121
+ <input id="rsfx-highpass" type="range" min="20" max="1800" step="10" value="180" aria-label={ui.highpassLabel} />
122
+ </label>
123
+ <label class="rsfx-slider-control" for="rsfx-noise-mix">
124
+ <span><span>{ui.noiseMixLabel}</span><output id="rsfx-noise-mix-value">0%</output></span>
125
+ <input id="rsfx-noise-mix" type="range" min="0" max="100" step="1" value="4" aria-label={ui.noiseMixLabel} />
126
+ </label>
127
+ </div>
128
+ </div>
129
+ </section>
130
+
131
+ <div class="rsfx-action-bar">
132
+ <div class="rsfx-primary-actions">
133
+ <button type="button" class="rsfx-action rsfx-action-play" id="rsfx-play" aria-label={ui.playButton}>{ui.playButton}</button>
134
+ <button type="button" class="rsfx-action rsfx-action-stop" id="rsfx-stop" aria-label={ui.stopButton}>{ui.stopButton}</button>
135
+ <button type="button" class="rsfx-action rsfx-action-download" id="rsfx-download" aria-label={ui.downloadButton}>{ui.downloadButton}</button>
136
+ </div>
137
+ <div class="rsfx-secondary-actions">
138
+ <button type="button" class="rsfx-text-action" id="rsfx-randomize" aria-label={ui.randomizeButton}>{ui.randomizeButton}</button>
139
+ <button type="button" class="rsfx-text-action" id="rsfx-reset" aria-label={ui.resetButton}>{ui.resetButton}</button>
140
+ </div>
141
+ </div>
142
+
143
+ <p class="rsfx-status" id="rsfx-status" role="status" aria-live="polite">{ui.statusReady}</p>
144
+ </div>
145
+
146
+ <script src="./client.ts"></script>
@@ -0,0 +1,27 @@
1
+ import type { GamesToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { RetroSfxGeneratorUI } from './ui';
3
+
4
+ export type { RetroSfxGeneratorUI };
5
+ export type RetroSfxGeneratorLocaleContent = ToolLocaleContent<RetroSfxGeneratorUI>;
6
+
7
+ export const retroSfxGenerator: GamesToolEntry<RetroSfxGeneratorUI> = {
8
+ id: 'retro-sfx-generator',
9
+ icons: { bg: 'mdi:gamepad-variant', fg: 'mdi:waveform' },
10
+ i18n: {
11
+ de: () => import('./i18n/de').then((m) => m.content),
12
+ en: () => import('./i18n/en').then((m) => m.content),
13
+ es: () => import('./i18n/es').then((m) => m.content),
14
+ fr: () => import('./i18n/fr').then((m) => m.content),
15
+ id: () => import('./i18n/id').then((m) => m.content),
16
+ it: () => import('./i18n/it').then((m) => m.content),
17
+ ja: () => import('./i18n/ja').then((m) => m.content),
18
+ ko: () => import('./i18n/ko').then((m) => m.content),
19
+ nl: () => import('./i18n/nl').then((m) => m.content),
20
+ pl: () => import('./i18n/pl').then((m) => m.content),
21
+ pt: () => import('./i18n/pt').then((m) => m.content),
22
+ ru: () => import('./i18n/ru').then((m) => m.content),
23
+ sv: () => import('./i18n/sv').then((m) => m.content),
24
+ tr: () => import('./i18n/tr').then((m) => m.content),
25
+ zh: () => import('./i18n/zh').then((m) => m.content),
26
+ },
27
+ };