diff --git a/src/classes/gui/EndGameDialog.ts b/src/classes/gui/EndGameDialog.ts index 43e67c5..30c419a 100644 --- a/src/classes/gui/EndGameDialog.ts +++ b/src/classes/gui/EndGameDialog.ts @@ -63,7 +63,7 @@ export default class EndGameDialog extends GuiObject { }); } - private setupButtons(lost: boolean) { + private showButtons(lost: boolean) { const buttonContainer = new PIXI.Container(); const buttonWidth = 200; const buttonHeight = 80; diff --git a/src/classes/gui/MessageBox.ts b/src/classes/gui/MessageBox.ts index 3583674..9bfd95f 100644 --- a/src/classes/gui/MessageBox.ts +++ b/src/classes/gui/MessageBox.ts @@ -1,127 +1,24 @@ import * as PIXI from 'pixi.js'; +import ModalDialogBase from './ModalDialog'; import GuiObject from '../GuiObject'; -import Assets from '../Assets'; -import { Engine } from '../Bastion'; -import GameAssets from '../Assets'; -import Button, { ButtonTexture } from './Button'; -import KeyboardManager from '../game/KeyboardManager'; -export default class MessageBox extends GuiObject { - private overlay: PIXI.Graphics; - private buttonPadding = 10; - private buttons: Button[] = []; - private escapeKeyIndex: number; - private keyboardManagerUnsubscribe: () => void; +export default class MessageBox extends ModalDialogBase { + private caption: string; constructor(caption: string, buttons: string[], escapeKeyIndex: number = buttons.length - 1) { - super(); - console.log(`MessageBox(caption: ${caption}, buttons: ${buttons})`); - this.escapeKeyIndex = escapeKeyIndex; - // Show overlay to prevent user from interacting with the game - this.overlay = new PIXI.Graphics(); - this.overlay.rect(0, 0, Engine.app.canvas.width, Engine.app.canvas.height); - this.overlay.fill({ color: 0x000000, alpha: 0.5 }); - // Prevent interaction with the underlying scene - this.overlay.interactive = true; - this.container.addChild(this.overlay); - - const buttonDefs = buttons.map((btn) => ({ - caption: btn, - width: btn.length * 10 + 40, - height: 60, - click: () => this.buttonClicked(btn), - })); - let buttonTotalWidth = 0; - for (const buttonDef of buttonDefs) { - if (buttonTotalWidth > 0) buttonTotalWidth += this.buttonPadding; - buttonTotalWidth += buttonDef.width; - } - const captionWidth = caption.length * 10 + 100; - let width = Math.max(buttonTotalWidth, captionWidth); - - const height = 150; - const inputContainerBounds = new PIXI.Rectangle( - Engine.app.canvas.width / 2 - width / 2, - Engine.app.canvas.height / 2 - height / 2, - width, - height - ); - - const inputContainer = new PIXI.Container(); - inputContainer.x = inputContainerBounds.x; - inputContainer.y = inputContainerBounds.y; - inputContainer.width = inputContainerBounds.width; - inputContainer.height = inputContainerBounds.height; - - const background = new PIXI.NineSliceSprite({ - texture: GameAssets.Frame04Texture, - leftWidth: 200, - topHeight: 200, - rightWidth: 200, - bottomHeight: 200, - }); - background.x = 0; - background.y = 0; - background.width = inputContainerBounds.width; - background.height = inputContainerBounds.height; - inputContainer.addChild(background); + super(buttons, escapeKeyIndex); + this.caption = caption; + } + protected override createContent(): PIXI.Container | GuiObject { const text = new PIXI.Text({ - text: caption, + text: this.caption, style: new PIXI.TextStyle({ fill: 0xffffff, fontSize: 24, }), }); - text.anchor.set(0.5, 0.5); - text.x = inputContainerBounds.width / 2; - text.y = 40; - inputContainer.addChild(text); - - let buttonXPos = inputContainerBounds.width / 2 - buttonTotalWidth / 2; - for (const buttonDef of buttonDefs) { - const button = new Button( - new PIXI.Rectangle( - buttonXPos, - inputContainerBounds.height - buttonDef.height - 20, - buttonDef.width, - buttonDef.height - ), - buttonDef.caption, - ButtonTexture.Button01 - ); - button.onClick = buttonDef.click; - this.buttons.push(button); - inputContainer.addChild(button.container); - buttonXPos += buttonDef.width + this.buttonPadding; - } - this.container.addChild(inputContainer); - this.keyboardManagerUnsubscribe = KeyboardManager.onKey('Escape', this.onKeyPress.bind(this)); - } - - /** - * Event that is triggered when the button is clicked. - */ - public onButtonClicked: (button: string) => void; - - override destroy(): void { - this.keyboardManagerUnsubscribe(); - super.destroy(); - } - - private buttonClicked(button: string) { - if (this.onButtonClicked) this.onButtonClicked(button); - this.destroy(); - } - - private onKeyPress(event: KeyboardEvent) { - // Message box is modal, so we can safely prevent the default behavior - event.preventDefault(); - if (event.key === 'Escape') { - this.onButtonClicked(this.buttons[this.escapeKeyIndex].getCaption()); - } else if (event.key === 'Enter') { - this.onButtonClicked(this.buttons[0].getCaption()); - } + return text; } /** @@ -131,13 +28,7 @@ export default class MessageBox extends GuiObject { * @returns A promise that resolves with the button that was clicked. */ public static show(caption: string, buttons: string[], escapeKeyButtonIndex: number = 0): Promise { - return new Promise((resolve, reject) => { - const messageBox = new MessageBox(caption, buttons); - Engine.app.stage.addChild(messageBox.container); - messageBox.onButtonClicked = (button) => { - messageBox.destroy(); - resolve(button); - }; - }); + const messageBox = new MessageBox(caption, buttons); + return messageBox.show(); } } diff --git a/src/classes/gui/ModalDialog.ts b/src/classes/gui/ModalDialog.ts new file mode 100644 index 0000000..a63480a --- /dev/null +++ b/src/classes/gui/ModalDialog.ts @@ -0,0 +1,158 @@ +import * as PIXI from 'pixi.js'; +import GuiObject from '../GuiObject'; +import Assets from '../Assets'; +import { Engine } from '../Bastion'; +import GameAssets from '../Assets'; +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; + + constructor(buttonCaptions: string[], escapeKeyIndex: number = buttonCaptions.length - 1) { + super(); + this.escapeKeyIndex = escapeKeyIndex; + this.buttonCaptions = buttonCaptions; + this.keyboardManagerUnsubscribe = KeyboardManager.onKey('Escape', this.onKeyPress.bind(this)); + } + + protected generate() { + if (this.generated) return; + this.generated = true; + // Show overlay to prevent user from interacting with the game + this.overlay = new PIXI.Graphics(); + this.overlay.rect(0, 0, Engine.app.canvas.width, Engine.app.canvas.height); + this.overlay.fill({ color: 0x000000, alpha: 0.5 }); + // Prevent interaction with the underlying scene + 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; + } + + const buttonDefs = this.buttonCaptions.map((btnCaption) => ({ + caption: btnCaption, + width: btnCaption.length * 16 + 40, + height: this.buttonHeight, + click: () => this.buttonClickHandler(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 modalBounds = new PIXI.Rectangle( + Engine.app.canvas.width / 2 - width / 2, + Engine.app.canvas.height / 2 - height / 2, + width, + height + ); + + const modalContainer = new PIXI.Container(); + modalContainer.x = modalBounds.x; + modalContainer.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); + + 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); + + let buttonXPos = modalBounds.width / 2 - buttonTotalWidth / 2; + for (const buttonDef of buttonDefs) { + const button = new Button( + new PIXI.Rectangle( + buttonXPos, + modalBounds.height - this.buttonAreaHeight - this.dialogPadding, + buttonDef.width, + buttonDef.height + ), + buttonDef.caption, + ButtonTexture.Button01 + ); + button.onClick = buttonDef.click; + this.buttons.push(button); + modalContainer.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); + }; + }); + } + + /** + * Event that is triggered when the button is clicked. + */ + public onButtonClicked: (button: string) => void; + + 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()); + } + } +} diff --git a/src/classes/gui/PlayerNameInput.ts b/src/classes/gui/PlayerNameInput.ts new file mode 100644 index 0000000..59c1a9d --- /dev/null +++ b/src/classes/gui/PlayerNameInput.ts @@ -0,0 +1,45 @@ +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 80595d4..9bcacc3 100644 --- a/src/classes/gui/TextInput.ts +++ b/src/classes/gui/TextInput.ts @@ -9,6 +9,10 @@ export default class TextInput extends GuiObject { private text: PIXI.Text; private maxLength: number; + public getText(): string { + return this.text.text; + } + constructor(bounds: PIXI.Rectangle, maxLength: number) { super(); this.bounds = bounds;