This commit is contained in:
koneko 2024-10-06 18:32:39 +02:00
parent 21f5f7d5b3
commit af81a95a54
10 changed files with 248 additions and 90 deletions

View File

@ -2,5 +2,6 @@
"trailingComma": "es5", "trailingComma": "es5",
"tabWidth": 4, "tabWidth": 4,
"semi": true, "semi": true,
"singleQuote": true "singleQuote": true,
"printWidth": 120
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -49,18 +49,18 @@
{ {
"waves": [ "waves": [
{ {
"firstCreepSpawnTick": 120, "firstCreepSpawnTick": 2000,
"spawnIntervalTicks": 60, "spawnIntervalTicks": 1000,
"creeps": [0, 0, 0, 0, 0, 1, 1, 1, 0] "creeps": [0, 0, 0, 0, 0, 1, 1, 1, 0]
}, },
{ {
"firstCreepSpawnTick": 480, "firstCreepSpawnTick": 2000,
"spawnIntervalTicks": 60, "spawnIntervalTicks": 1000,
"creeps": [0, 0, 0, 0, 0, 1, 1, 1, 0] "creeps": [0, 0, 0, 0, 0, 1, 1, 1, 0]
}, },
{ {
"firstCreepSpawnTick": 480, "firstCreepSpawnTick": 2000,
"spawnIntervalTicks": 60, "spawnIntervalTicks": 1000,
"creeps": [0, 0, 0, 0, 0, 1, 1, 1, 0] "creeps": [0, 0, 0, 0, 0, 1, 1, 1, 0]
} }
], ],

View File

@ -7,14 +7,15 @@ export default class Assets {
Assets.ButtonTexture = await PIXI.Assets.load({ Assets.ButtonTexture = await PIXI.Assets.load({
src: '/assets/gui/button_02.png', src: '/assets/gui/button_02.png',
}); });
Assets.BasicCreepTexture = await PIXI.Assets.load({
src: '/assets/creeps/basic.jpg',
});
console.log('Loading Missions'); console.log('Loading Missions');
await this.LoadMissions(); await this.LoadMissions();
} }
private static async LoadMissions() { private static async LoadMissions() {
Assets.Missions = [ Assets.Missions = [await this.LoadMission('/assets/missions/mission_01.json')];
await this.LoadMission('/assets/missions/mission_01.json'),
];
} }
private static async LoadMission(missionUrl: string) { private static async LoadMission(missionUrl: string) {
@ -24,6 +25,7 @@ export default class Assets {
} }
public static ButtonTexture: PIXI.Texture; public static ButtonTexture: PIXI.Texture;
public static BasicCreepTexture: PIXI.Texture;
public static Missions: MissionDefinition[]; public static Missions: MissionDefinition[];
} }

View File

@ -8,12 +8,7 @@ export default abstract class GameObject {
public setBounds(bounds: PIXI.Rectangle): void; public setBounds(bounds: PIXI.Rectangle): void;
public setBounds(x: number, y: number, width: number, height: number): void; public setBounds(x: number, y: number, width: number, height: number): void;
public setBounds( public setBounds(boundsOrX: PIXI.Rectangle | number, y?: number, width?: number, height?: number) {
boundsOrX: PIXI.Rectangle | number,
y?: number,
width?: number,
height?: number
) {
if (boundsOrX instanceof PIXI.Rectangle) { if (boundsOrX instanceof PIXI.Rectangle) {
this.bounds = boundsOrX; this.bounds = boundsOrX;
} else { } else {
@ -24,8 +19,7 @@ export default abstract class GameObject {
public destroy() { public destroy() {
this._events.removeAllListeners(); this._events.removeAllListeners();
if (this._container.parent) if (this._container.parent) this._container.parent.removeChild(this._container);
this._container.parent.removeChild(this._container);
this._container.destroy(); this._container.destroy();
} }
@ -37,6 +31,10 @@ export default abstract class GameObject {
return this._events; return this._events;
} }
public getBounds(): PIXI.Rectangle {
return this.bounds;
}
protected triggerBoundsChanged() { protected triggerBoundsChanged() {
this.draw(); this.draw();
} }

View File

@ -1,3 +1,4 @@
import Assets from '../base/Assets';
import { CreepType, PathDefinition } from '../base/Definitions'; import { CreepType, PathDefinition } from '../base/Definitions';
import GameObject from '../base/GameObject'; import GameObject from '../base/GameObject';
import * as PIXI from 'pixi.js'; 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 { export default class Creep extends GameObject {
public creepType: CreepType; public creepType: CreepType;
private path: PathDefinition; private path: PathDefinition;
private pathIndex: number = 0; private pathIndex: number = 0;
private health: number; private health: number;
private x: number; // X and Y are local to the grid, not canvas private speed: number = 0.5;
private y: number; public x: number; // X and Y are local to the grid, not canvas
constructor( public y: number;
creepType: CreepType, constructor(creepType: CreepType, path: PathDefinition, bounds?: PIXI.Rectangle) {
path: PathDefinition,
bounds?: PIXI.Rectangle
) {
super(bounds); super(bounds);
this.creepType = creepType; this.creepType = creepType;
this.path = path; 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() { protected draw() {
this.container.removeChildren(); 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.x = this.bounds.x;
this.container.y = this.bounds.y; this.container.y = this.bounds.y;
} }

View File

@ -1,6 +1,7 @@
import * as PIXI from 'pixi.js'; import * as PIXI from 'pixi.js';
import GameObject from '../base/GameObject'; import GameObject from '../base/GameObject';
import { GameMapDefinition, TerrainType } from '../base/Definitions'; import { GameMapDefinition, TerrainType } from '../base/Definitions';
import Creep, { CreepEvents } from './Creep';
export class Cell extends GameObject { export class Cell extends GameObject {
public type: TerrainType; public type: TerrainType;
@ -8,13 +9,7 @@ export class Cell extends GameObject {
public column: number; public column: number;
public isPath: boolean = false; public isPath: boolean = false;
constructor( constructor(type: TerrainType, row: number, column: number, isPath: boolean, bounds?: PIXI.Rectangle) {
type: TerrainType,
row: number,
column: number,
isPath: boolean,
bounds?: PIXI.Rectangle
) {
super(bounds); super(bounds);
this.type = type; this.type = type;
this.row = row; this.row = row;
@ -44,6 +39,7 @@ export class Cell extends GameObject {
export class Grid extends GameObject { export class Grid extends GameObject {
private gameMap: GameMapDefinition; private gameMap: GameMapDefinition;
private cells: Cell[] = []; private cells: Cell[] = [];
private creeps: Creep[] = [];
constructor(map: GameMapDefinition, bounds?: PIXI.Rectangle) { constructor(map: GameMapDefinition, bounds?: PIXI.Rectangle) {
super(bounds); super(bounds);
@ -52,9 +48,7 @@ export class Grid extends GameObject {
for (let y = 0; y < this.gameMap.rows; y++) { for (let y = 0; y < this.gameMap.rows; y++) {
for (let x = 0; x < this.gameMap.columns; x++) { for (let x = 0; x < this.gameMap.columns; x++) {
let type = this.gameMap.cells[x][y]; let type = this.gameMap.cells[x][y];
const isPath = this.gameMap.paths.some((path) => const isPath = this.gameMap.paths.some((path) => path.some((p) => p[0] === x && p[1] === y));
path.some((p) => p[0] === x && p[1] === y)
);
if (isPath) type = TerrainType.Restricted; if (isPath) type = TerrainType.Restricted;
let cell = new Cell(type, x, y, isPath); let cell = new Cell(type, x, y, isPath);
this.cells.push(cell); this.cells.push(cell);
@ -63,7 +57,33 @@ export class Grid extends GameObject {
console.log(this.cells); console.log(this.cells);
this.draw(); 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() { protected draw() {
console.log('Drawing Grid', this.bounds); console.log('Drawing Grid', this.bounds);
this.container.removeChildren(); this.container.removeChildren();
@ -80,6 +100,16 @@ export class Grid extends GameObject {
); );
this.container.addChild(cell.container); 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.x = this.bounds.x;
this.container.y = this.bounds.y; this.container.y = this.bounds.y;
} }

View File

@ -5,6 +5,10 @@ export default class MissionStats extends GameObject {
private hp: number = 100; private hp: number = 100;
private gold: number = 0; private gold: number = 0;
public getHP() {
return this.hp;
}
public setHP(hp: number) { public setHP(hp: number) {
this.hp = hp; this.hp = hp;
this.draw(); this.draw();
@ -15,11 +19,7 @@ export default class MissionStats extends GameObject {
this.draw(); this.draw();
} }
constructor( constructor(initialHP: number, initialGold: number, bounds?: PIXI.Rectangle) {
initialHP: number,
initialGold: number,
bounds?: PIXI.Rectangle
) {
super(bounds); super(bounds);
this.hp = initialHP; this.hp = initialHP;
this.gold = initialGold; this.gold = initialGold;

View File

@ -1,36 +1,72 @@
import { import { CreepType, MissionRoundDefinition, PathDefinition } from '../base/Definitions';
CreepType, import * as PIXI from 'pixi.js';
MissionRoundDefinition, import Creep, { CreepEvents } from './Creep';
PathDefinition,
} from '../base/Definitions'; export enum WaveManagerEvents {
import Creep from './Creep'; CreepSpawned = 'creepSpawned',
Finished = 'finished',
}
type CreepInstance = {
creep: Creep;
tickToSpawnAt: number;
spawned: boolean;
};
export default class WaveManager { export default class WaveManager {
// Doesn't need to extend GameObject since it does not render // Doesn't need to extend GameObject since it does not render
private currentWave: number; // public currentRound: number = 0;
private creeps: Creep[] = []; private creeps: CreepInstance[] = [];
private rounds: MissionRoundDefinition[]; private rounds: MissionRoundDefinition[];
private paths: PathDefinition[]; private paths: PathDefinition[];
private spawnIntervalTicks: number; private ticks: number = 0;
private firstCreepSpawnTick: number; private started: boolean = false;
public ticks: number = 0; public finished: boolean = false;
public events = new PIXI.EventEmitter();
constructor(rounds: MissionRoundDefinition[], paths: PathDefinition[]) { constructor(rounds: MissionRoundDefinition[], paths: PathDefinition[]) {
this.rounds = rounds; this.rounds = rounds;
this.paths = paths; 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) => { this.creeps.forEach((creep) => {
creep.update(); if (!creep.spawned && creep.tickToSpawnAt <= this.ticks) {
// TODO: updating here is fine, change to make spawning emit an event creep.spawned = true;
// which GameScene will catch and send to Grid who will draw the creep this.events.emit(WaveManagerEvents.CreepSpawned, creep.creep);
// based on the coordinates that the creep calculates. 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]));
}
}
} }

View File

@ -1,56 +1,87 @@
import Button from '../base/Button'; import Button from '../base/Button';
import { MissionDefinition } from '../base/Definitions'; import { MissionDefinition } from '../base/Definitions';
import Creep from '../components/Creep';
import { Grid } from '../components/Grid'; import { Grid } from '../components/Grid';
import MissionStats from '../components/MissionStats'; import MissionStats from '../components/MissionStats';
import WaveManager from '../components/WaveManager'; import WaveManager, { WaveManagerEvents } from '../components/WaveManager';
import SceneBase from './SceneBase'; import SceneBase from './SceneBase';
import * as PIXI from 'pixi.js'; import * as PIXI from 'pixi.js';
enum RoundMode {
Purchase = 0,
Combat = 1,
}
export default class GameScene extends SceneBase { export default class GameScene extends SceneBase {
private ticker: PIXI.Ticker; private ticker: PIXI.Ticker;
private stats: MissionStats; private stats: MissionStats;
private grid: Grid; 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) { constructor(mission: MissionDefinition, bounds: PIXI.Rectangle) {
super(bounds); super(bounds);
this.waveManager = new WaveManager( this.waveManager = new WaveManager(mission.rounds, mission.gameMap.paths);
mission.rounds, this.waveManager.events.on(WaveManagerEvents.CreepSpawned, (creep: Creep) => {
mission.gameMap.paths this.grid.addCreep(creep);
); });
this.stats = new MissionStats(100, 200); this.stats = new MissionStats(100, 200);
this.grid = new Grid(mission.gameMap); this.grid = new Grid(mission.gameMap);
this.ticker = new PIXI.Ticker(); this.ticker = new PIXI.Ticker();
this.ticker.maxFPS = 60; this.ticker.maxFPS = 60;
this.ticker.minFPS = 30; 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.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(); 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() { public destroy() {
super.destroy(); super.destroy();
this.ticker.stop(); this.ticker.stop();
this.ticker.destroy(); this.ticker.destroy();
} }
public update(fps) { public update(elapsedMS: number) {
this.waveManager.update(fps); 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() { protected draw() {
@ -62,9 +93,25 @@ export default class GameScene extends SceneBase {
this.container.addChild(g); this.container.addChild(g);
this.stats.setBounds(this.getStatusBounds()); this.stats.setBounds(this.getStatusBounds());
this.grid.setBounds(this.getGridBounds()); this.grid.setBounds(this.getGridBounds());
this.changeRoundButton.setBounds(this.getChangeRoundButtonBounds());
this.container.addChild(this.stats.container); this.container.addChild(this.stats.container);
this.container.addChild(this.grid.container); this.container.addChild(this.grid.container);
this.container.addChild(this.changeRoundButton.container);
this.container.x = this.bounds.x; this.container.x = this.bounds.x;
this.container.y = this.bounds.y; 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);
}
} }