Add post-move battle commands
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user