feat: add era-based camp soundscapes
This commit is contained in:
BIN
src/assets/audio/ambience/campfire-ambience.mp3
Normal file
BIN
src/assets/audio/ambience/campfire-ambience.mp3
Normal file
Binary file not shown.
BIN
src/assets/audio/ambience/mountain-wind-ambience.mp3
Normal file
BIN
src/assets/audio/ambience/mountain-wind-ambience.mp3
Normal file
Binary file not shown.
BIN
src/assets/audio/ambience/nanzhong-night-ambience.mp3
Normal file
BIN
src/assets/audio/ambience/nanzhong-night-ambience.mp3
Normal file
Binary file not shown.
BIN
src/assets/audio/ambience/river-night-ambience.mp3
Normal file
BIN
src/assets/audio/ambience/river-night-ambience.mp3
Normal file
Binary file not shown.
BIN
src/assets/audio/bgm/camp-frontier.mp3
Normal file
BIN
src/assets/audio/bgm/camp-frontier.mp3
Normal file
Binary file not shown.
BIN
src/assets/audio/bgm/camp-grand-strategy.mp3
Normal file
BIN
src/assets/audio/bgm/camp-grand-strategy.mp3
Normal file
Binary file not shown.
BIN
src/assets/audio/bgm/camp-rally.mp3
Normal file
BIN
src/assets/audio/bgm/camp-rally.mp3
Normal file
Binary file not shown.
BIN
src/assets/audio/bgm/camp-wayfarer.mp3
Normal file
BIN
src/assets/audio/bgm/camp-wayfarer.mp3
Normal file
Binary file not shown.
@@ -380,9 +380,6 @@ export class SoundDirector {
|
||||
|
||||
private setLoopTargetVolume(channel: LoopChannel, volume: number) {
|
||||
channel.targetVolume = Number.isFinite(volume) ? Math.min(1, Math.max(0, volume)) : channel.defaultVolume;
|
||||
if (channel.current && !channel.transition.active) {
|
||||
channel.current.volume = channel.targetVolume;
|
||||
}
|
||||
}
|
||||
|
||||
private requestLoopChannel(channel: LoopChannel, key?: string) {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import battlePrepUrl from '../../assets/audio/bgm/battle-prep.mp3';
|
||||
import campFrontierUrl from '../../assets/audio/bgm/camp-frontier.mp3';
|
||||
import campGrandStrategyUrl from '../../assets/audio/bgm/camp-grand-strategy.mp3';
|
||||
import campRallyUrl from '../../assets/audio/bgm/camp-rally.mp3';
|
||||
import campWayfarerUrl from '../../assets/audio/bgm/camp-wayfarer.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';
|
||||
@@ -15,13 +19,28 @@ 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';
|
||||
import campfireAmbienceUrl from '../../assets/audio/ambience/campfire-ambience.mp3';
|
||||
import mountainWindAmbienceUrl from '../../assets/audio/ambience/mountain-wind-ambience.mp3';
|
||||
import nanzhongNightAmbienceUrl from '../../assets/audio/ambience/nanzhong-night-ambience.mp3';
|
||||
import riverNightAmbienceUrl from '../../assets/audio/ambience/river-night-ambience.mp3';
|
||||
|
||||
export const musicTracks = {
|
||||
'title-theme': titleThemeUrl,
|
||||
'story-dark': storyDarkUrl,
|
||||
'oath-theme': oathThemeUrl,
|
||||
'militia-theme': militiaThemeUrl,
|
||||
'battle-prep': battlePrepUrl
|
||||
'battle-prep': battlePrepUrl,
|
||||
'camp-rally': campRallyUrl,
|
||||
'camp-wayfarer': campWayfarerUrl,
|
||||
'camp-grand-strategy': campGrandStrategyUrl,
|
||||
'camp-frontier': campFrontierUrl
|
||||
} as const;
|
||||
|
||||
export const ambienceTracks = {
|
||||
'campfire-ambience': campfireAmbienceUrl,
|
||||
'river-night-ambience': riverNightAmbienceUrl,
|
||||
'mountain-wind-ambience': mountainWindAmbienceUrl,
|
||||
'nanzhong-night-ambience': nanzhongNightAmbienceUrl
|
||||
} as const;
|
||||
|
||||
export const effectTracks = {
|
||||
@@ -40,4 +59,5 @@ export const effectTracks = {
|
||||
} as const;
|
||||
|
||||
export type MusicTrackKey = keyof typeof musicTracks;
|
||||
export type AmbienceTrackKey = keyof typeof ambienceTracks;
|
||||
export type EffectTrackKey = keyof typeof effectTracks;
|
||||
|
||||
119
src/game/data/campSoundscapes.ts
Normal file
119
src/game/data/campSoundscapes.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { AmbienceTrackKey, MusicTrackKey } from '../audio/audioAssets';
|
||||
import type { CampSkinId } from './campSkins';
|
||||
|
||||
export type CampMusicGroupId = 'rally' | 'wayfarer' | 'grand-strategy' | 'frontier';
|
||||
|
||||
export type CampAmbienceId = 'campfire' | 'river-night' | 'mountain-wind' | 'nanzhong-night';
|
||||
|
||||
export type CampMusicTrackKey = Extract<
|
||||
MusicTrackKey,
|
||||
'camp-rally' | 'camp-wayfarer' | 'camp-grand-strategy' | 'camp-frontier'
|
||||
>;
|
||||
|
||||
export type CampAmbienceTrackKey = Extract<
|
||||
AmbienceTrackKey,
|
||||
'campfire-ambience' | 'river-night-ambience' | 'mountain-wind-ambience' | 'nanzhong-night-ambience'
|
||||
>;
|
||||
|
||||
export type CampMusicGroupDefinition = {
|
||||
id: CampMusicGroupId;
|
||||
trackKey: CampMusicTrackKey;
|
||||
label: string;
|
||||
volume: number;
|
||||
};
|
||||
|
||||
export type CampAmbienceDefinition = {
|
||||
id: CampAmbienceId;
|
||||
trackKey: CampAmbienceTrackKey;
|
||||
label: string;
|
||||
volume: number;
|
||||
};
|
||||
|
||||
export type CampSoundscapeMapping = {
|
||||
musicGroupId: CampMusicGroupId;
|
||||
ambienceId: CampAmbienceId;
|
||||
};
|
||||
|
||||
export type CampSoundscape = {
|
||||
skinId: CampSkinId;
|
||||
music: CampMusicGroupDefinition;
|
||||
ambience: CampAmbienceDefinition;
|
||||
};
|
||||
|
||||
export const campMusicGroups: Record<CampMusicGroupId, CampMusicGroupDefinition> = {
|
||||
rally: {
|
||||
id: 'rally',
|
||||
trackKey: 'camp-rally',
|
||||
label: '결의와 출정',
|
||||
volume: 0.22
|
||||
},
|
||||
wayfarer: {
|
||||
id: 'wayfarer',
|
||||
trackKey: 'camp-wayfarer',
|
||||
label: '유랑의 밤',
|
||||
volume: 0.18
|
||||
},
|
||||
'grand-strategy': {
|
||||
id: 'grand-strategy',
|
||||
trackKey: 'camp-grand-strategy',
|
||||
label: '대업의 계책',
|
||||
volume: 0.2
|
||||
},
|
||||
frontier: {
|
||||
id: 'frontier',
|
||||
trackKey: 'camp-frontier',
|
||||
label: '변경과 북벌',
|
||||
volume: 0.2
|
||||
}
|
||||
};
|
||||
|
||||
export const campAmbienceDefinitions: Record<CampAmbienceId, CampAmbienceDefinition> = {
|
||||
campfire: {
|
||||
id: 'campfire',
|
||||
trackKey: 'campfire-ambience',
|
||||
label: '장작불',
|
||||
volume: 0.12
|
||||
},
|
||||
'river-night': {
|
||||
id: 'river-night',
|
||||
trackKey: 'river-night-ambience',
|
||||
label: '밤의 강물',
|
||||
volume: 0.1
|
||||
},
|
||||
'mountain-wind': {
|
||||
id: 'mountain-wind',
|
||||
trackKey: 'mountain-wind-ambience',
|
||||
label: '산바람',
|
||||
volume: 0.09
|
||||
},
|
||||
'nanzhong-night': {
|
||||
id: 'nanzhong-night',
|
||||
trackKey: 'nanzhong-night-ambience',
|
||||
label: '남중의 밤',
|
||||
volume: 0.11
|
||||
}
|
||||
};
|
||||
|
||||
export const campSoundscapeMapping: Record<CampSkinId, CampSoundscapeMapping> = {
|
||||
'yellow-turban': { musicGroupId: 'rally', ambienceId: 'campfire' },
|
||||
'anti-dong': { musicGroupId: 'rally', ambienceId: 'campfire' },
|
||||
xuzhou: { musicGroupId: 'wayfarer', ambienceId: 'campfire' },
|
||||
wandering: { musicGroupId: 'wayfarer', ambienceId: 'campfire' },
|
||||
wolong: { musicGroupId: 'wayfarer', ambienceId: 'mountain-wind' },
|
||||
'red-cliffs': { musicGroupId: 'grand-strategy', ambienceId: 'river-night' },
|
||||
'jing-yi': { musicGroupId: 'grand-strategy', ambienceId: 'river-night' },
|
||||
'hanzhong-shuhan': { musicGroupId: 'grand-strategy', ambienceId: 'mountain-wind' },
|
||||
'jingzhou-crisis': { musicGroupId: 'grand-strategy', ambienceId: 'river-night' },
|
||||
'yiling-baidi': { musicGroupId: 'grand-strategy', ambienceId: 'river-night' },
|
||||
nanzhong: { musicGroupId: 'frontier', ambienceId: 'nanzhong-night' },
|
||||
northern: { musicGroupId: 'frontier', ambienceId: 'mountain-wind' }
|
||||
};
|
||||
|
||||
export function getCampSoundscape(skinId: CampSkinId): CampSoundscape {
|
||||
const mapping = campSoundscapeMapping[skinId];
|
||||
return {
|
||||
skinId,
|
||||
music: campMusicGroups[mapping.musicGroupId],
|
||||
ambience: campAmbienceDefinitions[mapping.ambienceId]
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { compareUnitStrategyCoverage, getUnitStrategyCoverage, getUnitStrategyNa
|
||||
import { getUnitClass, terrainRules, type TerrainType, type UnitClassKey } from '../data/battleRules';
|
||||
import { defaultBattleScenario, getBattleScenario, type BattleScenarioDefinition, type BattleScenarioId } from '../data/battles';
|
||||
import { getSortieFlow } from '../data/campaignFlow';
|
||||
import { getCampSoundscape, type CampSoundscape } from '../data/campSoundscapes';
|
||||
import {
|
||||
campaignPortraitKeysByUnitId,
|
||||
portraitAssetEntriesForKey,
|
||||
@@ -11070,6 +11071,7 @@ export class CampScene extends Phaser.Scene {
|
||||
private campSkinSeal?: Phaser.GameObjects.Arc;
|
||||
private campSkinBadge?: Phaser.GameObjects.Rectangle;
|
||||
private campSkinSummary?: Phaser.GameObjects.Text;
|
||||
private campSoundscape?: CampSoundscape;
|
||||
private campSkinRenderedTextureKey?: string;
|
||||
private campSkinUsedFallback = false;
|
||||
private campSkinTransitionRevision = 0;
|
||||
@@ -11206,6 +11208,7 @@ export class CampScene extends Phaser.Scene {
|
||||
nextBattleId: sortieFlow.nextBattleId,
|
||||
currentBattleId: this.currentCampBattleId()
|
||||
});
|
||||
this.campSoundscape = getCampSoundscape(this.campSkinSelection.skin.id);
|
||||
this.campSkinBackdrop = undefined;
|
||||
this.campSkinWash = undefined;
|
||||
this.campSkinVignette = undefined;
|
||||
@@ -11223,7 +11226,12 @@ export class CampScene extends Phaser.Scene {
|
||||
this.sortieItemAssignments = this.normalizedSortieItemAssignments(this.campaign.sortieItemAssignments);
|
||||
this.selectedDialogueId = this.availableCampDialogues()[0]?.id ?? campDialogues[0].id;
|
||||
this.selectedVisitId = this.availableCampVisits()[0]?.id ?? campVisits[0].id;
|
||||
soundDirector.playMusic('militia-theme');
|
||||
soundDirector.playSoundscape({
|
||||
musicKey: this.campSoundscape.music.trackKey,
|
||||
ambienceKey: this.campSoundscape.ambience.trackKey,
|
||||
musicVolume: this.campSoundscape.music.volume,
|
||||
ambienceVolume: this.campSoundscape.ambience.volume
|
||||
});
|
||||
this.input.keyboard?.on('keydown-ESC', () => this.handleEscapeKey());
|
||||
this.input.keyboard?.on('keydown-ENTER', (event: KeyboardEvent) => this.handleSortieEnterKey(event));
|
||||
this.input.keyboard?.on('keydown', (event: KeyboardEvent) => this.handleSaveSlotKey(event));
|
||||
@@ -21673,6 +21681,20 @@ export class CampScene extends Phaser.Scene {
|
||||
campaignProgress: this.campaignTimelineProgress(),
|
||||
campBattleId: this.currentCampBattleId(),
|
||||
campTitle: this.currentCampTitle(),
|
||||
campSoundscape: this.campSoundscape
|
||||
? {
|
||||
skinId: this.campSoundscape.skinId,
|
||||
musicGroupId: this.campSoundscape.music.id,
|
||||
musicKey: this.campSoundscape.music.trackKey,
|
||||
musicLabel: this.campSoundscape.music.label,
|
||||
musicVolume: this.campSoundscape.music.volume,
|
||||
ambienceId: this.campSoundscape.ambience.id,
|
||||
ambienceKey: this.campSoundscape.ambience.trackKey,
|
||||
ambienceLabel: this.campSoundscape.ambience.label,
|
||||
ambienceVolume: this.campSoundscape.ambience.volume
|
||||
}
|
||||
: null,
|
||||
audio: soundDirector.getDebugState(),
|
||||
campSkin: this.campSkinSelection
|
||||
? {
|
||||
id: this.campSkinSelection.skin.id,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Phaser from 'phaser';
|
||||
import { effectTracks, musicTracks } from './game/audio/audioAssets';
|
||||
import { ambienceTracks, effectTracks, musicTracks } from './game/audio/audioAssets';
|
||||
import { soundDirector } from './game/audio/SoundDirector';
|
||||
import { BootScene } from './game/scenes/BootScene';
|
||||
import { startLazySceneFromGame } from './game/scenes/lazyScenes';
|
||||
@@ -26,6 +26,7 @@ type DebugCampScene = Phaser.Scene & {
|
||||
type HerosDebugApi = {
|
||||
game: Phaser.Game;
|
||||
activeScenes: () => string[];
|
||||
audio: () => ReturnType<typeof soundDirector.getDebugState>;
|
||||
battle: () => unknown;
|
||||
camp: () => unknown;
|
||||
goToBattle: (battleId?: string) => Promise<void>;
|
||||
@@ -57,6 +58,7 @@ const config: Phaser.Types.Core.GameConfig = {
|
||||
};
|
||||
|
||||
soundDirector.registerMusicTracks(musicTracks);
|
||||
soundDirector.registerAmbienceTracks(ambienceTracks);
|
||||
soundDirector.registerEffectTracks(effectTracks);
|
||||
|
||||
const game = new Phaser.Game(config);
|
||||
@@ -76,6 +78,7 @@ function createDebugApi(game: Phaser.Game): HerosDebugApi {
|
||||
return {
|
||||
game,
|
||||
activeScenes: () => game.scene.getScenes(true).map((activeScene) => activeScene.scene.key),
|
||||
audio: () => soundDirector.getDebugState(),
|
||||
battle: () => battleScene()?.getDebugState?.() ?? { active: false, reason: 'BattleScene is not active yet.' },
|
||||
camp: () => campScene()?.getDebugState?.() ?? { active: false, reason: 'CampScene is not active yet.' },
|
||||
goToBattle: (battleId) => startLazySceneFromGame(game, 'BattleScene', battleId ? { battleId } : undefined),
|
||||
|
||||
Reference in New Issue
Block a user