Add popup commands and move cancel
This commit is contained in:
@@ -16,7 +16,7 @@ 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, click-to-move unit movement, post-move command selection, and grayscale acted-unit feedback
|
||||
- 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, cursor-adjacent post-move command popup, right-click move cancel, and grayscale acted-unit feedback
|
||||
- Flow verification script from title to first battle
|
||||
|
||||
## Next Steps
|
||||
|
||||
@@ -49,6 +49,14 @@ type CommandButton = {
|
||||
text: Phaser.GameObjects.Text;
|
||||
};
|
||||
|
||||
type PendingMove = {
|
||||
unit: UnitData;
|
||||
fromX: number;
|
||||
fromY: number;
|
||||
toX: number;
|
||||
toY: number;
|
||||
};
|
||||
|
||||
const commandLabels: Record<BattleCommand, string> = {
|
||||
attack: '공격',
|
||||
strategy: '책략',
|
||||
@@ -64,8 +72,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
private turnText?: Phaser.GameObjects.Text;
|
||||
private markers: Phaser.GameObjects.Rectangle[] = [];
|
||||
private commandButtons: CommandButton[] = [];
|
||||
private commandMenuObjects: Array<Phaser.GameObjects.Rectangle | Phaser.GameObjects.Text> = [];
|
||||
private unitViews = new Map<string, UnitView>();
|
||||
private actedUnitIds = new Set<string>();
|
||||
private pendingMove?: PendingMove;
|
||||
|
||||
constructor() {
|
||||
super('BattleScene');
|
||||
@@ -75,6 +85,12 @@ export class BattleScene extends Phaser.Scene {
|
||||
const { width, height } = this.scale;
|
||||
this.layout = this.createLayout(width, height);
|
||||
soundDirector.playMusic('battle-prep');
|
||||
this.input.mouse?.disableContextMenu();
|
||||
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||
if (pointer.rightButtonDown()) {
|
||||
this.cancelPendingMove();
|
||||
}
|
||||
});
|
||||
|
||||
this.add.rectangle(0, 0, width, height, 0x080b0d).setOrigin(0);
|
||||
this.drawMap();
|
||||
@@ -245,6 +261,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
this.phase = 'moving';
|
||||
this.selectedUnit = unit;
|
||||
this.pendingMove = undefined;
|
||||
this.clearMarkers();
|
||||
this.hideCommandMenu();
|
||||
this.showMoveRange(unit);
|
||||
@@ -272,13 +289,17 @@ export class BattleScene extends Phaser.Scene {
|
||||
marker.setInteractive({ useHandCursor: true });
|
||||
marker.on('pointerover', () => marker.setFillStyle(palette.blue, 0.36));
|
||||
marker.on('pointerout', () => marker.setFillStyle(palette.blue, 0.24));
|
||||
marker.on('pointerdown', () => this.moveSelectedUnit(x, y));
|
||||
marker.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||
if (pointer.leftButtonDown()) {
|
||||
this.moveSelectedUnit(x, y, pointer);
|
||||
}
|
||||
});
|
||||
this.markers.push(marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private moveSelectedUnit(x: number, y: number) {
|
||||
private moveSelectedUnit(x: number, y: number, pointer?: Phaser.Input.Pointer) {
|
||||
if (!this.selectedUnit || !this.canMoveTo(this.selectedUnit, x, y)) {
|
||||
return;
|
||||
}
|
||||
@@ -289,30 +310,20 @@ export class BattleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
const fromX = unit.x;
|
||||
const fromY = unit.y;
|
||||
unit.x = x;
|
||||
unit.y = y;
|
||||
this.pendingMove = { unit, fromX, fromY, toX: x, toY: y };
|
||||
this.phase = 'command';
|
||||
this.clearMarkers();
|
||||
soundDirector.playSelect();
|
||||
|
||||
const targetX = this.tileCenterX(x);
|
||||
const targetY = this.tileCenterY(y);
|
||||
this.tweens.add({
|
||||
targets: view.sprite,
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
duration: 260,
|
||||
ease: 'Sine.easeInOut'
|
||||
});
|
||||
this.tweens.add({
|
||||
targets: view.label,
|
||||
x: targetX,
|
||||
y: targetY + this.layout.tileSize * 0.36,
|
||||
duration: 260,
|
||||
ease: 'Sine.easeInOut'
|
||||
});
|
||||
this.showCommandMenu(unit);
|
||||
this.setInfo(`${unit.name} 이동 완료\n위치: ${x + 1}, ${y + 1}\n이제 공격, 책략, 도구, 대기 중 하나를 선택하세요.`);
|
||||
this.moveUnitView(unit, x, y, view);
|
||||
this.showCommandMenu(unit, pointer?.x ?? targetX, pointer?.y ?? targetY);
|
||||
this.setInfo(`${unit.name} 이동 완료\n위치: ${x + 1}, ${y + 1}\n공격, 책략, 도구, 대기 중 하나를 선택하세요.\n우클릭하면 이동을 취소합니다.`);
|
||||
}
|
||||
|
||||
private canMoveTo(unit: UnitData, x: number, y: number) {
|
||||
@@ -358,33 +369,61 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.markers = [];
|
||||
}
|
||||
|
||||
private showCommandMenu(unit: UnitData) {
|
||||
private showCommandMenu(unit: UnitData, pointerX: number, pointerY: number) {
|
||||
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;
|
||||
const menuWidth = 136;
|
||||
const titleHeight = 31;
|
||||
const buttonHeight = 34;
|
||||
const gap = 5;
|
||||
const padding = 8;
|
||||
const menuHeight = padding * 2 + titleHeight + commands.length * buttonHeight + (commands.length - 1) * gap;
|
||||
const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight);
|
||||
|
||||
const panel = this.add.rectangle(left, top, menuWidth, menuHeight, 0x101821, 0.97);
|
||||
panel.setOrigin(0);
|
||||
panel.setStrokeStyle(2, palette.gold, 0.76);
|
||||
panel.setDepth(14);
|
||||
this.commandMenuObjects.push(panel);
|
||||
|
||||
const title = this.add.text(left + menuWidth / 2, top + padding + 2, '명령', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '16px',
|
||||
color: '#d8b15f',
|
||||
fontStyle: '700'
|
||||
});
|
||||
title.setOrigin(0.5, 0);
|
||||
title.setDepth(15);
|
||||
this.commandMenuObjects.push(title);
|
||||
|
||||
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);
|
||||
const buttonTop = top + padding + titleHeight + index * (buttonHeight + gap);
|
||||
const centerX = left + menuWidth / 2;
|
||||
const centerY = buttonTop + buttonHeight / 2;
|
||||
const background = this.add.rectangle(centerX, centerY, menuWidth - padding * 2, buttonHeight, 0x1a2630, 0.94);
|
||||
background.setStrokeStyle(1, command === 'wait' ? palette.gold : palette.blue, 0.7);
|
||||
background.setInteractive({ useHandCursor: true });
|
||||
background.setDepth(12);
|
||||
background.setDepth(15);
|
||||
|
||||
const text = this.add.text(centerX, centerY, commandLabels[command], {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '21px',
|
||||
fontSize: '18px',
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
});
|
||||
text.setOrigin(0.5);
|
||||
text.setDepth(13);
|
||||
text.setDepth(16);
|
||||
|
||||
const chooseCommand = () => this.completeUnitAction(unit, command);
|
||||
const chooseCommand = (choicePointer: Phaser.Input.Pointer) => {
|
||||
if (choicePointer.rightButtonDown()) {
|
||||
this.cancelPendingMove();
|
||||
return;
|
||||
}
|
||||
if (choicePointer.leftButtonDown()) {
|
||||
this.completeUnitAction(unit, command);
|
||||
}
|
||||
};
|
||||
background.on('pointerover', () => background.setFillStyle(0x283947, 0.98));
|
||||
background.on('pointerout', () => background.setFillStyle(0x1a2630, 0.94));
|
||||
background.on('pointerdown', chooseCommand);
|
||||
@@ -395,7 +434,21 @@ export class BattleScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
private commandMenuPosition(pointerX: number, pointerY: number, width: number, height: number) {
|
||||
const margin = 14;
|
||||
const offset = 18;
|
||||
const preferredLeft = pointerX + offset + width > this.scale.width - margin ? pointerX - offset - width : pointerX + offset;
|
||||
const preferredTop = pointerY + height > this.scale.height - margin ? this.scale.height - margin - height : pointerY;
|
||||
|
||||
return {
|
||||
left: Phaser.Math.Clamp(preferredLeft, margin, this.scale.width - margin - width),
|
||||
top: Phaser.Math.Clamp(preferredTop, margin, this.scale.height - margin - height)
|
||||
};
|
||||
}
|
||||
|
||||
private hideCommandMenu() {
|
||||
this.commandMenuObjects.forEach((object) => object.destroy());
|
||||
this.commandMenuObjects = [];
|
||||
this.commandButtons.forEach(({ background, text }) => {
|
||||
background.destroy();
|
||||
text.destroy();
|
||||
@@ -411,6 +464,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.actedUnitIds.add(unit.id);
|
||||
this.phase = 'idle';
|
||||
this.selectedUnit = undefined;
|
||||
this.pendingMove = undefined;
|
||||
this.hideCommandMenu();
|
||||
this.applyActedStyle(unit);
|
||||
soundDirector.playSelect();
|
||||
@@ -436,6 +490,57 @@ export class BattleScene extends Phaser.Scene {
|
||||
return firstBattleUnits.filter((unit) => unit.faction === 'ally' && !this.actedUnitIds.has(unit.id)).length;
|
||||
}
|
||||
|
||||
private cancelPendingMove() {
|
||||
if (this.phase !== 'command' || !this.pendingMove) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { unit, fromX, fromY, toX, toY } = this.pendingMove;
|
||||
const view = this.unitViews.get(unit.id);
|
||||
unit.x = fromX;
|
||||
unit.y = fromY;
|
||||
this.phase = 'moving';
|
||||
this.selectedUnit = unit;
|
||||
this.pendingMove = undefined;
|
||||
this.hideCommandMenu();
|
||||
soundDirector.playSelect();
|
||||
|
||||
this.moveUnitView(unit, fromX, fromY, view, 180);
|
||||
this.clearMarkers();
|
||||
this.showMoveRange(unit);
|
||||
this.setInfo(`${unit.name} 이동 취소\n${toX + 1}, ${toY + 1}에서 원래 위치 ${fromX + 1}, ${fromY + 1}로 되돌렸습니다.\n다시 이동할 파란 칸을 선택하세요.`);
|
||||
}
|
||||
|
||||
private moveUnitView(
|
||||
unit: UnitData,
|
||||
x: number,
|
||||
y: number,
|
||||
view = this.unitViews.get(unit.id),
|
||||
duration = 260
|
||||
) {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetX = this.tileCenterX(x);
|
||||
const targetY = this.tileCenterY(y);
|
||||
this.tweens.killTweensOf([view.sprite, view.label]);
|
||||
this.tweens.add({
|
||||
targets: view.sprite,
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
duration,
|
||||
ease: 'Sine.easeInOut'
|
||||
});
|
||||
this.tweens.add({
|
||||
targets: view.label,
|
||||
x: targetX,
|
||||
y: targetY + this.layout.tileSize * 0.36,
|
||||
duration,
|
||||
ease: 'Sine.easeInOut'
|
||||
});
|
||||
}
|
||||
|
||||
private applyActedStyle(unit: UnitData) {
|
||||
const view = this.unitViews.get(unit.id);
|
||||
if (!view) {
|
||||
|
||||
Reference in New Issue
Block a user