Add hit critical and counter combat
This commit is contained in:
@@ -119,16 +119,22 @@ type CombatPreview = {
|
|||||||
defender: UnitData;
|
defender: UnitData;
|
||||||
damage: number;
|
damage: number;
|
||||||
hitRate: number;
|
hitRate: number;
|
||||||
|
criticalRate: number;
|
||||||
|
counterAvailable: boolean;
|
||||||
terrainLabel: string;
|
terrainLabel: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CombatResult = CombatPreview & {
|
type CombatResult = CombatPreview & {
|
||||||
previousDefenderHp: number;
|
previousDefenderHp: number;
|
||||||
|
hit: boolean;
|
||||||
|
critical: boolean;
|
||||||
|
isCounter: boolean;
|
||||||
defeated: boolean;
|
defeated: boolean;
|
||||||
characterGrowth: CharacterGrowthResult;
|
characterGrowth: CharacterGrowthResult;
|
||||||
attackerGrowth: EquipmentGrowthResult;
|
attackerGrowth: EquipmentGrowthResult;
|
||||||
defenderGrowth: EquipmentGrowthResult;
|
defenderGrowth: EquipmentGrowthResult;
|
||||||
bondExp: number;
|
bondExp: number;
|
||||||
|
counter?: CombatResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SavedUnitState = Pick<UnitData, 'id' | 'level' | 'exp' | 'hp' | 'maxHp' | 'attack' | 'move' | 'x' | 'y'> & {
|
type SavedUnitState = Pick<UnitData, 'id' | 'level' | 'exp' | 'hp' | 'maxHp' | 'attack' | 'move' | 'x' | 'y'> & {
|
||||||
@@ -872,13 +878,19 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const result = this.resolveCombatAction(attacker, target, action);
|
const result = this.resolveCombatAction(attacker, target, action);
|
||||||
this.clearMarkers();
|
this.clearMarkers();
|
||||||
await this.playCombatCutIn(result);
|
await this.playCombatCutIn(result);
|
||||||
|
result.counter = this.resolveCounterAttack(result);
|
||||||
|
if (result.counter) {
|
||||||
|
await this.delay(180);
|
||||||
|
await this.playCombatCutIn(result.counter);
|
||||||
|
}
|
||||||
this.finishUnitAction(attacker, this.formatCombatResult(result));
|
this.finishUnitAction(attacker, this.formatCombatResult(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderAttackPreview(preview: CombatPreview) {
|
private renderAttackPreview(preview: CombatPreview) {
|
||||||
|
const counter = preview.counterAvailable ? ' / 반격 가능' : '';
|
||||||
this.renderUnitDetail(
|
this.renderUnitDetail(
|
||||||
preview.defender,
|
preview.defender,
|
||||||
`${preview.attacker.name} ${commandLabels[preview.action]} 예측\n피해 ${preview.damage} / 명중 ${preview.hitRate}% / 지형 ${preview.terrainLabel}`
|
`${preview.attacker.name} ${commandLabels[preview.action]} 예측\n피해 ${preview.damage} / 명중 ${preview.hitRate}% / 치명 ${preview.criticalRate}%\n지형 ${preview.terrainLabel}${counter}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1231,7 +1243,21 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const defensePower = this.actionDefense(defender, action, terrainRule.defenseBonus);
|
const defensePower = this.actionDefense(defender, action, terrainRule.defenseBonus);
|
||||||
const bondBonus = attacker.faction === 'ally' ? this.strongestBondBonus(attacker.id).damageBonus : 0;
|
const bondBonus = attacker.faction === 'ally' ? this.strongestBondBonus(attacker.id).damageBonus : 0;
|
||||||
const damage = Phaser.Math.Clamp(Math.round((attackPower - defensePower) * (1 + bondBonus / 100)), 3, defender.maxHp);
|
const damage = Phaser.Math.Clamp(Math.round((attackPower - defensePower) * (1 + bondBonus / 100)), 3, defender.maxHp);
|
||||||
const hitRate = Phaser.Math.Clamp(84 + Math.floor((attacker.stats.agility - defender.stats.agility) / 4), 65, 98);
|
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(
|
||||||
|
88 + Math.floor((attacker.stats.agility - defender.stats.agility) / 3) + Math.floor((attacker.stats.luck - defender.stats.luck) / 8) - terrainHitPenalty,
|
||||||
|
45,
|
||||||
|
98
|
||||||
|
);
|
||||||
|
const criticalRate = Phaser.Math.Clamp(
|
||||||
|
5 +
|
||||||
|
Math.floor((attacker.stats.luck - defender.stats.luck) / 7) +
|
||||||
|
Math.floor((attacker.stats.might - defender.stats.leadership) / 12) +
|
||||||
|
(action === 'attack' && getItem(attacker.equipment.weapon.itemId).rank === 'treasure' ? 2 : 0),
|
||||||
|
2,
|
||||||
|
28
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action,
|
action,
|
||||||
@@ -1239,6 +1265,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
defender,
|
defender,
|
||||||
damage,
|
damage,
|
||||||
hitRate,
|
hitRate,
|
||||||
|
criticalRate,
|
||||||
|
counterAvailable: this.canCounterAttack(defender, attacker, action),
|
||||||
terrainLabel: terrainRule.label
|
terrainLabel: terrainRule.label
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1247,17 +1275,24 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return this.resolveCombatAction(attacker, defender, 'attack');
|
return this.resolveCombatAction(attacker, defender, 'attack');
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveCombatAction(attacker: UnitData, defender: UnitData, action: DamageCommand): CombatResult {
|
private resolveCombatAction(attacker: UnitData, defender: UnitData, action: DamageCommand, isCounter = false): CombatResult {
|
||||||
const preview = this.combatPreview(attacker, defender, action);
|
const preview = this.combatPreview(attacker, defender, action);
|
||||||
const previousDefenderHp = defender.hp;
|
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 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', 6);
|
const armorGrowth = this.awardEquipmentExp(defender, 'armor', hit ? 6 : 3);
|
||||||
const characterGrowth = this.awardCharacterExp(attacker, this.characterExpGain(preview));
|
const characterGrowth = this.awardCharacterExp(attacker, this.characterExpGain(preview, damage, hit, critical));
|
||||||
const bondExp = attacker.faction === 'ally' ? this.recordBondAttackIntent(attacker, defender) : 0;
|
const bondExp = attacker.faction === 'ally' && !isCounter ? this.recordBondAttackIntent(attacker, defender) : 0;
|
||||||
|
|
||||||
defender.hp = Math.max(0, defender.hp - preview.damage);
|
defender.hp = Math.max(0, defender.hp - damage);
|
||||||
this.faceUnitToward(attacker, defender);
|
this.faceUnitToward(attacker, defender);
|
||||||
this.flashDamage(defender, preview.damage);
|
if (hit) {
|
||||||
|
this.flashDamage(defender, damage, critical);
|
||||||
|
} else {
|
||||||
|
this.flashMiss(defender);
|
||||||
|
}
|
||||||
|
|
||||||
if (defender.hp <= 0) {
|
if (defender.hp <= 0) {
|
||||||
this.applyDefeatedStyle(defender);
|
this.applyDefeatedStyle(defender);
|
||||||
@@ -1267,7 +1302,11 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...preview,
|
...preview,
|
||||||
|
damage,
|
||||||
previousDefenderHp,
|
previousDefenderHp,
|
||||||
|
hit,
|
||||||
|
critical,
|
||||||
|
isCounter,
|
||||||
defeated: defender.hp <= 0,
|
defeated: defender.hp <= 0,
|
||||||
characterGrowth,
|
characterGrowth,
|
||||||
attackerGrowth,
|
attackerGrowth,
|
||||||
@@ -1276,6 +1315,34 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveCounterAttack(result: CombatResult) {
|
||||||
|
if (result.defender.hp <= 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!this.canCounterAttack(result.defender, result.attacker, result.action)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.resolveCombatAction(result.defender, result.attacker, 'attack', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private canCounterAttack(defender: UnitData, attacker: UnitData, incomingAction: DamageCommand) {
|
||||||
|
if (incomingAction !== 'attack' || defender.hp <= 0 || attacker.hp <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = this.tileDistance(defender, attacker);
|
||||||
|
if (defender.classKey === 'archer' && distance <= 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.canAttack(defender, attacker);
|
||||||
|
}
|
||||||
|
|
||||||
|
private rollPercent(rate: number) {
|
||||||
|
return Phaser.Math.Between(1, 100) <= Phaser.Math.Clamp(rate, 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
private actionPower(unit: UnitData, action: DamageCommand) {
|
private actionPower(unit: UnitData, action: DamageCommand) {
|
||||||
if (action === 'strategy') {
|
if (action === 'strategy') {
|
||||||
return 10 + Math.floor(unit.stats.intelligence / 5) + this.equipmentStrategyBonus(unit);
|
return 10 + Math.floor(unit.stats.intelligence / 5) + this.equipmentStrategyBonus(unit);
|
||||||
@@ -1303,22 +1370,46 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return 5 + (getItem(unit.equipment.accessory.itemId).rank === 'treasure' ? 2 : 0);
|
return 5 + (getItem(unit.equipment.accessory.itemId).rank === 'treasure' ? 2 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private characterExpGain(result: CombatPreview) {
|
private characterExpGain(result: CombatPreview, damage: number, hit: boolean, critical: boolean) {
|
||||||
|
if (!hit) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
const levelGap = result.defender.level - result.attacker.level;
|
const levelGap = result.defender.level - result.attacker.level;
|
||||||
const actionBonus = result.action === 'attack' ? 10 : 8;
|
const actionBonus = result.action === 'attack' ? 10 : 8;
|
||||||
const defeatBonus = result.damage >= result.defender.hp ? 12 : 0;
|
const defeatBonus = damage >= result.defender.hp ? 12 : 0;
|
||||||
return Phaser.Math.Clamp(actionBonus + levelGap * 2 + defeatBonus, 4, 28);
|
const criticalBonus = critical ? 2 : 0;
|
||||||
|
return Phaser.Math.Clamp(actionBonus + levelGap * 2 + defeatBonus + criticalBonus, 4, 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatCombatResult(result: CombatResult) {
|
private formatCombatResult(result: CombatResult) {
|
||||||
|
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 defeated = result.defeated ? `\n${result.defender.name} 퇴각!` : `\n${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`;
|
||||||
const bond = result.bondExp > 0 ? `\n공명 경험치 +${result.bondExp}` : '';
|
const bond = result.bondExp > 0 ? `\n공명 경험치 +${result.bondExp}` : '';
|
||||||
return `${result.attacker.name} ${commandLabels[result.action]}\n${result.defender.name}에게 ${result.damage} 피해${defeated}\n장수 경험치 +${result.characterGrowth.amount}\n${this.formatEquipmentGrowth(result.attackerGrowth)}\n${this.formatEquipmentGrowth(result.defenderGrowth)}${bond}`;
|
const counter = result.counter ? `\n${this.formatCounterLine(result.counter)}` : '';
|
||||||
|
return `${result.attacker.name} ${commandLabels[result.action]}\n${outcome}${defeated}\n장수 경험치 +${result.characterGrowth.amount}\n${this.formatEquipmentGrowth(result.attackerGrowth)}\n${this.formatEquipmentGrowth(result.defenderGrowth)}${bond}${counter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatCombatOutcome(result: CombatResult) {
|
||||||
|
if (!result.hit) {
|
||||||
|
return `${result.defender.name}이 공격을 회피했습니다.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const critical = result.critical ? '치명타! ' : '';
|
||||||
|
return `${critical}${result.defender.name}에게 ${result.damage} 피해`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatCounterLine(result: CombatResult) {
|
||||||
|
const outcome = result.hit ? `${result.critical ? '치명타로 ' : ''}${result.damage} 피해` : '회피됨';
|
||||||
|
const defeated = result.defeated ? ` / ${result.defender.name} 퇴각` : ` / ${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`;
|
||||||
|
return `반격: ${result.attacker.name} -> ${result.defender.name} ${outcome}${defeated}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private combatGrowthLines(result: CombatResult) {
|
private combatGrowthLines(result: CombatResult) {
|
||||||
const characterLevel = result.characterGrowth.leveled ? ` / Lv ${result.characterGrowth.level}` : '';
|
const characterLevel = result.characterGrowth.leveled ? ` / Lv ${result.characterGrowth.level}` : '';
|
||||||
|
const verdict = result.hit ? `${result.critical ? '치명타 / ' : ''}명중 ${result.hitRate}%` : `회피됨 / 명중 ${result.hitRate}%`;
|
||||||
const lines = [
|
const lines = [
|
||||||
|
verdict,
|
||||||
`장수 경험치 +${result.characterGrowth.amount} (${result.characterGrowth.exp}/${result.characterGrowth.next})${characterLevel}`,
|
`장수 경험치 +${result.characterGrowth.amount} (${result.characterGrowth.exp}/${result.characterGrowth.next})${characterLevel}`,
|
||||||
this.formatEquipmentGrowth(result.attackerGrowth),
|
this.formatEquipmentGrowth(result.attackerGrowth),
|
||||||
this.formatEquipmentGrowth(result.defenderGrowth)
|
this.formatEquipmentGrowth(result.defenderGrowth)
|
||||||
@@ -1766,6 +1857,11 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
if (this.canAttack(enemy, target)) {
|
if (this.canAttack(enemy, target)) {
|
||||||
const result = this.resolveAttack(enemy, target);
|
const result = this.resolveAttack(enemy, target);
|
||||||
await this.playCombatCutIn(result);
|
await this.playCombatCutIn(result);
|
||||||
|
result.counter = this.resolveCounterAttack(result);
|
||||||
|
if (result.counter) {
|
||||||
|
await this.delay(180);
|
||||||
|
await this.playCombatCutIn(result.counter);
|
||||||
|
}
|
||||||
return this.formatEnemyCombatResult(result, behavior);
|
return this.formatEnemyCombatResult(result, behavior);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1833,8 +1929,10 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private formatEnemyCombatResult(result: CombatResult, behavior: EnemyAiBehavior) {
|
private formatEnemyCombatResult(result: CombatResult, behavior: EnemyAiBehavior) {
|
||||||
|
const outcome = result.hit ? `${result.critical ? '치명타 ' : ''}${result.damage} 피해` : '회피됨';
|
||||||
const defeated = result.defeated ? ` / ${result.defender.name} 퇴각` : ` / ${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`;
|
const defeated = result.defeated ? ` / ${result.defender.name} 퇴각` : ` / ${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`;
|
||||||
return `${result.attacker.name} ${this.enemyBehaviorLabel(behavior)} 공격: ${result.defender.name}에게 ${result.damage} 피해${defeated}`;
|
const counter = result.counter ? ` / ${this.formatCounterLine(result.counter)}` : '';
|
||||||
|
return `${result.attacker.name} ${this.enemyBehaviorLabel(behavior)} 공격: ${result.defender.name} ${outcome}${defeated}${counter}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async playCombatCutIn(result: CombatResult) {
|
private async playCombatCutIn(result: CombatResult) {
|
||||||
@@ -1876,7 +1974,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
stage.fillStyle(0x23472c, 1);
|
stage.fillStyle(0x23472c, 1);
|
||||||
stage.fillRect(left + 18, top + 82, panelWidth - 36, 18);
|
stage.fillRect(left + 18, top + 82, panelWidth - 36, 18);
|
||||||
|
|
||||||
const title = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 24, commandLabels[result.action], {
|
const titleText = result.isCounter ? '반격' : commandLabels[result.action];
|
||||||
|
const title = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 24, titleText, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '24px',
|
fontSize: '24px',
|
||||||
color: '#f4dfad',
|
color: '#f4dfad',
|
||||||
@@ -1910,7 +2009,11 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
await this.delay(180);
|
await this.delay(180);
|
||||||
await this.playCombatActionMotion(result, attackerSprite, defenderSprite, attackerX, defenderX, groundY, depth + 10);
|
await this.playCombatActionMotion(result, attackerSprite, defenderSprite, attackerX, defenderX, groundY, depth + 10);
|
||||||
|
if (result.hit) {
|
||||||
this.showCombatImpact(result, defenderX, groundY - 46, depth + 14);
|
this.showCombatImpact(result, defenderX, groundY - 46, depth + 14);
|
||||||
|
} else {
|
||||||
|
this.showCombatMiss(defenderX, groundY - 70, depth + 14);
|
||||||
|
}
|
||||||
|
|
||||||
await this.animateCombatGauge(defenderStatus.hpFill, result.previousDefenderHp / result.defender.maxHp, result.defender.hp / result.defender.maxHp, 420);
|
await this.animateCombatGauge(defenderStatus.hpFill, result.previousDefenderHp / result.defender.maxHp, result.defender.hp / result.defender.maxHp, 420);
|
||||||
defenderStatus.hpText.setText(`${result.defender.hp} / ${result.defender.maxHp}`);
|
defenderStatus.hpText.setText(`${result.defender.hp} / ${result.defender.maxHp}`);
|
||||||
@@ -1927,10 +2030,11 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
);
|
);
|
||||||
attackerStatus.expText.setText(`${result.characterGrowth.exp} / ${result.characterGrowth.next}`);
|
attackerStatus.expText.setText(`${result.characterGrowth.exp} / ${result.characterGrowth.next}`);
|
||||||
|
|
||||||
const resultText = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 252, `${result.damage} 피해`, {
|
const resultLabel = result.hit ? `${result.critical ? '치명타! ' : ''}${result.damage} 피해` : '회피!';
|
||||||
|
const resultText = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 252, resultLabel, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '22px',
|
fontSize: '22px',
|
||||||
color: '#ffdf7b',
|
color: result.hit ? (result.critical ? '#ff8f5f' : '#ffdf7b') : '#d9f1ff',
|
||||||
fontStyle: '700',
|
fontStyle: '700',
|
||||||
stroke: '#2b0909',
|
stroke: '#2b0909',
|
||||||
strokeThickness: 4
|
strokeThickness: 4
|
||||||
@@ -1938,23 +2042,13 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
resultText.setOrigin(0.5);
|
resultText.setOrigin(0.5);
|
||||||
resultText.setDepth(depth + 16);
|
resultText.setDepth(depth + 16);
|
||||||
|
|
||||||
const expText = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 386, `장수 경험치 +${result.characterGrowth.amount}${result.characterGrowth.leveled ? ` / Lv ${result.characterGrowth.level}` : ''}`, {
|
const growthText = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 330, this.combatGrowthLines(result).join('\n'), {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '17px',
|
fontSize: '14px',
|
||||||
color: '#d4dce6',
|
|
||||||
fontStyle: '700'
|
|
||||||
}));
|
|
||||||
expText.setOrigin(0.5);
|
|
||||||
expText.setDepth(depth + 16);
|
|
||||||
expText.setVisible(false);
|
|
||||||
|
|
||||||
const growthText = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 348, this.combatGrowthLines(result).join('\n'), {
|
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
||||||
fontSize: '15px',
|
|
||||||
color: '#d4dce6',
|
color: '#d4dce6',
|
||||||
fontStyle: '700',
|
fontStyle: '700',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
lineSpacing: 4
|
lineSpacing: 3
|
||||||
}));
|
}));
|
||||||
growthText.setOrigin(0.5, 0);
|
growthText.setOrigin(0.5, 0);
|
||||||
growthText.setDepth(depth + 16);
|
growthText.setDepth(depth + 16);
|
||||||
@@ -2061,7 +2155,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
) {
|
) {
|
||||||
const direction = result.attacker.faction === 'ally' ? 1 : -1;
|
const direction = result.attacker.faction === 'ally' ? 1 : -1;
|
||||||
if (result.action === 'attack') {
|
if (result.action === 'attack') {
|
||||||
soundDirector.playEffect('sword-slash', { volume: 0.52, stopAfterMs: 700 });
|
soundDirector.playEffect('sword-slash', { volume: result.hit ? 0.52 : 0.38, stopAfterMs: 700 });
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: attackerSprite,
|
targets: attackerSprite,
|
||||||
x: attackerX + direction * 84,
|
x: attackerX + direction * 84,
|
||||||
@@ -2072,8 +2166,9 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
await this.delay(180);
|
await this.delay(180);
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: defenderSprite,
|
targets: defenderSprite,
|
||||||
x: defenderX + direction * 12,
|
x: defenderX + direction * (result.hit ? 12 : 28),
|
||||||
duration: 70,
|
y: result.hit ? defenderSprite.y : defenderSprite.y - 10,
|
||||||
|
duration: result.hit ? 70 : 120,
|
||||||
yoyo: true
|
yoyo: true
|
||||||
});
|
});
|
||||||
await this.delay(230);
|
await this.delay(230);
|
||||||
@@ -2093,7 +2188,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.combatCutInObjects.push(projectile);
|
this.combatCutInObjects.push(projectile);
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: projectile,
|
targets: projectile,
|
||||||
x: defenderX - direction * 72,
|
x: defenderX - direction * (result.hit ? 72 : 12),
|
||||||
|
y: result.hit ? projectile.y : projectile.y - 24,
|
||||||
duration: 430,
|
duration: 430,
|
||||||
ease: 'Cubic.easeIn'
|
ease: 'Cubic.easeIn'
|
||||||
});
|
});
|
||||||
@@ -2129,13 +2225,15 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showCombatImpact(result: CombatResult, x: number, y: number, depth: number) {
|
private showCombatImpact(result: CombatResult, x: number, y: number, depth: number) {
|
||||||
soundDirector.playEffect('combat-impact', { volume: result.action === 'strategy' ? 0.3 : 0.48, stopAfterMs: 650 });
|
soundDirector.playEffect('combat-impact', { volume: result.critical ? 0.62 : result.action === 'strategy' ? 0.3 : 0.48, stopAfterMs: 650 });
|
||||||
|
|
||||||
const impact = this.trackCombatObject(this.add.star(x, y, 8, 12, 42, result.action === 'strategy' ? 0x6cc5ff : 0xffdf7b, 0.92));
|
const impact = this.trackCombatObject(
|
||||||
|
this.add.star(x, y, 8, 12, result.critical ? 54 : 42, result.critical ? 0xff8f5f : result.action === 'strategy' ? 0x6cc5ff : 0xffdf7b, 0.92)
|
||||||
|
);
|
||||||
impact.setDepth(depth);
|
impact.setDepth(depth);
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: impact,
|
targets: impact,
|
||||||
scale: 1.45,
|
scale: result.critical ? 1.7 : 1.45,
|
||||||
alpha: 0,
|
alpha: 0,
|
||||||
duration: 320,
|
duration: 320,
|
||||||
ease: 'Sine.easeOut',
|
ease: 'Sine.easeOut',
|
||||||
@@ -2143,6 +2241,27 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private showCombatMiss(x: number, y: number, depth: number) {
|
||||||
|
const miss = this.trackCombatObject(this.add.text(x, y, '회피', {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '24px',
|
||||||
|
color: '#d9f1ff',
|
||||||
|
fontStyle: '700',
|
||||||
|
stroke: '#071623',
|
||||||
|
strokeThickness: 4
|
||||||
|
}));
|
||||||
|
miss.setOrigin(0.5);
|
||||||
|
miss.setDepth(depth);
|
||||||
|
this.tweens.add({
|
||||||
|
targets: miss,
|
||||||
|
y: y - 20,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 460,
|
||||||
|
ease: 'Sine.easeOut',
|
||||||
|
onComplete: () => miss.destroy()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private animateCombatGauge(target: Phaser.GameObjects.Rectangle, fromRatio: number, toRatio: number, duration: number) {
|
private animateCombatGauge(target: Phaser.GameObjects.Rectangle, fromRatio: number, toRatio: number, duration: number) {
|
||||||
target.setScale(Phaser.Math.Clamp(fromRatio, 0, 1), 1);
|
target.setScale(Phaser.Math.Clamp(fromRatio, 0, 1), 1);
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
@@ -2574,6 +2693,10 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
if (!view) {
|
if (!view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (unit.hp <= 0) {
|
||||||
|
this.applyDefeatedStyle(unit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
view.sprite.postFX.clear();
|
view.sprite.postFX.clear();
|
||||||
view.sprite.clearTint();
|
view.sprite.clearTint();
|
||||||
@@ -2610,16 +2733,16 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.syncUnitMotion(unit, view, direction);
|
this.syncUnitMotion(unit, view, direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
private flashDamage(unit: UnitData, damage: number) {
|
private flashDamage(unit: UnitData, damage: number, critical = false) {
|
||||||
const view = this.unitViews.get(unit.id);
|
const view = this.unitViews.get(unit.id);
|
||||||
if (!view) {
|
if (!view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const popup = this.add.text(view.sprite.x, view.sprite.y - this.layout.tileSize * 0.5, `-${damage}`, {
|
const popup = this.add.text(view.sprite.x, view.sprite.y - this.layout.tileSize * 0.5, critical ? `치명 -${damage}` : `-${damage}`, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '20px',
|
fontSize: critical ? '22px' : '20px',
|
||||||
color: '#ffdf7b',
|
color: critical ? '#ff8f5f' : '#ffdf7b',
|
||||||
stroke: '#220909',
|
stroke: '#220909',
|
||||||
strokeThickness: 4,
|
strokeThickness: 4,
|
||||||
fontStyle: '700'
|
fontStyle: '700'
|
||||||
@@ -2647,6 +2770,41 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private flashMiss(unit: UnitData) {
|
||||||
|
const view = this.unitViews.get(unit.id);
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const popup = this.add.text(view.sprite.x, view.sprite.y - this.layout.tileSize * 0.5, '회피', {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '20px',
|
||||||
|
color: '#d9f1ff',
|
||||||
|
stroke: '#071623',
|
||||||
|
strokeThickness: 4,
|
||||||
|
fontStyle: '700'
|
||||||
|
});
|
||||||
|
popup.setOrigin(0.5);
|
||||||
|
popup.setDepth(30);
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: [view.sprite, view.label],
|
||||||
|
x: `+=${unit.faction === 'ally' ? -8 : 8}`,
|
||||||
|
duration: 90,
|
||||||
|
yoyo: true,
|
||||||
|
ease: 'Sine.easeOut'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: popup,
|
||||||
|
y: popup.y - 24,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 500,
|
||||||
|
ease: 'Sine.easeOut',
|
||||||
|
onComplete: () => popup.destroy()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private showTurnEndPrompt(message?: string) {
|
private showTurnEndPrompt(message?: string) {
|
||||||
this.hideTurnEndPrompt();
|
this.hideTurnEndPrompt();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user