Add animated combat growth rewards
This commit is contained in:
@@ -90,6 +90,21 @@ class SoundDirector {
|
|||||||
window.setTimeout(() => this.playTone(450, 0.08, 0.028, 'sine'), 70);
|
window.setTimeout(() => this.playTone(450, 0.08, 0.028, 'sine'), 70);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playGrowthTick() {
|
||||||
|
if (this.playEffect('exp-gain', { volume: 0.18, rate: 1.12, stopAfterMs: 360 })) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playTone(620, 0.05, 0.018, 'triangle');
|
||||||
|
}
|
||||||
|
|
||||||
|
playLevelUp() {
|
||||||
|
this.playEffect('exp-gain', { volume: 0.42, rate: 1.08, stopAfterMs: 820 });
|
||||||
|
this.playTone(520, 0.08, 0.035, 'triangle');
|
||||||
|
window.setTimeout(() => this.playTone(720, 0.09, 0.04, 'triangle'), 82);
|
||||||
|
window.setTimeout(() => this.playTone(980, 0.12, 0.034, 'sine'), 168);
|
||||||
|
}
|
||||||
|
|
||||||
playEffect(key: string, options: PlayEffectOptions = {}) {
|
playEffect(key: string, options: PlayEffectOptions = {}) {
|
||||||
if (!this.started || this.muted) {
|
if (!this.started || this.muted) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -150,6 +150,38 @@ type CharacterGrowthResult = {
|
|||||||
leveled: boolean;
|
leveled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BondGrowthResult = {
|
||||||
|
label: string;
|
||||||
|
amount: number;
|
||||||
|
previousLevel: number;
|
||||||
|
previousExp: number;
|
||||||
|
level: number;
|
||||||
|
exp: number;
|
||||||
|
next: number;
|
||||||
|
leveled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GrowthGaugeEntry = {
|
||||||
|
label: string;
|
||||||
|
owner: string;
|
||||||
|
amountLabel: string;
|
||||||
|
previousLevel: number;
|
||||||
|
previousExp: number;
|
||||||
|
previousNext: number;
|
||||||
|
level: number;
|
||||||
|
exp: number;
|
||||||
|
next: number;
|
||||||
|
leveled: boolean;
|
||||||
|
color: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GrowthGaugeView = {
|
||||||
|
fill: Phaser.GameObjects.Rectangle;
|
||||||
|
valueText: Phaser.GameObjects.Text;
|
||||||
|
levelText: Phaser.GameObjects.Text;
|
||||||
|
eventText: Phaser.GameObjects.Text;
|
||||||
|
};
|
||||||
|
|
||||||
type CombatPreview = {
|
type CombatPreview = {
|
||||||
action: DamageCommand;
|
action: DamageCommand;
|
||||||
usable?: BattleUsable;
|
usable?: BattleUsable;
|
||||||
@@ -172,6 +204,7 @@ type CombatResult = CombatPreview & {
|
|||||||
attackerGrowth: EquipmentGrowthResult;
|
attackerGrowth: EquipmentGrowthResult;
|
||||||
defenderGrowth: EquipmentGrowthResult;
|
defenderGrowth: EquipmentGrowthResult;
|
||||||
bondExp: number;
|
bondExp: number;
|
||||||
|
bondGrowth: BondGrowthResult[];
|
||||||
counter?: CombatResult;
|
counter?: CombatResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2036,7 +2069,8 @@ 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', hit ? 6 : 3);
|
const armorGrowth = this.awardEquipmentExp(defender, 'armor', hit ? 6 : 3);
|
||||||
const characterGrowth = this.awardCharacterExp(attacker, this.characterExpGain(preview, damage, hit, critical));
|
const characterGrowth = this.awardCharacterExp(attacker, this.characterExpGain(preview, damage, hit, critical));
|
||||||
const bondExp = attacker.faction === 'ally' && !isCounter ? this.recordBondAttackIntent(attacker, defender) : 0;
|
const bondGrowth = attacker.faction === 'ally' && !isCounter ? this.recordBondAttackIntent(attacker, defender) : [];
|
||||||
|
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);
|
||||||
this.faceUnitToward(attacker, defender);
|
this.faceUnitToward(attacker, defender);
|
||||||
@@ -2064,7 +2098,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
characterGrowth,
|
characterGrowth,
|
||||||
attackerGrowth,
|
attackerGrowth,
|
||||||
defenderGrowth: armorGrowth,
|
defenderGrowth: armorGrowth,
|
||||||
bondExp
|
bondExp,
|
||||||
|
bondGrowth
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2793,8 +2828,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private async playCombatCutIn(result: CombatResult) {
|
private async playCombatCutIn(result: CombatResult) {
|
||||||
this.hideCombatCutIn();
|
this.hideCombatCutIn();
|
||||||
|
|
||||||
const panelWidth = 900;
|
const panelWidth = 940;
|
||||||
const panelHeight = 430;
|
const panelHeight = 560;
|
||||||
const left = Math.floor((this.scale.width - panelWidth) / 2);
|
const left = Math.floor((this.scale.width - panelWidth) / 2);
|
||||||
const top = Math.floor((this.scale.height - panelHeight) / 2);
|
const top = Math.floor((this.scale.height - panelHeight) / 2);
|
||||||
const depth = 70;
|
const depth = 70;
|
||||||
@@ -2889,6 +2924,9 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
result.characterGrowth.leveled,
|
result.characterGrowth.leveled,
|
||||||
680
|
680
|
||||||
);
|
);
|
||||||
|
if (result.characterGrowth.leveled) {
|
||||||
|
attackerStatus.nameText.setText(`${result.attacker.name} Lv ${result.characterGrowth.level}`);
|
||||||
|
}
|
||||||
|
|
||||||
const resultLabel = result.hit ? `${result.critical ? '치명타! ' : ''}${result.damage} 피해` : '회피!';
|
const resultLabel = result.hit ? `${result.critical ? '치명타! ' : ''}${result.damage} 피해` : '회피!';
|
||||||
const resultText = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 252, resultLabel, {
|
const resultText = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 252, resultLabel, {
|
||||||
@@ -2902,18 +2940,17 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
resultText.setOrigin(0.5);
|
resultText.setOrigin(0.5);
|
||||||
resultText.setDepth(depth + 16);
|
resultText.setDepth(depth + 16);
|
||||||
|
|
||||||
const growthText = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 330, this.combatGrowthLines(result).join('\n'), {
|
await this.playGrowthRewardPanel({
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
x: left + 86,
|
||||||
fontSize: '14px',
|
y: top + 410,
|
||||||
color: '#d4dce6',
|
width: panelWidth - 172,
|
||||||
fontStyle: '700',
|
title: '성장 결과',
|
||||||
align: 'center',
|
entries: this.combatGrowthEntries(result),
|
||||||
lineSpacing: 3
|
celebrant: this.growthCelebrant(result),
|
||||||
}));
|
depth: depth + 18
|
||||||
growthText.setOrigin(0.5, 0);
|
});
|
||||||
growthText.setDepth(depth + 16);
|
|
||||||
|
|
||||||
await this.delay(760);
|
await this.delay(360);
|
||||||
this.hideCombatCutIn();
|
this.hideCombatCutIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2921,7 +2958,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.hideCombatCutIn();
|
this.hideCombatCutIn();
|
||||||
|
|
||||||
const panelWidth = 760;
|
const panelWidth = 760;
|
||||||
const panelHeight = 330;
|
const panelHeight = 500;
|
||||||
const left = Math.floor((this.scale.width - panelWidth) / 2);
|
const left = Math.floor((this.scale.width - panelWidth) / 2);
|
||||||
const top = Math.floor((this.scale.height - panelHeight) / 2);
|
const top = Math.floor((this.scale.height - panelHeight) / 2);
|
||||||
const depth = 70;
|
const depth = 70;
|
||||||
@@ -2989,20 +3026,17 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
resultText.setOrigin(0.5);
|
resultText.setOrigin(0.5);
|
||||||
resultText.setDepth(depth + 5);
|
resultText.setDepth(depth + 5);
|
||||||
|
|
||||||
const growth = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 292, [
|
await this.playGrowthRewardPanel({
|
||||||
`장수 경험치 +${result.characterGrowth.amount} (${result.characterGrowth.exp}/${result.characterGrowth.next})`,
|
x: left + 64,
|
||||||
this.formatEquipmentGrowth(result.equipmentGrowth)
|
y: top + 322,
|
||||||
].join(' '), {
|
width: panelWidth - 128,
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
title: '성장 결과',
|
||||||
fontSize: '14px',
|
entries: this.supportGrowthEntries(result),
|
||||||
color: '#d4dce6',
|
celebrant: result.user,
|
||||||
fontStyle: '700',
|
depth: depth + 8
|
||||||
align: 'center'
|
});
|
||||||
}));
|
|
||||||
growth.setOrigin(0.5);
|
|
||||||
growth.setDepth(depth + 5);
|
|
||||||
|
|
||||||
await this.delay(760);
|
await this.delay(360);
|
||||||
this.hideCombatCutIn();
|
this.hideCombatCutIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3023,13 +3057,13 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
unitIcon.setDisplaySize(64, 64);
|
unitIcon.setDisplaySize(64, 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = this.trackCombatObject(this.add.text(x + 78, y + 14, `${unit.name} Lv ${unit.level}`, {
|
const nameText = this.trackCombatObject(this.add.text(x + 78, y + 14, `${unit.name} Lv ${growth?.previousLevel ?? unit.level}`, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '17px',
|
fontSize: '17px',
|
||||||
color: '#f2e3bf',
|
color: '#f2e3bf',
|
||||||
fontStyle: '700'
|
fontStyle: '700'
|
||||||
}));
|
}));
|
||||||
name.setDepth(85);
|
nameText.setDepth(85);
|
||||||
|
|
||||||
const hpLabel = this.trackCombatObject(this.add.text(x + 78, y + 45, '병력', {
|
const hpLabel = this.trackCombatObject(this.add.text(x + 78, y + 45, '병력', {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
@@ -3077,7 +3111,283 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
expText.setOrigin(1, 0);
|
expText.setOrigin(1, 0);
|
||||||
expText.setDepth(85);
|
expText.setDepth(85);
|
||||||
|
|
||||||
return { hpFill, hpText, expFill, expText };
|
return { hpFill, hpText, expFill, expText, nameText };
|
||||||
|
}
|
||||||
|
|
||||||
|
private combatGrowthEntries(result: CombatResult): GrowthGaugeEntry[] {
|
||||||
|
const entries: GrowthGaugeEntry[] = [];
|
||||||
|
|
||||||
|
if (result.attacker.faction === 'ally') {
|
||||||
|
entries.push(this.characterGrowthEntry(result.attacker, result.characterGrowth));
|
||||||
|
entries.push(this.equipmentGrowthEntry(result.attacker, result.attackerGrowth));
|
||||||
|
}
|
||||||
|
if (result.defender.faction === 'ally') {
|
||||||
|
entries.push(this.equipmentGrowthEntry(result.defender, result.defenderGrowth));
|
||||||
|
}
|
||||||
|
result.bondGrowth.forEach((growth) => entries.push(this.bondGrowthEntry(growth)));
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private supportGrowthEntries(result: SupportResult): GrowthGaugeEntry[] {
|
||||||
|
return [this.characterGrowthEntry(result.user, result.characterGrowth), this.equipmentGrowthEntry(result.user, result.equipmentGrowth)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private characterGrowthEntry(unit: UnitData, growth: CharacterGrowthResult): GrowthGaugeEntry {
|
||||||
|
return {
|
||||||
|
label: '장수 경험치',
|
||||||
|
owner: unit.name,
|
||||||
|
amountLabel: `+${growth.amount}`,
|
||||||
|
previousLevel: growth.previousLevel,
|
||||||
|
previousExp: growth.previousExp,
|
||||||
|
previousNext: 100,
|
||||||
|
level: growth.level,
|
||||||
|
exp: growth.exp,
|
||||||
|
next: growth.next,
|
||||||
|
leveled: growth.leveled,
|
||||||
|
color: 0xd8b15f
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private equipmentGrowthEntry(unit: UnitData, growth: EquipmentGrowthResult): GrowthGaugeEntry {
|
||||||
|
return {
|
||||||
|
label: `${equipmentSlotLabels[growth.slot]} ${growth.itemName}`,
|
||||||
|
owner: unit.name,
|
||||||
|
amountLabel: `+${growth.amount}`,
|
||||||
|
previousLevel: growth.previousLevel,
|
||||||
|
previousExp: growth.previousExp,
|
||||||
|
previousNext: equipmentExpToNext(growth.previousLevel),
|
||||||
|
level: growth.level,
|
||||||
|
exp: growth.exp,
|
||||||
|
next: growth.next,
|
||||||
|
leveled: growth.leveled,
|
||||||
|
color: growth.slot === 'weapon' ? 0xffc45f : growth.slot === 'armor' ? 0x77b7ff : 0xb7df7a
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private bondGrowthEntry(growth: BondGrowthResult): GrowthGaugeEntry {
|
||||||
|
return {
|
||||||
|
label: '공명 경험치',
|
||||||
|
owner: growth.label,
|
||||||
|
amountLabel: `+${growth.amount}`,
|
||||||
|
previousLevel: growth.previousLevel,
|
||||||
|
previousExp: growth.previousExp,
|
||||||
|
previousNext: 100,
|
||||||
|
level: growth.level,
|
||||||
|
exp: growth.exp,
|
||||||
|
next: growth.next,
|
||||||
|
leveled: growth.leveled,
|
||||||
|
color: 0x83d6ff
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private growthCelebrant(result: CombatResult) {
|
||||||
|
if (result.attacker.faction === 'ally') {
|
||||||
|
return result.attacker;
|
||||||
|
}
|
||||||
|
if (result.defender.faction === 'ally') {
|
||||||
|
return result.defender;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async playGrowthRewardPanel(config: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
title: string;
|
||||||
|
entries: GrowthGaugeEntry[];
|
||||||
|
celebrant?: UnitData;
|
||||||
|
depth: number;
|
||||||
|
}) {
|
||||||
|
const entries = config.entries.filter((entry) => entry.amountLabel !== '+0' || entry.leveled);
|
||||||
|
if (entries.length === 0) {
|
||||||
|
await this.delay(280);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowHeight = 25;
|
||||||
|
const height = 48 + entries.length * rowHeight;
|
||||||
|
const panel = this.trackCombatObject(this.add.rectangle(config.x, config.y, config.width, height, 0x0b121a, 0.96));
|
||||||
|
panel.setOrigin(0);
|
||||||
|
panel.setDepth(config.depth);
|
||||||
|
panel.setStrokeStyle(2, palette.gold, 0.82);
|
||||||
|
|
||||||
|
const title = this.trackCombatObject(this.add.text(config.x + 16, config.y + 12, config.title, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '16px',
|
||||||
|
color: '#f4dfad',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
title.setDepth(config.depth + 1);
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < entries.length; index += 1) {
|
||||||
|
await this.animateGrowthEntry(views[index], entries[index], 540 + index * 70);
|
||||||
|
if (entries[index].leveled) {
|
||||||
|
this.flashGrowthLevelUp(views[index], config.x, config.y, config.width, height, config.depth + 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (celebration) {
|
||||||
|
await this.playGrowthCelebration(celebration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderGrowthGaugeRow(entry: GrowthGaugeEntry, x: number, y: number, width: number, depth: number): GrowthGaugeView {
|
||||||
|
const label = this.trackCombatObject(this.add.text(x, y - 3, `${entry.owner} ${entry.label}`, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#d4dce6',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
label.setDepth(depth);
|
||||||
|
|
||||||
|
const amount = this.trackCombatObject(this.add.text(x + 214, y - 3, entry.amountLabel, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#ffdf7b',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
amount.setDepth(depth);
|
||||||
|
|
||||||
|
const barX = x + 270;
|
||||||
|
const barWidth = Math.max(130, width - 440);
|
||||||
|
const track = this.trackCombatObject(this.add.rectangle(barX, y + 8, barWidth, 9, 0x05090e, 0.92));
|
||||||
|
track.setOrigin(0, 0.5);
|
||||||
|
track.setDepth(depth);
|
||||||
|
const fill = this.trackCombatObject(this.add.rectangle(barX, y + 8, barWidth, 7, entry.color, 0.98));
|
||||||
|
fill.setOrigin(0, 0.5);
|
||||||
|
fill.setDepth(depth + 1);
|
||||||
|
fill.setScale(entry.previousExp / entry.previousNext, 1);
|
||||||
|
|
||||||
|
const valueText = this.trackCombatObject(this.add.text(barX + barWidth + 10, y - 3, `${entry.previousExp}/${entry.previousNext}`, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#f0e4c8',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
valueText.setDepth(depth);
|
||||||
|
|
||||||
|
const levelText = this.trackCombatObject(this.add.text(x + width - 6, y - 3, `Lv ${entry.previousLevel}`, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#f4dfad',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
levelText.setOrigin(1, 0);
|
||||||
|
levelText.setDepth(depth);
|
||||||
|
|
||||||
|
const eventText = this.trackCombatObject(this.add.text(barX + Math.floor(barWidth / 2), y - 12, '', {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '15px',
|
||||||
|
color: '#fff2b8',
|
||||||
|
fontStyle: '700',
|
||||||
|
stroke: '#05070a',
|
||||||
|
strokeThickness: 3
|
||||||
|
}));
|
||||||
|
eventText.setOrigin(0.5);
|
||||||
|
eventText.setDepth(depth + 3);
|
||||||
|
|
||||||
|
return { fill, valueText, levelText, eventText };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async animateGrowthEntry(view: GrowthGaugeView, entry: GrowthGaugeEntry, duration: number) {
|
||||||
|
if (entry.leveled) {
|
||||||
|
await this.animateGrowthGauge(view.fill, view.valueText, entry.previousExp / entry.previousNext, 1, entry.previousExp, entry.previousNext, entry.previousNext, false, duration);
|
||||||
|
view.levelText.setText(`Lv ${entry.level}`);
|
||||||
|
view.fill.setScale(0, 1);
|
||||||
|
view.valueText.setText(`0/${entry.next}`);
|
||||||
|
this.showGrowthEventText(view.eventText, `Lv ${entry.level} 달성!`);
|
||||||
|
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)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.animateGrowthGauge(view.fill, view.valueText, entry.previousExp / entry.previousNext, entry.exp / entry.next, entry.previousExp, entry.exp, entry.next, false, duration);
|
||||||
|
view.levelText.setText(`Lv ${entry.level}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private showGrowthEventText(text: Phaser.GameObjects.Text, label: string) {
|
||||||
|
text.setText(label);
|
||||||
|
text.setAlpha(1);
|
||||||
|
text.setScale(0.9);
|
||||||
|
this.tweens.add({
|
||||||
|
targets: text,
|
||||||
|
y: text.y - 12,
|
||||||
|
scale: 1.16,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 780,
|
||||||
|
ease: 'Sine.easeOut'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private flashGrowthLevelUp(view: GrowthGaugeView, x: number, y: number, width: number, height: number, depth: number) {
|
||||||
|
const flash = this.trackCombatObject(this.add.rectangle(x + width / 2, y + height / 2, width - 10, height - 10, 0xffdf7b, 0.14));
|
||||||
|
flash.setDepth(depth);
|
||||||
|
this.tweens.add({
|
||||||
|
targets: flash,
|
||||||
|
alpha: 0,
|
||||||
|
scaleX: 1.03,
|
||||||
|
scaleY: 1.12,
|
||||||
|
duration: 520,
|
||||||
|
ease: 'Sine.easeOut',
|
||||||
|
onComplete: () => flash.destroy()
|
||||||
|
});
|
||||||
|
this.tweens.add({
|
||||||
|
targets: [view.levelText, view.valueText],
|
||||||
|
scale: 1.16,
|
||||||
|
duration: 130,
|
||||||
|
yoyo: true,
|
||||||
|
ease: 'Sine.easeOut'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createGrowthCelebrationSprite(unit: UnitData, x: number, y: number, depth: number) {
|
||||||
|
const sprite = this.trackCombatObject(
|
||||||
|
this.add.sprite(x, y, this.unitActionTexture(unit), this.unitActionFrameIndex('south', this.celebrationPose(unit)))
|
||||||
|
);
|
||||||
|
sprite.setDisplaySize(82, 82);
|
||||||
|
sprite.setDepth(depth);
|
||||||
|
return sprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
private celebrationPose(unit: UnitData): UnitActionPose {
|
||||||
|
if (unit.id === 'liu-bei') {
|
||||||
|
return 'strategy';
|
||||||
|
}
|
||||||
|
if (unit.id === 'zhang-fei') {
|
||||||
|
return 'attack';
|
||||||
|
}
|
||||||
|
return 'item';
|
||||||
|
}
|
||||||
|
|
||||||
|
private playGrowthCelebration(sprite: Phaser.GameObjects.Sprite) {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: sprite,
|
||||||
|
y: sprite.y - 18,
|
||||||
|
scale: 1.16,
|
||||||
|
angle: 3,
|
||||||
|
duration: 160,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: 2,
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
onComplete: () => {
|
||||||
|
sprite.setAngle(0);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private combatPortraitKey(unit: UnitData) {
|
private combatPortraitKey(unit: UnitData) {
|
||||||
@@ -3244,8 +3554,56 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
) {
|
) {
|
||||||
target.setScale(Phaser.Math.Clamp(fromRatio, 0, 1), 1);
|
target.setScale(Phaser.Math.Clamp(fromRatio, 0, 1), 1);
|
||||||
text.setText(`${fromValue} / ${next}`);
|
text.setText(`${fromValue} / ${next}`);
|
||||||
if (toValue !== fromValue || leveled) {
|
|
||||||
soundDirector.playEffect('exp-gain', { volume: leveled ? 0.44 : 0.3, stopAfterMs: duration + 220 });
|
if (leveled) {
|
||||||
|
soundDirector.playGrowthTick();
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
const firstTracker = { ratio: Phaser.Math.Clamp(fromRatio, 0, 1), value: fromValue };
|
||||||
|
this.tweens.add({
|
||||||
|
targets: firstTracker,
|
||||||
|
ratio: 1,
|
||||||
|
value: next,
|
||||||
|
duration: Math.floor(duration * 0.62),
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
onUpdate: () => {
|
||||||
|
target.setScale(firstTracker.ratio, 1);
|
||||||
|
text.setText(`${Math.round(firstTracker.value)} / ${next}`);
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
soundDirector.playLevelUp();
|
||||||
|
target.setScale(0, 1);
|
||||||
|
text.setText(`0 / ${next}`);
|
||||||
|
const secondTracker = { ratio: 0, value: 0 };
|
||||||
|
this.tweens.add({
|
||||||
|
targets: secondTracker,
|
||||||
|
ratio: Phaser.Math.Clamp(toRatio, 0, 1),
|
||||||
|
value: toValue,
|
||||||
|
duration: Math.floor(duration * 0.46),
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
onUpdate: () => {
|
||||||
|
target.setScale(secondTracker.ratio, 1);
|
||||||
|
text.setText(`${Math.round(secondTracker.value)} / ${next}`);
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
target.setScale(Phaser.Math.Clamp(toRatio, 0, 1), 1);
|
||||||
|
text.setText(`${toValue} / ${next}`);
|
||||||
|
this.tweens.add({
|
||||||
|
targets: text,
|
||||||
|
scale: 1.18,
|
||||||
|
duration: 120,
|
||||||
|
yoyo: true,
|
||||||
|
ease: 'Sine.easeOut'
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toValue !== fromValue) {
|
||||||
|
soundDirector.playGrowthTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
@@ -3464,26 +3822,40 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const partners = this.attackIntents
|
const partners = this.attackIntents
|
||||||
.filter((intent) => intent.targetId === target.id && intent.attackerId !== unit.id)
|
.filter((intent) => intent.targetId === target.id && intent.attackerId !== unit.id)
|
||||||
.map((intent) => intent.attackerId);
|
.map((intent) => intent.attackerId);
|
||||||
let gained = 0;
|
const growthResults: BondGrowthResult[] = [];
|
||||||
|
|
||||||
partners.forEach((partnerId) => {
|
partners.forEach((partnerId) => {
|
||||||
const bond = this.findBond(unit.id, partnerId);
|
const bond = this.findBond(unit.id, partnerId);
|
||||||
if (!bond) {
|
if (!bond) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const previousLevel = bond.level;
|
||||||
|
const previousExp = bond.exp;
|
||||||
|
let leveled = false;
|
||||||
bond.battleExp += 4;
|
bond.battleExp += 4;
|
||||||
bond.exp += 4;
|
bond.exp += 4;
|
||||||
while (bond.exp >= 100) {
|
while (bond.exp >= 100) {
|
||||||
bond.exp -= 100;
|
bond.exp -= 100;
|
||||||
bond.level = Math.min(100, bond.level + 1);
|
bond.level = Math.min(100, bond.level + 1);
|
||||||
|
leveled = true;
|
||||||
}
|
}
|
||||||
gained += 4;
|
const [first, second] = bond.unitIds.map((unitId) => this.unitName(unitId));
|
||||||
|
growthResults.push({
|
||||||
|
label: `${first}·${second}`,
|
||||||
|
amount: 4,
|
||||||
|
previousLevel,
|
||||||
|
previousExp,
|
||||||
|
level: bond.level,
|
||||||
|
exp: bond.exp,
|
||||||
|
next: 100,
|
||||||
|
leveled
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.attackIntents = this.attackIntents.filter((intent) => intent.attackerId !== unit.id);
|
this.attackIntents = this.attackIntents.filter((intent) => intent.attackerId !== unit.id);
|
||||||
this.attackIntents.push({ attackerId: unit.id, targetId: target.id });
|
this.attackIntents.push({ attackerId: unit.id, targetId: target.id });
|
||||||
|
|
||||||
return gained;
|
return growthResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
private recordAttackIntent(unit: UnitData) {
|
private recordAttackIntent(unit: UnitData) {
|
||||||
|
|||||||
Reference in New Issue
Block a user