@jjlmoya/utils-games-development 1.54.0 → 1.55.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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +4 -1
  3. package/src/index.ts +1 -0
  4. package/src/tests/contrast_legibility.test.ts +245 -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 +13 -13
  9. package/src/tool/retroSfxGenerator/bibliography.astro +6 -0
  10. package/src/tool/retroSfxGenerator/bibliography.ts +16 -0
  11. package/src/tool/retroSfxGenerator/client.ts +256 -0
  12. package/src/tool/retroSfxGenerator/component.astro +146 -0
  13. package/src/tool/retroSfxGenerator/entry.ts +27 -0
  14. package/src/tool/retroSfxGenerator/i18n/de.ts +211 -0
  15. package/src/tool/retroSfxGenerator/i18n/en.ts +224 -0
  16. package/src/tool/retroSfxGenerator/i18n/es.ts +211 -0
  17. package/src/tool/retroSfxGenerator/i18n/fr.ts +211 -0
  18. package/src/tool/retroSfxGenerator/i18n/id.ts +211 -0
  19. package/src/tool/retroSfxGenerator/i18n/it.ts +211 -0
  20. package/src/tool/retroSfxGenerator/i18n/ja.ts +211 -0
  21. package/src/tool/retroSfxGenerator/i18n/ko.ts +211 -0
  22. package/src/tool/retroSfxGenerator/i18n/nl.ts +211 -0
  23. package/src/tool/retroSfxGenerator/i18n/pl.ts +211 -0
  24. package/src/tool/retroSfxGenerator/i18n/pt.ts +211 -0
  25. package/src/tool/retroSfxGenerator/i18n/ru.ts +211 -0
  26. package/src/tool/retroSfxGenerator/i18n/sv.ts +211 -0
  27. package/src/tool/retroSfxGenerator/i18n/tr.ts +211 -0
  28. package/src/tool/retroSfxGenerator/i18n/zh.ts +211 -0
  29. package/src/tool/retroSfxGenerator/index.ts +11 -0
  30. package/src/tool/retroSfxGenerator/logic.test.ts +40 -0
  31. package/src/tool/retroSfxGenerator/logic.ts +214 -0
  32. package/src/tool/retroSfxGenerator/retro-sfx-generator.css +429 -0
  33. package/src/tool/retroSfxGenerator/seo.astro +15 -0
  34. package/src/tool/retroSfxGenerator/ui.ts +41 -0
  35. package/src/tool/saveFileEditor/game-save-file-editor.css +5 -5
  36. package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +5 -5
  37. package/src/tool/steamBbcodeTranslator/steam-bbcode-translator.css +8 -8
  38. package/src/tool/steamCapsuleGenerator/steam-capsule-generator.css +6 -6
  39. package/src/tools.ts +2 -0
@@ -0,0 +1,256 @@
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
+ let params = createDefaultParams();
41
+ let generated = generateSfx(params);
42
+ let audioContext: AudioContext | undefined;
43
+ let source: AudioBufferSourceNode | undefined;
44
+
45
+ const input = (id: string): HTMLInputElement | null => root.querySelector<HTMLInputElement>(id);
46
+ const output = (id: string): HTMLOutputElement | null => root.querySelector<HTMLOutputElement>(id);
47
+
48
+ function drawCenterLine(ctx: CanvasRenderingContext2D, width: number, height: number, color: string): void {
49
+ ctx.shadowBlur = 0;
50
+ ctx.strokeStyle = color;
51
+ ctx.globalAlpha = 0.45;
52
+ ctx.beginPath();
53
+ ctx.moveTo(0, height / 2);
54
+ ctx.lineTo(width, height / 2);
55
+ ctx.stroke();
56
+ ctx.globalAlpha = 1;
57
+ }
58
+
59
+ function drawSamplesPath(ctx: CanvasRenderingContext2D, samples: Float32Array, width: number, height: number): void {
60
+ ctx.beginPath();
61
+ const step = Math.max(1, Math.floor(samples.length / width));
62
+ for (let x = 0; x < width; x += 1) {
63
+ const index = Math.min(samples.length - 1, x * step);
64
+ const sample = samples[index] ?? 0;
65
+ const y = height / 2 - sample * height * 0.38;
66
+ if (x === 0) {
67
+ ctx.moveTo(x, y);
68
+ } else {
69
+ ctx.lineTo(x, y);
70
+ }
71
+ }
72
+ ctx.stroke();
73
+ }
74
+
75
+ function drawWaveform(): void {
76
+ if (!canvas || !root) return;
77
+ const rect = canvas.getBoundingClientRect();
78
+ const ratio = window.devicePixelRatio || 1;
79
+ const width = Math.max(320, Math.round(rect.width * ratio));
80
+ const height = Math.max(160, Math.round(rect.height * ratio));
81
+ canvas.width = width;
82
+ canvas.height = height;
83
+ const context = canvas.getContext('2d');
84
+ if (!context) return;
85
+ const styles = getComputedStyle(root);
86
+ const waveformColor = styles.getPropertyValue('--rsfx-waveform').trim() || '#72f2c8';
87
+ const waveformGlow = styles.getPropertyValue('--rsfx-waveform-glow').trim() || 'rgba(114, 242, 200, 0.75)';
88
+ context.clearRect(0, 0, width, height);
89
+ context.lineWidth = Math.max(1, ratio * 1.5);
90
+ context.strokeStyle = waveformColor;
91
+ context.shadowColor = waveformGlow;
92
+ context.shadowBlur = 8 * ratio;
93
+ drawSamplesPath(context, generated.samples, width, height);
94
+ drawCenterLine(context, width, height, waveformGlow);
95
+ }
96
+
97
+ function setStatus(value: string): void {
98
+ if (status) status.textContent = value;
99
+ }
100
+
101
+ function setOutputValue(id: string, value: string): void {
102
+ const target = output(id);
103
+ if (target) target.value = value;
104
+ }
105
+
106
+ function updateOutputReadouts(): void {
107
+ setOutputValue('#rsfx-frequency-value', formatFrequency(params.frequency));
108
+ setOutputValue('#rsfx-sweep-value', `${Math.round(params.sweep * 100)}%`);
109
+ setOutputValue('#rsfx-duration-value', formatDuration(params.duration));
110
+ setOutputValue('#rsfx-decay-value', `${Math.round(params.decay * 100)}%`);
111
+ setOutputValue('#rsfx-vibrato-value', `${Math.round(params.vibrato * 100)}%`);
112
+ setOutputValue('#rsfx-lowpass-value', formatFrequency(params.lowpass));
113
+ setOutputValue('#rsfx-highpass-value', formatFrequency(params.highpass));
114
+ setOutputValue('#rsfx-noise-mix-value', `${Math.round(params.noiseMix * 100)}%`);
115
+ }
116
+
117
+ function renderReadouts(): void {
118
+ updateOutputReadouts();
119
+ if (readout) readout.textContent = `${generatedLabel} · ${params.waveform}`;
120
+ if (frequencyReadout) frequencyReadout.textContent = `${frequencyLabel} ${formatFrequency(params.frequency)} → ${frequencyEndLabel} ${formatFrequency(params.frequencyEnd)}`;
121
+ if (durationReadout) durationReadout.textContent = `${durationLabel} ${formatDuration(params.duration)}`;
122
+ }
123
+
124
+ function syncInputs(): void {
125
+ const values: Record<string, string> = {
126
+ '#rsfx-frequency': String(Math.round(params.frequency)),
127
+ '#rsfx-sweep': String(Math.round(params.sweep * 100)),
128
+ '#rsfx-duration': params.duration.toFixed(2),
129
+ '#rsfx-decay': String(Math.round(params.decay * 100)),
130
+ '#rsfx-vibrato': String(Math.round(params.vibrato * 100)),
131
+ '#rsfx-lowpass': String(Math.round(params.lowpass)),
132
+ '#rsfx-highpass': String(Math.round(params.highpass)),
133
+ '#rsfx-noise-mix': String(Math.round(params.noiseMix * 100)),
134
+ };
135
+ Object.entries(values).forEach(([selector, value]) => {
136
+ const element = input(selector);
137
+ if (element) element.value = value;
138
+ });
139
+ if (waveformSelect) waveformSelect.value = params.waveform;
140
+ renderReadouts();
141
+ }
142
+
143
+ function regenerate(nextParams: SfxParams, nextStatus = statusReady): void {
144
+ setStatus(statusGenerating);
145
+ params = { ...nextParams, frequencyEnd: calculateEndFrequency(nextParams.frequency, nextParams.sweep) };
146
+ generated = generateSfx(params);
147
+ syncInputs();
148
+ drawWaveform();
149
+ setStatus(nextStatus);
150
+ }
151
+
152
+ function parseInputValue(selector: string, fallback: number, divisor = 1): number {
153
+ const raw = input(selector)?.value;
154
+ if (raw === undefined) return fallback;
155
+ const val = Number(raw) / divisor;
156
+ return Number.isFinite(val) ? val : fallback;
157
+ }
158
+
159
+ function readParams(): SfxParams {
160
+ const frequency = parseInputValue('#rsfx-frequency', params.frequency);
161
+ const sweep = parseInputValue('#rsfx-sweep', params.sweep * 100, 100);
162
+ const duration = parseInputValue('#rsfx-duration', params.duration);
163
+ const decay = parseInputValue('#rsfx-decay', params.decay * 100, 100);
164
+ const vibrato = parseInputValue('#rsfx-vibrato', params.vibrato * 100, 100);
165
+ const lowpass = parseInputValue('#rsfx-lowpass', params.lowpass);
166
+ const highpass = parseInputValue('#rsfx-highpass', params.highpass);
167
+ const noiseMix = parseInputValue('#rsfx-noise-mix', params.noiseMix * 100, 100);
168
+ const waveform = (waveformSelect?.value ?? params.waveform) as SfxWaveform;
169
+ return {
170
+ ...params,
171
+ waveform,
172
+ frequency,
173
+ frequencyEnd: calculateEndFrequency(frequency, sweep),
174
+ duration,
175
+ decay,
176
+ sweep,
177
+ vibrato,
178
+ lowpass,
179
+ highpass,
180
+ noiseMix,
181
+ };
182
+ }
183
+
184
+ function stopAudio(): void {
185
+ if (source) {
186
+ source.stop();
187
+ source.disconnect();
188
+ source = undefined;
189
+ }
190
+ if (led) led.classList.remove('is-on');
191
+ setStatus(statusStopped);
192
+ }
193
+
194
+ async function playAudio(): Promise<void> {
195
+ try {
196
+ if (!audioContext) audioContext = new AudioContext();
197
+ await audioContext.resume();
198
+ stopAudio();
199
+ const buffer = audioContext.createBuffer(1, generated.samples.length, generated.sampleRate);
200
+ buffer.getChannelData(0).set(generated.samples);
201
+ source = audioContext.createBufferSource();
202
+ source.buffer = buffer;
203
+ source.connect(audioContext.destination);
204
+ source.onended = () => {
205
+ source = undefined;
206
+ if (led) led.classList.remove('is-on');
207
+ setStatus(statusReady);
208
+ };
209
+ source.start();
210
+ if (led) led.classList.add('is-on');
211
+ setStatus(statusPlaying);
212
+ } catch {
213
+ setStatus(statusAudioBlocked);
214
+ }
215
+ }
216
+
217
+ function downloadWav(): void {
218
+ const wav = encodeWav(generated.samples, generated.sampleRate);
219
+ const url = URL.createObjectURL(new Blob([wav.buffer as ArrayBuffer], { type: 'audio/wav' }));
220
+ const anchor = document.createElement('a');
221
+ anchor.href = url;
222
+ anchor.download = `retro-${params.waveform}-${Math.round(params.frequency)}hz.wav`;
223
+ anchor.click();
224
+ URL.revokeObjectURL(url);
225
+ setStatus(statusDownloaded);
226
+ }
227
+
228
+ function bindControls(): void {
229
+ const ids = ['#rsfx-frequency', '#rsfx-sweep', '#rsfx-duration', '#rsfx-decay', '#rsfx-vibrato', '#rsfx-lowpass', '#rsfx-highpass', '#rsfx-noise-mix'];
230
+ ids.forEach((selector) => {
231
+ input(selector)?.addEventListener('input', () => regenerate(readParams()));
232
+ });
233
+ waveformSelect?.addEventListener('change', () => regenerate(readParams()));
234
+ presetButtons.forEach((button) => {
235
+ button.addEventListener('click', () => {
236
+ const preset = button.dataset.preset as SfxPreset;
237
+ presetButtons.forEach((item) => item.classList.toggle('is-active', item === button));
238
+ regenerate(getPresetParams(preset));
239
+ });
240
+ });
241
+ playButton?.addEventListener('click', () => void playAudio());
242
+ stopButton?.addEventListener('click', stopAudio);
243
+ downloadButton?.addEventListener('click', downloadWav);
244
+ randomizeButton?.addEventListener('click', () => regenerate(getPresetParams('laser')));
245
+ resetButton?.addEventListener('click', () => {
246
+ presetButtons.forEach((item) => item.classList.toggle('is-active', item.dataset.preset === 'laser'));
247
+ regenerate(createDefaultParams());
248
+ });
249
+ window.addEventListener('resize', drawWaveform);
250
+ }
251
+
252
+ syncInputs();
253
+ bindControls();
254
+ drawWaveform();
255
+ void canvas?.getContext('2d');
256
+ }
@@ -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
+ };
@@ -0,0 +1,211 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import type { RetroSfxGeneratorUI } from '../ui';
3
+ import { bibliographyEntries } from '../bibliography';
4
+
5
+ export const content: ToolLocaleContent<RetroSfxGeneratorUI> = {
6
+ slug: 'retro-soundeffekt-generator',
7
+ title: 'Retro Soundeffekt Generator fuer Spiele',
8
+ description: 'Erstellen Sie kurze Retro-Spielsoundeffekte im Browser mit Vorlagen, Wellenformsteuerung, Oszilloskop und WAV-Export.',
9
+ ui: {
10
+ waveformLabel: 'Wellenform',
11
+ waveformSquare: 'Rechteck',
12
+ waveformSawtooth: 'Saegezahn',
13
+ waveformSine: 'Sinus',
14
+ waveformTriangle: 'Dreieck',
15
+ waveformNoise: 'Rauschen',
16
+ presetLabel: 'Schnell-Soundbank',
17
+ presetExplosion: 'Explosion',
18
+ presetLaser: 'Laser',
19
+ presetJump: 'Sprung',
20
+ presetCoin: 'Muenze',
21
+ presetPowerUp: 'Power-up',
22
+ frequencyLabel: 'Startfrequenz',
23
+ frequencyEndLabel: 'Endfrequenz',
24
+ durationLabel: 'Laenge',
25
+ decayLabel: 'Ablauf',
26
+ sweepLabel: 'Tonhoehen-Sweep',
27
+ vibratoLabel: 'Vibrato',
28
+ lowpassLabel: 'Tiefpass',
29
+ highpassLabel: 'Hochpass',
30
+ noiseMixLabel: 'Rausch-Mix',
31
+ toneSection: 'Ton',
32
+ dynamicsSection: 'Dynamik',
33
+ filterSection: 'Textur',
34
+ playButton: 'Sound abspielen',
35
+ stopButton: 'Stopp',
36
+ downloadButton: 'WAV herunterladen',
37
+ randomizeButton: 'Zufall',
38
+ resetButton: 'Zuruecksetzen',
39
+ waveformPreviewLabel: 'Echtzeit-Wellenform',
40
+ generatedLabel: 'Generiert',
41
+ statusReady: 'Bereit zum Anhoeren',
42
+ statusPlaying: 'Wird im Browser abgespielt',
43
+ statusStopped: 'Wiedergabe gestoppt',
44
+ statusDownloaded: 'WAV heruntergeladen',
45
+ statusAudioBlocked: 'Wiedergabe erfordert ein audiofaehiges Browser-Tab',
46
+ statusGenerating: 'Sound wird gerendert',
47
+ presetHint: 'Waehlen Sie einen Startpunkt und formen Sie das Signal mit den Reglern.',
48
+ monoWavHint: '44.1 kHz · 16-Bit Mono-WAV',
49
+ },
50
+ seo: [
51
+ {
52
+ type: 'title',
53
+ level: 2,
54
+ text: 'Erstellen Sie Game-Audio waehrend eines Jams direkt im Browser',
55
+ },
56
+ {
57
+ type: 'paragraph',
58
+ html: 'Ein guter Spielsoundeffekt muss eine Aktion sofort vermitteln. Ein ansteigender Ton signalisiert einen Sprung oder ein Power-up, waehrend ein abfallender Sweep einen Laser darstellt. Dieser Generator verwandelt Arcade-Muster in bearbeitbare Audiosignale.',
59
+ },
60
+ {
61
+ type: 'stats',
62
+ columns: 4,
63
+ items: [
64
+ { label: 'Sound-Presets', value: '5 Startmuster' },
65
+ { label: 'Oszillatoren', value: '5 Wellenformen' },
66
+ { label: 'Exportformat', value: '16-Bit PCM WAV' },
67
+ { label: 'Verarbeitung', value: 'Im Browser' },
68
+ ],
69
+ },
70
+ {
71
+ type: 'title',
72
+ level: 2,
73
+ text: 'Was die Regler steuern',
74
+ },
75
+ {
76
+ type: 'comparative',
77
+ columns: 2,
78
+ items: [
79
+ {
80
+ title: 'Tonhoehe und Bewegung',
81
+ description: 'Die Tonregler bestimmen die Identitaet des Sounds.',
82
+ points: [
83
+ 'Die Startfrequenz legt die Anfangstonhoehe fest',
84
+ 'Der Sweep bewegt die Frequenz zum Endwert',
85
+ 'Vibrato fuegt periodische Schwankungen hinzu',
86
+ 'Wellenformen erzeugen unterschiedliche Klangfarben',
87
+ ],
88
+ },
89
+ {
90
+ title: 'Form und Textur',
91
+ description: 'Dynamik- und Filterregler bestimmen das Ausklingen.',
92
+ points: [
93
+ 'Die Laenge bestimmt die Gesamtdauer des Effekts',
94
+ 'Decay steuert das Abklingen der Amplitude',
95
+ 'Tiefpassfilter daempfen hohe Frequenzen',
96
+ 'Hochpassfilter entfernen tiefe Frequenzen',
97
+ ],
98
+ },
99
+ ],
100
+ },
101
+ {
102
+ type: 'title',
103
+ level: 2,
104
+ text: 'Praktische Rezepte fuer Spielevent-Sounds',
105
+ },
106
+ {
107
+ type: 'table',
108
+ headers: ['Event', 'Nützliches Preset', 'Erster Anpassungsschritt'],
109
+ rows: [
110
+ ['Explosion', 'Rauschen mit tiefer Frequenz', 'Laenge erhoehen und Tiefpass senken fuer mehr Druck'],
111
+ ['Laser', 'Saegezahn mit abfallendem Sweep', 'Dauer verkuerzen fuer schaerferen Anschlag'],
112
+ ['Sprung', 'Rechteck mit ansteigendem Sweep', 'Decay verringern, um Aufwaertsbegegung klarer zu hoeren'],
113
+ ['Muenze', 'Rechteck mit kurzem Anstieg', 'Startfrequenz erhoehen fuer hellen Aufklaub-Sound'],
114
+ ['Power-up', 'Dreieck mit langem Anstieg', 'Vibrato hinzufuegen fuer dynamischen Klang'],
115
+ ],
116
+ },
117
+ {
118
+ type: 'title',
119
+ level: 2,
120
+ text: 'Warum WAV das ideale Jam-Format ist',
121
+ },
122
+ {
123
+ type: 'paragraph',
124
+ html: 'WAV ist ein universelles Format fuer Prototypen, da es von allen gängigen Game-Engines unterstützt wird. Die Datei wird lokal ohne Serververbindung erzeugt.',
125
+ },
126
+ {
127
+ type: 'tip',
128
+ title: 'Bei gewuenschter Lautstaerke testen',
129
+ html: 'Ein einzeln spektakulaerer Sound kann im Spiel anstrengend wirken. Testen Sie den Sound direkt im Spielkontext.',
130
+ },
131
+ ],
132
+ faqTitle: 'Haeufig gestellte Fragen',
133
+ faq: [
134
+ {
135
+ question: 'Werden meine Sounds hochgeladen?',
136
+ answer: 'Nein. Synthese und WAV-Kodierung erfolgen vollstaendig im Browser.',
137
+ },
138
+ {
139
+ question: 'Kann ich die Sounds kommerziell nutzen?',
140
+ answer: 'Die Audiodateien sind fuer Prototypen und Game Jams gedacht.',
141
+ },
142
+ {
143
+ question: 'Wie funktioniert der Tonhoehen-Sweep?',
144
+ answer: 'Der Regler errechnet proportional die Endfrequenz ausgehend vom Startton.',
145
+ },
146
+ {
147
+ question: 'Was tun bei fehlender Audioausgabe?',
148
+ answer: 'Pruefen Sie die Browsereinstellungen fuer Soundwiedergabe.',
149
+ },
150
+ ],
151
+ howTo: [
152
+ {
153
+ name: 'Preset waehlen',
154
+ text: 'Waehlen Sie Explosion, Laser, Sprung, Muenze oder Power-up.',
155
+ },
156
+ {
157
+ name: 'Signal formen',
158
+ text: 'Passen Sie Wellenform, Tonhoehe, Dauer und Filter an.',
159
+ },
160
+ {
161
+ name: 'Sound testen',
162
+ text: 'Klicken Sie auf Sound abspielen fuer eine Live-Hörprobe.',
163
+ },
164
+ {
165
+ name: 'WAV herunterladen',
166
+ text: 'Klicken Sie auf WAV herunterladen, um die Datei zu speichern.',
167
+ },
168
+ ],
169
+ schemas: [
170
+ {
171
+ '@context': 'https://schema.org',
172
+ '@type': 'SoftwareApplication',
173
+ name: 'Retro Soundeffekt Generator fuer Spiele',
174
+ applicationCategory: 'MultimediaApplication',
175
+ operatingSystem: 'Any',
176
+ offers: {
177
+ '@type': 'Offer',
178
+ price: '0',
179
+ priceCurrency: 'EUR',
180
+ },
181
+ },
182
+ {
183
+ '@context': 'https://schema.org',
184
+ '@type': 'FAQPage',
185
+ mainEntity: [
186
+ {
187
+ '@type': 'Question',
188
+ name: 'Werden meine Sounds hochgeladen?',
189
+ acceptedAnswer: {
190
+ '@type': 'Answer',
191
+ text: 'Nein. Synthese und WAV-Kodierung erfolgen vollstaendig im Browser.',
192
+ },
193
+ },
194
+ ],
195
+ },
196
+ {
197
+ '@context': 'https://schema.org',
198
+ '@type': 'HowTo',
199
+ name: 'Erstellen eines Retro-Spielsoundeffekts',
200
+ step: [
201
+ {
202
+ '@type': 'HowToStep',
203
+ name: 'Preset waehlen',
204
+ text: 'Waehlen Sie ein Sound-Preset.',
205
+ },
206
+ ],
207
+ },
208
+ ],
209
+ bibliographyTitle: 'Referenzen',
210
+ bibliography: bibliographyEntries,
211
+ };