Improve battle unit feedback and detail panel
This commit is contained in:
@@ -17,6 +17,8 @@ class SoundDirector {
|
|||||||
private currentMusicKey?: string;
|
private currentMusicKey?: string;
|
||||||
private pendingMusicKey?: string;
|
private pendingMusicKey?: string;
|
||||||
private musicFadeTimer?: number;
|
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 musicVolume = 0.22;
|
||||||
private readonly effectVolume = 0.42;
|
private readonly effectVolume = 0.42;
|
||||||
|
|
||||||
@@ -118,6 +120,14 @@ class SoundDirector {
|
|||||||
this.playEffect('footstep-walk', { volume: 0.2, rate: 1.35, stopAfterMs: 320 });
|
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) {
|
playMeleeSwing(hit: boolean, critical: boolean) {
|
||||||
this.playEffect('sword-slash', {
|
this.playEffect('sword-slash', {
|
||||||
volume: critical ? 0.62 : hit ? 0.52 : 0.38,
|
volume: critical ? 0.62 : hit ? 0.52 : 0.38,
|
||||||
@@ -169,6 +179,7 @@ class SoundDirector {
|
|||||||
effect.volume = options.volume ?? this.effectVolume;
|
effect.volume = options.volume ?? this.effectVolume;
|
||||||
effect.playbackRate = options.rate ?? 1;
|
effect.playbackRate = options.rate ?? 1;
|
||||||
effect.muted = this.muted;
|
effect.muted = this.muted;
|
||||||
|
this.lastEffect = { key, at: performance.now(), volume: effect.volume, rate: effect.playbackRate };
|
||||||
effect.addEventListener(
|
effect.addEventListener(
|
||||||
'ended',
|
'ended',
|
||||||
() => {
|
() => {
|
||||||
@@ -188,6 +199,18 @@ class SoundDirector {
|
|||||||
return true;
|
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) {
|
playMusic(key?: string) {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
@@ -248,6 +271,51 @@ class SoundDirector {
|
|||||||
oscillator.stop(now + duration + 0.04);
|
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) {
|
private fadeMusic(previous: HTMLAudioElement | undefined, next: HTMLAudioElement, targetVolume: number) {
|
||||||
if (this.musicFadeTimer) {
|
if (this.musicFadeTimer) {
|
||||||
window.clearInterval(this.musicFadeTimer);
|
window.clearInterval(this.musicFadeTimer);
|
||||||
|
|||||||
@@ -2880,6 +2880,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private turnPromptObjects: Phaser.GameObjects.GameObject[] = [];
|
private turnPromptObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private turnPromptMode?: TurnPromptMode;
|
private turnPromptMode?: TurnPromptMode;
|
||||||
private combatCutInObjects: Phaser.GameObjects.GameObject[] = [];
|
private combatCutInObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
|
private facingIndicatorObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
|
private facingIndicatorUnitId?: string;
|
||||||
private resultObjects: Phaser.GameObjects.GameObject[] = [];
|
private resultObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private resultGaugeAnimations: ResultGaugeAnimation[] = [];
|
private resultGaugeAnimations: ResultGaugeAnimation[] = [];
|
||||||
private resultAnimationToken = 0;
|
private resultAnimationToken = 0;
|
||||||
@@ -2905,6 +2907,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private miniMapObjects: Phaser.GameObjects.GameObject[] = [];
|
private miniMapObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private miniMapUnitDots = new Map<string, Phaser.GameObjects.Rectangle>();
|
private miniMapUnitDots = new Map<string, Phaser.GameObjects.Rectangle>();
|
||||||
private miniMapViewport?: Phaser.GameObjects.Rectangle;
|
private miniMapViewport?: Phaser.GameObjects.Rectangle;
|
||||||
|
private miniMapVisible = true;
|
||||||
private cameraTileX = 0;
|
private cameraTileX = 0;
|
||||||
private cameraTileY = 0;
|
private cameraTileY = 0;
|
||||||
private edgeScrollElapsed = 0;
|
private edgeScrollElapsed = 0;
|
||||||
@@ -2952,6 +2955,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.phase = 'idle';
|
this.phase = 'idle';
|
||||||
soundDirector.playMusic('battle-prep');
|
soundDirector.playMusic('battle-prep');
|
||||||
this.input.mouse?.disableContextMenu();
|
this.input.mouse?.disableContextMenu();
|
||||||
|
this.installBattleAudioUnlock();
|
||||||
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||||
if (pointer.rightButtonDown()) {
|
if (pointer.rightButtonDown()) {
|
||||||
this.handleRightClick(pointer);
|
this.handleRightClick(pointer);
|
||||||
@@ -2968,6 +2972,16 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.ensureScenarioAssets(() => this.drawBattleScene());
|
this.ensureScenarioAssets(() => this.drawBattleScene());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private installBattleAudioUnlock() {
|
||||||
|
const unlock = () => {
|
||||||
|
soundDirector.start();
|
||||||
|
soundDirector.resume();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.input.once('pointerdown', unlock);
|
||||||
|
this.input.keyboard?.once('keydown', unlock);
|
||||||
|
}
|
||||||
|
|
||||||
private drawBattleScene() {
|
private drawBattleScene() {
|
||||||
this.drawMap();
|
this.drawMap();
|
||||||
this.drawUnits();
|
this.drawUnits();
|
||||||
@@ -3415,12 +3429,19 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
battleUnits.forEach((unit) => this.positionUnitView(unit));
|
battleUnits.forEach((unit) => this.positionUnitView(unit));
|
||||||
this.updateMiniMap();
|
this.updateMiniMap();
|
||||||
|
this.refreshFacingIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateMiniMap() {
|
private updateMiniMap() {
|
||||||
if (!this.miniMapLayout) {
|
if (!this.miniMapLayout) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!this.miniMapVisible) {
|
||||||
|
this.miniMapObjects.forEach((object) => this.setMiniMapObjectVisible(object, false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.miniMapObjects.forEach((object) => this.setMiniMapObjectVisible(object, true));
|
||||||
|
|
||||||
const { x, y, cellSize } = this.miniMapLayout;
|
const { x, y, cellSize } = this.miniMapLayout;
|
||||||
this.miniMapUnitDots.forEach((dot, unitId) => {
|
this.miniMapUnitDots.forEach((dot, unitId) => {
|
||||||
@@ -3436,6 +3457,22 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.miniMapViewport?.setPosition(x + this.cameraTileX * cellSize, y + this.cameraTileY * cellSize);
|
this.miniMapViewport?.setPosition(x + this.cameraTileX * cellSize, y + this.cameraTileY * cellSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setMiniMapVisible(visible: boolean) {
|
||||||
|
this.miniMapVisible = visible;
|
||||||
|
this.updateMiniMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setMiniMapObjectVisible(object: Phaser.GameObjects.GameObject, visible: boolean) {
|
||||||
|
const visibleObject = object as Phaser.GameObjects.GameObject & {
|
||||||
|
setVisible: (value: boolean) => Phaser.GameObjects.GameObject;
|
||||||
|
input?: { enabled: boolean };
|
||||||
|
};
|
||||||
|
visibleObject.setVisible(visible);
|
||||||
|
if (visibleObject.input) {
|
||||||
|
visibleObject.input.enabled = visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private positionTileObject(object: Phaser.GameObjects.Rectangle, x: number, y: number) {
|
private positionTileObject(object: Phaser.GameObjects.Rectangle, x: number, y: number) {
|
||||||
const screenX = this.layout.gridX + (x - this.cameraTileX) * this.layout.tileSize;
|
const screenX = this.layout.gridX + (x - this.cameraTileX) * this.layout.tileSize;
|
||||||
const screenY = this.layout.gridY + (y - this.cameraTileY) * this.layout.tileSize;
|
const screenY = this.layout.gridY + (y - this.cameraTileY) * this.layout.tileSize;
|
||||||
@@ -7138,13 +7175,14 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
attackerSprite.setDepth(depth + 6);
|
attackerSprite.setDepth(depth + 6);
|
||||||
attackerSprite.setDisplaySize(126, 126);
|
this.applyCombatSpriteDisplaySize(attackerSprite, result.attacker);
|
||||||
|
this.applyActionSpriteBlend(attackerSprite);
|
||||||
|
|
||||||
const defenderSprite = this.trackCombatObject(
|
const defenderSprite = this.trackCombatObject(
|
||||||
this.add.sprite(defenderX, groundY, this.unitTextureKey(result.defender), this.unitFrameIndex(defenderDirection))
|
this.add.sprite(defenderX, groundY, this.unitTextureKey(result.defender), this.unitFrameIndex(defenderDirection))
|
||||||
);
|
);
|
||||||
defenderSprite.setDepth(depth + 6);
|
defenderSprite.setDepth(depth + 6);
|
||||||
defenderSprite.setDisplaySize(126, 126);
|
this.applyCombatSpriteDisplaySize(defenderSprite, result.defender);
|
||||||
|
|
||||||
const attackerStatus = this.renderCombatStatusPanel(result.attacker, left + 26, top + 286, 392, result.characterGrowth);
|
const attackerStatus = this.renderCombatStatusPanel(result.attacker, left + 26, top + 286, 392, result.characterGrowth);
|
||||||
const defenderStatus = this.renderCombatStatusPanel(result.defender, left + panelWidth - 418, top + 286, 392);
|
const defenderStatus = this.renderCombatStatusPanel(result.defender, left + panelWidth - 418, top + 286, 392);
|
||||||
@@ -7211,6 +7249,28 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.hideCombatCutIn();
|
this.hideCombatCutIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyCombatSpriteDisplaySize(sprite: Phaser.GameObjects.Sprite, unit: UnitData, compact = false) {
|
||||||
|
const size = this.combatSpriteDisplaySize(unit, compact);
|
||||||
|
sprite.setDisplaySize(size, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private combatSpriteDisplaySize(unit: UnitData, compact = false) {
|
||||||
|
const baseSizeByClass: Record<UnitClassKey, number> = {
|
||||||
|
lord: 128,
|
||||||
|
infantry: 124,
|
||||||
|
cavalry: 136,
|
||||||
|
spearman: 126,
|
||||||
|
archer: 120,
|
||||||
|
strategist: 116,
|
||||||
|
quartermaster: 116,
|
||||||
|
bandit: 124,
|
||||||
|
yellowTurban: 124,
|
||||||
|
rebelLeader: 128
|
||||||
|
};
|
||||||
|
const size = baseSizeByClass[unit.classKey] ?? 124;
|
||||||
|
return Math.round(size * (compact ? 0.9 : 1));
|
||||||
|
}
|
||||||
|
|
||||||
private async playSupportCutIn(result: SupportResult) {
|
private async playSupportCutIn(result: SupportResult) {
|
||||||
this.hideCombatCutIn();
|
this.hideCombatCutIn();
|
||||||
|
|
||||||
@@ -7245,12 +7305,13 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.add.sprite(left + 160, top + 146, this.unitActionTexture(result.user), this.unitActionFrameIndex('east', supportPose))
|
this.add.sprite(left + 160, top + 146, this.unitActionTexture(result.user), this.unitActionFrameIndex('east', supportPose))
|
||||||
);
|
);
|
||||||
userSprite.setDepth(depth + 3);
|
userSprite.setDepth(depth + 3);
|
||||||
userSprite.setDisplaySize(112, 112);
|
this.applyCombatSpriteDisplaySize(userSprite, result.user, true);
|
||||||
|
this.applyActionSpriteBlend(userSprite);
|
||||||
void this.playUnitActionFrames(userSprite, result.user, 'east', supportPose, 100, 2);
|
void this.playUnitActionFrames(userSprite, result.user, 'east', supportPose, 100, 2);
|
||||||
|
|
||||||
const targetSprite = this.trackCombatObject(this.add.sprite(left + panelWidth - 160, top + 146, this.unitTextureKey(result.target), this.unitFrameIndex('west')));
|
const targetSprite = this.trackCombatObject(this.add.sprite(left + panelWidth - 160, top + 146, this.unitTextureKey(result.target), this.unitFrameIndex('west')));
|
||||||
targetSprite.setDepth(depth + 3);
|
targetSprite.setDepth(depth + 3);
|
||||||
targetSprite.setDisplaySize(112, 112);
|
this.applyCombatSpriteDisplaySize(targetSprite, result.target, true);
|
||||||
|
|
||||||
const effectColor = result.usable.effect === 'heal' ? 0x59d18c : 0xd8b15f;
|
const effectColor = result.usable.effect === 'heal' ? 0x59d18c : 0xd8b15f;
|
||||||
const effect = this.trackCombatObject(this.add.circle(left + panelWidth / 2, top + 145, 30, effectColor, 0.76));
|
const effect = this.trackCombatObject(this.add.circle(left + panelWidth / 2, top + 145, 30, effectColor, 0.76));
|
||||||
@@ -7776,6 +7837,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.add.sprite(x, y, this.unitActionTexture(unit), this.unitActionFrameIndex('south', this.celebrationPose(unit)))
|
this.add.sprite(x, y, this.unitActionTexture(unit), this.unitActionFrameIndex('south', this.celebrationPose(unit)))
|
||||||
);
|
);
|
||||||
sprite.setDisplaySize(82, 82);
|
sprite.setDisplaySize(82, 82);
|
||||||
|
this.applyActionSpriteBlend(sprite);
|
||||||
sprite.setDepth(depth);
|
sprite.setDepth(depth);
|
||||||
return sprite;
|
return sprite;
|
||||||
}
|
}
|
||||||
@@ -7855,6 +7917,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const mounted = result.attacker.classKey === 'cavalry';
|
const mounted = result.attacker.classKey === 'cavalry';
|
||||||
const windupX = attackerX - direction * 28;
|
const windupX = attackerX - direction * 28;
|
||||||
const contactX = defenderX - direction * 126;
|
const contactX = defenderX - direction * 126;
|
||||||
|
const baseScaleX = attackerSprite.scaleX;
|
||||||
|
const baseScaleY = attackerSprite.scaleY;
|
||||||
|
|
||||||
soundDirector.playMeleeRush(mounted);
|
soundDirector.playMeleeRush(mounted);
|
||||||
void this.playUnitActionFrames(attackerSprite, result.attacker, 'east', 'attack', mounted ? 72 : 82);
|
void this.playUnitActionFrames(attackerSprite, result.attacker, 'east', 'attack', mounted ? 72 : 82);
|
||||||
@@ -7863,7 +7927,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
targets: attackerSprite,
|
targets: attackerSprite,
|
||||||
x: windupX,
|
x: windupX,
|
||||||
y: groundY + 5,
|
y: groundY + 5,
|
||||||
scaleX: 0.96,
|
scaleX: baseScaleX * 0.96,
|
||||||
|
scaleY: baseScaleY,
|
||||||
duration: 110,
|
duration: 110,
|
||||||
ease: 'Sine.easeIn'
|
ease: 'Sine.easeIn'
|
||||||
});
|
});
|
||||||
@@ -7875,7 +7940,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
targets: attackerSprite,
|
targets: attackerSprite,
|
||||||
x: contactX,
|
x: contactX,
|
||||||
y: groundY - (mounted ? 4 : 2),
|
y: groundY - (mounted ? 4 : 2),
|
||||||
scaleX: 1.05,
|
scaleX: baseScaleX * 1.05,
|
||||||
|
scaleY: baseScaleY,
|
||||||
duration: mounted ? 170 : 205,
|
duration: mounted ? 170 : 205,
|
||||||
ease: 'Cubic.easeIn'
|
ease: 'Cubic.easeIn'
|
||||||
});
|
});
|
||||||
@@ -7906,7 +7972,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
targets: attackerSprite,
|
targets: attackerSprite,
|
||||||
x: attackerX,
|
x: attackerX,
|
||||||
y: groundY,
|
y: groundY,
|
||||||
scaleX: 1,
|
scaleX: baseScaleX,
|
||||||
|
scaleY: baseScaleY,
|
||||||
duration: 180,
|
duration: 180,
|
||||||
ease: 'Sine.easeOut'
|
ease: 'Sine.easeOut'
|
||||||
});
|
});
|
||||||
@@ -7991,10 +8058,13 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void this.playUnitActionFrames(attackerSprite, result.attacker, 'east', this.actionPoseForCommand(result.action), 92);
|
void this.playUnitActionFrames(attackerSprite, result.attacker, 'east', this.actionPoseForCommand(result.action), 92);
|
||||||
|
const baseScaleX = attackerSprite.scaleX;
|
||||||
|
const baseScaleY = attackerSprite.scaleY;
|
||||||
this.createCasterPulse(attackerX + direction * 48, groundY - 50, depth + 2, result.action === 'item' ? 0xd8b15f : 0x6cc5ff);
|
this.createCasterPulse(attackerX + direction * 48, groundY - 50, depth + 2, result.action === 'item' ? 0xd8b15f : 0x6cc5ff);
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: attackerSprite,
|
targets: attackerSprite,
|
||||||
scale: 1.08,
|
scaleX: baseScaleX * 1.08,
|
||||||
|
scaleY: baseScaleY * 1.08,
|
||||||
y: groundY - 4,
|
y: groundY - 4,
|
||||||
duration: 150,
|
duration: 150,
|
||||||
yoyo: true,
|
yoyo: true,
|
||||||
@@ -8486,6 +8556,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.setUnitBasePosition(view, targetX, targetY);
|
this.setUnitBasePosition(view, targetX, targetY);
|
||||||
this.syncUnitMotion(unit, view, direction);
|
this.syncUnitMotion(unit, view, direction);
|
||||||
this.updateMiniMap();
|
this.updateMiniMap();
|
||||||
|
this.refreshFacingIndicator(unit.id);
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -9019,6 +9090,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.setUnitBasePosition(view, targetX, targetY);
|
this.setUnitBasePosition(view, targetX, targetY);
|
||||||
this.syncUnitMotion(unit, view, direction);
|
this.syncUnitMotion(unit, view, direction);
|
||||||
this.updateMiniMap();
|
this.updateMiniMap();
|
||||||
|
this.refreshFacingIndicator(unit.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
@@ -9047,7 +9119,6 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private playMovementSound(unit: UnitData, duration: number, tileDistance = 1) {
|
private playMovementSound(unit: UnitData, duration: number, tileDistance = 1) {
|
||||||
const isMounted = this.isMountedUnit(unit);
|
const isMounted = this.isMountedUnit(unit);
|
||||||
const key = isMounted ? 'horse-gallop' : 'footstep-walk';
|
|
||||||
const pulseCount = isMounted
|
const pulseCount = isMounted
|
||||||
? Math.max(3, Math.min(10, tileDistance * 2 + 1))
|
? Math.max(3, Math.min(10, tileDistance * 2 + 1))
|
||||||
: Math.max(2, Math.min(8, tileDistance + 1));
|
: Math.max(2, Math.min(8, tileDistance + 1));
|
||||||
@@ -9055,7 +9126,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
for (let index = 0; index < pulseCount; index += 1) {
|
for (let index = 0; index < pulseCount; index += 1) {
|
||||||
this.time.delayedCall(Math.round(index * interval), () => {
|
this.time.delayedCall(Math.round(index * interval), () => {
|
||||||
soundDirector.playEffect(key, {
|
soundDirector.playMovementStep(isMounted, index, {
|
||||||
volume: isMounted ? 0.24 : 0.18,
|
volume: isMounted ? 0.24 : 0.18,
|
||||||
rate: isMounted ? 0.98 + (index % 2) * 0.12 : 0.9 + (index % 2) * 0.12,
|
rate: isMounted ? 0.98 + (index % 2) * 0.12 : 0.9 + (index % 2) * 0.12,
|
||||||
stopAfterMs: Math.round(Math.min(isMounted ? 210 : 260, interval + 80))
|
stopAfterMs: Math.round(Math.min(isMounted ? 210 : 260, interval + 80))
|
||||||
@@ -9071,6 +9142,107 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return deltaY >= 0 ? 'south' : 'north';
|
return deltaY >= 0 ? 'south' : 'north';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private currentUnitDirection(unit: UnitData): UnitDirection {
|
||||||
|
return this.unitViews.get(unit.id)?.direction ?? 'south';
|
||||||
|
}
|
||||||
|
|
||||||
|
private unitDirectionLabel(direction: UnitDirection) {
|
||||||
|
const labels: Record<UnitDirection, string> = {
|
||||||
|
south: '아래',
|
||||||
|
east: '오른쪽',
|
||||||
|
north: '위',
|
||||||
|
west: '왼쪽'
|
||||||
|
};
|
||||||
|
return labels[direction];
|
||||||
|
}
|
||||||
|
|
||||||
|
private directionSymbol(direction: UnitDirection) {
|
||||||
|
const symbols: Record<UnitDirection, string> = {
|
||||||
|
south: '▼',
|
||||||
|
east: '▶',
|
||||||
|
north: '▲',
|
||||||
|
west: '◀'
|
||||||
|
};
|
||||||
|
return symbols[direction];
|
||||||
|
}
|
||||||
|
|
||||||
|
private directionVector(direction: UnitDirection) {
|
||||||
|
const vectors: Record<UnitDirection, { x: number; y: number }> = {
|
||||||
|
south: { x: 0, y: 1 },
|
||||||
|
east: { x: 1, y: 0 },
|
||||||
|
north: { x: 0, y: -1 },
|
||||||
|
west: { x: -1, y: 0 }
|
||||||
|
};
|
||||||
|
return vectors[direction];
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshFacingIndicator(unitId = this.facingIndicatorUnitId) {
|
||||||
|
if (!unitId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unit = battleUnits.find((candidate) => candidate.id === unitId);
|
||||||
|
if (!unit) {
|
||||||
|
this.clearFacingIndicator();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showFacingIndicator(unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private showFacingIndicator(unit: UnitData) {
|
||||||
|
this.clearFacingIndicator();
|
||||||
|
this.facingIndicatorUnitId = unit.id;
|
||||||
|
|
||||||
|
const view = this.unitViews.get(unit.id);
|
||||||
|
if (!view || unit.hp <= 0 || !this.isTileVisible(unit.x, unit.y)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = this.currentUnitDirection(unit);
|
||||||
|
const vector = this.directionVector(direction);
|
||||||
|
const x = view.sprite.x + vector.x * this.layout.tileSize * 0.34;
|
||||||
|
const y = view.sprite.y + vector.y * this.layout.tileSize * 0.34 - this.layout.tileSize * 0.28;
|
||||||
|
const marker = this.add.circle(x, y, 13, 0x101820, 0.86);
|
||||||
|
marker.setStrokeStyle(1, palette.gold, 0.88);
|
||||||
|
marker.setDepth(34);
|
||||||
|
|
||||||
|
const arrow = this.add.text(x, y - 1, this.directionSymbol(direction), {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '18px',
|
||||||
|
color: '#ffe28a',
|
||||||
|
fontStyle: '700',
|
||||||
|
stroke: '#05070a',
|
||||||
|
strokeThickness: 3
|
||||||
|
});
|
||||||
|
arrow.setOrigin(0.5);
|
||||||
|
arrow.setDepth(35);
|
||||||
|
|
||||||
|
const label = this.add.text(x, y + 16, this.unitDirectionLabel(direction), {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#f4dfad',
|
||||||
|
stroke: '#05070a',
|
||||||
|
strokeThickness: 3
|
||||||
|
});
|
||||||
|
label.setOrigin(0.5, 0);
|
||||||
|
label.setDepth(35);
|
||||||
|
|
||||||
|
if (this.mapMask) {
|
||||||
|
marker.setMask(this.mapMask);
|
||||||
|
arrow.setMask(this.mapMask);
|
||||||
|
label.setMask(this.mapMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.facingIndicatorObjects = [marker, arrow, label];
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearFacingIndicator() {
|
||||||
|
this.facingIndicatorObjects.forEach((object) => object.destroy());
|
||||||
|
this.facingIndicatorObjects = [];
|
||||||
|
this.facingIndicatorUnitId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private playUnitWalk(view: UnitView, direction: UnitDirection) {
|
private playUnitWalk(view: UnitView, direction: UnitDirection) {
|
||||||
view.direction = direction;
|
view.direction = direction;
|
||||||
this.stopUnitIdle(view);
|
this.stopUnitIdle(view);
|
||||||
@@ -9110,6 +9282,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private setUnitDirectionFrame(view: UnitView, direction: UnitDirection) {
|
private setUnitDirectionFrame(view: UnitView, direction: UnitDirection) {
|
||||||
view.sprite.setTexture(view.textureBase, this.unitFrameIndex(direction));
|
view.sprite.setTexture(view.textureBase, this.unitFrameIndex(direction));
|
||||||
view.sprite.setFlipX(false);
|
view.sprite.setFlipX(false);
|
||||||
|
this.applyBaseSpriteBlend(view.sprite);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setUnitBasePosition(view: UnitView, x: number, y: number) {
|
private setUnitBasePosition(view: UnitView, x: number, y: number) {
|
||||||
@@ -9327,6 +9500,15 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private setUnitActionFrame(sprite: Phaser.GameObjects.Sprite, unit: UnitData, direction: UnitDirection, pose: UnitActionPose, frame = 0) {
|
private setUnitActionFrame(sprite: Phaser.GameObjects.Sprite, unit: UnitData, direction: UnitDirection, pose: UnitActionPose, frame = 0) {
|
||||||
sprite.setTexture(this.unitActionTexture(unit), this.unitActionFrameIndex(direction, pose, frame));
|
sprite.setTexture(this.unitActionTexture(unit), this.unitActionFrameIndex(direction, pose, frame));
|
||||||
|
this.applyActionSpriteBlend(sprite);
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyActionSpriteBlend(sprite: Phaser.GameObjects.Sprite) {
|
||||||
|
sprite.setBlendMode(Phaser.BlendModes.SCREEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyBaseSpriteBlend(sprite: Phaser.GameObjects.Sprite) {
|
||||||
|
sprite.setBlendMode(Phaser.BlendModes.NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
private playUnitActionFrames(
|
private playUnitActionFrames(
|
||||||
@@ -9663,6 +9845,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private renderSituationPanel(message?: string) {
|
private renderSituationPanel(message?: string) {
|
||||||
this.clearSidePanelContent();
|
this.clearSidePanelContent();
|
||||||
|
this.setMiniMapVisible(true);
|
||||||
const { panelX, panelWidth } = this.layout;
|
const { panelX, panelWidth } = this.layout;
|
||||||
const left = panelX + 24;
|
const left = panelX + 24;
|
||||||
const width = panelWidth - 48;
|
const width = panelWidth - 48;
|
||||||
@@ -9943,6 +10126,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private renderRosterPanel(tab: RosterTab = this.rosterTab, message?: string) {
|
private renderRosterPanel(tab: RosterTab = this.rosterTab, message?: string) {
|
||||||
this.rosterTab = tab;
|
this.rosterTab = tab;
|
||||||
this.clearSidePanelContent();
|
this.clearSidePanelContent();
|
||||||
|
this.setMiniMapVisible(true);
|
||||||
|
|
||||||
const { panelX, panelY, panelWidth, panelHeight } = this.layout;
|
const { panelX, panelY, panelWidth, panelHeight } = this.layout;
|
||||||
const left = panelX + 24;
|
const left = panelX + 24;
|
||||||
@@ -10028,6 +10212,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private renderUnitDetail(unit: UnitData, message?: string) {
|
private renderUnitDetail(unit: UnitData, message?: string) {
|
||||||
this.clearSidePanelContent();
|
this.clearSidePanelContent();
|
||||||
|
this.setMiniMapVisible(false);
|
||||||
|
|
||||||
const { panelX, panelWidth } = this.layout;
|
const { panelX, panelWidth } = this.layout;
|
||||||
const left = panelX + 24;
|
const left = panelX + 24;
|
||||||
@@ -10039,6 +10224,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const terrain = battleMap.terrain[unit.y][unit.x];
|
const terrain = battleMap.terrain[unit.y][unit.x];
|
||||||
const terrainRule = getTerrainRule(terrain);
|
const terrainRule = getTerrainRule(terrain);
|
||||||
const terrainRating = unitClass.terrainRatings[terrain];
|
const terrainRating = unitClass.terrainRatings[terrain];
|
||||||
|
const direction = this.currentUnitDirection(unit);
|
||||||
|
|
||||||
const header = this.trackSideObject(this.add.rectangle(left, top, width, 60, 0x101820, 0.94));
|
const header = this.trackSideObject(this.add.rectangle(left, top, width, 60, 0x101820, 0.94));
|
||||||
header.setOrigin(0);
|
header.setOrigin(0);
|
||||||
@@ -10056,6 +10242,13 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
color: acted ? '#bdbdbd' : '#f2e3bf',
|
color: acted ? '#bdbdbd' : '#f2e3bf',
|
||||||
fontStyle: '700'
|
fontStyle: '700'
|
||||||
}));
|
}));
|
||||||
|
const growthText = this.trackSideObject(this.add.text(left + width - 14, top + 42, `Lv ${unit.level} EXP ${unit.exp}/100`, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: acted ? '#a8a8a8' : '#f4dfad',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
growthText.setOrigin(1, 0);
|
||||||
|
|
||||||
if (this.phase !== 'command') {
|
if (this.phase !== 'command') {
|
||||||
const backBg = this.trackSideObject(this.add.rectangle(left + width - 66, top + 10, 52, 28, 0x1b2834, 0.9));
|
const backBg = this.trackSideObject(this.add.rectangle(left + width - 66, top + 10, 52, 28, 0x1b2834, 0.9));
|
||||||
@@ -10089,31 +10282,40 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.drawGauge(left + 76, top + 84, width - 160, 10, unit.hp / unit.maxHp, 0x59d18c);
|
this.drawGauge(left + 76, top + 84, width - 160, 10, unit.hp / unit.maxHp, 0x59d18c);
|
||||||
|
|
||||||
const attackBonus = this.equipmentAttackBonus(unit);
|
const attackBonus = this.equipmentAttackBonus(unit);
|
||||||
this.renderSmallValueBox(left, top + 112, '공격', `${unit.attack + attackBonus}`);
|
const summaryGap = 6;
|
||||||
this.renderSmallValueBox(left + width / 2 + 6, top + 112, '이동', `${unit.move}`);
|
const summaryBoxWidth = (width - summaryGap * 2) / 3;
|
||||||
|
this.renderCompactValueBox(left, top + 112, summaryBoxWidth, '공격', `${unit.attack + attackBonus}`);
|
||||||
|
this.renderCompactValueBox(left + summaryBoxWidth + summaryGap, top + 112, summaryBoxWidth, '방어+', `${this.equipmentDefenseBonus(unit)}`);
|
||||||
|
this.renderCompactValueBox(
|
||||||
|
left + (summaryBoxWidth + summaryGap) * 2,
|
||||||
|
top + 112,
|
||||||
|
summaryBoxWidth,
|
||||||
|
'방향',
|
||||||
|
`${this.directionSymbol(direction)} ${this.unitDirectionLabel(direction)}`
|
||||||
|
);
|
||||||
|
|
||||||
const statTop = top + 158;
|
const statTop = top + 158;
|
||||||
statLabels.forEach((stat, index) => {
|
statLabels.forEach((stat, index) => {
|
||||||
this.renderStatRow(stat.label, unit.stats[stat.key], left, statTop + index * 28, width);
|
this.renderStatRow(stat.label, unit.stats[stat.key], left, statTop + index * 28, width);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (message) {
|
const positionText = `위치 ${unit.x + 1}, ${unit.y + 1} / ${terrainRule.label} ${terrainRating}% / 이동 ${unit.move}${acted ? ' / 행동완료' : ''}`;
|
||||||
this.renderPanelMessage(message, left, top + 306, width, 66);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const positionText = `위치 ${unit.x + 1}, ${unit.y + 1} / ${terrainRule.label} ${terrainRating}%${acted ? ' / 행동완료' : ''}`;
|
|
||||||
this.trackSideObject(this.add.text(left, top + 306, positionText, {
|
this.trackSideObject(this.add.text(left, top + 306, positionText, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '15px',
|
fontSize: '14px',
|
||||||
color: acted ? '#bdbdbd' : '#9fb0bf'
|
color: acted ? '#bdbdbd' : '#9fb0bf',
|
||||||
|
wordWrap: { width, useAdvancedWrap: true }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.renderEquipmentSummary(unit, left, top + 330, width);
|
this.renderEquipmentSummary(unit, left, top + 330, width);
|
||||||
|
if (message) {
|
||||||
|
this.renderPanelMessage(message, left, top + 444, width, 66);
|
||||||
|
}
|
||||||
|
this.showFacingIndicator(unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderEquipmentSummary(unit: UnitData, x: number, y: number, width: number) {
|
private renderEquipmentSummary(unit: UnitData, x: number, y: number, width: number) {
|
||||||
this.trackSideObject(this.add.text(x, y, '장비', {
|
this.trackSideObject(this.add.text(x, y, '장비 / 숙련도', {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
color: '#f2e3bf',
|
color: '#f2e3bf',
|
||||||
@@ -10216,23 +10418,22 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSmallValueBox(x: number, y: number, label: string, value: string) {
|
private renderCompactValueBox(x: number, y: number, width: number, label: string, value: string) {
|
||||||
const boxWidth = (this.layout.panelWidth - 60) / 2;
|
const bg = this.trackSideObject(this.add.rectangle(x, y, width, 38, 0x16212d, 0.92));
|
||||||
const bg = this.trackSideObject(this.add.rectangle(x, y, boxWidth, 38, 0x16212d, 0.92));
|
|
||||||
bg.setOrigin(0);
|
bg.setOrigin(0);
|
||||||
bg.setStrokeStyle(1, 0x53606c, 0.56);
|
bg.setStrokeStyle(1, 0x53606c, 0.56);
|
||||||
this.trackSideObject(this.add.text(x + 12, y + 9, label, {
|
this.trackSideObject(this.add.text(x + 8, y + 6, label, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '14px',
|
fontSize: '12px',
|
||||||
color: '#9fb0bf'
|
color: '#9fb0bf'
|
||||||
}));
|
}));
|
||||||
const valueText = this.trackSideObject(this.add.text(x + boxWidth - 12, y + 7, value, {
|
const valueText = this.trackSideObject(this.add.text(x + width - 8, y + 20, value, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '18px',
|
fontSize: '14px',
|
||||||
color: '#f2e3bf',
|
color: '#f2e3bf',
|
||||||
fontStyle: '700'
|
fontStyle: '700'
|
||||||
}));
|
}));
|
||||||
valueText.setOrigin(1, 0);
|
valueText.setOrigin(1, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderStatRow(label: string, value: number, x: number, y: number, width: number) {
|
private renderStatRow(label: string, value: number, x: number, y: number, width: number) {
|
||||||
@@ -10292,6 +10493,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private clearSidePanelContent() {
|
private clearSidePanelContent() {
|
||||||
this.sidePanelObjects.forEach((object) => object.destroy());
|
this.sidePanelObjects.forEach((object) => object.destroy());
|
||||||
this.sidePanelObjects = [];
|
this.sidePanelObjects = [];
|
||||||
|
this.clearFacingIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
private trackSideObject<T extends Phaser.GameObjects.GameObject>(object: T) {
|
private trackSideObject<T extends Phaser.GameObjects.GameObject>(object: T) {
|
||||||
@@ -10351,6 +10553,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
battleStats: this.serializeBattleStats(),
|
battleStats: this.serializeBattleStats(),
|
||||||
triggeredBattleEvents: Array.from(this.triggeredBattleEvents),
|
triggeredBattleEvents: Array.from(this.triggeredBattleEvents),
|
||||||
selectedUsable: this.selectedUsable?.id ?? null,
|
selectedUsable: this.selectedUsable?.id ?? null,
|
||||||
|
audio: soundDirector.getDebugState(),
|
||||||
itemStocks: this.serializeItemStocks(),
|
itemStocks: this.serializeItemStocks(),
|
||||||
battleBuffs: Array.from(this.battleBuffs.values()).map((buff) => ({ ...buff })),
|
battleBuffs: Array.from(this.battleBuffs.values()).map((buff) => ({ ...buff })),
|
||||||
treasureEffectPreviews: this.debugTreasureEffectPreviews(),
|
treasureEffectPreviews: this.debugTreasureEffectPreviews(),
|
||||||
@@ -10371,6 +10574,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
maxHp: unit.maxHp,
|
maxHp: unit.maxHp,
|
||||||
x: unit.x,
|
x: unit.x,
|
||||||
y: unit.y,
|
y: unit.y,
|
||||||
|
direction: this.unitViews.get(unit.id)?.direction ?? null,
|
||||||
acted: this.actedUnitIds.has(unit.id),
|
acted: this.actedUnitIds.has(unit.id),
|
||||||
animating: this.unitViews.get(unit.id)?.sprite.anims?.isPlaying ?? false,
|
animating: this.unitViews.get(unit.id)?.sprite.anims?.isPlaying ?? false,
|
||||||
animationKey: this.unitViews.get(unit.id)?.sprite.anims?.currentAnim?.key ?? null
|
animationKey: this.unitViews.get(unit.id)?.sprite.anims?.currentAnim?.key ?? null
|
||||||
|
|||||||
Reference in New Issue
Block a user