Apply treasure equipment battle effects

This commit is contained in:
2026-06-23 06:15:00 +09:00
parent 8f083a6ca4
commit 2d4b08558d
3 changed files with 201 additions and 14 deletions

View File

@@ -404,6 +404,11 @@ type CombatPreview = {
damage: number;
bondDamageBonus: number;
bondLabel?: string;
equipmentDamageBonus: number;
equipmentDamageReduction: number;
equipmentHitBonus: number;
equipmentCriticalBonus: number;
equipmentEffectLabels: string[];
hitRate: number;
criticalRate: number;
counterAvailable: boolean;
@@ -2244,9 +2249,12 @@ export class BattleScene extends Phaser.Scene {
private renderAttackPreview(preview: CombatPreview) {
const counter = preview.counterAvailable ? ' / 반격 가능' : '';
const bond = preview.bondDamageBonus > 0 && preview.bondLabel ? `\n공명 ${preview.bondLabel} 피해 +${preview.bondDamageBonus}%` : '';
const equipment = preview.equipmentEffectLabels.length > 0
? `\n보물 ${preview.equipmentEffectLabels.join(', ')} (${this.equipmentPreviewBonusText(preview)})`
: '';
this.renderUnitDetail(
preview.defender,
`${preview.attacker.name} ${commandLabels[preview.action]} 예측\n피해 ${preview.damage} / 명중 ${preview.hitRate}% / 치명 ${preview.criticalRate}%\n지형 ${preview.terrainLabel}${counter}${bond}`
`${preview.attacker.name} ${commandLabels[preview.action]} 예측\n피해 ${preview.damage} / 명중 ${preview.hitRate}% / 치명 ${preview.criticalRate}%\n지형 ${preview.terrainLabel}${counter}${bond}${equipment}`
);
}
@@ -2263,6 +2271,16 @@ export class BattleScene extends Phaser.Scene {
);
}
private equipmentPreviewBonusText(preview: CombatPreview) {
const parts = [
preview.equipmentDamageBonus > 0 ? `피해 +${preview.equipmentDamageBonus}%` : '',
preview.equipmentDamageReduction > 0 ? `피해 -${preview.equipmentDamageReduction}%` : '',
preview.equipmentHitBonus > 0 ? `명중 +${preview.equipmentHitBonus}` : '',
preview.equipmentCriticalBonus > 0 ? `치명 +${preview.equipmentCriticalBonus}` : ''
].filter(Boolean);
return parts.join(' / ') || '보정 없음';
}
private finishUnitAction(unit: UnitData, message: string) {
this.actedUnitIds.add(unit.id);
this.phase = 'idle';
@@ -3198,7 +3216,9 @@ export class BattleScene extends Phaser.Scene {
const attackPower = this.actionPower(attacker, action, usable);
const defensePower = this.actionDefense(defender, action, terrainRule.defenseBonus);
const bondBonus = attacker.faction === 'ally' ? this.strongestBondBonus(attacker.id) : { damageBonus: 0, chainRate: 0 };
const damage = Phaser.Math.Clamp(Math.round((attackPower - defensePower) * (1 + bondBonus.damageBonus / 100)), 3, defender.maxHp);
const equipmentEffect = this.combatEquipmentEffect(attacker, defender, action, usable);
const totalDamageBonus = bondBonus.damageBonus + equipmentEffect.damageBonus - equipmentEffect.damageReduction;
const damage = Phaser.Math.Clamp(Math.round((attackPower - defensePower) * (1 + totalDamageBonus / 100)), 3, defender.maxHp);
const defenderTerrainRating = getUnitClass(defender.classKey).terrainRatings[terrain] ?? 100;
const terrainHitPenalty = Math.floor(terrainRule.defenseBonus / 2) + Math.max(0, Math.floor((defenderTerrainRating - 100) / 4));
const hitRate = Phaser.Math.Clamp(
@@ -3207,6 +3227,7 @@ export class BattleScene extends Phaser.Scene {
Math.floor((attacker.stats.luck - defender.stats.luck) / 8) -
terrainHitPenalty +
(usable?.accuracyBonus ?? 0) +
equipmentEffect.hitBonus +
this.hitBuff(attacker),
45,
98
@@ -3217,6 +3238,7 @@ export class BattleScene extends Phaser.Scene {
Math.floor((attacker.stats.might - defender.stats.leadership) / 12) +
(action === 'attack' && getItem(attacker.equipment.weapon.itemId).rank === 'treasure' ? 2 : 0) +
(usable?.criticalBonus ?? 0) +
equipmentEffect.criticalBonus +
this.criticalBuff(attacker),
2,
28
@@ -3230,6 +3252,11 @@ export class BattleScene extends Phaser.Scene {
damage,
bondDamageBonus: bondBonus.damageBonus,
bondLabel: bondBonus.label,
equipmentDamageBonus: equipmentEffect.damageBonus,
equipmentDamageReduction: equipmentEffect.damageReduction,
equipmentHitBonus: equipmentEffect.hitBonus,
equipmentCriticalBonus: equipmentEffect.criticalBonus,
equipmentEffectLabels: equipmentEffect.labels,
hitRate,
criticalRate,
counterAvailable: this.canCounterAttack(defender, attacker, action),
@@ -3277,7 +3304,7 @@ export class BattleScene extends Phaser.Scene {
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 attackerGrowth = this.awardEquipmentExp(attacker, action === 'attack' ? 'weapon' : 'accessory', this.attackerEquipmentExpGain(attacker, action));
const armorGrowth = this.awardEquipmentExp(defender, 'armor', hit ? 6 : 3);
const armorGrowth = this.awardEquipmentExp(defender, 'armor', this.defenderArmorExpGain(defender, hit, preview));
const characterGrowth = this.awardCharacterExp(attacker, this.characterExpGain(preview, damage, hit, critical));
const bondGrowth = attacker.faction === 'ally' && !isCounter ? this.recordBondAttackIntent(attacker, defender) : [];
const bondExp = bondGrowth.reduce((total, growth) => total + growth.amount, 0);
@@ -3367,7 +3394,7 @@ export class BattleScene extends Phaser.Scene {
this.flashSupport(target, usable.name, '#ffdf7b');
}
const equipmentGrowth = this.awardEquipmentExp(user, 'accessory', usable.command === 'strategy' ? 7 : 5);
const equipmentGrowth = this.awardEquipmentExp(user, 'accessory', this.supportEquipmentExpGain(user, usable));
const characterGrowth = this.awardCharacterExp(user, usable.effect === 'heal' ? 8 : 6);
this.recordSupportStats(user, healAmount);
@@ -3385,7 +3412,89 @@ export class BattleScene extends Phaser.Scene {
private supportHealAmount(user: UnitData, target: UnitData, usable: BattleUsable) {
const bonus = usable.command === 'strategy' ? Math.floor(user.stats.intelligence / 12) : Math.floor(user.stats.luck / 14);
return Math.min(target.maxHp - target.hp, Math.max(1, usable.power + bonus));
const equipmentBonus =
getItem(user.equipment.weapon.itemId).id === 'white-feather-fan' && usable.command === 'strategy'
? 3
: getItem(user.equipment.accessory.itemId).id === 'peach-charm'
? 2
: 0;
return Math.min(target.maxHp - target.hp, Math.max(1, usable.power + bonus + equipmentBonus));
}
private combatEquipmentEffect(attacker: UnitData, defender: UnitData, action: DamageCommand, usable?: BattleUsable) {
const labels: string[] = [];
let damageBonus = 0;
let damageReduction = 0;
let hitBonus = 0;
let criticalBonus = 0;
const weapon = getItem(attacker.equipment.weapon.itemId);
const armor = getItem(defender.equipment.armor.itemId);
const accessory = getItem(attacker.equipment.accessory.itemId);
const defenderAccessory = getItem(defender.equipment.accessory.itemId);
const bondBonus = attacker.faction === 'ally' ? this.strongestBondBonus(attacker.id) : { damageBonus: 0, chainRate: 0 };
if (action === 'attack' && weapon.id === 'twin-oath-blades' && bondBonus.chainRate > 0) {
damageBonus += 6;
hitBonus += 4;
labels.push('자웅일대검 공명');
}
if (action === 'attack' && weapon.id === 'green-dragon-glaive') {
damageBonus += attacker.stats.might > defender.stats.might ? 8 : 4;
labels.push(attacker.stats.might > defender.stats.might ? '청룡언월도 무력 우위' : '청룡언월도 대도');
}
if (action === 'attack' && weapon.id === 'serpent-spear') {
criticalBonus += 8;
if (attacker.hp <= Math.ceil(attacker.maxHp * 0.55)) {
damageBonus += 5;
labels.push('장팔사모 투지');
} else {
labels.push('장팔사모 치명');
}
}
if ((action === 'strategy' || usable?.command === 'strategy') && weapon.id === 'white-feather-fan') {
damageBonus += usable?.effect === 'damage' || action === 'strategy' ? 7 : 0;
hitBonus += 6;
labels.push('백우선 책략');
}
if (accessory.id === 'war-manual' && attacker.faction === 'ally' && bondBonus.damageBonus > 0) {
damageBonus += 3;
hitBonus += 3;
labels.push('병법서 공명 보정');
}
if (accessory.id === 'bravery-token' && attacker.hp <= Math.ceil(attacker.maxHp * 0.45)) {
damageBonus += 6;
criticalBonus += 4;
labels.push('호담패 저력');
}
if (accessory.id === 'wind-quiver' && (attacker.classKey === 'archer' || weapon.id === 'short-bow')) {
hitBonus += 8;
criticalBonus += 3;
labels.push('바람 화살통 명중');
}
if (armor.id === 'reinforced-lamellar' && action === 'attack') {
damageReduction += 8;
labels.push('철린갑 피해 감소');
}
if (armor.id === 'oath-robe' && defender.faction === 'ally' && this.strongestBondBonus(defender.id).damageBonus > 0) {
damageReduction += 4;
labels.push('도원포 공명 방호');
}
if (defenderAccessory.id === 'peach-charm' && defender.hp <= Math.ceil(defender.maxHp * 0.4)) {
damageReduction += 4;
labels.push('복숭아 부적 가호');
}
return { damageBonus, damageReduction, hitBonus, criticalBonus, labels };
}
private actionPower(unit: UnitData, action: DamageCommand, usable?: BattleUsable) {
@@ -3436,9 +3545,10 @@ export class BattleScene extends Phaser.Scene {
const outcome = this.formatCombatOutcome(result);
const defeated = result.defeated ? `\n${result.defender.name} 퇴각!` : `\n${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`;
const bondBonus = result.bondDamageBonus > 0 && result.bondLabel ? `\n공명 효과 ${result.bondLabel}: 피해 +${result.bondDamageBonus}%` : '';
const equipment = result.equipmentEffectLabels.length > 0 ? `\n보물 효과: ${result.equipmentEffectLabels.join(', ')}` : '';
const bond = result.bondExp > 0 ? `\n공명 경험치 +${result.bondExp}` : '';
const counter = result.counter ? `\n${this.formatCounterLine(result.counter)}` : '';
return `${result.attacker.name} ${result.usable?.name ?? commandLabels[result.action]}\n${outcome}${defeated}${bondBonus}\n장수 경험치 +${result.characterGrowth.amount}\n${this.formatEquipmentGrowth(result.attackerGrowth)}\n${this.formatEquipmentGrowth(result.defenderGrowth)}${bond}${counter}`;
return `${result.attacker.name} ${result.usable?.name ?? commandLabels[result.action]}\n${outcome}${defeated}${bondBonus}${equipment}\n장수 경험치 +${result.characterGrowth.amount}\n${this.formatEquipmentGrowth(result.attackerGrowth)}\n${this.formatEquipmentGrowth(result.defenderGrowth)}${bond}${counter}`;
}
private formatCombatOutcome(result: CombatResult) {
@@ -3469,6 +3579,9 @@ export class BattleScene extends Phaser.Scene {
if (result.bondExp > 0) {
lines.push(`공명 경험치 +${result.bondExp}`);
}
if (result.equipmentEffectLabels.length > 0) {
lines.push(`보물 효과: ${result.equipmentEffectLabels.join(', ')}`);
}
return lines;
}
@@ -5409,7 +5522,8 @@ export class BattleScene extends Phaser.Scene {
.forEach((unit) => {
const terrain = battleMap.terrain[unit.y][unit.x];
const terrainRule = getTerrainRule(terrain);
const hpGain = Math.min(terrainRule.recoveryHp ?? 0, unit.maxHp - unit.hp);
const itemRecovery = this.turnStartEquipmentRecovery(unit);
const hpGain = Math.min((terrainRule.recoveryHp ?? 0) + itemRecovery.amount, unit.maxHp - unit.hp);
const moraleBonus = terrainRule.moraleBonus ?? 0;
if (hpGain <= 0 && moraleBonus <= 0) {
return;
@@ -5423,7 +5537,7 @@ export class BattleScene extends Phaser.Scene {
}
const parts = [
hpGain > 0 ? `병력 +${hpGain}` : '',
hpGain > 0 ? `병력 +${hpGain}${itemRecovery.label ? ` (${itemRecovery.label})` : ''}` : '',
moraleBonus > 0 ? `사기 +${moraleBonus}` : ''
].filter(Boolean);
messages.push(`${unit.name} ${terrainRule.label}: ${parts.join(' / ')}`);
@@ -5440,6 +5554,24 @@ export class BattleScene extends Phaser.Scene {
return message;
}
private turnStartEquipmentRecovery(unit: UnitData) {
let amount = 0;
const labels: string[] = [];
const accessory = getItem(unit.equipment.accessory.itemId);
const armor = getItem(unit.equipment.armor.itemId);
if (accessory.id === 'peach-charm' && unit.hp <= Math.ceil(unit.maxHp * 0.55)) {
amount += 4;
labels.push('복숭아 부적');
}
if (armor.id === 'oath-robe' && this.strongestBondBonus(unit.id).damageBonus > 0) {
amount += 2;
labels.push('도원포');
}
return { amount, label: labels.join('/') };
}
private applyTerrainMorale(unit: UnitData, moraleBonus: number) {
const existing = this.battleBuffs.get(unit.id);
this.battleBuffs.set(unit.id, {
@@ -5611,7 +5743,25 @@ export class BattleScene extends Phaser.Scene {
private weaponExpGain(unit: UnitData) {
const weapon = getItem(unit.equipment.weapon.itemId);
const bonus = this.strongestBondBonus(unit.id);
return 8 + (weapon.rank === 'treasure' ? 2 : 0) + (bonus.chainRate > 0 ? 2 : 0);
const oathBonus = weapon.id === 'twin-oath-blades' && bonus.chainRate > 0 ? 2 : 0;
const strategyWeaponBonus = weapon.id === 'white-feather-fan' ? 2 : 0;
return 8 + (weapon.rank === 'treasure' ? 2 : 0) + (bonus.chainRate > 0 ? 2 : 0) + oathBonus + strategyWeaponBonus;
}
private defenderArmorExpGain(unit: UnitData, hit: boolean, preview: CombatPreview) {
const armor = getItem(unit.equipment.armor.itemId);
const bondGuardBonus = armor.id === 'oath-robe' && preview.equipmentEffectLabels.includes('도원포 공명 방호') ? 2 : 0;
const treasureGuardBonus = armor.id === 'reinforced-lamellar' && hit ? 2 : 0;
return (hit ? 6 : 3) + bondGuardBonus + treasureGuardBonus;
}
private supportEquipmentExpGain(user: UnitData, usable: BattleUsable) {
const accessory = getItem(user.equipment.accessory.itemId);
const weapon = getItem(user.equipment.weapon.itemId);
const strategyBonus = usable.command === 'strategy' ? 2 : 0;
const manualBonus = accessory.id === 'war-manual' ? 2 : 0;
const featherBonus = weapon.id === 'white-feather-fan' && usable.command === 'strategy' ? 2 : 0;
return (usable.command === 'strategy' ? 7 : 5) + strategyBonus + manualBonus + featherBonus;
}
private awardEquipmentExp(unit: UnitData, slot: EquipmentSlot, amount: number): EquipmentGrowthResult {
@@ -6846,6 +6996,7 @@ export class BattleScene extends Phaser.Scene {
selectedUsable: this.selectedUsable?.id ?? null,
itemStocks: this.serializeItemStocks(),
battleBuffs: Array.from(this.battleBuffs.values()).map((buff) => ({ ...buff })),
treasureEffectPreviews: this.debugTreasureEffectPreviews(),
units: battleUnits.map((unit) => ({
id: unit.id,
name: unit.name,
@@ -6873,6 +7024,34 @@ export class BattleScene extends Phaser.Scene {
};
}
private debugTreasureEffectPreviews() {
return ['liu-bei', 'guan-yu', 'zhang-fei', 'zhuge-liang']
.map((unitId) => battleUnits.find((unit) => unit.id === unitId && unit.hp > 0))
.filter((unit): unit is UnitData => Boolean(unit))
.map((unit) => {
const target = this.findNearestEnemy(unit);
if (!target) {
return undefined;
}
const action: DamageCommand = unit.id === 'zhuge-liang' ? 'strategy' : 'attack';
const preview = this.combatPreview(unit, target, action);
return {
attackerId: unit.id,
defenderId: target.id,
action,
damage: preview.damage,
hitRate: preview.hitRate,
criticalRate: preview.criticalRate,
equipmentDamageBonus: preview.equipmentDamageBonus,
equipmentDamageReduction: preview.equipmentDamageReduction,
equipmentHitBonus: preview.equipmentHitBonus,
equipmentCriticalBonus: preview.equipmentCriticalBonus,
equipmentEffectLabels: preview.equipmentEffectLabels
};
})
.filter((preview): preview is NonNullable<typeof preview> => Boolean(preview));
}
debugForceBattleOutcome(outcome: BattleOutcome = 'victory') {
if (!import.meta.env.DEV || this.battleOutcome) {
return;