diff --git a/src/app/components/game/command.ts b/src/app/components/game/command.ts deleted file mode 100644 index 9773fe5..0000000 --- a/src/app/components/game/command.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Command { - step: number; - type: string; - payload: any; -} diff --git a/src/app/components/game/data/EEnemySize.ts b/src/app/components/game/data/EEnemySize.ts new file mode 100644 index 0000000..fc1552f --- /dev/null +++ b/src/app/components/game/data/EEnemySize.ts @@ -0,0 +1,5 @@ +export enum EEnemySize { + Tiny, + Normal, + Huge +} \ No newline at end of file diff --git a/src/app/components/game/data/EProjectileEffectType.ts b/src/app/components/game/data/EProjectileEffectType.ts new file mode 100644 index 0000000..5bd19a0 --- /dev/null +++ b/src/app/components/game/data/EProjectileEffectType.ts @@ -0,0 +1,3 @@ +export enum EProjectileEffectType { + Damage = 0 +} diff --git a/src/app/components/game/game.component.html b/src/app/components/game/game.component.html index 5c2ccd7..6642ba5 100644 --- a/src/app/components/game/game.component.html +++ b/src/app/components/game/game.component.html @@ -3,7 +3,7 @@
- +
@@ -11,8 +11,7 @@ -
Step: {{ engine.currentStep }}
- +
Step: {{ simMain.currentStep }}
diff --git a/src/app/components/game/game.component.ts b/src/app/components/game/game.component.ts index 15505ff..698c80c 100644 --- a/src/app/components/game/game.component.ts +++ b/src/app/components/game/game.component.ts @@ -1,9 +1,8 @@ import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { OptionsComponent } from '../options/options.component'; -import { GameState } from './gameState'; -import { SimulationEngine } from './simulationEngine'; -import { GameRules } from './gameRules'; -import { Command } from './command'; +import { SimMain } from './sim/SimMain'; +import { GdRoot } from './data/GdRoot'; +import { SimCommand } from './sim/commands/SimCommand'; @Component({ selector: 'app-game', @@ -12,10 +11,11 @@ import { Command } from './command'; imports: [OptionsComponent], }) export class GameComponent implements OnInit { - engine!: SimulationEngine; + simMain!: SimMain; private optionsOpen = false; private isPaused = false; private lastFrameTime = 0; + private tileSize = 10; // Size of each tile in pixels @ViewChild('gameCanvas') canvasRef!: ElementRef; @ViewChild('canvasWrapper') wrapperRef!: ElementRef; @@ -26,93 +26,78 @@ export class GameComponent implements OnInit { pixelScale: number = 1; async ngOnInit() { - const initialState: GameState = { - tick: 0, - units: [{ id: 'u1', x: 0, y: 0 }], - commandHistory: [], - }; - - const json = await fetch('/assets/gameData.json').then((r) => r.json()); - this.engine = new SimulationEngine(initialState, GameRules, json); + const gdRoot = await this.loadGdRoot(); + this.simMain = new SimMain(gdRoot); requestAnimationFrame(this.gameLoop.bind(this)); } start() { - this.engine.start(); + this.simMain.start(performance.now()); } stop() { - this.engine.stop(); + this.simMain.stop(); } step() { - this.engine.step(); + this.simMain.step(); } rewind() { - this.engine.rewindToZero(); + //this.simMain.rewindToZero(); } fastForward() { - this.engine.fastForwardToEnd(); + this.simMain.fastForwardToEnd(); + } + + async loadGdRoot(): Promise { + const data = await fetch('/assets/data/gdRoot.json').then((r) => r.json()); + // if (!isGdRoot(data)) { + // throw new Error("Invalid GdRoot!"); + // } + const gdRoot: GdRoot = data; + return gdRoot; } async reloadGameData() { - const json = await fetch('/assets/gameData.json').then((r) => r.json()); - this.engine.reinitializeWithNewData(json); - } - - issueMove() { - const cmd: Command = { - step: this.engine.currentStep + 1, - type: 'move', - payload: { id: 'u1', dx: 1, dy: 0 }, - }; - this.engine.issueCommand(cmd); + this.simMain.setGdRoot(await this.loadGdRoot()); } ngAfterViewInit(): void { this.resizeCanvas(); - requestAnimationFrame(this.render); + requestAnimationFrame(this.update); window.addEventListener('resize', this.resizeCanvas.bind(this)); } - render = (now: number) => { - if (!this.engine) return; + update = (now: number) => { + if (!this.simMain) { + return; + } - const [prevState, currState] = this.engine.getRenderStates(); + this.simMain.update(now) - // Compute interpolation factor (0..1) - const lastStepTime = this.engine.isRunning ? this.engine.lastStepTime : now; - const t = this.engine.isRunning - ? Math.max(Math.min((now - lastStepTime) / this.engine.interval, 1), 0) - : 1; // fully render the last known state + const lastStepTime = this.simMain.isRunning + ? this.simMain.lastStepTime + : now; + const t = this.simMain.isRunning + ? Math.max( + Math.min((now - lastStepTime) / this.simMain.interval, 1), + 0 + ) + : 1; - this.renderFrame(prevState, currState, t); + this.renderFrame(t); - requestAnimationFrame(this.render); + requestAnimationFrame(this.update); }; - renderFrame(prev: GameState, curr: GameState, t: number) { + renderFrame(t: number) { const ctx = this.canvasRef.nativeElement.getContext('2d'); if (!ctx) return; const canvas = this.canvasRef.nativeElement; ctx.clearRect(0, 0, canvas.width, canvas.height); - - for (let i = 0; i < curr.units.length; i++) { - const currUnit = curr.units[i]; - const prevUnit = - prev.units.find((u) => u.id === currUnit.id) ?? currUnit; - - const x = prevUnit.x + (currUnit.x - prevUnit.x) * t; - const y = prevUnit.y + (currUnit.y - prevUnit.y) * t; - - ctx.fillStyle = 'cyan'; - ctx.beginPath(); - ctx.arc(x * 10, y * 10, 5, 0, Math.PI * 2); - ctx.fill(); - } } resizeCanvas(): void { @@ -149,6 +134,18 @@ export class GameComponent implements OnInit { return [px / this.pixelScale, py / this.pixelScale]; } + onCanvasClick(event: MouseEvent) { + const canvas = this.canvasRef.nativeElement; + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + + const px = (event.clientX - rect.left) * scaleX; + const py = (event.clientY - rect.top) * scaleY; + + const [gx, gy] = this.fromScreen(px, py); + } + private gameLoop(currentTime: number): void { if (this.lastFrameTime === 0) { this.lastFrameTime = currentTime; diff --git a/src/app/components/game/gameData.ts b/src/app/components/game/gameData.ts deleted file mode 100644 index 93a2711..0000000 --- a/src/app/components/game/gameData.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface GameData { - unitSpeed: number; - maxUnits: number; - enemySpawnRate: number; - // anything else that's tunable -} diff --git a/src/app/components/game/gameRules.ts b/src/app/components/game/gameRules.ts deleted file mode 100644 index 6381f3e..0000000 --- a/src/app/components/game/gameRules.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Command } from './command'; -import { GameData } from './gameData'; -import { GameState } from './gameState'; - -export const GameRules = { - step(state: GameState, data: GameData): GameState { - const nextTick = state.tick + 1; - - // Get all commands for this step - const commands = state.commandHistory.filter( - (c) => c.step === nextTick - ); - // Clone and update - const newState: GameState = { - ...state, - tick: state.tick + 1, - units: state.units.map((u) => ({ - ...u, - x: u.x + 1, // Simple movement - })), - }; - for (const cmd of commands) { - this.applyCommand(newState, cmd, data); - } - return newState; - }, - - applyCommand(state: GameState, cmd: Command, data: GameData): void { - if (cmd.type === 'move') { - const unit = state.units.find((u) => u.id === cmd.payload.id); - if (unit) { - unit.x += cmd.payload.dx * data.unitSpeed; - unit.y += cmd.payload.dy * data.unitSpeed; - } - } - }, -}; diff --git a/src/app/components/game/gameState.ts b/src/app/components/game/gameState.ts deleted file mode 100644 index 1c2933e..0000000 --- a/src/app/components/game/gameState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Command } from "./command"; - -export interface GameState { - tick: number; - units: { id: string; x: number; y: number }[]; - commandHistory: Command[]; -} diff --git a/src/app/components/game/sim/ECellType.ts b/src/app/components/game/sim/ECellType.ts new file mode 100644 index 0000000..45f2538 --- /dev/null +++ b/src/app/components/game/sim/ECellType.ts @@ -0,0 +1,6 @@ +export enum ECellType { + Free = 0, + Blocked = 1, + Entry = 2, + Reserved +}; diff --git a/src/app/components/game/sim/SimCell.ts b/src/app/components/game/sim/SimCell.ts new file mode 100644 index 0000000..49b0624 --- /dev/null +++ b/src/app/components/game/sim/SimCell.ts @@ -0,0 +1,20 @@ +import { Hex } from "../util/Hex"; +import { ECellType } from "./ECellType"; +import { SimTower } from "./SimTower"; + +export class SimCell { + hex: Hex; + distance: number; + type: ECellType; + index: number; + blockedType: number = -1; + tower: SimTower | null = null; + pathsToTarget: number[][] = []; + + constructor(hex: Hex, distance: number, type: ECellType, cellIndex: number) { + this.hex = hex; + this.distance = distance; + this.type = type; + this.index = cellIndex; + } +} \ No newline at end of file diff --git a/src/app/components/game/sim/SimEnemy.ts b/src/app/components/game/sim/SimEnemy.ts new file mode 100644 index 0000000..bc0e06f --- /dev/null +++ b/src/app/components/game/sim/SimEnemy.ts @@ -0,0 +1,71 @@ +import { EEnemySize } from '../data/EEnemySize'; +import { EProjectileEffectType } from '../data/EProjectileEffectType'; +import { GdProjectileEffect } from '../data/GdProjectileEffect'; +import { Hex } from '../util/Hex'; +import { Vector2 } from '../util/Vector2'; +import { SimLevel } from './SimLevel'; + +export class SimEnemy { + index: number = -1; + routeIdx: number = -1; + startHex: Hex = new Hex(0, 0); + endHex: Hex = new Hex(0, 0); + path: number[] = []; + prevPosition: Vector2 = new Vector2(0, 0); + position: Vector2 = new Vector2(0, 0); + direction: Vector2 = new Vector2(0, 0); + currentPathIndex: number = -1; + currentPathStep: number = -1; + currentPathPosition: Vector2 = new Vector2(0, 0); + dead: boolean = false; + hitPonts: number = 0; + size: EEnemySize = EEnemySize.Tiny; + speed: number = 0; + gain: number = 0; + + constructor(index: number, routeIdx: number) { + this.index = index; + this.routeIdx = routeIdx; + } + + public suffer(level: SimLevel, effect: GdProjectileEffect) { + switch (effect.type) { + case EProjectileEffectType.Damage: { + this.hitPonts -= effect.amount; + if (this.hitPonts <= 0) { + level.currency += this.gain; + this.dead = true; + } + break; + } + } + } + + public onPathUpdated(level: SimLevel) { + const myPos = this.position; + this.currentPathPosition = myPos; + + if (this.path == null) { + return; + } + + const current = this.path[this.currentPathIndex]; + const next = this.path[this.currentPathIndex + 1]; + if (!next) { + return; + } + + const cells = level.cells; + const pos1 = cells[current].hex.toWorld(); + const pos2 = cells[next].hex.toWorld(); + const myDir = pos2.subtract(myPos); + const myLen = myDir.magnitude(); + const hexDir = pos2.subtract(pos1); + const hexLen = hexDir.magnitude(); + if (myLen > hexLen) { + this.path.unshift(this.path[0]); + } + + this.direction = myDir.multiplyScalar(1 / myLen); + } +} diff --git a/src/app/components/game/sim/SimLevel.ts b/src/app/components/game/sim/SimLevel.ts new file mode 100644 index 0000000..5826db2 --- /dev/null +++ b/src/app/components/game/sim/SimLevel.ts @@ -0,0 +1,181 @@ +import { GdRoot } from "../data/GdRoot"; +import { EDirection } from "../util/EDirection"; +import { Hex } from "../util/Hex"; +import { PathFinding } from "../util/PathFinding"; +import { ECellType } from "./ECellType"; +import { SimCell } from "./SimCell"; +import { SimEnemy } from "./SimEnemy"; +import { SimProjectile } from "./SimProjectile"; + +export class SimLevel { + paused: boolean = true; + currentStep: number = -1; + highlightedIndex: number = -1; + currency: number = -1; + stride: number = -1; + cells: SimCell[] = []; + enemies: SimEnemy[] = []; + projectiles: SimProjectile[] = []; + nextWaveStep: number = -1; + lastEnemySpawnStep: number = 0; + currentWave: number = -1; + enemiesLeftToSpawn: number = 0; + index: number = 0; + radius: number = 0; + + constructor(gdRoot: GdRoot, levelIdx: number) { + const gdLevel = gdRoot.levels[levelIdx]; + this.index = levelIdx; + this.currency = gdLevel.currency; + this.enemiesLeftToSpawn = gdLevel.waves[0].amount; + + this.radius = gdLevel.radius; + this.stride = 2 * this.radius + 1; + const h0 = new Hex(0, 0); + for (let y = -this.radius; y <= this.radius; ++y) { + for (let x = -this.radius; x <= this.radius; ++x) { + const hex = new Hex(x, y); + const distance = Hex.distance(hex, h0); + const type = distance >= this.radius ? ECellType.Blocked : ECellType.Free; + const cellIndex = this.getCellIndex(hex); + this.cells[cellIndex] = new SimCell(hex, distance, type, cellIndex); + } + } + + gdLevel.walls.forEach((wall: Hex) => { + const cellIndex = this.getCellIndex(wall); + this.cells[cellIndex].type = ECellType.Blocked; + }); + + gdLevel.enemySpawns.forEach((hex: Hex) => { + const cellIndex = this.getCellIndex(hex); + this.cells[cellIndex].type = ECellType.Entry; + }); + + gdLevel.enemyTargets.forEach((hex: Hex) => { + const cellIndex = this.getCellIndex(hex); + this.cells[cellIndex].type = ECellType.Entry; + }); + + this.cells.forEach((cell: SimCell) => { + this.updateBlockedType(cell); + }); + + this.updatePaths(gdRoot); + }; + + public getCellIndex(hex: Hex) { + const x = hex.col + this.radius; + const y = hex.row + this.radius; + if (x < 0 || x >= this.stride || y < 0 || y >= this.stride) { + return -1; + } + return y * this.stride + x; + } + + public getNeighbourCell(cell: SimCell, direction: EDirection) { + const hex = cell.hex; + const neighbourHex = Hex.neighbour(hex, direction); + const neighbourIndex = this.getCellIndex(neighbourHex); + return this.cells[neighbourIndex]; + } + + public updateBlockedType(cell: SimCell) { + if (cell.type == ECellType.Free) { + cell.blockedType = -1; + return; + } + + if (cell.type == ECellType.Entry && cell.blockedType != -1) { + return; + } + + let blockedType = 0; + for (let direction = 0; direction < 6; ++direction) { + const neighbourCell = this.getNeighbourCell(cell, direction); + if (!!neighbourCell && neighbourCell.type == ECellType.Free) { + blockedType |= 1 << direction; + } + } + + cell.blockedType = blockedType; + } + + public reservePaths(gdRoot: GdRoot, hexToBlock: Hex | null): boolean { + try { + if (!!hexToBlock) { + const reservedCell = this.cells[this.getCellIndex(hexToBlock)]; + if (reservedCell.type != ECellType.Free || !!reservedCell.tower) + return false; + + reservedCell.type = ECellType.Reserved; + } + + return this.updatePaths(gdRoot); + } + finally { + if (!!hexToBlock) { + const reservedCell = this.cells[this.getCellIndex(hexToBlock)]; + if (reservedCell.type == ECellType.Reserved) + reservedCell.type = ECellType.Free + } + } + } + + public updatePaths(gdRoot: GdRoot): boolean { + const newRoutePaths: number[][] = []; + const newEnemyPaths: number[][] = []; + const gdLevel = gdRoot.levels[this.index]; + let invalid = false; + + for (const routeIdx in gdLevel.enemyRoutes) { + const route = gdLevel.enemyRoutes[routeIdx]; + const enemySpawnHex = gdLevel.enemySpawns[route[0]]; + const enemySpawnCell = this.cells[this.getCellIndex(enemySpawnHex)]; + const enemyTargetHex = gdLevel.enemyTargets[route[1]]; + const enemyTargetCell = this.cells[this.getCellIndex(enemyTargetHex)]; + const path = PathFinding.bfs(this, enemySpawnCell.index, enemyTargetCell.index); + if (path == null) { + invalid = true; + break; + } + newRoutePaths[routeIdx] = path; + } + + if (invalid) { + return false; + } + + for (let idx in this.enemies) { + const simEnemy = this.enemies[idx]; + const hex = Hex.fromWorld(simEnemy.position); + const startIndex = this.getCellIndex(hex); + const endIndex = this.getCellIndex(simEnemy.endHex); + const path = PathFinding.bfs(this, startIndex, endIndex); + if (path == null) { + invalid = true; + break; + } + newEnemyPaths[idx] = path; + } + + if (invalid) { + return false; + } + + for (const routeIdx in gdLevel.enemyRoutes) { + const route = gdLevel.enemyRoutes[routeIdx]; + const enemySpawnHex = gdLevel.enemySpawns[route[0]]; + const enemySpawnCell = this.cells[this.getCellIndex(enemySpawnHex)]; + enemySpawnCell.pathsToTarget[routeIdx] = newRoutePaths[routeIdx]; + } + + this.enemies.forEach((simEnemy: SimEnemy, idx: number) => { + simEnemy.path = newEnemyPaths[idx]; + simEnemy.currentPathIndex = 0; + simEnemy.onPathUpdated(this); + }); + + return true; + } +} diff --git a/src/app/components/game/sim/SimMain.ts b/src/app/components/game/sim/SimMain.ts new file mode 100644 index 0000000..55aaee2 --- /dev/null +++ b/src/app/components/game/sim/SimMain.ts @@ -0,0 +1,95 @@ +import { SplashComponent } from '../../splash/splash.component'; +import { GdRoot } from '../data/GdRoot'; +import { ISimAction } from './actions/ISimAction'; +import { SimActionFireTowers } from './actions/SimActionFireTowers'; +import { SimActionMoveEnemies } from './actions/SimActionMoveEnemies'; +import { SimActionMoveProjectiles } from './actions/SimActionMoveProjectiles'; +import { SimActionSpawnEnemies } from './actions/SimActionSpawnEnemies'; +import { SimCommand } from './commands/SimCommand'; +import { SimLevel } from './SimLevel'; + +export class SimMain { + executeUntilStep(targetStep: number) { + throw new Error("Method not implemented."); + } + lastTime = 0; + currentStep = -1; + isRunning = false; + lastStepTime: number = 0; + currentLevel: SimLevel | null = null; + commandHistory: SimCommand[] = []; + gdRoot: GdRoot = null!; + interval: number = 0; + private actions: ISimAction[] = []; + + constructor(gdRoot: GdRoot) { + this.setGdRoot(gdRoot); + this.actions.push(new SimActionMoveEnemies()); + this.actions.push(new SimActionSpawnEnemies()); + this.actions.push(new SimActionFireTowers()); + this.actions.push(new SimActionMoveProjectiles()); + } + + setGdRoot(gdRoot: GdRoot) { + this.gdRoot = gdRoot; + this.interval = 1.0 / this.gdRoot.simulation.stepsPerSecond; + } + + fastForwardToEnd() { + this.fastForwardTo( + this.commandHistory[this.commandHistory.length - 1].step + ); + } + + private fastForwardTo(target: number) { + this.currentStep = 0; + while (this.currentStep < target) { + this.step(); + } + } + + step() { + const nextStep = this.currentStep + 1; + + // Get all commands for this step + const commands = this.commandHistory.filter((c) => c.step === nextStep); + + this.currentStep = nextStep; + for (const action of this.actions) { + action.execute(this); + } + + for (const cmd of commands) { + cmd.execute(this); + } + + this.lastStepTime = performance.now(); + } + + start(now: number) { + if (this.isRunning) return; + + this.isRunning = true; + this.lastTime = now; + } + + stop() { + this.isRunning = false; + } + + addCommand(command: SimCommand) { + this.commandHistory = this.commandHistory.filter( + (c) => c.step < command.step + ); + this.commandHistory.push(command); + } + + update = (now: number) => { + if (!this.isRunning) return; + + while (now - this.lastTime >= this.interval) { + this.step(); + this.lastTime += this.interval; + } + }; +} diff --git a/src/app/components/game/sim/SimProjectile.ts b/src/app/components/game/sim/SimProjectile.ts new file mode 100644 index 0000000..de4c7d2 --- /dev/null +++ b/src/app/components/game/sim/SimProjectile.ts @@ -0,0 +1,24 @@ +import { GdProjectileEffect } from "../data/GdProjectileEffect"; +import { Vector2 } from "../util/Vector2"; + +export class SimProjectile { + index: number; + prevPosition: Vector2; + position: Vector2; + targetEnemyIdx: number; + spawnStep: number; + size: number; + dead: boolean; + projectileEffect: GdProjectileEffect; + + constructor(spawnStep: number, index: number, position: Vector2, targetEnemyIdx: number, size: number, projectileEffect: GdProjectileEffect) { + this.index = index; + this.position = position.clone(); + this.prevPosition = position.clone(); + this.targetEnemyIdx = targetEnemyIdx; + this.spawnStep = spawnStep; + this.size = size; + this.projectileEffect = projectileEffect; + this.dead = false; + } +} \ No newline at end of file diff --git a/src/app/components/game/sim/SimTower.ts b/src/app/components/game/sim/SimTower.ts new file mode 100644 index 0000000..b5e4115 --- /dev/null +++ b/src/app/components/game/sim/SimTower.ts @@ -0,0 +1,91 @@ +import { GdProjectileEffect } from "../data/GdProjectileEffect"; +import { Vector2 } from "../util/Vector2"; +import { SimLevel } from "./SimLevel"; +import { SimProjectile } from "./SimProjectile"; + +export class SimTower { + lastProjectileStep: number = -1; + lastAoeStep: number = -1; + projectileEffect: GdProjectileEffect | null = null; + projectileRange: number = -1; + projectileRate: number = -1; + projectileSize: number = -1; + aoeEffect: number = -1; + aoeRange: number = -1; + aoeRate: number = -1; + position: Vector2 = new Vector2(0, 0); + currentProjectileTarget: number = -1; + index: number = -1; + + constructor(index: number, position: Vector2) { + this.index = index; + this.position = position; + } + + public fireIfAble(level: SimLevel) { + const currentStep = level.currentStep; + if (this.canFireProjectile(level) && !!this.projectileEffect) { + level.projectiles.push(new SimProjectile(currentStep, level.projectiles.length, this.position, this.pickProjectileTarget(level), this.projectileSize, this.projectileEffect)); + this.lastProjectileStep = currentStep; + } + if (this.canFireAoe(level)) { + // TODO + } + } + + private canFireProjectile(level: SimLevel) { + if (!this.projectileEffect) + return false; + + const currentStep = level.currentStep; + if (this.lastProjectileStep == -1 || this.lastProjectileStep + this.projectileRate < currentStep) { + return this.isEnemyInRange(level, this.projectileRange); + } + return false; + } + + private canFireAoe(level: SimLevel) { + const currentStep = level.currentStep; + if (this.aoeEffect != null) { + if (this.lastAoeStep == -1 || this.lastAoeStep + this.aoeRate < currentStep) { + return this.isEnemyInRange(level, this.aoeRange); + } + } + return false; + } + + private pickProjectileTarget(level: SimLevel) { + if (this.currentProjectileTarget != -1) { + return this.currentProjectileTarget; + } + + let minLength = -1; + let candidate = -1; + for (let idx = 0; idx < level.enemies.length; idx++) { + const enemy = level.enemies[idx]; + const delta = enemy.currentPathPosition.subtract(this.position); + const length = delta.magnitude(); + if (enemy.path != null && length < this.projectileRange) { + const pathLength = enemy.path.length - enemy.currentPathIndex; + if (minLength == -1 || pathLength < minLength) { + candidate = idx; + minLength = pathLength; + } + } + } + + return candidate; + } + + private isEnemyInRange(level: SimLevel, range: number) { + for (const idx in level.enemies) { + const enemy = level.enemies[idx]; + const delta = enemy.currentPathPosition.subtract(this.position); + const length = delta.magnitude(); + if (length < range) { + return true; + } + } + return false; + } +} diff --git a/src/app/components/game/sim/actions/ISimAction.ts b/src/app/components/game/sim/actions/ISimAction.ts new file mode 100644 index 0000000..64c4d1c --- /dev/null +++ b/src/app/components/game/sim/actions/ISimAction.ts @@ -0,0 +1,5 @@ +import { SimMain } from "../SimMain"; + +export interface ISimAction { + execute(simMain: SimMain): void; +} diff --git a/src/app/components/game/sim/actions/SimActionFireTowers.ts b/src/app/components/game/sim/actions/SimActionFireTowers.ts new file mode 100644 index 0000000..8623468 --- /dev/null +++ b/src/app/components/game/sim/actions/SimActionFireTowers.ts @@ -0,0 +1,16 @@ +import { SimCell } from "../SimCell"; +import { SimMain } from "../SimMain"; +import { ISimAction } from "./ISimAction"; + +export class SimActionFireTowers implements ISimAction { + public execute(simMain: SimMain) { + const level = simMain.currentLevel; + if (!level) return; + + level.cells.forEach((simCell: SimCell) => { + if (simCell.tower != null) { + simCell.tower.fireIfAble(level); + } + }); + } +} diff --git a/src/app/components/game/sim/actions/SimActionMoveEnemies.ts b/src/app/components/game/sim/actions/SimActionMoveEnemies.ts new file mode 100644 index 0000000..cfe8ab4 --- /dev/null +++ b/src/app/components/game/sim/actions/SimActionMoveEnemies.ts @@ -0,0 +1,43 @@ +import { Hex } from "../../util/Hex"; +import { Vector2 } from "../../util/Vector2"; +import { SimEnemy } from "../SimEnemy"; +import { SimMain } from "../SimMain"; +import { ISimAction } from "./ISimAction"; + +export class SimActionMoveEnemies implements ISimAction { + public execute(simMain: SimMain) { + const level = simMain.currentLevel; + if (!level) return; + + const deadEnemies: number[] = []; + level.enemies.forEach((enemy: SimEnemy, idx: number) => { + if (enemy.dead) + deadEnemies.push(idx); + }); + + for (const idx of deadEnemies) { + level.enemies.splice(idx, 1); + } + + for (const simEnemy of level.enemies) { + const duration = simEnemy.speed / simMain.gdRoot.simulation.stepsPerSecond; + const t = (level.currentStep - simEnemy.currentPathStep) * duration; + const path2 = level.cells[simEnemy.path[simEnemy.currentPathIndex + 1]]; + if (!path2) { + simEnemy.dead = true; + return; + } + + const hex2 = path2.hex; + const pos1 = simEnemy.currentPathPosition; + const pos2 = Hex.toWorld(hex2); + simEnemy.prevPosition = simEnemy.position; + simEnemy.position = Vector2.lerp(pos1, pos2, t); + if (t >= 1) { + simEnemy.currentPathIndex += 1; + simEnemy.currentPathStep = level.currentStep; + simEnemy.onPathUpdated(level); + } + } + } +} diff --git a/src/app/components/game/sim/actions/SimActionMoveProjectiles.ts b/src/app/components/game/sim/actions/SimActionMoveProjectiles.ts new file mode 100644 index 0000000..4b6037b --- /dev/null +++ b/src/app/components/game/sim/actions/SimActionMoveProjectiles.ts @@ -0,0 +1,41 @@ +import { SimMain } from "../SimMain"; +import { SimProjectile } from "../SimProjectile"; +import { ISimAction } from "./ISimAction"; + +export class SimActionMoveProjectiles implements ISimAction { + public execute(simMain: SimMain) { + const level = simMain.currentLevel; + if (!level) return; + + const deadProjectiles: number[] = []; + level.projectiles.forEach((simProjectile: SimProjectile, idx: number) => { + if (simProjectile.dead) + deadProjectiles.push(idx); + }); + + for (const idx of deadProjectiles) { + level.projectiles.splice(idx, 1); + } + + level.projectiles.forEach((simProjectile: SimProjectile) => { + const target = level.enemies[simProjectile.targetEnemyIdx]; + if (!target || target.dead) { + simProjectile.dead = true; + return; + } + + const pos1 = simProjectile.position; + const pos2 = target.position; + const dir = pos2.subtract(pos1); + const len = dir.magnitude(); + let duration = simProjectile.projectileEffect.speed / simMain.gdRoot.simulation.stepsPerSecond; + if (len < duration) { + duration = len; + simProjectile.dead = true; + target.suffer(level, simProjectile.projectileEffect); + } + simProjectile.prevPosition = simProjectile.position; + simProjectile.position = pos1.add(dir.normalized().multiplyScalar(duration)); + }); + } +} diff --git a/src/app/components/game/sim/actions/SimActionSpawnEnemies.ts b/src/app/components/game/sim/actions/SimActionSpawnEnemies.ts new file mode 100644 index 0000000..1d68dff --- /dev/null +++ b/src/app/components/game/sim/actions/SimActionSpawnEnemies.ts @@ -0,0 +1,39 @@ +import { EEnemySize } from "../../data/EEnemySize"; +import { SimCommandStartNextWave } from "../commands/SimCommandStartNextWave"; +import { SimEnemy } from "../SimEnemy"; +import { SimMain } from "../SimMain"; +import { ISimAction } from "./ISimAction"; + +export class SimActionSpawnEnemies implements ISimAction { + public execute(simMain: SimMain) { + const level = simMain.currentLevel; + if (!level) return; + + const gdLevel = simMain.gdRoot.levels[level.index]; + const step = level.currentStep; + const gdWave = gdLevel.waves[level.currentWave]; + let spawnDelay = simMain.gdRoot.simulation.spawnDelay; + switch (gdWave.size) { + case EEnemySize.Huge: + spawnDelay *= 2; + break; + case EEnemySize.Tiny: + spawnDelay *= 0.5; + break; + } + + if (level.enemiesLeftToSpawn > 0 && level.lastEnemySpawnStep + spawnDelay * simMain.gdRoot.simulation.stepsPerSecond <= level.currentStep) { + level.enemiesLeftToSpawn -= 1; + level.lastEnemySpawnStep = step; + + const route = Math.floor(Math.random() * gdLevel.enemyRoutes.length); + const enemy = new SimEnemy(gdWave.enemy, route); + enemy.onPathUpdated(level); + level.enemies.push(enemy); + } + + if (level.nextWaveStep == step && !!gdLevel.waves[level.currentWave + 1]) { + simMain.addCommand(new SimCommandStartNextWave()); + } + } +} diff --git a/src/app/components/game/sim/commands/SimCommand.ts b/src/app/components/game/sim/commands/SimCommand.ts new file mode 100644 index 0000000..3325455 --- /dev/null +++ b/src/app/components/game/sim/commands/SimCommand.ts @@ -0,0 +1,6 @@ +import { SimMain } from "../SimMain"; + +export abstract class SimCommand { + step: number = -1; + abstract execute(simMain: SimMain): void; +} diff --git a/src/app/components/game/sim/commands/SimCommandBlockTerrain.ts b/src/app/components/game/sim/commands/SimCommandBlockTerrain.ts new file mode 100644 index 0000000..4613ad1 --- /dev/null +++ b/src/app/components/game/sim/commands/SimCommandBlockTerrain.ts @@ -0,0 +1,32 @@ +import { Hex } from "../../util/Hex"; +import { ECellType } from "../ECellType"; +import { SimMain } from "../SimMain"; +import { SimCommand } from "./SimCommand"; + +export class SimCommandBlockTerrain extends SimCommand { + private hex: Hex; + + constructor(hex: Hex) { + super(); + this.hex = hex; + } + + public execute(simMain: SimMain) { + const level = simMain.currentLevel; + if (!level) return; + + const cellIndex = level.getCellIndex(this.hex); + const cell = level.cells[cellIndex]; + if (cell.type != ECellType.Free || cell.tower != null || !level.reservePaths(simMain.gdRoot, this.hex)) { + return; + } + cell.type = ECellType.Blocked; + level.updateBlockedType(cell); + for (let i = 0; i < 6; ++i) { + const neighbourHex = Hex.neighbour(cell.hex, i); + const neighbourIndex = level.getCellIndex(neighbourHex); + level.updateBlockedType(level.cells[neighbourIndex]); + } + level.updatePaths(simMain.gdRoot); + } +} diff --git a/src/app/components/game/sim/commands/SimCommandCreateTower.ts b/src/app/components/game/sim/commands/SimCommandCreateTower.ts new file mode 100644 index 0000000..0a8b24b --- /dev/null +++ b/src/app/components/game/sim/commands/SimCommandCreateTower.ts @@ -0,0 +1,54 @@ +import { Hex } from "../../util/Hex"; +import { ECellType } from "../ECellType"; +import { SimEnemy } from "../SimEnemy"; +import { SimMain } from "../SimMain"; +import { SimTower } from "../SimTower"; +import { SimCommand } from "./SimCommand"; + +export class SimCommandCreateTower extends SimCommand { + hex: Hex; + index: number; + + constructor(hex: Hex, index: number) { + super(); + this.hex = hex; + this.index = index; + } + + public execute(simMain: SimMain) { + const level = simMain.currentLevel; + if (!level) { + return; + } + + const cellIndex = level.getCellIndex(this.hex); + const cell = level.cells[cellIndex]; + const towerData = simMain.gdRoot.towers[this.index]; + + if (cell.type != ECellType.Free) { + return; + } + + if (cell.tower != null) { + return; + } + + level.enemies.forEach((enemy: SimEnemy) => { + const hex = Hex.fromWorld(enemy.position); + const enemyCellIndex = level.getCellIndex(hex); + if (cellIndex == enemyCellIndex) { + return; + } + }); + + if (level.currency < towerData.cost) { + return; + } + + if (level.reservePaths(simMain.gdRoot, this.hex)) { + level.currency -= towerData.cost; + cell.tower = new SimTower(this.index, Hex.toWorld(this.hex)); + level.updatePaths(simMain.gdRoot); + } + } +} diff --git a/src/app/components/game/sim/commands/SimCommandStartNextWave.ts b/src/app/components/game/sim/commands/SimCommandStartNextWave.ts new file mode 100644 index 0000000..d004d4d --- /dev/null +++ b/src/app/components/game/sim/commands/SimCommandStartNextWave.ts @@ -0,0 +1,22 @@ +import { GdRoot } from "../../data/GdRoot"; +import { SimLevel } from "../SimLevel"; +import { SimMain } from "../SimMain"; +import { SimCommand } from "./SimCommand"; + +export class SimCommandStartNextWave extends SimCommand { + + execute(simMain: SimMain) { + const level = simMain.currentLevel; + if (!level) return; + + const data = simMain.gdRoot.levels[level.index]; + level.currentWave += 1; + if (!data.waves[level.currentWave]) { + level.paused = true; + return; + } + level.nextWaveStep = level.currentStep + simMain.gdRoot.simulation.waveDuration * simMain.gdRoot.simulation.stepsPerSecond - 1; + level.lastEnemySpawnStep = level.currentStep; + level.enemiesLeftToSpawn = data.waves[level.currentWave].amount; + } +} diff --git a/src/app/components/game/simulationEngine.ts b/src/app/components/game/simulationEngine.ts deleted file mode 100644 index bbe232f..0000000 --- a/src/app/components/game/simulationEngine.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Command } from './command'; -import { GameData } from './gameData'; -import { GameRules } from './gameRules'; -import { GameState } from './gameState'; - -export class SimulationEngine { - interval: number = 50; // ms per step - lastTime = 0; - private frameRequestId: number | null = null; - private data: GameData; - private previousState: GameState | null = null; - - currentStep = 0; - initialState: GameState; - state: GameState; - isRunning = false; - lastStepTime: number = 0; - - constructor( - initialState: GameState, - private rules: typeof GameRules, - initialData: GameData - ) { - this.initialState = initialState; - this.state = this.cloneState(this.initialState); - this.data = initialData; - } - - setGameData(newData: GameData) { - this.data = newData; - } - - reinitializeWithNewData(data: GameData) { - this.setGameData(data); - const rewound = JSON.parse(JSON.stringify(this.initialState)); - rewound.tick = this.state.tick; - rewound.commandHistory = this.state.commandHistory.filter( - (c) => c.step <= rewound.tick - ); - this.state = rewound; - this.fastForwardTo(this.currentStep); - } - - fastForwardToEnd() { - this.fastForwardTo( - this.state.commandHistory[this.state.commandHistory.length - 1].step - ); - } - - private fastForwardTo(target: number) { - this.state.tick = 0; - while (this.state.tick < target) { - this.step(); - } - } - - step() { - this.previousState = this.cloneState(this.state); - this.rules.step(this.state, this.data); - this.currentStep = this.state.tick; - this.lastStepTime = performance.now(); - } - - start() { - if (this.isRunning) return; - this.isRunning = true; - this.lastTime = performance.now(); - this.loop(); - } - - stop() { - this.isRunning = false; - if (this.frameRequestId) { - cancelAnimationFrame(this.frameRequestId); - this.frameRequestId = null; - } - } - - rewindToZero() { - this.stop(); - this.state = this.cloneState(this.initialState); - this.previousState = this.cloneState(this.state); - this.currentStep = 0; - } - - issueCommand(command: Command) { - // Purge future commands - this.state.commandHistory = this.state.commandHistory.filter( - (c) => c.step < command.step - ); - this.state.commandHistory.push(command); - } - - getRenderStates(): [GameState, GameState] { - return [this.previousState ?? this.state, this.state]; - } - - cloneState(state: GameState): GameState { - return { - tick: state.tick, - commandHistory: [...state.commandHistory], - units: state.units.map((u) => ({ ...u })), - }; - } - - private loop = () => { - const now = performance.now(); - while (now - this.lastTime >= this.interval) { - this.step(); - this.lastTime += this.interval; - } - if (this.isRunning) { - this.frameRequestId = requestAnimationFrame(this.loop); - } - }; -} diff --git a/src/app/components/game/util/EDirection.js b/src/app/components/game/util/EDirection.js new file mode 100644 index 0000000..e5aa42d --- /dev/null +++ b/src/app/components/game/util/EDirection.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.EDirection = void 0; +var EDirection; +(function (EDirection) { + EDirection[EDirection["Right"] = 0] = "Right"; + EDirection[EDirection["TopRight"] = 1] = "TopRight"; + EDirection[EDirection["TopLeft"] = 2] = "TopLeft"; + EDirection[EDirection["Left"] = 3] = "Left"; + EDirection[EDirection["BottomLeft"] = 4] = "BottomLeft"; + EDirection[EDirection["BottomRight"] = 5] = "BottomRight"; +})(EDirection || (exports.EDirection = EDirection = {})); diff --git a/src/app/components/game/util/EDirection.ts b/src/app/components/game/util/EDirection.ts new file mode 100644 index 0000000..90824ff --- /dev/null +++ b/src/app/components/game/util/EDirection.ts @@ -0,0 +1,8 @@ +export enum EDirection { + Right = 0, + TopRight = 1, + TopLeft = 2, + Left = 3, + BottomLeft = 4, + BottomRight = 5 +} diff --git a/src/app/components/game/util/Hex.js b/src/app/components/game/util/Hex.js new file mode 100644 index 0000000..609d9a1 --- /dev/null +++ b/src/app/components/game/util/Hex.js @@ -0,0 +1,97 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Hex = void 0; +var Vector2_1 = require("./Vector2"); +var Cube = /** @class */ (function () { + function Cube(x, y, z) { + this.x = x; + this.y = y; + this.z = z; + } + return Cube; +}()); +var Hex = /** @class */ (function () { + function Hex(x, y) { + this.col = x; + this.row = y; + } + Hex.prototype.toWorld = function () { + return Hex.toWorld(this); + }; + Hex.prototype.toPixel = function (size) { + return Hex.toPixel(this, size); + }; + Hex.offsetDirections = [ + [ + new Hex(+1, 0), + new Hex(0, -1), + new Hex(-1, -1), + new Hex(-1, 0), + new Hex(-1, +1), + new Hex(0, +1) + ], + [ + new Hex(+1, 0), + new Hex(+1, -1), + new Hex(0, -1), + new Hex(-1, 0), + new Hex(0, +1), + new Hex(+1, +1), + new Hex(+1, +1) + ] + ]; + Hex.neighbour = function (hex, direction) { + var parity = hex.row & 1; + var dir = Hex.offsetDirections[parity][direction]; + return new Hex(hex.col + dir.col, hex.row + dir.row); + }; + Hex.distance = function (a, b) { + var ac = Hex.offsetToCube(a); + var bc = Hex.offsetToCube(b); + return Math.max(Math.abs(ac.x - bc.x), Math.abs(ac.y - bc.y), Math.abs(ac.z - bc.z)); + }; + Hex.offsetToCube = function (hex) { + var x = hex.col - (hex.row - (hex.row & 1)) / 2; + var z = hex.row; + var y = -x - z; + return new Cube(x, y, z); + }; + Hex.toWorld = function (hex) { + var x = Math.sqrt(3) * (hex.col + 0.5 * (hex.row & 1)); + var y = (3 / 2) * hex.row; + return new Vector2_1.Vector2(x, y); + }; + Hex.fromWorld = function (coord) { + var q = (coord.x * Math.sqrt(3)) / 3 - coord.y / 3; + var r = (coord.y * 2) / 3; + var cube = new Cube(q, -q - r, r); + var rx = Math.round(cube.x); + var ry = Math.round(cube.y); + var rz = Math.round(cube.z); + var xDiff = Math.abs(rx - cube.x); + var yDiff = Math.abs(ry - cube.y); + var zDiff = Math.abs(rz - cube.z); + if (xDiff > yDiff && xDiff > zDiff) { + rx = -ry - rz; + } + else if (yDiff > zDiff) { + ry = -rx - rz; + } + else { + rz = -rx - ry; + } + var rounded = new Cube(rx, ry, rz); + var col = rounded.x + (rounded.z - (rounded.z & 1)) / 2; + var row = rounded.z; + return new Hex(col, row); + }; + Hex.toPixel = function (hex, size) { + var w = Hex.toWorld(hex); + return new Vector2_1.Vector2(w.x * size, w.y * size); + }; + Hex.fromPixel = function (coord, size) { + return Hex.fromWorld(new Vector2_1.Vector2(coord.x / size, coord.y / size)); + }; + return Hex; +}()); +exports.Hex = Hex; diff --git a/src/app/components/game/util/Hex.ts b/src/app/components/game/util/Hex.ts new file mode 100644 index 0000000..567caf1 --- /dev/null +++ b/src/app/components/game/util/Hex.ts @@ -0,0 +1,116 @@ +import { EDirection } from "./EDirection"; +import { Vector2 } from "./Vector2"; + +class Cube { + x: number; + y: number; + z: number; + + constructor(x: number, y: number, z: number) { + this.x = x; + this.y = y; + this.z = z; + } +} + +export class Hex { + public col: number; + public row: number; + + constructor(x: number, y: number) { + this.col = x; + this.row = y; + } + + private static offsetDirections: Hex[][] = [ + [ + new Hex(+1, 0), + new Hex(0, -1), + new Hex(-1, -1), + new Hex(-1, 0), + new Hex(-1, +1), + new Hex(0, +1) + ], + [ + new Hex(+1, 0), + new Hex(+1, -1), + new Hex(0, -1), + new Hex(-1, 0), + new Hex(0, +1), + new Hex(+1, +1), + new Hex(+1, +1) + ] + ]; + + public static neighbour = (hex: Hex, direction: EDirection): Hex => { + const parity = hex.row & 1; + const dir = Hex.offsetDirections[parity][direction]; + return new Hex(hex.col + dir.col, hex.row + dir.row); + }; + + public static distance = (a: Hex, b: Hex): number => { + const ac = Hex.offsetToCube(a); + const bc = Hex.offsetToCube(b); + return Math.max( + Math.abs(ac.x - bc.x), + Math.abs(ac.y - bc.y), + Math.abs(ac.z - bc.z) + ); + }; + + private static offsetToCube = (hex: Hex): Cube => { + const x = hex.col - (hex.row - (hex.row & 1)) / 2; + const z = hex.row; + const y = -x - z; + return new Cube(x, y, z); + }; + + public static toWorld = (hex: Hex): Vector2 => { + const x = Math.sqrt(3) * (hex.col + 0.5 * (hex.row & 1)); + const y = (3 / 2) * hex.row; + return new Vector2(x, y); + }; + + public toWorld(): Vector2 { + return Hex.toWorld(this); + } + + public static fromWorld = (coord: Vector2): Hex => { + const q = (coord.x * Math.sqrt(3)) / 3 - coord.y / 3; + const r = (coord.y * 2) / 3; + const cube = new Cube(q, -q - r, r); + let rx = Math.round(cube.x); + let ry = Math.round(cube.y); + let rz = Math.round(cube.z); + + const xDiff = Math.abs(rx - cube.x); + const yDiff = Math.abs(ry - cube.y); + const zDiff = Math.abs(rz - cube.z); + + if (xDiff > yDiff && xDiff > zDiff) { + rx = -ry - rz; + } else if (yDiff > zDiff) { + ry = -rx - rz; + } else { + rz = -rx - ry; + } + + const rounded = new Cube(rx, ry, rz); + const col = rounded.x + (rounded.z - (rounded.z & 1)) / 2; + const row = rounded.z; + return new Hex(col, row); + }; + + public static toPixel = (hex: Hex, size: number): Vector2 => { + const w = Hex.toWorld(hex); + return new Vector2(w.x * size, w.y * size); + }; + + public toPixel(size: number): Vector2 { + return Hex.toPixel(this, size); + } + + public static fromPixel = (coord: Vector2, size: number): Hex => { + return Hex.fromWorld(new Vector2(coord.x / size, coord.y / size)); + }; +} diff --git a/src/app/components/game/util/PathFinding.ts b/src/app/components/game/util/PathFinding.ts new file mode 100644 index 0000000..d1b6196 --- /dev/null +++ b/src/app/components/game/util/PathFinding.ts @@ -0,0 +1,51 @@ +import { ECellType } from "../sim/ECellType"; +import { SimLevel } from "../sim/SimLevel"; +import { Hex } from "./Hex"; + +export abstract class PathFinding { + public static bfs(level: SimLevel, startIndex: number, endIndex: number): number[] | null { + const listToExplore: number[] = [startIndex]; + let cameFrom: number[] = new Array(level.cells.length); + cameFrom.fill(-1); + + while (listToExplore.length > 0) { + const nodeIndex = listToExplore.shift()!; + const cell = level.cells[nodeIndex]; + + for (let i = 0; i < 6; ++i) { + const neighbourHex = Hex.neighbour(cell.hex, i); + const neighbourIndex = level.getCellIndex(neighbourHex); + + if (neighbourIndex === -1) { + continue; + } + + const neighbourCell = level.cells[neighbourIndex]; + if (neighbourCell.type === ECellType.Blocked || neighbourCell.type === ECellType.Reserved || neighbourCell.tower !== null) { + continue; + } + + if (cameFrom[neighbourIndex] === -1) { + cameFrom[neighbourIndex] = nodeIndex; + + if (neighbourIndex !== endIndex) { + listToExplore.push(neighbourIndex); + } else { + let idx = neighbourIndex; + const path: number[] = [idx]; + + while (idx !== startIndex) { + const prev = cameFrom[idx]; + idx = prev; + path.unshift(idx); + } + + return path; + } + } + } + } + + return null; + } +} diff --git a/src/app/components/game/util/Vector2.js b/src/app/components/game/util/Vector2.js new file mode 100644 index 0000000..9d31bd8 --- /dev/null +++ b/src/app/components/game/util/Vector2.js @@ -0,0 +1,74 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Vector2 = void 0; +var Vector2 = /** @class */ (function () { + function Vector2(x, y) { + this.x = x; + this.y = y; + } + Vector2.lerp = function (a, b, t) { + return a.multiplyScalar(1 - t).add(b.multiplyScalar(t)); + }; + Vector2.prototype.add = function (vector) { + return new Vector2(this.x + vector.x, this.y + vector.y); + }; + Vector2.prototype.subtract = function (vector) { + return new Vector2(this.x - vector.x, this.y - vector.y); + }; + Vector2.prototype.multiplyScalar = function (scalar) { + return new Vector2(this.x * scalar, this.y * scalar); + }; + Vector2.prototype.dot = function (vector) { + return this.x * vector.x + this.y * vector.y; + }; + Vector2.prototype.cross = function (vector) { + return this.x * vector.y - this.y * vector.x; + }; + Vector2.prototype.magnitude = function () { + return Math.sqrt(this.x * this.x + this.y * this.y); + }; + Vector2.prototype.normalized = function () { + var magnitude = this.magnitude(); + if (magnitude === 0) { + throw new Error("Cannot normalize a vector with magnitude 0"); + } + return this.multiplyScalar(1 / magnitude); + }; + Vector2.prototype.distance = function (vector) { + return Math.sqrt(Math.pow((this.x - vector.x), 2) + Math.pow((this.y - vector.y), 2)); + }; + Vector2.prototype.distanceSquared = function (vector) { + return Math.pow((this.x - vector.x), 2) + Math.pow((this.y - vector.y), 2); + }; + Vector2.prototype.limit = function (max) { + var magnitude = this.magnitude(); + if (magnitude > max) { + return this.normalized().multiplyScalar(max); + } + return this; + }; + Vector2.prototype.angle = function () { + return Math.atan2(this.y, this.x); + }; + Vector2.prototype.angleBetween = function (vector) { + var dotProd = this.dot(vector); + var magnitudes = this.magnitude() * vector.magnitude(); + if (magnitudes === 0) { + throw new Error("Cannot calculate angle with a zero-magnitude vector"); + } + return Math.acos(dotProd / magnitudes); + }; + Vector2.prototype.clone = function () { + return new Vector2(this.x, this.y); + }; + Vector2.prototype.equals = function (vector) { + return this.x === vector.x && this.y === vector.y; + }; + Vector2.prototype.rotate = function (angle) { + var cos = Math.cos(angle); + var sin = Math.sin(angle); + return new Vector2(this.x * cos - this.y * sin, this.x * sin + this.y * cos); + }; + return Vector2; +}()); +exports.Vector2 = Vector2; diff --git a/src/app/components/game/util/Vector2.ts b/src/app/components/game/util/Vector2.ts new file mode 100644 index 0000000..7dcc58e --- /dev/null +++ b/src/app/components/game/util/Vector2.ts @@ -0,0 +1,91 @@ +export class Vector2 { + public x: number; + public y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + public static lerp(a: Vector2, b: Vector2, t: number) { + return a.multiplyScalar(1 - t).add(b.multiplyScalar(t)); + } + + public add(vector: Vector2): Vector2 { + return new Vector2(this.x + vector.x, this.y + vector.y); + } + + public subtract(vector: Vector2): Vector2 { + return new Vector2(this.x - vector.x, this.y - vector.y); + } + + public multiplyScalar(scalar: number): Vector2 { + return new Vector2(this.x * scalar, this.y * scalar); + } + + public dot(vector: Vector2): number { + return this.x * vector.x + this.y * vector.y; + } + + public cross(vector: Vector2): number { + return this.x * vector.y - this.y * vector.x; + } + + public magnitude(): number { + return Math.sqrt(this.x * this.x + this.y * this.y); + } + + public normalized(): Vector2 { + const magnitude = this.magnitude(); + if (magnitude === 0) { + throw new Error("Cannot normalize a vector with magnitude 0"); + } + return this.multiplyScalar(1 / magnitude); + } + + public distance(vector: Vector2): number { + return Math.sqrt((this.x - vector.x) ** 2 + (this.y - vector.y) ** 2); + } + + public distanceSquared(vector: Vector2): number { + return (this.x - vector.x) ** 2 + (this.y - vector.y) ** 2; + } + + public limit(max: number): Vector2 { + const magnitude = this.magnitude(); + if (magnitude > max) { + return this.normalized().multiplyScalar(max); + } + return this; + } + + public angle(): number { + return Math.atan2(this.y, this.x); + } + + public angleBetween(vector: Vector2): number { + const dotProd = this.dot(vector); + const magnitudes = this.magnitude() * vector.magnitude(); + if (magnitudes === 0) { + throw new Error("Cannot calculate angle with a zero-magnitude vector"); + } + return Math.acos(dotProd / magnitudes); + } + + public clone(): Vector2 { + return new Vector2(this.x, this.y); + } + + public equals(vector: Vector2): boolean { + return this.x === vector.x && this.y === vector.y; + } + + public rotate(angle: number): Vector2 { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + return new Vector2( + this.x * cos - this.y * sin, + this.x * sin + this.y * cos + ); + } +} diff --git a/src/app/components/splash/splash.component.ts b/src/app/components/splash/splash.component.ts index de80360..274ffdf 100644 --- a/src/app/components/splash/splash.component.ts +++ b/src/app/components/splash/splash.component.ts @@ -8,12 +8,16 @@ import { AssetPreloaderService } from '../../assetPreloaderService'; styleUrls: ['./splash.component.css'], }) export class SplashComponent { + static assetPreloader: AssetPreloaderService = new AssetPreloaderService(); show: boolean = true; - assetPreloader: AssetPreloaderService = new AssetPreloaderService(); - progress: number = 0; + progress: number = 0; constructor(private router: Router) { - this.assetPreloader.progress$.subscribe(p => this.progress = p); - this.assetPreloader.preload().then(() => this.router.navigate(['/menu'])); + SplashComponent.assetPreloader.progress$.subscribe( + (p) => (this.progress = p) + ); + SplashComponent.assetPreloader + .preload() + .then(() => this.router.navigate(['/menu'])); } }