feat: strengthen semantic battle feedback
This commit is contained in:
BIN
src/assets/audio/sfx/burn-tick.mp3
Normal file
BIN
src/assets/audio/sfx/burn-tick.mp3
Normal file
Binary file not shown.
BIN
src/assets/audio/sfx/recovery-cue.mp3
Normal file
BIN
src/assets/audio/sfx/recovery-cue.mp3
Normal file
Binary file not shown.
BIN
src/assets/audio/sfx/reward-reveal.mp3
Normal file
BIN
src/assets/audio/sfx/reward-reveal.mp3
Normal file
Binary file not shown.
@@ -57,7 +57,7 @@ export type MusicDuckOptions = {
|
||||
releaseMs?: number;
|
||||
};
|
||||
|
||||
type SemanticCueGroup = 'turn' | 'operation' | 'objective' | 'narrative';
|
||||
type SemanticCueGroup = 'turn' | 'operation' | 'objective' | 'narrative' | 'recovery' | 'status' | 'reward';
|
||||
|
||||
type MusicDuckState = {
|
||||
multiplier: number;
|
||||
@@ -103,7 +103,10 @@ export class SoundDirector {
|
||||
turn: 900,
|
||||
operation: 650,
|
||||
objective: 1200,
|
||||
narrative: 220
|
||||
narrative: 220,
|
||||
recovery: 1900,
|
||||
status: 280,
|
||||
reward: 1100
|
||||
};
|
||||
private readonly semanticCuePlayedAt = new Map<SemanticCueGroup, number>();
|
||||
private lastPlayedSemanticCue?: { group: SemanticCueGroup; key: string; at: number };
|
||||
@@ -442,6 +445,28 @@ export class SoundDirector {
|
||||
});
|
||||
}
|
||||
|
||||
playRecoveryCue() {
|
||||
return this.playSemanticCue('recovery', 'recovery-cue', {
|
||||
volume: 0.26,
|
||||
stopAfterMs: 1900
|
||||
});
|
||||
}
|
||||
|
||||
playBurnTickCue() {
|
||||
return this.playSemanticCue('status', 'burn-tick', {
|
||||
volume: 0.24,
|
||||
stopAfterMs: 680
|
||||
});
|
||||
}
|
||||
|
||||
playRewardRevealCue() {
|
||||
return this.playSemanticCue('reward', 'reward-reveal', {
|
||||
volume: 0.26,
|
||||
stopAfterMs: 1400,
|
||||
duck: { multiplier: 0.76, attackMs: 36, holdMs: 420, releaseMs: 260 }
|
||||
});
|
||||
}
|
||||
|
||||
playStrategyPulse() {
|
||||
this.playEffect('strategy-cast', { volume: 0.44, rate: 0.96, stopAfterMs: 960 });
|
||||
this.playTone(460, 0.08, 0.012, 'sine');
|
||||
|
||||
@@ -14,6 +14,7 @@ import arrowSwishUrl from '../../assets/audio/sfx/arrow-swish.mp3';
|
||||
import arrowWoodImpactUrl from '../../assets/audio/sfx/arrow-wood-impact.mp3';
|
||||
import bondResonanceUrl from '../../assets/audio/sfx/bond-resonance.wav';
|
||||
import bowReleaseUrl from '../../assets/audio/sfx/bow-release.mp3';
|
||||
import burnTickUrl from '../../assets/audio/sfx/burn-tick.mp3';
|
||||
import cartRollUrl from '../../assets/audio/sfx/cart-roll.mp3';
|
||||
import combatImpactUrl from '../../assets/audio/sfx/combat-impact.mp3';
|
||||
import criticalBoomUrl from '../../assets/audio/sfx/critical-boom.mp3';
|
||||
@@ -27,6 +28,8 @@ import levelUpUrl from '../../assets/audio/sfx/level-up.wav';
|
||||
import objectiveFailureUrl from '../../assets/audio/sfx/objective-failure.mp3';
|
||||
import objectiveSuccessUrl from '../../assets/audio/sfx/objective-success.mp3';
|
||||
import operationAlertUrl from '../../assets/audio/sfx/operation-alert.mp3';
|
||||
import recoveryCueUrl from '../../assets/audio/sfx/recovery-cue.mp3';
|
||||
import rewardRevealUrl from '../../assets/audio/sfx/reward-reveal.mp3';
|
||||
import shieldBlockUrl from '../../assets/audio/sfx/shield-block.mp3';
|
||||
import strategyCastUrl from '../../assets/audio/sfx/strategy-cast.mp3';
|
||||
import storyPageTurnUrl from '../../assets/audio/sfx/story-page-turn.mp3';
|
||||
@@ -69,6 +72,9 @@ export const effectTracks = {
|
||||
'operation-alert': operationAlertUrl,
|
||||
'objective-success': objectiveSuccessUrl,
|
||||
'objective-failure': objectiveFailureUrl,
|
||||
'recovery-cue': recoveryCueUrl,
|
||||
'burn-tick': burnTickUrl,
|
||||
'reward-reveal': rewardRevealUrl,
|
||||
'sword-slash': swordSlashUrl,
|
||||
'sword-swing-variant': swordSwingVariantUrl,
|
||||
'combat-impact': combatImpactUrl,
|
||||
|
||||
@@ -1401,11 +1401,21 @@ type TerrainPanelLayout = {
|
||||
messageBounds: HudBounds;
|
||||
};
|
||||
|
||||
type UnitStatusBadgeView = {
|
||||
container: Phaser.GameObjects.Container;
|
||||
background: Phaser.GameObjects.Arc;
|
||||
icon: Phaser.GameObjects.Image;
|
||||
turnsText: Phaser.GameObjects.Text;
|
||||
kind: BattleStatusKind;
|
||||
};
|
||||
|
||||
type UnitView = {
|
||||
sprite: Phaser.GameObjects.Sprite;
|
||||
hitZone: Phaser.GameObjects.Zone;
|
||||
label: Phaser.GameObjects.Text;
|
||||
roleSeal?: Phaser.GameObjects.Text;
|
||||
statusBadges: UnitStatusBadgeView[];
|
||||
statusBadgeSide: -1 | 1;
|
||||
textureBase: string;
|
||||
direction: UnitDirection;
|
||||
baseX: number;
|
||||
@@ -1426,6 +1436,8 @@ type DamageCommand = Exclude<BattleCommand, 'wait'>;
|
||||
type SpecialDamageMotionKind = 'item' | 'arcane' | 'fire' | 'roar';
|
||||
type RosterTab = 'ally' | 'enemy';
|
||||
type ActiveFaction = 'ally' | 'enemy';
|
||||
type BattleStatusTickResult = { message?: string; burnTicked: boolean };
|
||||
type TerrainRecoveryResult = { message?: string; applied: boolean };
|
||||
const battleSpeedOptions = ['normal', 'fast', 'very-fast'] as const;
|
||||
type BattleSpeed = (typeof battleSpeedOptions)[number];
|
||||
type MapMenuAction = 'endTurn' | 'threat' | 'save' | 'load' | 'roster' | 'bond' | 'situation' | 'speed' | 'presentation' | 'bgm' | 'close';
|
||||
@@ -5563,6 +5575,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
sprite,
|
||||
hitZone,
|
||||
label,
|
||||
statusBadges: [],
|
||||
statusBadgeSide: unit.faction === 'ally' ? 1 : -1,
|
||||
textureBase,
|
||||
direction: 'south' as UnitDirection,
|
||||
baseX: sprite.x,
|
||||
@@ -6238,6 +6252,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
const statusesCleared = this.battleStatuses.get(supportTarget.id)?.length ?? 0;
|
||||
supportTarget.hp += healAmount;
|
||||
this.battleStatuses.delete(supportTarget.id);
|
||||
this.syncUnitStatusBadges(supportTarget);
|
||||
this.tacticalCommand.effectValue = healAmount;
|
||||
this.tacticalCommand.statusesCleared = statusesCleared;
|
||||
this.tacticalCommand.effectPending = false;
|
||||
@@ -6253,7 +6268,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
const previousPhase: BattlePhase = this.phase === 'moving' ? 'moving' : 'idle';
|
||||
this.phase = 'animating';
|
||||
this.hideTacticalInitiativePanel();
|
||||
soundDirector.playStrategyPulse();
|
||||
if (role === 'support') {
|
||||
soundDirector.playRecoveryCue();
|
||||
} else {
|
||||
soundDirector.playStrategyPulse();
|
||||
}
|
||||
this.pushBattleLog(message);
|
||||
this.renderTacticalInitiativeChip();
|
||||
this.renderSortieOrderHud();
|
||||
@@ -7124,6 +7143,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
this.syncUnitRoleSeal(unit, view);
|
||||
this.syncUnitStatusBadges(unit, view);
|
||||
this.setUnitBasePosition(view, this.tileCenterX(unit.x), this.tileCenterY(unit.y));
|
||||
view.label.setPosition(view.baseX, view.baseY + this.layout.tileSize * 0.52);
|
||||
this.setUnitRoleSealPosition(view, view.baseX, view.baseY);
|
||||
@@ -7132,6 +7152,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.setUnitHitZoneEnabled(view, visible);
|
||||
view.label.setVisible(visible);
|
||||
view.roleSeal?.setVisible(visible);
|
||||
view.statusBadges.forEach((badge) => badge.container.setVisible(visible));
|
||||
this.syncUnitMotion(unit, view);
|
||||
this.applyUnitLegibilityStyle(unit, view);
|
||||
}
|
||||
@@ -7174,6 +7195,56 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.setUnitRoleSealPosition(view, view.baseX, view.baseY);
|
||||
}
|
||||
|
||||
private syncUnitStatusBadges(unit: UnitData, view = this.unitViews.get(unit.id)) {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statuses = unit.hp > 0 ? this.unitStatuses(unit).slice(0, 2) : [];
|
||||
while (view.statusBadges.length > statuses.length) {
|
||||
view.statusBadges.pop()?.container.destroy();
|
||||
}
|
||||
|
||||
statuses.forEach((status, index) => {
|
||||
let badge = view.statusBadges[index];
|
||||
const size = Math.max(this.battleUiLength(16), Math.round(this.layout.tileSize * 0.3));
|
||||
if (!badge?.container.active) {
|
||||
const container = this.add.container(0, 0);
|
||||
const background = this.add.circle(0, 0, size * 0.56, 0x071018, 0.96);
|
||||
const icon = this.createBattleUiIcon(0, 0, this.statusEffectIcon(status.kind), size);
|
||||
const turnsText = this.add.text(size * 0.42, size * 0.34, `${status.turns}`, {
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
fontSize: this.battleUiFontSize(7),
|
||||
color: '#fff8e8',
|
||||
fontStyle: '700',
|
||||
backgroundColor: 'rgba(57, 14, 10, 0.96)',
|
||||
stroke: '#120704',
|
||||
strokeThickness: this.battleUiLength(1),
|
||||
padding: { x: this.battleUiLength(1), y: 0 }
|
||||
});
|
||||
turnsText.setOrigin(0.5);
|
||||
container.add([background, icon, turnsText]);
|
||||
container.setDepth(11.22);
|
||||
container.setName(`unit-status-${unit.id}-${index}`);
|
||||
if (this.mapMask) {
|
||||
container.setMask(this.mapMask);
|
||||
}
|
||||
badge = { container, background, icon, turnsText, kind: status.kind };
|
||||
view.statusBadges[index] = badge;
|
||||
}
|
||||
|
||||
const tone = this.statusEffectTone(status.kind);
|
||||
badge.kind = status.kind;
|
||||
badge.background.setStrokeStyle(this.battleUiLength(2), tone, 0.96);
|
||||
badge.icon.setFrame(battleUiIconFrames[this.statusEffectIcon(status.kind)]);
|
||||
badge.turnsText.setText(`${status.turns}`);
|
||||
});
|
||||
|
||||
this.setUnitStatusBadgePositions(view, view.baseX, view.baseY);
|
||||
const visible = unit.hp > 0 && this.isTileVisible(unit.x, unit.y);
|
||||
view.statusBadges.forEach((badge) => badge.container.setVisible(visible));
|
||||
}
|
||||
|
||||
private refreshUnitLegibilityStyles() {
|
||||
battleUnits.forEach((unit) => this.applyUnitLegibilityStyle(unit));
|
||||
}
|
||||
@@ -7201,6 +7272,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
label.setBackgroundColor(this.unitLabelBackground(unit));
|
||||
});
|
||||
view.roleSeal?.setVisible(visible).setAlpha(1);
|
||||
this.syncUnitStatusBadges(unit, view);
|
||||
view.statusBadges.forEach((badge) => badge.container.setVisible(visible).setAlpha(1));
|
||||
}
|
||||
|
||||
private unitLabelBackground(unit: UnitData) {
|
||||
@@ -8944,7 +9017,6 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.refreshEnemyIntentForecast();
|
||||
this.showOpeningBattleEvent();
|
||||
this.scheduleFirstBattleTutorial();
|
||||
soundDirector.playSelect();
|
||||
}
|
||||
|
||||
private deploymentSignature() {
|
||||
@@ -18777,12 +18849,18 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.updateTurnText();
|
||||
this.renderBattleSpeedControl();
|
||||
this.updateObjectiveTracker();
|
||||
const statusMessage = this.tickBattleStatuses('enemy');
|
||||
if (this.battleOutcome || this.resolveBattleOutcomeIfNeeded()) {
|
||||
const statusResult = this.tickBattleStatuses('enemy', false);
|
||||
const statusMessage = statusResult.message;
|
||||
const statusOutcomeDelayMs = statusResult.burnTicked ? 720 : 0;
|
||||
if (this.battleOutcome || this.resolveBattleOutcomeIfNeeded(statusOutcomeDelayMs)) {
|
||||
if (statusResult.burnTicked) {
|
||||
soundDirector.playBurnTickCue();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const recoveryMessage = this.applyTerrainRecovery('enemy');
|
||||
soundDirector.playEnemyTurnCue();
|
||||
const recoveryResult = this.applyTerrainRecovery('enemy', false);
|
||||
const recoveryMessage = recoveryResult.message;
|
||||
this.playTurnStartFeedbackCue('enemy', statusResult.burnTicked, recoveryResult.applied);
|
||||
this.renderSituationPanel(['아군 턴을 종료했습니다.', statusMessage, recoveryMessage, '적군이 행동을 시작합니다.'].filter(Boolean).join('\n'));
|
||||
void this.runEnemyTurn();
|
||||
}
|
||||
@@ -19854,10 +19932,14 @@ export class BattleScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
soundDirector.playEffect(result.usable.command === 'strategy' ? 'strategy-cast' : 'exp-gain', {
|
||||
volume: result.usable.command === 'strategy' ? 0.3 : 0.24,
|
||||
stopAfterMs: 620
|
||||
});
|
||||
if (result.usable.effect === 'heal') {
|
||||
soundDirector.playRecoveryCue();
|
||||
} else {
|
||||
soundDirector.playEffect(result.usable.command === 'strategy' ? 'strategy-cast' : 'exp-gain', {
|
||||
volume: result.usable.command === 'strategy' ? 0.3 : 0.24,
|
||||
stopAfterMs: 620
|
||||
});
|
||||
}
|
||||
const ring = this.add.circle(targetX, targetY - this.layout.tileSize * 0.18, this.layout.tileSize * 0.3);
|
||||
ring.setStrokeStyle(4, accent, 0.92);
|
||||
ring.setDepth(12.8);
|
||||
@@ -20629,10 +20711,14 @@ export class BattleScene extends Phaser.Scene {
|
||||
effect.setDepth(depth + 9);
|
||||
this.tweens.add({ targets: effect, scale: 1.45, alpha: 0.18, duration: 520, yoyo: true });
|
||||
this.showSortieSupportCombatEffect(result, left + panelWidth / 2, top + 156, depth + 10);
|
||||
soundDirector.playEffect(result.usable.command === 'strategy' ? 'strategy-cast' : 'exp-gain', {
|
||||
volume: result.usable.command === 'strategy' ? 0.38 : 0.28,
|
||||
stopAfterMs: 900
|
||||
});
|
||||
if (result.usable.effect === 'heal') {
|
||||
soundDirector.playRecoveryCue();
|
||||
} else {
|
||||
soundDirector.playEffect(result.usable.command === 'strategy' ? 'strategy-cast' : 'exp-gain', {
|
||||
volume: result.usable.command === 'strategy' ? 0.38 : 0.28,
|
||||
stopAfterMs: 900
|
||||
});
|
||||
}
|
||||
|
||||
const targetStatus = this.renderCombatStatusPanel(result.target, left + panelWidth / 2 - 196, top + 202, 392);
|
||||
targetStatus.hpFill.setScale(result.previousTargetHp / result.target.maxHp, 1);
|
||||
@@ -22780,6 +22866,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (view.roleSeal) {
|
||||
this.tweens.killTweensOf(view.roleSeal);
|
||||
}
|
||||
view.statusBadges.forEach((badge) => this.tweens.killTweensOf(badge.container));
|
||||
this.setUnitBasePosition(view, startX, startY);
|
||||
this.resetUnitSpritePose(view);
|
||||
view.label.setPosition(startX, startY + this.layout.tileSize * 0.52);
|
||||
@@ -22816,6 +22903,16 @@ export class BattleScene extends Phaser.Scene {
|
||||
ease: 'Sine.easeInOut'
|
||||
});
|
||||
}
|
||||
view.statusBadges.forEach((badge, index) => {
|
||||
const position = this.unitStatusBadgePosition(view, targetX, targetY, index);
|
||||
this.tweens.add({
|
||||
targets: badge.container,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
duration: movementDuration,
|
||||
ease: 'Sine.easeInOut'
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22825,8 +22922,13 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
this.turnNumber += 1;
|
||||
const statusMessage = this.tickBattleStatuses('ally');
|
||||
if (this.battleOutcome || this.resolveBattleOutcomeIfNeeded()) {
|
||||
const statusResult = this.tickBattleStatuses('ally', false);
|
||||
const statusMessage = statusResult.message;
|
||||
const statusOutcomeDelayMs = statusResult.burnTicked ? 720 : 0;
|
||||
if (this.battleOutcome || this.resolveBattleOutcomeIfNeeded(statusOutcomeDelayMs)) {
|
||||
if (statusResult.burnTicked) {
|
||||
soundDirector.playBurnTickCue();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const expiredBuffMessage = this.tickBattleBuffs();
|
||||
@@ -22834,16 +22936,16 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.fastForwardHeld = false;
|
||||
this.actedUnitIds.clear();
|
||||
this.attackIntents = this.emptyAllyTurnAttackIntents();
|
||||
const recoveryMessage = this.applyTerrainRecovery('ally');
|
||||
const recoveryResult = this.applyTerrainRecovery('ally', false);
|
||||
const recoveryMessage = recoveryResult.message;
|
||||
this.resetActedStyles();
|
||||
this.updateTurnText();
|
||||
this.renderBattleSpeedControl();
|
||||
this.updateObjectiveTracker();
|
||||
const eventCountBeforeTurnCheck = this.triggeredBattleEvents.size;
|
||||
this.checkBattleEvents();
|
||||
if (this.triggeredBattleEvents.size === eventCountBeforeTurnCheck) {
|
||||
soundDirector.playAllyTurnCue();
|
||||
}
|
||||
const operationCuePlayed = this.triggeredBattleEvents.size !== eventCountBeforeTurnCheck;
|
||||
this.playTurnStartFeedbackCue('ally', statusResult.burnTicked, recoveryResult.applied, operationCuePlayed);
|
||||
this.centerCameraOnAllyLine();
|
||||
const turnMessage = [`${this.turnNumber}턴 아군 차례입니다.`, statusMessage, expiredBuffMessage, recoveryMessage, '행동할 장수를 선택하세요.']
|
||||
.filter(Boolean)
|
||||
@@ -22871,7 +22973,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
return [];
|
||||
}
|
||||
|
||||
private applyTerrainRecovery(faction: ActiveFaction) {
|
||||
private applyTerrainRecovery(faction: ActiveFaction, playCue = true): TerrainRecoveryResult {
|
||||
const messages: string[] = [];
|
||||
|
||||
battleUnits
|
||||
@@ -22902,13 +23004,40 @@ export class BattleScene extends Phaser.Scene {
|
||||
});
|
||||
|
||||
if (messages.length === 0) {
|
||||
return undefined;
|
||||
return { applied: false };
|
||||
}
|
||||
|
||||
soundDirector.playEffect('exp-gain', { volume: 0.18, stopAfterMs: 520 });
|
||||
if (playCue) {
|
||||
soundDirector.playRecoveryCue();
|
||||
}
|
||||
const message = `거점 효과\n${messages.slice(0, 3).join('\n')}${messages.length > 3 ? `\n외 ${messages.length - 3}명` : ''}`;
|
||||
this.pushBattleLog(message);
|
||||
return message;
|
||||
return { message, applied: true };
|
||||
}
|
||||
|
||||
private playTurnStartFeedbackCue(
|
||||
faction: ActiveFaction,
|
||||
burnTicked: boolean,
|
||||
recoveryApplied: boolean,
|
||||
operationCuePlayed = false
|
||||
) {
|
||||
if (operationCuePlayed) {
|
||||
return 'operation';
|
||||
}
|
||||
if (burnTicked) {
|
||||
soundDirector.playBurnTickCue();
|
||||
return 'status';
|
||||
}
|
||||
if (recoveryApplied) {
|
||||
soundDirector.playRecoveryCue();
|
||||
return 'recovery';
|
||||
}
|
||||
if (faction === 'ally') {
|
||||
soundDirector.playAllyTurnCue();
|
||||
} else {
|
||||
soundDirector.playEnemyTurnCue();
|
||||
}
|
||||
return 'turn';
|
||||
}
|
||||
|
||||
private turnStartEquipmentRecovery(unit: UnitData) {
|
||||
@@ -23516,12 +23645,14 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
this.battleStatuses.set(unit.id, unitStatuses);
|
||||
this.syncUnitStatusBadges(unit);
|
||||
return { ...nextStatus };
|
||||
}
|
||||
|
||||
private tickBattleStatuses(faction: ActiveFaction) {
|
||||
private tickBattleStatuses(faction: ActiveFaction, playCue = true): BattleStatusTickResult {
|
||||
const messages: string[] = [];
|
||||
const units = battleUnits.filter((unit) => unit.faction === faction && unit.hp > 0);
|
||||
let burnTicked = false;
|
||||
|
||||
units.forEach((unit) => {
|
||||
const statuses = this.unitStatuses(unit);
|
||||
@@ -23535,10 +23666,25 @@ export class BattleScene extends Phaser.Scene {
|
||||
const previousHp = unit.hp;
|
||||
const damage = Math.min(unit.hp, Math.max(1, status.power));
|
||||
unit.hp = Math.max(0, unit.hp - damage);
|
||||
burnTicked = true;
|
||||
messages.push(`${unit.name} ${status.label}: 지속 피해 ${damage} · HP ${previousHp}→${unit.hp}`);
|
||||
const visibleOnMap = this.isTileVisible(unit.x, unit.y);
|
||||
if (visibleOnMap) {
|
||||
this.showMapResultPopup(
|
||||
unit,
|
||||
[`${status.label} -${damage}`, `HP ${previousHp}→${unit.hp}`],
|
||||
'#ffb07c',
|
||||
'#2b0b05',
|
||||
17,
|
||||
30
|
||||
);
|
||||
}
|
||||
if (unit.hp <= 0) {
|
||||
this.applyDefeatedStyle(unit);
|
||||
} else {
|
||||
if (visibleOnMap) {
|
||||
this.flashDamage(unit, previousHp);
|
||||
}
|
||||
this.syncUnitMotion(unit);
|
||||
}
|
||||
}
|
||||
@@ -23565,16 +23711,20 @@ export class BattleScene extends Phaser.Scene {
|
||||
} else {
|
||||
this.battleStatuses.delete(unit.id);
|
||||
}
|
||||
this.syncUnitStatusBadges(unit);
|
||||
});
|
||||
|
||||
if (messages.length === 0) {
|
||||
return undefined;
|
||||
return { burnTicked: false };
|
||||
}
|
||||
|
||||
if (playCue && burnTicked) {
|
||||
soundDirector.playBurnTickCue();
|
||||
}
|
||||
this.updateMiniMap();
|
||||
const message = `상태효과 변화\n${messages.slice(0, 4).join('\n')}${messages.length > 4 ? `\n외 ${messages.length - 4}건` : ''}`;
|
||||
this.pushBattleLog(message);
|
||||
return message;
|
||||
return { message, burnTicked };
|
||||
}
|
||||
|
||||
private tickBattleBuffs() {
|
||||
@@ -23697,6 +23847,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (view.roleSeal) {
|
||||
this.tweens.killTweensOf(view.roleSeal);
|
||||
}
|
||||
view.statusBadges.forEach((badge) => this.tweens.killTweensOf(badge.container));
|
||||
this.setUnitBasePosition(view, startX, startY);
|
||||
this.resetUnitSpritePose(view);
|
||||
view.label.setPosition(startX, startY + this.layout.tileSize * 0.52);
|
||||
@@ -23732,6 +23883,16 @@ export class BattleScene extends Phaser.Scene {
|
||||
ease: 'Sine.easeInOut'
|
||||
});
|
||||
}
|
||||
view.statusBadges.forEach((badge, index) => {
|
||||
const position = this.unitStatusBadgePosition(view, targetX, targetY, index);
|
||||
this.tweens.add({
|
||||
targets: badge.container,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
duration: movementDuration,
|
||||
ease: 'Sine.easeInOut'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private movementTileDistance(unit: UnitData, x: number, y: number) {
|
||||
@@ -23973,6 +24134,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
view.sprite.setPosition(x, y);
|
||||
view.hitZone.setPosition(x, y);
|
||||
view.hitZone.setSize(this.layout.tileSize, this.layout.tileSize);
|
||||
this.setUnitStatusBadgePositions(view, x, y);
|
||||
}
|
||||
|
||||
private setUnitRoleSealPosition(view: UnitView, x: number, y: number) {
|
||||
@@ -23982,6 +24144,21 @@ export class BattleScene extends Phaser.Scene {
|
||||
);
|
||||
}
|
||||
|
||||
private setUnitStatusBadgePositions(view: UnitView, x: number, y: number) {
|
||||
view.statusBadges.forEach((badge, index) => {
|
||||
const position = this.unitStatusBadgePosition(view, x, y, index);
|
||||
badge.container.setPosition(position.x, position.y);
|
||||
});
|
||||
}
|
||||
|
||||
private unitStatusBadgePosition(view: UnitView, x: number, y: number, index: number) {
|
||||
const spacing = this.layout.tileSize * 0.29;
|
||||
return {
|
||||
x: x + view.statusBadgeSide * (this.layout.tileSize * 0.34 + index * spacing),
|
||||
y: y - this.layout.tileSize * 0.38
|
||||
};
|
||||
}
|
||||
|
||||
private setUnitHitZoneEnabled(view: UnitView, enabled: boolean) {
|
||||
view.hitZone.setVisible(enabled);
|
||||
if (view.hitZone.input) {
|
||||
@@ -24283,6 +24460,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
this.syncUnitStatusBadges(unit, view);
|
||||
|
||||
view.sprite.stop();
|
||||
view.sprite.disableInteractive();
|
||||
@@ -24297,6 +24475,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
label.setBackgroundColor('');
|
||||
});
|
||||
view.roleSeal?.setAlpha(0).setVisible(false);
|
||||
view.statusBadges.forEach((badge) => badge.container.setAlpha(0).setVisible(false));
|
||||
}
|
||||
|
||||
private faceUnitToward(unit: UnitData, target: UnitData) {
|
||||
@@ -24462,7 +24641,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
private showMapResultPopup(unit: UnitData, lines: string[], color: string, stroke: string, fontSize: number, lift: number) {
|
||||
const view = this.unitViews.get(unit.id);
|
||||
if (!view) {
|
||||
if (!view || !this.isTileVisible(unit.x, unit.y)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -28012,6 +28191,13 @@ export class BattleScene extends Phaser.Scene {
|
||||
roleSealPresent: Boolean(this.unitViews.get(unit.id)?.roleSeal),
|
||||
roleSealText: this.unitViews.get(unit.id)?.roleSeal?.text ?? null,
|
||||
roleSealVisible: this.unitViews.get(unit.id)?.roleSeal?.visible ?? false,
|
||||
statusBadges: (this.unitViews.get(unit.id)?.statusBadges ?? []).map((badge) => ({
|
||||
kind: badge.kind,
|
||||
visible: badge.container.visible,
|
||||
x: badge.container.x,
|
||||
y: badge.container.y,
|
||||
turns: Number.parseInt(badge.turnsText.text, 10) || 0
|
||||
})),
|
||||
level: unit.level,
|
||||
exp: unit.exp,
|
||||
hp: unit.hp,
|
||||
|
||||
@@ -13352,6 +13352,7 @@ export class CampScene extends Phaser.Scene {
|
||||
});
|
||||
this.victoryRewardActions.push({ id: definition.id, label: definition.label, button });
|
||||
});
|
||||
soundDirector.playRewardRevealCue();
|
||||
}
|
||||
|
||||
private completeVictoryRewardAcknowledgement(
|
||||
@@ -22875,7 +22876,7 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
this.campaign = saveCampaignState(campaign);
|
||||
this.report = this.campaign.firstBattleReport ?? this.report;
|
||||
soundDirector.playEffect('exp-gain', { volume: 0.24, stopAfterMs: 620 });
|
||||
soundDirector.playRecoveryCue();
|
||||
|
||||
const hpText = hpGain > 0 ? `병력 +${hpGain}` : '';
|
||||
const bondText = bondGain > 0 ? `연결 공명 총 +${bondGain}` : '';
|
||||
|
||||
Reference in New Issue
Block a user