Improve battle unit feedback and detail panel

This commit is contained in:
2026-06-29 01:47:53 +09:00
parent e4f2754b08
commit d8177c88dc
2 changed files with 301 additions and 29 deletions

View File

@@ -17,6 +17,8 @@ class SoundDirector {
private currentMusicKey?: string;
private pendingMusicKey?: string;
private musicFadeTimer?: number;
private lastEffect?: { key: string; at: number; volume: number; rate: number };
private lastMovementStep?: { key: string; mounted: boolean; stepIndex: number; at: number; volume?: number; rate?: number };
private readonly musicVolume = 0.22;
private readonly effectVolume = 0.42;
@@ -118,6 +120,14 @@ class SoundDirector {
this.playEffect('footstep-walk', { volume: 0.2, rate: 1.35, stopAfterMs: 320 });
}
playMovementStep(mounted: boolean, stepIndex: number, options: PlayEffectOptions = {}) {
const key = mounted ? 'horse-gallop' : 'footstep-walk';
this.lastMovementStep = { key, mounted, stepIndex, at: performance.now(), volume: options.volume, rate: options.rate };
const played = this.playEffect(key, options);
this.playStepPulse(mounted, stepIndex, options.volume);
return played;
}
playMeleeSwing(hit: boolean, critical: boolean) {
this.playEffect('sword-slash', {
volume: critical ? 0.62 : hit ? 0.52 : 0.38,
@@ -169,6 +179,7 @@ class SoundDirector {
effect.volume = options.volume ?? this.effectVolume;
effect.playbackRate = options.rate ?? 1;
effect.muted = this.muted;
this.lastEffect = { key, at: performance.now(), volume: effect.volume, rate: effect.playbackRate };
effect.addEventListener(
'ended',
() => {
@@ -188,6 +199,18 @@ class SoundDirector {
return true;
}
getDebugState() {
return {
started: this.started,
muted: this.muted,
contextState: this.context?.state ?? null,
currentMusicKey: this.currentMusicKey ?? null,
pendingMusicKey: this.pendingMusicKey ?? null,
lastEffect: this.lastEffect ?? null,
lastMovementStep: this.lastMovementStep ?? null
};
}
playMusic(key?: string) {
if (!key) {
return;
@@ -248,6 +271,51 @@ class SoundDirector {
oscillator.stop(now + duration + 0.04);
}
private playStepPulse(mounted: boolean, stepIndex: number, volume = 0.2) {
if (!this.context || this.muted) {
return;
}
const scale = Math.max(0.35, Math.min(1.2, volume / 0.24));
const start = this.context.currentTime;
const primaryFrequency = mounted ? 112 + (stepIndex % 2) * 26 : 148 + (stepIndex % 2) * 34;
const duration = mounted ? 0.09 : 0.075;
const gainValue = (mounted ? 0.022 : 0.016) * scale;
this.playPercussivePulse(primaryFrequency, start, duration, gainValue, mounted ? 'square' : 'triangle');
if (mounted) {
this.playPercussivePulse(82 + (stepIndex % 2) * 18, start + 0.055, 0.075, gainValue * 0.78, 'square');
}
}
private playPercussivePulse(
frequency: number,
start: number,
duration: number,
gainValue: number,
type: OscillatorType
) {
if (!this.context || this.muted) {
return;
}
const oscillator = this.context.createOscillator();
const gain = this.context.createGain();
oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, start);
oscillator.frequency.exponentialRampToValueAtTime(Math.max(40, frequency * 0.62), start + duration);
gain.gain.setValueAtTime(0.0001, start);
gain.gain.linearRampToValueAtTime(gainValue, start + 0.006);
gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
oscillator.connect(gain);
gain.connect(this.master ?? this.context.destination);
oscillator.start(start);
oscillator.stop(start + duration + 0.025);
}
private fadeMusic(previous: HTMLAudioElement | undefined, next: HTMLAudioElement, targetVolume: number) {
if (this.musicFadeTimer) {
window.clearInterval(this.musicFadeTimer);