Newer
Older
rpg / apps / game / src / systems / StepVariants.ts
import type { AudioManager } from '@rpg/engine';
import { bandNoise, decay, normalize, synth } from '@rpg/engine/tools/synth.mjs';

/**
 * Вариативные шаги: 3 процедурных варианта на поверхность, синтезируются
 * в рантайме из примитивов wav.mjs (те же полосы/огибающие, что в
 * tools/audio/gen.mjs, сиды смещены — звучат похоже, но не идентично WAV).
 * Выбор варианта — по кругу. До unlock буферы не создать (createBuffer →
 * null) — играем файловый шаг, варианты синтезируются заново при следующих
 * шагах, пока не получится (обычно вторая попытка, после первого play).
 */

export type StepSurface = 'grass' | 'ash' | 'water' | 'floor';

/** Параметры поверхности — как в gen.mjs (шум + низкий тон). */
const SURFACES: Record<StepSurface, { seed: number; low: number; high: number; dur: number; power: number; noise: number; hum: number; humGain: number }> = {
    grass: { seed: 253, low: 400, high: 2000, dur: 0.12, power: 2, noise: 1, hum: 0, humGain: 0 },
    ash: { seed: 254, low: 200, high: 1100, dur: 0.14, power: 2.5, noise: 1, hum: 0, humGain: 0 },
    water: { seed: 255, low: 600, high: 3200, dur: 0.18, power: 2, noise: 0.6, hum: 300, humGain: 0.2 },
    floor: { seed: 256, low: 150, high: 800, dur: 0.1, power: 3, noise: 0.4, hum: 140, humGain: 0.5 }
};

const VARIANTS = 3;

export class StepVariants {
    /** Буферы по поверхности (синтезируются при первом успешном шаге). */
    private cache = new Map<StepSurface, AudioBuffer[]>();
    private seq = 0;

    constructor(private audio: AudioManager) {}

    /** Играть очередной вариант шага (до готовности буферов — файловый шаг). */
    play(surface: StepSurface, volume: number): void {
        const key = `sfx/step_${surface}`;
        const list = this.ensure(surface);
        if (list.length > 0) {
            const i = this.seq++ % list.length;
            void this.audio.playBuffer(`${key}#${i}`, list[i]!, { volume });
        } else {
            void this.audio.play(key, { volume });
        }
    }

    /** Синтезировать варианты поверхности (если контекст ещё не готов — пустой список). */
    private ensure(surface: StepSurface): AudioBuffer[] {
        const existing = this.cache.get(surface);
        if (existing) return existing;
        const s = SURFACES[surface];
        const list: AudioBuffer[] = [];
        for (let i = 0; i < VARIANTS; i++) {
            const noise = bandNoise(s.seed + i * 10, s.low, s.high, s.dur);
            const samples = normalize(
                synth(
                    s.dur,
                    (t) => (s.noise * noise() + s.humGain * Math.sin(2 * Math.PI * s.hum * t)) * decay(t, s.dur, s.power)
                ),
                0.3
            );
            const buf = this.audio.createBuffer(samples, 22050);
            if (!buf) return []; // контекста нет — попробуем на следующем шаге
            list.push(buf);
        }
        this.cache.set(surface, list);
        return list;
    }
}