diff --git a/.prettierrc b/.prettierrc index f914884..141d99b 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,5 +2,6 @@ "trailingComma": "es5", "tabWidth": 4, "semi": true, - "singleQuote": true + "singleQuote": true, + "printWidth": 120 } diff --git a/public/assets/creeps/basic.jpg b/public/assets/creeps/basic.jpg new file mode 100644 index 0000000..c803937 Binary files /dev/null and b/public/assets/creeps/basic.jpg differ diff --git a/public/assets/missions/mission_01.json b/public/assets/missions/mission_01.json index ab69c4d..2b57f8a 100644 --- a/public/assets/missions/mission_01.json +++ b/public/assets/missions/mission_01.json @@ -49,18 +49,18 @@ { "waves": [ { - "firstCreepSpawnTick": 120, - "spawnIntervalTicks": 60, + "firstCreepSpawnTick": 2000, + "spawnIntervalTicks": 1000, "creeps": [0, 0, 0, 0, 0, 1, 1, 1, 0] }, { - "firstCreepSpawnTick": 480, - "spawnIntervalTicks": 60, + "firstCreepSpawnTick": 2000, + "spawnIntervalTicks": 1000, "creeps": [0, 0, 0, 0, 0, 1, 1, 1, 0] }, { - "firstCreepSpawnTick": 480, - "spawnIntervalTicks": 60, + "firstCreepSpawnTick": 2000, + "spawnIntervalTicks": 1000, "creeps": [0, 0, 0, 0, 0, 1, 1, 1, 0] } ], diff --git a/src/base/Assets.ts b/src/base/Assets.ts index 50dee96..51bd5eb 100644 --- a/src/base/Assets.ts +++ b/src/base/Assets.ts @@ -7,14 +7,15 @@ export default class Assets { Assets.ButtonTexture = await PIXI.Assets.load({ src: '/assets/gui/button_02.png', }); + Assets.BasicCreepTexture = await PIXI.Assets.load({ + src: '/assets/creeps/basic.jpg', + }); console.log('Loading Missions'); await this.LoadMissions(); } private static async LoadMissions() { - Assets.Missions = [ - await this.LoadMission('/assets/missions/mission_01.json'), - ]; + Assets.Missions = [await this.LoadMission('/assets/missions/mission_01.json')]; } private static async LoadMission(missionUrl: string) { @@ -24,6 +25,7 @@ export default class Assets { } public static ButtonTexture: PIXI.Texture; + public static BasicCreepTexture: PIXI.Texture; public static Missions: MissionDefinition[]; } diff --git a/src/base/GameObject.ts b/src/base/GameObject.ts index 6c3c9b3..5647012 100644 --- a/src/base/GameObject.ts +++ b/src/base/GameObject.ts @@ -8,12 +8,7 @@ export default abstract class GameObject { public setBounds(bounds: PIXI.Rectangle): void; public setBounds(x: number, y: number, width: number, height: number): void; - public setBounds( - boundsOrX: PIXI.Rectangle | number, - y?: number, - width?: number, - height?: number - ) { + public setBounds(boundsOrX: PIXI.Rectangle | number, y?: number, width?: number, height?: number) { if (boundsOrX instanceof PIXI.Rectangle) { this.bounds = boundsOrX; } else { @@ -24,8 +19,7 @@ export default abstract class GameObject { public destroy() { this._events.removeAllListeners(); - if (this._container.parent) - this._container.parent.removeChild(this._container); + if (this._container.parent) this._container.parent.removeChild(this._container); this._container.destroy(); } @@ -37,6 +31,10 @@ export default abstract class GameObject { return this._events; } + public getBounds(): PIXI.Rectangle { + return this.bounds; + } + protected triggerBoundsChanged() { this.draw(); } diff --git a/src/components/Creep.ts b/src/components/Creep.ts index c4fb37e..b833c92 100644 --- a/src/components/Creep.ts +++ b/src/components/Creep.ts @@ -1,3 +1,4 @@ +import Assets from '../base/Assets'; import { CreepType, PathDefinition } from '../base/Definitions'; import GameObject from '../base/GameObject'; import * as PIXI from 'pixi.js'; @@ -34,27 +35,70 @@ export function CreepStats(ctype: CreepType): object { }; } } + +export enum CreepEvents { + Died = 'died', + TakenDamage = 'takenDamage', + Escaped = 'escaped', + Moved = 'moved', +} + export default class Creep extends GameObject { public creepType: CreepType; private path: PathDefinition; private pathIndex: number = 0; private health: number; - private x: number; // X and Y are local to the grid, not canvas - private y: number; - constructor( - creepType: CreepType, - path: PathDefinition, - bounds?: PIXI.Rectangle - ) { + private speed: number = 0.5; + public x: number; // X and Y are local to the grid, not canvas + public y: number; + constructor(creepType: CreepType, path: PathDefinition, bounds?: PIXI.Rectangle) { super(bounds); this.creepType = creepType; this.path = path; + this.x = path[0][0] + 0.5; // centered + this.y = path[0][1] + 0.5; + } + public update(elapsedMS: number) { + if (this.pathIndex + 1 == this.path.length) { + this.events.emit(CreepEvents.Escaped, this); + return; + } + const currentCell = this.path[this.pathIndex]; + const targetCell = this.path[this.pathIndex + 1]; + const directionX = targetCell[1] - currentCell[1]; + const directionY = targetCell[0] - currentCell[0]; + let deltaX = this.speed * elapsedMS * directionX; + let deltaY = this.speed * elapsedMS * directionY; + if (this.x + deltaX > targetCell[1] + 0.5) { + // limit to center of target cell + deltaX = targetCell[1] + 0.5 - this.x; + this.pathIndex++; + } + if (this.y + deltaY > targetCell[0] + 0.5) { + // limit to center of target cell + deltaY = targetCell[0] + 0.5 - this.y; + this.pathIndex++; + } + this.x += deltaX; + this.y += deltaY; + console.log('creep moved', deltaX, deltaY); + this.events.emit(CreepEvents.Moved, this); + } + + public takeDamage(amount: number) { + this.health -= amount; + if (this.health < 0) { + this.events.emit(CreepEvents.Died, this); + } } - public update() {} protected draw() { this.container.removeChildren(); - + const sprite = new PIXI.Sprite(Assets.BasicCreepTexture); + sprite.x = 0; + sprite.y = 0; + sprite.width = this.bounds.width; + sprite.height = this.bounds.height; this.container.x = this.bounds.x; this.container.y = this.bounds.y; } diff --git a/src/components/Grid.ts b/src/components/Grid.ts index d6301d8..634ad3b 100644 --- a/src/components/Grid.ts +++ b/src/components/Grid.ts @@ -1,6 +1,7 @@ import * as PIXI from 'pixi.js'; import GameObject from '../base/GameObject'; import { GameMapDefinition, TerrainType } from '../base/Definitions'; +import Creep, { CreepEvents } from './Creep'; export class Cell extends GameObject { public type: TerrainType; @@ -8,13 +9,7 @@ export class Cell extends GameObject { public column: number; public isPath: boolean = false; - constructor( - type: TerrainType, - row: number, - column: number, - isPath: boolean, - bounds?: PIXI.Rectangle - ) { + constructor(type: TerrainType, row: number, column: number, isPath: boolean, bounds?: PIXI.Rectangle) { super(bounds); this.type = type; this.row = row; @@ -44,6 +39,7 @@ export class Cell extends GameObject { export class Grid extends GameObject { private gameMap: GameMapDefinition; private cells: Cell[] = []; + private creeps: Creep[] = []; constructor(map: GameMapDefinition, bounds?: PIXI.Rectangle) { super(bounds); @@ -52,9 +48,7 @@ export class Grid extends GameObject { for (let y = 0; y < this.gameMap.rows; y++) { for (let x = 0; x < this.gameMap.columns; x++) { let type = this.gameMap.cells[x][y]; - const isPath = this.gameMap.paths.some((path) => - path.some((p) => p[0] === x && p[1] === y) - ); + const isPath = this.gameMap.paths.some((path) => path.some((p) => p[0] === x && p[1] === y)); if (isPath) type = TerrainType.Restricted; let cell = new Cell(type, x, y, isPath); this.cells.push(cell); @@ -63,7 +57,33 @@ export class Grid extends GameObject { console.log(this.cells); this.draw(); } - + public addCreep(creep: Creep) { + this.creeps.push(creep); + creep.events.on(CreepEvents.Moved, (movedCreep) => { + this.onCreepMoved(movedCreep); + }); + creep.events.on(CreepEvents.Died, (diedCreep) => { + this.onCreepDiedOrEscaped(diedCreep); + }); + creep.events.on(CreepEvents.Escaped, (escapedCreep) => { + this.onCreepDiedOrEscaped(escapedCreep); + }); + this.draw(); + } + private onCreepMoved(movedCreep: Creep) { + movedCreep.setBounds( + new PIXI.Rectangle( + this.gridUnitsToPixels(movedCreep.x), + this.gridUnitsToPixels(movedCreep.y), + this.gridUnitsToPixels(0.3), + this.gridUnitsToPixels(0.3) + ) + ); + } + private onCreepDiedOrEscaped(creep: Creep) { + this.creeps.splice(this.creeps.indexOf(creep), 1); + this.draw(); + } protected draw() { console.log('Drawing Grid', this.bounds); this.container.removeChildren(); @@ -80,6 +100,16 @@ export class Grid extends GameObject { ); this.container.addChild(cell.container); } + for (const creep of this.creeps) { + creep.setBounds( + this.gridUnitsToPixels(creep.x), + this.gridUnitsToPixels(creep.y), + this.gridUnitsToPixels(0.3), + this.gridUnitsToPixels(0.3) + ); + // console.log(creep.getBounds()); + this.container.addChild(creep.container); + } this.container.x = this.bounds.x; this.container.y = this.bounds.y; } diff --git a/src/components/MissionStats.ts b/src/components/MissionStats.ts index 07ef765..0456d0c 100644 --- a/src/components/MissionStats.ts +++ b/src/components/MissionStats.ts @@ -5,6 +5,10 @@ export default class MissionStats extends GameObject { private hp: number = 100; private gold: number = 0; + public getHP() { + return this.hp; + } + public setHP(hp: number) { this.hp = hp; this.draw(); @@ -15,11 +19,7 @@ export default class MissionStats extends GameObject { this.draw(); } - constructor( - initialHP: number, - initialGold: number, - bounds?: PIXI.Rectangle - ) { + constructor(initialHP: number, initialGold: number, bounds?: PIXI.Rectangle) { super(bounds); this.hp = initialHP; this.gold = initialGold; diff --git a/src/components/WaveManager.ts b/src/components/WaveManager.ts index e14418f..c02f432 100644 --- a/src/components/WaveManager.ts +++ b/src/components/WaveManager.ts @@ -1,36 +1,72 @@ -import { - CreepType, - MissionRoundDefinition, - PathDefinition, -} from '../base/Definitions'; -import Creep from './Creep'; +import { CreepType, MissionRoundDefinition, PathDefinition } from '../base/Definitions'; +import * as PIXI from 'pixi.js'; +import Creep, { CreepEvents } from './Creep'; + +export enum WaveManagerEvents { + CreepSpawned = 'creepSpawned', + Finished = 'finished', +} + +type CreepInstance = { + creep: Creep; + tickToSpawnAt: number; + spawned: boolean; +}; export default class WaveManager { // Doesn't need to extend GameObject since it does not render - private currentWave: number; - private creeps: Creep[] = []; + // public currentRound: number = 0; + private creeps: CreepInstance[] = []; private rounds: MissionRoundDefinition[]; private paths: PathDefinition[]; - private spawnIntervalTicks: number; - private firstCreepSpawnTick: number; - public ticks: number = 0; + private ticks: number = 0; + private started: boolean = false; + public finished: boolean = false; + public events = new PIXI.EventEmitter(); constructor(rounds: MissionRoundDefinition[], paths: PathDefinition[]) { this.rounds = rounds; this.paths = paths; } - private updateCreeps() { + public start(roundIndex) { + this.started = true; + this.ticks = 0; + this.creeps = []; + this.finished = false; + let tickToSpawnAt = 0; + this.rounds[roundIndex].waves.forEach((wave) => { + tickToSpawnAt += wave.firstCreepSpawnTick; + wave.creeps.forEach((creep) => { + const creepObj = new Creep(creep, this.paths[0]); + const creepInstance = { + creep: creepObj, + tickToSpawnAt, + spawned: false, + }; + tickToSpawnAt += wave.spawnIntervalTicks; + this.creeps.push(creepInstance); + }); + }); + console.log(this.creeps); + } + public end() { + this.started = false; + } + public update(elapsedMS: number): void { + if (this.started == false) return; + this.ticks += elapsedMS; this.creeps.forEach((creep) => { - creep.update(); - // TODO: updating here is fine, change to make spawning emit an event - // which GameScene will catch and send to Grid who will draw the creep - // based on the coordinates that the creep calculates. + if (!creep.spawned && creep.tickToSpawnAt <= this.ticks) { + creep.spawned = true; + this.events.emit(WaveManagerEvents.CreepSpawned, creep.creep); + console.log('Wave manager creep spawned, ', creep, this.ticks); + if (!this.finished && this.creeps.every((creep) => creep.spawned)) { + this.finished = true; + console.log('wave maanger finisehd'); + this.events.emit(WaveManagerEvents.Finished); + } + } else if (creep.spawned) { + creep.creep.update(elapsedMS); + } }); } - public update(fps): void { - if (this.creeps.length != 0) this.updateCreeps(); - this.ticks++; - if (this.ticks == 200) { - this.creeps.push(new Creep(CreepType.Basic, this.paths[0])); - } - } } diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index f9fb443..1e56f6b 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -1,56 +1,87 @@ import Button from '../base/Button'; import { MissionDefinition } from '../base/Definitions'; +import Creep from '../components/Creep'; import { Grid } from '../components/Grid'; import MissionStats from '../components/MissionStats'; -import WaveManager from '../components/WaveManager'; +import WaveManager, { WaveManagerEvents } from '../components/WaveManager'; import SceneBase from './SceneBase'; import * as PIXI from 'pixi.js'; +enum RoundMode { + Purchase = 0, + Combat = 1, +} + export default class GameScene extends SceneBase { private ticker: PIXI.Ticker; private stats: MissionStats; private grid: Grid; - public waveManager: WaveManager; + private waveManager: WaveManager; + private roundMode = RoundMode.Purchase; + private changeRoundButton: Button; + private currentRound: number = 0; constructor(mission: MissionDefinition, bounds: PIXI.Rectangle) { super(bounds); - this.waveManager = new WaveManager( - mission.rounds, - mission.gameMap.paths - ); + this.waveManager = new WaveManager(mission.rounds, mission.gameMap.paths); + this.waveManager.events.on(WaveManagerEvents.CreepSpawned, (creep: Creep) => { + this.grid.addCreep(creep); + }); this.stats = new MissionStats(100, 200); this.grid = new Grid(mission.gameMap); this.ticker = new PIXI.Ticker(); this.ticker.maxFPS = 60; this.ticker.minFPS = 30; - this.ticker.add(() => this.update(this.ticker.FPS)); // bruh + this.ticker.add(() => this.update(this.ticker.elapsedMS)); // bruh this.ticker.start(); + this.changeRoundButton = new Button('Start', new PIXI.Color('white'), true); + this.changeRoundButton.events.on('click', () => { + console.log('clicked'); + this.changeRoundButton.setEnabled(false); + this.changeRoundButton.setCaption('[X]'); + this.setRoundMode(RoundMode.Combat); + }); this.draw(); } - private getStatusBounds(): PIXI.Rectangle { - // Top / Center - return new PIXI.Rectangle(this.bounds.width / 2 - 200 / 2, 0, 200, 100); - } - - private getGridBounds(): PIXI.Rectangle { - // Center / Center - return new PIXI.Rectangle( - this.bounds.width / 2 - 600 / 2, - this.bounds.height / 2 - 600 / 2, - 600, - 600 - ); - } - public destroy() { super.destroy(); this.ticker.stop(); this.ticker.destroy(); } - public update(fps) { - this.waveManager.update(fps); + public update(elapsedMS: number) { + if (this.checkGameOver()) return; + this.waveManager.update(elapsedMS); + this.checkToEndCombat(); + } + + private setRoundMode(roundMode: RoundMode) { + this.roundMode = roundMode; + if (this.roundMode == RoundMode.Combat) { + this.waveManager.start(this.currentRound); + } else { + this.waveManager.end(); + } + } + + private checkToEndCombat() { + let isFinished = false; // todo: implement + if (!this.waveManager.finished) { + isFinished = false; + } + if (isFinished) { + this.currentRound++; + this.setRoundMode(RoundMode.Purchase); + } + } + + private checkGameOver() { + if (this.stats.getHP() <= 0) { + // TODO: end game + return true; + } + return false; } protected draw() { @@ -62,9 +93,25 @@ export default class GameScene extends SceneBase { this.container.addChild(g); this.stats.setBounds(this.getStatusBounds()); this.grid.setBounds(this.getGridBounds()); + this.changeRoundButton.setBounds(this.getChangeRoundButtonBounds()); this.container.addChild(this.stats.container); this.container.addChild(this.grid.container); + this.container.addChild(this.changeRoundButton.container); this.container.x = this.bounds.x; this.container.y = this.bounds.y; } + + private getStatusBounds(): PIXI.Rectangle { + // Top / Center + return new PIXI.Rectangle(this.bounds.width / 2 - 200 / 2, 0, 200, 100); + } + + private getGridBounds(): PIXI.Rectangle { + // Center / Center + return new PIXI.Rectangle(this.bounds.width / 2 - 600 / 2, this.bounds.height / 2 - 600 / 2, 600, 600); + } + private getChangeRoundButtonBounds(): PIXI.Rectangle { + // Center / Center + return new PIXI.Rectangle(this.bounds.width - 300, this.bounds.height - 150, 300, 150); + } }