Enhance combat growth feedback
This commit is contained in:
@@ -1147,6 +1147,11 @@ type EquipmentGrowthResult = {
|
||||
leveled: boolean;
|
||||
};
|
||||
|
||||
type LevelUpStatGain = {
|
||||
label: string;
|
||||
amount: number;
|
||||
};
|
||||
|
||||
type CharacterGrowthResult = {
|
||||
amount: number;
|
||||
previousLevel: number;
|
||||
@@ -1155,6 +1160,8 @@ type CharacterGrowthResult = {
|
||||
exp: number;
|
||||
next: number;
|
||||
leveled: boolean;
|
||||
levelUps: number;
|
||||
statGains: LevelUpStatGain[];
|
||||
};
|
||||
|
||||
type BondGrowthResult = {
|
||||
@@ -1169,6 +1176,8 @@ type BondGrowthResult = {
|
||||
};
|
||||
|
||||
type GrowthGaugeEntry = {
|
||||
kind: 'character' | 'equipment' | 'bond';
|
||||
slot?: EquipmentSlot;
|
||||
label: string;
|
||||
owner: string;
|
||||
amountLabel: string;
|
||||
@@ -1180,6 +1189,7 @@ type GrowthGaugeEntry = {
|
||||
next: number;
|
||||
leveled: boolean;
|
||||
color: number;
|
||||
statGains?: LevelUpStatGain[];
|
||||
};
|
||||
|
||||
type GrowthGaugeView = {
|
||||
@@ -1203,6 +1213,7 @@ type ResultGaugeAnimation = {
|
||||
exp: number;
|
||||
next: number;
|
||||
leveled: boolean;
|
||||
statGains?: LevelUpStatGain[];
|
||||
flashArea: {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -5007,6 +5018,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
exp: unit.exp,
|
||||
next: 100,
|
||||
leveled: unit.level > previousLevel,
|
||||
statGains: this.characterLevelUpStatGains(unit.level - previousLevel),
|
||||
flashArea: { x, y, width, height: 68, depth: depth + 3 }
|
||||
});
|
||||
}
|
||||
@@ -5200,7 +5212,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
animation.levelText.setText(`${animation.levelPrefix}${animation.level}`);
|
||||
animation.fill.setScale(0, 1);
|
||||
animation.valueText.setText(`0 / ${animation.next}`);
|
||||
this.showGrowthEventText(animation.eventText, `Lv ${animation.level} 달성!`);
|
||||
this.showGrowthEventText(animation.eventText, this.levelUpEventLabel(animation));
|
||||
this.flashResultGauge(animation);
|
||||
soundDirector.playLevelUp();
|
||||
await this.playResultLevelUpCelebration(animation);
|
||||
@@ -5238,20 +5250,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private async playResultLevelUpCelebration(animation: ResultGaugeAnimation) {
|
||||
if (!animation.unit) {
|
||||
await this.delay(180);
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, y, width, height, depth } = animation.flashArea;
|
||||
const spriteX = Math.min(x + width - 52, animation.valueText.x + 40);
|
||||
const spriteY = y + height / 2 + 2;
|
||||
this.createResultLevelUpBurst(spriteX, spriteY, depth + 4);
|
||||
const sprite = this.trackResultObject(
|
||||
this.add.sprite(spriteX, spriteY + 2, this.unitActionTexture(animation.unit), this.unitActionFrameIndex('south', this.celebrationPose(animation.unit)))
|
||||
);
|
||||
sprite.setDisplaySize(70, 70);
|
||||
sprite.setDepth(depth + 6);
|
||||
const banner = this.trackResultObject(this.add.text(spriteX, spriteY - 44, 'LEVEL UP', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '13px',
|
||||
@@ -5262,7 +5264,81 @@ export class BattleScene extends Phaser.Scene {
|
||||
}));
|
||||
banner.setOrigin(0.5);
|
||||
banner.setDepth(depth + 7);
|
||||
await this.playUnitCelebrationFrames(sprite, animation.unit);
|
||||
|
||||
let sprite: Phaser.GameObjects.Sprite | undefined;
|
||||
if (animation.unit) {
|
||||
sprite = this.trackResultObject(
|
||||
this.add.sprite(spriteX, spriteY + 2, this.unitActionTexture(animation.unit), this.unitActionFrameIndex('south', this.celebrationPose(animation.unit)))
|
||||
);
|
||||
sprite.setDisplaySize(70, 70);
|
||||
sprite.setDepth(depth + 6);
|
||||
}
|
||||
|
||||
await this.playResultLevelUpSpotlight(animation);
|
||||
if (sprite && animation.unit) {
|
||||
await this.playUnitCelebrationFrames(sprite, animation.unit);
|
||||
}
|
||||
}
|
||||
|
||||
private levelUpEventLabel(animation: ResultGaugeAnimation) {
|
||||
const stats = this.statGainLine(animation.statGains);
|
||||
return stats ? `Lv ${animation.level} 달성! ${stats}` : `Lv ${animation.level} 달성!`;
|
||||
}
|
||||
|
||||
private async playResultLevelUpSpotlight(animation: ResultGaugeAnimation) {
|
||||
const stats = this.statGainLine(animation.statGains);
|
||||
const depth = animation.flashArea.depth + 12;
|
||||
const container = this.trackResultObject(this.add.container(this.scale.width / 2, 146));
|
||||
container.setDepth(depth);
|
||||
container.setAlpha(0);
|
||||
container.setScale(0.9);
|
||||
|
||||
const width = 480;
|
||||
const bg = this.add.rectangle(0, 0, width, 96, 0x211624, 0.98);
|
||||
bg.setStrokeStyle(3, palette.gold, 0.96);
|
||||
const burst = this.add.star(-width / 2 + 56, -2, 8, 14, 42, 0xffdf7b, 0.78);
|
||||
const title = this.add.text(-width / 2 + 110, -34, animation.unit ? `${animation.unit.name} LEVEL UP` : 'LEVEL UP', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '27px',
|
||||
color: '#fff2b8',
|
||||
fontStyle: '700',
|
||||
stroke: '#2b1606',
|
||||
strokeThickness: 5
|
||||
});
|
||||
const levelText = this.add.text(-width / 2 + 110, 0, `${animation.levelPrefix}${animation.previousLevel} → ${animation.levelPrefix}${animation.level}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '17px',
|
||||
color: '#f4dfad',
|
||||
fontStyle: '700'
|
||||
});
|
||||
const statText = this.add.text(-width / 2 + 110, 26, stats ?? '숙련 효과 강화', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '16px',
|
||||
color: stats ? '#a8ffd0' : '#d8b15f',
|
||||
fontStyle: '700',
|
||||
stroke: '#05070a',
|
||||
strokeThickness: 3
|
||||
});
|
||||
container.add([bg, burst, title, levelText, statText]);
|
||||
|
||||
if (animation.unit) {
|
||||
const sprite = this.add.sprite(-width / 2 + 56, 10, this.unitActionTexture(animation.unit), this.unitActionFrameIndex('south', this.celebrationPose(animation.unit)));
|
||||
sprite.setDisplaySize(76, 76);
|
||||
container.add(sprite);
|
||||
}
|
||||
|
||||
this.tweens.add({ targets: burst, angle: 240, scale: 1.34, alpha: 0.16, duration: 760, ease: 'Sine.easeOut' });
|
||||
this.tweens.add({ targets: container, alpha: 1, scale: 1.04, duration: 180, ease: 'Back.easeOut' });
|
||||
await this.delay(760);
|
||||
this.tweens.add({
|
||||
targets: container,
|
||||
alpha: 0,
|
||||
y: container.y - 18,
|
||||
duration: 240,
|
||||
ease: 'Sine.easeIn',
|
||||
onComplete: () => container.destroy()
|
||||
});
|
||||
await this.delay(260);
|
||||
}
|
||||
|
||||
private createResultLevelUpBurst(x: number, y: number, depth: number) {
|
||||
@@ -5748,7 +5824,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
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}${equipment}\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${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.attackerGrowth)}\n${this.formatEquipmentGrowth(result.defenderGrowth)}${bond}${counter}`;
|
||||
}
|
||||
|
||||
private formatCombatOutcome(result: CombatResult) {
|
||||
@@ -5767,11 +5843,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private combatGrowthLines(result: CombatResult) {
|
||||
const characterLevel = result.characterGrowth.leveled ? ` / Lv ${result.characterGrowth.level}` : '';
|
||||
const verdict = result.hit ? `${result.critical ? '치명타 / ' : ''}명중 ${result.hitRate}%` : `회피됨 / 명중 ${result.hitRate}%`;
|
||||
const lines = [
|
||||
verdict,
|
||||
`장수 경험치 +${result.characterGrowth.amount} (${result.characterGrowth.exp}/${result.characterGrowth.next})${characterLevel}`,
|
||||
this.formatCharacterGrowth(result.characterGrowth),
|
||||
this.formatEquipmentGrowth(result.attackerGrowth),
|
||||
this.formatEquipmentGrowth(result.defenderGrowth)
|
||||
];
|
||||
@@ -5791,7 +5866,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
result.usable.effect === 'heal'
|
||||
? `${result.target.name} 병력 +${result.healAmount} (${result.target.hp}/${result.target.maxHp})`
|
||||
: `${result.target.name} 강화: 공격 +${result.buff?.attackBonus ?? 0} / 명중 +${result.buff?.hitBonus ?? 0} / 치명 +${result.buff?.criticalBonus ?? 0}`;
|
||||
return `${result.user.name} ${result.usable.name}\n${effect}\n장수 경험치 +${result.characterGrowth.amount}\n${this.formatEquipmentGrowth(result.equipmentGrowth)}`;
|
||||
return `${result.user.name} ${result.usable.name}\n${effect}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.equipmentGrowth)}`;
|
||||
}
|
||||
|
||||
private commandResultMessage(unit: UnitData, command: BattleCommand) {
|
||||
@@ -7023,6 +7098,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
defenderStatus.hpText.setText(`${result.previousDefenderHp} / ${result.defender.maxHp}`);
|
||||
attackerStatus.expFill.setScale(result.characterGrowth.previousExp / result.characterGrowth.next, 1);
|
||||
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 + 106, attackerX, groundY - 70, depth + 15);
|
||||
@@ -7035,6 +7111,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
} else {
|
||||
this.showCombatMiss(defenderX, groundY - 70, depth + 14);
|
||||
}
|
||||
void this.playImmediateGrowthPopups(growthEntries, left + panelWidth / 2, top + 214, depth + 22);
|
||||
|
||||
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}`);
|
||||
@@ -7071,7 +7148,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
y: top + 410,
|
||||
width: panelWidth - 172,
|
||||
title: '성장 결과',
|
||||
entries: this.combatGrowthEntries(result),
|
||||
entries: growthEntries,
|
||||
celebrant: this.growthCelebrant(result),
|
||||
depth: depth + 18
|
||||
});
|
||||
@@ -7132,6 +7209,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
const targetStatus = this.renderCombatStatusPanel(result.target, left + panelWidth / 2 - 196, top + 190, 392);
|
||||
targetStatus.hpFill.setScale(result.previousTargetHp / result.target.maxHp, 1);
|
||||
targetStatus.hpText.setText(`${result.previousTargetHp} / ${result.target.maxHp}`);
|
||||
const growthEntries = this.supportGrowthEntries(result);
|
||||
|
||||
await this.delay(250);
|
||||
await this.animateCombatGauge(targetStatus.hpFill, result.previousTargetHp / result.target.maxHp, result.target.hp / result.target.maxHp, 480);
|
||||
@@ -7151,13 +7229,14 @@ export class BattleScene extends Phaser.Scene {
|
||||
}));
|
||||
resultText.setOrigin(0.5);
|
||||
resultText.setDepth(depth + 5);
|
||||
void this.playImmediateGrowthPopups(growthEntries, left + panelWidth / 2, top + 156, depth + 8);
|
||||
|
||||
await this.playGrowthRewardPanel({
|
||||
x: left + 64,
|
||||
y: top + 322,
|
||||
width: panelWidth - 128,
|
||||
title: '성장 결과',
|
||||
entries: this.supportGrowthEntries(result),
|
||||
entries: growthEntries,
|
||||
celebrant: result.user,
|
||||
depth: depth + 8
|
||||
});
|
||||
@@ -7261,6 +7340,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
private characterGrowthEntry(unit: UnitData, growth: CharacterGrowthResult): GrowthGaugeEntry {
|
||||
return {
|
||||
kind: 'character',
|
||||
label: '장수 경험치',
|
||||
owner: unit.name,
|
||||
amountLabel: `+${growth.amount}`,
|
||||
@@ -7271,12 +7351,15 @@ export class BattleScene extends Phaser.Scene {
|
||||
exp: growth.exp,
|
||||
next: growth.next,
|
||||
leveled: growth.leveled,
|
||||
color: 0xd8b15f
|
||||
color: 0xd8b15f,
|
||||
statGains: growth.statGains
|
||||
};
|
||||
}
|
||||
|
||||
private equipmentGrowthEntry(unit: UnitData, growth: EquipmentGrowthResult): GrowthGaugeEntry {
|
||||
return {
|
||||
kind: 'equipment',
|
||||
slot: growth.slot,
|
||||
label: `${equipmentSlotLabels[growth.slot]} ${growth.itemName}`,
|
||||
owner: unit.name,
|
||||
amountLabel: `+${growth.amount}`,
|
||||
@@ -7293,6 +7376,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
private bondGrowthEntry(growth: BondGrowthResult): GrowthGaugeEntry {
|
||||
return {
|
||||
kind: 'bond',
|
||||
label: '공명 경험치',
|
||||
owner: growth.label,
|
||||
amountLabel: `+${growth.amount}`,
|
||||
@@ -7317,6 +7401,93 @@ export class BattleScene extends Phaser.Scene {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private playImmediateGrowthPopups(entries: GrowthGaugeEntry[], x: number, y: number, depth: number) {
|
||||
const visibleEntries = entries.filter((entry) => entry.amountLabel !== '+0' || entry.leveled).slice(0, 5);
|
||||
if (visibleEntries.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
soundDirector.playGrowthTick();
|
||||
visibleEntries.forEach((entry, index) => {
|
||||
const label = this.growthPopupLabel(entry);
|
||||
const statLine = entry.leveled ? this.statGainLine(entry.statGains) : undefined;
|
||||
const width = Math.min(500, Math.max(230, label.length * 13 + (statLine ? 72 : 34)));
|
||||
const rowY = y + index * 31;
|
||||
const container = this.trackCombatObject(this.add.container(x, rowY));
|
||||
container.setAlpha(0);
|
||||
container.setScale(0.92);
|
||||
container.setDepth(depth + index);
|
||||
|
||||
const bg = this.add.rectangle(0, 0, width, statLine ? 36 : 28, 0x101820, 0.94);
|
||||
bg.setStrokeStyle(2, entry.leveled ? palette.gold : entry.color, entry.leveled ? 0.96 : 0.74);
|
||||
const text = this.add.text(statLine ? -width / 2 + 18 : 0, statLine ? -12 : -9, label, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: entry.leveled ? '16px' : '15px',
|
||||
color: entry.leveled ? '#fff2b8' : '#f4dfad',
|
||||
fontStyle: '700',
|
||||
stroke: '#05070a',
|
||||
strokeThickness: 3
|
||||
});
|
||||
text.setOrigin(statLine ? 0 : 0.5, 0);
|
||||
container.add([bg, text]);
|
||||
|
||||
if (statLine) {
|
||||
const statText = this.add.text(-width / 2 + 18, 8, statLine, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '13px',
|
||||
color: '#a8ffd0',
|
||||
fontStyle: '700',
|
||||
stroke: '#05070a',
|
||||
strokeThickness: 3
|
||||
});
|
||||
container.add(statText);
|
||||
}
|
||||
|
||||
this.tweens.add({
|
||||
targets: container,
|
||||
alpha: 1,
|
||||
scale: entry.leveled ? 1.08 : 1,
|
||||
y: rowY - 6,
|
||||
delay: index * 82,
|
||||
duration: 150,
|
||||
ease: 'Back.easeOut'
|
||||
});
|
||||
this.tweens.add({
|
||||
targets: container,
|
||||
alpha: 0,
|
||||
y: rowY - 34,
|
||||
delay: 620 + index * 82,
|
||||
duration: 420,
|
||||
ease: 'Sine.easeIn',
|
||||
onComplete: () => container.destroy()
|
||||
});
|
||||
});
|
||||
|
||||
return this.delay(visibleEntries.length * 82 + 720);
|
||||
}
|
||||
|
||||
private growthPopupLabel(entry: GrowthGaugeEntry) {
|
||||
if (entry.kind === 'character') {
|
||||
return `${entry.owner} 경험치 ${entry.amountLabel}${entry.leveled ? ' LEVEL UP' : ''}`;
|
||||
}
|
||||
if (entry.kind === 'bond') {
|
||||
return `${entry.owner} 공명 ${entry.amountLabel}${entry.leveled ? ' LEVEL UP' : ''}`;
|
||||
}
|
||||
|
||||
const mastery =
|
||||
entry.slot === 'weapon'
|
||||
? '무기 숙련'
|
||||
: entry.slot === 'armor'
|
||||
? '방어구 숙련'
|
||||
: '보조구 숙련';
|
||||
return `${entry.owner} ${mastery} ${entry.amountLabel}${entry.leveled ? ' LEVEL UP' : ''}`;
|
||||
}
|
||||
|
||||
private statGainLine(gains?: LevelUpStatGain[]) {
|
||||
const visibleGains = gains?.filter((gain) => gain.amount > 0) ?? [];
|
||||
return visibleGains.length > 0 ? visibleGains.map((gain) => `${gain.label} +${gain.amount}`).join(' ') : undefined;
|
||||
}
|
||||
|
||||
private async playGrowthRewardPanel(config: {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -7350,12 +7521,17 @@ export class BattleScene extends Phaser.Scene {
|
||||
const levelEvent = entries.some((entry) => entry.leveled);
|
||||
const rowWidth = config.width - (levelEvent && config.celebrant ? 126 : 36);
|
||||
const views = entries.map((entry, index) => this.renderGrowthGaugeRow(entry, config.x + 18, config.y + 42 + index * rowHeight, rowWidth, config.depth + 1));
|
||||
const spotlightEntry = entries.find((entry) => entry.kind === 'character' && entry.leveled) ?? entries.find((entry) => entry.leveled);
|
||||
|
||||
let celebration: Phaser.GameObjects.Sprite | undefined;
|
||||
if (levelEvent && config.celebrant) {
|
||||
celebration = this.createGrowthCelebrationSprite(config.celebrant, config.x + config.width - 46, config.y + 24, config.depth + 5);
|
||||
}
|
||||
|
||||
const spotlight = spotlightEntry
|
||||
? this.playCombatLevelUpSpotlight(spotlightEntry, config.celebrant, config.x + config.width / 2, config.y - 88, config.width, config.depth + 8)
|
||||
: undefined;
|
||||
|
||||
for (let index = 0; index < entries.length; index += 1) {
|
||||
await this.animateGrowthEntry(views[index], entries[index], 540 + index * 70);
|
||||
if (entries[index].leveled) {
|
||||
@@ -7366,6 +7542,69 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (celebration && config.celebrant) {
|
||||
await this.playGrowthCelebration(celebration, config.celebrant);
|
||||
}
|
||||
|
||||
if (spotlight) {
|
||||
await spotlight;
|
||||
}
|
||||
}
|
||||
|
||||
private async playCombatLevelUpSpotlight(entry: GrowthGaugeEntry, celebrant: UnitData | undefined, x: number, y: number, width: number, depth: number) {
|
||||
const panelWidth = Math.min(540, width - 34);
|
||||
const panelHeight = 104;
|
||||
const container = this.trackCombatObject(this.add.container(x, y));
|
||||
container.setDepth(depth);
|
||||
container.setAlpha(0);
|
||||
container.setScale(0.9);
|
||||
|
||||
const bg = this.add.rectangle(0, 0, panelWidth, panelHeight, 0x211624, 0.97);
|
||||
bg.setStrokeStyle(3, palette.gold, 0.96);
|
||||
const glow = this.add.circle(-panelWidth / 2 + 58, 0, 42, 0xffdf7b, 0.12);
|
||||
const title = this.add.text(celebrant ? -panelWidth / 2 + 112 : 0, -36, `${entry.owner} LEVEL UP`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '25px',
|
||||
color: '#fff2b8',
|
||||
fontStyle: '700',
|
||||
stroke: '#2b1606',
|
||||
strokeThickness: 5
|
||||
});
|
||||
title.setOrigin(celebrant ? 0 : 0.5, 0);
|
||||
const levelText = this.add.text(celebrant ? -panelWidth / 2 + 112 : 0, -3, `Lv ${entry.previousLevel} → Lv ${entry.level}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '18px',
|
||||
color: '#f4dfad',
|
||||
fontStyle: '700'
|
||||
});
|
||||
levelText.setOrigin(celebrant ? 0 : 0.5, 0);
|
||||
const statText = this.add.text(celebrant ? -panelWidth / 2 + 112 : 0, 25, this.statGainLine(entry.statGains) ?? `${entry.label} 효과 강화`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '16px',
|
||||
color: '#a8ffd0',
|
||||
fontStyle: '700',
|
||||
stroke: '#05070a',
|
||||
strokeThickness: 3
|
||||
});
|
||||
statText.setOrigin(celebrant ? 0 : 0.5, 0);
|
||||
container.add([bg, glow, title, levelText, statText]);
|
||||
|
||||
if (celebrant) {
|
||||
const sprite = this.add.sprite(-panelWidth / 2 + 56, 6, this.unitActionTexture(celebrant), this.unitActionFrameIndex('south', this.celebrationPose(celebrant)));
|
||||
sprite.setDisplaySize(82, 82);
|
||||
const burst = this.add.star(-panelWidth / 2 + 56, -12, 8, 12, 34, 0xffdf7b, 0.72);
|
||||
container.add([burst, sprite]);
|
||||
this.tweens.add({ targets: burst, angle: 220, scale: 1.32, alpha: 0.18, duration: 760, ease: 'Sine.easeOut' });
|
||||
}
|
||||
|
||||
this.tweens.add({ targets: container, alpha: 1, scale: 1.04, duration: 180, ease: 'Back.easeOut' });
|
||||
await this.delay(740);
|
||||
this.tweens.add({
|
||||
targets: container,
|
||||
alpha: 0,
|
||||
y: y - 16,
|
||||
duration: 240,
|
||||
ease: 'Sine.easeIn',
|
||||
onComplete: () => container.destroy()
|
||||
});
|
||||
await this.delay(260);
|
||||
}
|
||||
|
||||
private renderGrowthGaugeRow(entry: GrowthGaugeEntry, x: number, y: number, width: number, depth: number): GrowthGaugeView {
|
||||
@@ -7432,7 +7671,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
view.levelText.setText(`Lv ${entry.level}`);
|
||||
view.fill.setScale(0, 1);
|
||||
view.valueText.setText(`0/${entry.next}`);
|
||||
this.showGrowthEventText(view.eventText, `Lv ${entry.level} 달성!`);
|
||||
this.showGrowthEventText(view.eventText, this.growthLevelUpEventLabel(entry));
|
||||
soundDirector.playLevelUp();
|
||||
await this.delay(260);
|
||||
await this.animateGrowthGauge(view.fill, view.valueText, 0, entry.exp / entry.next, 0, entry.exp, entry.next, false, Math.max(260, Math.floor(duration * 0.48)));
|
||||
@@ -7443,6 +7682,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
view.levelText.setText(`Lv ${entry.level}`);
|
||||
}
|
||||
|
||||
private growthLevelUpEventLabel(entry: GrowthGaugeEntry) {
|
||||
const stats = this.statGainLine(entry.statGains);
|
||||
return stats ? `Lv ${entry.level} 달성! ${stats}` : `Lv ${entry.level} 달성!`;
|
||||
}
|
||||
|
||||
private showGrowthEventText(text: Phaser.GameObjects.Text, label: string) {
|
||||
text.setText(label);
|
||||
text.setAlpha(1);
|
||||
@@ -8493,11 +8737,16 @@ export class BattleScene extends Phaser.Scene {
|
||||
const previousExp = unit.exp;
|
||||
let exp = unit.exp + amount;
|
||||
let leveled = false;
|
||||
let levelUps = 0;
|
||||
|
||||
while (unit.level < maxCharacterLevel && exp >= 100) {
|
||||
exp -= 100;
|
||||
unit.level += 1;
|
||||
unit.maxHp += 2;
|
||||
unit.hp = Math.min(unit.maxHp, unit.hp + 2);
|
||||
unit.attack += 1;
|
||||
leveled = true;
|
||||
levelUps += 1;
|
||||
}
|
||||
|
||||
unit.exp = unit.level >= maxCharacterLevel ? Math.min(exp, 100) : exp;
|
||||
@@ -8509,15 +8758,34 @@ export class BattleScene extends Phaser.Scene {
|
||||
level: unit.level,
|
||||
exp: unit.exp,
|
||||
next: 100,
|
||||
leveled
|
||||
leveled,
|
||||
levelUps,
|
||||
statGains: this.characterLevelUpStatGains(levelUps)
|
||||
};
|
||||
}
|
||||
|
||||
private characterLevelUpStatGains(levelUps: number): LevelUpStatGain[] {
|
||||
if (levelUps <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: '병력', amount: levelUps * 2 },
|
||||
{ label: '공격', amount: levelUps }
|
||||
];
|
||||
}
|
||||
|
||||
private formatEquipmentGrowth(result: EquipmentGrowthResult) {
|
||||
const levelUp = result.leveled ? ` / Lv ${result.level} 달성` : '';
|
||||
return `${equipmentSlotLabels[result.slot]} ${result.itemName} 경험치 +${result.amount} (${result.exp}/${result.next})${levelUp}`;
|
||||
}
|
||||
|
||||
private formatCharacterGrowth(result: CharacterGrowthResult) {
|
||||
const levelUp = result.leveled ? ` / Lv ${result.level} 달성` : '';
|
||||
const statGains = this.statGainLine(result.statGains);
|
||||
return `장수 경험치 +${result.amount} (${result.exp}/${result.next})${levelUp}${statGains ? ` / ${statGains}` : ''}`;
|
||||
}
|
||||
|
||||
private findNearestEnemy(unit: UnitData) {
|
||||
const enemies = battleUnits.filter((candidate) => candidate.faction === 'enemy' && candidate.hp > 0);
|
||||
return enemies.sort((a, b) => this.tileDistance(unit, a) - this.tileDistance(unit, b))[0];
|
||||
|
||||
Reference in New Issue
Block a user