diff --git a/docs/audio-sources.md b/docs/audio-sources.md new file mode 100644 index 0000000..445eb63 --- /dev/null +++ b/docs/audio-sources.md @@ -0,0 +1,26 @@ +# Audio Sources + +Downloaded from Pixabay on 2026-06-22 and used under the Pixabay Content License. + +License reference: https://pixabay.com/service/license-summary/ + +## Background Music + +| Project file | Pixabay title | Creator | Source | +| --- | --- | --- | --- | +| `src/assets/audio/bgm/title-theme.mp3` | Chinese ancient music, World, Ethnic music | ET11LX | https://pixabay.com/music/china-chinese-ancient-style-music-love-et11lx-%E5%8F%A4%E9%A3%8E-%E6%8A%92%E6%83%85-%E8%AF%86-155926/ | +| `src/assets/audio/bgm/story-dark.mp3` | Chinese, Asia, China music | Reklamistrz | https://pixabay.com/music/meditationspiritual-ancient-chinese-dali-448882/ | +| `src/assets/audio/bgm/oath-theme.mp3` | Chinese ancient style, Ancient style, World music | ET11LX | https://pixabay.com/music/world-chinese-ancient-style-music-love-et%E5%8D%81%E4%B8%80lx-%E5%8F%A4%E9%A3%8E-%E6%8A%92%E6%83%85-%E8%AF%86%E4%B8%B0%E5%AF%8C%E9%85%8D%E5%99%A8%E6%8E%A8%E9%AB%98%E6%BD%AE%E7%89%88-247345/ | +| `src/assets/audio/bgm/militia-theme.mp3` | Orchestra, Heroic, Warrior music | NR-Music | https://pixabay.com/music/adventure-short-heroic-orchestral-loop-541095/ | +| `src/assets/audio/bgm/battle-prep.mp3` | Epic, Battle, Knight music | DeusLower | https://pixabay.com/music/mystery-epic-battle-orchestra-music-241006/ | + +## Sound Effects + +| Project file | Pixabay title | Creator | Source | +| --- | --- | --- | --- | +| `src/assets/audio/sfx/ui-select.mp3` | Ui, Click, Retro sound effect | SoundShelfStudio | https://pixabay.com/sound-effects/film-special-effects-ui-click-retro-514601/ | +| `src/assets/audio/sfx/sword-slash.mp3` | Sword, Slash, Swing sound effect | DavidDumaisAudio | https://pixabay.com/sound-effects/film-special-effects-sword-slash-and-swing-185432/ | +| `src/assets/audio/sfx/combat-impact.mp3` | Sword, Slash, Weapon sound effect | DavidDumaisAudio | https://pixabay.com/sound-effects/film-special-effects-sword-slash-with-metal-shield-impact-185433/ | +| `src/assets/audio/sfx/strategy-cast.mp3` | Designed, Magic spell, Projectile sound effect | RescopicSound | https://pixabay.com/sound-effects/film-special-effects-elemental-magic-spell-impact-outgoing-228342/ | +| `src/assets/audio/sfx/cart-roll.mp3` | Cart, Wheel, Cobblestones sound effect | Fronbondi_Skegs | https://pixabay.com/sound-effects/film-special-effects-foley-heavily-laden-cart-moving-over-cobblestones-sound-effect-246657/ | +| `src/assets/audio/sfx/exp-gain.mp3` | Level up, Level up sound, Game level up sound effect | Universfield | https://pixabay.com/sound-effects/film-special-effects-level-up-06-370051/ | diff --git a/src/assets/audio/bgm/battle-prep.mp3 b/src/assets/audio/bgm/battle-prep.mp3 new file mode 100644 index 0000000..8fd306d Binary files /dev/null and b/src/assets/audio/bgm/battle-prep.mp3 differ diff --git a/src/assets/audio/bgm/battle-prep.wav b/src/assets/audio/bgm/battle-prep.wav deleted file mode 100644 index 7779a1c..0000000 Binary files a/src/assets/audio/bgm/battle-prep.wav and /dev/null differ diff --git a/src/assets/audio/bgm/militia-theme.mp3 b/src/assets/audio/bgm/militia-theme.mp3 new file mode 100644 index 0000000..b9c2e6c Binary files /dev/null and b/src/assets/audio/bgm/militia-theme.mp3 differ diff --git a/src/assets/audio/bgm/militia-theme.wav b/src/assets/audio/bgm/militia-theme.wav deleted file mode 100644 index 9203dd2..0000000 Binary files a/src/assets/audio/bgm/militia-theme.wav and /dev/null differ diff --git a/src/assets/audio/bgm/oath-theme.mp3 b/src/assets/audio/bgm/oath-theme.mp3 new file mode 100644 index 0000000..03a716e Binary files /dev/null and b/src/assets/audio/bgm/oath-theme.mp3 differ diff --git a/src/assets/audio/bgm/oath-theme.wav b/src/assets/audio/bgm/oath-theme.wav deleted file mode 100644 index d46d50e..0000000 Binary files a/src/assets/audio/bgm/oath-theme.wav and /dev/null differ diff --git a/src/assets/audio/bgm/story-dark.mp3 b/src/assets/audio/bgm/story-dark.mp3 new file mode 100644 index 0000000..88c91ab Binary files /dev/null and b/src/assets/audio/bgm/story-dark.mp3 differ diff --git a/src/assets/audio/bgm/story-dark.wav b/src/assets/audio/bgm/story-dark.wav deleted file mode 100644 index 03f6fa3..0000000 Binary files a/src/assets/audio/bgm/story-dark.wav and /dev/null differ diff --git a/src/assets/audio/bgm/title-theme.mp3 b/src/assets/audio/bgm/title-theme.mp3 new file mode 100644 index 0000000..a71645c Binary files /dev/null and b/src/assets/audio/bgm/title-theme.mp3 differ diff --git a/src/assets/audio/bgm/title-theme.wav b/src/assets/audio/bgm/title-theme.wav deleted file mode 100644 index 1944de6..0000000 Binary files a/src/assets/audio/bgm/title-theme.wav and /dev/null differ diff --git a/src/assets/audio/sfx/cart-roll.mp3 b/src/assets/audio/sfx/cart-roll.mp3 new file mode 100644 index 0000000..b84d324 Binary files /dev/null and b/src/assets/audio/sfx/cart-roll.mp3 differ diff --git a/src/assets/audio/sfx/combat-impact.mp3 b/src/assets/audio/sfx/combat-impact.mp3 new file mode 100644 index 0000000..61ce1bb Binary files /dev/null and b/src/assets/audio/sfx/combat-impact.mp3 differ diff --git a/src/assets/audio/sfx/exp-gain.mp3 b/src/assets/audio/sfx/exp-gain.mp3 new file mode 100644 index 0000000..178583a Binary files /dev/null and b/src/assets/audio/sfx/exp-gain.mp3 differ diff --git a/src/assets/audio/sfx/strategy-cast.mp3 b/src/assets/audio/sfx/strategy-cast.mp3 new file mode 100644 index 0000000..806b4aa Binary files /dev/null and b/src/assets/audio/sfx/strategy-cast.mp3 differ diff --git a/src/assets/audio/sfx/sword-slash.mp3 b/src/assets/audio/sfx/sword-slash.mp3 new file mode 100644 index 0000000..bb0e2b7 Binary files /dev/null and b/src/assets/audio/sfx/sword-slash.mp3 differ diff --git a/src/assets/audio/sfx/ui-select.mp3 b/src/assets/audio/sfx/ui-select.mp3 new file mode 100644 index 0000000..d02d1c5 Binary files /dev/null and b/src/assets/audio/sfx/ui-select.mp3 differ diff --git a/src/game/audio/SoundDirector.ts b/src/game/audio/SoundDirector.ts index e93c2d0..f0087bd 100644 --- a/src/game/audio/SoundDirector.ts +++ b/src/game/audio/SoundDirector.ts @@ -1,21 +1,33 @@ type AudioTrackMap = Record; +type PlayEffectOptions = { + volume?: number; + rate?: number; + stopAfterMs?: number; +}; + class SoundDirector { private context?: AudioContext; private master?: GainNode; private started = false; private muted = false; private musicTracks: AudioTrackMap = {}; + private effectTracks: AudioTrackMap = {}; private music?: HTMLAudioElement; private currentMusicKey?: string; private pendingMusicKey?: string; private musicFadeTimer?: number; - private readonly musicVolume = 0.14; + private readonly musicVolume = 0.22; + private readonly effectVolume = 0.42; registerMusicTracks(tracks: AudioTrackMap) { this.musicTracks = tracks; } + registerEffectTracks(tracks: AudioTrackMap) { + this.effectTracks = tracks; + } + start() { if (this.started) { this.resume(); @@ -70,10 +82,48 @@ class SoundDirector { } playSelect() { + if (this.playEffect('ui-select', { volume: 0.24, stopAfterMs: 420 })) { + return; + } + this.playTone(300, 0.07, 0.04, 'sine'); window.setTimeout(() => this.playTone(450, 0.08, 0.028, 'sine'), 70); } + playEffect(key: string, options: PlayEffectOptions = {}) { + if (!this.started || this.muted) { + return false; + } + + const src = this.effectTracks[key]; + if (!src) { + return false; + } + + const effect = new Audio(src); + effect.preload = 'auto'; + effect.volume = options.volume ?? this.effectVolume; + effect.playbackRate = options.rate ?? 1; + effect.muted = this.muted; + effect.addEventListener( + 'ended', + () => { + effect.src = ''; + }, + { once: true } + ); + + if (options.stopAfterMs) { + window.setTimeout(() => { + effect.pause(); + effect.src = ''; + }, options.stopAfterMs); + } + + void effect.play().catch(() => undefined); + return true; + } + playMusic(key?: string) { if (!key) { return; diff --git a/src/game/audio/audioAssets.ts b/src/game/audio/audioAssets.ts index 2ddc3ce..f87a9ca 100644 --- a/src/game/audio/audioAssets.ts +++ b/src/game/audio/audioAssets.ts @@ -1,8 +1,14 @@ -import battlePrepUrl from '../../assets/audio/bgm/battle-prep.wav'; -import militiaThemeUrl from '../../assets/audio/bgm/militia-theme.wav'; -import oathThemeUrl from '../../assets/audio/bgm/oath-theme.wav'; -import storyDarkUrl from '../../assets/audio/bgm/story-dark.wav'; -import titleThemeUrl from '../../assets/audio/bgm/title-theme.wav'; +import battlePrepUrl from '../../assets/audio/bgm/battle-prep.mp3'; +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 cartRollUrl from '../../assets/audio/sfx/cart-roll.mp3'; +import combatImpactUrl from '../../assets/audio/sfx/combat-impact.mp3'; +import expGainUrl from '../../assets/audio/sfx/exp-gain.mp3'; +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'; export const musicTracks = { 'title-theme': titleThemeUrl, @@ -12,4 +18,14 @@ export const musicTracks = { 'battle-prep': battlePrepUrl } as const; +export const effectTracks = { + 'ui-select': uiSelectUrl, + 'sword-slash': swordSlashUrl, + 'combat-impact': combatImpactUrl, + 'strategy-cast': strategyCastUrl, + 'cart-roll': cartRollUrl, + 'exp-gain': expGainUrl +} as const; + export type MusicTrackKey = keyof typeof musicTracks; +export type EffectTrackKey = keyof typeof effectTracks; diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index dbad807..08ebd26 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -1270,6 +1270,10 @@ export class BattleScene extends Phaser.Scene { await this.animateCombatGauge(defenderStatus.hpFill, result.previousDefenderHp / result.defender.maxHp, result.defender.hp / result.defender.maxHp, 420); defenderStatus.hpText.setText(`${result.defender.hp} / ${result.defender.maxHp}`); + if (result.characterGrowth.amount > 0) { + soundDirector.playEffect('exp-gain', { volume: result.characterGrowth.leveled ? 0.44 : 0.28, stopAfterMs: 1000 }); + } + await this.animateCombatGauge( attackerStatus.expFill, result.characterGrowth.previousExp / result.characterGrowth.next, @@ -1400,6 +1404,7 @@ export class BattleScene extends Phaser.Scene { ) { const direction = result.attacker.faction === 'ally' ? 1 : -1; if (result.action === 'attack') { + soundDirector.playEffect('sword-slash', { volume: 0.52, stopAfterMs: 700 }); this.tweens.add({ targets: attackerSprite, x: attackerX + direction * 84, @@ -1418,6 +1423,11 @@ export class BattleScene extends Phaser.Scene { return; } + soundDirector.playEffect(result.action === 'item' ? 'cart-roll' : 'strategy-cast', { + volume: result.action === 'item' ? 0.34 : 0.44, + stopAfterMs: result.action === 'item' ? 720 : 950 + }); + const projectile = result.action === 'item' ? this.createCombatCart(attackerX + direction * 78, groundY - 10, depth) @@ -1462,6 +1472,8 @@ export class BattleScene extends Phaser.Scene { } private showCombatImpact(result: CombatResult, x: number, y: number, depth: number) { + soundDirector.playEffect('combat-impact', { volume: result.action === 'strategy' ? 0.3 : 0.48, stopAfterMs: 650 }); + const impact = this.trackCombatObject(this.add.star(x, y, 8, 12, 42, result.action === 'strategy' ? 0x6cc5ff : 0xffdf7b, 0.92)); impact.setDepth(depth); this.tweens.add({ diff --git a/src/main.ts b/src/main.ts index 2249672..ff7ef55 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,5 @@ import Phaser from 'phaser'; -import { musicTracks } from './game/audio/audioAssets'; +import { effectTracks, musicTracks } from './game/audio/audioAssets'; import { soundDirector } from './game/audio/SoundDirector'; import { BattleScene } from './game/scenes/BattleScene'; import { BootScene } from './game/scenes/BootScene'; @@ -42,6 +42,7 @@ const config: Phaser.Types.Core.GameConfig = { }; soundDirector.registerMusicTracks(musicTracks); +soundDirector.registerEffectTracks(effectTracks); const game = new Phaser.Game(config);