diff --git a/public/Tileset.tsx b/public/Tileset.tsx
index 7f536c1..f5b0984 100644
--- a/public/Tileset.tsx
+++ b/public/Tileset.tsx
@@ -1,4 +1,4 @@
-
+
diff --git a/src/classes/Assets.ts b/src/classes/Assets.ts
index ee2edbe..ea3f2c5 100644
--- a/src/classes/Assets.ts
+++ b/src/classes/Assets.ts
@@ -24,6 +24,7 @@ export default class GameAssets {
public static SwordsTexture: PIXI.Texture;
public static TitleTexture: PIXI.Texture;
public static BannerGemsmith: PIXI.Texture;
+ public static EndScreenDialog: PIXI.Texture;
public static PlayIconTexture: PIXI.Texture;
public static PauseIconTexture: PIXI.Texture;
@@ -96,6 +97,7 @@ export default class GameAssets {
this.Load('./assets/gui/frame_green.png').then((texture) => (this.GreenBackground = texture)),
this.Load('./assets/gui/frame_blue.png').then((texture) => (this.BlueBackground = texture)),
this.Load('./assets/gui/banner_01.png').then((texture) => (this.BannerGemsmith = texture)),
+ this.Load('./assets/gui/note.png').then((texture) => (this.EndScreenDialog = texture)),
this.Load('./assets/gui/heart.png').then((texture) => (this.HealthTexture = texture)),
this.Load('./assets/gui/money.png').then((texture) => (this.GoldTexture = texture)),
this.Load('./assets/gui/wave.png').then((texture) => (this.WaveTexture = texture)),
diff --git a/src/classes/Events.ts b/src/classes/Events.ts
index 67be16b..581ff8e 100644
--- a/src/classes/Events.ts
+++ b/src/classes/Events.ts
@@ -28,3 +28,9 @@ 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 a875166..f78bf33 100644
--- a/src/classes/GameUIConstants.ts
+++ b/src/classes/GameUIConstants.ts
@@ -1,8 +1,10 @@
import * as PIXI from 'pixi.js';
import { Engine } from './Bastion';
export default class GameUIConstants {
- public static SidebarRect;
- public static ChangeRoundButtonRect;
+ public static SidebarRect: PIXI.Rectangle;
+ public static ChangeRoundButtonRect: PIXI.Rectangle;
+ public static EndGameDialogRect: PIXI.Rectangle;
+
public static init() {
GameUIConstants.SidebarRect = new PIXI.Rectangle(
Engine.app.canvas.width - 360,
@@ -11,5 +13,13 @@ 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
+ );
}
}
diff --git a/src/classes/game/Creep.ts b/src/classes/game/Creep.ts
index af4f4e1..37e75a0 100644
--- a/src/classes/game/Creep.ts
+++ b/src/classes/game/Creep.ts
@@ -51,6 +51,7 @@ export default class Creep extends GameObject {
// Added + 32 to center them.
this.x = path[0][1] * Engine.GridCellSize + Engine.GridCellSize / 2;
this.y = path[0][0] * Engine.GridCellSize + Engine.GridCellSize / 2;
+ // TODO: Unsubscribe from events once the scene is destroyed
Engine.GameScene.events.on(CreepEvents.TakenDamage, (creepID, damage) => {
if (creepID != this.id) return;
this.health -= damage;
diff --git a/src/classes/game/KeyboardManager.ts b/src/classes/game/KeyboardManager.ts
new file mode 100644
index 0000000..37ab5db
--- /dev/null
+++ b/src/classes/game/KeyboardManager.ts
@@ -0,0 +1,52 @@
+/**
+ * Handles keyboard events.
+ */
+class KeyboardManager {
+ private static listeners: Map void)[]> = new Map();
+
+ public static init() {
+ window.addEventListener('keydown', KeyboardManager.handleKeyDown);
+ }
+
+ /**
+ * Add a callback to be called when the specified 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);
+ }
+
+ /**
+ * Remove a callback from the specified key.
+ */
+ 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);
+ }
+ }
+ }
+
+ private static handleKeyDown(event: KeyboardEvent) {
+ if (KeyboardManager.listeners.has(event.key)) {
+ 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);
+ if (event.defaultPrevented) {
+ break;
+ }
+ }
+ }
+ }
+}
+
+export default KeyboardManager;
diff --git a/src/classes/game/TowerManager.ts b/src/classes/game/TowerManager.ts
index d63d0e6..1e425d9 100644
--- a/src/classes/game/TowerManager.ts
+++ b/src/classes/game/TowerManager.ts
@@ -23,6 +23,7 @@ export default class TowerManager {
});
private towers: Tower[] = [];
constructor() {
+ // TODO: Unsubscribe from events once the scene is destroyed
Engine.TowerManager = this;
Engine.GameScene.events.on(GridEvents.CellMouseOver, (cell: Cell) => {
if (this.isPlacingTower) {
diff --git a/src/classes/gui/Button.ts b/src/classes/gui/Button.ts
index 05fb20a..fcf6dd5 100644
--- a/src/classes/gui/Button.ts
+++ b/src/classes/gui/Button.ts
@@ -22,6 +22,10 @@ export default class Button extends GuiObject {
this.buttonText.text = caption;
}
+ getCaption(): string {
+ return this.caption;
+ }
+
constructor(bounds: PIXI.Rectangle, caption: string, buttonTexture: ButtonTexture, enabled: boolean = true) {
super(true);
if (buttonTexture == ButtonTexture.Button01) this.buttonTexture = Assets.Button01Texture;
diff --git a/src/classes/gui/EndGameDialog.ts b/src/classes/gui/EndGameDialog.ts
new file mode 100644
index 0000000..43e67c5
--- /dev/null
+++ b/src/classes/gui/EndGameDialog.ts
@@ -0,0 +1,119 @@
+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 MessageBox from './MessageBox';
+import KeyboardManager from '../game/KeyboardManager';
+
+export default class EndGameDialog extends GuiObject {
+ private dialogSprite: PIXI.NineSliceSprite;
+ private dialogCaption: PIXI.Text;
+ private nextMissionButton: Button;
+ private retryButton: Button;
+ private mainMenuButton: Button;
+ private overlay: PIXI.Graphics;
+ private lost: boolean;
+ private keyboardManagerUnsubscribe: () => void;
+
+ constructor(bounds: PIXI.Rectangle, lost: boolean) {
+ super();
+ 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({
+ 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!',
+ style: new PIXI.TextStyle({
+ fill: 0xffffff,
+ fontSize: 36,
+ }),
+ });
+ 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 setupButtons(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
+ );
+ 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;
+
+ 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);
+ }
+ }
+}
diff --git a/src/classes/gui/MessageBox.ts b/src/classes/gui/MessageBox.ts
new file mode 100644
index 0000000..3583674
--- /dev/null
+++ b/src/classes/gui/MessageBox.ts
@@ -0,0 +1,143 @@
+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 class MessageBox extends GuiObject {
+ private overlay: PIXI.Graphics;
+ private buttonPadding = 10;
+ private buttons: Button[] = [];
+ private escapeKeyIndex: number;
+ private keyboardManagerUnsubscribe: () => void;
+
+ 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);
+
+ const text = new PIXI.Text({
+ text: 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());
+ }
+ }
+
+ /**
+ * Shows a message box with the specified caption and buttons.
+ * @param caption The caption of the message box.
+ * @param buttons The buttons to show.
+ * @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);
+ };
+ });
+ }
+}
diff --git a/src/classes/gui/TextInput.ts b/src/classes/gui/TextInput.ts
new file mode 100644
index 0000000..80595d4
--- /dev/null
+++ b/src/classes/gui/TextInput.ts
@@ -0,0 +1,53 @@
+import * as PIXI from 'pixi.js';
+import GuiObject from '../GuiObject';
+import Assets from '../Assets';
+import GameAssets from '../Assets';
+
+export default class TextInput extends GuiObject {
+ private bounds: PIXI.Rectangle;
+ private backgroundSprite: PIXI.NineSliceSprite;
+ private text: PIXI.Text;
+ private maxLength: number;
+
+ constructor(bounds: PIXI.Rectangle, 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,
+ topHeight: 20,
+ rightWidth: 20,
+ bottomHeight: 20,
+ });
+ this.backgroundSprite.x = 0;
+ this.backgroundSprite.y = 0;
+ this.backgroundSprite.width = this.bounds.width;
+ this.backgroundSprite.height = this.bounds.height;
+ 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,
+ }),
+ });
+ this.text.x = 10;
+ this.text.y = 10;
+ this.container.addChild(this.text);
+ }
+
+ private onKeyPress(event: KeyboardEvent) {
+ if (event.key == 'Backspace') {
+ this.text.text = this.text.text.slice(0, -1);
+ } else if (event.key.length == 1 && this.text.text.length < this.maxLength) {
+ this.text.text += event.key;
+ }
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index 39659c1..7b6a584 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -6,6 +6,8 @@ import { GameScene } from './scenes/Game';
import { AnimationManager } from './classes/game/AnimationManager';
import NotificationManager from './classes/game/NotificationManager';
import GameUIConstants from './classes/GameUIConstants';
+import MessageBox from './classes/gui/MessageBox';
+import KeyboardManager from './classes/game/KeyboardManager';
(async () => {
const app = new PIXI.Application();
@@ -47,6 +49,7 @@ import GameUIConstants from './classes/GameUIConstants';
resize();
await Assets.LoadAssets();
GameUIConstants.init();
+ KeyboardManager.init();
new GameMaster();
Engine.AnimationManager = new AnimationManager();
Engine.NotificationManager = new NotificationManager();
diff --git a/src/scenes/Game.ts b/src/scenes/Game.ts
index fb01b7c..2655e6d 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 } from '../classes/Events';
+import { WaveManagerEvents, CreepEvents, GemEvents, EndMissionDialogEvents } from '../classes/Events';
import Sidebar from '../classes/gui/Sidebar';
import Button, { ButtonTexture } from '../classes/gui/Button';
import Scene from './Scene';
@@ -16,6 +16,7 @@ import GameUIConstants from '../classes/GameUIConstants';
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';
enum RoundMode {
Purchase = 0,
@@ -40,6 +41,7 @@ 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,
@@ -117,6 +119,9 @@ 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;
@@ -130,6 +135,7 @@ export class GameScene extends Scene {
});
this.ticker.start();
}
+
public update(elapsedMS) {
if (this.isGameOver) {
if (this.destroyTicker) {
@@ -242,12 +248,9 @@ export class GameScene extends Scene {
}
private ShowScoreScreen(lost) {
- // TODO: show to player for real (see how this.OfferPlayerGems() does it)
- if (lost) {
- console.log('LOSE!');
- } else {
- console.log('WIN!');
- }
+ 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);
}
public onCreepEscaped(creep: Creep) {
@@ -263,6 +266,14 @@ export class GameScene extends Scene {
}
}
+ private onEndMissionDialogMainMenuClicked() {
+ this.ReturnToMain();
+ }
+
+ private onEndMissionDialogRetryMissionClicked() {}
+
+ private onEndMissionDialogNextMissionClicked() {}
+
public destroy(): void {
super.destroy();
this.isGameOver = true;