feat: add sortie contribution feedback
This commit is contained in:
@@ -56,7 +56,12 @@ import {
|
||||
type CampaignSortieItemAssignments,
|
||||
type CampaignStep
|
||||
} from '../state/campaignState';
|
||||
import { normalizeBattleSaveSlot, parseBattleSaveState, type BattleSaveState } from '../state/battleSaveState';
|
||||
import {
|
||||
normalizeBattleSaveSlot,
|
||||
parseBattleSaveState,
|
||||
type BattleSaveState,
|
||||
type BattleSaveUnitStats
|
||||
} from '../state/battleSaveState';
|
||||
import { palette } from '../ui/palette';
|
||||
import { startLazyScene } from './lazyScenes';
|
||||
|
||||
@@ -1235,6 +1240,10 @@ type UnitBattleStats = {
|
||||
defeats: number;
|
||||
actions: number;
|
||||
support: number;
|
||||
sortieBonusDamage: number;
|
||||
sortiePreventedDamage: number;
|
||||
sortieBonusHealing: number;
|
||||
sortieExtendedBondTriggers: number;
|
||||
};
|
||||
|
||||
type ThreatTile = {
|
||||
@@ -1404,6 +1413,8 @@ type CombatResult = CombatPreview & {
|
||||
bondExp: number;
|
||||
bondGrowth: BondGrowthResult[];
|
||||
statusApplied?: BattleStatusState;
|
||||
sortieBonusDamageAmount: number;
|
||||
sortiePreventedDamageAmount: number;
|
||||
counter?: CombatResult;
|
||||
};
|
||||
|
||||
@@ -1464,6 +1475,7 @@ type SupportResult = {
|
||||
target: UnitData;
|
||||
previousTargetHp: number;
|
||||
healAmount: number;
|
||||
sortieHealBonus: number;
|
||||
buff?: BattleBuffState;
|
||||
characterGrowth: CharacterGrowthResult;
|
||||
equipmentGrowth: EquipmentGrowthResult;
|
||||
@@ -3397,7 +3409,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
initialBattleUnits,
|
||||
effectiveSortieUnitIds,
|
||||
assignments,
|
||||
(unit) => this.defaultSortieFormationRole(unit),
|
||||
(unit) => this.recommendedSortieFormationRole(unit),
|
||||
this.isFirstPursuitRoleEffectBattle()
|
||||
? battleScenario.sortie?.deploymentSlots.map((slot) => ({
|
||||
x: slot.x,
|
||||
@@ -3416,12 +3428,16 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
private applyDebugBattleSetup() {
|
||||
const setupKey = this.debugBattleSetupKey();
|
||||
if (setupKey !== 'attack-preview' && setupKey !== 'status-ui') {
|
||||
if (setupKey !== 'attack-preview' && setupKey !== 'status-ui' && setupKey !== 'sortie-feedback') {
|
||||
return;
|
||||
}
|
||||
|
||||
const attacker = battleUnits.find((unit) => unit.id === 'guan-yu' && unit.hp > 0) ?? battleUnits.find((unit) => unit.faction === 'ally' && unit.hp > 0);
|
||||
const target = battleUnits.find((unit) => unit.id === 'rebel-a' && unit.hp > 0) ?? battleUnits.find((unit) => unit.faction === 'enemy' && unit.hp > 0);
|
||||
const target = setupKey === 'sortie-feedback'
|
||||
? battleUnits
|
||||
.filter((unit) => unit.faction === 'enemy' && unit.hp > 0)
|
||||
.sort((left, right) => right.maxHp - left.maxHp)[0]
|
||||
: battleUnits.find((unit) => unit.id === 'rebel-a' && unit.hp > 0) ?? battleUnits.find((unit) => unit.faction === 'enemy' && unit.hp > 0);
|
||||
if (!attacker || !target) {
|
||||
return;
|
||||
}
|
||||
@@ -3433,7 +3449,12 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
target.x = targetTile.x;
|
||||
target.y = targetTile.y;
|
||||
target.hp = Math.max(1, target.hp);
|
||||
if (setupKey === 'sortie-feedback') {
|
||||
target.maxHp = Math.max(target.maxHp, 80);
|
||||
target.hp = target.maxHp;
|
||||
} else {
|
||||
target.hp = Math.max(1, target.hp);
|
||||
}
|
||||
}
|
||||
|
||||
private applyDebugStatusUiSetup() {
|
||||
@@ -3537,6 +3558,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
return 'reserve';
|
||||
}
|
||||
|
||||
private recommendedSortieFormationRole(unit: UnitData): SortieFormationRole {
|
||||
return battleScenario.sortie?.recommendedUnits.find((entry) => entry.unitId === unit.id)?.role
|
||||
?? this.defaultSortieFormationRole(unit);
|
||||
}
|
||||
|
||||
private isFirstPursuitRoleEffectBattle() {
|
||||
return battleScenario.id === firstPursuitRoleEffectBattleId;
|
||||
}
|
||||
@@ -3550,17 +3576,21 @@ export class BattleScene extends Phaser.Scene {
|
||||
return undefined;
|
||||
}
|
||||
const assignments = this.effectiveSortieFormationAssignments(getCampaignState());
|
||||
return assignments[unit.id] ?? this.defaultSortieFormationRole(unit);
|
||||
return assignments[unit.id] ?? this.recommendedSortieFormationRole(unit);
|
||||
}
|
||||
|
||||
private firstPursuitRoleAssignments() {
|
||||
return firstPursuitRoleOrder
|
||||
.map((role) => {
|
||||
const unit = battleUnits.find((candidate) => this.firstPursuitRoleForUnit(candidate) === role);
|
||||
return battleUnits
|
||||
.map((unit) => {
|
||||
const role = this.firstPursuitRoleForUnit(unit);
|
||||
if (!role) {
|
||||
return undefined;
|
||||
}
|
||||
const definition = firstPursuitRoleEffects[role];
|
||||
return unit && definition ? { role, unit, definition } : undefined;
|
||||
return definition ? { role, unit, definition } : undefined;
|
||||
})
|
||||
.filter((entry): entry is NonNullable<typeof entry> => Boolean(entry));
|
||||
.filter((entry): entry is NonNullable<typeof entry> => Boolean(entry))
|
||||
.sort((left, right) => firstPursuitRoleOrder.indexOf(left.role) - firstPursuitRoleOrder.indexOf(right.role));
|
||||
}
|
||||
|
||||
private firstPursuitTrinityConfigured() {
|
||||
@@ -3643,12 +3673,12 @@ export class BattleScene extends Phaser.Scene {
|
||||
const parts = this.firstPursuitRoleAssignments().map(({ role, unit }) => {
|
||||
const stats = this.statsFor(unit.id);
|
||||
if (role === 'front') {
|
||||
return `전열 ${unit.name}: 방호·반격 / 받은 피해 ${stats.damageTaken}`;
|
||||
return `전열 ${unit.name}: 감소 ${stats.sortiePreventedDamage}·반격 추가 ${stats.sortieBonusDamage}`;
|
||||
}
|
||||
if (role === 'flank') {
|
||||
return `돌파 ${unit.name}: 초반 강화 / 피해 ${stats.damageDealt}·격파 ${stats.defeats}`;
|
||||
return `돌파 ${unit.name}: 추가 피해 ${stats.sortieBonusDamage}`;
|
||||
}
|
||||
return `후원 ${unit.name}: 책략·회복 / 지원 ${stats.support}`;
|
||||
return `후원 ${unit.name}: 추가 피해 ${stats.sortieBonusDamage}·회복 ${stats.sortieBonusHealing}`;
|
||||
});
|
||||
if (this.firstPursuitTrinityConfigured()) {
|
||||
parts.push(this.firstPursuitTrinityActive() ? '삼재진: 공명 2칸 유지' : '삼재진: 퇴각으로 해제');
|
||||
@@ -3656,19 +3686,34 @@ export class BattleScene extends Phaser.Scene {
|
||||
return `편성 기여 · ${parts.join(' · ')}`;
|
||||
}
|
||||
|
||||
private firstPursuitContributionTotals() {
|
||||
return this.firstPursuitRoleAssignments().reduce(
|
||||
(totals, { unit }) => {
|
||||
const stats = this.statsFor(unit.id);
|
||||
totals.bonusDamage += stats.sortieBonusDamage;
|
||||
totals.preventedDamage += stats.sortiePreventedDamage;
|
||||
totals.bonusHealing += stats.sortieBonusHealing;
|
||||
totals.extendedBondTriggers += stats.sortieExtendedBondTriggers;
|
||||
return totals;
|
||||
},
|
||||
{ bonusDamage: 0, preventedDamage: 0, bonusHealing: 0, extendedBondTriggers: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
private firstPursuitUnitContributionLine(unit: UnitData) {
|
||||
const role = this.firstPursuitRoleForUnit(unit);
|
||||
if (!role) {
|
||||
return undefined;
|
||||
}
|
||||
const stats = this.statsFor(unit.id);
|
||||
const extendedBond = stats.sortieExtendedBondTriggers > 0 ? ` · 공명 ${stats.sortieExtendedBondTriggers}회` : '';
|
||||
if (role === 'front') {
|
||||
return `편성 전열 · 방호/반격 · 받은 피해 ${stats.damageTaken}`;
|
||||
return `전열 · 피해 ${stats.sortiePreventedDamage} 감소 · 반격 +${stats.sortieBonusDamage}${extendedBond}`;
|
||||
}
|
||||
if (role === 'flank') {
|
||||
return `편성 돌파 · 초반 강화 · 피해 ${stats.damageDealt} / 격파 ${stats.defeats}`;
|
||||
return `돌파 · 추가 피해 +${stats.sortieBonusDamage}${extendedBond}`;
|
||||
}
|
||||
return `편성 후원 · 책략/회복 · 지원 ${stats.support}`;
|
||||
return `후원 · 피해 +${stats.sortieBonusDamage} · 회복 +${stats.sortieBonusHealing}${extendedBond}`;
|
||||
}
|
||||
|
||||
private applyCampaignUnitProgress(unit: UnitData, campaign?: CampaignState) {
|
||||
@@ -8958,7 +9003,24 @@ export class BattleScene extends Phaser.Scene {
|
||||
}));
|
||||
unitTitle.setDepth(depth + 2);
|
||||
|
||||
if (hiddenAllyCount > 0) {
|
||||
if (this.isFirstPursuitRoleEffectBattle()) {
|
||||
const totals = this.firstPursuitContributionTotals();
|
||||
const contributionSummary = this.trackResultObject(this.add.text(
|
||||
left + panelWidth - 44,
|
||||
top + 358,
|
||||
`추가 피해 +${totals.bonusDamage} · 피해 감소 ${totals.preventedDamage} · 추가 회복 +${totals.bonusHealing} · 2칸 공명 ${totals.extendedBondTriggers}회`,
|
||||
{
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '12px',
|
||||
color: '#d8b15f',
|
||||
fontStyle: '700'
|
||||
}
|
||||
));
|
||||
contributionSummary.setOrigin(1, 0);
|
||||
contributionSummary.setDepth(depth + 2);
|
||||
}
|
||||
|
||||
if (hiddenAllyCount > 0 && !this.isFirstPursuitRoleEffectBattle()) {
|
||||
const hiddenSummary = this.trackResultObject(this.add.text(left + panelWidth - 44, top + 358, `외 ${hiddenAllyCount}명 정산 반영`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '13px',
|
||||
@@ -9604,7 +9666,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
const sortieContribution = this.firstPursuitUnitContributionLine(unit);
|
||||
if (sortieContribution) {
|
||||
const sortieText = this.trackResultObject(this.add.text(x + 14, y + 31, this.truncateUiText(sortieContribution, 38), {
|
||||
const sortieText = this.trackResultObject(this.add.text(x + 14, y + 31, this.truncateUiText(sortieContribution, 32), {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '11px',
|
||||
color: '#d8b15f',
|
||||
@@ -10292,30 +10354,51 @@ export class BattleScene extends Phaser.Scene {
|
||||
return created;
|
||||
}
|
||||
|
||||
private recordCombatStats(attacker: UnitData, defender: UnitData, damage: number, defeated: boolean) {
|
||||
private recordCombatStats(
|
||||
attacker: UnitData,
|
||||
defender: UnitData,
|
||||
damage: number,
|
||||
defeated: boolean,
|
||||
sortieBonusDamage: number,
|
||||
sortiePreventedDamage: number,
|
||||
sortieExtendedBondTriggered: boolean
|
||||
) {
|
||||
const attackerStats = this.statsFor(attacker.id);
|
||||
attackerStats.actions += 1;
|
||||
attackerStats.damageDealt += damage;
|
||||
attackerStats.sortieBonusDamage += sortieBonusDamage;
|
||||
if (sortieExtendedBondTriggered) {
|
||||
attackerStats.sortieExtendedBondTriggers += 1;
|
||||
}
|
||||
if (defeated) {
|
||||
attackerStats.defeats += 1;
|
||||
}
|
||||
|
||||
const defenderStats = this.statsFor(defender.id);
|
||||
defenderStats.damageTaken += damage;
|
||||
defenderStats.sortiePreventedDamage += sortiePreventedDamage;
|
||||
}
|
||||
|
||||
private recordSupportStats(user: UnitData, amount: number) {
|
||||
private recordSupportStats(user: UnitData, amount: number, sortieBonusHealing = 0) {
|
||||
const stats = this.statsFor(user.id);
|
||||
stats.actions += 1;
|
||||
stats.support += amount;
|
||||
stats.sortieBonusHealing += sortieBonusHealing;
|
||||
}
|
||||
|
||||
private resolveCombatAction(attacker: UnitData, defender: UnitData, action: DamageCommand, isCounter = false, usable?: BattleUsable): CombatResult {
|
||||
const preview = this.combatPreview(attacker, defender, action, usable, isCounter);
|
||||
const baselinePreview = this.combatPreview(attacker, defender, action, usable, isCounter, true);
|
||||
const previousDefenderHp = defender.hp;
|
||||
const hit = this.rollPercent(preview.hitRate);
|
||||
const critical = hit && this.rollPercent(preview.criticalRate);
|
||||
const damage = hit ? Phaser.Math.Clamp(Math.round(preview.damage * (critical ? 1.5 : 1)), 1, defender.hp) : 0;
|
||||
const baselineDamage = hit
|
||||
? Phaser.Math.Clamp(Math.round(baselinePreview.damage * (critical ? 1.5 : 1)), 1, previousDefenderHp)
|
||||
: 0;
|
||||
const sortieBonusDamageAmount = preview.sortieDamageBonus > 0 ? Math.max(0, damage - baselineDamage) : 0;
|
||||
const sortiePreventedDamageAmount = preview.sortieDamageReduction > 0 ? Math.max(0, baselineDamage - damage) : 0;
|
||||
const sortieExtendedBondTriggered = preview.bondExtendedRange && preview.bondDamageBonus > 0 && !isCounter;
|
||||
const attackerGrowth = this.awardEquipmentExp(attacker, action === 'attack' ? 'weapon' : 'accessory', this.attackerEquipmentExpGain(attacker, action));
|
||||
const armorGrowth = this.awardEquipmentExp(defender, 'armor', this.defenderArmorExpGain(defender, hit, preview));
|
||||
const characterGrowth = this.awardCharacterExp(attacker, this.characterExpGain(preview, damage, hit, critical));
|
||||
@@ -10332,7 +10415,15 @@ export class BattleScene extends Phaser.Scene {
|
||||
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,
|
||||
sortieBonusDamageAmount,
|
||||
sortiePreventedDamageAmount,
|
||||
sortieExtendedBondTriggered
|
||||
);
|
||||
this.faceUnitToward(attacker, defender);
|
||||
if (hit) {
|
||||
this.flashDamage(defender, damage, critical, previousDefenderHp);
|
||||
@@ -10361,7 +10452,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
defenderGrowth: armorGrowth,
|
||||
bondExp,
|
||||
bondGrowth,
|
||||
statusApplied
|
||||
statusApplied,
|
||||
sortieBonusDamageAmount,
|
||||
sortiePreventedDamageAmount
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10396,6 +10489,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
private resolveSupportAction(user: UnitData, target: UnitData, usable: BattleUsable): SupportResult {
|
||||
const previousTargetHp = target.hp;
|
||||
const healAmount = usable.effect === 'heal' ? this.supportHealAmount(user, target, usable) : 0;
|
||||
const baselineHealAmount = usable.effect === 'heal' ? this.supportHealAmount(user, target, usable, true) : 0;
|
||||
const sortieHealBonus = Math.max(0, healAmount - baselineHealAmount);
|
||||
let buff: BattleBuffState | undefined;
|
||||
|
||||
if (healAmount > 0) {
|
||||
@@ -10420,7 +10515,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
const equipmentGrowth = this.awardEquipmentExp(user, 'accessory', this.supportEquipmentExpGain(user, usable));
|
||||
const characterGrowth = this.awardCharacterExp(user, usable.effect === 'heal' ? 8 : 6);
|
||||
this.recordSupportStats(user, healAmount);
|
||||
this.recordSupportStats(user, healAmount, sortieHealBonus);
|
||||
|
||||
return {
|
||||
usable,
|
||||
@@ -10428,6 +10523,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
target,
|
||||
previousTargetHp,
|
||||
healAmount,
|
||||
sortieHealBonus,
|
||||
buff,
|
||||
characterGrowth,
|
||||
equipmentGrowth
|
||||
@@ -10624,7 +10720,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
const headline = this.combatResultSummaryLine(result);
|
||||
const resolution = this.combatResolutionLine(result);
|
||||
const summaryBondBonus = result.bondDamageBonus > 0 && result.bondLabel ? `\n공명 효과 ${result.bondLabel}: 피해 +${result.bondDamageBonus}%` : '';
|
||||
const summarySortie = result.sortieEffectLabels.length > 0 ? `\n편성 효과: ${result.sortieEffectLabels.join(', ')}` : '';
|
||||
const sortieContribution = this.combatSortieContributionText(result);
|
||||
const summarySortie = result.sortieEffectLabels.length > 0 || sortieContribution
|
||||
? `\n편성 효과: ${[...result.sortieEffectLabels, sortieContribution].filter(Boolean).join(' · ')}`
|
||||
: '';
|
||||
const summaryEquipment = result.equipmentEffectLabels.length > 0 ? `\n장비 효과: ${result.equipmentEffectLabels.join(', ')}` : '';
|
||||
const summaryBond = result.bondExp > 0 ? `\n공명 경험치 +${result.bondExp}` : '';
|
||||
const summaryCounter = result.counter ? `\n${this.formatCounterLine(result.counter)}` : '';
|
||||
@@ -10707,7 +10806,42 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private formatCounterLine(result: CombatResult) {
|
||||
return `반격 결과: ${this.combatResultSummaryLine(result)}`;
|
||||
const sortieContribution = this.combatSortieContributionText(result);
|
||||
return `반격 결과: ${this.combatResultSummaryLine(result)}${sortieContribution ? ` · ${sortieContribution}` : ''}`;
|
||||
}
|
||||
|
||||
private combatSortieContributionText(result: CombatResult) {
|
||||
const parts: string[] = [];
|
||||
if (result.sortiePreventedDamageAmount > 0) {
|
||||
parts.push(`전열 방호 ${result.sortiePreventedDamageAmount} 감소`);
|
||||
}
|
||||
if (result.sortieBonusDamageAmount > 0) {
|
||||
const label = result.sortieRole === 'front' && result.isCounter
|
||||
? '전열 반격'
|
||||
: result.sortieRole === 'flank'
|
||||
? '돌파 강화'
|
||||
: result.sortieRole === 'support'
|
||||
? '후원 책략'
|
||||
: '편성 추가';
|
||||
parts.push(`${label} +${result.sortieBonusDamageAmount}`);
|
||||
}
|
||||
if (result.bondExtendedRange && result.bondDamageBonus > 0) {
|
||||
parts.push('삼재진 2칸 공명');
|
||||
}
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
private combatSortieRibbonText(result: CombatResult) {
|
||||
if (result.sortiePreventedDamageAmount > 0) {
|
||||
return ` · 방호 -${result.sortiePreventedDamageAmount}`;
|
||||
}
|
||||
if (result.sortieBonusDamageAmount > 0) {
|
||||
return ` · 편성 +${result.sortieBonusDamageAmount}`;
|
||||
}
|
||||
if (result.bondExtendedRange && result.bondDamageBonus > 0) {
|
||||
return ' · 공명 2칸';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private combatGrowthLines(result: CombatResult) {
|
||||
@@ -10734,13 +10868,14 @@ export class BattleScene extends Phaser.Scene {
|
||||
result.usable.effect === 'heal'
|
||||
? `결과: 회복 +${result.healAmount} · HP ${result.previousTargetHp}→${result.target.hp}`
|
||||
: `결과: 강화 ${result.buff?.turns ?? result.usable.duration ?? 1}턴 · ${this.supportBuffCompactText(result.buff)}`;
|
||||
return `${this.supportResultSummaryLine(result)}\n${supportEffect}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.equipmentGrowth)}`;
|
||||
const sortieContribution = result.sortieHealBonus > 0 ? `\n편성 기여: 후원 회복 +${result.sortieHealBonus}` : '';
|
||||
return `${this.supportResultSummaryLine(result)}\n${supportEffect}${sortieContribution}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.equipmentGrowth)}`;
|
||||
|
||||
}
|
||||
|
||||
private supportResultSummaryLine(result: SupportResult) {
|
||||
if (result.usable.effect === 'heal') {
|
||||
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.healAmount}${result.sortieHealBonus > 0 ? ` · 후원 +${result.sortieHealBonus}` : ''} · 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)}`;
|
||||
@@ -10827,7 +10962,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
damageTaken: 0,
|
||||
defeats: 0,
|
||||
actions: 0,
|
||||
support: 0
|
||||
support: 0,
|
||||
sortieBonusDamage: 0,
|
||||
sortiePreventedDamage: 0,
|
||||
sortieBonusHealing: 0,
|
||||
sortieExtendedBondTriggers: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11903,7 +12042,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
return Object.fromEntries(Array.from(this.battleStats.entries()).map(([unitId, stats]) => [unitId, { ...stats }]));
|
||||
}
|
||||
|
||||
private deserializeBattleStats(savedStats?: Record<string, UnitBattleStats>) {
|
||||
private deserializeBattleStats(savedStats?: Record<string, BattleSaveUnitStats>) {
|
||||
const stats = this.createBattleStats();
|
||||
if (!savedStats) {
|
||||
return stats;
|
||||
@@ -11915,7 +12054,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
damageTaken: value.damageTaken ?? 0,
|
||||
defeats: value.defeats ?? 0,
|
||||
actions: value.actions ?? 0,
|
||||
support: value.support ?? 0
|
||||
support: value.support ?? 0,
|
||||
sortieBonusDamage: value.sortieBonusDamage ?? 0,
|
||||
sortiePreventedDamage: value.sortiePreventedDamage ?? 0,
|
||||
sortieBonusHealing: value.sortieBonusHealing ?? 0,
|
||||
sortieExtendedBondTriggers: value.sortieExtendedBondTriggers ?? 0
|
||||
});
|
||||
});
|
||||
return stats;
|
||||
@@ -12514,8 +12657,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private formatEnemyCombatResult(result: CombatResult, behavior: EnemyAiBehavior) {
|
||||
const sortieContribution = this.combatSortieContributionText(result);
|
||||
const sortie = sortieContribution ? ` · ${sortieContribution}` : '';
|
||||
const counter = result.counter ? `\n${this.formatCounterLine(result.counter)}` : '';
|
||||
return `${this.combatResultSummaryLine(result)} · ${this.enemyBehaviorResultLabel(behavior)}${counter}`;
|
||||
return `${this.combatResultSummaryLine(result)} · ${this.enemyBehaviorResultLabel(behavior)}${sortie}${counter}`;
|
||||
}
|
||||
|
||||
private enemyBehaviorResultLabel(behavior: EnemyAiBehavior) {
|
||||
@@ -12609,14 +12754,18 @@ export class BattleScene extends Phaser.Scene {
|
||||
attackerStatus.expText.setText(`${result.characterGrowth.previousExp} / ${result.characterGrowth.next}`);
|
||||
const growthEntries = this.combatGrowthEntries(result);
|
||||
|
||||
if (result.bondDamageBonus > 0 && result.bondLabel) {
|
||||
await this.showBondCombatEffect(result, left + panelWidth / 2, top + 140, attackerX, groundY - 70, depth + 15);
|
||||
}
|
||||
await Promise.all([
|
||||
result.bondDamageBonus > 0 && result.bondLabel
|
||||
? this.showBondCombatEffect(result, left + panelWidth / 2, top + 140, attackerX, groundY - 70, depth + 15)
|
||||
: Promise.resolve(),
|
||||
this.showSortieAttackCombatEffect(result, attackerX, groundY - 70, depth + 16)
|
||||
]);
|
||||
|
||||
await this.delay(180);
|
||||
await this.playCombatActionMotion(result, attackerSprite, defenderSprite, attackerX, defenderX, groundY, depth + 10, defenderDirection);
|
||||
if (result.hit) {
|
||||
this.showCombatImpact(result, defenderX, groundY - 46, depth + 14);
|
||||
void this.showSortieDefenseCombatEffect(result, defenderX, groundY - 70, depth + 17);
|
||||
} else {
|
||||
this.showCombatMiss(defenderX, groundY - 70, depth + 14);
|
||||
}
|
||||
@@ -12778,6 +12927,17 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (this.combatCounterWillTrigger(result)) {
|
||||
return { icon: 'counter', label: '반격 예정', tone: 0xb64a45 };
|
||||
}
|
||||
if (result.sortiePreventedDamageAmount > 0) {
|
||||
return { icon: 'leadership', label: `방호 -${result.sortiePreventedDamageAmount}`, tone: 0x59d18c };
|
||||
}
|
||||
if (result.sortieBonusDamageAmount > 0 && result.sortieRole) {
|
||||
const definition = firstPursuitRoleEffects[result.sortieRole];
|
||||
return {
|
||||
icon: definition?.icon ?? 'leadership',
|
||||
label: `편성 +${result.sortieBonusDamageAmount}`,
|
||||
tone: definition?.tone ?? palette.gold
|
||||
};
|
||||
}
|
||||
if (result.bondDamageBonus > 0) {
|
||||
return { icon: 'leadership', label: `공명 +${result.bondDamageBonus}%`, tone: palette.gold };
|
||||
}
|
||||
@@ -12936,7 +13096,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
}));
|
||||
actionLabel.setDepth(depth + 1);
|
||||
|
||||
const damageText = this.trackCombatObject(this.add.text(x + width - 14, y + 8, `피해 ${dealtDamage}`, {
|
||||
const damageText = this.trackCombatObject(this.add.text(x + width - 14, y + 8, `피해 ${dealtDamage}${this.combatSortieRibbonText(result)}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '15px',
|
||||
color: result.hit ? '#ffcf91' : '#9fb0bf',
|
||||
@@ -13156,6 +13316,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
const effect = this.trackCombatObject(this.add.circle(left + panelWidth / 2, top + 156, 30, effectColor, 0.76));
|
||||
effect.setDepth(depth + 4);
|
||||
this.tweens.add({ targets: effect, scale: 1.45, alpha: 0.18, duration: 520, yoyo: true });
|
||||
this.showSortieSupportCombatEffect(result, left + panelWidth / 2, top + 156, depth + 6);
|
||||
soundDirector.playEffect(result.usable.command === 'strategy' ? 'strategy-cast' : 'exp-gain', {
|
||||
volume: result.usable.command === 'strategy' ? 0.38 : 0.28,
|
||||
stopAfterMs: 900
|
||||
@@ -13337,7 +13498,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
const resultLabel =
|
||||
result.usable.effect === 'heal'
|
||||
? `회복 +${result.healAmount}`
|
||||
? `회복 +${result.healAmount}${result.sortieHealBonus > 0 ? ` · 편성 +${result.sortieHealBonus}` : ''}`
|
||||
: `강화 ${result.buff?.turns ?? result.usable.duration ?? 1}턴`;
|
||||
const resultText = this.trackCombatObject(this.add.text(x + width - 14, y + 8, resultLabel, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
@@ -14480,25 +14641,42 @@ export class BattleScene extends Phaser.Scene {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
soundDirector.playEffect('bond-resonance', { volume: 0.22, rate: 1.04, stopAfterMs: 620 });
|
||||
soundDirector.playEffect('bond-resonance', {
|
||||
volume: result.bondExtendedRange ? 0.28 : 0.22,
|
||||
rate: result.bondExtendedRange ? 0.94 : 1.04,
|
||||
stopAfterMs: 620
|
||||
});
|
||||
|
||||
const objects: Phaser.GameObjects.GameObject[] = [];
|
||||
const line = this.add.graphics();
|
||||
line.setDepth(34);
|
||||
line.lineStyle(4, 0xffdf7b, 0.78);
|
||||
visiblePartners.forEach((partner) => {
|
||||
const partnerView = this.unitViews.get(partner.id);
|
||||
if (!partnerView) {
|
||||
return;
|
||||
}
|
||||
line.lineStyle(result.bondExtendedRange ? 6 : 4, 0xffdf7b, result.bondExtendedRange ? 0.68 : 0.78);
|
||||
line.beginPath();
|
||||
line.moveTo(attackerView.sprite.x, attackerView.sprite.y - this.layout.tileSize * 0.28);
|
||||
line.lineTo(partnerView.sprite.x, partnerView.sprite.y - this.layout.tileSize * 0.28);
|
||||
line.strokePath();
|
||||
objects.push(this.createBondBadge(partnerView.sprite.x, partnerView.sprite.y - this.layout.tileSize * 0.86, `${partner.name} ♥ 공명`, 35));
|
||||
if (result.bondExtendedRange) {
|
||||
line.lineStyle(2, 0x83d6ff, 0.94);
|
||||
line.beginPath();
|
||||
line.moveTo(attackerView.sprite.x, attackerView.sprite.y - this.layout.tileSize * 0.28);
|
||||
line.lineTo(partnerView.sprite.x, partnerView.sprite.y - this.layout.tileSize * 0.28);
|
||||
line.strokePath();
|
||||
}
|
||||
const partnerLabel = result.bondExtendedRange ? `${partner.name} · 2칸` : `${partner.name} ♥ 공명`;
|
||||
objects.push(this.createBondBadge(partnerView.sprite.x, partnerView.sprite.y - this.layout.tileSize * 0.86, partnerLabel, 35));
|
||||
});
|
||||
objects.push(line);
|
||||
objects.push(this.createBondBadge(attackerView.sprite.x, attackerView.sprite.y - this.layout.tileSize * 0.96, '연계 발동', 36));
|
||||
objects.push(this.createBondBadge(
|
||||
attackerView.sprite.x,
|
||||
attackerView.sprite.y - this.layout.tileSize * 0.96,
|
||||
result.bondExtendedRange ? '삼재진 · 2칸 공명' : '연계 발동',
|
||||
36
|
||||
));
|
||||
|
||||
this.tweens.add({ targets: objects, alpha: 0, y: '-=12', duration: 620, ease: 'Sine.easeOut' });
|
||||
return this.delay(660).then(() => objects.forEach((object) => object.destroy()));
|
||||
@@ -14542,7 +14720,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
const spark = this.trackCombatObject(this.add.star(auraX, auraY, 6, 8, 24, 0xfff0b8, 0.86));
|
||||
spark.setDepth(depth + 1);
|
||||
|
||||
const text = this.trackCombatObject(this.add.text(textX, textY, `공명 효과 ${result.bondLabel}\n피해 +${result.bondDamageBonus}%`, {
|
||||
const bondTitle = result.bondExtendedRange ? '삼재진 2칸 공명' : `공명 효과 ${result.bondLabel}`;
|
||||
const text = this.trackCombatObject(this.add.text(textX, textY, `${bondTitle}\n피해 +${result.bondDamageBonus}%`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '18px',
|
||||
color: '#fff2b8',
|
||||
@@ -14553,7 +14732,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
lineSpacing: 2
|
||||
}));
|
||||
text.setOrigin(0.5);
|
||||
text.setText(`공명 효과 ${result.bondLabel}\n피해 +${result.bondDamageBonus}%`);
|
||||
text.setText(`${bondTitle}\n피해 +${result.bondDamageBonus}%`);
|
||||
text.setDepth(depth + 2);
|
||||
|
||||
this.tweens.add({ targets: aura, scale: 1.7, alpha: 0, duration: 560, ease: 'Sine.easeOut' });
|
||||
@@ -14564,6 +14743,87 @@ export class BattleScene extends Phaser.Scene {
|
||||
return this.delay(580);
|
||||
}
|
||||
|
||||
private showSortieAttackCombatEffect(result: CombatResult, x: number, y: number, depth: number) {
|
||||
if (result.sortieBonusDamageAmount <= 0 || !result.sortieRole) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const label = result.sortieRole === 'front' && result.isCounter
|
||||
? `전열 반격 +${result.sortieBonusDamageAmount}`
|
||||
: result.sortieRole === 'flank'
|
||||
? `돌파 강화 +${result.sortieBonusDamageAmount}`
|
||||
: `후원 책략 +${result.sortieBonusDamageAmount}`;
|
||||
return this.showSortieCombatCue(result.sortieRole, label, x, y, depth);
|
||||
}
|
||||
|
||||
private showSortieDefenseCombatEffect(result: CombatResult, x: number, y: number, depth: number) {
|
||||
if (result.sortiePreventedDamageAmount <= 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.showSortieCombatCue('front', `전열 방호 -${result.sortiePreventedDamageAmount}`, x, y, depth);
|
||||
}
|
||||
|
||||
private showSortieCombatCue(role: SortieFormationRole, label: string, x: number, y: number, depth: number) {
|
||||
const definition = firstPursuitRoleEffects[role];
|
||||
if (!definition) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const container = this.trackCombatObject(this.add.container(x, y));
|
||||
container.setDepth(depth);
|
||||
container.setAlpha(0);
|
||||
container.setScale(0.86);
|
||||
|
||||
const outer = this.add.circle(0, 0, role === 'front' ? 42 : 38);
|
||||
outer.setStrokeStyle(role === 'front' ? 5 : 3, definition.tone, 0.92);
|
||||
const inner = role === 'front'
|
||||
? this.add.rectangle(0, 0, 38, 48, definition.tone, 0.14).setStrokeStyle(2, 0xe9fff2, 0.78)
|
||||
: this.add.circle(0, 0, 20, definition.tone, 0.22).setStrokeStyle(2, 0xf8edc5, 0.72);
|
||||
const spark = this.add.star(0, 0, role === 'support' ? 6 : 5, 7, 20, definition.tone, 0.88);
|
||||
const badgeWidth = Math.max(116, label.length * 13 + 28);
|
||||
const badge = this.add.rectangle(0, -58, badgeWidth, 28, 0x0b1218, 0.94);
|
||||
badge.setStrokeStyle(2, definition.tone, 0.88);
|
||||
const icon = this.createBattleUiIcon(-badgeWidth / 2 + 17, -58, definition.icon, 22);
|
||||
const text = this.add.text(8, -58, label, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '13px',
|
||||
color: '#fff2c8',
|
||||
fontStyle: '700',
|
||||
stroke: '#071018',
|
||||
strokeThickness: 3
|
||||
});
|
||||
text.setOrigin(0.5);
|
||||
container.add([outer, inner, spark, badge, icon, text]);
|
||||
|
||||
this.tweens.add({ targets: container, alpha: 1, scale: 1, duration: 120, ease: 'Back.easeOut' });
|
||||
this.tweens.add({ targets: outer, scale: 1.55, alpha: 0.08, duration: 360, ease: 'Sine.easeOut' });
|
||||
this.tweens.add({ targets: spark, angle: role === 'flank' ? 220 : -180, scale: 1.22, duration: 360, ease: 'Sine.easeInOut' });
|
||||
this.tweens.add({
|
||||
targets: container,
|
||||
y: y - 8,
|
||||
alpha: 0,
|
||||
delay: 360,
|
||||
duration: 240,
|
||||
ease: 'Sine.easeIn',
|
||||
onComplete: () => container.destroy()
|
||||
});
|
||||
return this.delay(280);
|
||||
}
|
||||
|
||||
private showSortieSupportCombatEffect(result: SupportResult, x: number, y: number, depth: number) {
|
||||
if (result.sortieHealBonus <= 0) {
|
||||
return;
|
||||
}
|
||||
const outer = this.trackCombatObject(this.add.circle(x, y, 44));
|
||||
outer.setStrokeStyle(4, 0x83d6ff, 0.9);
|
||||
outer.setDepth(depth);
|
||||
const inner = this.trackCombatObject(this.add.circle(x, y, 25, 0x83d6ff, 0.18));
|
||||
inner.setDepth(depth - 1);
|
||||
const spark = this.trackCombatObject(this.add.star(x, y, 6, 7, 22, 0xd9f1ff, 0.84));
|
||||
spark.setDepth(depth + 1);
|
||||
this.tweens.add({ targets: outer, scale: 1.55, alpha: 0, duration: 230, ease: 'Sine.easeOut' });
|
||||
this.tweens.add({ targets: inner, scale: 1.3, alpha: 0.02, duration: 230, ease: 'Sine.easeOut' });
|
||||
this.tweens.add({ targets: spark, angle: 180, scale: 1.18, alpha: 0.08, duration: 230, ease: 'Sine.easeOut' });
|
||||
}
|
||||
|
||||
private showCombatImpact(result: CombatResult, x: number, y: number, depth: number) {
|
||||
if (this.isRangedAttack(result)) {
|
||||
soundDirector.playArrowImpact(true, result.critical);
|
||||
@@ -14762,6 +15022,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
const direction = this.directionFromDelta(x - fromX, y - fromY);
|
||||
const movementDistance = this.movementTileDistanceFrom(fromX, fromY, x, y);
|
||||
const movementDuration = this.movementDuration(duration, movementDistance);
|
||||
this.showSortieFlankMovementTrail(unit, startX, startY, targetX, targetY, movementDuration);
|
||||
this.tweens.killTweensOf([view.sprite, view.hitZone, view.label]);
|
||||
this.setUnitBasePosition(view, startX, startY);
|
||||
this.resetUnitSpritePose(view);
|
||||
@@ -15512,7 +15773,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.refreshUnitLegibilityStyles();
|
||||
soundDirector.playSelect();
|
||||
|
||||
this.moveUnitView(unit, fromX, fromY, view, 180, toX, toY);
|
||||
this.moveUnitView(unit, fromX, fromY, view, 180, toX, toY, false);
|
||||
this.clearMarkers();
|
||||
this.showMoveRange(unit);
|
||||
this.renderUnitDetail(
|
||||
@@ -15529,7 +15790,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
view = this.unitViews.get(unit.id),
|
||||
duration = 260,
|
||||
fromX = unit.x,
|
||||
fromY = unit.y
|
||||
fromY = unit.y,
|
||||
showSortieTrail = true
|
||||
) {
|
||||
if (!view) {
|
||||
return;
|
||||
@@ -15542,6 +15804,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
const direction = this.directionFromDelta(x - fromX, y - fromY);
|
||||
const movementDistance = this.movementTileDistanceFrom(fromX, fromY, x, y);
|
||||
const movementDuration = this.movementDuration(duration, movementDistance);
|
||||
if (showSortieTrail) {
|
||||
this.showSortieFlankMovementTrail(unit, startX, startY, targetX, targetY, movementDuration);
|
||||
}
|
||||
this.tweens.killTweensOf([view.sprite, view.hitZone, view.label]);
|
||||
this.setUnitBasePosition(view, startX, startY);
|
||||
this.resetUnitSpritePose(view);
|
||||
@@ -15574,6 +15839,62 @@ export class BattleScene extends Phaser.Scene {
|
||||
return this.movementTileDistanceFrom(unit.x, unit.y, x, y);
|
||||
}
|
||||
|
||||
private showSortieFlankMovementTrail(
|
||||
unit: UnitData,
|
||||
startX: number,
|
||||
startY: number,
|
||||
targetX: number,
|
||||
targetY: number,
|
||||
duration: number
|
||||
) {
|
||||
if (
|
||||
this.phase === 'deployment' ||
|
||||
this.activeFaction !== 'ally' ||
|
||||
this.turnNumber > 2 ||
|
||||
this.firstPursuitRoleForUnit(unit) !== 'flank' ||
|
||||
(startX === targetX && startY === targetY)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trail = this.add.graphics();
|
||||
trail.setDepth(7);
|
||||
trail.lineStyle(7, 0xd8b15f, 0.2);
|
||||
trail.lineBetween(startX, startY - 5, targetX, targetY - 5);
|
||||
trail.lineStyle(2, 0xffefb3, 0.78);
|
||||
trail.lineBetween(startX, startY - 5, targetX, targetY - 5);
|
||||
if (this.mapMask) {
|
||||
trail.setMask(this.mapMask);
|
||||
}
|
||||
|
||||
const sparks = [0.22, 0.48, 0.74].map((ratio, index) => {
|
||||
const spark = this.add.star(
|
||||
Phaser.Math.Linear(startX, targetX, ratio),
|
||||
Phaser.Math.Linear(startY, targetY, ratio) - 5,
|
||||
4,
|
||||
3,
|
||||
8 + index,
|
||||
index === 1 ? 0xfff1bd : 0xd8b15f,
|
||||
0.78
|
||||
);
|
||||
spark.setDepth(7.2);
|
||||
if (this.mapMask) {
|
||||
spark.setMask(this.mapMask);
|
||||
}
|
||||
this.tweens.add({ targets: spark, angle: 140 + index * 55, scale: 0.3, duration, ease: 'Sine.easeOut' });
|
||||
return spark;
|
||||
});
|
||||
const objects: Phaser.GameObjects.GameObject[] = [trail, ...sparks];
|
||||
this.tweens.add({
|
||||
targets: objects,
|
||||
alpha: 0,
|
||||
delay: Math.round(duration * 0.28),
|
||||
duration: Math.max(150, Math.round(duration * 0.72)),
|
||||
ease: 'Sine.easeOut',
|
||||
onComplete: () => objects.forEach((object) => object.destroy())
|
||||
});
|
||||
}
|
||||
|
||||
private movementTileDistanceFrom(fromX: number, fromY: number, x: number, y: number) {
|
||||
return Math.max(1, Math.abs(x - fromX) + Math.abs(y - fromY));
|
||||
}
|
||||
@@ -16141,10 +16462,13 @@ export class BattleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
const effectLine = [this.statusEffectMapText(result.statusApplied), this.combatSortieContributionText(result)]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
const lines = [
|
||||
this.combatMapDamageTitle(result),
|
||||
`HP ${result.previousDefenderHp}→${result.defender.hp}`,
|
||||
this.statusEffectMapText(result.statusApplied)
|
||||
this.truncateUiText(effectLine, 28)
|
||||
].filter(Boolean);
|
||||
|
||||
this.showMapResultPopup(
|
||||
@@ -16169,11 +16493,15 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (result.usable.effect === 'heal') {
|
||||
this.showMapResultPopup(
|
||||
result.target,
|
||||
[`회복 +${result.healAmount}`, `HP ${result.previousTargetHp}→${result.target.hp}`],
|
||||
[
|
||||
`회복 +${result.healAmount}`,
|
||||
`HP ${result.previousTargetHp}→${result.target.hp}`,
|
||||
result.sortieHealBonus > 0 ? `후원 편성 +${result.sortieHealBonus}` : ''
|
||||
],
|
||||
'#a8ffd0',
|
||||
'#071623',
|
||||
18,
|
||||
28
|
||||
result.sortieHealBonus > 0 ? 36 : 28
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -17876,6 +18204,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
baseMove: unit.move,
|
||||
effectiveMove: this.movementAllowance(unit)
|
||||
})),
|
||||
contributionTotals: this.firstPursuitContributionTotals(),
|
||||
contributionLine: this.firstPursuitContributionLine() ?? null
|
||||
};
|
||||
}
|
||||
@@ -18031,7 +18360,15 @@ export class BattleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
this.time.delayedCall(900, () => this.debugForceBattleOutcome(outcome));
|
||||
this.time.delayedCall(this.debugForcedBattleOutcomeDelay(), () => this.debugForceBattleOutcome(outcome));
|
||||
}
|
||||
|
||||
private debugForcedBattleOutcomeDelay() {
|
||||
if (typeof window === 'undefined') {
|
||||
return 900;
|
||||
}
|
||||
const raw = Number(new URLSearchParams(window.location.search).get('debugForceBattleOutcomeDelay'));
|
||||
return Number.isFinite(raw) ? Phaser.Math.Clamp(Math.round(raw), 900, 60000) : 900;
|
||||
}
|
||||
|
||||
private debugForcedBattleOutcomeParam(): BattleOutcome | undefined {
|
||||
@@ -18092,6 +18429,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
this.input.keyboard?.on('keydown-F9', () => this.toggleDebugOverlay());
|
||||
this.input.keyboard?.on('keydown-F8', () => this.debugForceBattleOutcome('victory'));
|
||||
this.input.keyboard?.on('keydown-F10', () => {
|
||||
const state = this.getDebugState();
|
||||
console.info('[Battle Debug]', state);
|
||||
@@ -18131,12 +18469,14 @@ export class BattleScene extends Phaser.Scene {
|
||||
? `${this.pendingMove.unit.name}: (${this.pendingMove.fromX},${this.pendingMove.fromY}) -> (${this.pendingMove.toX},${this.pendingMove.toY})`
|
||||
: 'none';
|
||||
const sortieProbe = this.debugFirstPursuitRoleMechanicsProbe();
|
||||
const sortieTotals = this.isFirstPursuitRoleEffectBattle() ? this.firstPursuitContributionTotals() : undefined;
|
||||
const sortieProbeLines = sortieProbe
|
||||
? [
|
||||
`role front in=${sortieProbe.front.incomingDamage}/${sortieProbe.front.incomingBaseline} counter=${sortieProbe.front.counterDamage}/${sortieProbe.front.counterBaseline}`,
|
||||
`role flank move=${sortieProbe.flank.effectiveMove}/${sortieProbe.flank.baseMove} attack=${sortieProbe.flank.firstTurnDamage}/${sortieProbe.flank.firstTurnBaseline}`,
|
||||
`role support strategy=${sortieProbe.support.strategyDamage}/${sortieProbe.support.strategyBaseline} heal=${sortieProbe.support.healAmount}/${sortieProbe.support.healBaseline}`,
|
||||
`trinity d2=${sortieProbe.trinity.distanceTwoCanJoin}/${sortieProbe.trinity.distanceTwoExtended} d3=${sortieProbe.trinity.distanceThreeCanJoin}`
|
||||
`trinity d2=${sortieProbe.trinity.distanceTwoCanJoin}/${sortieProbe.trinity.distanceTwoExtended} d3=${sortieProbe.trinity.distanceThreeCanJoin}`,
|
||||
`contrib dmg+${sortieTotals?.bonusDamage ?? 0} guard=${sortieTotals?.preventedDamage ?? 0} heal+${sortieTotals?.bonusHealing ?? 0} d2=${sortieTotals?.extendedBondTriggers ?? 0}`
|
||||
]
|
||||
: [];
|
||||
|
||||
@@ -18149,7 +18489,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
`acted=${Array.from(this.actedUnitIds).join(',') || 'none'}`,
|
||||
`pointerTile=${pointerTile ? `${pointerTile.x},${pointerTile.y}` : 'outside'}`,
|
||||
...sortieProbeLines,
|
||||
'F9 toggle overlay / F10 log state'
|
||||
'F8 force victory / F9 overlay / F10 log state'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,10 @@ export type BattleSaveUnitStats = {
|
||||
defeats: number;
|
||||
actions: number;
|
||||
support: number;
|
||||
sortieBonusDamage?: number;
|
||||
sortiePreventedDamage?: number;
|
||||
sortieBonusHealing?: number;
|
||||
sortieExtendedBondTriggers?: number;
|
||||
};
|
||||
|
||||
export type BattleSaveState = {
|
||||
@@ -512,10 +516,18 @@ function isOptionalStatsRecord(value: unknown, options: BattleSaveValidationOpti
|
||||
isBattleUnitStatValue(stats.damageTaken) &&
|
||||
isBattleUnitStatValue(stats.defeats) &&
|
||||
isBattleUnitStatValue(stats.actions) &&
|
||||
isBattleUnitStatValue(stats.support)
|
||||
isBattleUnitStatValue(stats.support) &&
|
||||
isOptionalBattleUnitStatValue(stats.sortieBonusDamage) &&
|
||||
isOptionalBattleUnitStatValue(stats.sortiePreventedDamage) &&
|
||||
isOptionalBattleUnitStatValue(stats.sortieBonusHealing) &&
|
||||
isOptionalBattleUnitStatValue(stats.sortieExtendedBondTriggers)
|
||||
);
|
||||
}
|
||||
|
||||
function isOptionalBattleUnitStatValue(value: unknown) {
|
||||
return value === undefined || isBattleUnitStatValue(value);
|
||||
}
|
||||
|
||||
function isOptionalEnemyUsableUseKeyArray(value: unknown, battleId: string, options: BattleSaveValidationOptions) {
|
||||
return (
|
||||
value === undefined ||
|
||||
|
||||
Reference in New Issue
Block a user