Add post-move battle commands

This commit is contained in:
2026-06-22 01:16:39 +09:00
parent 2bde2ef84e
commit 34c23def39
2 changed files with 135 additions and 7 deletions

View File

@@ -40,13 +40,32 @@ type UnitView = {
label: Phaser.GameObjects.Text;
};
type BattlePhase = 'idle' | 'moving' | 'command';
type BattleCommand = 'attack' | 'strategy' | 'item' | 'wait';
type CommandButton = {
command: BattleCommand;
background: Phaser.GameObjects.Rectangle;
text: Phaser.GameObjects.Text;
};
const commandLabels: Record<BattleCommand, string> = {
attack: '공격',
strategy: '책략',
item: '도구',
wait: '대기'
};
export class BattleScene extends Phaser.Scene {
private layout!: BattleLayout;
private phase: BattlePhase = 'idle';
private selectedUnit?: UnitData;
private infoText?: Phaser.GameObjects.Text;
private turnText?: Phaser.GameObjects.Text;
private markers: Phaser.GameObjects.Rectangle[] = [];
private commandButtons: CommandButton[] = [];
private unitViews = new Map<string, UnitView>();
private actedUnitIds = new Set<string>();
constructor() {
super('BattleScene');
@@ -61,7 +80,7 @@ export class BattleScene extends Phaser.Scene {
this.drawMap();
this.drawUnits();
this.drawSidePanel();
this.setInfo('아군 차례입니다.\n유비를 선택한 뒤 파란 칸을 눌러 이동하세요.');
this.setInfo('아군 차례입니다.\n행동할 장수를 선택한 뒤 파란 칸으로 이동하세요.');
}
private createLayout(width: number, height: number): BattleLayout {
@@ -197,7 +216,7 @@ export class BattleScene extends Phaser.Scene {
fontSize: '18px',
color: '#9aa3ad'
});
this.add.text(panelX + 24, panelY + panelHeight - 86, '다음 단계: 공격 / 대기 / AI', {
this.add.text(panelX + 24, panelY + panelHeight - 86, '이동 후 명령: 공격 / 책략 / 도구 / 대기', {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '18px',
color: '#9aa3ad'
@@ -205,13 +224,29 @@ export class BattleScene extends Phaser.Scene {
}
private selectUnit(unit: UnitData) {
if (this.phase === 'command') {
this.setInfo('먼저 현재 장수의 행동을 결정하세요.\n공격, 책략, 도구, 대기 중 하나를 선택해야 합니다.');
return;
}
if (unit.faction !== 'ally') {
this.setInfo(`${unit.name}: ${unit.className}\nHP ${unit.hp}/${unit.maxHp}`);
return;
}
if (this.actedUnitIds.has(unit.id)) {
this.selectedUnit = undefined;
this.clearMarkers();
this.hideCommandMenu();
this.phase = 'idle';
this.setInfo(`${unit.name}은 이미 행동을 마쳤습니다.\n흑백으로 표시된 장수는 이번 턴에 다시 움직일 수 없습니다.`);
return;
}
this.phase = 'moving';
this.selectedUnit = unit;
this.clearMarkers();
this.hideCommandMenu();
this.showMoveRange(unit);
this.setInfo(`${unit.name} (${unit.className})\nHP ${unit.hp}/${unit.maxHp}\n공격 ${unit.attack} / 이동 ${unit.move}\n이동할 파란 칸을 선택하세요.`);
}
@@ -256,7 +291,7 @@ export class BattleScene extends Phaser.Scene {
unit.x = x;
unit.y = y;
this.selectedUnit = undefined;
this.phase = 'command';
this.clearMarkers();
soundDirector.playSelect();
@@ -276,7 +311,8 @@ export class BattleScene extends Phaser.Scene {
duration: 260,
ease: 'Sine.easeInOut'
});
this.setInfo(`${unit.name} 이동 완료\n위치: ${x + 1}, ${y + 1}\n다음 단계에서 공격/대기 명령을 붙일 예정입니다.`);
this.showCommandMenu(unit);
this.setInfo(`${unit.name} 이동 완료\n위치: ${x + 1}, ${y + 1}\n이제 공격, 책략, 도구, 대기 중 하나를 선택하세요.`);
}
private canMoveTo(unit: UnitData, x: number, y: number) {
@@ -322,6 +358,98 @@ export class BattleScene extends Phaser.Scene {
this.markers = [];
}
private showCommandMenu(unit: UnitData) {
this.hideCommandMenu();
const { panelX, panelY, panelWidth } = this.layout;
const commands: BattleCommand[] = ['attack', 'strategy', 'item', 'wait'];
const buttonWidth = panelWidth - 48;
const buttonHeight = 44;
const startY = panelY + 354;
commands.forEach((command, index) => {
const centerX = panelX + 24 + buttonWidth / 2;
const centerY = startY + index * 56;
const background = this.add.rectangle(centerX, centerY, buttonWidth, buttonHeight, 0x1a2630, 0.94);
background.setStrokeStyle(1, command === 'wait' ? palette.gold : palette.blue, 0.7);
background.setInteractive({ useHandCursor: true });
background.setDepth(12);
const text = this.add.text(centerX, centerY, commandLabels[command], {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '21px',
color: '#f2e3bf',
fontStyle: '700'
});
text.setOrigin(0.5);
text.setDepth(13);
const chooseCommand = () => this.completeUnitAction(unit, command);
background.on('pointerover', () => background.setFillStyle(0x283947, 0.98));
background.on('pointerout', () => background.setFillStyle(0x1a2630, 0.94));
background.on('pointerdown', chooseCommand);
text.setInteractive({ useHandCursor: true });
text.on('pointerdown', chooseCommand);
this.commandButtons.push({ command, background, text });
});
}
private hideCommandMenu() {
this.commandButtons.forEach(({ background, text }) => {
background.destroy();
text.destroy();
});
this.commandButtons = [];
}
private completeUnitAction(unit: UnitData, command: BattleCommand) {
if (this.phase !== 'command' || this.selectedUnit?.id !== unit.id || this.actedUnitIds.has(unit.id)) {
return;
}
this.actedUnitIds.add(unit.id);
this.phase = 'idle';
this.selectedUnit = undefined;
this.hideCommandMenu();
this.applyActedStyle(unit);
soundDirector.playSelect();
const actionMessage = this.commandResultMessage(unit, command);
this.setInfo(`${actionMessage}\n\n${this.remainingAllyCount()}명의 아군이 아직 행동할 수 있습니다.`);
}
private commandResultMessage(unit: UnitData, command: BattleCommand) {
if (command === 'attack') {
return `${unit.name} 공격 명령 완료\n다음 구현에서 대상 선택과 피해 처리를 연결할 예정입니다.`;
}
if (command === 'strategy') {
return `${unit.name} 책략 명령 완료\n책략 목록과 MP는 다음 전투 확장 때 붙이겠습니다.`;
}
if (command === 'item') {
return `${unit.name} 도구 명령 완료\n도구 가방은 이후 회복 아이템과 함께 열겠습니다.`;
}
return `${unit.name} 대기 명령 완료`;
}
private remainingAllyCount() {
return firstBattleUnits.filter((unit) => unit.faction === 'ally' && !this.actedUnitIds.has(unit.id)).length;
}
private applyActedStyle(unit: UnitData) {
const view = this.unitViews.get(unit.id);
if (!view) {
return;
}
view.sprite.postFX.addColorMatrix().grayscale(1);
view.sprite.setAlpha(0.46);
view.sprite.setTint(0xb7b7b7);
view.label.setAlpha(0.55);
view.label.setColor('#b9b9b9');
view.label.setBackgroundColor('rgba(20,20,20,0.66)');
}
private setInfo(text: string) {
this.infoText?.setText(text);
}