From 2e1c73c9dc90de4bd2c08b8dbc7138ae1f4b81aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20=C4=8Carapi=C4=87?= Date: Thu, 6 Feb 2025 18:20:00 +0100 Subject: [PATCH] Working on end-game menu --- src/classes/Bastion.ts | 4 - src/classes/Events.ts | 6 - src/classes/GameUIConstants.ts | 14 +- src/classes/game/KeyboardManager.ts | 35 +++-- src/classes/gui/EndGameDialog.ts | 158 +++++++++------------ src/classes/gui/HighScoreDialog.ts | 78 +++++++++++ src/classes/gui/MessageBox.ts | 9 +- src/classes/gui/ModalDialog.ts | 204 +++++++++++++++++----------- src/classes/gui/PlayerNameInput.ts | 45 ------ src/classes/gui/TextInput.ts | 23 ++-- src/scenes/Game.ts | 37 +++-- 11 files changed, 316 insertions(+), 297 deletions(-) create mode 100644 src/classes/gui/HighScoreDialog.ts delete mode 100644 src/classes/gui/PlayerNameInput.ts diff --git a/src/classes/Bastion.ts b/src/classes/Bastion.ts index 36c9f56..9a9f95d 100644 --- a/src/classes/Bastion.ts +++ b/src/classes/Bastion.ts @@ -41,7 +41,6 @@ export class Engine { export default class GameMaster { public currentScene: Scene; - private GameObjects: GameObject[] = []; constructor() { Engine.GameMaster = this; @@ -61,9 +60,6 @@ export default class GameMaster { if (this.currentScene) { this.currentScene.destroy(); } - this.GameObjects.forEach((element) => { - element.destroy(); - }); this.currentScene = newScene; this.currentScene.init(); } diff --git a/src/classes/Events.ts b/src/classes/Events.ts index 581ff8e..67be16b 100644 --- a/src/classes/Events.ts +++ b/src/classes/Events.ts @@ -28,9 +28,3 @@ export enum StatsEvents { export enum GemEvents { TowerPanelSelectGem = 'towerTabSelectGem', } - -export enum EndMissionDialogEvents { - NextMission = 'nextMission', - RetryMission = 'retryMission', - MainMenu = 'mainMenu', -} diff --git a/src/classes/GameUIConstants.ts b/src/classes/GameUIConstants.ts index f78bf33..abae08f 100644 --- a/src/classes/GameUIConstants.ts +++ b/src/classes/GameUIConstants.ts @@ -3,7 +3,9 @@ import { Engine } from './Bastion'; export default class GameUIConstants { public static SidebarRect: 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() { GameUIConstants.SidebarRect = new PIXI.Rectangle( @@ -13,13 +15,7 @@ export default class GameUIConstants { Engine.app.canvas.height ); GameUIConstants.ChangeRoundButtonRect = new PIXI.Rectangle(50, Engine.app.canvas.height - 100, 310, 100); - const endGameDialogWidth = 600; - const endGameDialogHeight = 800; - GameUIConstants.EndGameDialogRect = new PIXI.Rectangle( - (Engine.app.canvas.width - endGameDialogWidth) / 2, - (Engine.app.canvas.height - endGameDialogHeight) / 2, - endGameDialogWidth, - endGameDialogHeight - ); + GameUIConstants.StandardDialogWidth = 600; + GameUIConstants.StandardDialogHeight = 800; } } diff --git a/src/classes/game/KeyboardManager.ts b/src/classes/game/KeyboardManager.ts index 37ab5db..bffe515 100644 --- a/src/classes/game/KeyboardManager.ts +++ b/src/classes/game/KeyboardManager.ts @@ -2,45 +2,42 @@ * Handles keyboard events. */ class KeyboardManager { - private static listeners: Map void)[]> = new Map(); + private static listeners: ((event: KeyboardEvent) => void)[] = []; public static init() { 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. * @param key The key to listen for. * @param callback The callback to call when the key is pressed. * @returns A function that can be called to remove the callback. */ - public static onKey(key: string, callback: (event: KeyboardEvent) => void) { - if (!KeyboardManager.listeners.has(key)) { - KeyboardManager.listeners.set(key, []); - } - KeyboardManager.listeners.get(key).push(callback); - return () => KeyboardManager.offKey(key, callback); + public static onKeyPressed(callback: (event: KeyboardEvent) => void) { + KeyboardManager.listeners = [...KeyboardManager.listeners, callback]; + return () => KeyboardManager.offKey(callback); } /** - * Remove a callback from the specified key. + * Remove a callback. */ - public static offKey(key: string, callback: (event: KeyboardEvent) => void) { - if (KeyboardManager.listeners.has(key)) { - const index = KeyboardManager.listeners.get(key).indexOf(callback); - if (index >= 0) { - KeyboardManager.listeners.get(key).splice(index, 1); - } + public static offKey(callback: (event: KeyboardEvent) => void) { + const index = KeyboardManager.listeners.indexOf(callback); + if (index >= 0) { + KeyboardManager.listeners = [ + ...KeyboardManager.listeners.slice(0, index), + ...KeyboardManager.listeners.slice(index + 1), + ]; } } private static handleKeyDown(event: KeyboardEvent) { - if (KeyboardManager.listeners.has(event.key)) { + if (KeyboardManager.listeners.length > 0) { console.log(`Key down: ${event.key}`); - const callbacks = KeyboardManager.listeners.get(event.key); - for (let i = callbacks.length - 1; i >= 0; i--) { - callbacks[i](event); + for (let i = KeyboardManager.listeners.length - 1; i >= 0; i--) { + KeyboardManager.listeners[i](event); if (event.defaultPrevented) { break; } diff --git a/src/classes/gui/EndGameDialog.ts b/src/classes/gui/EndGameDialog.ts index 30c419a..99e0e86 100644 --- a/src/classes/gui/EndGameDialog.ts +++ b/src/classes/gui/EndGameDialog.ts @@ -1,119 +1,91 @@ import * as PIXI from 'pixi.js'; -import GuiObject from '../GuiObject'; import GameAssets from '../Assets'; import GameUIConstants from '../GameUIConstants'; -import Button, { ButtonTexture } from './Button'; -import { Engine } from '../Bastion'; -import { EndMissionDialogEvents } from '../Events'; +import ModalDialogBase from './ModalDialog'; +import TextInput from './TextInput'; import MessageBox from './MessageBox'; -import KeyboardManager from '../game/KeyboardManager'; -export default class EndGameDialog extends GuiObject { - private dialogSprite: PIXI.NineSliceSprite; +export const EndGameDialogButtons = { + Confirm: 'OK', + Skip: 'Skip', +}; + +export default class EndGameDialog extends ModalDialogBase { private dialogCaption: PIXI.Text; - private nextMissionButton: Button; - private retryButton: Button; - private mainMenuButton: Button; - private overlay: PIXI.Graphics; + private playerNameTextInput: TextInput; private lost: boolean; - private keyboardManagerUnsubscribe: () => void; - constructor(bounds: PIXI.Rectangle, lost: boolean) { - super(); + constructor(lost: boolean) { + super( + [EndGameDialogButtons.Confirm, EndGameDialogButtons.Skip], + EndGameDialogButtons.Confirm, + EndGameDialogButtons.Skip + ); 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, leftWidth: 50, topHeight: 100, rightWidth: 50, bottomHeight: 50, }); - this.dialogSprite.x = GameUIConstants.EndGameDialogRect.x; - this.dialogSprite.y = GameUIConstants.EndGameDialogRect.y; - this.dialogSprite.width = GameUIConstants.EndGameDialogRect.width; - this.dialogSprite.height = GameUIConstants.EndGameDialogRect.height; - this.container.addChild(this.dialogSprite); - this.dialogCaption = new PIXI.Text({ - text: lost ? 'You lost!' : 'You won!', + 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: 'Enter your name:', style: new PIXI.TextStyle({ fill: 0xffffff, - fontSize: 36, + fontSize: 24, }), }); - this.container.addChild(this.dialogCaption); - this.dialogCaption.anchor.set(0.5, 0.5); - this.dialogCaption.x = GameUIConstants.EndGameDialogRect.x + GameUIConstants.EndGameDialogRect.width / 2; - this.dialogCaption.y = GameUIConstants.EndGameDialogRect.y + 50; - //this.setupButtons(lost); - this.keyboardManagerUnsubscribe = KeyboardManager.onKey('Escape', (e) => { - if (e.key === 'Escape') { - this.onMainMission(); - } - }); - this.container.on('destroyed', () => { - this.keyboardManagerUnsubscribe(); - }); - } - - private showButtons(lost: boolean) { - const buttonContainer = new PIXI.Container(); - const buttonWidth = 200; - 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 { - this.nextMissionButton = new 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 + container.addChild(caption); + this.playerNameTextInput = new TextInput( + GameUIConstants.MaximumPlayerNameLength * 20, + GameUIConstants.MaximumPlayerNameLength ); - 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; + this.playerNameTextInput.container.y = caption.height + 10; + container.addChild(this.playerNameTextInput.container); + return container; } - private showingMessageBox: boolean; - - private async onMainMission() { - if (this.showingMessageBox) return; - 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); + override close(button?: string): void { + if (button === EndGameDialogButtons.Confirm && this.playerNameTextInput.getText().length == 0) { + MessageBox.show('Please enter your name.', ['OK']); + } else { + super.close(button); } } + + protected override getWidth(): number | undefined { + return GameUIConstants.StandardDialogWidth; + } + + protected override getHeight(): number | undefined { + return GameUIConstants.StandardDialogHeight; + } } diff --git a/src/classes/gui/HighScoreDialog.ts b/src/classes/gui/HighScoreDialog.ts new file mode 100644 index 0000000..e07980a --- /dev/null +++ b/src/classes/gui/HighScoreDialog.ts @@ -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; + } +} diff --git a/src/classes/gui/MessageBox.ts b/src/classes/gui/MessageBox.ts index 9bfd95f..7febad1 100644 --- a/src/classes/gui/MessageBox.ts +++ b/src/classes/gui/MessageBox.ts @@ -5,12 +5,15 @@ import GuiObject from '../GuiObject'; export default class MessageBox extends ModalDialogBase { private caption: string; - constructor(caption: string, buttons: string[], escapeKeyIndex: number = buttons.length - 1) { - super(buttons, escapeKeyIndex); + constructor(caption: string, buttons: string[], escapeKeyButton?: string | null, enterKeyButton?: string | null) { + 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; } - protected override createContent(): PIXI.Container | GuiObject { + protected override createContent(): PIXI.Container { const text = new PIXI.Text({ text: this.caption, style: new PIXI.TextStyle({ diff --git a/src/classes/gui/ModalDialog.ts b/src/classes/gui/ModalDialog.ts index a63480a..c7a9495 100644 --- a/src/classes/gui/ModalDialog.ts +++ b/src/classes/gui/ModalDialog.ts @@ -7,25 +7,97 @@ import Button, { ButtonTexture } from './Button'; import KeyboardManager from '../game/KeyboardManager'; export default abstract class ModalDialogBase extends GuiObject { - private overlay: PIXI.Graphics; - private dialogPadding = 40; - private contentPadding = 10; - private buttonPadding = 10; - private buttonAreaHeight = 40; - private buttonHeight = 60; - private buttonCaptions: string[]; - private buttons: Button[] = []; - private escapeKeyIndex: number; - private keyboardManagerUnsubscribe: () => void; - private pixiContent: PIXI.Container; - private guiContent: GuiObject; - private generated = false; + protected overlay: PIXI.Graphics; + protected dialogPadding = 40; + protected contentPadding = 10; + protected buttonPadding = 10; + protected buttonAreaHeight = 40; + protected buttonHeight = 60; + protected buttonCaptions: string[]; + protected buttons: Button[] = []; + protected dialogContent: PIXI.Container; + protected dialogContainer: PIXI.Container; - 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(); - this.escapeKeyIndex = escapeKeyIndex; - this.buttonCaptions = buttonCaptions; - this.keyboardManagerUnsubscribe = KeyboardManager.onKey('Escape', this.onKeyPress.bind(this)); + this.buttonCaptions = buttons; + if (escapeKeyButton && !buttons.includes(escapeKeyButton)) + 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 { + 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() { @@ -39,30 +111,22 @@ export default abstract class ModalDialogBase extends GuiObject { this.overlay.interactive = true; this.container.addChild(this.overlay); - const content = this.createContent(); - if (content instanceof GuiObject) { - this.guiContent = content; - this.pixiContent = content.container; - } else { - this.pixiContent = content; - } - + this.dialogContent = this.createContent(); const buttonDefs = this.buttonCaptions.map((btnCaption) => ({ caption: btnCaption, width: btnCaption.length * 16 + 40, height: this.buttonHeight, - click: () => this.buttonClickHandler(btnCaption), + click: () => this.close(btnCaption), })); let buttonTotalWidth = 0; for (const buttonDef of buttonDefs) { if (buttonTotalWidth > 0) buttonTotalWidth += this.buttonPadding; buttonTotalWidth += buttonDef.width; } - const contentWidth = this.pixiContent.width + this.contentPadding * 2; - const contentHeight = this.pixiContent.height + this.contentPadding * 2; - let width = Math.max(buttonTotalWidth, contentWidth) + this.dialogPadding * 2; - - const height = contentHeight + this.buttonAreaHeight + this.dialogPadding * 2; + const contentWidth = this.dialogContent.width + this.contentPadding * 2; + const contentHeight = this.dialogContent.height + this.contentPadding * 2; + let width = this.getWidth() || Math.max(buttonTotalWidth, contentWidth) + this.dialogPadding * 2; + let height = this.getHeight() || contentHeight + this.buttonAreaHeight + this.dialogPadding * 2; const modalBounds = new PIXI.Rectangle( Engine.app.canvas.width / 2 - width / 2, Engine.app.canvas.height / 2 - height / 2, @@ -70,28 +134,18 @@ export default abstract class ModalDialogBase extends GuiObject { height ); - const modalContainer = new PIXI.Container(); - modalContainer.x = modalBounds.x; - modalContainer.y = modalBounds.y; + this.dialogContainer = new PIXI.Container(); + this.dialogContainer.x = modalBounds.x; + this.dialogContainer.y = modalBounds.y; - const background = new PIXI.NineSliceSprite({ - texture: GameAssets.Frame04Texture, - 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); + const background = this.createDialogBackground(modalBounds.width, modalBounds.height); + this.dialogContainer.addChild(background); - if (this.pixiContent.width < modalBounds.width) - this.pixiContent.x = modalBounds.width / 2 - this.pixiContent.width / 2; - if (this.pixiContent.height < modalBounds.height - this.buttonAreaHeight) - this.pixiContent.y = (modalBounds.height - this.buttonAreaHeight) / 2 - this.pixiContent.height / 2; - modalContainer.addChild(this.pixiContent); + if (this.dialogContent.width < modalBounds.width) + this.dialogContent.x = modalBounds.width / 2 - this.dialogContent.width / 2; + if (this.dialogContent.height < modalBounds.height - this.buttonAreaHeight) + this.dialogContent.y = (modalBounds.height - this.buttonAreaHeight) / 2 - this.dialogContent.height / 2; + this.dialogContainer.addChild(this.dialogContent); let buttonXPos = modalBounds.width / 2 - buttonTotalWidth / 2; for (const buttonDef of buttonDefs) { @@ -107,52 +161,36 @@ export default abstract class ModalDialogBase extends GuiObject { ); button.onClick = buttonDef.click; this.buttons.push(button); - modalContainer.addChild(button.container); + this.dialogContainer.addChild(button.container); buttonXPos += buttonDef.width + this.buttonPadding; } - this.container.addChild(modalContainer); - } - - protected abstract createContent(): PIXI.Container | GuiObject; - - public show(): Promise { - 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); - }; - }); + this.container.addChild(this.dialogContainer); } /** - * 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 { this.keyboardManagerUnsubscribe(); - this.guiContent?.destroy(); super.destroy(); } - protected buttonClickHandler(button: string) { - if (this.onButtonClicked) this.onButtonClicked(button); - this.destroy(); - } - private onKeyPress(event: KeyboardEvent) { if (this.buttons.length === 0) return; - // Message box is modal, so we can safely prevent the default behavior - event.preventDefault(); - if (event.key === 'Escape') { - this.buttonClickHandler(this.buttons[this.escapeKeyIndex].getCaption()); - } else if (event.key === 'Enter') { - this.buttonClickHandler(this.buttons[0].getCaption()); + if (this.escapeKeyButton && event.key === 'Escape') { + // Message box is modal, so we can safely prevent the default behavior + event.preventDefault(); + this.close(this.escapeKeyButton); + } else if (this.enterKeyButton && event.key === 'Enter') { + // Message box is modal, so we can safely prevent the default behavior + event.preventDefault(); + this.close(this.enterKeyButton); } } } diff --git a/src/classes/gui/PlayerNameInput.ts b/src/classes/gui/PlayerNameInput.ts deleted file mode 100644 index 59c1a9d..0000000 --- a/src/classes/gui/PlayerNameInput.ts +++ /dev/null @@ -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); - } - } -} diff --git a/src/classes/gui/TextInput.ts b/src/classes/gui/TextInput.ts index 9bcacc3..6843f94 100644 --- a/src/classes/gui/TextInput.ts +++ b/src/classes/gui/TextInput.ts @@ -2,25 +2,21 @@ import * as PIXI from 'pixi.js'; import GuiObject from '../GuiObject'; import Assets from '../Assets'; import GameAssets from '../Assets'; +import KeyboardManager from '../game/KeyboardManager'; export default class TextInput extends GuiObject { - private bounds: PIXI.Rectangle; private backgroundSprite: PIXI.NineSliceSprite; private text: PIXI.Text; private maxLength: number; + private keyboardManagerUnsubscribe: () => void; public getText(): string { return this.text.text; } - constructor(bounds: PIXI.Rectangle, maxLength: number) { + constructor(width: number, maxLength: number) { super(); - this.bounds = bounds; 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({ texture: GameAssets.Frame01Texture, leftWidth: 20, @@ -30,21 +26,20 @@ export default class TextInput extends GuiObject { }); this.backgroundSprite.x = 0; this.backgroundSprite.y = 0; - this.backgroundSprite.width = this.bounds.width; - this.backgroundSprite.height = this.bounds.height; + this.backgroundSprite.width = width; + this.backgroundSprite.height = 60; this.container.addChild(this.backgroundSprite); - this.container.x = this.bounds.x; - this.container.y = this.bounds.y; this.text = new PIXI.Text({ text: '', style: new PIXI.TextStyle({ fill: 0xffffff, - fontSize: 16, + fontSize: 24, }), }); - this.text.x = 10; - this.text.y = 10; + this.text.x = 20; + this.text.y = 20; this.container.addChild(this.text); + this.keyboardManagerUnsubscribe = KeyboardManager.onKeyPressed(this.onKeyPress.bind(this)); } private onKeyPress(event: KeyboardEvent) { diff --git a/src/scenes/Game.ts b/src/scenes/Game.ts index 2655e6d..05c3b7c 100644 --- a/src/scenes/Game.ts +++ b/src/scenes/Game.ts @@ -4,7 +4,7 @@ import { MissionDefinition } from '../classes/Definitions'; import Creep from '../classes/game/Creep'; import { Grid } from '../classes/game/Grid'; 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 Button, { ButtonTexture } from '../classes/gui/Button'; import Scene from './Scene'; @@ -17,6 +17,7 @@ import Tooltip from '../classes/gui/Tooltip'; import TowerPanel, { VisualGemSlot } from '../classes/gui/TowerPanel'; import Gem from '../classes/game/Gem'; import EndGameDialog from '../classes/gui/EndGameDialog'; +import HighScoreDialog, { HighScoreDialogButtons } from '../classes/gui/HighScoreDialog'; enum RoundMode { Purchase = 0, @@ -41,7 +42,6 @@ export class GameScene extends Scene { private playerWon: boolean = false; private destroyTicker: boolean = false; private offerGemsSprite: PIXI.NineSliceSprite; - private endGameDialog: EndGameDialog; private dimGraphics: PIXI.Graphics = new PIXI.Graphics({ x: 0, y: 0, @@ -119,9 +119,6 @@ export class GameScene extends Scene { } 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.maxFPS = 60; this.ticker.minFPS = 30; @@ -168,10 +165,10 @@ export class GameScene extends Scene { if (this.MissionStats.getHP() <= 0) { this.isGameOver = true; - this.ShowScoreScreen(true); + this.ShowEndgameDialog(true); } else if (this.playerWon) { this.isGameOver = true; - this.ShowScoreScreen(false); + this.ShowEndgameDialog(false); } } public DarkenScreen() { @@ -247,10 +244,18 @@ export class GameScene extends Scene { this.setRoundMode(RoundMode.Purchase); } - private ShowScoreScreen(lost) { - const bounds = new PIXI.Rectangle(0, 0, Engine.app.canvas.width, Engine.app.canvas.height); - this.endGameDialog = new EndGameDialog(bounds, lost); - Engine.GameMaster.currentScene.stage.addChild(this.endGameDialog.container); + private async ShowEndgameDialog(lost) { + const endGameDialog = new EndGameDialog(lost); + await endGameDialog.show(); + 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) { @@ -266,14 +271,6 @@ export class GameScene extends Scene { } } - private onEndMissionDialogMainMenuClicked() { - this.ReturnToMain(); - } - - private onEndMissionDialogRetryMissionClicked() {} - - private onEndMissionDialogNextMissionClicked() {} - public destroy(): void { super.destroy(); this.isGameOver = true; @@ -282,8 +279,6 @@ export class GameScene extends Scene { } private ReturnToMain() { - this.destroy(); - Engine.GameMaster.currentScene.stage.removeChildren(); Engine.GameMaster.changeScene(new MissionPickerScene()); } }