Animate battle result growth settlement
This commit is contained in:
@@ -226,6 +226,28 @@ type GrowthGaugeView = {
|
||||
eventText: Phaser.GameObjects.Text;
|
||||
};
|
||||
|
||||
type ResultGaugeAnimation = {
|
||||
fill: Phaser.GameObjects.Rectangle;
|
||||
valueText: Phaser.GameObjects.Text;
|
||||
levelText: Phaser.GameObjects.Text;
|
||||
eventText: Phaser.GameObjects.Text;
|
||||
levelPrefix: string;
|
||||
previousLevel: number;
|
||||
previousExp: number;
|
||||
previousNext: number;
|
||||
level: number;
|
||||
exp: number;
|
||||
next: number;
|
||||
leveled: boolean;
|
||||
flashArea: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
depth: number;
|
||||
};
|
||||
};
|
||||
|
||||
type CombatPreview = {
|
||||
action: DamageCommand;
|
||||
usable?: BattleUsable;
|
||||
@@ -551,6 +573,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
private turnPromptObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private combatCutInObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private resultObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private resultGaugeAnimations: ResultGaugeAnimation[] = [];
|
||||
private resultAnimationToken = 0;
|
||||
private resultSettlementText?: Phaser.GameObjects.Text;
|
||||
private saveSlotPanelObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private unitViews = new Map<string, UnitView>();
|
||||
private actedUnitIds = new Set<string>();
|
||||
@@ -1940,6 +1965,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
private showBattleResult(outcome: BattleOutcome) {
|
||||
this.hideBattleResult();
|
||||
this.resultAnimationToken += 1;
|
||||
this.resultGaugeAnimations = [];
|
||||
const animationToken = this.resultAnimationToken;
|
||||
|
||||
const depth = 120;
|
||||
const panelWidth = 1040;
|
||||
@@ -1996,8 +2024,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
this.renderResultObjectivePanel(objectives, left + 44, top + 178, 456, depth + 2);
|
||||
this.renderResultRewardPanel(outcome, objectives, left + 526, top + 178, 470, depth + 2);
|
||||
this.renderResultBondStrip(left + 44, top + 318, panelWidth - 88, depth + 2);
|
||||
|
||||
const unitTitle = this.trackResultObject(this.add.text(left + 44, top + 332, '장수 성장', {
|
||||
const unitTitle = this.trackResultObject(this.add.text(left + 44, top + 354, '장수 성장', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '21px',
|
||||
color: '#f2e3bf',
|
||||
@@ -2006,9 +2035,17 @@ export class BattleScene extends Phaser.Scene {
|
||||
unitTitle.setDepth(depth + 2);
|
||||
|
||||
allies.forEach((unit, index) => {
|
||||
this.renderResultUnitRow(unit, left + 44, top + 366 + index * 70, panelWidth - 88, depth + 2);
|
||||
this.renderResultUnitRow(unit, left + 44, top + 386 + index * 70, panelWidth - 88, depth + 2);
|
||||
});
|
||||
|
||||
this.resultSettlementText = this.trackResultObject(this.add.text(left + 44, top + 604, '성장 정산 대기', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '14px',
|
||||
color: '#d8b15f',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
this.resultSettlementText.setDepth(depth + 2);
|
||||
|
||||
if (outcome === 'victory') {
|
||||
this.addResultButton('군영으로', left + panelWidth - 422, top + 612, 132, () => {
|
||||
soundDirector.playSelect();
|
||||
@@ -2023,11 +2060,16 @@ export class BattleScene extends Phaser.Scene {
|
||||
soundDirector.playSelect();
|
||||
this.scene.start('TitleScene');
|
||||
}, depth + 3);
|
||||
|
||||
void this.playResultSettlementAnimation(animationToken);
|
||||
}
|
||||
|
||||
private hideBattleResult() {
|
||||
this.resultAnimationToken += 1;
|
||||
this.resultObjects.forEach((object) => object.destroy());
|
||||
this.resultObjects = [];
|
||||
this.resultGaugeAnimations = [];
|
||||
this.resultSettlementText = undefined;
|
||||
}
|
||||
|
||||
private resultObjectives(outcome: BattleOutcome): BattleObjectiveResult[] {
|
||||
@@ -2233,6 +2275,106 @@ export class BattleScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
private renderResultBondStrip(x: number, y: number, width: number, depth: number) {
|
||||
const bonds = Array.from(this.bondStates.values());
|
||||
const bg = this.trackResultObject(this.add.rectangle(x, y, width, 30, 0x101820, 0.9));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, palette.gold, 0.46);
|
||||
|
||||
const title = this.trackResultObject(this.add.text(x + 12, y + 7, '공명 성장', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '13px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
title.setDepth(depth + 1);
|
||||
|
||||
if (bonds.length === 0) {
|
||||
const empty = this.trackResultObject(this.add.text(x + 110, y + 7, '공명 없음', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '13px',
|
||||
color: '#9fb0bf'
|
||||
}));
|
||||
empty.setDepth(depth + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
const startX = x + 104;
|
||||
const columnWidth = Math.floor((width - 118) / bonds.length);
|
||||
bonds.forEach((bond, index) => {
|
||||
const initial = initialBattleBonds.find((candidate) => candidate.id === bond.id);
|
||||
const previousLevel = initial?.level ?? bond.level;
|
||||
const previousExp = initial?.exp ?? bond.exp;
|
||||
const gained = initial ? Math.max(0, this.characterProgressScore(bond.level, bond.exp) - this.characterProgressScore(initial.level, initial.exp)) : bond.battleExp;
|
||||
const colX = startX + index * columnWidth;
|
||||
const [first, second] = bond.unitIds.map((unitId) => this.unitName(unitId));
|
||||
const label = this.trackResultObject(this.add.text(colX, y + 6, `${first}·${second}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '12px',
|
||||
color: '#d4dce6',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
label.setDepth(depth + 1);
|
||||
|
||||
const amount = this.trackResultObject(this.add.text(colX + 86, y + 6, gained > 0 ? `+${gained}` : '+0', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '12px',
|
||||
color: gained > 0 ? '#d8b15f' : '#6f7a84',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
amount.setDepth(depth + 1);
|
||||
|
||||
const fill = this.renderResultProgressGauge(colX + 122, y + 11, 82, 7, previousExp / 100, 0x83d6ff, depth + 1);
|
||||
const valueText = this.trackResultObject(this.add.text(colX + 210, y + 4, `${previousExp}/100`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '11px',
|
||||
color: '#f0e4c8',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
valueText.setOrigin(1, 0);
|
||||
valueText.setDepth(depth + 1);
|
||||
const levelText = this.trackResultObject(this.add.text(colX + columnWidth - 8, y + 6, `Lv ${previousLevel}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '12px',
|
||||
color: '#f4dfad',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
levelText.setOrigin(1, 0);
|
||||
levelText.setDepth(depth + 1);
|
||||
|
||||
const eventText = this.trackResultObject(this.add.text(colX + 164, y + 2, '', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '12px',
|
||||
color: '#fff2b8',
|
||||
fontStyle: '700',
|
||||
stroke: '#05070a',
|
||||
strokeThickness: 3
|
||||
}));
|
||||
eventText.setOrigin(0.5);
|
||||
eventText.setDepth(depth + 4);
|
||||
eventText.setAlpha(0);
|
||||
|
||||
if (gained > 0 || bond.level > previousLevel) {
|
||||
this.queueResultGaugeAnimation({
|
||||
fill,
|
||||
valueText,
|
||||
levelText,
|
||||
eventText,
|
||||
levelPrefix: 'Lv ',
|
||||
previousLevel,
|
||||
previousExp,
|
||||
previousNext: 100,
|
||||
level: bond.level,
|
||||
exp: bond.exp,
|
||||
next: 100,
|
||||
leveled: bond.level > previousLevel,
|
||||
flashArea: { x: colX - 6, y: y + 3, width: Math.max(220, columnWidth - 12), height: 24, depth: depth + 3 }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private renderResultMetric(label: string, value: string, x: number, y: number, width: number, depth: number) {
|
||||
const bg = this.trackResultObject(this.add.rectangle(x, y, width, 48, 0x16212d, 0.94));
|
||||
bg.setOrigin(0);
|
||||
@@ -2258,6 +2400,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
private renderResultUnitRow(unit: UnitData, x: number, y: number, width: number, depth: number) {
|
||||
const alive = unit.hp > 0;
|
||||
const initial = initialBattleUnits.find((candidate) => candidate.id === unit.id);
|
||||
const bg = this.trackResultObject(this.add.rectangle(x, y, width, 68, 0x101820, alive ? 0.94 : 0.66));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
@@ -2286,6 +2429,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
}));
|
||||
hpText.setDepth(depth + 1);
|
||||
|
||||
const previousLevel = initial?.level ?? unit.level;
|
||||
const previousExp = initial?.exp ?? unit.exp;
|
||||
const characterGained = initial ? Math.max(0, this.characterProgressScore(unit.level, unit.exp) - this.characterProgressScore(initial.level, initial.exp)) : 0;
|
||||
const expText = this.trackResultObject(this.add.text(x + 392, y + 9, this.characterResultText(unit), {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '14px',
|
||||
@@ -2293,20 +2439,105 @@ export class BattleScene extends Phaser.Scene {
|
||||
fontStyle: '700'
|
||||
}));
|
||||
expText.setDepth(depth + 1);
|
||||
this.renderResultProgressGauge(x + 392, y + 36, 200, 8, unit.exp / 100, palette.gold, depth + 1);
|
||||
const characterFill = this.renderResultProgressGauge(x + 392, y + 36, 158, 8, previousExp / 100, palette.gold, depth + 1);
|
||||
const characterValueText = this.trackResultObject(this.add.text(x + 564, y + 28, `${previousExp} / 100`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '11px',
|
||||
color: '#f0e4c8',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
characterValueText.setOrigin(1, 0);
|
||||
characterValueText.setDepth(depth + 1);
|
||||
const characterLevelText = this.trackResultObject(this.add.text(x + 570, y + 9, `Lv ${previousLevel}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '12px',
|
||||
color: '#f4dfad',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
characterLevelText.setDepth(depth + 1);
|
||||
const characterEventText = this.trackResultObject(this.add.text(x + 476, y + 31, '', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '14px',
|
||||
color: '#fff2b8',
|
||||
fontStyle: '700',
|
||||
stroke: '#05070a',
|
||||
strokeThickness: 3
|
||||
}));
|
||||
characterEventText.setOrigin(0.5);
|
||||
characterEventText.setDepth(depth + 4);
|
||||
characterEventText.setAlpha(0);
|
||||
if (characterGained > 0 || unit.level > previousLevel) {
|
||||
this.queueResultGaugeAnimation({
|
||||
fill: characterFill,
|
||||
valueText: characterValueText,
|
||||
levelText: characterLevelText,
|
||||
eventText: characterEventText,
|
||||
levelPrefix: 'Lv ',
|
||||
previousLevel,
|
||||
previousExp,
|
||||
previousNext: 100,
|
||||
level: unit.level,
|
||||
exp: unit.exp,
|
||||
next: 100,
|
||||
leveled: unit.level > previousLevel,
|
||||
flashArea: { x, y, width, height: 68, depth: depth + 3 }
|
||||
});
|
||||
}
|
||||
|
||||
equipmentSlots.forEach((slot, index) => {
|
||||
const state = unit.equipment[slot];
|
||||
const initialState = initial?.equipment[slot];
|
||||
const previousEquipmentLevel = initialState?.level ?? state.level;
|
||||
const previousEquipmentExp = initialState?.exp ?? state.exp;
|
||||
const previousNext = equipmentExpToNext(previousEquipmentLevel);
|
||||
const next = equipmentExpToNext(state.level);
|
||||
const gained = initialState ? Math.max(0, this.equipmentProgressScore(state.level, state.exp) - this.equipmentProgressScore(initialState.level, initialState.exp)) : 0;
|
||||
const color = slot === 'weapon' ? 0xffc45f : slot === 'armor' ? 0x77b7ff : 0xb7df7a;
|
||||
const slotX = x + 632 + index * 96;
|
||||
const label = this.trackResultObject(this.add.text(slotX, y + 9, equipmentSlotLabels[slot], {
|
||||
const label = this.trackResultObject(this.add.text(slotX, y + 9, `${equipmentSlotLabels[slot]} Lv${previousEquipmentLevel}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '11px',
|
||||
color: alive ? '#9fb0bf' : '#777777',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
label.setDepth(depth + 1);
|
||||
this.renderResultProgressGauge(slotX, y + 36, 76, 8, state.exp / equipmentExpToNext(state.level), color, depth + 1);
|
||||
const valueText = this.trackResultObject(this.add.text(slotX + 38, y + 22, `${previousEquipmentExp}/${previousNext}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '10px',
|
||||
color: alive ? '#f0e4c8' : '#7e858d',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
valueText.setOrigin(0.5, 0);
|
||||
valueText.setDepth(depth + 1);
|
||||
const fill = this.renderResultProgressGauge(slotX, y + 38, 76, 7, previousEquipmentExp / previousNext, color, depth + 1);
|
||||
const eventText = this.trackResultObject(this.add.text(slotX + 38, y + 31, '', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '12px',
|
||||
color: '#fff2b8',
|
||||
fontStyle: '700',
|
||||
stroke: '#05070a',
|
||||
strokeThickness: 3
|
||||
}));
|
||||
eventText.setOrigin(0.5);
|
||||
eventText.setDepth(depth + 4);
|
||||
eventText.setAlpha(0);
|
||||
if (gained > 0 || state.level > previousEquipmentLevel) {
|
||||
this.queueResultGaugeAnimation({
|
||||
fill,
|
||||
valueText,
|
||||
levelText: label,
|
||||
eventText,
|
||||
levelPrefix: `${equipmentSlotLabels[slot]} Lv`,
|
||||
previousLevel: previousEquipmentLevel,
|
||||
previousExp: previousEquipmentExp,
|
||||
previousNext,
|
||||
level: state.level,
|
||||
exp: state.exp,
|
||||
next,
|
||||
leveled: state.level > previousEquipmentLevel,
|
||||
flashArea: { x: slotX - 6, y: y + 5, width: 88, height: 44, depth: depth + 3 }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const equipment = this.trackResultObject(this.add.text(x + 14, y + 48, this.equipmentResultText(unit), {
|
||||
@@ -2324,10 +2555,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
track.setDepth(depth);
|
||||
track.setStrokeStyle(1, 0x40515e, 0.5);
|
||||
|
||||
const fillWidth = Math.max(2, Math.floor((width - 2) * Phaser.Math.Clamp(ratio, 0, 1)));
|
||||
const fill = this.trackResultObject(this.add.rectangle(x + 1, y + 1, fillWidth, Math.max(2, height - 2), color, 0.96));
|
||||
const fill = this.trackResultObject(this.add.rectangle(x + 1, y + 1, Math.max(2, width - 2), Math.max(2, height - 2), color, 0.96));
|
||||
fill.setOrigin(0);
|
||||
fill.setDepth(depth + 1);
|
||||
fill.setScale(Phaser.Math.Clamp(ratio, 0, 1), 1);
|
||||
return fill;
|
||||
}
|
||||
|
||||
private characterResultText(unit: UnitData) {
|
||||
@@ -2396,6 +2628,94 @@ export class BattleScene extends Phaser.Scene {
|
||||
return `공명 ${bond.level} 경험 +${gained}${levelText} 전투 +${bond.battleExp}`;
|
||||
}
|
||||
|
||||
private queueResultGaugeAnimation(animation: ResultGaugeAnimation) {
|
||||
this.resultGaugeAnimations.push(animation);
|
||||
}
|
||||
|
||||
private async playResultSettlementAnimation(token: number) {
|
||||
await this.delay(360);
|
||||
if (token !== this.resultAnimationToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.resultGaugeAnimations.length === 0) {
|
||||
this.resultSettlementText?.setText('성장 변화 없음');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let index = 0; index < this.resultGaugeAnimations.length; index += 1) {
|
||||
if (token !== this.resultAnimationToken) {
|
||||
return;
|
||||
}
|
||||
this.resultSettlementText?.setText(`성장 정산 ${index + 1} / ${this.resultGaugeAnimations.length}`);
|
||||
await this.animateResultGauge(this.resultGaugeAnimations[index], 280);
|
||||
await this.delay(44);
|
||||
}
|
||||
|
||||
if (token === this.resultAnimationToken) {
|
||||
this.resultSettlementText?.setText('성장 정산 완료');
|
||||
}
|
||||
}
|
||||
|
||||
private async animateResultGauge(animation: ResultGaugeAnimation, duration: number) {
|
||||
if (animation.leveled) {
|
||||
await this.animateGrowthGauge(
|
||||
animation.fill,
|
||||
animation.valueText,
|
||||
animation.previousExp / animation.previousNext,
|
||||
1,
|
||||
animation.previousExp,
|
||||
animation.previousNext,
|
||||
animation.previousNext,
|
||||
false,
|
||||
duration
|
||||
);
|
||||
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.flashResultGauge(animation);
|
||||
soundDirector.playLevelUp();
|
||||
await this.delay(180);
|
||||
await this.animateGrowthGauge(animation.fill, animation.valueText, 0, animation.exp / animation.next, 0, animation.exp, animation.next, false, duration);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.animateGrowthGauge(
|
||||
animation.fill,
|
||||
animation.valueText,
|
||||
animation.previousExp / animation.previousNext,
|
||||
animation.exp / animation.next,
|
||||
animation.previousExp,
|
||||
animation.exp,
|
||||
animation.next,
|
||||
false,
|
||||
duration
|
||||
);
|
||||
animation.levelText.setText(`${animation.levelPrefix}${animation.level}`);
|
||||
}
|
||||
|
||||
private flashResultGauge(animation: ResultGaugeAnimation) {
|
||||
const { x, y, width, height, depth } = animation.flashArea;
|
||||
const flash = this.trackResultObject(this.add.rectangle(x + width / 2, y + height / 2, width, height, 0xffdf7b, 0.16));
|
||||
flash.setDepth(depth);
|
||||
this.tweens.add({
|
||||
targets: flash,
|
||||
alpha: 0,
|
||||
scaleX: 1.04,
|
||||
scaleY: 1.16,
|
||||
duration: 520,
|
||||
ease: 'Sine.easeOut'
|
||||
});
|
||||
this.tweens.add({
|
||||
targets: [animation.levelText, animation.valueText],
|
||||
scale: 1.14,
|
||||
duration: 120,
|
||||
yoyo: true,
|
||||
ease: 'Sine.easeOut'
|
||||
});
|
||||
}
|
||||
|
||||
private addResultButton(label: string, x: number, y: number, width: number, action: () => void, depth: number) {
|
||||
const bg = this.trackResultObject(this.add.rectangle(x, y, width, 34, 0x1a2630, 0.96));
|
||||
bg.setDepth(depth);
|
||||
|
||||
Reference in New Issue
Block a user