Add first battle outcome screen
This commit is contained in:
@@ -59,13 +59,14 @@ type UnitView = {
|
||||
|
||||
type UnitDirection = 'south' | 'east' | 'north' | 'west';
|
||||
|
||||
type BattlePhase = 'idle' | 'moving' | 'command' | 'targeting' | 'animating';
|
||||
type BattlePhase = 'idle' | 'moving' | 'command' | 'targeting' | 'animating' | 'resolved';
|
||||
type BattleCommand = 'attack' | 'strategy' | 'item' | 'wait';
|
||||
type DamageCommand = Exclude<BattleCommand, 'wait'>;
|
||||
type RosterTab = 'ally' | 'enemy';
|
||||
type ActiveFaction = 'ally' | 'enemy';
|
||||
type MapMenuAction = 'endTurn' | 'save' | 'load' | 'roster' | 'bond' | 'situation' | 'bgm' | 'close';
|
||||
type EnemyAiBehavior = 'aggressive' | 'guard' | 'hold';
|
||||
type BattleOutcome = 'victory' | 'defeat';
|
||||
|
||||
type CommandButton = {
|
||||
command: BattleCommand;
|
||||
@@ -151,6 +152,8 @@ type BattleSaveState = {
|
||||
const maxEquipmentLevel = 9;
|
||||
const maxCharacterLevel = 99;
|
||||
const battleSaveStorageKey = 'heros-web:first-battle-state';
|
||||
const initialFirstBattleUnits = firstBattleUnits.map(cloneUnitData);
|
||||
const initialFirstBattleBonds = firstBattleBonds.map(cloneBattleBond);
|
||||
|
||||
const attackRangeByClass: Partial<Record<UnitClassKey, number>> = {
|
||||
archer: 2
|
||||
@@ -216,6 +219,21 @@ const statLabels: Array<{ key: keyof UnitStats; label: string }> = [
|
||||
{ key: 'luck', label: '운' }
|
||||
];
|
||||
|
||||
function cloneUnitData(unit: UnitData): UnitData {
|
||||
return {
|
||||
...unit,
|
||||
stats: { ...unit.stats },
|
||||
equipment: JSON.parse(JSON.stringify(unit.equipment)) as UnitData['equipment']
|
||||
};
|
||||
}
|
||||
|
||||
function cloneBattleBond(bond: BattleBond): BattleBond {
|
||||
return {
|
||||
...bond,
|
||||
unitIds: [...bond.unitIds] as [string, string]
|
||||
};
|
||||
}
|
||||
|
||||
export class BattleScene extends Phaser.Scene {
|
||||
private layout!: BattleLayout;
|
||||
private phase: BattlePhase = 'idle';
|
||||
@@ -234,6 +252,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
private alertDismissEvent?: Phaser.Time.TimerEvent;
|
||||
private turnPromptObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private combatCutInObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private resultObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private unitViews = new Map<string, UnitView>();
|
||||
private actedUnitIds = new Set<string>();
|
||||
private bondStates = new Map<string, BondState>();
|
||||
@@ -241,6 +260,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
private battleLog: string[] = [];
|
||||
private enemyHomeTiles = new Map<string, { x: number; y: number }>();
|
||||
private pendingMove?: PendingMove;
|
||||
private battleOutcome?: BattleOutcome;
|
||||
private debugOverlay?: Phaser.GameObjects.Text;
|
||||
|
||||
constructor() {
|
||||
@@ -248,11 +268,19 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
create() {
|
||||
this.resetBattleData();
|
||||
const { width, height } = this.scale;
|
||||
this.layout = this.createLayout(width, height);
|
||||
this.bondStates = this.createBondStates();
|
||||
this.enemyHomeTiles = this.createEnemyHomeTiles();
|
||||
this.battleLog = [];
|
||||
this.actedUnitIds.clear();
|
||||
this.attackIntents = [];
|
||||
this.selectedUnit = undefined;
|
||||
this.pendingMove = undefined;
|
||||
this.targetingAction = undefined;
|
||||
this.battleOutcome = undefined;
|
||||
this.phase = 'idle';
|
||||
soundDirector.playMusic('battle-prep');
|
||||
this.input.mouse?.disableContextMenu();
|
||||
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||
@@ -275,6 +303,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.renderRosterPanel('ally', '행동할 장수를 선택하세요.');
|
||||
}
|
||||
|
||||
private resetBattleData() {
|
||||
firstBattleUnits.splice(0, firstBattleUnits.length, ...initialFirstBattleUnits.map(cloneUnitData));
|
||||
firstBattleBonds.splice(0, firstBattleBonds.length, ...initialFirstBattleBonds.map(cloneBattleBond));
|
||||
}
|
||||
|
||||
private createLayout(width: number, height: number): BattleLayout {
|
||||
const margin = 24;
|
||||
const gap = 20;
|
||||
@@ -415,6 +448,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.hideMapMenu();
|
||||
this.hideTurnEndPrompt();
|
||||
|
||||
if (this.battleOutcome) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.phase === 'animating') {
|
||||
return;
|
||||
}
|
||||
@@ -855,13 +892,17 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.hideTurnEndPrompt();
|
||||
this.applyActedStyle(unit);
|
||||
soundDirector.playSelect();
|
||||
this.pushBattleLog(message);
|
||||
|
||||
if (this.resolveBattleOutcomeIfNeeded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remaining = this.remainingAllyCount();
|
||||
const turnHint =
|
||||
remaining > 0
|
||||
? `${remaining}명의 아군이 아직 행동할 수 있습니다.`
|
||||
: '모든 아군이 행동했습니다. 턴 종료 여부를 선택하세요.';
|
||||
this.pushBattleLog(message);
|
||||
this.renderRosterPanel('ally', `${message}\n${turnHint}`);
|
||||
|
||||
if (remaining === 0) {
|
||||
@@ -869,6 +910,289 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
}
|
||||
|
||||
private resolveBattleOutcomeIfNeeded() {
|
||||
if (this.battleOutcome) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const liuBei = firstBattleUnits.find((unit) => unit.id === 'liu-bei');
|
||||
if (!liuBei || liuBei.hp <= 0) {
|
||||
this.completeBattle('defeat');
|
||||
return true;
|
||||
}
|
||||
|
||||
const enemiesAlive = firstBattleUnits.some((unit) => unit.faction === 'enemy' && unit.hp > 0);
|
||||
if (!enemiesAlive) {
|
||||
this.completeBattle('victory');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private completeBattle(outcome: BattleOutcome) {
|
||||
if (this.battleOutcome) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.battleOutcome = outcome;
|
||||
this.phase = 'resolved';
|
||||
this.selectedUnit = undefined;
|
||||
this.pendingMove = undefined;
|
||||
this.targetingAction = undefined;
|
||||
this.clearMarkers();
|
||||
this.hideCommandMenu();
|
||||
this.hideMapMenu();
|
||||
this.hideTurnEndPrompt();
|
||||
this.hideBattleAlert();
|
||||
|
||||
const message =
|
||||
outcome === 'victory'
|
||||
? '승리! 황건적을 모두 물리쳤습니다.'
|
||||
: '패배... 유비가 패주했습니다.';
|
||||
this.pushBattleLog(message);
|
||||
this.renderSituationPanel(message);
|
||||
soundDirector.playEffect(outcome === 'victory' ? 'exp-gain' : 'combat-impact', {
|
||||
volume: outcome === 'victory' ? 0.38 : 0.28,
|
||||
stopAfterMs: 1200
|
||||
});
|
||||
this.showBattleResult(outcome);
|
||||
}
|
||||
|
||||
private showBattleResult(outcome: BattleOutcome) {
|
||||
this.hideBattleResult();
|
||||
|
||||
const depth = 120;
|
||||
const panelWidth = 920;
|
||||
const panelHeight = 590;
|
||||
const left = Math.floor((this.scale.width - panelWidth) / 2);
|
||||
const top = Math.floor((this.scale.height - panelHeight) / 2);
|
||||
const allies = firstBattleUnits.filter((unit) => unit.faction === 'ally');
|
||||
const defeatedEnemies = firstBattleUnits.filter((unit) => unit.faction === 'enemy' && unit.hp <= 0).length;
|
||||
const totalEnemies = firstBattleUnits.filter((unit) => unit.faction === 'enemy').length;
|
||||
const aliveAllies = allies.filter((unit) => unit.hp > 0).length;
|
||||
|
||||
const shade = this.trackResultObject(this.add.rectangle(0, 0, this.scale.width, this.scale.height, 0x020406, 0.68));
|
||||
shade.setOrigin(0);
|
||||
shade.setDepth(depth);
|
||||
shade.setInteractive();
|
||||
|
||||
const panel = this.trackResultObject(this.add.rectangle(left, top, panelWidth, panelHeight, 0x101821, 0.98));
|
||||
panel.setOrigin(0);
|
||||
panel.setDepth(depth + 1);
|
||||
panel.setStrokeStyle(3, outcome === 'victory' ? palette.gold : 0xb86b55, 0.94);
|
||||
|
||||
const title = this.trackResultObject(this.add.text(left + panelWidth / 2, top + 26, outcome === 'victory' ? '전투 승리' : '전투 패배', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '34px',
|
||||
color: outcome === 'victory' ? '#f4dfad' : '#ffb6a6',
|
||||
fontStyle: '700',
|
||||
stroke: '#05070a',
|
||||
strokeThickness: 5
|
||||
}));
|
||||
title.setOrigin(0.5, 0);
|
||||
title.setDepth(depth + 2);
|
||||
|
||||
const subtitleText =
|
||||
outcome === 'victory'
|
||||
? '탁현 의용군은 첫 싸움에서 황건적을 몰아내고 마을을 지켜냈습니다.'
|
||||
: '유비가 전장을 이탈했습니다. 진형을 가다듬고 다시 도전해야 합니다.';
|
||||
const subtitle = this.trackResultObject(this.add.text(left + panelWidth / 2, top + 72, subtitleText, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '17px',
|
||||
color: '#d4dce6',
|
||||
align: 'center'
|
||||
}));
|
||||
subtitle.setOrigin(0.5, 0);
|
||||
subtitle.setDepth(depth + 2);
|
||||
|
||||
const metricTop = top + 112;
|
||||
const metricWidth = 196;
|
||||
const metricGap = 18;
|
||||
this.renderResultMetric('소요 턴', `${this.turnNumber}턴`, left + 44, metricTop, metricWidth, depth + 2);
|
||||
this.renderResultMetric('생존 아군', `${aliveAllies} / ${allies.length}`, left + 44 + (metricWidth + metricGap), metricTop, metricWidth, depth + 2);
|
||||
this.renderResultMetric('격파 적군', `${defeatedEnemies} / ${totalEnemies}`, left + 44 + (metricWidth + metricGap) * 2, metricTop, metricWidth, depth + 2);
|
||||
this.renderResultMetric('전투 보상', outcome === 'victory' ? '획득' : '없음', left + 44 + (metricWidth + metricGap) * 3, metricTop, metricWidth, depth + 2);
|
||||
|
||||
const unitTitle = this.trackResultObject(this.add.text(left + 44, top + 178, '장수 성장', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '21px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
unitTitle.setDepth(depth + 2);
|
||||
|
||||
allies.forEach((unit, index) => {
|
||||
this.renderResultUnitRow(unit, left + 44, top + 214 + index * 66, panelWidth - 88, depth + 2);
|
||||
});
|
||||
|
||||
const bondTitleTop = top + 424;
|
||||
const bondTitle = this.trackResultObject(this.add.text(left + 44, bondTitleTop, '공명 변화', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '21px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
bondTitle.setDepth(depth + 2);
|
||||
|
||||
Array.from(this.bondStates.values()).forEach((bond, index) => {
|
||||
this.renderResultBondRow(bond, left + 44 + index * 278, bondTitleTop + 38, 258, depth + 2);
|
||||
});
|
||||
|
||||
const rewardLines =
|
||||
outcome === 'victory'
|
||||
? ['군자금 300', '마을 민심 +1', '의용군 명망 +1']
|
||||
: ['보상 없음', '유비를 보호하며 다시 진군하세요.'];
|
||||
const reward = this.trackResultObject(this.add.text(left + panelWidth - 44, top + 520, rewardLines.join(' '), {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '15px',
|
||||
color: outcome === 'victory' ? '#d8b15f' : '#aeb7c2',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
reward.setOrigin(1, 0);
|
||||
reward.setDepth(depth + 2);
|
||||
|
||||
this.addResultButton('다시 하기', left + panelWidth - 286, top + 552, 124, () => {
|
||||
soundDirector.playSelect();
|
||||
this.scene.restart();
|
||||
}, depth + 3);
|
||||
this.addResultButton('타이틀로', left + panelWidth - 150, top + 552, 124, () => {
|
||||
soundDirector.playSelect();
|
||||
this.scene.start('TitleScene');
|
||||
}, depth + 3);
|
||||
}
|
||||
|
||||
private hideBattleResult() {
|
||||
this.resultObjects.forEach((object) => object.destroy());
|
||||
this.resultObjects = [];
|
||||
}
|
||||
|
||||
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);
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, 0x53606c, 0.6);
|
||||
|
||||
const labelText = this.trackResultObject(this.add.text(x + 12, y + 8, label, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '13px',
|
||||
color: '#9fb0bf'
|
||||
}));
|
||||
labelText.setDepth(depth + 1);
|
||||
|
||||
const valueText = this.trackResultObject(this.add.text(x + width - 12, y + 22, value, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '18px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
valueText.setOrigin(1, 0);
|
||||
valueText.setDepth(depth + 1);
|
||||
}
|
||||
|
||||
private renderResultUnitRow(unit: UnitData, x: number, y: number, width: number, depth: number) {
|
||||
const alive = unit.hp > 0;
|
||||
const bg = this.trackResultObject(this.add.rectangle(x, y, width, 56, 0x101820, alive ? 0.94 : 0.66));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, alive ? palette.blue : 0x646464, alive ? 0.62 : 0.5);
|
||||
|
||||
const name = this.trackResultObject(this.add.text(x + 14, y + 8, `${unit.name} Lv ${unit.level}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '17px',
|
||||
color: alive ? '#e9f0f8' : '#9a9a9a',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
name.setDepth(depth + 1);
|
||||
|
||||
const classText = this.trackResultObject(this.add.text(x + 150, y + 10, getUnitClass(unit.classKey).name, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '14px',
|
||||
color: alive ? '#9fb0bf' : '#777777'
|
||||
}));
|
||||
classText.setDepth(depth + 1);
|
||||
|
||||
const hpText = this.trackResultObject(this.add.text(x + 270, y + 9, `HP ${unit.hp}/${unit.maxHp}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '14px',
|
||||
color: alive ? '#f0e4c8' : '#8c8c8c',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
hpText.setDepth(depth + 1);
|
||||
|
||||
const expText = this.trackResultObject(this.add.text(x + 392, y + 9, `경험 ${unit.exp}/100`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '14px',
|
||||
color: '#d8b15f',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
expText.setDepth(depth + 1);
|
||||
|
||||
const equipmentText = equipmentSlots
|
||||
.map((slot) => {
|
||||
const state = unit.equipment[slot];
|
||||
const item = getItem(state.itemId);
|
||||
return `${equipmentSlotLabels[slot]} ${item.name} Lv${state.level} ${state.exp}/${equipmentExpToNext(state.level)}`;
|
||||
})
|
||||
.join(' ');
|
||||
const equipment = this.trackResultObject(this.add.text(x + 14, y + 32, equipmentText, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '12px',
|
||||
color: alive ? '#c8d2dd' : '#7e858d',
|
||||
wordWrap: { width: width - 28, useAdvancedWrap: true }
|
||||
}));
|
||||
equipment.setDepth(depth + 1);
|
||||
}
|
||||
|
||||
private renderResultBondRow(bond: BondState, x: number, y: number, width: number, depth: number) {
|
||||
const bg = this.trackResultObject(this.add.rectangle(x, y, width, 58, 0x101820, 0.88));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, bond.battleExp > 0 ? palette.gold : 0x53606c, bond.battleExp > 0 ? 0.72 : 0.5);
|
||||
|
||||
const [first, second] = bond.unitIds.map((unitId) => this.unitName(unitId));
|
||||
const title = this.trackResultObject(this.add.text(x + 12, y + 8, `${first} · ${second}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '14px',
|
||||
color: '#e8dfca',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
title.setDepth(depth + 1);
|
||||
|
||||
const value = this.trackResultObject(this.add.text(x + 12, y + 31, `공명 ${bond.level} 경험 ${bond.exp}/100 전투 +${bond.battleExp}`, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '13px',
|
||||
color: bond.battleExp > 0 ? '#d8b15f' : '#9fb0bf'
|
||||
}));
|
||||
value.setDepth(depth + 1);
|
||||
}
|
||||
|
||||
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);
|
||||
bg.setStrokeStyle(1, label === '다시 하기' ? palette.blue : palette.gold, 0.78);
|
||||
bg.setInteractive({ useHandCursor: true });
|
||||
bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98));
|
||||
bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.96));
|
||||
bg.on('pointerdown', action);
|
||||
|
||||
const text = this.trackResultObject(this.add.text(x, y, label, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '16px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
text.setOrigin(0.5);
|
||||
text.setDepth(depth + 1);
|
||||
text.setInteractive({ useHandCursor: true });
|
||||
text.on('pointerdown', action);
|
||||
}
|
||||
|
||||
private trackResultObject<T extends Phaser.GameObjects.GameObject>(object: T) {
|
||||
this.resultObjects.push(object);
|
||||
return object;
|
||||
}
|
||||
|
||||
private attackableTargets(attacker: UnitData) {
|
||||
return this.damageableTargets(attacker, 'attack');
|
||||
}
|
||||
@@ -1045,6 +1369,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private handleRightClick(pointer: Phaser.Input.Pointer) {
|
||||
if (this.battleOutcome) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.phase === 'animating') {
|
||||
return;
|
||||
}
|
||||
@@ -1066,6 +1394,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private handleLeftClick(pointer: Phaser.Input.Pointer) {
|
||||
if (this.battleOutcome) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.phase !== 'idle' || !this.isPointerOnEmptyMapTile(pointer)) {
|
||||
return;
|
||||
}
|
||||
@@ -1283,9 +1615,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.hideCommandMenu();
|
||||
this.hideTurnEndPrompt();
|
||||
this.hideCombatCutIn();
|
||||
this.hideBattleResult();
|
||||
this.selectedUnit = undefined;
|
||||
this.pendingMove = undefined;
|
||||
this.targetingAction = undefined;
|
||||
this.battleOutcome = undefined;
|
||||
this.phase = 'idle';
|
||||
|
||||
this.turnNumber = Math.max(1, state.turnNumber || 1);
|
||||
@@ -1356,6 +1690,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.renderSituationPanel('이미 적군 행동 중입니다.');
|
||||
return;
|
||||
}
|
||||
if (this.battleOutcome || this.resolveBattleOutcomeIfNeeded()) {
|
||||
return;
|
||||
}
|
||||
if (this.phase === 'command' || this.phase === 'targeting' || this.phase === 'animating') {
|
||||
this.setInfo('현재 장수의 행동을 먼저 결정하세요.');
|
||||
return;
|
||||
@@ -1372,30 +1709,39 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.updateTurnText();
|
||||
this.renderSituationPanel('아군 턴을 종료했습니다.\n적군이 행동을 시작합니다.');
|
||||
void this.runEnemyTurn();
|
||||
return;
|
||||
|
||||
this.activeFaction = 'enemy';
|
||||
this.updateTurnText();
|
||||
const pressureMessage = this.resolveEnemyPressure();
|
||||
this.renderSituationPanel(`아군 턴을 종료했습니다.\n${pressureMessage}`);
|
||||
|
||||
this.time.delayedCall(700, () => this.beginNextAllyTurn());
|
||||
}
|
||||
|
||||
private async runEnemyTurn() {
|
||||
if (this.battleOutcome) {
|
||||
return;
|
||||
}
|
||||
|
||||
const enemies = firstBattleUnits.filter((unit) => unit.faction === 'enemy' && unit.hp > 0);
|
||||
const messages: string[] = [];
|
||||
|
||||
for (const enemy of enemies) {
|
||||
if (this.battleOutcome) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await this.executeEnemyAction(enemy);
|
||||
if (message) {
|
||||
messages.push(message);
|
||||
this.pushBattleLog(message);
|
||||
this.renderSituationPanel(messages.slice(-3).join('\n'));
|
||||
}
|
||||
|
||||
if (this.resolveBattleOutcomeIfNeeded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.delay(260);
|
||||
}
|
||||
|
||||
if (this.resolveBattleOutcomeIfNeeded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.time.delayedCall(520, () => this.beginNextAllyTurn());
|
||||
}
|
||||
|
||||
@@ -1871,6 +2217,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private beginNextAllyTurn() {
|
||||
if (this.battleOutcome || this.resolveBattleOutcomeIfNeeded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.turnNumber += 1;
|
||||
this.activeFaction = 'ally';
|
||||
this.actedUnitIds.clear();
|
||||
@@ -2901,6 +3251,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
turnNumber: this.turnNumber,
|
||||
activeFaction: this.activeFaction,
|
||||
phase: this.phase,
|
||||
battleOutcome: this.battleOutcome ?? null,
|
||||
resultVisible: this.resultObjects.length > 0,
|
||||
selectedUnitId: this.selectedUnit?.id ?? null,
|
||||
pendingMove: this.pendingMove
|
||||
? {
|
||||
@@ -2939,6 +3291,29 @@ export class BattleScene extends Phaser.Scene {
|
||||
};
|
||||
}
|
||||
|
||||
debugForceBattleOutcome(outcome: BattleOutcome = 'victory') {
|
||||
if (!import.meta.env.DEV || this.battleOutcome) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (outcome === 'victory') {
|
||||
firstBattleUnits
|
||||
.filter((unit) => unit.faction === 'enemy')
|
||||
.forEach((unit) => {
|
||||
unit.hp = 0;
|
||||
this.applyDefeatedStyle(unit);
|
||||
});
|
||||
} else {
|
||||
const liuBei = firstBattleUnits.find((unit) => unit.id === 'liu-bei');
|
||||
if (liuBei) {
|
||||
liuBei.hp = 0;
|
||||
this.applyDefeatedStyle(liuBei);
|
||||
}
|
||||
}
|
||||
|
||||
this.completeBattle(outcome);
|
||||
}
|
||||
|
||||
toggleDebugOverlay() {
|
||||
if (this.debugOverlay) {
|
||||
this.debugOverlay.destroy();
|
||||
|
||||
Reference in New Issue
Block a user