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 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
|
- 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
|
- 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
|
- Flow verification script from title to first battle
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
1. Add selectable commands: attack, wait, and end turn
|
1. Implement attack range, damage, defeat, and victory checks
|
||||||
2. 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
|
3. Add a simple enemy AI turn
|
||||||
4. Add battle result scene and local save/load
|
4. Add battle result scene and local save/load
|
||||||
|
|
||||||
|
|||||||
@@ -40,13 +40,32 @@ type UnitView = {
|
|||||||
label: Phaser.GameObjects.Text;
|
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 {
|
export class BattleScene extends Phaser.Scene {
|
||||||
private layout!: BattleLayout;
|
private layout!: BattleLayout;
|
||||||
|
private phase: BattlePhase = 'idle';
|
||||||
private selectedUnit?: UnitData;
|
private selectedUnit?: UnitData;
|
||||||
private infoText?: Phaser.GameObjects.Text;
|
private infoText?: Phaser.GameObjects.Text;
|
||||||
private turnText?: Phaser.GameObjects.Text;
|
private turnText?: Phaser.GameObjects.Text;
|
||||||
private markers: Phaser.GameObjects.Rectangle[] = [];
|
private markers: Phaser.GameObjects.Rectangle[] = [];
|
||||||
|
private commandButtons: CommandButton[] = [];
|
||||||
private unitViews = new Map<string, UnitView>();
|
private unitViews = new Map<string, UnitView>();
|
||||||
|
private actedUnitIds = new Set<string>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('BattleScene');
|
super('BattleScene');
|
||||||
@@ -61,7 +80,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.drawMap();
|
this.drawMap();
|
||||||
this.drawUnits();
|
this.drawUnits();
|
||||||
this.drawSidePanel();
|
this.drawSidePanel();
|
||||||
this.setInfo('아군 차례입니다.\n유비를 선택한 뒤 파란 칸을 눌러 이동하세요.');
|
this.setInfo('아군 차례입니다.\n행동할 장수를 선택한 뒤 파란 칸으로 이동하세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
private createLayout(width: number, height: number): BattleLayout {
|
private createLayout(width: number, height: number): BattleLayout {
|
||||||
@@ -197,7 +216,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
color: '#9aa3ad'
|
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',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
color: '#9aa3ad'
|
color: '#9aa3ad'
|
||||||
@@ -205,13 +224,29 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private selectUnit(unit: UnitData) {
|
private selectUnit(unit: UnitData) {
|
||||||
|
if (this.phase === 'command') {
|
||||||
|
this.setInfo('먼저 현재 장수의 행동을 결정하세요.\n공격, 책략, 도구, 대기 중 하나를 선택해야 합니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (unit.faction !== 'ally') {
|
if (unit.faction !== 'ally') {
|
||||||
this.setInfo(`${unit.name}: ${unit.className}\nHP ${unit.hp}/${unit.maxHp}`);
|
this.setInfo(`${unit.name}: ${unit.className}\nHP ${unit.hp}/${unit.maxHp}`);
|
||||||
return;
|
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.selectedUnit = unit;
|
||||||
this.clearMarkers();
|
this.clearMarkers();
|
||||||
|
this.hideCommandMenu();
|
||||||
this.showMoveRange(unit);
|
this.showMoveRange(unit);
|
||||||
this.setInfo(`${unit.name} (${unit.className})\nHP ${unit.hp}/${unit.maxHp}\n공격 ${unit.attack} / 이동 ${unit.move}\n이동할 파란 칸을 선택하세요.`);
|
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.x = x;
|
||||||
unit.y = y;
|
unit.y = y;
|
||||||
this.selectedUnit = undefined;
|
this.phase = 'command';
|
||||||
this.clearMarkers();
|
this.clearMarkers();
|
||||||
soundDirector.playSelect();
|
soundDirector.playSelect();
|
||||||
|
|
||||||
@@ -276,7 +311,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
duration: 260,
|
duration: 260,
|
||||||
ease: 'Sine.easeInOut'
|
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) {
|
private canMoveTo(unit: UnitData, x: number, y: number) {
|
||||||
@@ -322,6 +358,98 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.markers = [];
|
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) {
|
private setInfo(text: string) {
|
||||||
this.infoText?.setText(text);
|
this.infoText?.setText(text);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user