Polish battle result feedback
This commit is contained in:
BIN
docs/result-feedback-local-map-popup.png
Normal file
BIN
docs/result-feedback-local-map-popup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
BIN
docs/result-feedback-local-post-attack.png
Normal file
BIN
docs/result-feedback-local-post-attack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
BIN
docs/result-feedback-local-target-preview-card.png
Normal file
BIN
docs/result-feedback-local-target-preview-card.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
BIN
docs/result-feedback-prod-map-popup.png
Normal file
BIN
docs/result-feedback-prod-map-popup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
BIN
docs/result-feedback-prod-post-attack.png
Normal file
BIN
docs/result-feedback-prod-post-attack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
BIN
docs/result-feedback-prod-target-preview.png
Normal file
BIN
docs/result-feedback-prod-target-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
31
docs/result-feedback-report.md
Normal file
31
docs/result-feedback-report.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Battle Result Feedback Verification
|
||||||
|
|
||||||
|
Date: 2026-07-03
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Reworked post-action combat feedback for attack, strategy, item, heal, buff, miss, critical, counter, and defeat summaries.
|
||||||
|
- Kept map feedback short and off the unit body.
|
||||||
|
- Added compact icon result cards in the unit detail panel when the next unit is auto-selected after an action.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
- `pnpm build`: pass
|
||||||
|
- `pnpm deploy:nas`: pass
|
||||||
|
|
||||||
|
## Browser Checks
|
||||||
|
- Local: `http://127.0.0.1:4195/heros_web/?debug&debugBattleSetup=attack-preview&v=result-feedback-local-card`
|
||||||
|
- Deployed: `https://comtropy.synology.me/heros_web/?debug&debugBattleSetup=attack-preview&v=result-feedback-20260703`
|
||||||
|
- PC viewport: 1280x720
|
||||||
|
- Console errors: 0 local, 0 deployed
|
||||||
|
|
||||||
|
## Captures
|
||||||
|
- Local target preview: `docs/result-feedback-local-target-preview-card.png`
|
||||||
|
- Local map popup: `docs/result-feedback-local-map-popup.png`
|
||||||
|
- Local post-attack card: `docs/result-feedback-local-post-attack.png`
|
||||||
|
- Deployed target preview: `docs/result-feedback-prod-target-preview.png`
|
||||||
|
- Deployed map popup: `docs/result-feedback-prod-map-popup.png`
|
||||||
|
- Deployed post-attack card: `docs/result-feedback-prod-post-attack.png`
|
||||||
|
|
||||||
|
## Result
|
||||||
|
- Attack result shows `퇴각 -19 / HP 19→0` as a short floating map popup.
|
||||||
|
- The right detail panel shows a compact icon card with `관우 → 황건병 · 공격` and `피해 19 · 퇴각 · HP 19→0`.
|
||||||
|
- No selection ring, base, or large badge was added.
|
||||||
@@ -6545,6 +6545,11 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
await this.delay(180);
|
await this.delay(180);
|
||||||
await this.playCombatCutIn(result.counter);
|
await this.playCombatCutIn(result.counter);
|
||||||
}
|
}
|
||||||
|
this.showCombatMapResult(result);
|
||||||
|
if (result.counter) {
|
||||||
|
this.showCombatMapResult(result.counter);
|
||||||
|
}
|
||||||
|
await this.delay(result.counter ? 220 : 160);
|
||||||
this.finishUnitAction(attacker, this.formatCombatResult(result));
|
this.finishUnitAction(attacker, this.formatCombatResult(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6569,6 +6574,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const result = this.resolveSupportAction(user, target, usable);
|
const result = this.resolveSupportAction(user, target, usable);
|
||||||
this.clearMarkers();
|
this.clearMarkers();
|
||||||
await this.playSupportCutIn(result);
|
await this.playSupportCutIn(result);
|
||||||
|
this.showSupportMapResult(result);
|
||||||
|
await this.delay(160);
|
||||||
this.finishUnitAction(user, this.formatSupportResult(result));
|
this.finishUnitAction(user, this.formatSupportResult(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8619,9 +8626,9 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
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) {
|
||||||
this.flashDamage(defender, damage, critical);
|
this.flashDamage(defender, damage, critical, previousDefenderHp);
|
||||||
} else {
|
} else {
|
||||||
this.flashMiss(defender);
|
this.flashMiss(defender, this.missFeedbackLabel(action, usable));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defender.hp <= 0) {
|
if (defender.hp <= 0) {
|
||||||
@@ -8684,7 +8691,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
if (healAmount > 0) {
|
if (healAmount > 0) {
|
||||||
target.hp = Math.min(target.maxHp, target.hp + healAmount);
|
target.hp = Math.min(target.maxHp, target.hp + healAmount);
|
||||||
this.syncUnitMotion(target);
|
this.syncUnitMotion(target);
|
||||||
this.flashSupport(target, `+${healAmount}`, '#a8ffd0');
|
this.flashSupport(target, `회복 +${healAmount}`, '#a8ffd0', `HP ${previousTargetHp}→${target.hp}`);
|
||||||
this.updateMiniMap();
|
this.updateMiniMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8698,7 +8705,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
criticalBonus: usable.criticalBonus ?? 0
|
criticalBonus: usable.criticalBonus ?? 0
|
||||||
};
|
};
|
||||||
this.battleBuffs.set(target.id, buff);
|
this.battleBuffs.set(target.id, buff);
|
||||||
this.flashSupport(target, usable.name, '#ffdf7b');
|
this.flashSupport(target, `강화 ${usable.duration ?? 1}턴`, '#ffdf7b', this.supportBuffCompactText(buff));
|
||||||
}
|
}
|
||||||
|
|
||||||
const equipmentGrowth = this.awardEquipmentExp(user, 'accessory', this.supportEquipmentExpGain(user, usable));
|
const equipmentGrowth = this.awardEquipmentExp(user, 'accessory', this.supportEquipmentExpGain(user, usable));
|
||||||
@@ -8893,7 +8900,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private formatCombatResult(result: CombatResult) {
|
private formatCombatResult(result: CombatResult) {
|
||||||
const headline = this.combatResultHeadline(result);
|
const headline = this.combatResultSummaryLine(result);
|
||||||
const resolution = this.combatResolutionLine(result);
|
const resolution = this.combatResolutionLine(result);
|
||||||
const summaryBondBonus = result.bondDamageBonus > 0 && result.bondLabel ? `\n공명 효과 ${result.bondLabel}: 피해 +${result.bondDamageBonus}%` : '';
|
const summaryBondBonus = result.bondDamageBonus > 0 && result.bondLabel ? `\n공명 효과 ${result.bondLabel}: 피해 +${result.bondDamageBonus}%` : '';
|
||||||
const summaryEquipment = result.equipmentEffectLabels.length > 0 ? `\n장비 효과: ${result.equipmentEffectLabels.join(', ')}` : '';
|
const summaryEquipment = result.equipmentEffectLabels.length > 0 ? `\n장비 효과: ${result.equipmentEffectLabels.join(', ')}` : '';
|
||||||
@@ -8907,10 +8914,42 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return result.isCounter ? '반격' : result.usable?.name ?? commandLabels[result.action];
|
return result.isCounter ? '반격' : result.usable?.name ?? commandLabels[result.action];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private combatActionLogName(result: CombatResult) {
|
||||||
|
if (result.isCounter) {
|
||||||
|
return '반격';
|
||||||
|
}
|
||||||
|
if (result.usable) {
|
||||||
|
return result.usable.name;
|
||||||
|
}
|
||||||
|
if (result.action === 'strategy') {
|
||||||
|
return '책략';
|
||||||
|
}
|
||||||
|
if (result.action === 'item') {
|
||||||
|
return '도구';
|
||||||
|
}
|
||||||
|
return '공격';
|
||||||
|
}
|
||||||
|
|
||||||
private combatDamageValue(result: CombatResult) {
|
private combatDamageValue(result: CombatResult) {
|
||||||
return result.hit ? result.damage : 0;
|
return result.hit ? result.damage : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private combatResultSummaryLine(result: CombatResult) {
|
||||||
|
const hpLine = result.hit ? ` · HP ${result.previousDefenderHp}→${result.defender.hp}` : '';
|
||||||
|
const counter = !result.isCounter && result.counter ? ' · 반격 발생' : '';
|
||||||
|
return `${result.attacker.name} → ${result.defender.name} | ${this.combatActionLogName(result)} | ${this.combatOutcomeSummary(result)}${hpLine}${counter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private combatOutcomeSummary(result: CombatResult) {
|
||||||
|
if (!result.hit) {
|
||||||
|
return this.missFeedbackLabel(result.action, result.usable);
|
||||||
|
}
|
||||||
|
|
||||||
|
const critical = result.critical ? '치명 ' : '';
|
||||||
|
const defeated = result.defeated ? ' · 퇴각' : '';
|
||||||
|
return `${critical}피해 ${result.damage}${defeated}`;
|
||||||
|
}
|
||||||
|
|
||||||
private combatResultHeadline(result: CombatResult) {
|
private combatResultHeadline(result: CombatResult) {
|
||||||
const damage = this.combatDamageValue(result);
|
const damage = this.combatDamageValue(result);
|
||||||
const outcome = !result.hit
|
const outcome = !result.hit
|
||||||
@@ -8926,11 +8965,11 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private combatResolutionLine(result: CombatResult) {
|
private combatResolutionLine(result: CombatResult) {
|
||||||
const successLabel = result.action === 'strategy' ? '성공' : '명중';
|
const successLabel = result.action === 'strategy' || result.usable?.command === 'strategy' ? '성공률' : '명중률';
|
||||||
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.critical ? '치명 발생' : `치명 ${result.criticalRate}%`;
|
const critical = result.hit ? (result.critical ? '치명 발생' : `치명 ${result.criticalRate}% 미발동`) : '치명 없음';
|
||||||
return `판정: ${verdict} · ${critical} · 예상 ${result.expectedDamage}/실제 ${actual}`;
|
return `판정: ${verdict} · ${critical} · 예측 ${result.expectedDamage} / 실제 ${actual}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatCombatOutcome(result: CombatResult) {
|
private formatCombatOutcome(result: CombatResult) {
|
||||||
@@ -8943,10 +8982,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private formatCounterLine(result: CombatResult) {
|
private formatCounterLine(result: CombatResult) {
|
||||||
const counterOutcome = result.hit ? `${result.critical ? '치명 ' : ''}${result.damage} 피해` : '회피됨';
|
return `반격 결과: ${this.combatResultSummaryLine(result)}`;
|
||||||
const hpLine = result.hit ? ` · HP ${result.previousDefenderHp}→${result.defender.hp}` : '';
|
|
||||||
const defeated = result.defeated ? ' · 퇴각' : '';
|
|
||||||
return `반격: ${result.attacker.name} → ${result.defender.name}: ${counterOutcome}${hpLine}${defeated}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private combatGrowthLines(result: CombatResult) {
|
private combatGrowthLines(result: CombatResult) {
|
||||||
@@ -8971,15 +9007,36 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private formatSupportResult(result: SupportResult) {
|
private formatSupportResult(result: SupportResult) {
|
||||||
const supportEffect =
|
const supportEffect =
|
||||||
result.usable.effect === 'heal'
|
result.usable.effect === 'heal'
|
||||||
? `${result.target.name} 병력 +${result.healAmount} (${result.target.hp}/${result.target.maxHp})`
|
? `결과: 회복 +${result.healAmount} · HP ${result.previousTargetHp}→${result.target.hp}`
|
||||||
: `${result.target.name} 강화: 공격 +${result.buff?.attackBonus ?? 0} / 명중 +${result.buff?.hitBonus ?? 0} / 치명 +${result.buff?.criticalBonus ?? 0}`;
|
: `결과: 강화 ${result.buff?.turns ?? result.usable.duration ?? 1}턴 · ${this.supportBuffCompactText(result.buff)}`;
|
||||||
return `${result.user.name} ${result.usable.name}\n${supportEffect}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.equipmentGrowth)}`;
|
return `${this.supportResultSummaryLine(result)}\n${supportEffect}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.equipmentGrowth)}`;
|
||||||
|
|
||||||
const effect =
|
}
|
||||||
result.usable.effect === 'heal'
|
|
||||||
? `${result.target.name} 병력 +${result.healAmount} (${result.target.hp}/${result.target.maxHp})`
|
private supportResultSummaryLine(result: SupportResult) {
|
||||||
: `${result.target.name} 강화: 공격 +${result.buff?.attackBonus ?? 0} / 명중 +${result.buff?.hitBonus ?? 0} / 치명 +${result.buff?.criticalBonus ?? 0}`;
|
if (result.usable.effect === 'heal') {
|
||||||
return `${result.user.name} ${result.usable.name}\n${effect}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.equipmentGrowth)}`;
|
return `${result.user.name} → ${result.target.name} | ${result.usable.name} | 회복 +${result.healAmount} · HP ${result.previousTargetHp}→${result.target.hp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${result.user.name} → ${result.target.name} | ${result.usable.name} | 강화 ${result.buff?.turns ?? result.usable.duration ?? 1}턴 · ${this.supportBuffCompactText(result.buff)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private supportBuffCompactText(buff?: BattleBuffState) {
|
||||||
|
if (!buff) {
|
||||||
|
return '강화 유지';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [`공+${buff.attackBonus}`, `명+${buff.hitBonus}`, `치+${buff.criticalBonus}`].join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private missFeedbackLabel(action: DamageCommand, usable?: BattleUsable) {
|
||||||
|
if (action === 'strategy' || usable?.command === 'strategy') {
|
||||||
|
return '책략 실패';
|
||||||
|
}
|
||||||
|
if (action === 'item' || usable?.command === 'item') {
|
||||||
|
return '효과 실패';
|
||||||
|
}
|
||||||
|
return '회피';
|
||||||
}
|
}
|
||||||
|
|
||||||
private commandResultMessage(unit: UnitData, command: BattleCommand) {
|
private commandResultMessage(unit: UnitData, command: BattleCommand) {
|
||||||
@@ -9922,6 +9979,11 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
await this.delay(180);
|
await this.delay(180);
|
||||||
await this.playCombatCutIn(result.counter);
|
await this.playCombatCutIn(result.counter);
|
||||||
}
|
}
|
||||||
|
this.showCombatMapResult(result);
|
||||||
|
if (result.counter) {
|
||||||
|
this.showCombatMapResult(result.counter);
|
||||||
|
}
|
||||||
|
await this.delay(result.counter ? 220 : 160);
|
||||||
return this.formatEnemyCombatResult(result, behavior);
|
return this.formatEnemyCombatResult(result, behavior);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10179,10 +10241,18 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private formatEnemyCombatResult(result: CombatResult, behavior: EnemyAiBehavior) {
|
private formatEnemyCombatResult(result: CombatResult, behavior: EnemyAiBehavior) {
|
||||||
const outcome = result.hit ? `${result.critical ? '치명타 ' : ''}${result.damage} 피해` : '회피됨';
|
const counter = result.counter ? `\n${this.formatCounterLine(result.counter)}` : '';
|
||||||
const defeated = result.defeated ? ` / ${result.defender.name} 퇴각` : ` / ${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`;
|
return `${this.combatResultSummaryLine(result)} · ${this.enemyBehaviorResultLabel(behavior)}${counter}`;
|
||||||
const counter = result.counter ? ` / ${this.formatCounterLine(result.counter)}` : '';
|
}
|
||||||
return `${result.attacker.name} ${this.enemyBehaviorLabel(behavior)} 공격: ${result.defender.name} ${outcome}${defeated}${counter}`;
|
|
||||||
|
private enemyBehaviorResultLabel(behavior: EnemyAiBehavior) {
|
||||||
|
if (behavior === 'aggressive') {
|
||||||
|
return '추격';
|
||||||
|
}
|
||||||
|
if (behavior === 'hold') {
|
||||||
|
return '고수';
|
||||||
|
}
|
||||||
|
return '경계';
|
||||||
}
|
}
|
||||||
|
|
||||||
private async playCombatCutIn(result: CombatResult) {
|
private async playCombatCutIn(result: CombatResult) {
|
||||||
@@ -13397,22 +13467,15 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.syncUnitMotion(unit, view, direction);
|
this.syncUnitMotion(unit, view, direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
private flashDamage(unit: UnitData, damage: number, critical = false) {
|
private flashDamage(unit: UnitData, damage: number, critical = false, previousHp?: number) {
|
||||||
const view = this.unitViews.get(unit.id);
|
const view = this.unitViews.get(unit.id);
|
||||||
if (!view) {
|
if (!view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const popup = this.add.text(view.sprite.x, view.sprite.y - this.layout.tileSize * 0.5, critical ? `치명 -${damage}` : `-${damage}`, {
|
const title = unit.hp <= 0 ? `${critical ? '치명 ' : ''}퇴각 -${damage}` : `${critical ? '치명 ' : '피해 '}-${damage}`;
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
const detail = previousHp === undefined ? '' : `HP ${previousHp}→${unit.hp}`;
|
||||||
fontSize: critical ? '22px' : '20px',
|
this.showMapResultPopup(unit, [title, detail], critical ? '#ff8f5f' : '#ffdf7b', '#220909', critical ? 20 : 18, critical ? 34 : 30);
|
||||||
color: critical ? '#ff8f5f' : '#ffdf7b',
|
|
||||||
stroke: '#220909',
|
|
||||||
strokeThickness: 4,
|
|
||||||
fontStyle: '700'
|
|
||||||
});
|
|
||||||
popup.setOrigin(0.5);
|
|
||||||
popup.setDepth(30);
|
|
||||||
|
|
||||||
view.sprite.setTintFill(0xffe0a3);
|
view.sprite.setTintFill(0xffe0a3);
|
||||||
this.time.delayedCall(120, () => {
|
this.time.delayedCall(120, () => {
|
||||||
@@ -13424,33 +13487,15 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.applyUnitLegibilityStyle(unit, view);
|
this.applyUnitLegibilityStyle(unit, view);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tweens.add({
|
|
||||||
targets: popup,
|
|
||||||
y: popup.y - 28,
|
|
||||||
alpha: 0,
|
|
||||||
duration: 520,
|
|
||||||
ease: 'Sine.easeOut',
|
|
||||||
onComplete: () => popup.destroy()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private flashMiss(unit: UnitData) {
|
private flashMiss(unit: UnitData, label = '회피') {
|
||||||
const view = this.unitViews.get(unit.id);
|
const view = this.unitViews.get(unit.id);
|
||||||
if (!view) {
|
if (!view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const popup = this.add.text(view.sprite.x, view.sprite.y - this.layout.tileSize * 0.5, '회피', {
|
this.showMapResultPopup(unit, [label], '#d9f1ff', '#071623', label.length > 4 ? 17 : 19, 26);
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
||||||
fontSize: '20px',
|
|
||||||
color: '#d9f1ff',
|
|
||||||
stroke: '#071623',
|
|
||||||
strokeThickness: 4,
|
|
||||||
fontStyle: '700'
|
|
||||||
});
|
|
||||||
popup.setOrigin(0.5);
|
|
||||||
popup.setDepth(30);
|
|
||||||
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: [view.sprite, view.label],
|
targets: [view.sprite, view.label],
|
||||||
@@ -13459,33 +13504,15 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
yoyo: true,
|
yoyo: true,
|
||||||
ease: 'Sine.easeOut'
|
ease: 'Sine.easeOut'
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tweens.add({
|
|
||||||
targets: popup,
|
|
||||||
y: popup.y - 24,
|
|
||||||
alpha: 0,
|
|
||||||
duration: 500,
|
|
||||||
ease: 'Sine.easeOut',
|
|
||||||
onComplete: () => popup.destroy()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private flashSupport(unit: UnitData, label: string, color: string) {
|
private flashSupport(unit: UnitData, label: string, color: string, detail = '') {
|
||||||
const view = this.unitViews.get(unit.id);
|
const view = this.unitViews.get(unit.id);
|
||||||
if (!view) {
|
if (!view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const popup = this.add.text(view.sprite.x, view.sprite.y - this.layout.tileSize * 0.5, label, {
|
this.showMapResultPopup(unit, [label, detail], color, '#071623', 18, 28);
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
||||||
fontSize: '20px',
|
|
||||||
color,
|
|
||||||
stroke: '#071623',
|
|
||||||
strokeThickness: 4,
|
|
||||||
fontStyle: '700'
|
|
||||||
});
|
|
||||||
popup.setOrigin(0.5);
|
|
||||||
popup.setDepth(30);
|
|
||||||
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: view.sprite,
|
targets: view.sprite,
|
||||||
@@ -13494,12 +13521,91 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
yoyo: true,
|
yoyo: true,
|
||||||
ease: 'Sine.easeOut'
|
ease: 'Sine.easeOut'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private showCombatMapResult(result: CombatResult) {
|
||||||
|
if (!result.hit) {
|
||||||
|
this.showMapResultPopup(
|
||||||
|
result.defender,
|
||||||
|
[this.missFeedbackLabel(result.action, result.usable)],
|
||||||
|
'#d9f1ff',
|
||||||
|
'#071623',
|
||||||
|
19,
|
||||||
|
26
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showMapResultPopup(
|
||||||
|
result.defender,
|
||||||
|
[this.combatMapDamageTitle(result), `HP ${result.previousDefenderHp}→${result.defender.hp}`],
|
||||||
|
result.critical ? '#ff8f5f' : '#ffdf7b',
|
||||||
|
'#220909',
|
||||||
|
result.critical ? 20 : 18,
|
||||||
|
result.critical ? 34 : 30
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private combatMapDamageTitle(result: CombatResult) {
|
||||||
|
const damage = `-${result.damage}`;
|
||||||
|
if (result.defeated) {
|
||||||
|
return `${result.critical ? '치명 ' : ''}퇴각 ${damage}`;
|
||||||
|
}
|
||||||
|
return `${result.critical ? '치명 ' : '피해 '}${damage}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private showSupportMapResult(result: SupportResult) {
|
||||||
|
if (result.usable.effect === 'heal') {
|
||||||
|
this.showMapResultPopup(
|
||||||
|
result.target,
|
||||||
|
[`회복 +${result.healAmount}`, `HP ${result.previousTargetHp}→${result.target.hp}`],
|
||||||
|
'#a8ffd0',
|
||||||
|
'#071623',
|
||||||
|
18,
|
||||||
|
28
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showMapResultPopup(
|
||||||
|
result.target,
|
||||||
|
[`강화 ${result.buff?.turns ?? result.usable.duration ?? 1}턴`, this.supportBuffCompactText(result.buff)],
|
||||||
|
'#ffdf7b',
|
||||||
|
'#071623',
|
||||||
|
18,
|
||||||
|
28
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private showMapResultPopup(unit: UnitData, lines: string[], color: string, stroke: string, fontSize: number, lift: number) {
|
||||||
|
const view = this.unitViews.get(unit.id);
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const popup = this.add.text(
|
||||||
|
view.sprite.x,
|
||||||
|
view.sprite.y - this.layout.tileSize * 0.64,
|
||||||
|
lines.filter(Boolean).join('\n'),
|
||||||
|
{
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: `${fontSize}px`,
|
||||||
|
color,
|
||||||
|
stroke,
|
||||||
|
strokeThickness: 4,
|
||||||
|
fontStyle: '700',
|
||||||
|
align: 'center',
|
||||||
|
lineSpacing: -3
|
||||||
|
}
|
||||||
|
);
|
||||||
|
popup.setOrigin(0.5, 1);
|
||||||
|
popup.setDepth(35);
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: popup,
|
targets: popup,
|
||||||
y: popup.y - 24,
|
y: popup.y - lift,
|
||||||
alpha: 0,
|
alpha: 0,
|
||||||
duration: 560,
|
duration: 680,
|
||||||
|
delay: 80,
|
||||||
ease: 'Sine.easeOut',
|
ease: 'Sine.easeOut',
|
||||||
onComplete: () => popup.destroy()
|
onComplete: () => popup.destroy()
|
||||||
});
|
});
|
||||||
@@ -13592,11 +13698,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private compactBattleLogEntry(message: string) {
|
private compactBattleLogEntry(message: string) {
|
||||||
const firstLine = message
|
const firstLine = this.firstMessageLine(message);
|
||||||
.split('\n')
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.find(Boolean);
|
|
||||||
|
|
||||||
if (!firstLine) {
|
if (!firstLine) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -13605,6 +13707,31 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return this.truncateBattleLogText(normalized, 42);
|
return this.truncateBattleLogText(normalized, 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private firstMessageLine(message: string) {
|
||||||
|
return (
|
||||||
|
message
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.find(Boolean) ?? ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private actionResultSummaryParts(line: string) {
|
||||||
|
const parts = line
|
||||||
|
.split('|')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (parts.length < 3 || !parts[0].includes('→')) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subject: parts[0],
|
||||||
|
action: parts[1],
|
||||||
|
outcome: parts.slice(2).join(' · ')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private truncateBattleLogText(text: string, maxLength: number) {
|
private truncateBattleLogText(text: string, maxLength: number) {
|
||||||
return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 3))}...` : text;
|
return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 3))}...` : text;
|
||||||
}
|
}
|
||||||
@@ -13619,21 +13746,27 @@ 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('반격')) {
|
||||||
|
return { icon: 'counter', tone: 0xb64a45 };
|
||||||
|
}
|
||||||
if (text.includes('퇴각') || text.includes('격파') || text.includes('전투 불능')) {
|
if (text.includes('퇴각') || text.includes('격파') || text.includes('전투 불능')) {
|
||||||
return { icon: 'critical', tone: 0xd8732c };
|
return { icon: 'critical', tone: 0xd8732c };
|
||||||
}
|
}
|
||||||
if (text.includes('치명')) {
|
if (text.includes('치명')) {
|
||||||
return { icon: 'critical', tone: 0xd8732c };
|
return { icon: 'critical', tone: 0xd8732c };
|
||||||
}
|
}
|
||||||
if (text.includes('반격')) {
|
if (text.includes('책략 실패') || text.includes('효과 실패') || text.includes('회피')) {
|
||||||
return { icon: 'counter', tone: 0xb64a45 };
|
return { icon: 'move', tone: 0x83d6ff };
|
||||||
}
|
}
|
||||||
if (text.includes('책략') || text.includes('화계') || text.includes('교란') || text.includes('고무')) {
|
if (text.includes('책략') || text.includes('화계') || text.includes('교란') || text.includes('고무')) {
|
||||||
return { icon: 'strategy', tone: palette.blue };
|
return { icon: 'strategy', tone: palette.blue };
|
||||||
}
|
}
|
||||||
if (text.includes('회복') || 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('상태효과')) {
|
||||||
|
return { icon: 'focus', tone: palette.gold };
|
||||||
|
}
|
||||||
if (text.includes('이동') || text.includes('접근')) {
|
if (text.includes('이동') || text.includes('접근')) {
|
||||||
return { icon: 'move', tone: 0x83d6ff };
|
return { icon: 'move', tone: 0x83d6ff };
|
||||||
}
|
}
|
||||||
@@ -13678,7 +13811,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
countText.setOrigin(1, 0);
|
countText.setOrigin(1, 0);
|
||||||
countText.setDepth(31);
|
countText.setDepth(31);
|
||||||
|
|
||||||
const entries = this.battleLog.slice(0, 4);
|
const entries = this.battleLog.slice(0, 3);
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
const emptyText = this.trackSideObject(this.add.text(x + 12, y + 45, '아직 기록된 행동이 없습니다.', {
|
const emptyText = this.trackSideObject(this.add.text(x + 12, y + 45, '아직 기록된 행동이 없습니다.', {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
@@ -13690,21 +13823,21 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entries.forEach((entry, index) => {
|
entries.forEach((entry, index) => {
|
||||||
const rowY = y + 32 + index * 20;
|
const rowY = y + 32 + index * 28;
|
||||||
const { turn, text } = this.battleLogDisplayParts(entry);
|
const { turn, text } = this.battleLogDisplayParts(entry);
|
||||||
const visual = this.battleLogVisual(entry);
|
const visual = this.battleLogVisual(entry);
|
||||||
const row = this.trackSideObject(this.add.rectangle(x + 8, rowY, width - 16, 18, index === 0 ? 0x17232e : 0x101820, index === 0 ? 0.98 : 0.86));
|
const row = this.trackSideObject(this.add.rectangle(x + 8, rowY, width - 16, 24, index === 0 ? 0x17232e : 0x101820, index === 0 ? 0.98 : 0.88));
|
||||||
row.setOrigin(0);
|
row.setOrigin(0);
|
||||||
row.setDepth(31);
|
row.setDepth(31);
|
||||||
row.setStrokeStyle(1, visual.tone, index === 0 ? 0.62 : 0.28);
|
row.setStrokeStyle(1, visual.tone, index === 0 ? 0.72 : 0.34);
|
||||||
|
|
||||||
const iconFrame = this.trackSideObject(this.add.rectangle(x + 21, rowY + 9, 18, 18, 0x0a0f14, 0.96));
|
const iconFrame = this.trackSideObject(this.add.rectangle(x + 23, rowY + 12, 22, 22, 0x0a0f14, 0.96));
|
||||||
iconFrame.setDepth(32);
|
iconFrame.setDepth(32);
|
||||||
iconFrame.setStrokeStyle(1, visual.tone, 0.5);
|
iconFrame.setStrokeStyle(1, visual.tone, index === 0 ? 0.72 : 0.52);
|
||||||
const icon = this.trackSideIcon(x + 21, rowY + 9, visual.icon, 17);
|
const icon = this.trackSideIcon(x + 23, rowY + 12, visual.icon, 21);
|
||||||
icon.setDepth(33);
|
icon.setDepth(33);
|
||||||
|
|
||||||
const turnText = this.trackSideObject(this.add.text(x + width - 12, rowY + 3, turn, {
|
const turnText = this.trackSideObject(this.add.text(x + width - 12, rowY + 6, turn, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
color: '#9fb0bf',
|
color: '#9fb0bf',
|
||||||
@@ -13713,12 +13846,12 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
turnText.setOrigin(1, 0);
|
turnText.setOrigin(1, 0);
|
||||||
turnText.setDepth(32);
|
turnText.setDepth(32);
|
||||||
|
|
||||||
const eventText = this.trackSideObject(this.add.text(x + 36, rowY + 2, this.truncateBattleLogText(text, 32), {
|
const eventText = this.trackSideObject(this.add.text(x + 42, rowY + 5, this.truncateBattleLogText(text, index === 0 ? 39 : 34), {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '12px',
|
fontSize: index === 0 ? '13px' : '12px',
|
||||||
color: index === 0 ? '#e8dfca' : '#c8d2dd',
|
color: index === 0 ? '#e8dfca' : '#c8d2dd',
|
||||||
fontStyle: index === 0 ? '700' : '400',
|
fontStyle: index === 0 ? '700' : '400',
|
||||||
fixedWidth: width - 84
|
fixedWidth: width - 92
|
||||||
}));
|
}));
|
||||||
eventText.setDepth(32);
|
eventText.setDepth(32);
|
||||||
});
|
});
|
||||||
@@ -14289,7 +14422,12 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
this.renderEquipmentSummary(unit, left, equipmentTop, width, hasEffects);
|
this.renderEquipmentSummary(unit, left, equipmentTop, width, hasEffects);
|
||||||
if (message) {
|
if (message) {
|
||||||
this.renderPanelMessage(message, left, equipmentTop + 156, width, 42);
|
const resultLine = this.actionResultSummaryParts(this.firstMessageLine(message));
|
||||||
|
if (resultLine) {
|
||||||
|
this.renderPanelActionResultMessage(resultLine, left, equipmentTop + 150, width);
|
||||||
|
} else {
|
||||||
|
this.renderPanelMessage(message, left, equipmentTop + 156, width, 42);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.showFacingIndicator(unit);
|
this.showFacingIndicator(unit);
|
||||||
}
|
}
|
||||||
@@ -14622,6 +14760,43 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
fill.setOrigin(0);
|
fill.setOrigin(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderPanelActionResultMessage(
|
||||||
|
result: { subject: string; action: string; outcome: string },
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number
|
||||||
|
) {
|
||||||
|
const height = 54;
|
||||||
|
const safeY = Math.max(this.sideContentTop(), Math.min(y, this.sideContentBottom() - height));
|
||||||
|
const visual = this.battleLogVisual(`0T ${result.subject} | ${result.action} | ${result.outcome}`);
|
||||||
|
const bg = this.trackSideObject(this.add.rectangle(x, safeY, width, height, 0x101820, 0.97));
|
||||||
|
bg.setOrigin(0);
|
||||||
|
bg.setStrokeStyle(1, visual.tone, 0.72);
|
||||||
|
|
||||||
|
const iconFrame = this.trackSideObject(this.add.rectangle(x + 24, safeY + 27, 34, 34, 0x0a0f14, 0.94));
|
||||||
|
iconFrame.setStrokeStyle(1, visual.tone, 0.64);
|
||||||
|
const icon = this.trackSideIcon(x + 24, safeY + 27, visual.icon, 31);
|
||||||
|
icon.setDepth(33);
|
||||||
|
|
||||||
|
const title = this.trackSideObject(this.add.text(x + 48, safeY + 7, this.truncateBattleLogText(`${result.subject} · ${result.action}`, 31), {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#9fb0bf',
|
||||||
|
fontStyle: '700',
|
||||||
|
fixedWidth: width - 62
|
||||||
|
}));
|
||||||
|
title.setDepth(33);
|
||||||
|
|
||||||
|
const outcome = this.trackSideObject(this.add.text(x + 48, safeY + 27, this.truncateBattleLogText(result.outcome, 34), {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#f2e3bf',
|
||||||
|
fontStyle: '700',
|
||||||
|
fixedWidth: width - 62
|
||||||
|
}));
|
||||||
|
outcome.setDepth(33);
|
||||||
|
}
|
||||||
|
|
||||||
private renderPanelMessage(text: string, x: number, y: number, width: number, height = 74) {
|
private renderPanelMessage(text: string, x: number, y: number, width: number, height = 74) {
|
||||||
const safeY = Math.max(this.sideContentTop(), Math.min(y, this.sideContentBottom() - height));
|
const safeY = Math.max(this.sideContentTop(), Math.min(y, this.sideContentBottom() - height));
|
||||||
const compact = height < 60;
|
const compact = height < 60;
|
||||||
|
|||||||
Reference in New Issue
Block a user