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

@@ -16,13 +16,13 @@ Build a small complete tactical RPG loop:
- Cinematic title scene with an original Taoyuan Oath background, subtle motion, menu UI, and first-input audio
- Cinematic prologue pages with high-resolution story backgrounds, character portraits, non-black scene transitions, and scenario data
- File-based original orchestral BGM loops for title, prologue, oath, militia, and battle-prep scenes
- First battle scene with a high-resolution battlefield background, large 12x12 tactical grid, high-detail pixel-style unit sprites, movement range preview, and click-to-move unit movement
- First battle scene with a high-resolution battlefield background, large 12x12 tactical grid, high-detail pixel-style unit sprites, movement range preview, click-to-move unit movement, post-move command selection, and grayscale acted-unit feedback
- Flow verification script from title to first battle
## Next Steps
1. Add selectable commands: attack, wait, and end turn
2. Implement attack range, damage, defeat, and victory checks
1. Implement attack range, damage, defeat, and victory checks
2. Add strategy and item inventories with usable battle effects
3. Add a simple enemy AI turn
4. Add battle result scene and local save/load

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);
}