Working on end-game menu

This commit is contained in:
Dalibor Čarapić 2025-02-05 22:33:32 +01:00
parent 13f4c4610e
commit f6879046fa
5 changed files with 219 additions and 121 deletions

View File

@ -63,7 +63,7 @@ export default class EndGameDialog extends GuiObject {
}); });
} }
private setupButtons(lost: boolean) { private showButtons(lost: boolean) {
const buttonContainer = new PIXI.Container(); const buttonContainer = new PIXI.Container();
const buttonWidth = 200; const buttonWidth = 200;
const buttonHeight = 80; const buttonHeight = 80;

View File

@ -1,127 +1,24 @@
import * as PIXI from 'pixi.js'; import * as PIXI from 'pixi.js';
import ModalDialogBase from './ModalDialog';
import GuiObject from '../GuiObject'; 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 { export default class MessageBox extends ModalDialogBase {
private overlay: PIXI.Graphics; private caption: string;
private buttonPadding = 10;
private buttons: Button[] = [];
private escapeKeyIndex: number;
private keyboardManagerUnsubscribe: () => void;
constructor(caption: string, buttons: string[], escapeKeyIndex: number = buttons.length - 1) { constructor(caption: string, buttons: string[], escapeKeyIndex: number = buttons.length - 1) {
super(); super(buttons, escapeKeyIndex);
console.log(`MessageBox(caption: ${caption}, buttons: ${buttons})`); this.caption = caption;
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);
protected override createContent(): PIXI.Container | GuiObject {
const text = new PIXI.Text({ const text = new PIXI.Text({
text: caption, text: this.caption,
style: new PIXI.TextStyle({ style: new PIXI.TextStyle({
fill: 0xffffff, fill: 0xffffff,
fontSize: 24, fontSize: 24,
}), }),
}); });
text.anchor.set(0.5, 0.5); return text;
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());
}
} }
/** /**
@ -131,13 +28,7 @@ export default class MessageBox extends GuiObject {
* @returns A promise that resolves with the button that was clicked. * @returns A promise that resolves with the button that was clicked.
*/ */
public static show(caption: string, buttons: string[], escapeKeyButtonIndex: number = 0): Promise<string> { public static show(caption: string, buttons: string[], escapeKeyButtonIndex: number = 0): Promise<string> {
return new Promise((resolve, reject) => {
const messageBox = new MessageBox(caption, buttons); const messageBox = new MessageBox(caption, buttons);
Engine.app.stage.addChild(messageBox.container); return messageBox.show();
messageBox.onButtonClicked = (button) => {
messageBox.destroy();
resolve(button);
};
});
} }
} }

View File

@ -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<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.
*/
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());
}
}
}

View File

@ -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);
}
}
}

View File

@ -9,6 +9,10 @@ export default class TextInput extends GuiObject {
private text: PIXI.Text; private text: PIXI.Text;
private maxLength: number; private maxLength: number;
public getText(): string {
return this.text.text;
}
constructor(bounds: PIXI.Rectangle, maxLength: number) { constructor(bounds: PIXI.Rectangle, maxLength: number) {
super(); super();
this.bounds = bounds; this.bounds = bounds;