|
|
|
@@ -1282,6 +1282,25 @@ type BondCombatBonus = {
|
|
|
|
extendedRange?: boolean;
|
|
|
|
extendedRange?: boolean;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type BondChainCandidate = {
|
|
|
|
|
|
|
|
bondId: string;
|
|
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
|
|
partner: UnitData;
|
|
|
|
|
|
|
|
rate: number;
|
|
|
|
|
|
|
|
damage: number;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type BondChainResult = {
|
|
|
|
|
|
|
|
bondId: string;
|
|
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
|
|
partnerId: string;
|
|
|
|
|
|
|
|
partnerName: string;
|
|
|
|
|
|
|
|
rate: number;
|
|
|
|
|
|
|
|
damage: number;
|
|
|
|
|
|
|
|
previousDefenderHp: number;
|
|
|
|
|
|
|
|
defeated: boolean;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type UnitBattleStats = {
|
|
|
|
type UnitBattleStats = {
|
|
|
|
damageDealt: number;
|
|
|
|
damageDealt: number;
|
|
|
|
damageTaken: number;
|
|
|
|
damageTaken: number;
|
|
|
|
@@ -1473,6 +1492,12 @@ type CombatPreview = {
|
|
|
|
bondLabel?: string;
|
|
|
|
bondLabel?: string;
|
|
|
|
bondPartnerIds: string[];
|
|
|
|
bondPartnerIds: string[];
|
|
|
|
bondExtendedRange: boolean;
|
|
|
|
bondExtendedRange: boolean;
|
|
|
|
|
|
|
|
bondChainRate: number;
|
|
|
|
|
|
|
|
bondChainDamage: number;
|
|
|
|
|
|
|
|
bondChainBondId?: string;
|
|
|
|
|
|
|
|
bondChainPartnerId?: string;
|
|
|
|
|
|
|
|
bondChainPartnerName?: string;
|
|
|
|
|
|
|
|
bondChainLabel?: string;
|
|
|
|
equipmentDamageBonus: number;
|
|
|
|
equipmentDamageBonus: number;
|
|
|
|
equipmentDamageReduction: number;
|
|
|
|
equipmentDamageReduction: number;
|
|
|
|
equipmentHitBonus: number;
|
|
|
|
equipmentHitBonus: number;
|
|
|
|
@@ -1506,6 +1531,7 @@ type CombatResult = CombatPreview & {
|
|
|
|
hit: boolean;
|
|
|
|
hit: boolean;
|
|
|
|
critical: boolean;
|
|
|
|
critical: boolean;
|
|
|
|
isCounter: boolean;
|
|
|
|
isCounter: boolean;
|
|
|
|
|
|
|
|
baseDefeated: boolean;
|
|
|
|
defeated: boolean;
|
|
|
|
defeated: boolean;
|
|
|
|
characterGrowth: CharacterGrowthResult;
|
|
|
|
characterGrowth: CharacterGrowthResult;
|
|
|
|
attackerGrowth: EquipmentGrowthResult;
|
|
|
|
attackerGrowth: EquipmentGrowthResult;
|
|
|
|
@@ -1518,6 +1544,7 @@ type CombatResult = CombatPreview & {
|
|
|
|
initiativeBonusDamageAmount: number;
|
|
|
|
initiativeBonusDamageAmount: number;
|
|
|
|
initiativePreventedDamageAmount: number;
|
|
|
|
initiativePreventedDamageAmount: number;
|
|
|
|
initiativeReadied: boolean;
|
|
|
|
initiativeReadied: boolean;
|
|
|
|
|
|
|
|
bondChain?: BondChainResult;
|
|
|
|
counter?: CombatResult;
|
|
|
|
counter?: CombatResult;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
@@ -9996,6 +10023,15 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
preview.terrainAffinity !== 100 ? `적성${preview.terrainAffinity}%` : ''
|
|
|
|
preview.terrainAffinity !== 100 ? `적성${preview.terrainAffinity}%` : ''
|
|
|
|
].filter(Boolean);
|
|
|
|
].filter(Boolean);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (preview.bondChainPartnerName && preview.bondChainRate > 0 && preview.bondChainDamage > 0) {
|
|
|
|
|
|
|
|
summaries.push({
|
|
|
|
|
|
|
|
icon: 'leadership',
|
|
|
|
|
|
|
|
label: `추격·${preview.bondChainPartnerName}`,
|
|
|
|
|
|
|
|
value: `${preview.bondChainRate}% · +${preview.bondChainDamage}`,
|
|
|
|
|
|
|
|
tone: palette.gold
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (preview.action === 'strategy') {
|
|
|
|
if (preview.action === 'strategy') {
|
|
|
|
summaries.push({
|
|
|
|
summaries.push({
|
|
|
|
icon: 'intelligence',
|
|
|
|
icon: 'intelligence',
|
|
|
|
@@ -10276,6 +10312,15 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
if (preview.bondDamageBonus > 0 && preview.bondLabel) {
|
|
|
|
if (preview.bondDamageBonus > 0 && preview.bondLabel) {
|
|
|
|
detailBadgeX = this.renderPreviewBadge(detailBadgeX, badgeY, 'leadership', `공명 +${preview.bondDamageBonus}%`, maxBadgeRight);
|
|
|
|
detailBadgeX = this.renderPreviewBadge(detailBadgeX, badgeY, 'leadership', `공명 +${preview.bondDamageBonus}%`, maxBadgeRight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (preview.bondChainPartnerName && preview.bondChainRate > 0) {
|
|
|
|
|
|
|
|
detailBadgeX = this.renderPreviewBadge(
|
|
|
|
|
|
|
|
detailBadgeX,
|
|
|
|
|
|
|
|
badgeY,
|
|
|
|
|
|
|
|
'leadership',
|
|
|
|
|
|
|
|
`추격 ${preview.bondChainPartnerName} ${preview.bondChainRate}% +${preview.bondChainDamage}`,
|
|
|
|
|
|
|
|
maxBadgeRight
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
if (preview.equipmentEffectLabels.length > 0) {
|
|
|
|
if (preview.equipmentEffectLabels.length > 0) {
|
|
|
|
this.renderPreviewBadge(detailBadgeX, badgeY, 'mastery', '장비 보정', maxBadgeRight);
|
|
|
|
this.renderPreviewBadge(detailBadgeX, badgeY, 'mastery', '장비 보정', maxBadgeRight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -13114,6 +13159,7 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
const attackPower = this.actionPower(attacker, action, usable);
|
|
|
|
const attackPower = this.actionPower(attacker, action, usable);
|
|
|
|
const defensePower = this.actionDefense(defender, action, terrainRule.defenseBonus);
|
|
|
|
const defensePower = this.actionDefense(defender, action, terrainRule.defenseBonus);
|
|
|
|
const bondBonus = this.activeBondCombatBonus(attacker, defender, action, isCounter);
|
|
|
|
const bondBonus = this.activeBondCombatBonus(attacker, defender, action, isCounter);
|
|
|
|
|
|
|
|
const bondChainCandidate = this.activeBondChainCandidate(attacker, defender, action, isCounter);
|
|
|
|
const equipmentEffect = this.combatEquipmentEffect(attacker, defender, action, usable, bondBonus);
|
|
|
|
const equipmentEffect = this.combatEquipmentEffect(attacker, defender, action, usable, bondBonus);
|
|
|
|
const sortieEffect = this.sortieRoleCombatEffect(attacker, defender, action, isCounter, ignoreSortieEffects);
|
|
|
|
const sortieEffect = this.sortieRoleCombatEffect(attacker, defender, action, isCounter, ignoreSortieEffects);
|
|
|
|
const initiativeEffect = this.tacticalInitiativeCombatEffect(
|
|
|
|
const initiativeEffect = this.tacticalInitiativeCombatEffect(
|
|
|
|
@@ -13219,6 +13265,12 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
bondLabel: bondBonus.label,
|
|
|
|
bondLabel: bondBonus.label,
|
|
|
|
bondPartnerIds: bondBonus.partnerIds,
|
|
|
|
bondPartnerIds: bondBonus.partnerIds,
|
|
|
|
bondExtendedRange: Boolean(bondBonus.extendedRange),
|
|
|
|
bondExtendedRange: Boolean(bondBonus.extendedRange),
|
|
|
|
|
|
|
|
bondChainRate: bondChainCandidate?.rate ?? 0,
|
|
|
|
|
|
|
|
bondChainDamage: bondChainCandidate?.damage ?? 0,
|
|
|
|
|
|
|
|
bondChainBondId: bondChainCandidate?.bondId,
|
|
|
|
|
|
|
|
bondChainPartnerId: bondChainCandidate?.partner.id,
|
|
|
|
|
|
|
|
bondChainPartnerName: bondChainCandidate?.partner.name,
|
|
|
|
|
|
|
|
bondChainLabel: bondChainCandidate?.label,
|
|
|
|
equipmentDamageBonus: equipmentEffect.damageBonus,
|
|
|
|
equipmentDamageBonus: equipmentEffect.damageBonus,
|
|
|
|
equipmentDamageReduction: equipmentEffect.damageReduction,
|
|
|
|
equipmentDamageReduction: equipmentEffect.damageReduction,
|
|
|
|
equipmentHitBonus: equipmentEffect.hitBonus,
|
|
|
|
equipmentHitBonus: equipmentEffect.hitBonus,
|
|
|
|
@@ -13294,6 +13346,79 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
stats.sortieBonusHealing += sortieBonusHealing;
|
|
|
|
stats.sortieBonusHealing += sortieBonusHealing;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private recordBondChainStats(partner: UnitData, defender: UnitData, damage: number, defeated: boolean) {
|
|
|
|
|
|
|
|
const credit = this.bondChainStatCredit(damage, defeated);
|
|
|
|
|
|
|
|
const partnerStats = this.statsFor(partner.id);
|
|
|
|
|
|
|
|
partnerStats.damageDealt += credit.partnerDamage;
|
|
|
|
|
|
|
|
partnerStats.defeats += credit.partnerDefeats;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.statsFor(defender.id).damageTaken += credit.defenderDamageTaken;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private bondChainStatCredit(damage: number, defeated: boolean) {
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
partnerDamage: Math.max(0, damage),
|
|
|
|
|
|
|
|
partnerDefeats: defeated ? 1 : 0,
|
|
|
|
|
|
|
|
partnerActions: 0,
|
|
|
|
|
|
|
|
defenderDamageTaken: Math.max(0, damage)
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private canResolveBondChain(preview: CombatPreview, hit: boolean, defenderHp: number) {
|
|
|
|
|
|
|
|
return Boolean(
|
|
|
|
|
|
|
|
hit &&
|
|
|
|
|
|
|
|
defenderHp > 0 &&
|
|
|
|
|
|
|
|
preview.action === 'attack' &&
|
|
|
|
|
|
|
|
preview.bondChainRate > 0 &&
|
|
|
|
|
|
|
|
preview.bondChainDamage > 0 &&
|
|
|
|
|
|
|
|
preview.bondChainPartnerId
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private debugBondChainRollOverride() {
|
|
|
|
|
|
|
|
if (!this.debugToolsEnabled() || typeof window === 'undefined') {
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const value = new URLSearchParams(window.location.search).get('debugForceBondChain');
|
|
|
|
|
|
|
|
return value === '1' ? true : value === '0' ? false : undefined;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private bondChainRollSucceeded(rate: number, forcedRoll = this.debugBondChainRollOverride()) {
|
|
|
|
|
|
|
|
return forcedRoll ?? this.rollPercent(rate);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private resolveBondChainFollowUp(preview: CombatPreview, hit: boolean): BondChainResult | undefined {
|
|
|
|
|
|
|
|
if (!this.canResolveBondChain(preview, hit, preview.defender.hp)) {
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const partner = battleUnits.find((unit) => unit.id === preview.bondChainPartnerId && unit.hp > 0);
|
|
|
|
|
|
|
|
if (!partner) {
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.bondChainRollSucceeded(preview.bondChainRate)) {
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const previousDefenderHp = preview.defender.hp;
|
|
|
|
|
|
|
|
const damage = Phaser.Math.Clamp(preview.bondChainDamage, 1, previousDefenderHp);
|
|
|
|
|
|
|
|
preview.defender.hp = Math.max(0, preview.defender.hp - damage);
|
|
|
|
|
|
|
|
const defeated = preview.defender.hp <= 0;
|
|
|
|
|
|
|
|
this.recordBondChainStats(partner, preview.defender, damage, defeated);
|
|
|
|
|
|
|
|
this.faceUnitToward(partner, preview.defender);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
bondId: preview.bondChainBondId ?? '',
|
|
|
|
|
|
|
|
label: preview.bondChainLabel ?? '공명 연계',
|
|
|
|
|
|
|
|
partnerId: partner.id,
|
|
|
|
|
|
|
|
partnerName: partner.name,
|
|
|
|
|
|
|
|
rate: preview.bondChainRate,
|
|
|
|
|
|
|
|
damage,
|
|
|
|
|
|
|
|
previousDefenderHp,
|
|
|
|
|
|
|
|
defeated
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private resolveCombatAction(attacker: UnitData, defender: UnitData, action: DamageCommand, isCounter = false, usable?: BattleUsable): CombatResult {
|
|
|
|
private resolveCombatAction(attacker: UnitData, defender: UnitData, action: DamageCommand, isCounter = false, usable?: BattleUsable): CombatResult {
|
|
|
|
const preview = this.combatPreview(attacker, defender, action, usable, isCounter);
|
|
|
|
const preview = this.combatPreview(attacker, defender, action, usable, isCounter);
|
|
|
|
const sortieOnlyPreview = this.combatPreview(attacker, defender, action, usable, isCounter, false, true);
|
|
|
|
const sortieOnlyPreview = this.combatPreview(attacker, defender, action, usable, isCounter, false, true);
|
|
|
|
@@ -13316,10 +13441,11 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
const attackerGrowth = this.awardEquipmentExp(attacker, action === 'attack' ? 'weapon' : 'accessory', this.attackerEquipmentExpGain(attacker, action));
|
|
|
|
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 armorGrowth = this.awardEquipmentExp(defender, 'armor', this.defenderArmorExpGain(defender, hit, preview));
|
|
|
|
const characterGrowth = this.awardCharacterExp(attacker, this.characterExpGain(preview, damage, hit, critical));
|
|
|
|
const characterGrowth = this.awardCharacterExp(attacker, this.characterExpGain(preview, damage, hit, critical));
|
|
|
|
const bondGrowth = attacker.faction === 'ally' && !isCounter ? this.recordBondAttackIntent(attacker, defender) : [];
|
|
|
|
const bondGrowth = attacker.faction === 'ally' && action === 'attack' && !isCounter ? this.recordBondAttackIntent(attacker, defender) : [];
|
|
|
|
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 baseDefeated = defender.hp <= 0;
|
|
|
|
const statusApplied =
|
|
|
|
const statusApplied =
|
|
|
|
hit && defender.hp > 0 && usable?.statusEffect
|
|
|
|
hit && defender.hp > 0 && usable?.statusEffect
|
|
|
|
? this.applyBattleStatus(
|
|
|
|
? this.applyBattleStatus(
|
|
|
|
@@ -13345,14 +13471,18 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
initiativeBonusDamageAmount,
|
|
|
|
initiativeBonusDamageAmount,
|
|
|
|
initiativePreventedDamageAmount
|
|
|
|
initiativePreventedDamageAmount
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
const bondChain = !isCounter ? this.resolveBondChainFollowUp(preview, hit) : undefined;
|
|
|
|
this.faceUnitToward(attacker, defender);
|
|
|
|
this.faceUnitToward(attacker, defender);
|
|
|
|
if (hit) {
|
|
|
|
if (hit) {
|
|
|
|
this.flashDamage(defender, damage, critical, previousDefenderHp);
|
|
|
|
this.flashDamage(defender, damage, critical, previousDefenderHp, bondChain?.previousDefenderHp);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
this.flashMiss(defender, this.missFeedbackLabel(action, usable));
|
|
|
|
this.flashMiss(defender, this.missFeedbackLabel(action, usable));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (defender.hp <= 0) {
|
|
|
|
if (defender.hp <= 0) {
|
|
|
|
|
|
|
|
this.attackIntents = this.attackIntents.filter(
|
|
|
|
|
|
|
|
(intent) => intent.targetId !== defender.id && intent.attackerId !== defender.id
|
|
|
|
|
|
|
|
);
|
|
|
|
this.applyDefeatedStyle(defender);
|
|
|
|
this.applyDefeatedStyle(defender);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
this.syncUnitMotion(defender);
|
|
|
|
this.syncUnitMotion(defender);
|
|
|
|
@@ -13367,6 +13497,7 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
hit,
|
|
|
|
hit,
|
|
|
|
critical,
|
|
|
|
critical,
|
|
|
|
isCounter,
|
|
|
|
isCounter,
|
|
|
|
|
|
|
|
baseDefeated,
|
|
|
|
defeated: defender.hp <= 0,
|
|
|
|
defeated: defender.hp <= 0,
|
|
|
|
characterGrowth,
|
|
|
|
characterGrowth,
|
|
|
|
attackerGrowth,
|
|
|
|
attackerGrowth,
|
|
|
|
@@ -13378,7 +13509,8 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
sortiePreventedDamageAmount,
|
|
|
|
sortiePreventedDamageAmount,
|
|
|
|
initiativeBonusDamageAmount,
|
|
|
|
initiativeBonusDamageAmount,
|
|
|
|
initiativePreventedDamageAmount,
|
|
|
|
initiativePreventedDamageAmount,
|
|
|
|
initiativeReadied
|
|
|
|
initiativeReadied,
|
|
|
|
|
|
|
|
bondChain
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -13660,8 +13792,11 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
const summaryInitiative = initiativeContribution ? `\n전술 명령: ${initiativeContribution}` : '';
|
|
|
|
const summaryInitiative = initiativeContribution ? `\n전술 명령: ${initiativeContribution}` : '';
|
|
|
|
const summaryEquipment = result.equipmentEffectLabels.length > 0 ? `\n장비 효과: ${result.equipmentEffectLabels.join(', ')}` : '';
|
|
|
|
const summaryEquipment = result.equipmentEffectLabels.length > 0 ? `\n장비 효과: ${result.equipmentEffectLabels.join(', ')}` : '';
|
|
|
|
const summaryBond = result.bondExp > 0 ? `\n공명 경험치 +${result.bondExp}` : '';
|
|
|
|
const summaryBond = result.bondExp > 0 ? `\n공명 경험치 +${result.bondExp}` : '';
|
|
|
|
|
|
|
|
const summaryBondChain = result.bondChain
|
|
|
|
|
|
|
|
? `\n공명 연계 · ${result.bondChain.partnerName} 추격 피해 +${result.bondChain.damage}${result.bondChain.defeated ? ' · 퇴각' : ''}`
|
|
|
|
|
|
|
|
: '';
|
|
|
|
const summaryCounter = result.counter ? `\n${this.formatCounterLine(result.counter)}` : '';
|
|
|
|
const summaryCounter = result.counter ? `\n${this.formatCounterLine(result.counter)}` : '';
|
|
|
|
return `${headline}\n${resolution}${summaryCounter}${summaryBondBonus}${summarySortie}${summaryInitiative}${summaryEquipment}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.attackerGrowth)}\n${this.formatEquipmentGrowth(result.defenderGrowth)}${summaryBond}`;
|
|
|
|
return `${headline}\n${resolution}${summaryBondChain}${summaryCounter}${summaryBondBonus}${summarySortie}${summaryInitiative}${summaryEquipment}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.attackerGrowth)}\n${this.formatEquipmentGrowth(result.defenderGrowth)}${summaryBond}`;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -13701,7 +13836,7 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const critical = result.critical ? '치명 ' : '';
|
|
|
|
const critical = result.critical ? '치명 ' : '';
|
|
|
|
const defeated = result.defeated ? ' · 퇴각' : '';
|
|
|
|
const defeated = result.baseDefeated ? ' · 퇴각' : '';
|
|
|
|
const status = result.statusApplied ? ` · ${this.statusEffectMapText(result.statusApplied)}` : '';
|
|
|
|
const status = result.statusApplied ? ` · ${this.statusEffectMapText(result.statusApplied)}` : '';
|
|
|
|
return `${critical}피해 ${result.damage}${defeated}${status}`;
|
|
|
|
return `${critical}피해 ${result.damage}${defeated}${status}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -13716,7 +13851,7 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
? `치명 ${damage} 피해`
|
|
|
|
? `치명 ${damage} 피해`
|
|
|
|
: `${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.baseDefeated ? ' · 퇴각' : '';
|
|
|
|
const status = result.statusApplied ? ` · ${this.statusEffectMapText(result.statusApplied)}` : '';
|
|
|
|
const status = result.statusApplied ? ` · ${this.statusEffectMapText(result.statusApplied)}` : '';
|
|
|
|
return `${result.attacker.name} ${this.combatActionName(result)} → ${result.defender.name}: ${outcome}${hpLine}${defeated}${status}`;
|
|
|
|
return `${result.attacker.name} ${this.combatActionName(result)} → ${result.defender.name}: ${outcome}${hpLine}${defeated}${status}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -16038,8 +16173,33 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const outcomeCard = this.renderCombatOutcomeCard(result, left + panelWidth / 2 - 226, top + 184, 452, depth + 16);
|
|
|
|
const outcomeCard = this.renderCombatOutcomeCard(result, left + panelWidth / 2 - 226, top + 184, 452, depth + 16);
|
|
|
|
|
|
|
|
|
|
|
|
await this.animateCombatGauge(defenderStatus.hpFill, result.previousDefenderHp / result.defender.maxHp, result.defender.hp / result.defender.maxHp, 420);
|
|
|
|
const hpAfterBaseAttack = result.bondChain?.previousDefenderHp ?? result.defender.hp;
|
|
|
|
defenderStatus.hpText.setText(`${result.defender.hp} / ${result.defender.maxHp}`);
|
|
|
|
await this.animateCombatGauge(
|
|
|
|
|
|
|
|
defenderStatus.hpFill,
|
|
|
|
|
|
|
|
result.previousDefenderHp / result.defender.maxHp,
|
|
|
|
|
|
|
|
hpAfterBaseAttack / result.defender.maxHp,
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
defenderStatus.hpText.setText(`${hpAfterBaseAttack} / ${result.defender.maxHp}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (result.bondChain) {
|
|
|
|
|
|
|
|
await this.showBondChainCombatEffect(
|
|
|
|
|
|
|
|
result,
|
|
|
|
|
|
|
|
defenderSprite,
|
|
|
|
|
|
|
|
defenderX,
|
|
|
|
|
|
|
|
groundY,
|
|
|
|
|
|
|
|
left + panelWidth / 2,
|
|
|
|
|
|
|
|
top + 142,
|
|
|
|
|
|
|
|
depth + 19
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.animateCombatGauge(
|
|
|
|
|
|
|
|
defenderStatus.hpFill,
|
|
|
|
|
|
|
|
result.bondChain.previousDefenderHp / result.defender.maxHp,
|
|
|
|
|
|
|
|
result.defender.hp / result.defender.maxHp,
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
defenderStatus.hpText.setText(`${result.defender.hp} / ${result.defender.maxHp}`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.animateGrowthGauge(
|
|
|
|
await this.animateGrowthGauge(
|
|
|
|
attackerStatus.expFill,
|
|
|
|
attackerStatus.expFill,
|
|
|
|
@@ -16073,7 +16233,7 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
|
|
|
|
|
|
|
|
private renderCombatOutcomeSummaryCard(result: CombatResult, x: number, y: number, width: number, depth: number) {
|
|
|
|
private renderCombatOutcomeSummaryCard(result: CombatResult, x: number, y: number, width: number, depth: number) {
|
|
|
|
const height = 96;
|
|
|
|
const height = 96;
|
|
|
|
const icon: BattleUiIconKey = result.defeated
|
|
|
|
const icon: BattleUiIconKey = result.baseDefeated
|
|
|
|
? 'critical'
|
|
|
|
? 'critical'
|
|
|
|
: !result.hit
|
|
|
|
: !result.hit
|
|
|
|
? 'move'
|
|
|
|
? 'move'
|
|
|
|
@@ -16105,15 +16265,15 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
fixedWidth: 150
|
|
|
|
fixedWidth: 150
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const detail = result.defeated
|
|
|
|
const detail = result.baseDefeated
|
|
|
|
? `${result.defender.name} 퇴각`
|
|
|
|
? `${result.defender.name} 퇴각`
|
|
|
|
: result.hit
|
|
|
|
: result.hit
|
|
|
|
? `${result.defender.name} HP ${result.previousDefenderHp} → ${result.defender.hp}`
|
|
|
|
? `${result.defender.name} HP ${result.previousDefenderHp} → ${result.bondChain?.previousDefenderHp ?? result.defender.hp}`
|
|
|
|
: `${result.defender.name} HP 유지 ${result.defender.hp}/${result.defender.maxHp}`;
|
|
|
|
: `${result.defender.name} HP 유지 ${result.defender.hp}/${result.defender.maxHp}`;
|
|
|
|
const detailText = this.add.text(86, 39, detail, {
|
|
|
|
const detailText = this.add.text(86, 39, detail, {
|
|
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
|
|
fontSize: '13px',
|
|
|
|
fontSize: '13px',
|
|
|
|
color: result.defeated ? '#ffb6a6' : '#c8d2dd',
|
|
|
|
color: result.baseDefeated ? '#ffb6a6' : '#c8d2dd',
|
|
|
|
fontStyle: '700',
|
|
|
|
fontStyle: '700',
|
|
|
|
fixedWidth: 190
|
|
|
|
fixedWidth: 190
|
|
|
|
});
|
|
|
|
});
|
|
|
|
@@ -16160,7 +16320,7 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private combatOutcomeTitle(result: CombatResult) {
|
|
|
|
private combatOutcomeTitle(result: CombatResult) {
|
|
|
|
if (result.defeated) {
|
|
|
|
if (result.baseDefeated) {
|
|
|
|
return '퇴각';
|
|
|
|
return '퇴각';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!result.hit) {
|
|
|
|
if (!result.hit) {
|
|
|
|
@@ -16173,7 +16333,7 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private combatOutcomeTone(result: CombatResult) {
|
|
|
|
private combatOutcomeTone(result: CombatResult) {
|
|
|
|
return result.defeated || result.critical ? 0xd8732c : result.hit ? this.previewAccentColor(result) : 0x83d6ff;
|
|
|
|
return result.baseDefeated || result.critical ? 0xd8732c : result.hit ? this.previewAccentColor(result) : 0x83d6ff;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private combatJudgementChipLabel(result: CombatResult) {
|
|
|
|
private combatJudgementChipLabel(result: CombatResult) {
|
|
|
|
@@ -16188,9 +16348,12 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private combatOutcomeExtraChip(result: CombatResult): { icon: BattleUiIconKey; label: string; tone: number } | undefined {
|
|
|
|
private combatOutcomeExtraChip(result: CombatResult): { icon: BattleUiIconKey; label: string; tone: number } | undefined {
|
|
|
|
if (result.defeated) {
|
|
|
|
if (result.baseDefeated) {
|
|
|
|
return { icon: 'counter', label: '퇴각', tone: 0xd8732c };
|
|
|
|
return { icon: 'counter', label: '퇴각', tone: 0xd8732c };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (result.bondChain) {
|
|
|
|
|
|
|
|
return { icon: 'leadership', label: `추격 +${result.bondChain.damage}`, tone: palette.gold };
|
|
|
|
|
|
|
|
}
|
|
|
|
if (this.combatCounterWillTrigger(result)) {
|
|
|
|
if (this.combatCounterWillTrigger(result)) {
|
|
|
|
return { icon: 'counter', label: '반격 예정', tone: 0xb64a45 };
|
|
|
|
return { icon: 'counter', label: '반격 예정', tone: 0xb64a45 };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -16377,7 +16540,7 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
this.renderCombatRibbonBadge(x + 46, y + 38, this.actionIcon(result), this.previewActionTypeLabel(result), depth + 1, 78);
|
|
|
|
this.renderCombatRibbonBadge(x + 46, y + 38, this.actionIcon(result), this.previewActionTypeLabel(result), depth + 1, 78);
|
|
|
|
this.renderCombatRibbonBadge(x + 130, y + 38, outcomeIcon, outcomeLabel, depth + 1, 62);
|
|
|
|
this.renderCombatRibbonBadge(x + 130, y + 38, outcomeIcon, outcomeLabel, depth + 1, 62);
|
|
|
|
this.renderCombatRibbonBadge(x + 198, y + 38, 'advantage', result.matchupLabel, depth + 1, 76);
|
|
|
|
this.renderCombatRibbonBadge(x + 198, y + 38, 'advantage', result.matchupLabel, depth + 1, 76);
|
|
|
|
if (result.defeated) {
|
|
|
|
if (result.baseDefeated) {
|
|
|
|
this.renderCombatRibbonBadge(x + 280, y + 38, 'counter', '퇴각', depth + 1, 58);
|
|
|
|
this.renderCombatRibbonBadge(x + 280, y + 38, 'counter', '퇴각', depth + 1, 58);
|
|
|
|
} else if (this.combatCounterWillTrigger(result) || result.counter) {
|
|
|
|
} else if (this.combatCounterWillTrigger(result) || result.counter) {
|
|
|
|
this.renderCombatRibbonBadge(x + 280, y + 38, 'counter', '반격', depth + 1, 58);
|
|
|
|
this.renderCombatRibbonBadge(x + 280, y + 38, 'counter', '반격', depth + 1, 58);
|
|
|
|
@@ -17900,7 +18063,8 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
return Promise.resolve();
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const partnerUnits = result.bondPartnerIds
|
|
|
|
const effectPartnerIds = result.bondChain ? [result.bondChain.partnerId] : result.bondPartnerIds;
|
|
|
|
|
|
|
|
const partnerUnits = effectPartnerIds
|
|
|
|
.map((partnerId) => battleUnits.find((unit) => unit.id === partnerId && unit.hp > 0))
|
|
|
|
.map((partnerId) => battleUnits.find((unit) => unit.id === partnerId && unit.hp > 0))
|
|
|
|
.filter((unit): unit is UnitData => Boolean(unit));
|
|
|
|
.filter((unit): unit is UnitData => Boolean(unit));
|
|
|
|
const visiblePartners = partnerUnits.filter((unit) => this.isTileVisible(unit.x, unit.y) && this.unitViews.has(unit.id));
|
|
|
|
const visiblePartners = partnerUnits.filter((unit) => this.isTileVisible(unit.x, unit.y) && this.unitViews.has(unit.id));
|
|
|
|
@@ -17941,7 +18105,11 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
objects.push(this.createBondBadge(
|
|
|
|
objects.push(this.createBondBadge(
|
|
|
|
attackerView.sprite.x,
|
|
|
|
attackerView.sprite.x,
|
|
|
|
attackerView.sprite.y - this.layout.tileSize * 0.96,
|
|
|
|
attackerView.sprite.y - this.layout.tileSize * 0.96,
|
|
|
|
result.bondExtendedRange ? '삼재진 · 2칸 공명' : '연계 발동',
|
|
|
|
result.bondChain
|
|
|
|
|
|
|
|
? `공명 추격 · ${result.bondChain.partnerName}`
|
|
|
|
|
|
|
|
: result.bondExtendedRange
|
|
|
|
|
|
|
|
? '삼재진 · 2칸 공명'
|
|
|
|
|
|
|
|
: '공명 강화',
|
|
|
|
36
|
|
|
|
36
|
|
|
|
));
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
@@ -18004,12 +18172,90 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
|
|
|
|
|
|
|
|
this.tweens.add({ targets: aura, scale: 1.7, alpha: 0, duration: 560, ease: 'Sine.easeOut' });
|
|
|
|
this.tweens.add({ targets: aura, scale: 1.7, alpha: 0, duration: 560, ease: 'Sine.easeOut' });
|
|
|
|
this.tweens.add({ targets: inner, scale: 1.35, alpha: 0.04, duration: 560, ease: 'Sine.easeOut' });
|
|
|
|
this.tweens.add({ targets: inner, scale: 1.35, alpha: 0.04, duration: 560, ease: 'Sine.easeOut' });
|
|
|
|
this.tweens.add({ targets: spark, angle: 180, scale: 1.22, duration: 560, ease: 'Sine.easeInOut' });
|
|
|
|
this.tweens.add({ targets: spark, angle: 180, scale: 1.22, alpha: 0, duration: 560, ease: 'Sine.easeInOut' });
|
|
|
|
this.tweens.add({ targets: text, y: textY - 8, scale: 1.05, duration: 180, yoyo: true, ease: 'Sine.easeOut' });
|
|
|
|
this.tweens.add({ targets: text, y: textY - 8, scale: 1.05, duration: 180, yoyo: true, ease: 'Sine.easeOut' });
|
|
|
|
|
|
|
|
this.tweens.add({ targets: text, alpha: 0, duration: 180, delay: 360, ease: 'Sine.easeIn' });
|
|
|
|
|
|
|
|
|
|
|
|
return this.delay(580);
|
|
|
|
return this.delay(580);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async showBondChainCombatEffect(
|
|
|
|
|
|
|
|
result: CombatResult,
|
|
|
|
|
|
|
|
defenderSprite: Phaser.GameObjects.Sprite,
|
|
|
|
|
|
|
|
defenderX: number,
|
|
|
|
|
|
|
|
groundY: number,
|
|
|
|
|
|
|
|
textX: number,
|
|
|
|
|
|
|
|
textY: number,
|
|
|
|
|
|
|
|
depth: number
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
const chain = result.bondChain;
|
|
|
|
|
|
|
|
const partner = chain ? battleUnits.find((unit) => unit.id === chain.partnerId) : undefined;
|
|
|
|
|
|
|
|
if (!chain || !partner) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
soundDirector.playEffect('bond-resonance', { volume: 0.3, rate: 1.08, stopAfterMs: 780 });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const entryX = defenderX - 230;
|
|
|
|
|
|
|
|
const strikeX = defenderX - 82;
|
|
|
|
|
|
|
|
const supporterSprite = this.trackCombatObject(
|
|
|
|
|
|
|
|
this.add.sprite(
|
|
|
|
|
|
|
|
entryX,
|
|
|
|
|
|
|
|
groundY,
|
|
|
|
|
|
|
|
this.unitActionTexture(partner),
|
|
|
|
|
|
|
|
this.unitActionFrameIndex('east', this.actionPoseForCommand('attack'))
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
supporterSprite.setDepth(depth);
|
|
|
|
|
|
|
|
supporterSprite.setAlpha(0);
|
|
|
|
|
|
|
|
this.applyCombatSpriteDisplaySize(supporterSprite, partner);
|
|
|
|
|
|
|
|
this.applyActionSpriteBlend(supporterSprite);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const banner = this.trackCombatObject(this.add.text(
|
|
|
|
|
|
|
|
textX,
|
|
|
|
|
|
|
|
textY,
|
|
|
|
|
|
|
|
`공명 추격 ${chain.partnerName}\n추가 피해 +${chain.damage}`,
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
|
|
|
|
|
|
fontSize: '20px',
|
|
|
|
|
|
|
|
color: '#fff2b8',
|
|
|
|
|
|
|
|
fontStyle: '700',
|
|
|
|
|
|
|
|
align: 'center',
|
|
|
|
|
|
|
|
stroke: '#2b1606',
|
|
|
|
|
|
|
|
strokeThickness: 5,
|
|
|
|
|
|
|
|
lineSpacing: 2
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
banner.setOrigin(0.5);
|
|
|
|
|
|
|
|
banner.setDepth(depth + 3);
|
|
|
|
|
|
|
|
banner.setScale(0.88);
|
|
|
|
|
|
|
|
banner.setAlpha(0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const trail = this.trackCombatObject(this.add.graphics());
|
|
|
|
|
|
|
|
trail.setDepth(depth - 1);
|
|
|
|
|
|
|
|
trail.lineStyle(7, 0xffdf7b, 0.7);
|
|
|
|
|
|
|
|
trail.beginPath();
|
|
|
|
|
|
|
|
trail.moveTo(entryX, groundY - 40);
|
|
|
|
|
|
|
|
trail.lineTo(defenderX - 36, groundY - 40);
|
|
|
|
|
|
|
|
trail.strokePath();
|
|
|
|
|
|
|
|
trail.setAlpha(0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.tweens.add({ targets: [supporterSprite, banner, trail], alpha: 1, duration: 120, ease: 'Sine.easeOut' });
|
|
|
|
|
|
|
|
this.tweens.add({ targets: banner, scale: 1, y: textY - 5, duration: 180, ease: 'Back.easeOut' });
|
|
|
|
|
|
|
|
this.tweens.add({ targets: supporterSprite, x: strikeX, duration: 230, ease: 'Cubic.easeIn' });
|
|
|
|
|
|
|
|
await this.delay(240);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const impact = this.trackCombatObject(this.add.star(defenderX - 24, groundY - 46, 8, 18, 52, 0xfff0b8, 0.94));
|
|
|
|
|
|
|
|
impact.setDepth(depth + 2);
|
|
|
|
|
|
|
|
defenderSprite.setTintFill(0xffe0a3);
|
|
|
|
|
|
|
|
this.tweens.add({ targets: impact, scale: 1.55, angle: 90, alpha: 0, duration: 300, ease: 'Sine.easeOut' });
|
|
|
|
|
|
|
|
this.tweens.add({ targets: defenderSprite, x: defenderX + 14, duration: 90, yoyo: true, ease: 'Sine.easeOut' });
|
|
|
|
|
|
|
|
this.tweens.add({ targets: supporterSprite, x: entryX + 30, alpha: 0, duration: 260, delay: 80, ease: 'Sine.easeIn' });
|
|
|
|
|
|
|
|
await this.delay(220);
|
|
|
|
|
|
|
|
defenderSprite.clearTint();
|
|
|
|
|
|
|
|
await this.delay(180);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private showSortieAttackCombatEffect(result: CombatResult, x: number, y: number, depth: number) {
|
|
|
|
private showSortieAttackCombatEffect(result: CombatResult, x: number, y: number, depth: number) {
|
|
|
|
if (result.initiativeBonusDamageAmount > 0) {
|
|
|
|
if (result.initiativeBonusDamageAmount > 0) {
|
|
|
|
return this.showSortieCombatCue('flank', `맹진 호령 +${result.initiativeBonusDamageAmount}`, x, y, depth);
|
|
|
|
return this.showSortieCombatCue('flank', `맹진 호령 +${result.initiativeBonusDamageAmount}`, x, y, depth);
|
|
|
|
@@ -18368,7 +18614,7 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
this.activeFaction = 'ally';
|
|
|
|
this.activeFaction = 'ally';
|
|
|
|
this.fastForwardHeld = false;
|
|
|
|
this.fastForwardHeld = false;
|
|
|
|
this.actedUnitIds.clear();
|
|
|
|
this.actedUnitIds.clear();
|
|
|
|
this.attackIntents = [];
|
|
|
|
this.attackIntents = this.emptyAllyTurnAttackIntents();
|
|
|
|
const recoveryMessage = this.applyTerrainRecovery('ally');
|
|
|
|
const recoveryMessage = this.applyTerrainRecovery('ally');
|
|
|
|
this.resetActedStyles();
|
|
|
|
this.resetActedStyles();
|
|
|
|
this.updateTurnText();
|
|
|
|
this.updateTurnText();
|
|
|
|
@@ -18398,6 +18644,10 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private emptyAllyTurnAttackIntents(): AttackIntent[] {
|
|
|
|
|
|
|
|
return [];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private applyTerrainRecovery(faction: ActiveFaction) {
|
|
|
|
private applyTerrainRecovery(faction: ActiveFaction) {
|
|
|
|
const messages: string[] = [];
|
|
|
|
const messages: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
@@ -18818,6 +19068,75 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private bondChainPartnerEligible(
|
|
|
|
|
|
|
|
attacker: UnitData,
|
|
|
|
|
|
|
|
partner: UnitData,
|
|
|
|
|
|
|
|
defender: UnitData,
|
|
|
|
|
|
|
|
bond: BondState,
|
|
|
|
|
|
|
|
action: DamageCommand,
|
|
|
|
|
|
|
|
isCounter: boolean,
|
|
|
|
|
|
|
|
intents: AttackIntent[] = this.attackIntents,
|
|
|
|
|
|
|
|
actedUnitIds: ReadonlySet<string> = this.actedUnitIds
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
|
|
isCounter ||
|
|
|
|
|
|
|
|
action !== 'attack' ||
|
|
|
|
|
|
|
|
attacker.faction !== 'ally' ||
|
|
|
|
|
|
|
|
partner.faction !== attacker.faction ||
|
|
|
|
|
|
|
|
defender.faction === attacker.faction ||
|
|
|
|
|
|
|
|
attacker.hp <= 0 ||
|
|
|
|
|
|
|
|
partner.hp <= 0 ||
|
|
|
|
|
|
|
|
defender.hp <= 0 ||
|
|
|
|
|
|
|
|
sortieBondBonusForLevel(bond.level).chainRate <= 0 ||
|
|
|
|
|
|
|
|
!actedUnitIds.has(partner.id) ||
|
|
|
|
|
|
|
|
!intents.some((intent) => intent.attackerId === partner.id && intent.targetId === defender.id)
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return this.bondPartnerJoinState(attacker, partner, defender).canJoin;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private bondChainSupportDamage(partner: UnitData, defender: UnitData) {
|
|
|
|
|
|
|
|
const supportPreview = this.combatPreview(partner, defender, 'attack', undefined, true, false, true);
|
|
|
|
|
|
|
|
return Phaser.Math.Clamp(supportPreview.damage, 1, defender.maxHp);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private activeBondChainCandidate(attacker: UnitData, defender: UnitData, action: DamageCommand, isCounter = false) {
|
|
|
|
|
|
|
|
if (isCounter || action !== 'attack' || attacker.faction !== 'ally' || defender.faction === attacker.faction) {
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const candidates = Array.from(this.bondStates.values())
|
|
|
|
|
|
|
|
.map((bond) => {
|
|
|
|
|
|
|
|
if (!bond.unitIds.includes(attacker.id)) {
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const partnerId = bond.unitIds.find((unitId) => unitId !== attacker.id);
|
|
|
|
|
|
|
|
const partner = partnerId ? battleUnits.find((unit) => unit.id === partnerId && unit.hp > 0) : undefined;
|
|
|
|
|
|
|
|
if (!partner || !this.bondChainPartnerEligible(attacker, partner, defender, bond, action, isCounter)) {
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return { bond, partner };
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.filter((candidate): candidate is { bond: BondState; partner: UnitData } => Boolean(candidate))
|
|
|
|
|
|
|
|
.sort((left, right) => right.bond.level - left.bond.level || left.bond.id.localeCompare(right.bond.id));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const strongest = candidates[0];
|
|
|
|
|
|
|
|
if (!strongest) {
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [first, second] = strongest.bond.unitIds.map((id) => this.unitName(id));
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
bondId: strongest.bond.id,
|
|
|
|
|
|
|
|
label: `${strongest.bond.title}: ${first}·${second}`,
|
|
|
|
|
|
|
|
partner: strongest.partner,
|
|
|
|
|
|
|
|
rate: sortieBondBonusForLevel(strongest.bond.level).chainRate,
|
|
|
|
|
|
|
|
damage: this.bondChainSupportDamage(strongest.partner, defender)
|
|
|
|
|
|
|
|
} satisfies BondChainCandidate;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private activeBondCombatBonus(attacker: UnitData, defender: UnitData, action: DamageCommand, isCounter = false): BondCombatBonus {
|
|
|
|
private activeBondCombatBonus(attacker: UnitData, defender: UnitData, action: DamageCommand, isCounter = false): BondCombatBonus {
|
|
|
|
if (isCounter || action !== 'attack' || attacker.faction !== 'ally') {
|
|
|
|
if (isCounter || action !== 'attack' || attacker.faction !== 'ally') {
|
|
|
|
return { damageBonus: 0, chainRate: 0, partnerIds: [] };
|
|
|
|
return { damageBonus: 0, chainRate: 0, partnerIds: [] };
|
|
|
|
@@ -19717,14 +20036,14 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
this.syncUnitMotion(unit, view, direction);
|
|
|
|
this.syncUnitMotion(unit, view, direction);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private flashDamage(unit: UnitData, damage: number, critical = false, previousHp?: number) {
|
|
|
|
private flashDamage(unit: UnitData, damage: number, critical = false, previousHp?: number, displayedHp = unit.hp) {
|
|
|
|
const view = this.unitViews.get(unit.id);
|
|
|
|
const view = this.unitViews.get(unit.id);
|
|
|
|
if (!view) {
|
|
|
|
if (!view) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const title = unit.hp <= 0 ? `${critical ? '치명 ' : ''}퇴각 -${damage}` : `${critical ? '치명 ' : '피해 '}-${damage}`;
|
|
|
|
const title = displayedHp <= 0 ? `${critical ? '치명 ' : ''}퇴각 -${damage}` : `${critical ? '치명 ' : '피해 '}-${damage}`;
|
|
|
|
const detail = previousHp === undefined ? '' : `HP ${previousHp}→${unit.hp}`;
|
|
|
|
const detail = previousHp === undefined ? '' : `HP ${previousHp}→${displayedHp}`;
|
|
|
|
this.showMapResultPopup(unit, [title, detail], critical ? '#ff8f5f' : '#ffdf7b', '#220909', critical ? 20 : 18, critical ? 34 : 30);
|
|
|
|
this.showMapResultPopup(unit, [title, detail], critical ? '#ff8f5f' : '#ffdf7b', '#220909', critical ? 20 : 18, critical ? 34 : 30);
|
|
|
|
|
|
|
|
|
|
|
|
view.sprite.setTintFill(0xffe0a3);
|
|
|
|
view.sprite.setTintFill(0xffe0a3);
|
|
|
|
@@ -19787,6 +20106,7 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const effectLine = [
|
|
|
|
const effectLine = [
|
|
|
|
|
|
|
|
result.bondChain ? `공명 추격 ${result.bondChain.partnerName} +${result.bondChain.damage}` : '',
|
|
|
|
this.statusEffectMapText(result.statusApplied),
|
|
|
|
this.statusEffectMapText(result.statusApplied),
|
|
|
|
this.combatSortieContributionText(result),
|
|
|
|
this.combatSortieContributionText(result),
|
|
|
|
this.combatInitiativeContributionText(result)
|
|
|
|
this.combatInitiativeContributionText(result)
|
|
|
|
@@ -19810,8 +20130,12 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private combatMapDamageTitle(result: CombatResult) {
|
|
|
|
private combatMapDamageTitle(result: CombatResult) {
|
|
|
|
|
|
|
|
if (result.bondChain) {
|
|
|
|
|
|
|
|
const totalDamage = result.damage + result.bondChain.damage;
|
|
|
|
|
|
|
|
return result.bondChain.defeated ? `공명 퇴각 -${totalDamage}` : `합동 피해 -${totalDamage}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
const damage = `-${result.damage}`;
|
|
|
|
const damage = `-${result.damage}`;
|
|
|
|
if (result.defeated) {
|
|
|
|
if (result.baseDefeated) {
|
|
|
|
return `${result.critical ? '치명 ' : ''}퇴각 ${damage}`;
|
|
|
|
return `${result.critical ? '치명 ' : ''}퇴각 ${damage}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return `${result.critical ? '치명 ' : '피해 '}${damage}`;
|
|
|
|
return `${result.critical ? '치명 ' : '피해 '}${damage}`;
|
|
|
|
@@ -21825,7 +22149,7 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
private debugCombatMechanicsProbe() {
|
|
|
|
private debugCombatMechanicsProbe() {
|
|
|
|
const bond = Array.from(this.bondStates.values()).find((candidate) => {
|
|
|
|
const bond = Array.from(this.bondStates.values()).find((candidate) => {
|
|
|
|
const units = candidate.unitIds.map((unitId) => battleUnits.find((unit) => unit.id === unitId && unit.hp > 0));
|
|
|
|
const units = candidate.unitIds.map((unitId) => battleUnits.find((unit) => unit.id === unitId && unit.hp > 0));
|
|
|
|
return units.length === 2 && units.every((unit) => unit?.faction === 'ally') && sortieBondBonusForLevel(candidate.level).damageBonus > 0;
|
|
|
|
return units.length === 2 && units.every((unit) => unit?.faction === 'ally') && sortieBondBonusForLevel(candidate.level).chainRate > 0;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
const attacker = bond ? battleUnits.find((unit) => unit.id === bond.unitIds[0] && unit.hp > 0) : undefined;
|
|
|
|
const attacker = bond ? battleUnits.find((unit) => unit.id === bond.unitIds[0] && unit.hp > 0) : undefined;
|
|
|
|
const partner = bond ? battleUnits.find((unit) => unit.id === bond.unitIds[1] && unit.hp > 0) : undefined;
|
|
|
|
const partner = bond ? battleUnits.find((unit) => unit.id === bond.unitIds[1] && unit.hp > 0) : undefined;
|
|
|
|
@@ -21847,11 +22171,28 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
return null;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const adjacentPreview = this.combatPreview({ ...attacker, x: nearX, y: partner.y }, { ...enemy, x: edgeX, y: partner.y }, 'attack');
|
|
|
|
const syntheticAttacker = { ...attacker, x: nearX, y: partner.y };
|
|
|
|
|
|
|
|
const syntheticPartner = { ...partner };
|
|
|
|
|
|
|
|
const syntheticEnemy = { ...enemy, x: edgeX, y: partner.y };
|
|
|
|
|
|
|
|
const adjacentPreview = this.combatPreview(syntheticAttacker, syntheticEnemy, 'attack');
|
|
|
|
const sameTargetPreview = this.combatPreview({ ...attacker, x: edgeX, y: partner.y }, { ...enemy, x: nearX, y: partner.y }, 'attack');
|
|
|
|
const sameTargetPreview = this.combatPreview({ ...attacker, x: edgeX, y: partner.y }, { ...enemy, x: nearX, y: partner.y }, 'attack');
|
|
|
|
const farPreview = this.combatPreview({ ...attacker, x: farAttackerX, y: partner.y }, { ...enemy, x: farDefenderX, y: partner.y }, 'attack');
|
|
|
|
const farPreview = this.combatPreview({ ...attacker, x: farAttackerX, y: partner.y }, { ...enemy, x: farDefenderX, y: partner.y }, 'attack');
|
|
|
|
const normalIncomingPreview = this.combatPreview(enemy, attacker, 'attack');
|
|
|
|
const normalIncomingPreview = this.combatPreview(enemy, attacker, 'attack');
|
|
|
|
const counterIncomingPreview = this.combatPreview(enemy, attacker, 'attack', undefined, true);
|
|
|
|
const counterIncomingPreview = this.combatPreview(enemy, attacker, 'attack', undefined, true);
|
|
|
|
|
|
|
|
const matchingIntent = [{ attackerId: syntheticPartner.id, targetId: syntheticEnemy.id }];
|
|
|
|
|
|
|
|
const syntheticActedUnitIds = new Set([syntheticPartner.id]);
|
|
|
|
|
|
|
|
const chainBonus = sortieBondBonusForLevel(bond.level);
|
|
|
|
|
|
|
|
const supportDamage = this.bondChainSupportDamage(partner, enemy);
|
|
|
|
|
|
|
|
const chainGatePreview: CombatPreview = {
|
|
|
|
|
|
|
|
...adjacentPreview,
|
|
|
|
|
|
|
|
bondChainRate: chainBonus.chainRate,
|
|
|
|
|
|
|
|
bondChainDamage: supportDamage,
|
|
|
|
|
|
|
|
bondChainBondId: bond.id,
|
|
|
|
|
|
|
|
bondChainPartnerId: syntheticPartner.id,
|
|
|
|
|
|
|
|
bondChainPartnerName: syntheticPartner.name,
|
|
|
|
|
|
|
|
bondChainLabel: bond.title
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const statCredit = this.bondChainStatCredit(supportDamage, true);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
bondId: bond.id,
|
|
|
|
bondId: bond.id,
|
|
|
|
@@ -21865,7 +22206,64 @@ export class BattleScene extends Phaser.Scene {
|
|
|
|
farBondDamageBonus: farPreview.bondDamageBonus,
|
|
|
|
farBondDamageBonus: farPreview.bondDamageBonus,
|
|
|
|
farPartnerIds: farPreview.bondPartnerIds,
|
|
|
|
farPartnerIds: farPreview.bondPartnerIds,
|
|
|
|
normalDamage: normalIncomingPreview.damage,
|
|
|
|
normalDamage: normalIncomingPreview.damage,
|
|
|
|
counterDamage: counterIncomingPreview.damage
|
|
|
|
counterDamage: counterIncomingPreview.damage,
|
|
|
|
|
|
|
|
chain: {
|
|
|
|
|
|
|
|
rate: chainBonus.chainRate,
|
|
|
|
|
|
|
|
expectedSupporterId: syntheticPartner.id,
|
|
|
|
|
|
|
|
supportDamage,
|
|
|
|
|
|
|
|
ineligibleWithoutPriorIntent: !this.bondChainPartnerEligible(
|
|
|
|
|
|
|
|
syntheticAttacker,
|
|
|
|
|
|
|
|
syntheticPartner,
|
|
|
|
|
|
|
|
syntheticEnemy,
|
|
|
|
|
|
|
|
bond,
|
|
|
|
|
|
|
|
'attack',
|
|
|
|
|
|
|
|
false,
|
|
|
|
|
|
|
|
[],
|
|
|
|
|
|
|
|
syntheticActedUnitIds
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
eligibleWithMatchingPriorIntent: this.bondChainPartnerEligible(
|
|
|
|
|
|
|
|
syntheticAttacker,
|
|
|
|
|
|
|
|
syntheticPartner,
|
|
|
|
|
|
|
|
syntheticEnemy,
|
|
|
|
|
|
|
|
bond,
|
|
|
|
|
|
|
|
'attack',
|
|
|
|
|
|
|
|
false,
|
|
|
|
|
|
|
|
matchingIntent,
|
|
|
|
|
|
|
|
syntheticActedUnitIds
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
strategyIgnored: !this.bondChainPartnerEligible(
|
|
|
|
|
|
|
|
syntheticAttacker,
|
|
|
|
|
|
|
|
syntheticPartner,
|
|
|
|
|
|
|
|
syntheticEnemy,
|
|
|
|
|
|
|
|
bond,
|
|
|
|
|
|
|
|
'strategy',
|
|
|
|
|
|
|
|
false,
|
|
|
|
|
|
|
|
matchingIntent,
|
|
|
|
|
|
|
|
syntheticActedUnitIds
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
counterIgnored: !this.bondChainPartnerEligible(
|
|
|
|
|
|
|
|
syntheticAttacker,
|
|
|
|
|
|
|
|
syntheticPartner,
|
|
|
|
|
|
|
|
syntheticEnemy,
|
|
|
|
|
|
|
|
bond,
|
|
|
|
|
|
|
|
'attack',
|
|
|
|
|
|
|
|
true,
|
|
|
|
|
|
|
|
matchingIntent,
|
|
|
|
|
|
|
|
syntheticActedUnitIds
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
missedBaseBlocked: !this.canResolveBondChain(chainGatePreview, false, syntheticEnemy.hp),
|
|
|
|
|
|
|
|
lethalBaseBlocked: !this.canResolveBondChain(chainGatePreview, true, 0),
|
|
|
|
|
|
|
|
survivingBaseAllowed: this.canResolveBondChain(chainGatePreview, true, syntheticEnemy.hp),
|
|
|
|
|
|
|
|
forcedSuccessTriggers: this.bondChainRollSucceeded(chainBonus.chainRate, true),
|
|
|
|
|
|
|
|
forcedFailureBlocked: !this.bondChainRollSucceeded(chainBonus.chainRate, false),
|
|
|
|
|
|
|
|
statCredit,
|
|
|
|
|
|
|
|
followUpKillCancelsCounter: !this.canCounterAttack(
|
|
|
|
|
|
|
|
{ ...syntheticEnemy, hp: 0 },
|
|
|
|
|
|
|
|
syntheticAttacker,
|
|
|
|
|
|
|
|
'attack'
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
intentClearedAtTurnReset: this.emptyAllyTurnAttackIntents().length === 0
|
|
|
|
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|