Working on end-game menu

This commit is contained in:
Dalibor Čarapić 2025-02-06 18:20:00 +01:00
parent f6879046fa
commit 2e1c73c9dc
11 changed files with 316 additions and 297 deletions

View File

@ -41,7 +41,6 @@ export class Engine {
export default class GameMaster { export default class GameMaster {
public currentScene: Scene; public currentScene: Scene;
private GameObjects: GameObject[] = [];
constructor() { constructor() {
Engine.GameMaster = this; Engine.GameMaster = this;
@ -61,9 +60,6 @@ export default class GameMaster {
if (this.currentScene) { if (this.currentScene) {
this.currentScene.destroy(); this.currentScene.destroy();
} }
this.GameObjects.forEach((element) => {
element.destroy();
});
this.currentScene = newScene; this.currentScene = newScene;
this.currentScene.init(); this.currentScene.init();
} }

View File

@ -28,9 +28,3 @@ export enum StatsEvents {
export enum GemEvents { export enum GemEvents {
TowerPanelSelectGem = 'towerTabSelectGem', TowerPanelSelectGem = 'towerTabSelectGem',
} }
export enum EndMissionDialogEvents {
NextMission = 'nextMission',
RetryMission = 'retryMission',
MainMenu = 'mainMenu',
}

View File

@ -3,7 +3,9 @@ import { Engine } from './Bastion';
export default class GameUIConstants { export default class GameUIConstants {
public static SidebarRect: PIXI.Rectangle; public static SidebarRect: PIXI.Rectangle;
public static ChangeRoundButtonRect: PIXI.Rectangle; public static ChangeRoundButtonRect: PIXI.Rectangle;
public static EndGameDialogRect: PIXI.Rectangle; public static StandardDialogWidth: number;
public static StandardDialogHeight: number;
public static MaximumPlayerNameLength = 20;
public static init() { public static init() {
GameUIConstants.SidebarRect = new PIXI.Rectangle( GameUIConstants.SidebarRect = new PIXI.Rectangle(
@ -13,13 +15,7 @@ export default class GameUIConstants {
Engine.app.canvas.height Engine.app.canvas.height
); );
GameUIConstants.ChangeRoundButtonRect = new PIXI.Rectangle(50, Engine.app.canvas.height - 100, 310, 100); GameUIConstants.ChangeRoundButtonRect = new PIXI.Rectangle(50, Engine.app.canvas.height - 100, 310, 100);
const endGameDialogWidth = 600; GameUIConstants.StandardDialogWidth = 600;
const endGameDialogHeight = 800; GameUIConstants.StandardDialogHeight = 800;
GameUIConstants.EndGameDialogRect = new PIXI.Rectangle(
(Engine.app.canvas.width - endGameDialogWidth) / 2,
(Engine.app.canvas.height - endGameDialogHeight) / 2,
endGameDialogWidth,
endGameDialogHeight
);
} }
} }

View File

@ -2,45 +2,42 @@
* Handles keyboard events. * Handles keyboard events.
*/ */
class KeyboardManager { class KeyboardManager {
private static listeners: Map<string, ((event: KeyboardEvent) => void)[]> = new Map(); private static listeners: ((event: KeyboardEvent) => void)[] = [];
public static init() { public static init() {
window.addEventListener('keydown', KeyboardManager.handleKeyDown); window.addEventListener('keydown', KeyboardManager.handleKeyDown);
} }
/** /**
* Add a callback to be called when the specified key is pressed. * Add a callback to be called when a key is pressed.
* Note: Calling preventDefault() on the event will prevent other callbacks from being called. * Note: Calling preventDefault() on the event will prevent other callbacks from being called.
* @param key The key to listen for. * @param key The key to listen for.
* @param callback The callback to call when the key is pressed. * @param callback The callback to call when the key is pressed.
* @returns A function that can be called to remove the callback. * @returns A function that can be called to remove the callback.
*/ */
public static onKey(key: string, callback: (event: KeyboardEvent) => void) { public static onKeyPressed(callback: (event: KeyboardEvent) => void) {
if (!KeyboardManager.listeners.has(key)) { KeyboardManager.listeners = [...KeyboardManager.listeners, callback];
KeyboardManager.listeners.set(key, []); return () => KeyboardManager.offKey(callback);
}
KeyboardManager.listeners.get(key).push(callback);
return () => KeyboardManager.offKey(key, callback);
} }
/** /**
* Remove a callback from the specified key. * Remove a callback.
*/ */
public static offKey(key: string, callback: (event: KeyboardEvent) => void) { public static offKey(callback: (event: KeyboardEvent) => void) {
if (KeyboardManager.listeners.has(key)) { const index = KeyboardManager.listeners.indexOf(callback);
const index = KeyboardManager.listeners.get(key).indexOf(callback);
if (index >= 0) { if (index >= 0) {
KeyboardManager.listeners.get(key).splice(index, 1); KeyboardManager.listeners = [
} ...KeyboardManager.listeners.slice(0, index),
...KeyboardManager.listeners.slice(index + 1),
];
} }
} }
private static handleKeyDown(event: KeyboardEvent) { private static handleKeyDown(event: KeyboardEvent) {
if (KeyboardManager.listeners.has(event.key)) { if (KeyboardManager.listeners.length > 0) {
console.log(`Key down: ${event.key}`); console.log(`Key down: ${event.key}`);
const callbacks = KeyboardManager.listeners.get(event.key); for (let i = KeyboardManager.listeners.length - 1; i >= 0; i--) {
for (let i = callbacks.length - 1; i >= 0; i--) { KeyboardManager.listeners[i](event);
callbacks[i](event);
if (event.defaultPrevented) { if (event.defaultPrevented) {
break; break;
} }

View File

@ -1,119 +1,91 @@
import * as PIXI from 'pixi.js'; import * as PIXI from 'pixi.js';
import GuiObject from '../GuiObject';
import GameAssets from '../Assets'; import GameAssets from '../Assets';
import GameUIConstants from '../GameUIConstants'; import GameUIConstants from '../GameUIConstants';
import Button, { ButtonTexture } from './Button'; import ModalDialogBase from './ModalDialog';
import { Engine } from '../Bastion'; import TextInput from './TextInput';
import { EndMissionDialogEvents } from '../Events';
import MessageBox from './MessageBox'; import MessageBox from './MessageBox';
import KeyboardManager from '../game/KeyboardManager';
export default class EndGameDialog extends GuiObject { export const EndGameDialogButtons = {
private dialogSprite: PIXI.NineSliceSprite; Confirm: 'OK',
Skip: 'Skip',
};
export default class EndGameDialog extends ModalDialogBase {
private dialogCaption: PIXI.Text; private dialogCaption: PIXI.Text;
private nextMissionButton: Button; private playerNameTextInput: TextInput;
private retryButton: Button;
private mainMenuButton: Button;
private overlay: PIXI.Graphics;
private lost: boolean; private lost: boolean;
private keyboardManagerUnsubscribe: () => void;
constructor(bounds: PIXI.Rectangle, lost: boolean) { constructor(lost: boolean) {
super(); super(
[EndGameDialogButtons.Confirm, EndGameDialogButtons.Skip],
EndGameDialogButtons.Confirm,
EndGameDialogButtons.Skip
);
this.lost = lost; this.lost = lost;
// Show overlay to prevent user from interacting with the game }
this.overlay = new PIXI.Graphics();
this.overlay.rect(0, 0, bounds.width, bounds.height);
this.overlay.fill({ color: 0x000000, alpha: 0.5 });
// Prevent interaction with the underlying scene
this.overlay.interactive = true;
this.container.addChild(this.overlay);
this.dialogSprite = new PIXI.NineSliceSprite({ protected override generate(): void {
super.generate();
this.dialogCaption = new PIXI.Text({
text: this.lost ? 'You lost!' : 'You won!',
style: new PIXI.TextStyle({
fill: 0xffffff,
fontSize: 36,
}),
});
this.dialogContainer.addChild(this.dialogCaption);
this.dialogCaption.anchor.set(0.5, 0.5);
this.dialogCaption.x = this.dialogContainer.width / 2;
this.dialogCaption.y = 50;
}
protected override createDialogBackground(width: number, height: number): PIXI.Container {
const background = new PIXI.NineSliceSprite({
texture: GameAssets.EndScreenDialog, texture: GameAssets.EndScreenDialog,
leftWidth: 50, leftWidth: 50,
topHeight: 100, topHeight: 100,
rightWidth: 50, rightWidth: 50,
bottomHeight: 50, bottomHeight: 50,
}); });
this.dialogSprite.x = GameUIConstants.EndGameDialogRect.x; background.x = 0;
this.dialogSprite.y = GameUIConstants.EndGameDialogRect.y; background.y = 0;
this.dialogSprite.width = GameUIConstants.EndGameDialogRect.width; background.width = width;
this.dialogSprite.height = GameUIConstants.EndGameDialogRect.height; background.height = height;
this.container.addChild(this.dialogSprite); return background;
this.dialogCaption = new PIXI.Text({ }
text: lost ? 'You lost!' : 'You won!',
protected override createContent(): PIXI.Container {
const container = new PIXI.Container();
const caption = new PIXI.Text({
text: 'Enter your name:',
style: new PIXI.TextStyle({ style: new PIXI.TextStyle({
fill: 0xffffff, fill: 0xffffff,
fontSize: 36, fontSize: 24,
}), }),
}); });
this.container.addChild(this.dialogCaption); container.addChild(caption);
this.dialogCaption.anchor.set(0.5, 0.5); this.playerNameTextInput = new TextInput(
this.dialogCaption.x = GameUIConstants.EndGameDialogRect.x + GameUIConstants.EndGameDialogRect.width / 2; GameUIConstants.MaximumPlayerNameLength * 20,
this.dialogCaption.y = GameUIConstants.EndGameDialogRect.y + 50; GameUIConstants.MaximumPlayerNameLength
//this.setupButtons(lost); );
this.keyboardManagerUnsubscribe = KeyboardManager.onKey('Escape', (e) => { this.playerNameTextInput.container.y = caption.height + 10;
if (e.key === 'Escape') { container.addChild(this.playerNameTextInput.container);
this.onMainMission(); return container;
}
});
this.container.on('destroyed', () => {
this.keyboardManagerUnsubscribe();
});
} }
private showButtons(lost: boolean) { override close(button?: string): void {
const buttonContainer = new PIXI.Container(); if (button === EndGameDialogButtons.Confirm && this.playerNameTextInput.getText().length == 0) {
const buttonWidth = 200; MessageBox.show('Please enter your name.', ['OK']);
const buttonHeight = 80;
const buttonPadding = 10;
if (lost) {
this.retryButton = new Button(
new PIXI.Rectangle(0, 0, buttonWidth, buttonHeight),
'Retry',
ButtonTexture.Button01
);
this.retryButton.onClick = () => {
Engine.GameScene.events.emit(EndMissionDialogEvents.RetryMission);
};
buttonContainer.addChild(this.retryButton.container);
} else { } else {
this.nextMissionButton = new Button( super.close(button);
new PIXI.Rectangle(0, 0, buttonWidth, buttonHeight),
'Next Mission',
ButtonTexture.Button01
);
this.nextMissionButton.onClick = () => {
Engine.GameScene.events.emit(EndMissionDialogEvents.NextMission);
};
buttonContainer.addChild(this.nextMissionButton.container);
} }
this.mainMenuButton = new Button(
new PIXI.Rectangle(0, buttonHeight + buttonPadding, buttonWidth, buttonHeight),
'Main Menu',
ButtonTexture.Button01
);
this.mainMenuButton.onClick = this.onMainMission.bind(this);
buttonContainer.addChild(this.mainMenuButton.container);
this.container.addChild(buttonContainer);
buttonContainer.x =
GameUIConstants.EndGameDialogRect.x +
GameUIConstants.EndGameDialogRect.width / 2 -
buttonContainer.width / 2;
buttonContainer.y =
GameUIConstants.EndGameDialogRect.y + GameUIConstants.EndGameDialogRect.height - buttonContainer.height;
} }
private showingMessageBox: boolean; protected override getWidth(): number | undefined {
return GameUIConstants.StandardDialogWidth;
}
private async onMainMission() { protected override getHeight(): number | undefined {
if (this.showingMessageBox) return; return GameUIConstants.StandardDialogHeight;
this.showingMessageBox = true;
const result = await MessageBox.show('Are you sure you want to return to the main menu?', ['Yes', 'No']);
this.showingMessageBox = false;
if (result === 'Yes') {
Engine.GameScene.events.emit(EndMissionDialogEvents.MainMenu);
}
} }
} }

View File

@ -0,0 +1,78 @@
import * as PIXI from 'pixi.js';
import GameAssets from '../Assets';
import GameUIConstants from '../GameUIConstants';
import ModalDialogBase from './ModalDialog';
import TextInput from './TextInput';
export const HighScoreDialogButtons = {
Retry: 'Retry',
MainMenu: 'Main Menu',
NextMission: 'Next Mission',
};
export default class HighScoreDialog extends ModalDialogBase {
private dialogCaption: PIXI.Text;
private playerNameTextInput: TextInput;
private lost: boolean;
constructor(nextMissionAvailable: boolean) {
super(
nextMissionAvailable
? [HighScoreDialogButtons.Retry, HighScoreDialogButtons.NextMission, HighScoreDialogButtons.MainMenu]
: [HighScoreDialogButtons.Retry, HighScoreDialogButtons.MainMenu],
nextMissionAvailable ? HighScoreDialogButtons.NextMission : HighScoreDialogButtons.Retry,
HighScoreDialogButtons.MainMenu
);
}
protected override generate(): void {
super.generate();
this.dialogCaption = new PIXI.Text({
text: 'Highscore',
style: new PIXI.TextStyle({
fill: 0xffffff,
fontSize: 36,
}),
});
this.dialogContainer.addChild(this.dialogCaption);
this.dialogCaption.anchor.set(0.5, 0.5);
this.dialogCaption.x = this.dialogContainer.width / 2;
this.dialogCaption.y = 50;
}
protected override createDialogBackground(width: number, height: number): PIXI.Container {
const background = new PIXI.NineSliceSprite({
texture: GameAssets.EndScreenDialog,
leftWidth: 50,
topHeight: 100,
rightWidth: 50,
bottomHeight: 50,
});
background.x = 0;
background.y = 0;
background.width = width;
background.height = height;
return background;
}
protected override createContent(): PIXI.Container {
const container = new PIXI.Container();
const caption = new PIXI.Text({
text: 'Leaderboard:',
style: new PIXI.TextStyle({
fill: 0xffffff,
fontSize: 24,
}),
});
container.addChild(caption);
return container;
}
protected override getWidth(): number | undefined {
return GameUIConstants.StandardDialogWidth;
}
protected override getHeight(): number | undefined {
return GameUIConstants.StandardDialogHeight;
}
}

View File

@ -5,12 +5,15 @@ import GuiObject from '../GuiObject';
export default class MessageBox extends ModalDialogBase { export default class MessageBox extends ModalDialogBase {
private caption: string; private caption: string;
constructor(caption: string, buttons: string[], escapeKeyIndex: number = buttons.length - 1) { constructor(caption: string, buttons: string[], escapeKeyButton?: string | null, enterKeyButton?: string | null) {
super(buttons, escapeKeyIndex); if (!enterKeyButton && buttons.length > 0) enterKeyButton = buttons[0];
if (!escapeKeyButton && buttons.length > 0) escapeKeyButton = buttons[buttons.length - 1];
super(buttons, escapeKeyButton, enterKeyButton);
this.caption = caption; this.caption = caption;
} }
protected override createContent(): PIXI.Container | GuiObject { protected override createContent(): PIXI.Container {
const text = new PIXI.Text({ const text = new PIXI.Text({
text: this.caption, text: this.caption,
style: new PIXI.TextStyle({ style: new PIXI.TextStyle({

View File

@ -7,25 +7,97 @@ import Button, { ButtonTexture } from './Button';
import KeyboardManager from '../game/KeyboardManager'; import KeyboardManager from '../game/KeyboardManager';
export default abstract class ModalDialogBase extends GuiObject { export default abstract class ModalDialogBase extends GuiObject {
private overlay: PIXI.Graphics; protected overlay: PIXI.Graphics;
private dialogPadding = 40; protected dialogPadding = 40;
private contentPadding = 10; protected contentPadding = 10;
private buttonPadding = 10; protected buttonPadding = 10;
private buttonAreaHeight = 40; protected buttonAreaHeight = 40;
private buttonHeight = 60; protected buttonHeight = 60;
private buttonCaptions: string[]; protected buttonCaptions: string[];
private buttons: Button[] = []; protected buttons: Button[] = [];
private escapeKeyIndex: number; protected dialogContent: PIXI.Container;
private keyboardManagerUnsubscribe: () => void; protected dialogContainer: PIXI.Container;
private pixiContent: PIXI.Container;
private guiContent: GuiObject;
private generated = false;
constructor(buttonCaptions: string[], escapeKeyIndex: number = buttonCaptions.length - 1) { private generated = false;
private escapeKeyButton?: string | null;
private enterKeyButton?: string | null;
private keyboardManagerUnsubscribe: () => void;
protected constructor(buttons: string[], enterKeyButton?: string | null, escapeKeyButton?: string | null) {
super(); super();
this.escapeKeyIndex = escapeKeyIndex; this.buttonCaptions = buttons;
this.buttonCaptions = buttonCaptions; if (escapeKeyButton && !buttons.includes(escapeKeyButton))
this.keyboardManagerUnsubscribe = KeyboardManager.onKey('Escape', this.onKeyPress.bind(this)); throw new Error(`Escape key button "${escapeKeyButton}" not found in buttons: ${buttons}`);
this.escapeKeyButton = escapeKeyButton;
if (enterKeyButton && !buttons.includes(enterKeyButton))
throw new Error(`Enter key button "${enterKeyButton}" not found in buttons: ${buttons}`);
this.enterKeyButton = enterKeyButton;
this.keyboardManagerUnsubscribe = KeyboardManager.onKeyPressed(this.onKeyPress.bind(this));
}
/**
* Show the dialog and await the user's response.
* @param width The width of the dialog (optional, if not provided then it is automatically calculated).
* @param height The height of the dialog (optional, if not provided then it is automatically calculated).
* @returns Button that was clicked or null if dialog was closed without clicking a button (potentially no buttons defined).
*/
public show(): Promise<string | null> {
this.generate();
console.debug(
`ModalDialogBase.show(content: ${this.dialogContainer.width}x${this.dialogContainer.height}, buttons: ${this.buttonCaptions})`
);
return new Promise((resolve, reject) => {
Engine.app.stage.addChild(this.container);
this.onClosed = (button) => {
this.destroy();
resolve(button);
};
});
}
/**
* Event that is triggered when the dialog is closed.
* @param button The button that was clicked or null if the dialog was closed without clicking a button.
*/
public onClosed: (button?: string) => void;
/**
* Create the content of the dialog.
*/
protected abstract createContent(): PIXI.Container;
/**
* Creates dialog background.
*/
protected createDialogBackground(width: number, height: number): PIXI.Container {
const background = new PIXI.NineSliceSprite({
texture: GameAssets.Frame04Texture,
leftWidth: 60,
topHeight: 60,
rightWidth: 60,
bottomHeight: 60,
});
background.x = 0;
background.y = 0;
background.width = width;
background.height = height;
return background;
}
/**
* Gets the width of the dialog.
* If not overridden, the width is automatically calculated.
*/
protected getWidth(): number | undefined {
return undefined;
}
/**
* Gets the height of the dialog.
* If not overridden, the height is automatically calculated.
*/
protected getHeight(): number | undefined {
return undefined;
} }
protected generate() { protected generate() {
@ -39,30 +111,22 @@ export default abstract class ModalDialogBase extends GuiObject {
this.overlay.interactive = true; this.overlay.interactive = true;
this.container.addChild(this.overlay); this.container.addChild(this.overlay);
const content = this.createContent(); this.dialogContent = this.createContent();
if (content instanceof GuiObject) {
this.guiContent = content;
this.pixiContent = content.container;
} else {
this.pixiContent = content;
}
const buttonDefs = this.buttonCaptions.map((btnCaption) => ({ const buttonDefs = this.buttonCaptions.map((btnCaption) => ({
caption: btnCaption, caption: btnCaption,
width: btnCaption.length * 16 + 40, width: btnCaption.length * 16 + 40,
height: this.buttonHeight, height: this.buttonHeight,
click: () => this.buttonClickHandler(btnCaption), click: () => this.close(btnCaption),
})); }));
let buttonTotalWidth = 0; let buttonTotalWidth = 0;
for (const buttonDef of buttonDefs) { for (const buttonDef of buttonDefs) {
if (buttonTotalWidth > 0) buttonTotalWidth += this.buttonPadding; if (buttonTotalWidth > 0) buttonTotalWidth += this.buttonPadding;
buttonTotalWidth += buttonDef.width; buttonTotalWidth += buttonDef.width;
} }
const contentWidth = this.pixiContent.width + this.contentPadding * 2; const contentWidth = this.dialogContent.width + this.contentPadding * 2;
const contentHeight = this.pixiContent.height + this.contentPadding * 2; const contentHeight = this.dialogContent.height + this.contentPadding * 2;
let width = Math.max(buttonTotalWidth, contentWidth) + this.dialogPadding * 2; let width = this.getWidth() || Math.max(buttonTotalWidth, contentWidth) + this.dialogPadding * 2;
let height = this.getHeight() || contentHeight + this.buttonAreaHeight + this.dialogPadding * 2;
const height = contentHeight + this.buttonAreaHeight + this.dialogPadding * 2;
const modalBounds = new PIXI.Rectangle( const modalBounds = new PIXI.Rectangle(
Engine.app.canvas.width / 2 - width / 2, Engine.app.canvas.width / 2 - width / 2,
Engine.app.canvas.height / 2 - height / 2, Engine.app.canvas.height / 2 - height / 2,
@ -70,28 +134,18 @@ export default abstract class ModalDialogBase extends GuiObject {
height height
); );
const modalContainer = new PIXI.Container(); this.dialogContainer = new PIXI.Container();
modalContainer.x = modalBounds.x; this.dialogContainer.x = modalBounds.x;
modalContainer.y = modalBounds.y; this.dialogContainer.y = modalBounds.y;
const background = new PIXI.NineSliceSprite({ const background = this.createDialogBackground(modalBounds.width, modalBounds.height);
texture: GameAssets.Frame04Texture, this.dialogContainer.addChild(background);
leftWidth: 60,
topHeight: 60,
rightWidth: 60,
bottomHeight: 60,
});
background.x = 0;
background.y = 0;
background.width = modalBounds.width;
background.height = modalBounds.height;
modalContainer.addChild(background);
if (this.pixiContent.width < modalBounds.width) if (this.dialogContent.width < modalBounds.width)
this.pixiContent.x = modalBounds.width / 2 - this.pixiContent.width / 2; this.dialogContent.x = modalBounds.width / 2 - this.dialogContent.width / 2;
if (this.pixiContent.height < modalBounds.height - this.buttonAreaHeight) if (this.dialogContent.height < modalBounds.height - this.buttonAreaHeight)
this.pixiContent.y = (modalBounds.height - this.buttonAreaHeight) / 2 - this.pixiContent.height / 2; this.dialogContent.y = (modalBounds.height - this.buttonAreaHeight) / 2 - this.dialogContent.height / 2;
modalContainer.addChild(this.pixiContent); this.dialogContainer.addChild(this.dialogContent);
let buttonXPos = modalBounds.width / 2 - buttonTotalWidth / 2; let buttonXPos = modalBounds.width / 2 - buttonTotalWidth / 2;
for (const buttonDef of buttonDefs) { for (const buttonDef of buttonDefs) {
@ -107,52 +161,36 @@ export default abstract class ModalDialogBase extends GuiObject {
); );
button.onClick = buttonDef.click; button.onClick = buttonDef.click;
this.buttons.push(button); this.buttons.push(button);
modalContainer.addChild(button.container); this.dialogContainer.addChild(button.container);
buttonXPos += buttonDef.width + this.buttonPadding; buttonXPos += buttonDef.width + this.buttonPadding;
} }
this.container.addChild(modalContainer); this.container.addChild(this.dialogContainer);
}
protected abstract createContent(): PIXI.Container | GuiObject;
public show(): Promise<string> {
this.generate();
console.debug(
`ModalDialogBase.show(content: ${this.pixiContent.width}x${this.pixiContent.height}, buttons: ${this.buttonCaptions})`
);
return new Promise((resolve, reject) => {
Engine.app.stage.addChild(this.container);
this.onButtonClicked = (button) => {
this.destroy();
resolve(button);
};
});
} }
/** /**
* Event that is triggered when the button is clicked. * Close the dialog.
* Only use this method if you want to close the dialog without clicking a button.
*/ */
public onButtonClicked: (button: string) => void; public close(button?: string) {
this.destroy();
if (this.onClosed) this.onClosed(button);
}
override destroy(): void { override destroy(): void {
this.keyboardManagerUnsubscribe(); this.keyboardManagerUnsubscribe();
this.guiContent?.destroy();
super.destroy(); super.destroy();
} }
protected buttonClickHandler(button: string) {
if (this.onButtonClicked) this.onButtonClicked(button);
this.destroy();
}
private onKeyPress(event: KeyboardEvent) { private onKeyPress(event: KeyboardEvent) {
if (this.buttons.length === 0) return; if (this.buttons.length === 0) return;
if (this.escapeKeyButton && event.key === 'Escape') {
// Message box is modal, so we can safely prevent the default behavior // Message box is modal, so we can safely prevent the default behavior
event.preventDefault(); event.preventDefault();
if (event.key === 'Escape') { this.close(this.escapeKeyButton);
this.buttonClickHandler(this.buttons[this.escapeKeyIndex].getCaption()); } else if (this.enterKeyButton && event.key === 'Enter') {
} else if (event.key === 'Enter') { // Message box is modal, so we can safely prevent the default behavior
this.buttonClickHandler(this.buttons[0].getCaption()); event.preventDefault();
this.close(this.enterKeyButton);
} }
} }
} }

View File

@ -1,45 +0,0 @@
import * as PIXI from 'pixi.js';
import { Engine } from '../Bastion';
import ModalDialogBase from './ModalDialog';
import TextInput from './TextInput';
import GuiObject from '../GuiObject';
const maxNameLength = 20;
export default class PlayerNameInput extends ModalDialogBase {
private textInput: TextInput;
constructor(content: PIXI.Container) {
super(['OK', 'Cancel']);
}
public getName(): string {
return this.textInput.getText();
}
protected override createContent(): PIXI.Container | GuiObject {
const container = new PIXI.Container();
const caption = new PIXI.Text({
text: 'Enter your name:',
style: new PIXI.TextStyle({
fill: 0xffffff,
fontSize: 24,
}),
});
container.addChild(caption);
this.textInput = new TextInput(new PIXI.Rectangle(0, 0, maxNameLength * 20, 40), maxNameLength);
this.textInput.container.y = caption.height + 10;
container.addChild(this.textInput.container);
return container;
}
override buttonClickHandler(button: string) {
if (button === 'OK') {
if (this.textInput.getText().length > 0) {
super.buttonClickHandler(button);
}
} else {
super.buttonClickHandler(button);
}
}
}

View File

@ -2,25 +2,21 @@ import * as PIXI from 'pixi.js';
import GuiObject from '../GuiObject'; import GuiObject from '../GuiObject';
import Assets from '../Assets'; import Assets from '../Assets';
import GameAssets from '../Assets'; import GameAssets from '../Assets';
import KeyboardManager from '../game/KeyboardManager';
export default class TextInput extends GuiObject { export default class TextInput extends GuiObject {
private bounds: PIXI.Rectangle;
private backgroundSprite: PIXI.NineSliceSprite; private backgroundSprite: PIXI.NineSliceSprite;
private text: PIXI.Text; private text: PIXI.Text;
private maxLength: number; private maxLength: number;
private keyboardManagerUnsubscribe: () => void;
public getText(): string { public getText(): string {
return this.text.text; return this.text.text;
} }
constructor(bounds: PIXI.Rectangle, maxLength: number) { constructor(width: number, maxLength: number) {
super(); super();
this.bounds = bounds;
this.maxLength = maxLength; this.maxLength = maxLength;
this.container.x = this.bounds.x;
this.container.y = this.bounds.y;
this.container.width = this.bounds.width;
this.container.height = this.bounds.height;
this.backgroundSprite = new PIXI.NineSliceSprite({ this.backgroundSprite = new PIXI.NineSliceSprite({
texture: GameAssets.Frame01Texture, texture: GameAssets.Frame01Texture,
leftWidth: 20, leftWidth: 20,
@ -30,21 +26,20 @@ export default class TextInput extends GuiObject {
}); });
this.backgroundSprite.x = 0; this.backgroundSprite.x = 0;
this.backgroundSprite.y = 0; this.backgroundSprite.y = 0;
this.backgroundSprite.width = this.bounds.width; this.backgroundSprite.width = width;
this.backgroundSprite.height = this.bounds.height; this.backgroundSprite.height = 60;
this.container.addChild(this.backgroundSprite); this.container.addChild(this.backgroundSprite);
this.container.x = this.bounds.x;
this.container.y = this.bounds.y;
this.text = new PIXI.Text({ this.text = new PIXI.Text({
text: '', text: '',
style: new PIXI.TextStyle({ style: new PIXI.TextStyle({
fill: 0xffffff, fill: 0xffffff,
fontSize: 16, fontSize: 24,
}), }),
}); });
this.text.x = 10; this.text.x = 20;
this.text.y = 10; this.text.y = 20;
this.container.addChild(this.text); this.container.addChild(this.text);
this.keyboardManagerUnsubscribe = KeyboardManager.onKeyPressed(this.onKeyPress.bind(this));
} }
private onKeyPress(event: KeyboardEvent) { private onKeyPress(event: KeyboardEvent) {

View File

@ -4,7 +4,7 @@ import { MissionDefinition } from '../classes/Definitions';
import Creep from '../classes/game/Creep'; import Creep from '../classes/game/Creep';
import { Grid } from '../classes/game/Grid'; import { Grid } from '../classes/game/Grid';
import WaveManager from '../classes/game/WaveManager'; import WaveManager from '../classes/game/WaveManager';
import { WaveManagerEvents, CreepEvents, GemEvents, EndMissionDialogEvents } from '../classes/Events'; import { WaveManagerEvents, CreepEvents, GemEvents } from '../classes/Events';
import Sidebar from '../classes/gui/Sidebar'; import Sidebar from '../classes/gui/Sidebar';
import Button, { ButtonTexture } from '../classes/gui/Button'; import Button, { ButtonTexture } from '../classes/gui/Button';
import Scene from './Scene'; import Scene from './Scene';
@ -17,6 +17,7 @@ import Tooltip from '../classes/gui/Tooltip';
import TowerPanel, { VisualGemSlot } from '../classes/gui/TowerPanel'; import TowerPanel, { VisualGemSlot } from '../classes/gui/TowerPanel';
import Gem from '../classes/game/Gem'; import Gem from '../classes/game/Gem';
import EndGameDialog from '../classes/gui/EndGameDialog'; import EndGameDialog from '../classes/gui/EndGameDialog';
import HighScoreDialog, { HighScoreDialogButtons } from '../classes/gui/HighScoreDialog';
enum RoundMode { enum RoundMode {
Purchase = 0, Purchase = 0,
@ -41,7 +42,6 @@ export class GameScene extends Scene {
private playerWon: boolean = false; private playerWon: boolean = false;
private destroyTicker: boolean = false; private destroyTicker: boolean = false;
private offerGemsSprite: PIXI.NineSliceSprite; private offerGemsSprite: PIXI.NineSliceSprite;
private endGameDialog: EndGameDialog;
private dimGraphics: PIXI.Graphics = new PIXI.Graphics({ private dimGraphics: PIXI.Graphics = new PIXI.Graphics({
x: 0, x: 0,
y: 0, y: 0,
@ -119,9 +119,6 @@ export class GameScene extends Scene {
} }
this.sidebar.gemTab.TowerPanelSelectingGem(gem, index, tower); this.sidebar.gemTab.TowerPanelSelectingGem(gem, index, tower);
}); });
this.events.on(EndMissionDialogEvents.MainMenu, this.onEndMissionDialogMainMenuClicked.bind(this));
this.events.on(EndMissionDialogEvents.RetryMission, this.onEndMissionDialogRetryMissionClicked.bind(this));
this.events.on(EndMissionDialogEvents.NextMission, this.onEndMissionDialogNextMissionClicked.bind(this));
this.ticker = new PIXI.Ticker(); this.ticker = new PIXI.Ticker();
this.ticker.maxFPS = 60; this.ticker.maxFPS = 60;
this.ticker.minFPS = 30; this.ticker.minFPS = 30;
@ -168,10 +165,10 @@ export class GameScene extends Scene {
if (this.MissionStats.getHP() <= 0) { if (this.MissionStats.getHP() <= 0) {
this.isGameOver = true; this.isGameOver = true;
this.ShowScoreScreen(true); this.ShowEndgameDialog(true);
} else if (this.playerWon) { } else if (this.playerWon) {
this.isGameOver = true; this.isGameOver = true;
this.ShowScoreScreen(false); this.ShowEndgameDialog(false);
} }
} }
public DarkenScreen() { public DarkenScreen() {
@ -247,10 +244,18 @@ export class GameScene extends Scene {
this.setRoundMode(RoundMode.Purchase); this.setRoundMode(RoundMode.Purchase);
} }
private ShowScoreScreen(lost) { private async ShowEndgameDialog(lost) {
const bounds = new PIXI.Rectangle(0, 0, Engine.app.canvas.width, Engine.app.canvas.height); const endGameDialog = new EndGameDialog(lost);
this.endGameDialog = new EndGameDialog(bounds, lost); await endGameDialog.show();
Engine.GameMaster.currentScene.stage.addChild(this.endGameDialog.container); const highScore = new HighScoreDialog(this.missionIndex + 1 < GameAssets.Missions.length);
const result = await highScore.show();
if (result === HighScoreDialogButtons.MainMenu) {
this.ReturnToMain();
} else if (result === HighScoreDialogButtons.NextMission) {
Engine.GameMaster.changeScene(new GameScene(GameAssets.Missions[this.missionIndex + 1].name));
} else if (result === HighScoreDialogButtons.Retry) {
Engine.GameMaster.changeScene(new GameScene(this.mission.name));
}
} }
public onCreepEscaped(creep: Creep) { public onCreepEscaped(creep: Creep) {
@ -266,14 +271,6 @@ export class GameScene extends Scene {
} }
} }
private onEndMissionDialogMainMenuClicked() {
this.ReturnToMain();
}
private onEndMissionDialogRetryMissionClicked() {}
private onEndMissionDialogNextMissionClicked() {}
public destroy(): void { public destroy(): void {
super.destroy(); super.destroy();
this.isGameOver = true; this.isGameOver = true;
@ -282,8 +279,6 @@ export class GameScene extends Scene {
} }
private ReturnToMain() { private ReturnToMain() {
this.destroy();
Engine.GameMaster.currentScene.stage.removeChildren();
Engine.GameMaster.changeScene(new MissionPickerScene()); Engine.GameMaster.changeScene(new MissionPickerScene());
} }
} }