Improve battle status effect UI

This commit is contained in:
2026-07-03 21:46:57 +09:00
parent cc373f22e4
commit 69d9346c53

View File

@@ -1336,6 +1336,7 @@ type CombatResult = CombatPreview & {
defenderGrowth: EquipmentGrowthResult; defenderGrowth: EquipmentGrowthResult;
bondExp: number; bondExp: number;
bondGrowth: BondGrowthResult[]; bondGrowth: BondGrowthResult[];
statusApplied?: BattleStatusState;
counter?: CombatResult; counter?: CombatResult;
}; };
@@ -1352,6 +1353,9 @@ type BattleUsable = {
attackBonus?: number; attackBonus?: number;
hitBonus?: number; hitBonus?: number;
duration?: number; duration?: number;
statusEffect?: BattleStatusKind;
statusDuration?: number;
statusPower?: number;
description: string; description: string;
}; };
@@ -1397,6 +1401,16 @@ type SupportResult = {
equipmentGrowth: EquipmentGrowthResult; equipmentGrowth: EquipmentGrowthResult;
}; };
type BattleStatusKind = 'burn' | 'confusion';
type BattleStatusState = {
unitId: string;
kind: BattleStatusKind;
label: string;
turns: number;
power: number;
};
type BattleBuffState = { type BattleBuffState = {
unitId: string; unitId: string;
label: string; label: string;
@@ -1412,6 +1426,7 @@ type UnitEffectSummary = {
value: string; value: string;
tone: number; tone: number;
turns?: number; turns?: number;
polarity?: 'buff' | 'debuff' | 'terrain';
}; };
type PreviewModifierSummary = { type PreviewModifierSummary = {
@@ -1471,6 +1486,7 @@ type BattleSaveState = {
bonds: BondState[]; bonds: BondState[];
itemStocks?: Record<string, Record<string, number>>; itemStocks?: Record<string, Record<string, number>>;
battleBuffs?: BattleBuffState[]; battleBuffs?: BattleBuffState[];
battleStatuses?: BattleStatusState[];
battleStats?: Record<string, UnitBattleStats>; battleStats?: Record<string, UnitBattleStats>;
triggeredBattleEvents?: string[]; triggeredBattleEvents?: string[];
}; };
@@ -1521,6 +1537,9 @@ const usableCatalog: Record<string, BattleUsable> = {
power: 12, power: 12,
accuracyBonus: 6, accuracyBonus: 6,
criticalBonus: 0, criticalBonus: 0,
statusEffect: 'burn',
statusDuration: 2,
statusPower: 4,
description: '적 한 부대에 책략 피해를 준다.' description: '적 한 부대에 책략 피해를 준다.'
}, },
roar: { roar: {
@@ -1533,6 +1552,9 @@ const usableCatalog: Record<string, BattleUsable> = {
power: 9, power: 9,
accuracyBonus: 8, accuracyBonus: 8,
criticalBonus: 4, criticalBonus: 4,
statusEffect: 'confusion',
statusDuration: 1,
statusPower: 1,
description: '가까운 적을 위압해 피해를 준다.' description: '가까운 적을 위압해 피해를 준다.'
}, },
bean: { bean: {
@@ -3011,6 +3033,7 @@ export class BattleScene extends Phaser.Scene {
private bondStates = new Map<string, BondState>(); private bondStates = new Map<string, BondState>();
private itemStocks = new Map<string, Map<string, number>>(); private itemStocks = new Map<string, Map<string, number>>();
private battleBuffs = new Map<string, BattleBuffState>(); private battleBuffs = new Map<string, BattleBuffState>();
private battleStatuses = new Map<string, BattleStatusState[]>();
private attackIntents: AttackIntent[] = []; private attackIntents: AttackIntent[] = [];
private battleLog: string[] = []; private battleLog: string[] = [];
private enemyHomeTiles = new Map<string, { x: number; y: number }>(); private enemyHomeTiles = new Map<string, { x: number; y: number }>();
@@ -3068,6 +3091,8 @@ export class BattleScene extends Phaser.Scene {
this.bondStates = this.createBondStates(); this.bondStates = this.createBondStates();
this.itemStocks = this.createItemStocks(); this.itemStocks = this.createItemStocks();
this.battleBuffs.clear(); this.battleBuffs.clear();
this.battleStatuses.clear();
this.applyDebugStatusUiSetup();
this.enemyHomeTiles = this.createEnemyHomeTiles(); this.enemyHomeTiles = this.createEnemyHomeTiles();
this.battleStats = this.createBattleStats(); this.battleStats = this.createBattleStats();
this.triggeredBattleEvents.clear(); this.triggeredBattleEvents.clear();
@@ -3259,7 +3284,8 @@ export class BattleScene extends Phaser.Scene {
} }
private applyDebugBattleSetup() { private applyDebugBattleSetup() {
if (this.debugBattleSetupKey() !== 'attack-preview') { const setupKey = this.debugBattleSetupKey();
if (setupKey !== 'attack-preview' && setupKey !== 'status-ui') {
return; return;
} }
@@ -3279,6 +3305,34 @@ export class BattleScene extends Phaser.Scene {
target.hp = Math.max(1, target.hp); target.hp = Math.max(1, target.hp);
} }
private applyDebugStatusUiSetup() {
if (this.debugBattleSetupKey() !== 'status-ui') {
return;
}
const zhangFei = battleUnits.find((unit) => unit.id === 'zhang-fei' && unit.hp > 0);
if (zhangFei) {
this.battleBuffs.set(zhangFei.id, {
unitId: zhangFei.id,
label: '술',
turns: 2,
attackBonus: 3,
hitBonus: 6,
criticalBonus: 10
});
}
const guanYu = battleUnits.find((unit) => unit.id === 'guan-yu' && unit.hp > 0);
if (guanYu) {
this.applyBattleStatus(guanYu, 'burn', 2, 4);
}
const target = battleUnits.find((unit) => unit.id === 'rebel-a' && unit.hp > 0) ?? battleUnits.find((unit) => unit.faction === 'enemy' && unit.hp > 0);
if (target) {
this.applyBattleStatus(target, 'confusion', 1, 1);
}
}
private debugBattleSetupKey() { private debugBattleSetupKey() {
if (!this.debugToolsEnabled() || typeof window === 'undefined') { if (!this.debugToolsEnabled() || typeof window === 'undefined') {
return undefined; return undefined;
@@ -6650,6 +6704,14 @@ export class BattleScene extends Phaser.Scene {
}); });
} }
} }
this.unitStatuses(preview.attacker).forEach((status) => {
summaries.push({
icon: this.statusEffectIcon(status.kind),
label: status.label,
value: `${this.statusEffectShortText(status)} · ${status.turns}`,
tone: this.statusEffectTone(status.kind)
});
});
if (buff) { if (buff) {
summaries.push({ summaries.push({
icon: 'focus', icon: 'focus',
@@ -8623,6 +8685,15 @@ export class BattleScene extends Phaser.Scene {
const bondExp = bondGrowth.reduce((total, growth) => total + growth.amount, 0); const bondExp = bondGrowth.reduce((total, growth) => total + growth.amount, 0);
defender.hp = Math.max(0, defender.hp - damage); defender.hp = Math.max(0, defender.hp - damage);
const statusApplied =
hit && defender.hp > 0 && usable?.statusEffect
? this.applyBattleStatus(
defender,
usable.statusEffect,
usable.statusDuration ?? usable.duration ?? 1,
usable.statusPower ?? this.defaultStatusPower(usable.statusEffect)
)
: undefined;
this.recordCombatStats(attacker, defender, damage, defender.hp <= 0 && previousDefenderHp > 0); this.recordCombatStats(attacker, defender, damage, defender.hp <= 0 && previousDefenderHp > 0);
this.faceUnitToward(attacker, defender); this.faceUnitToward(attacker, defender);
if (hit) { if (hit) {
@@ -8651,7 +8722,8 @@ export class BattleScene extends Phaser.Scene {
attackerGrowth, attackerGrowth,
defenderGrowth: armorGrowth, defenderGrowth: armorGrowth,
bondExp, bondExp,
bondGrowth bondGrowth,
statusApplied
}; };
} }
@@ -8947,7 +9019,8 @@ export class BattleScene extends Phaser.Scene {
const critical = result.critical ? '치명 ' : ''; const critical = result.critical ? '치명 ' : '';
const defeated = result.defeated ? ' · 퇴각' : ''; const defeated = result.defeated ? ' · 퇴각' : '';
return `${critical}피해 ${result.damage}${defeated}`; const status = result.statusApplied ? ` · ${this.statusEffectMapText(result.statusApplied)}` : '';
return `${critical}피해 ${result.damage}${defeated}${status}`;
} }
private combatResultHeadline(result: CombatResult) { private combatResultHeadline(result: CombatResult) {
@@ -8961,7 +9034,8 @@ export class BattleScene extends Phaser.Scene {
: `${damage} 피해`; : `${damage} 피해`;
const hpLine = result.hit ? ` · HP ${result.previousDefenderHp}${result.defender.hp}` : ''; const hpLine = result.hit ? ` · HP ${result.previousDefenderHp}${result.defender.hp}` : '';
const defeated = result.defeated ? ' · 퇴각' : ''; const defeated = result.defeated ? ' · 퇴각' : '';
return `${result.attacker.name} ${this.combatActionName(result)}${result.defender.name}: ${outcome}${hpLine}${defeated}`; const status = result.statusApplied ? ` · ${this.statusEffectMapText(result.statusApplied)}` : '';
return `${result.attacker.name} ${this.combatActionName(result)}${result.defender.name}: ${outcome}${hpLine}${defeated}${status}`;
} }
private combatResolutionLine(result: CombatResult) { private combatResolutionLine(result: CombatResult) {
@@ -8969,7 +9043,8 @@ export class BattleScene extends Phaser.Scene {
const actual = this.combatDamageValue(result); const actual = this.combatDamageValue(result);
const verdict = result.hit ? `${successLabel} ${result.hitRate}% 통과` : `${successLabel} ${result.hitRate}% 실패`; const verdict = result.hit ? `${successLabel} ${result.hitRate}% 통과` : `${successLabel} ${result.hitRate}% 실패`;
const critical = result.hit ? (result.critical ? '치명 발생' : `치명 ${result.criticalRate}% 미발동`) : '치명 없음'; const critical = result.hit ? (result.critical ? '치명 발생' : `치명 ${result.criticalRate}% 미발동`) : '치명 없음';
return `판정: ${verdict} · ${critical} · 예측 ${result.expectedDamage} / 실제 ${actual}`; const status = result.statusApplied ? ` · 상태 ${this.statusEffectMapText(result.statusApplied)} 적용` : '';
return `판정: ${verdict} · ${critical} · 예측 ${result.expectedDamage} / 실제 ${actual}${status}`;
} }
private formatCombatOutcome(result: CombatResult) { private formatCombatOutcome(result: CombatResult) {
@@ -9744,6 +9819,7 @@ export class BattleScene extends Phaser.Scene {
})), })),
itemStocks: this.serializeItemStocks(), itemStocks: this.serializeItemStocks(),
battleBuffs: Array.from(this.battleBuffs.values()).map((buff) => ({ ...buff })), battleBuffs: Array.from(this.battleBuffs.values()).map((buff) => ({ ...buff })),
battleStatuses: this.serializeBattleStatuses(),
battleStats: this.serializeBattleStats(), battleStats: this.serializeBattleStats(),
triggeredBattleEvents: Array.from(this.triggeredBattleEvents) triggeredBattleEvents: Array.from(this.triggeredBattleEvents)
}; };
@@ -9781,6 +9857,7 @@ export class BattleScene extends Phaser.Scene {
); );
this.itemStocks = this.deserializeItemStocks(state.itemStocks); this.itemStocks = this.deserializeItemStocks(state.itemStocks);
this.battleBuffs = new Map((state.battleBuffs ?? []).map((buff) => [buff.unitId, { ...buff }])); this.battleBuffs = new Map((state.battleBuffs ?? []).map((buff) => [buff.unitId, { ...buff }]));
this.battleStatuses = this.deserializeBattleStatuses(state.battleStatuses);
this.battleStats = this.deserializeBattleStats(state.battleStats); this.battleStats = this.deserializeBattleStats(state.battleStats);
this.triggeredBattleEvents = new Set(state.triggeredBattleEvents ?? []); this.triggeredBattleEvents = new Set(state.triggeredBattleEvents ?? []);
@@ -9851,6 +9928,20 @@ export class BattleScene extends Phaser.Scene {
); );
} }
private serializeBattleStatuses() {
return Array.from(this.battleStatuses.values()).flatMap((statuses) => statuses.map((status) => ({ ...status })));
}
private deserializeBattleStatuses(savedStatuses?: BattleStatusState[]) {
const statuses = new Map<string, BattleStatusState[]>();
(savedStatuses ?? []).forEach((status) => {
const unitStatuses = statuses.get(status.unitId) ?? [];
unitStatuses.push({ ...status });
statuses.set(status.unitId, unitStatuses);
});
return statuses;
}
private serializeBattleStats() { private serializeBattleStats() {
return Object.fromEntries(Array.from(this.battleStats.entries()).map(([unitId, stats]) => [unitId, { ...stats }])); return Object.fromEntries(Array.from(this.battleStats.entries()).map(([unitId, stats]) => [unitId, { ...stats }]));
} }
@@ -9902,8 +9993,12 @@ export class BattleScene extends Phaser.Scene {
this.updateTurnText(); this.updateTurnText();
this.renderBattleSpeedControl(); this.renderBattleSpeedControl();
this.updateObjectiveTracker(); this.updateObjectiveTracker();
const statusMessage = this.tickBattleStatuses('enemy');
if (this.battleOutcome || this.resolveBattleOutcomeIfNeeded()) {
return;
}
const recoveryMessage = this.applyTerrainRecovery('enemy'); const recoveryMessage = this.applyTerrainRecovery('enemy');
this.renderSituationPanel(['아군 턴을 종료했습니다.', recoveryMessage, '적군이 행동을 시작합니다.'].filter(Boolean).join('\n')); this.renderSituationPanel(['아군 턴을 종료했습니다.', statusMessage, recoveryMessage, '적군이 행동을 시작합니다.'].filter(Boolean).join('\n'));
void this.runEnemyTurn(); void this.runEnemyTurn();
} }
@@ -12366,6 +12461,10 @@ export class BattleScene extends Phaser.Scene {
} }
this.turnNumber += 1; this.turnNumber += 1;
const statusMessage = this.tickBattleStatuses('ally');
if (this.battleOutcome || this.resolveBattleOutcomeIfNeeded()) {
return;
}
const expiredBuffMessage = this.tickBattleBuffs(); const expiredBuffMessage = this.tickBattleBuffs();
this.activeFaction = 'ally'; this.activeFaction = 'ally';
this.fastForwardHeld = false; this.fastForwardHeld = false;
@@ -12378,15 +12477,23 @@ export class BattleScene extends Phaser.Scene {
this.updateObjectiveTracker(); this.updateObjectiveTracker();
this.checkBattleEvents(); this.checkBattleEvents();
this.centerCameraOnAllyLine(); this.centerCameraOnAllyLine();
const turnMessage = [`${this.turnNumber}턴 아군 차례입니다.`, expiredBuffMessage, recoveryMessage, '행동할 장수를 선택하세요.'].filter(Boolean).join('\n'); const turnMessage = [`${this.turnNumber}턴 아군 차례입니다.`, statusMessage, expiredBuffMessage, recoveryMessage, '행동할 장수를 선택하세요.']
.filter(Boolean)
.join('\n');
this.renderRosterPanel('ally', turnMessage); this.renderRosterPanel('ally', turnMessage);
const firstUnit = this.firstActionableAlly(); const firstUnit = this.firstActionableAlly();
if (firstUnit) { if (firstUnit) {
this.renderSituationPanel([`${this.turnNumber}턴 아군 차례입니다.`, expiredBuffMessage, recoveryMessage, `다음 행동: ${firstUnit.name}`].filter(Boolean).join('\n')); this.renderSituationPanel(
[`${this.turnNumber}턴 아군 차례입니다.`, statusMessage, expiredBuffMessage, recoveryMessage, `다음 행동: ${firstUnit.name}`]
.filter(Boolean)
.join('\n')
);
this.focusActionableAlly( this.focusActionableAlly(
firstUnit, firstUnit,
[`${this.turnNumber}턴 아군 차례입니다.`, expiredBuffMessage, recoveryMessage, '이동할 파란 칸을 선택하세요.'].filter(Boolean).join('\n') [`${this.turnNumber}턴 아군 차례입니다.`, statusMessage, expiredBuffMessage, recoveryMessage, '이동할 파란 칸을 선택하세요.']
.filter(Boolean)
.join('\n')
); );
} }
} }
@@ -12861,15 +12968,144 @@ export class BattleScene extends Phaser.Scene {
} }
private attackBuff(unit: UnitData) { private attackBuff(unit: UnitData) {
return this.battleBuffs.get(unit.id)?.attackBonus ?? 0; return (this.battleBuffs.get(unit.id)?.attackBonus ?? 0) - this.statusAttackPenalty(unit);
} }
private hitBuff(unit: UnitData) { private hitBuff(unit: UnitData) {
return this.battleBuffs.get(unit.id)?.hitBonus ?? 0; return (this.battleBuffs.get(unit.id)?.hitBonus ?? 0) - this.statusHitPenalty(unit);
} }
private criticalBuff(unit: UnitData) { private criticalBuff(unit: UnitData) {
return this.battleBuffs.get(unit.id)?.criticalBonus ?? 0; return (this.battleBuffs.get(unit.id)?.criticalBonus ?? 0) - this.statusCriticalPenalty(unit);
}
private unitStatuses(unit: UnitData) {
return this.battleStatuses.get(unit.id) ?? [];
}
private hasBattleStatus(unit: UnitData, kind: BattleStatusKind) {
return this.unitStatuses(unit).some((status) => status.kind === kind && status.turns > 0);
}
private statusAttackPenalty(unit: UnitData) {
return this.hasBattleStatus(unit, 'confusion') ? 2 : 0;
}
private statusHitPenalty(unit: UnitData) {
return this.hasBattleStatus(unit, 'confusion') ? 12 : 0;
}
private statusCriticalPenalty(unit: UnitData) {
return this.hasBattleStatus(unit, 'confusion') ? 6 : 0;
}
private statusEffectLabel(kind: BattleStatusKind) {
return kind === 'burn' ? '화상' : '혼란';
}
private statusEffectIcon(kind: BattleStatusKind): BattleUiIconKey {
return kind === 'burn' ? 'fire' : 'confusion';
}
private statusEffectTone(kind: BattleStatusKind) {
return kind === 'burn' ? 0xd8732c : 0xb36dff;
}
private statusEffectShortText(status: BattleStatusState) {
if (status.kind === 'burn') {
return `지속 -${status.power}`;
}
return '공-2 명-12 치-6';
}
private statusEffectMapText(status?: BattleStatusState) {
return status ? `${status.label} ${status.turns}` : '';
}
private defaultStatusPower(kind: BattleStatusKind) {
return kind === 'burn' ? 4 : 1;
}
private applyBattleStatus(unit: UnitData, kind: BattleStatusKind, turns = 1, power = this.defaultStatusPower(kind)) {
const label = this.statusEffectLabel(kind);
const unitStatuses = [...this.unitStatuses(unit)];
const existing = unitStatuses.find((status) => status.kind === kind);
const nextStatus: BattleStatusState = {
unitId: unit.id,
kind,
label,
turns: Math.max(existing?.turns ?? 0, turns),
power: Math.max(existing?.power ?? 0, power)
};
if (existing) {
existing.turns = nextStatus.turns;
existing.power = nextStatus.power;
existing.label = nextStatus.label;
} else {
unitStatuses.push(nextStatus);
}
this.battleStatuses.set(unit.id, unitStatuses);
return { ...nextStatus };
}
private tickBattleStatuses(faction: ActiveFaction) {
const messages: string[] = [];
const units = battleUnits.filter((unit) => unit.faction === faction && unit.hp > 0);
units.forEach((unit) => {
const statuses = this.unitStatuses(unit);
if (statuses.length === 0) {
return;
}
const remaining: BattleStatusState[] = [];
for (const status of statuses) {
if (status.kind === 'burn' && unit.hp > 0) {
const previousHp = unit.hp;
const damage = Math.min(unit.hp, Math.max(1, status.power));
unit.hp = Math.max(0, unit.hp - damage);
messages.push(`${unit.name} ${status.label}: 지속 피해 ${damage} · HP ${previousHp}${unit.hp}`);
if (unit.hp <= 0) {
this.applyDefeatedStyle(unit);
} else {
this.syncUnitMotion(unit);
}
}
if (unit.hp <= 0) {
messages.push(`${unit.name} 전투 불능`);
break;
}
const nextTurns = status.turns - 1;
if (nextTurns > 0 && unit.hp > 0) {
remaining.push({ ...status, turns: nextTurns });
} else {
messages.push(`${unit.name} ${status.label} 해제`);
}
if (unit.hp <= 0) {
break;
}
}
if (remaining.length > 0) {
this.battleStatuses.set(unit.id, remaining);
} else {
this.battleStatuses.delete(unit.id);
}
});
if (messages.length === 0) {
return undefined;
}
this.updateMiniMap();
const message = `상태효과 변화\n${messages.slice(0, 4).join('\n')}${messages.length > 4 ? `\n외 ${messages.length - 4}` : ''}`;
this.pushBattleLog(message);
return message;
} }
private tickBattleBuffs() { private tickBattleBuffs() {
@@ -13440,6 +13676,8 @@ export class BattleScene extends Phaser.Scene {
} }
private applyDefeatedStyle(unit: UnitData, view = this.unitViews.get(unit.id)) { private applyDefeatedStyle(unit: UnitData, view = this.unitViews.get(unit.id)) {
this.battleBuffs.delete(unit.id);
this.battleStatuses.delete(unit.id);
if (!view) { if (!view) {
return; return;
} }
@@ -13536,13 +13774,19 @@ export class BattleScene extends Phaser.Scene {
return; return;
} }
const lines = [
this.combatMapDamageTitle(result),
`HP ${result.previousDefenderHp}${result.defender.hp}`,
this.statusEffectMapText(result.statusApplied)
].filter(Boolean);
this.showMapResultPopup( this.showMapResultPopup(
result.defender, result.defender,
[this.combatMapDamageTitle(result), `HP ${result.previousDefenderHp}${result.defender.hp}`], lines,
result.critical ? '#ff8f5f' : '#ffdf7b', result.critical ? '#ff8f5f' : '#ffdf7b',
'#220909', '#220909',
result.critical ? 20 : 18, result.critical ? 20 : 18,
result.critical ? 34 : 30 lines.length > 2 ? 38 : result.critical ? 34 : 30
); );
} }
@@ -13746,6 +13990,12 @@ export class BattleScene extends Phaser.Scene {
private battleLogVisual(entry: string): { icon: BattleUiIconKey; tone: number } { private battleLogVisual(entry: string): { icon: BattleUiIconKey; tone: number } {
const { text } = this.battleLogDisplayParts(entry); const { text } = this.battleLogDisplayParts(entry);
if (text.includes('화상') || text.includes('지속 피해')) {
return { icon: 'fire', tone: 0xd8732c };
}
if (text.includes('혼란') || text.includes('위압')) {
return { icon: 'confusion', tone: 0xb36dff };
}
if (text.includes('반격')) { if (text.includes('반격')) {
return { icon: 'counter', tone: 0xb64a45 }; return { icon: 'counter', tone: 0xb64a45 };
} }
@@ -13764,7 +14014,7 @@ export class BattleScene extends Phaser.Scene {
if (text.includes('회복') || text.includes('병력') || text.includes('거점')) { if (text.includes('회복') || text.includes('병력') || text.includes('거점')) {
return { icon: 'heal', tone: 0x59d18c }; return { icon: 'heal', tone: 0x59d18c };
} }
if (text.includes('강화') || text.includes('상태효과')) { if (text.includes('강화') || text.includes('상태효과') || text.includes('해제') || text.includes('만료')) {
return { icon: 'focus', tone: palette.gold }; return { icon: 'focus', tone: palette.gold };
} }
if (text.includes('이동') || text.includes('접근')) { if (text.includes('이동') || text.includes('접근')) {
@@ -14416,7 +14666,7 @@ export class BattleScene extends Phaser.Scene {
})); }));
const effectTop = top + (hasEffects ? 302 : 326); const effectTop = top + (hasEffects ? 302 : 326);
const equipmentTop = hasEffects ? top + 350 : effectTop; const equipmentTop = hasEffects ? top + 360 : effectTop;
if (hasEffects) { if (hasEffects) {
this.renderUnitEffectCards(effects, left, effectTop, width); this.renderUnitEffectCards(effects, left, effectTop, width);
} }
@@ -14441,13 +14691,24 @@ export class BattleScene extends Phaser.Scene {
terrainRule.moraleBonus ? `사기 +${terrainRule.moraleBonus}` : '' terrainRule.moraleBonus ? `사기 +${terrainRule.moraleBonus}` : ''
].filter(Boolean); ].filter(Boolean);
this.unitStatuses(unit).forEach((status) => {
effects.push({
icon: this.statusEffectIcon(status.kind),
label: status.label,
value: this.statusEffectShortText(status),
tone: this.statusEffectTone(status.kind),
turns: status.turns,
polarity: 'debuff'
});
});
if (buff) { if (buff) {
effects.push({ effects.push({
icon: 'focus', icon: 'focus',
label: buff.label, label: buff.label,
value: this.battleBuffShortText(buff), value: this.supportBuffCompactText(buff),
tone: palette.gold, tone: palette.gold,
turns: buff.turns turns: buff.turns,
polarity: 'buff'
}); });
} }
if (turnStartParts.length > 0) { if (turnStartParts.length > 0) {
@@ -14455,7 +14716,8 @@ export class BattleScene extends Phaser.Scene {
icon: 'terrain', icon: 'terrain',
label: `${terrainRule.label} 턴 시작`, label: `${terrainRule.label} 턴 시작`,
value: turnStartParts.join(' / '), value: turnStartParts.join(' / '),
tone: 0x59d18c tone: 0x59d18c,
polarity: 'terrain'
}); });
} }
@@ -14463,38 +14725,40 @@ export class BattleScene extends Phaser.Scene {
} }
private renderUnitEffectCards(effects: UnitEffectSummary[], x: number, y: number, width: number) { private renderUnitEffectCards(effects: UnitEffectSummary[], x: number, y: number, width: number) {
const visible = effects.slice(0, 2); const visible = effects.slice(0, 3);
const gap = 6; const gap = 6;
const cardWidth = (width - gap) / 2; const cardHeight = 50;
const cardWidth = (width - gap * (visible.length - 1)) / Math.max(1, visible.length);
visible.forEach((effect, index) => { visible.forEach((effect, index) => {
const cardX = x + index * (cardWidth + gap); const cardX = x + index * (cardWidth + gap);
const bg = this.trackSideObject(this.add.rectangle(cardX, y, cardWidth, 42, 0x101820, 0.97)); const fill = effect.polarity === 'debuff' ? 0x1d1516 : effect.polarity === 'terrain' ? 0x101d18 : 0x101820;
const bg = this.trackSideObject(this.add.rectangle(cardX, y, cardWidth, cardHeight, fill, 0.98));
bg.setOrigin(0); bg.setOrigin(0);
bg.setStrokeStyle(1, effect.tone, 0.62); bg.setStrokeStyle(1, effect.tone, effect.polarity === 'debuff' ? 0.78 : 0.62);
const iconFrame = this.trackSideObject(this.add.rectangle(cardX + 20, y + 21, 32, 32, 0x0a0f14, 0.92)); const iconFrame = this.trackSideObject(this.add.rectangle(cardX + 23, y + 25, 36, 36, 0x0a0f14, 0.94));
iconFrame.setStrokeStyle(1, effect.tone, 0.56); iconFrame.setStrokeStyle(1, effect.tone, 0.56);
this.trackSideIcon(cardX + 20, y + 21, effect.icon, 30); this.trackSideIcon(cardX + 23, y + 25, effect.icon, 34);
this.trackSideObject(this.add.text(cardX + 42, y + 4, effect.label, { this.trackSideObject(this.add.text(cardX + 47, y + 6, effect.label, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '10px', fontSize: '11px',
color: '#9fb0bf', color: effect.polarity === 'debuff' ? '#ffb9a8' : '#9fb0bf',
fontStyle: '700', fontStyle: '700',
fixedWidth: cardWidth - 52 fixedWidth: Math.max(36, cardWidth - 56)
})); }));
this.trackSideObject(this.add.text(cardX + 42, y + 20, this.truncateUiText(effect.value, 15), { this.trackSideObject(this.add.text(cardX + 47, y + 25, this.truncateUiText(effect.value, 15), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px', fontSize: '13px',
color: '#f2e3bf', color: '#f2e3bf',
fontStyle: '700', fontStyle: '700',
fixedWidth: cardWidth - 52 fixedWidth: Math.max(36, cardWidth - 56)
})); }));
if (effect.turns) { if (effect.turns) {
const badge = this.trackSideObject(this.add.rectangle(cardX + cardWidth - 18, y + 14, 26, 18, 0x211624, 0.96)); const badge = this.trackSideObject(this.add.rectangle(cardX + cardWidth - 20, y + 16, 30, 19, 0x211624, 0.96));
badge.setStrokeStyle(1, effect.tone, 0.78); badge.setStrokeStyle(1, effect.tone, 0.78);
const turnText = this.trackSideObject(this.add.text(cardX + cardWidth - 18, y + 14, `${effect.turns}`, { const turnText = this.trackSideObject(this.add.text(cardX + cardWidth - 20, y + 16, `${effect.turns}T`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '11px', fontSize: '10px',
color: '#fff2b8', color: '#fff2b8',
fontStyle: '700' fontStyle: '700'
})); }));
@@ -14909,6 +15173,7 @@ export class BattleScene extends Phaser.Scene {
audio: soundDirector.getDebugState(), 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 })),
battleStatuses: this.serializeBattleStatuses(),
treasureEffectPreviews: this.debugTreasureEffectPreviews(), treasureEffectPreviews: this.debugTreasureEffectPreviews(),
combatMechanicsProbe: this.debugCombatMechanicsProbe(), combatMechanicsProbe: this.debugCombatMechanicsProbe(),
units: battleUnits.map((unit) => ({ units: battleUnits.map((unit) => ({