diff --git a/scripts/generate-sfx.mjs b/scripts/generate-sfx.mjs index 9c5bce8..3772a79 100644 --- a/scripts/generate-sfx.mjs +++ b/scripts/generate-sfx.mjs @@ -1,8 +1,8 @@ import { mkdir, writeFile } from 'node:fs/promises'; -import { dirname, join } from 'node:path'; +import { join } from 'node:path'; const sampleRate = 44100; -const outputPath = join('src', 'assets', 'audio', 'sfx', 'level-up.wav'); +const outputDir = join('src', 'assets', 'audio', 'sfx'); function envelope(time, start, duration) { const local = time - start; @@ -10,7 +10,7 @@ function envelope(time, start, duration) { return 0; } const attack = 0.018; - const release = 0.22; + const release = Math.min(0.24, duration * 0.42); if (local < attack) { return local / attack; } @@ -29,10 +29,17 @@ function tone(time, frequency, start, duration, gain, wave = 'sine') { if (wave === 'triangle') { return amp * (2 / Math.PI) * Math.asin(Math.sin(phase)); } + if (wave === 'square') { + return amp * Math.sign(Math.sin(phase)); + } return amp * Math.sin(phase); } -function sampleAt(time) { +function clamp(sample) { + return Math.max(-0.92, Math.min(0.92, sample)); +} + +function levelUpSample(time) { const notes = [ { start: 0, frequency: 523.25, gain: 0.22, duration: 0.52 }, { start: 0.09, frequency: 659.25, gain: 0.2, duration: 0.56 }, @@ -44,7 +51,44 @@ function sampleAt(time) { const shimmer = tone(time, 1318.51, 0.42, 0.36, 0.055, 'triangle') + tone(time, 1567.98, 0.52, 0.28, 0.042, 'triangle'); - return Math.max(-0.92, Math.min(0.92, chord + shimmer)); + return clamp(chord + shimmer); +} + +function bondResonanceSample(time) { + const pulse = + tone(time, 392, 0, 0.34, 0.1, 'triangle') + + tone(time, 587.33, 0.04, 0.42, 0.13, 'triangle') + + tone(time, 783.99, 0.1, 0.42, 0.11, 'sine') + + tone(time, 1174.66, 0.2, 0.38, 0.065, 'sine'); + const sparkle = + tone(time, 1567.98, 0.34, 0.24, 0.045, 'triangle') + + tone(time, 1760, 0.42, 0.2, 0.035, 'triangle'); + const tremolo = 1 + Math.sin(time * Math.PI * 18) * 0.08; + return clamp((pulse + sparkle) * tremolo); +} + +function victoryFanfareSample(time) { + const brass = + tone(time, 392, 0, 0.26, 0.14, 'triangle') + + tone(time, 493.88, 0.16, 0.26, 0.14, 'triangle') + + tone(time, 587.33, 0.32, 0.28, 0.14, 'triangle') + + tone(time, 783.99, 0.5, 0.58, 0.16, 'triangle'); + const harmony = + tone(time, 196, 0, 0.92, 0.07, 'sine') + + tone(time, 293.66, 0.5, 0.64, 0.06, 'sine') + + tone(time, 987.77, 0.72, 0.34, 0.055, 'sine'); + return clamp(brass + harmony); +} + +function defeatStingSample(time) { + const low = + tone(time, 174.61, 0, 0.46, 0.16, 'triangle') + + tone(time, 146.83, 0.24, 0.52, 0.15, 'triangle') + + tone(time, 110, 0.48, 0.56, 0.15, 'sine'); + const strike = + tone(time, 87.31, 0, 0.18, 0.13, 'square') + + tone(time, 261.63, 0.02, 0.16, 0.06, 'triangle'); + return clamp(low + strike); } function wavBuffer(samples) { @@ -71,8 +115,17 @@ function wavBuffer(samples) { return buffer; } -await mkdir(dirname(outputPath), { recursive: true }); -const duration = 1.12; -const samples = Array.from({ length: Math.floor(sampleRate * duration) }, (_, index) => sampleAt(index / sampleRate)); -await writeFile(outputPath, wavBuffer(samples)); -console.log(`Generated ${outputPath}`); +const clips = [ + { name: 'level-up', duration: 1.12, sampleAt: levelUpSample }, + { name: 'bond-resonance', duration: 0.82, sampleAt: bondResonanceSample }, + { name: 'victory-fanfare', duration: 1.2, sampleAt: victoryFanfareSample }, + { name: 'defeat-sting', duration: 1.08, sampleAt: defeatStingSample } +]; + +await mkdir(outputDir, { recursive: true }); +for (const clip of clips) { + const samples = Array.from({ length: Math.floor(sampleRate * clip.duration) }, (_, index) => clip.sampleAt(index / sampleRate)); + const outputPath = join(outputDir, `${clip.name}.wav`); + await writeFile(outputPath, wavBuffer(samples)); + console.log(`Generated ${outputPath}`); +} diff --git a/src/assets/audio/sfx/bond-resonance.wav b/src/assets/audio/sfx/bond-resonance.wav new file mode 100644 index 0000000..c7aeafc Binary files /dev/null and b/src/assets/audio/sfx/bond-resonance.wav differ diff --git a/src/assets/audio/sfx/defeat-sting.wav b/src/assets/audio/sfx/defeat-sting.wav new file mode 100644 index 0000000..fd3292c Binary files /dev/null and b/src/assets/audio/sfx/defeat-sting.wav differ diff --git a/src/assets/audio/sfx/level-up.wav b/src/assets/audio/sfx/level-up.wav index a64fc66..b5dcad7 100644 Binary files a/src/assets/audio/sfx/level-up.wav and b/src/assets/audio/sfx/level-up.wav differ diff --git a/src/assets/audio/sfx/victory-fanfare.wav b/src/assets/audio/sfx/victory-fanfare.wav new file mode 100644 index 0000000..06177b6 Binary files /dev/null and b/src/assets/audio/sfx/victory-fanfare.wav differ diff --git a/src/game/audio/audioAssets.ts b/src/game/audio/audioAssets.ts index aa78094..7d188c3 100644 --- a/src/game/audio/audioAssets.ts +++ b/src/game/audio/audioAssets.ts @@ -3,8 +3,10 @@ import militiaThemeUrl from '../../assets/audio/bgm/militia-theme.mp3'; import oathThemeUrl from '../../assets/audio/bgm/oath-theme.mp3'; import storyDarkUrl from '../../assets/audio/bgm/story-dark.mp3'; import titleThemeUrl from '../../assets/audio/bgm/title-theme.mp3'; +import bondResonanceUrl from '../../assets/audio/sfx/bond-resonance.wav'; import cartRollUrl from '../../assets/audio/sfx/cart-roll.mp3'; import combatImpactUrl from '../../assets/audio/sfx/combat-impact.mp3'; +import defeatStingUrl from '../../assets/audio/sfx/defeat-sting.wav'; import expGainUrl from '../../assets/audio/sfx/exp-gain.mp3'; import footstepWalkUrl from '../../assets/audio/sfx/footstep-walk.mp3'; import horseGallopUrl from '../../assets/audio/sfx/horse-gallop.mp3'; @@ -12,6 +14,7 @@ import levelUpUrl from '../../assets/audio/sfx/level-up.wav'; import strategyCastUrl from '../../assets/audio/sfx/strategy-cast.mp3'; import swordSlashUrl from '../../assets/audio/sfx/sword-slash.mp3'; import uiSelectUrl from '../../assets/audio/sfx/ui-select.mp3'; +import victoryFanfareUrl from '../../assets/audio/sfx/victory-fanfare.wav'; export const musicTracks = { 'title-theme': titleThemeUrl, @@ -29,6 +32,9 @@ export const effectTracks = { 'cart-roll': cartRollUrl, 'exp-gain': expGainUrl, 'level-up': levelUpUrl, + 'bond-resonance': bondResonanceUrl, + 'victory-fanfare': victoryFanfareUrl, + 'defeat-sting': defeatStingUrl, 'footstep-walk': footstepWalkUrl, 'horse-gallop': horseGallopUrl } as const; diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 6a3a699..90708e6 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -4340,8 +4340,8 @@ export class BattleScene extends Phaser.Scene { : `패배... ${battleScenario.defeatConditionLabel} 조건이 발생했습니다.`; this.pushBattleLog(message); this.renderSituationPanel(message); - soundDirector.playEffect(outcome === 'victory' ? 'exp-gain' : 'combat-impact', { - volume: outcome === 'victory' ? 0.38 : 0.28, + soundDirector.playEffect(outcome === 'victory' ? 'victory-fanfare' : 'defeat-sting', { + volume: outcome === 'victory' ? 0.34 : 0.26, stopAfterMs: 1200 }); this.publishFirstBattleReport(outcome); @@ -7335,7 +7335,7 @@ export class BattleScene extends Phaser.Scene { return Promise.resolve(); } - soundDirector.playEffect('strategy-cast', { volume: 0.18, rate: 1.14, stopAfterMs: 520 }); + soundDirector.playEffect('bond-resonance', { volume: 0.22, rate: 1.04, stopAfterMs: 620 }); const objects: Phaser.GameObjects.GameObject[] = []; const line = this.add.graphics(); @@ -7385,7 +7385,7 @@ export class BattleScene extends Phaser.Scene { return Promise.resolve(); } - soundDirector.playEffect('strategy-cast', { volume: 0.22, rate: 1.08, stopAfterMs: 720 }); + soundDirector.playEffect('bond-resonance', { volume: 0.24, rate: 1, stopAfterMs: 760 }); const aura = this.trackCombatObject(this.add.circle(auraX, auraY, 42)); aura.setStrokeStyle(4, 0xffdf7b, 0.88);