diff --git a/public/assets/CreepStats.json b/public/assets/CreepStats.json new file mode 100644 index 0000000..2d76ba3 --- /dev/null +++ b/public/assets/CreepStats.json @@ -0,0 +1,14 @@ +[ + { + "health": 2, + "speed": 0.0005, + "special": null, + "resistance": { + "physical": 0, + "divine": 0, + "fire": 0, + "ice": 0, + "frostfire": 0 + } + } +] diff --git a/public/maps/mission_01.png b/public/assets/maps/mission_01.png similarity index 100% rename from public/maps/mission_01.png rename to public/assets/maps/mission_01.png diff --git a/public/assets/missions/mission_01.json b/public/assets/missions/mission_01.json index 2d81053..4c142f7 100644 --- a/public/assets/missions/mission_01.json +++ b/public/assets/missions/mission_01.json @@ -1,6 +1,11 @@ { "name": "Mission 1", "description": "This is the first mission", + "mapImage": { + "url": "/assets/maps/mission_01.png", + "width": 1200, + "height": 900 + }, "gameMap": { "rows": 15, "columns": 20, @@ -19,29 +24,36 @@ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ], "paths": [ [ - [0, 3], - [1, 3], - [2, 3], - [3, 3], - [3, 2], - [4, 2], - [5, 2], - [6, 2], - [7, 2], - [7, 3], - [7, 4], - [8, 4], - [9, 4] + [8, 0], + [8, 1], + [9, 2], + [10, 2], + [11, 2], + [12, 3], + [12, 4], + [12, 5], + [12, 6], + [12, 7], + [12, 8], + [12, 9], + [11, 10], + [10, 10], + [9, 10], + [8, 11], + [8, 12], + [8, 13], + [7, 14], + [6, 14], + [5, 14], + [4, 14], + [3, 14], + [2, 14], + [1, 14], + [0, 15] ] ] }, diff --git a/src/base/Assets.ts b/src/base/Assets.ts index 51bd5eb..af333dc 100644 --- a/src/base/Assets.ts +++ b/src/base/Assets.ts @@ -1,5 +1,5 @@ import * as PIXI from 'pixi.js'; -import { MissionDefinition } from './Definitions'; +import { CreepStats, MissionDefinition } from './Definitions'; export default class Assets { public static async LoadAssets() { @@ -12,6 +12,13 @@ export default class Assets { }); console.log('Loading Missions'); await this.LoadMissions(); + await this.LoadCreepStats(); + } + + public static async LoadCreepStats() { + const res = await fetch('/assets/CreepStats.json'); + const stats = await res.json(); + this.CreepStats = stats; } private static async LoadMissions() { @@ -21,11 +28,22 @@ export default class Assets { private static async LoadMission(missionUrl: string) { const res = await fetch(missionUrl); const mission = await res.json(); + await this.LoadBackground(mission.mapImage.url); return mission; } + private static async LoadBackground(backgroundUrl: string) { + let index = this.MissionBackgrounds.length - 1; + if (index == -1) index = 0; + this.MissionBackgrounds[index] = await PIXI.Assets.load({ + src: backgroundUrl, + }); + } + public static ButtonTexture: PIXI.Texture; public static BasicCreepTexture: PIXI.Texture; + public static MissionBackgrounds: PIXI.Texture[] = []; public static Missions: MissionDefinition[]; + public static CreepStats: CreepStats[]; } diff --git a/src/base/Definitions.ts b/src/base/Definitions.ts index 5ae1cb2..3963d13 100644 --- a/src/base/Definitions.ts +++ b/src/base/Definitions.ts @@ -1,11 +1,17 @@ export type MissionDefinition = { name: string; description: string; - mapImageUrl: string; + mapImage: MapImageDefinition; gameMap: GameMapDefinition; rounds: MissionRoundDefinition[]; }; +export type MapImageDefinition = { + url: string; + width: number; + height: number; +}; + export type GameMapDefinition = { rows: number; columns: number; @@ -24,6 +30,21 @@ export type WaveDefinition = { creeps: CreepType[]; }; +export type CreepStats = { + health: number; + speed: number; + special: Function; + resistance: CreepResistances; +}; + +export type CreepResistances = { + physical: number; + divine: number; + fire: number; + ice: number; + frostfire: number; +}; + export type PathDefinition = [[row: number, column: number]]; export enum CreepType { @@ -34,6 +55,7 @@ export enum CreepType { export enum TerrainType { Restricted = 0, Buildable = 1, + Path = 9, } export enum GemType { diff --git a/src/base/Game.ts b/src/base/Game.ts index 8801beb..6a58641 100644 --- a/src/base/Game.ts +++ b/src/base/Game.ts @@ -13,7 +13,7 @@ export default class Game extends GameObject { super(bounds); let params = new URLSearchParams(location.href); if (params.entries().next().value[1] == 'game') { - this.setScene(new GameScene(Assets.Missions[0], this.bounds)); + this.setScene(new GameScene(Assets.Missions[0], 0, this.bounds)); } else this.onMainMenu(); } @@ -39,7 +39,7 @@ export default class Game extends GameObject { const missionSelectScene = new MissionMenuSelect(this.bounds); missionSelectScene.events.on('mission', (mission) => { console.log('Mission selected', mission); - this.setScene(new GameScene(mission, this.bounds)); + this.setScene(new GameScene(mission, Assets.Missions.indexOf(mission), this.bounds)); }); missionSelectScene.events.on('back', () => { this.onMainMenu(); @@ -53,12 +53,7 @@ export default class Game extends GameObject { protected triggerBoundsChanged(): void { if (this._currentScene) { - this._currentScene.setBounds( - 0, - 0, - this.bounds.width, - this.bounds.height - ); + this._currentScene.setBounds(0, 0, this.bounds.width, this.bounds.height); } } diff --git a/src/components/Creep.ts b/src/components/Creep.ts index 577e236..4154d78 100644 --- a/src/components/Creep.ts +++ b/src/components/Creep.ts @@ -1,40 +1,8 @@ import Assets from '../base/Assets'; -import { CreepType, PathDefinition } from '../base/Definitions'; +import { CreepStats, CreepType, PathDefinition } from '../base/Definitions'; import GameObject from '../base/GameObject'; import * as PIXI from 'pixi.js'; - -export function CreepStats(ctype: CreepType): object { - switch (ctype) { - case CreepType.Basic: - return { - health: 2, - speed: 0.45, - special: null, - resistance: { - physical: 0, - divine: 0, - fire: 0, - ice: 0, - frostfire: 0, - }, - }; - case CreepType.Fast: - throw new Error('Fast creep not defined.'); - default: - return { - health: null, - speed: null, - special: null, - resistance: { - physical: null, - divine: null, - fire: null, - ice: null, - frostfire: null, - }, - }; - } -} +import GameScene from '../scenes/GameScene'; export enum CreepEvents { Died = 'died', @@ -46,19 +14,29 @@ export enum CreepEvents { export default class Creep extends GameObject { public creepType: CreepType; private path: PathDefinition; + private stats: CreepStats; private pathIndex: number = 0; - private speed: number = 0.002; - public health: number = 2; + private speed: number; + private gameScene: GameScene; + public health: number; public escaped: boolean = false; public died: boolean = false; public x: number; // X and Y are local to the grid, not canvas public y: number; - constructor(creepType: CreepType, path: PathDefinition, bounds?: PIXI.Rectangle) { + constructor(creepType: CreepType, path: PathDefinition, gameScene: GameScene, bounds?: PIXI.Rectangle) { super(bounds); + this.gameScene = gameScene; this.creepType = creepType; + this.stats = Assets.CreepStats[this.creepType]; + this.speed = this.stats.speed; + this.health = this.stats.health; this.path = path; this.x = path[0][1] + 0.5; // centered this.y = path[0][0] + 0.5; + this.gameScene.grid.container.addChild(this.container); + } + private gridUnitsToPixels(gridUnits) { + return this.gameScene.grid.gridUnitsToPixels(gridUnits); } public update(elapsedMS: number) { if (this.pathIndex + 1 == this.path.length) { @@ -101,7 +79,14 @@ export default class Creep extends GameObject { this.x += deltaX; this.y += deltaY; if (increaseIndex) this.pathIndex++; - this.events.emit(CreepEvents.Moved, this); + this.setBounds( + new PIXI.Rectangle( + this.gridUnitsToPixels(this.x), + this.gridUnitsToPixels(this.y), + this.gridUnitsToPixels(0.5), + this.gridUnitsToPixels(0.6) + ) + ); } public takeDamage(amount: number) { @@ -112,6 +97,11 @@ export default class Creep extends GameObject { } } + public override destroy() { + super.destroy(); + this.draw = null; + this.container.removeChildren(); + } protected draw() { this.container.removeChildren(); const sprite = new PIXI.Sprite(Assets.BasicCreepTexture); diff --git a/src/components/Grid.ts b/src/components/Grid.ts index 871efdf..d7d4d75 100644 --- a/src/components/Grid.ts +++ b/src/components/Grid.ts @@ -2,19 +2,23 @@ import * as PIXI from 'pixi.js'; import GameObject from '../base/GameObject'; import { GameMapDefinition, TerrainType } from '../base/Definitions'; import Creep, { CreepEvents } from './Creep'; +import GameScene from '../scenes/GameScene'; +import Assets from '../base/Assets'; export class Cell extends GameObject { public type: TerrainType; public row: number; public column: number; public isPath: boolean = false; + private grid: Grid; - constructor(type: TerrainType, row: number, column: number, isPath: boolean, bounds?: PIXI.Rectangle) { + constructor(type: TerrainType, row: number, column: number, isPath: boolean, grid: Grid, bounds?: PIXI.Rectangle) { super(bounds); this.type = type; this.row = row; this.column = column; this.isPath = isPath; + this.grid = grid; this.draw(); } @@ -26,31 +30,55 @@ export class Cell extends GameObject { case TerrainType.Restricted: g.fill(0xff0000); break; + case TerrainType.Path: + g.fill(0xff00ff); + break; case TerrainType.Buildable: - g.fill(0x00ff00); + g.stroke(0x00ff00); break; } this.container.addChild(g); this.container.x = this.bounds.x; this.container.y = this.bounds.y; + return; // comment to enable debugging + const text = new PIXI.Text({ + text: `${this.row}|${this.column}`, + style: new PIXI.TextStyle({ + fill: 0xffffff, + dropShadow: true, + fontSize: 16, + }), + }); + this.container.addChild(text); + text.anchor.set(0.5, 0.5); + text.x = this.bounds.width / 2; + text.y = this.bounds.height / 2; + if (this.isPath) text.text += 'P'; } } export class Grid extends GameObject { private gameMap: GameMapDefinition; private cells: Cell[] = []; - private creeps: Creep[] = []; + private gameScene: GameScene; + public creeps: Creep[] = []; - constructor(map: GameMapDefinition, bounds?: PIXI.Rectangle) { + constructor(map: GameMapDefinition, gameScene: GameScene, bounds?: PIXI.Rectangle) { super(bounds); this.gameMap = map; + this.gameScene = gameScene; console.log(this.gameMap.paths); - 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]; + for (let y = 0; y < this.gameMap.columns; y++) { + for (let x = 0; x < this.gameMap.rows; x++) { + let type; + try { + type = this.gameMap.cells[x][y]; + } catch (e) { + type = 1; + } 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); + if (isPath) type = TerrainType.Path; + let cell = new Cell(type, x, y, isPath, this); this.cells.push(cell); } } @@ -59,38 +87,30 @@ export class Grid extends GameObject { } 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.5), - this.gridUnitsToPixels(0.6) - ) - ); } private onCreepDiedOrEscaped(creep: Creep) { this.creeps.splice(this.creeps.indexOf(creep), 1); - this.draw(); + creep.destroy(); } protected draw() { console.log('Drawing Grid', this.bounds); this.container.removeChildren(); - let g = new PIXI.Graphics(); - g.rect(0, 0, this.bounds.width, this.bounds.height); - g.fill(0x00aa00); - this.container.addChild(g); + // let g = new PIXI.Graphics(); + // g.rect(0, 0, this.bounds.width, this.bounds.height + 100); + // g.fill(0xffffff); + // this.container.addChild(g); + let background = new PIXI.Sprite(Assets.MissionBackgrounds[this.gameScene.missionIndex]); + background.x = 0; + background.y = 0; + background.width = this.bounds.width; + background.height = this.bounds.height; + this.container.addChild(background); for (let cell of this.cells) { cell.setBounds( this.gridUnitsToPixels(cell.column), @@ -100,16 +120,6 @@ 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.5), - this.gridUnitsToPixels(0.6) - ); - // 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/Tower.ts b/src/components/Tower.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/components/WaveManager.ts b/src/components/WaveManager.ts index c02f432..387a57b 100644 --- a/src/components/WaveManager.ts +++ b/src/components/WaveManager.ts @@ -1,6 +1,7 @@ import { CreepType, MissionRoundDefinition, PathDefinition } from '../base/Definitions'; import * as PIXI from 'pixi.js'; import Creep, { CreepEvents } from './Creep'; +import GameScene from '../scenes/GameScene'; export enum WaveManagerEvents { CreepSpawned = 'creepSpawned', @@ -21,11 +22,13 @@ export default class WaveManager { private paths: PathDefinition[]; private ticks: number = 0; private started: boolean = false; + private gameScene: GameScene; public finished: boolean = false; public events = new PIXI.EventEmitter(); - constructor(rounds: MissionRoundDefinition[], paths: PathDefinition[]) { + constructor(rounds: MissionRoundDefinition[], paths: PathDefinition[], gameScene) { this.rounds = rounds; this.paths = paths; + this.gameScene = gameScene; } public start(roundIndex) { this.started = true; @@ -36,7 +39,7 @@ export default class WaveManager { this.rounds[roundIndex].waves.forEach((wave) => { tickToSpawnAt += wave.firstCreepSpawnTick; wave.creeps.forEach((creep) => { - const creepObj = new Creep(creep, this.paths[0]); + const creepObj = new Creep(creep, this.paths[0], this.gameScene); const creepInstance = { creep: creepObj, tickToSpawnAt, diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index 0c496e1..ffb2697 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -13,17 +13,21 @@ enum RoundMode { } export default class GameScene extends SceneBase { + public grid: Grid; private ticker: PIXI.Ticker; private stats: MissionStats; - private grid: Grid; private waveManager: WaveManager; private roundMode = RoundMode.Purchase; private changeRoundButton: Button; private currentRound: number = 0; + public missionIndex: number; - constructor(mission: MissionDefinition, bounds: PIXI.Rectangle) { + private gridWidth: number; + private gridHeight: number; + + constructor(mission: MissionDefinition, missionIndex: number, bounds: PIXI.Rectangle) { super(bounds); - this.waveManager = new WaveManager(mission.rounds, mission.gameMap.paths); + this.waveManager = new WaveManager(mission.rounds, mission.gameMap.paths, this); this.waveManager.events.on(WaveManagerEvents.CreepSpawned, (creep: Creep) => { this.grid.addCreep(creep); creep.events.on(CreepEvents.Escaped, () => { @@ -31,12 +35,15 @@ export default class GameScene extends SceneBase { }); }); this.stats = new MissionStats(100, 200); - this.grid = new Grid(mission.gameMap); + this.grid = new Grid(mission.gameMap, this); + this.gridWidth = mission.mapImage.width; + this.gridHeight = mission.mapImage.height; this.ticker = new PIXI.Ticker(); this.ticker.maxFPS = 60; this.ticker.minFPS = 30; this.ticker.add(() => this.update(this.ticker.elapsedMS)); // bruh this.ticker.start(); + this.missionIndex = missionIndex; this.changeRoundButton = new Button('Start', new PIXI.Color('white'), true); this.changeRoundButton.events.on('click', () => { console.log('clicked'); @@ -56,6 +63,7 @@ export default class GameScene extends SceneBase { public update(elapsedMS: number) { if (this.checkGameOver()) return; this.waveManager.update(elapsedMS); + this.grid.creeps.forEach((creep) => creep.update(elapsedMS)); this.checkToEndCombat(); } @@ -115,10 +123,19 @@ export default class GameScene extends SceneBase { private getGridBounds(): PIXI.Rectangle { // Center / Center - return new PIXI.Rectangle(this.bounds.width / 2 - 600 / 2, this.bounds.height / 2 - 600 / 2, 600, 600); + let width = 600; + let height = 600; + return new PIXI.Rectangle( + this.bounds.width / 2 - this.gridWidth / 2, + this.bounds.height / 2 - this.gridHeight / 2, + this.gridWidth, + this.gridHeight + ); } private getChangeRoundButtonBounds(): PIXI.Rectangle { // Center / Center - return new PIXI.Rectangle(this.bounds.width - 300, this.bounds.height - 150, 300, 150); + let width = 300; + let height = 150; + return new PIXI.Rectangle(this.bounds.width - width, this.bounds.height - height, width, height); } }