diff --git a/src/classes/Assets.ts b/src/classes/Assets.ts index 802e1f2..383e0b5 100644 --- a/src/classes/Assets.ts +++ b/src/classes/Assets.ts @@ -81,6 +81,7 @@ export default class GameAssets { } private static async LoadMissions() { + // When adding missions, make sure to keep order. GameAssets.Missions = [await this.LoadMission('/assets/missions/mission_01.json')]; } diff --git a/src/classes/Bastion.ts b/src/classes/Bastion.ts index ec5590d..bfad197 100644 --- a/src/classes/Bastion.ts +++ b/src/classes/Bastion.ts @@ -12,9 +12,6 @@ import NotificationManager from './game/NotificationManager'; export class Engine { public static app: PIXI.Application; public static GameMaster: GameMaster; - public static WindowHeight: number; - public static WindowWidth: number; - public static AspectRatio: number = 16 / 9; public static Grid: Grid; public static WaveManager: WaveManager; public static TowerManager: TowerManager; @@ -22,11 +19,14 @@ export class Engine { public static NotificationManager: NotificationManager; public static GameScene: GameScene; public static latestCommit: string; + + public static GridCellSize: number = 64; + public static GridColumns: number = 25; + public static GridRows: number = 17; } export default class GameMaster { public currentScene: Scene; - private gameScene: GameScene; private GameObjects: GameObject[] = []; constructor() { @@ -35,12 +35,12 @@ export default class GameMaster { public _CreateGuiObject(object: GuiObject) { this.currentScene.gui.push(object); - Engine.app.stage.addChild(object.container); + Engine.GameMaster.currentScene.stage.addChild(object.container); } public _RemoveGuiObject(object: GuiObject) { this.currentScene.gui.splice(this.currentScene.gui.indexOf(object), 1); - Engine.app.stage.removeChild(object.container); + Engine.GameMaster.currentScene.stage.removeChild(object.container); } public changeScene(newScene: Scene) { diff --git a/src/classes/game/AnimationManager.ts b/src/classes/game/AnimationManager.ts index 416fbbb..3a4f861 100644 --- a/src/classes/game/AnimationManager.ts +++ b/src/classes/game/AnimationManager.ts @@ -44,7 +44,7 @@ export class FadeInOut extends Animateable { } else { this.pixiObject.alpha -= 1 / this.fadeTime; } - if (this.ticks >= this.fadeTime || this.pixiObject.alpha <= 0) this.Finish(); + if (this.ticks >= this.fadeTime) this.Finish(); } } @@ -67,14 +67,11 @@ export class Tween extends Animateable { public update(deltaMS) { super.update(deltaMS); this.ticks += deltaMS; - // Calculate the fraction of time elapsed - const progress = this.ticks / (this.tweenTime * 16.67); // Assuming 60 FPS, 1 frame = 16.67ms + const progress = this.ticks / (this.tweenTime * 16.67); - // Update the position based on the progress this.pixiObject.x = (this.goalX - this.pixiObject.x) * progress + this.pixiObject.x; this.pixiObject.y = (this.goalY - this.pixiObject.y) * progress + this.pixiObject.y; - // Finish the animation if the time is up if (this.ticks >= this.tweenTime * 16.67) { this.pixiObject.x = this.goalX; this.pixiObject.y = this.goalY; @@ -89,6 +86,9 @@ export class AnimationManager { this.AnimationQueue.push(animatable); } public update(ms) { + // Explanation: we go from the back of the array so that we can remove items freely without messing with + // foreach. From experience it gets a little screwy if you do this.AnimationQueue.forEach and doesn't properly + // remove elements. for (let i = this.AnimationQueue.length - 1; i >= 0; i--) { const anim = this.AnimationQueue[i]; if (anim.finished) { diff --git a/src/classes/game/Creep.ts b/src/classes/game/Creep.ts index 8657b0c..a7aa9d2 100644 --- a/src/classes/game/Creep.ts +++ b/src/classes/game/Creep.ts @@ -30,24 +30,26 @@ export default class Creep extends GameObject { constructor(creepType: CreepType, path: PathDefinition, id) { super(); this.creepType = creepType; + // Structured clone is used just in case, so that 1 creep doesnt alter stats for all creeps. this.stats = structuredClone(Assets.CreepStats[this.creepType]); this.sprite = new PIXI.Sprite({ texture: GameAssets.BasicCreepTexture, }); this.id = id; - // because wave manager spawns all instantly and i dont want - // it to look like a shit game (they all spawn in top left corner) - // i want to hide minion - mario + // Explanation: WaveManager spawns all creeps instantly, and since I don't want + // them to show up on the beginning while they are waiting, I put them outside the visible + // part of the currentScene.stage map. this.container.x = -70; this.container.y = -50; - this.sprite.width = 64; - this.sprite.height = 64; + this.sprite.width = Engine.GridCellSize; + this.sprite.height = Engine.GridCellSize; this.speed = this.stats.speed; this.health = this.stats.health; this.maxHealth = this.stats.health; this.path = path; - this.x = path[0][1] * 64 + 32; // centered - this.y = path[0][0] * 64 + 32; + // 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; Engine.GameScene.events.on(CreepEvents.TakenDamage, (creepID, damage) => { if (creepID != this.id) return; this.health -= damage; @@ -60,6 +62,8 @@ export default class Creep extends GameObject { if (this.health <= 0) { Engine.GameScene.events.emit(CreepEvents.Died, this.maxHealth, this); this.destroy(); + // The reason for setting this.dead instead of deleting self is because + // I need to allow WaveManager/Grid to manage their death and keep array up to date. this.dead = true; return; } @@ -72,8 +76,9 @@ export default class Creep extends GameObject { const currentCell = this.path[this.pathIndex]; const targetCell = this.path[this.pathIndex + 1]; - const targetX = targetCell[1] * 64 + 32; - const targetY = targetCell[0] * 64 + 32; + // Added + 32 for centering. + const targetX = targetCell[1] * Engine.GridCellSize + Engine.GridCellSize / 2; + const targetY = targetCell[0] * Engine.GridCellSize + Engine.GridCellSize / 2; const directionX = targetCell[1] - currentCell[1]; const directionY = targetCell[0] - currentCell[0]; let deltaX = this.speed * elapsedMS * directionX; @@ -114,7 +119,7 @@ export default class Creep extends GameObject { } } - public override destroy() { + public destroy() { super.destroy(); this.container.removeChildren(); } diff --git a/src/classes/game/Grid.ts b/src/classes/game/Grid.ts index 3c797ce..e33e55d 100644 --- a/src/classes/game/Grid.ts +++ b/src/classes/game/Grid.ts @@ -19,10 +19,10 @@ export class Cell extends GameObject { this.row = row; this.column = column; this.isPath = isPath; - this.bb.x = this.column * 64; - this.bb.y = this.row * 64; - this.bb.width = 64; - this.bb.height = 64; + this.bb.x = this.column * Engine.GridCellSize; + this.bb.y = this.row * Engine.GridCellSize; + this.bb.width = Engine.GridCellSize; + this.bb.height = Engine.GridCellSize; Engine.Grid.container.addChild(this.container); this.container.x = this.bb.x; this.container.y = this.bb.y; @@ -35,7 +35,7 @@ export class Cell extends GameObject { this.clickDetector.fill({ color: 0xff0000, alpha: 0 }); this.container.addChild(this.clickDetector); this.clickDetector.onpointerdown = (e) => { - Engine.Grid._gridCellClicked(row, column); + Engine.Grid.onGridCellClicked(row, column); }; if (!GameAssets.DebuggingEnabled) return; @@ -92,9 +92,9 @@ export class Grid extends GameObject { Engine.Grid = this; this.bb.x = 0; this.bb.y = 0; - this.bb.width = 64 * 30; - this.bb.height = 64 * 17; - Engine.app.stage.addChild(this.container); + this.bb.width = Engine.GridCellSize * Engine.GridColumns; + this.bb.height = Engine.GridCellSize * Engine.GridRows; + Engine.GameMaster.currentScene.stage.addChild(this.container); let background = new PIXI.Sprite(GameAssets.MissionBackgrounds[missionIndex]); background.x = 0; @@ -126,7 +126,6 @@ export class Grid extends GameObject { this.gridShown = !this.gridShown; } public addCreep(creep: Creep) { - console.log('ADD CREEP'); this.creeps.push(creep); creep.events.on(CreepEvents.Died, (diedCreep) => { this.onCreepDiedOrEscaped(diedCreep); @@ -150,9 +149,5 @@ export class Grid extends GameObject { public getCellByRowAndCol(row, column) { return this.cells.filter((item) => item.row == row && item.column == column)[0]; } - public _gridCellClicked(row, column) { - // function will be assigned by GameScene, but must be predefined here. - this.onGridCellClicked(row, column); - } public onGridCellClicked(row, column) {} } diff --git a/src/classes/game/MissionStats.ts b/src/classes/game/MissionStats.ts index 63fea9a..e8457d9 100644 --- a/src/classes/game/MissionStats.ts +++ b/src/classes/game/MissionStats.ts @@ -50,7 +50,7 @@ export default class MissionStats extends GameObject { this.gold = initialGold; this.container.x = 0; this.container.y = 20; - Engine.app.stage.addChild(this.container); + Engine.GameMaster.currentScene.stage.addChild(this.container); this.healthText = new PIXI.Text({ text: `${this.hp}`, style: new PIXI.TextStyle({ diff --git a/src/classes/game/NotificationManager.ts b/src/classes/game/NotificationManager.ts index 443208c..1aa5aeb 100644 --- a/src/classes/game/NotificationManager.ts +++ b/src/classes/game/NotificationManager.ts @@ -58,10 +58,8 @@ export default class NotificationManager extends GameObject { } public Notify(text, type: NotificationType) { let x = 0; - let y = this.notifications.length * 30; + let y = this.notifications.length * 32; this.notifications.push(new Notification(text, type, x, y, this.ticks + 180)); - console.log('CREATED NOTIFICATION '); - console.log(text, type, x, y, this.ticks + 180); } public update(_) { this.ticks++; diff --git a/src/classes/game/Projectile.ts b/src/classes/game/Projectile.ts index b080825..d36bb38 100644 --- a/src/classes/game/Projectile.ts +++ b/src/classes/game/Projectile.ts @@ -20,7 +20,6 @@ export default class Projectile extends GameObject { public timeToLive: number = 1; constructor(x, y, spriteTexture, angle, damage) { super(); - console.log('I SHOOTTED!'); this.x = x; this.y = y; this.damage = damage; @@ -30,7 +29,7 @@ export default class Projectile extends GameObject { this.container.x = this.x; this.container.y = this.y; this.container.addChild(this.sprite); - Engine.app.stage.addChild(this.container); + Engine.GameMaster.currentScene.stage.addChild(this.container); this.angle = angle; this.speed = 0.9; @@ -59,7 +58,6 @@ export default class Projectile extends GameObject { } public onCollide(creep) { - console.log('COLLIDED WITH' + creep); Engine.GameScene.events.emit(CreepEvents.TakenDamage, creep.id, this.damage); } @@ -68,12 +66,5 @@ export default class Projectile extends GameObject { let mybb = this.copyContainerToBB(); let otherbb = creep.copyContainerToBB(); return mybb.getBounds().intersects(otherbb.getBounds()); - // console.log(boundsA, boundsB); - // return ( - // boundsA.x < boundsB.x + boundsB.width && - // boundsA.x + boundsA.width > boundsB.x && - // boundsA.y < boundsB.y + boundsB.height && - // boundsA.y + boundsA.height > boundsB.y - // ); } } diff --git a/src/classes/game/Tower.ts b/src/classes/game/Tower.ts index 7cf77d2..1622116 100644 --- a/src/classes/game/Tower.ts +++ b/src/classes/game/Tower.ts @@ -50,36 +50,40 @@ export class Tower extends GameObject { let parent: Cell = Engine.Grid.getCellByRowAndCol(row, column); this.sprite = new PIXI.Sprite({ texture: texture, - height: 64, - width: 64, + height: Engine.GridCellSize, + width: Engine.GridCellSize, zIndex: 10, }); this.container.addChild(this.sprite); parent.container.addChild(this.container); parent.clickDetector.onmouseenter = (e) => { - this.graphics.circle(this.column * 64 + 32, this.row * 64 + 32, this.definition.stats.range * 64); + this.graphics.circle( + this.column * Engine.GridCellSize + Engine.GridCellSize / 2, + this.row * Engine.GridCellSize + Engine.GridCellSize / 2, + this.definition.stats.range * Engine.GridCellSize + ); this.graphics.fill({ color: 0xff0000, alpha: 0.5 }); }; parent.clickDetector.onmouseleave = (e) => { this.graphics.clear(); }; - Engine.app.stage.addChild(this.graphics); + Engine.GameMaster.currentScene.stage.addChild(this.graphics); } public GetCreepsInRange() { let creeps = Engine.Grid.creeps; return creeps.filter((creep) => { const x = creep.x; const y = creep.y; - const towerX = this.column * 64 + 32; - const towerY = this.row * 64 + 32; - const radius = this.definition.stats.range * 64; + const towerX = this.column * Engine.GridCellSize + Engine.GridCellSize / 2; + const towerY = this.row * Engine.GridCellSize + Engine.GridCellSize / 2; + const radius = this.definition.stats.range * Engine.GridCellSize; const d = distance(towerX, towerY, x, y); return d < radius; }); } public Shoot(creep: Creep) { - let x = this.column * 64 + 32; - let y = this.row * 64 + 32; + let x = this.column * Engine.GridCellSize + Engine.GridCellSize / 2; + let y = this.row * Engine.GridCellSize + Engine.GridCellSize / 2; let angle = calculateAngleToPoint(x, y, creep.x, creep.y); this.projectiles.push( new Projectile(x, y, GameAssets.BasicProjectileTexture, angle, this.definition.stats.damage) diff --git a/src/classes/gui/GemTab.ts b/src/classes/gui/GemTab.ts index 7673687..4084551 100644 --- a/src/classes/gui/GemTab.ts +++ b/src/classes/gui/GemTab.ts @@ -18,9 +18,6 @@ export default class GemTab extends GuiObject { rightWidth: 1000, bottomHeight: 1000, }); - // this.towerTabSprite = new PIXI.Sprite({ - // texture: GameAssets.FrameBackground, - // }); this.gemTabSprite.x = 0; this.gemTabSprite.y = 0; this.gemTabSprite.width = this.bounds.width; diff --git a/src/main.ts b/src/main.ts index 5dbcd1a..ddd3276 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,7 +10,6 @@ import NotificationManager from './classes/game/NotificationManager'; (async () => { const app = new PIXI.Application(); Engine.app = app; - log('main - init()'); await app.init({ width: 1920, // Base width height: 1080, // Base height @@ -19,7 +18,6 @@ import NotificationManager from './classes/game/NotificationManager'; backgroundColor: 0xffffff, sharedTicker: true, }); - log('main - init() complete'); document.body.appendChild(app.canvas); diff --git a/src/scenes/Game.ts b/src/scenes/Game.ts index 066078c..14d2702 100644 --- a/src/scenes/Game.ts +++ b/src/scenes/Game.ts @@ -10,7 +10,6 @@ import Scene from './Scene'; import * as PIXI from 'pixi.js'; import MissionStats from '../classes/game/MissionStats'; import TowerManager from '../classes/game/TowerManager'; -import NotificationManager from '../classes/game/NotificationManager'; import { MissionPickerScene } from './MissionPicker'; enum RoundMode { @@ -30,6 +29,7 @@ export class GameScene extends Scene { private currentRound: number = 0; private isWaveManagerFinished: boolean = false; private playerWon: boolean = false; + private destroyTicker: boolean = false; constructor(name: string) { super(); @@ -45,9 +45,11 @@ export class GameScene extends Scene { this.ticker = new PIXI.Ticker(); this.ticker.maxFPS = 60; this.ticker.minFPS = 30; - this.ticker.add(() => this.update(this.ticker.elapsedMS)); // bruh + this.ticker.add(() => { + if (this.update) this.update(this.ticker.elapsedMS); + }); this.ticker.start(); - const SidebarRect = new PIXI.Rectangle(64 * 30 - 360, 0, 360, Engine.app.canvas.height); + const SidebarRect = new PIXI.Rectangle(Engine.GridCellSize * 30 - 360, 0, 360, Engine.app.canvas.height); const changeRoundButtonRect = new PIXI.Rectangle(50, Engine.app.canvas.height - 100, 310, 100); new Grid(this.mission.gameMap, this.missionIndex); new TowerManager(); @@ -85,6 +87,13 @@ export class GameScene extends Scene { this.MissionStats = new MissionStats(100, 200); } public update(elapsedMS) { + if (this.isGameOver) { + if (this.destroyTicker) { + this.destroyTicker = false; + this.ticker.destroy(); + } + return; + } Engine.WaveManager.update(elapsedMS); Engine.Grid.update(elapsedMS); Engine.TowerManager.update(elapsedMS); @@ -110,16 +119,13 @@ export class GameScene extends Scene { if (this.MissionStats.getHP() <= 0) { this.isGameOver = true; this.ShowScoreScreen(true); - this.ticker.stop(); } else if (this.playerWon) { this.isGameOver = true; this.ShowScoreScreen(false); - this.ticker.stop(); } } private ShowScoreScreen(lost) { - this.ticker.stop(); // TODO: show to player for real if (lost) { console.log('LOSE!'); @@ -141,9 +147,16 @@ export class GameScene extends Scene { } } + public destroy(): void { + super.destroy(); + this.isGameOver = true; + this.destroyTicker = true; + Engine.GameScene = null; + } + private ReturnToMain() { this.destroy(); - Engine.app.stage.removeChildren(); + Engine.GameMaster.currentScene.stage.removeChildren(); Engine.GameMaster.changeScene(new MissionPickerScene()); } public onTowerPlaced() {} diff --git a/src/scenes/Main.ts b/src/scenes/Main.ts index b9d142c..d1d6269 100644 --- a/src/scenes/Main.ts +++ b/src/scenes/Main.ts @@ -39,7 +39,7 @@ export class MainScene extends Scene { }, }); text.x = text.x - text.width / 5; - Engine.app.stage.addChild(text); + Engine.GameMaster.currentScene.stage.addChild(text); let text2 = new PIXI.Text({ x: 0, y: 0, @@ -50,17 +50,17 @@ export class MainScene extends Scene { fontWeight: 'bold', }, }); - Engine.app.stage.addChild(text2); + Engine.GameMaster.currentScene.stage.addChild(text2); const button01 = new Button(NewGameButton.rect, NewGameButton.caption, NewGameButton.texture, true); button01.onClick = (e) => { - Engine.app.stage.removeChild(text); - Engine.app.stage.removeChild(text2); + Engine.GameMaster.currentScene.stage.removeChild(text); + Engine.GameMaster.currentScene.stage.removeChild(text2); Engine.GameMaster.changeScene(new MissionPickerScene()); }; let b2 = new Button(SettingsButton.rect, SettingsButton.caption, SettingsButton.texture, true); b2.onClick = (e) => { - alert('Does nothing for now, just placeholder.'); + Engine.NotificationManager.Notify('Not finished.', 'info'); }; } } diff --git a/src/scenes/Scene.ts b/src/scenes/Scene.ts index 0aa3888..0e2d9d6 100644 --- a/src/scenes/Scene.ts +++ b/src/scenes/Scene.ts @@ -1,21 +1,21 @@ +import { Engine } from '../classes/Bastion'; import GuiObject from '../classes/GuiObject'; import * as PIXI from 'pixi.js'; export default class Scene { + public stage: PIXI.Container = new PIXI.Container(); public gui: GuiObject[] = []; private _events: PIXI.EventEmitter = new PIXI.EventEmitter(); + constructor() { + Engine.app.stage.addChild(this.stage); + } public destroy() { + this.stage.destroy(); this.gui.forEach((element) => { element.destroy(); }); } - public GetGuiObject(object: GuiObject) { - return this.gui.find((obj) => obj == object); - } - public GetGuiObjectByName(name: string) { - return this.gui.filter((obj) => obj.name == name); - } public get events(): PIXI.EventEmitter { return this._events;