diff --git a/.editorconfig b/.editorconfig index f166060..1727d87 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,11 +3,13 @@ root = true [*] charset = utf-8 -indent_style = space -indent_size = 2 +indent_style = tab insert_final_newline = true trim_trailing_whitespace = true +# Add this to help VSCode and Copilot use tabs for indentation +indent_size = tab + [*.ts] quote_type = single ij_typescript_use_double_quotes = false diff --git a/.vscode/settings.json b/.vscode/settings.json index 3e83928..734fd36 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,11 @@ { - "json.schemas": [ - { - "fileMatch": [ "src/assets/data/*.json" ], - "url": "./schemas/gdRoot.json" - } - ] + "json.schemas": [ + { + "fileMatch": ["src/assets/data/*.json"], + "url": "./schemas/gdRoot.json" + } + ], + "editor.insertSpaces": false, + "editor.detectIndentation": false, + "editor.tabSize": 4 } diff --git a/src/.prettierrc b/src/.prettierrc index dfcc0e9..9d8a85c 100644 --- a/src/.prettierrc +++ b/src/.prettierrc @@ -1,4 +1,4 @@ { - "tabWidth": 4, - "useTabs": true + "useTabs": true, + "printWidth": 9999 } diff --git a/src/app/components/game/game.component.html b/src/app/components/game/game.component.html index c3fe8f5..90af569 100644 --- a/src/app/components/game/game.component.html +++ b/src/app/components/game/game.component.html @@ -12,6 +12,7 @@
Step: {{ simMain.currentStep }}
+ diff --git a/src/app/components/game/game.component.ts b/src/app/components/game/game.component.ts index 0f5cf56..8e39565 100644 --- a/src/app/components/game/game.component.ts +++ b/src/app/components/game/game.component.ts @@ -5,6 +5,7 @@ import { GdRoot } from './data/GdRoot'; import { SimCommand } from './sim/commands/SimCommand'; import { VisMain } from './vis/VisMain'; import { SplashComponent } from '../splash/splash.component'; +import { SimCommandStartNextWave } from './sim/commands/SimCommandStartNextWave'; @Component({ selector: 'app-game', @@ -27,7 +28,6 @@ export class GameComponent { const gdRoot = await this.loadGdRoot(); this.simMain.setGdRoot(gdRoot); this.visMain = new VisMain(this.simMain, SplashComponent.assetPreloader, this.wrapperRef.nativeElement, this.canvasRef.nativeElement); - window.addEventListener('resize', _ => this.visMain.onResized()); } start() { @@ -35,15 +35,24 @@ export class GameComponent { } stop() { - this.visMain.start(); + this.visMain.stop(); } step() { this.simMain.step(); + this.visMain.onRender(); } rewind() { - //this.simMain.rewindToZero(); + if (this.simMain.currentLevel) + this.simMain.currentLevel.currentStep = -1; + + this.simMain.currentStep = -1; + } + + startNextWave() { + this.simMain.addCommand(new SimCommandStartNextWave()); + this.simMain.currentLevel!.paused = false; } fastForward() { diff --git a/src/app/components/game/sim/SimLevel.ts b/src/app/components/game/sim/SimLevel.ts index 5826db2..6fe9657 100644 --- a/src/app/components/game/sim/SimLevel.ts +++ b/src/app/components/game/sim/SimLevel.ts @@ -1,11 +1,11 @@ -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"; +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; @@ -23,159 +23,156 @@ export class SimLevel { 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; + 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); - } - } + 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.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.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; - }); + 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.cells.forEach((cell: SimCell) => { + this.updateBlockedType(cell); + }); - this.updatePaths(gdRoot); - }; + 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 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 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; - } + public updateBlockedType(cell: SimCell) { + if (cell.type == ECellType.Free) { + cell.blockedType = -1; + return; + } - if (cell.type == ECellType.Entry && 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; - } - } + 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; - } + 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; + 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; - } + 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 - } - } - } + 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; + 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; - } + 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; - } + 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; - } + 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; - } + 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]; - } + 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); - }); + this.enemies.forEach((simEnemy: SimEnemy, idx: number) => { + simEnemy.path = newEnemyPaths[idx]; + simEnemy.currentPathIndex = 0; + simEnemy.onPathUpdated(this); + }); - return true; - } + return true; + } } diff --git a/src/app/components/game/sim/SimMain.ts b/src/app/components/game/sim/SimMain.ts index 05992ea..f77cd24 100644 --- a/src/app/components/game/sim/SimMain.ts +++ b/src/app/components/game/sim/SimMain.ts @@ -36,29 +36,29 @@ export class SimMain { } executeUntilStep(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; + this.currentStep++; + if (this.currentLevel && !this.currentLevel.paused) { + this.currentLevel.currentStep++; + } + for (const action of this.actions) { action.execute(this); } + const commands = this.commandHistory.filter((c) => c.step === this.currentStep); for (const cmd of commands) { cmd.execute(this); } } addCommand(command: SimCommand) { + command.step = this.currentStep + 1; this.commandHistory = this.commandHistory.filter( (c) => c.step < command.step ); diff --git a/src/app/components/game/sim/actions/SimActionSpawnEnemies.ts b/src/app/components/game/sim/actions/SimActionSpawnEnemies.ts index 1d68dff..5284afe 100644 --- a/src/app/components/game/sim/actions/SimActionSpawnEnemies.ts +++ b/src/app/components/game/sim/actions/SimActionSpawnEnemies.ts @@ -1,39 +1,45 @@ -import { EEnemySize } from "../../data/EEnemySize"; -import { SimCommandStartNextWave } from "../commands/SimCommandStartNextWave"; -import { SimEnemy } from "../SimEnemy"; -import { SimMain } from "../SimMain"; -import { ISimAction } from "./ISimAction"; +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; + 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; - } + const gdLevel = simMain.gdRoot.levels[level.index]; + const step = level.currentStep; + const gdWave = gdLevel.waves[level.currentWave]; + if (!gdWave) { + return; + } + + 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; + 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); - } + 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()); - } - } + 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 index 3325455..e261e41 100644 --- a/src/app/components/game/sim/commands/SimCommand.ts +++ b/src/app/components/game/sim/commands/SimCommand.ts @@ -1,6 +1,7 @@ -import { SimMain } from "../SimMain"; +import { SimMain } from '../SimMain'; export abstract class SimCommand { - step: number = -1; - abstract execute(simMain: SimMain): void; + step: number = -1; + abstract execute(simMain: SimMain): void; + abstract check(simMain: SimMain): boolean; } diff --git a/src/app/components/game/sim/commands/SimCommandBlockTerrain.ts b/src/app/components/game/sim/commands/SimCommandBlockTerrain.ts index 4613ad1..5b871d9 100644 --- a/src/app/components/game/sim/commands/SimCommandBlockTerrain.ts +++ b/src/app/components/game/sim/commands/SimCommandBlockTerrain.ts @@ -1,32 +1,46 @@ -import { Hex } from "../../util/Hex"; -import { ECellType } from "../ECellType"; -import { SimMain } from "../SimMain"; -import { SimCommand } from "./SimCommand"; +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; + private hex: Hex; - constructor(hex: Hex) { - super(); - this.hex = hex; - } + constructor(hex: Hex) { + super(); + this.hex = hex; + } - public execute(simMain: SimMain) { - const level = simMain.currentLevel; - if (!level) return; + 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); - } + const cellIndex = level.getCellIndex(this.hex); + const cell = level.cells[cellIndex]; + 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); + } + + public check(simMain: SimMain): boolean { + const level = simMain.currentLevel; + if (!level) { + return false; + } + const cellIndex = level.getCellIndex(this.hex); + const cell = level.cells[cellIndex]; + if (!cell) { + return false; + } + + if (cell.type != ECellType.Free || cell.tower != null || !level.reservePaths(simMain.gdRoot, this.hex)) { + return false; + } + return true; + } } diff --git a/src/app/components/game/sim/commands/SimCommandCreateTower.ts b/src/app/components/game/sim/commands/SimCommandCreateTower.ts index 0a8b24b..85b9cf3 100644 --- a/src/app/components/game/sim/commands/SimCommandCreateTower.ts +++ b/src/app/components/game/sim/commands/SimCommandCreateTower.ts @@ -1,54 +1,75 @@ -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"; +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; + hex: Hex; + index: number; - constructor(hex: Hex, index: number) { - super(); - this.hex = hex; - this.index = index; - } + constructor(hex: Hex, index: number) { + super(); + this.hex = hex; + this.index = index; + } - public execute(simMain: SimMain) { - const level = simMain.currentLevel; - if (!level) { - return; - } + 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]; + const cellIndex = level.getCellIndex(this.hex); + const cell = level.cells[cellIndex]; + const towerData = simMain.gdRoot.towers[this.index]; - if (cell.type != ECellType.Free) { - return; - } + level.enemies.forEach((enemy: SimEnemy) => { + const hex = Hex.fromWorld(enemy.position); + const enemyCellIndex = level.getCellIndex(hex); + if (cellIndex == enemyCellIndex) { + return; + } + }); - if (cell.tower != null) { - return; - } + if (level.currency < towerData.cost) { + return; + } - level.enemies.forEach((enemy: SimEnemy) => { - const hex = Hex.fromWorld(enemy.position); - const enemyCellIndex = level.getCellIndex(hex); - if (cellIndex == enemyCellIndex) { - 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); + } + } - if (level.currency < towerData.cost) { - return; - } + public check(simMain: SimMain): boolean { + const level = simMain.currentLevel; + if (!level) { + return false; + } - 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); - } - } + const cellIndex = level.getCellIndex(this.hex); + const cell = level.cells[cellIndex]; + if (!cell) { + return false; + } + + const towerData = simMain.gdRoot.towers[this.index]; + + if (cell.type != ECellType.Free) { + return false; + } + + if (cell.tower != null) { + return false; + } + + if (level.currency < towerData.cost) { + return false; + } + + return true; + } } diff --git a/src/app/components/game/sim/commands/SimCommandStartNextWave.ts b/src/app/components/game/sim/commands/SimCommandStartNextWave.ts index d004d4d..cd2c87c 100644 --- a/src/app/components/game/sim/commands/SimCommandStartNextWave.ts +++ b/src/app/components/game/sim/commands/SimCommandStartNextWave.ts @@ -1,22 +1,29 @@ -import { GdRoot } from "../../data/GdRoot"; -import { SimLevel } from "../SimLevel"; -import { SimMain } from "../SimMain"; -import { SimCommand } from "./SimCommand"; +import { SimMain } from '../SimMain'; +import { SimCommand } from './SimCommand'; export class SimCommandStartNextWave extends SimCommand { + public execute(simMain: SimMain) { + const level = simMain.currentLevel; + if (!level) { + return; + } - 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; + } - 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; - } + public check(simMain: SimMain): boolean { + const level = simMain.currentLevel; + if (!level) { + return false; + } + return true; + } } diff --git a/src/app/components/game/vis/VisLevel.ts b/src/app/components/game/vis/VisLevel.ts index 5c28b90..2ca1313 100644 --- a/src/app/components/game/vis/VisLevel.ts +++ b/src/app/components/game/vis/VisLevel.ts @@ -27,6 +27,7 @@ export class VisLevel { private simMain: SimMain; private gdRoot: GdRoot; assets: AssetPreloaderService; + private hoveredHex: Hex | null = null; constructor(visMain: VisMain, simMain: SimMain, gdRoot: GdRoot, assets: AssetPreloaderService) { this.assets = assets; @@ -60,6 +61,15 @@ export class VisLevel { this.drawBackground(); ctx.globalCompositeOperation = "source-over"; + // Highlight hovered cell first (under everything else) + if (this.hoveredHex) { + const hoveredIdx = simLevel.getCellIndex(this.hoveredHex); + const hoveredCell = simLevel.cells[hoveredIdx]; + if (hoveredCell && hoveredCell.distance <= gdLevel.radius) { + this.drawCellImage(ctx, hoveredCell, "cell-highlighted.svg"); + } + } + simLevel.cells.forEach((cell: SimCell) => { if (cell.distance > gdLevel.radius) { return; @@ -161,6 +171,10 @@ export class VisLevel { this.lastStep = currentStep; } + public setHoveredHex(hex: Hex | null) { + this.hoveredHex = hex; + } + public getScreenCoords(hex: Hex): Vector2 { const coord = Hex.toPixel(hex, this.hexSize); return new Vector2(coord.x + this.screenXOffset - this.screenCellWidth / 2, coord.y + this.screenYOffset - this.screenCellHeight / 2); diff --git a/src/app/components/game/vis/VisMain.ts b/src/app/components/game/vis/VisMain.ts index d62a364..553d8df 100644 --- a/src/app/components/game/vis/VisMain.ts +++ b/src/app/components/game/vis/VisMain.ts @@ -1,115 +1,166 @@ -import { AssetPreloaderService } from "../../../assetPreloaderService"; -import { SplashComponent } from "../../splash/splash.component"; -import { SimMain } from "../sim/SimMain"; -import { VisLevel } from "./VisLevel"; +import { AssetPreloaderService } from '../../../assetPreloaderService'; +import { SimMain } from '../sim/SimMain'; +import { VisLevel } from './VisLevel'; +import { Vector2 } from '../util/Vector2'; +import { InputHandler } from './input/InputHandler'; +import { Hex } from '../util/Hex'; +import { DefaultInputHandler } from './input/DefaultInputHandler'; export class VisMain { - canvas: HTMLCanvasElement; - context: CanvasRenderingContext2D; - wallPattern: CanvasPattern; - visLevel: VisLevel; - simMain: SimMain; - private startTimestamp: number = 0; - private active: boolean = true; - private ready: boolean = false; - private gap: number = 0; - assets: AssetPreloaderService; - wrapper: HTMLDivElement; + canvas: HTMLCanvasElement; + context: CanvasRenderingContext2D; + wallPattern: CanvasPattern; + visLevel: VisLevel; + simMain: SimMain; + active: boolean = true; + assets: AssetPreloaderService; + wrapper: HTMLDivElement; + inputHandlers: InputHandler[] = []; + private startTimestamp: number = 0; + private ready: boolean = false; + private gap: number = 0; - constructor(simMain: SimMain, assets: AssetPreloaderService, wrapper: HTMLDivElement, canvas: HTMLCanvasElement) { - this.assets = assets; - this.simMain = simMain; - this.canvas = canvas; - this.wrapper = wrapper; - this.context = this.canvas.getContext("2d")!; - this.context.globalCompositeOperation = "source-over"; - this.wallPattern = this.createPattern(assets.getImage("images/wall.png"), 48); - this.visLevel = new VisLevel(this, this.simMain, this.simMain.gdRoot, assets); - this.start(); - } + constructor(simMain: SimMain, assets: AssetPreloaderService, wrapper: HTMLDivElement, canvas: HTMLCanvasElement) { + this.assets = assets; + this.simMain = simMain; + this.canvas = canvas; + this.wrapper = wrapper; + this.context = this.canvas.getContext('2d')!; + this.context.globalCompositeOperation = 'source-over'; + this.wallPattern = this.createPattern(assets.getImage('images/wall.png'), 48); + this.visLevel = new VisLevel(this, this.simMain, this.simMain.gdRoot, assets); + this.setupInput(); + this.start(); + this.inputHandlers.push(new DefaultInputHandler(this)); + } - private createPattern(image: HTMLImageElement, size: number): CanvasPattern { - const tempCanvas = document.createElement("canvas"); - const tempContext = tempCanvas.getContext("2d")!; + private createPattern(image: HTMLImageElement, size: number): CanvasPattern { + const tempCanvas = document.createElement('canvas'); + const tempContext = tempCanvas.getContext('2d')!; - tempCanvas.width = size; - tempCanvas.height = size; - tempContext.drawImage(image, 0, 0, image.width, image.height, 0, 0, size, size); + tempCanvas.width = size; + tempCanvas.height = size; + tempContext.drawImage(image, 0, 0, image.width, image.height, 0, 0, size, size); - return this.context.createPattern(tempCanvas, 'repeat')!; - } + return this.context.createPattern(tempCanvas, 'repeat')!; + } - private step(timestamp: number) { - if (!this.active) { - return; - } + private step(timestamp: number) { + if (!this.active) { + return; + } - requestAnimationFrame((timestamp: number) => { - this.step(timestamp); - }); + requestAnimationFrame((timestamp: number) => { + this.step(timestamp); + }); - if (!this.startTimestamp) { - this.startTimestamp = timestamp; - } + const simLevel = this.simMain.currentLevel; + if (!simLevel) { + return; + } - const simLevel = this.simMain.currentLevel; - if (!simLevel) { - return; - } + let targetStep = Math.floor(((timestamp - this.startTimestamp) * this.simMain.gdRoot.simulation.stepsPerSecond) / 1000 - this.gap); + if (simLevel.paused) { + this.gap += targetStep - this.simMain.currentStep; + } + this.simMain.executeUntilStep(targetStep); + this.visLevel.updateEveryFrame(targetStep); + this.onRender(); + } - let targetStep = (timestamp - this.startTimestamp) * this.simMain.gdRoot.simulation.stepsPerSecond / 1000 - this.gap; - if (simLevel.paused) { - this.gap += targetStep - simLevel.currentStep; - targetStep = simLevel.currentStep; - } - this.simMain.executeUntilStep(targetStep); - this.visLevel.updateEveryFrame(targetStep); - this.onRender(); - }; + public onResized() { + // Always get the latest wrapper size + const width = this.wrapper.clientWidth; + const height = this.wrapper.clientHeight; + const ratio = window.devicePixelRatio; - public onResized() { - // Always get the latest wrapper size - const width = this.wrapper.clientWidth; - const height = this.wrapper.clientHeight; - const ratio = window.devicePixelRatio; + // Set canvas style to always fill the wrapper + this.canvas.width = width * ratio; + this.canvas.height = height * ratio; - // Set canvas style to always fill the wrapper - this.canvas.width = width * ratio; - this.canvas.height = height * ratio; + // Reset transform before scaling to avoid compounding + this.context.setTransform(1, 0, 0, 1, 0, 0); + this.context.scale(ratio, ratio); - // Reset transform before scaling to avoid compounding - this.context.setTransform(1, 0, 0, 1, 0, 0); - this.context.scale(ratio, ratio); + this.visLevel.updateSize(); + } - this.visLevel.updateSize(); - }; + onRender() { + this.clear(); + const ctx = this.context; + ctx.font = '12px Tahoma'; + const simLevel = this.simMain.currentLevel; + if (!!simLevel) { + if (!this.ready) { + this.onResized(); + this.ready = true; + } + this.visLevel.draw(); + } + } - private onRender() { - this.clear(); - const ctx = this.context; - ctx.font = "12px Tahoma"; - const simLevel = this.simMain.currentLevel; - if (!!simLevel) { - if (!this.ready) { - this.onResized(); - this.ready = true; - } - this.visLevel.draw(); - } - }; + private clear() { + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + } - private clear() { - this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); - } + stop() { + this.active = false; + } - stop() { - this.active = false; - } + start() { + this.active = true; + requestAnimationFrame((timestamp: number) => { + this.startTimestamp = timestamp - (this.simMain.currentStep / this.simMain.gdRoot.simulation.stepsPerSecond) * 1000; + this.step(timestamp); + }); + } - start() { - this.active = true; - requestAnimationFrame((timestamp: number) => { - this.step(timestamp); - }); - } + private setupInput() { + this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e)); + this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e)); + window.addEventListener('keydown', (e) => this.handleKeyDown(e)); + window.addEventListener('resize', (_) => this.onResized()); + } + + private getHexFromMouseCoords(event: MouseEvent): Hex { + const rect = this.canvas.getBoundingClientRect(); + const ratio = this.canvas.width / rect.width; + const x = ((event.clientX - rect.left) * ratio) / window.devicePixelRatio; + const y = ((event.clientY - rect.top) * ratio) / window.devicePixelRatio; + const coords = new Vector2(x, y); + const hex = this.visLevel.getHexFromScreenCoords(coords); + return hex; + } + + private handleMouseDown(event: MouseEvent) { + const hex = this.getHexFromMouseCoords(event); + for (let i = this.inputHandlers.length - 1; i >= 0; i--) { + const handled = this.inputHandlers[i].onMouseDown?.(event, hex); + if (handled) { + event.preventDefault(); + return; + } + } + } + + private handleMouseMove(event: MouseEvent) { + const hex = this.getHexFromMouseCoords(event); + for (let i = this.inputHandlers.length - 1; i >= 0; i--) { + const handled = this.inputHandlers[i].onMouseMove?.(event, hex); + if (handled) { + event.preventDefault(); + return; + } + } + } + + handleKeyDown(event: KeyboardEvent) { + for (let i = this.inputHandlers.length - 1; i >= 0; i--) { + const handled = this.inputHandlers[i].onKeyDown?.(event); + if (handled) { + event.preventDefault(); + return; + } + } + } } diff --git a/src/app/components/game/vis/input/BlockTerrainInputHandler.ts b/src/app/components/game/vis/input/BlockTerrainInputHandler.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/game/vis/input/DefaultInputHandler.ts b/src/app/components/game/vis/input/DefaultInputHandler.ts new file mode 100644 index 0000000..0ae9701 --- /dev/null +++ b/src/app/components/game/vis/input/DefaultInputHandler.ts @@ -0,0 +1,47 @@ +import { SimCommandBlockTerrain } from '../../sim/commands/SimCommandBlockTerrain'; +import { SimCommandCreateTower } from '../../sim/commands/SimCommandCreateTower'; +import { Hex } from '../../util/Hex'; +import { VisMain } from '../VisMain'; +import { HexInteractionInputHandler } from './HexInteractionInputHandler'; +import { InputHandler } from './InputHandler'; + +export class DefaultInputHandler implements InputHandler { + visMain: VisMain; + + constructor(visMain: VisMain) { + this.visMain = visMain; + } + + onKeyDown(event: KeyboardEvent): boolean { + if (event.code === 'Escape') { + if (this.visMain.inputHandlers.length > 1) { + this.visMain.inputHandlers.pop(); + this.visMain.visLevel.setHoveredHex(null); + event.preventDefault(); + return true; + } + } else if (event.code === 'Space') { + if (this.visMain.active) { + this.visMain.stop(); + } else { + this.visMain.start(); + } + return true; + } else if (event.code === 'KeyT') { + this.visMain.inputHandlers.push(new HexInteractionInputHandler(this.visMain, (hex) => new SimCommandCreateTower(hex, 0))); + return true; + } else if (event.code === 'KeyB') { + this.visMain.inputHandlers.push(new HexInteractionInputHandler(this.visMain, (hex) => new SimCommandBlockTerrain(hex))); + return true; + } + return false; + } + + onMouseDown(event: MouseEvent, hex: Hex): boolean { + return false; + } + + onMouseMove(event: MouseEvent, hex: Hex): boolean { + return false; + } +} diff --git a/src/app/components/game/vis/input/HexInteractionInputHandler.ts b/src/app/components/game/vis/input/HexInteractionInputHandler.ts new file mode 100644 index 0000000..6340f08 --- /dev/null +++ b/src/app/components/game/vis/input/HexInteractionInputHandler.ts @@ -0,0 +1,39 @@ +import { SimCommand } from '../../sim/commands/SimCommand'; +import { Hex } from '../../util/Hex'; +import { VisMain } from '../VisMain'; +import { InputHandler } from './InputHandler'; + +export class HexInteractionInputHandler implements InputHandler { + private hexCallback: (hex: Hex) => SimCommand; + private command: SimCommand | null = null; + visMain: VisMain; + + constructor(visMain: VisMain, hexCallback: (hex: Hex) => SimCommand) { + this.visMain = visMain; + this.hexCallback = hexCallback; + } + + onKeyDown(event: KeyboardEvent): boolean { + return false; + } + + onMouseDown(event: MouseEvent, hex: Hex): boolean { + if (!this.command) { + return false; + } + + this.visMain.simMain.addCommand(this.command); + return true; + } + + onMouseMove(event: MouseEvent, hex: Hex): boolean { + this.command = this.hexCallback(hex); + if (this.command && this.command.check(this.visMain.simMain)) { + this.visMain.visLevel.setHoveredHex(hex); + } + else { + this.visMain.visLevel.setHoveredHex(null); + } + return false; + } +} diff --git a/src/app/components/game/vis/input/InputHandler.ts b/src/app/components/game/vis/input/InputHandler.ts new file mode 100644 index 0000000..4bf3125 --- /dev/null +++ b/src/app/components/game/vis/input/InputHandler.ts @@ -0,0 +1,7 @@ +import { Hex } from "../../util/Hex"; + +export interface InputHandler { + onKeyDown?: (event: KeyboardEvent) => boolean; + onMouseDown?: (event: MouseEvent, hex: Hex) => boolean; + onMouseMove?: (event: MouseEvent, hex: Hex) => boolean; +} \ No newline at end of file