用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/mbianchidev/2dnd --skill procedural-audio命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
| name | procedural-audio |
| description | Create and modify procedural audio (music, SFX, footsteps) using Web Audio API in 2D&D |
| license | MIT |
All audio in 2D&D is synthesized at runtime via the Web Audio API — no external audio files.
The main engine lives in src/systems/audio.ts; focused trap synthesis lives in
src/systems/trapAudio.ts.
import { audioEngine } from "../systems/audio";
// Must be called from a user gesture (click/keydown) — browsers block autoplay
audioEngine.init();
AudioContext.destination
└── masterGain (volume * muted)
├── musicGain (music tracks)
├── sfxGain (attack, chest, dungeon, potion, trap SFX)
├── dialogGain (NPC dialogue blips)
└── footstepGain (terrain footsteps, very low volume)
All volume settings are persisted with accessibility settings in the versioned
2dnd_preferences document. audioEngine subscribes to that shared store so
title and in-game controls update the live gain graph immediately. The legacy
2dnd_audio_prefs key migrates automatically and preferences remain separate
from 2dnd_save.
audioEngine.setMasterVolume(0.8); // Affects all channels
audioEngine.setMusicVolume(0.6); // Music only
audioEngine.setSFXVolume(0.4); // SFX + footsteps
audioEngine.setDialogVolume(0.5); // Dialog blips
audioEngine.toggleMute(); // All persisted to localStorage
Six scales define the musical character of each location:
| Scale | Mood | Used For |
|---|---|---|
MAJOR_PENTA | Happy, bright | Grasslands, villages, highlands, campaign ending |
MINOR_PENTA | Melancholic | Frozen, ancient, mystical areas |
HARMONIC_MINOR | Exotic, desert | Arid, canyon, title screen |
DIMINISHED | Eerie, unsettling | Swamp, murky areas |
NATURAL_MINOR | Dark, moody | Scorched, volcanic, industrial |
PHRYGIAN_DOM | Tense, intense | Boss fights, battle theme |
Every music track is driven by a BiomeProfile:
interface BiomeProfile {
baseNote: number; // Semitone offset from A4 (440Hz)
scale: Scale; // Array of semitone intervals
bpm: number; // Beats per minute
wave: OscillatorType; // Lead oscillator: "sine" | "square" | "sawtooth" | "triangle"
padWave: OscillatorType; // Bass/pad oscillator type
}
Add to BIOME_PROFILES record. The key must match the first word of chunk names:
export const BIOME_PROFILES: Record<string, BiomeProfile> = {
// ...existing entries...
Mystic: { baseNote: 3, scale: HARMONIC_MINOR, bpm: 74, wave: "triangle", padWave: "sine" },
};
Add to BOSS_OVERRIDES with the boss monster's ID as key:
const BOSS_OVERRIDES: Record<string, Partial<BiomeProfile>> = {
// ...existing entries...
ancientLich: { baseNote: -12, bpm: 130, scale: DIMINISHED, wave: "square", padWave: "sawtooth" },
};
Add to CITY_OVERRIDES with the city name as key:
const CITY_OVERRIDES: Record<string, Partial<BiomeProfile>> = {
// ...existing entries...
Starhaven: { baseNote: 7, bpm: 110, scale: MAJOR_PENTA, wave: "sine", padWave: "triangle" },
};
Every track automatically layers these instruments via playNote():
wave type at the melody frequencypadWave at half frequency, every other beatMajor scales automatically shift to their relative minor at night. Already-minor scales drop the root by 2–3 semitones for a darker feel.
audioEngine.playEndingMusic() selects the warm, resolved ENDING_PROFILE.
Keep ending as its own TrackKind, include it in playAllSounds(), and call
playTitleMusic() before Ending hands off to Boot so the ending loop does not
continue on the title screen.
audioEngine.playDefeatMusic() selects the slow natural-minor
DEFEAT_PROFILE. Keep defeat as its own TrackKind, include it in
playAllSounds(), and start it from DefeatScene rather than Battle so the
result sequence owns its music lifecycle. Stop the weather overlay first with
playWeatherSFX(WeatherType.Clear) without mutating the persisted weather state.
audioEngine.playCutsceneCue() accepts the typed cues exported by the cutscene
data hub. Keep cue selection in immutable cutscene definitions and synthesis in
audio.ts. Route cues through the existing SFX channel, keep them short, and
disconnect each oscillator and gain node after ended so long cutscene chains
do not retain Web Audio graph nodes.
SFX methods follow this pattern using the sfxGain node:
playNewSFX(): void {
if (!this.ctx || !this.sfxGain) return;
const ctx = this.ctx;
const dest = this.sfxGain;
// Create oscillators/noise, connect through gain nodes to dest
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = "sine";
osc.frequency.value = 440;
gain.gain.setValueAtTime(0.15, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
osc.connect(gain);
gain.connect(dest);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.25);
}
| Method | Sound Design | Trigger |
|---|---|---|
playAttackSFX() | Swoosh + impact thump + metallic clang | Normal melee/spell hit |
playMissSFX() | Airy highpass whoosh + descending pitch | Missed attack |
playCriticalHitSFX() | Deep slam + noise crunch + rising sting + bell | Critical hit (nat 20) |
playChestOpenSFX() | 4-note ascending twinkle + shimmer | Opening a chest |
playDungeonEnterSFX() | Deep boom + eerie tone + stone scrape | Entering a dungeon |
playTrapSFX(type) | Per-type oscillator/noise profile | Triggering a dungeon trap |
playPotionSFX() | 3 glug bubbles + healing shimmer | Using a consumable |
playSpellSFX() | Rising magical charge + bright release | Spell presentation |
playAbilitySFX() | Compact class-action pulse | Ability presentation |
playDefendSFX() | Two-tone shield brace | Defend presentation |
playFleeSFX() | Rising filtered retreat sweep | Flee attempt |
playFaintSFX() | Descending collapse tone | Once-only faint state |
playFootstepSFX(terrain) | Filtered noise burst, varies by terrain | Every player step |
playDialogueBlip(pitch) | Quick square wave blip | NPC dialogue (future) |
playCutsceneCue(cue) | Typed procedural sting | Campaign cutscene step |
playGatheringStartSFX(discipline) | Discipline-specific start cue | Gathering begins |
playGatheringActionSFX(discipline, action) | Short input feedback | Gathering input |
The playFootstepSFX(terrainType) method uses the Terrain enum value to pick filter parameters:
Weather SFX use looping noise buffers routed through sfxGain:
Audio tests verify the API surface and state (no actual audio output in test env):
expect(typeof audioEngine.playAttackSFX).toBe("function");
expect(() => audioEngine.playAttackSFX()).not.toThrow(); // no-op without AudioContext
audioEngine.init() outside a user gesture — browsers will block itplayAllSounds() demo and test filetrapAudio.ts; keep audio.ts as the public routing
surface.playGatheringResultSFX(success, rarity) |
| Success/failure rarity phrase |
| Gathering resolves |