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 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, 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
|
- Flow verification script from title to first battle
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ type CommandButton = {
|
|||||||
text: Phaser.GameObjects.Text;
|
text: Phaser.GameObjects.Text;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PendingMove = {
|
||||||
|
unit: UnitData;
|
||||||
|
fromX: number;
|
||||||
|
fromY: number;
|
||||||
|
toX: number;
|
||||||
|
toY: number;
|
||||||
|
};
|
||||||
|
|
||||||
const commandLabels: Record<BattleCommand, string> = {
|
const commandLabels: Record<BattleCommand, string> = {
|
||||||
attack: '공격',
|
attack: '공격',
|
||||||
strategy: '책략',
|
strategy: '책략',
|
||||||
@@ -64,8 +72,10 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private turnText?: Phaser.GameObjects.Text;
|
private turnText?: Phaser.GameObjects.Text;
|
||||||
private markers: Phaser.GameObjects.Rectangle[] = [];
|
private markers: Phaser.GameObjects.Rectangle[] = [];
|
||||||
private commandButtons: CommandButton[] = [];
|
private commandButtons: CommandButton[] = [];
|
||||||
|
private commandMenuObjects: Array<Phaser.GameObjects.Rectangle | Phaser.GameObjects.Text> = [];
|
||||||
private unitViews = new Map<string, UnitView>();
|
private unitViews = new Map<string, UnitView>();
|
||||||
private actedUnitIds = new Set<string>();
|
private actedUnitIds = new Set<string>();
|
||||||
|
private pendingMove?: PendingMove;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('BattleScene');
|
super('BattleScene');
|
||||||
@@ -75,6 +85,12 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const { width, height } = this.scale;
|
const { width, height } = this.scale;
|
||||||
this.layout = this.createLayout(width, height);
|
this.layout = this.createLayout(width, height);
|
||||||
soundDirector.playMusic('battle-prep');
|
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.add.rectangle(0, 0, width, height, 0x080b0d).setOrigin(0);
|
||||||
this.drawMap();
|
this.drawMap();
|
||||||
@@ -245,6 +261,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
this.phase = 'moving';
|
this.phase = 'moving';
|
||||||
this.selectedUnit = unit;
|
this.selectedUnit = unit;
|
||||||
|
this.pendingMove = undefined;
|
||||||
this.clearMarkers();
|
this.clearMarkers();
|
||||||
this.hideCommandMenu();
|
this.hideCommandMenu();
|
||||||
this.showMoveRange(unit);
|
this.showMoveRange(unit);
|
||||||
@@ -272,13 +289,17 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
marker.setInteractive({ useHandCursor: true });
|
marker.setInteractive({ useHandCursor: true });
|
||||||
marker.on('pointerover', () => marker.setFillStyle(palette.blue, 0.36));
|
marker.on('pointerover', () => marker.setFillStyle(palette.blue, 0.36));
|
||||||
marker.on('pointerout', () => marker.setFillStyle(palette.blue, 0.24));
|
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);
|
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)) {
|
if (!this.selectedUnit || !this.canMoveTo(this.selectedUnit, x, y)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -289,30 +310,20 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fromX = unit.x;
|
||||||
|
const fromY = unit.y;
|
||||||
unit.x = x;
|
unit.x = x;
|
||||||
unit.y = y;
|
unit.y = y;
|
||||||
|
this.pendingMove = { unit, fromX, fromY, toX: x, toY: y };
|
||||||
this.phase = 'command';
|
this.phase = 'command';
|
||||||
this.clearMarkers();
|
this.clearMarkers();
|
||||||
soundDirector.playSelect();
|
soundDirector.playSelect();
|
||||||
|
|
||||||
const targetX = this.tileCenterX(x);
|
const targetX = this.tileCenterX(x);
|
||||||
const targetY = this.tileCenterY(y);
|
const targetY = this.tileCenterY(y);
|
||||||
this.tweens.add({
|
this.moveUnitView(unit, x, y, view);
|
||||||
targets: view.sprite,
|
this.showCommandMenu(unit, pointer?.x ?? targetX, pointer?.y ?? targetY);
|
||||||
x: targetX,
|
this.setInfo(`${unit.name} 이동 완료\n위치: ${x + 1}, ${y + 1}\n공격, 책략, 도구, 대기 중 하나를 선택하세요.\n우클릭하면 이동을 취소합니다.`);
|
||||||
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이제 공격, 책략, 도구, 대기 중 하나를 선택하세요.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private canMoveTo(unit: UnitData, x: number, y: number) {
|
private canMoveTo(unit: UnitData, x: number, y: number) {
|
||||||
@@ -358,33 +369,61 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.markers = [];
|
this.markers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private showCommandMenu(unit: UnitData) {
|
private showCommandMenu(unit: UnitData, pointerX: number, pointerY: number) {
|
||||||
this.hideCommandMenu();
|
this.hideCommandMenu();
|
||||||
|
|
||||||
const { panelX, panelY, panelWidth } = this.layout;
|
|
||||||
const commands: BattleCommand[] = ['attack', 'strategy', 'item', 'wait'];
|
const commands: BattleCommand[] = ['attack', 'strategy', 'item', 'wait'];
|
||||||
const buttonWidth = panelWidth - 48;
|
const menuWidth = 136;
|
||||||
const buttonHeight = 44;
|
const titleHeight = 31;
|
||||||
const startY = panelY + 354;
|
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) => {
|
commands.forEach((command, index) => {
|
||||||
const centerX = panelX + 24 + buttonWidth / 2;
|
const buttonTop = top + padding + titleHeight + index * (buttonHeight + gap);
|
||||||
const centerY = startY + index * 56;
|
const centerX = left + menuWidth / 2;
|
||||||
const background = this.add.rectangle(centerX, centerY, buttonWidth, buttonHeight, 0x1a2630, 0.94);
|
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.setStrokeStyle(1, command === 'wait' ? palette.gold : palette.blue, 0.7);
|
||||||
background.setInteractive({ useHandCursor: true });
|
background.setInteractive({ useHandCursor: true });
|
||||||
background.setDepth(12);
|
background.setDepth(15);
|
||||||
|
|
||||||
const text = this.add.text(centerX, centerY, commandLabels[command], {
|
const text = this.add.text(centerX, centerY, commandLabels[command], {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '21px',
|
fontSize: '18px',
|
||||||
color: '#f2e3bf',
|
color: '#f2e3bf',
|
||||||
fontStyle: '700'
|
fontStyle: '700'
|
||||||
});
|
});
|
||||||
text.setOrigin(0.5);
|
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('pointerover', () => background.setFillStyle(0x283947, 0.98));
|
||||||
background.on('pointerout', () => background.setFillStyle(0x1a2630, 0.94));
|
background.on('pointerout', () => background.setFillStyle(0x1a2630, 0.94));
|
||||||
background.on('pointerdown', chooseCommand);
|
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() {
|
private hideCommandMenu() {
|
||||||
|
this.commandMenuObjects.forEach((object) => object.destroy());
|
||||||
|
this.commandMenuObjects = [];
|
||||||
this.commandButtons.forEach(({ background, text }) => {
|
this.commandButtons.forEach(({ background, text }) => {
|
||||||
background.destroy();
|
background.destroy();
|
||||||
text.destroy();
|
text.destroy();
|
||||||
@@ -411,6 +464,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.actedUnitIds.add(unit.id);
|
this.actedUnitIds.add(unit.id);
|
||||||
this.phase = 'idle';
|
this.phase = 'idle';
|
||||||
this.selectedUnit = undefined;
|
this.selectedUnit = undefined;
|
||||||
|
this.pendingMove = undefined;
|
||||||
this.hideCommandMenu();
|
this.hideCommandMenu();
|
||||||
this.applyActedStyle(unit);
|
this.applyActedStyle(unit);
|
||||||
soundDirector.playSelect();
|
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;
|
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) {
|
private applyActedStyle(unit: UnitData) {
|
||||||
const view = this.unitViews.get(unit.id);
|
const view = this.unitViews.get(unit.id);
|
||||||
if (!view) {
|
if (!view) {
|
||||||
|
|||||||
Reference in New Issue
Block a user