input handlers

This commit is contained in:
2025-05-18 16:34:33 +02:00
parent e0797d2cfc
commit 102c1f59dc
18 changed files with 595 additions and 376 deletions

View File

@@ -3,11 +3,13 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
indent_style = space indent_style = tab
indent_size = 2
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
# Add this to help VSCode and Copilot use tabs for indentation
indent_size = tab
[*.ts] [*.ts]
quote_type = single quote_type = single
ij_typescript_use_double_quotes = false ij_typescript_use_double_quotes = false

15
.vscode/settings.json vendored
View File

@@ -1,8 +1,11 @@
{ {
"json.schemas": [ "json.schemas": [
{ {
"fileMatch": [ "src/assets/data/*.json" ], "fileMatch": ["src/assets/data/*.json"],
"url": "./schemas/gdRoot.json" "url": "./schemas/gdRoot.json"
} }
] ],
"editor.insertSpaces": false,
"editor.detectIndentation": false,
"editor.tabSize": 4
} }

View File

@@ -1,4 +1,4 @@
{ {
"tabWidth": 4, "useTabs": true,
"useTabs": true "printWidth": 9999
} }

View File

@@ -12,6 +12,7 @@
<button (click)="step()">Step</button> <button (click)="step()">Step</button>
<button (click)="fastForward()">⏭ Fast Forward</button> <button (click)="fastForward()">⏭ Fast Forward</button>
<div>Step: {{ simMain.currentStep }}</div> <div>Step: {{ simMain.currentStep }}</div>
<button (click)="startNextWave()">Start next wave</button>
<button (click)="openOptions()">Options</button> <button (click)="openOptions()">Options</button>
</div> </div>
</div> </div>

View File

@@ -5,6 +5,7 @@ import { GdRoot } from './data/GdRoot';
import { SimCommand } from './sim/commands/SimCommand'; import { SimCommand } from './sim/commands/SimCommand';
import { VisMain } from './vis/VisMain'; import { VisMain } from './vis/VisMain';
import { SplashComponent } from '../splash/splash.component'; import { SplashComponent } from '../splash/splash.component';
import { SimCommandStartNextWave } from './sim/commands/SimCommandStartNextWave';
@Component({ @Component({
selector: 'app-game', selector: 'app-game',
@@ -27,7 +28,6 @@ export class GameComponent {
const gdRoot = await this.loadGdRoot(); const gdRoot = await this.loadGdRoot();
this.simMain.setGdRoot(gdRoot); this.simMain.setGdRoot(gdRoot);
this.visMain = new VisMain(this.simMain, SplashComponent.assetPreloader, this.wrapperRef.nativeElement, this.canvasRef.nativeElement); this.visMain = new VisMain(this.simMain, SplashComponent.assetPreloader, this.wrapperRef.nativeElement, this.canvasRef.nativeElement);
window.addEventListener('resize', _ => this.visMain.onResized());
} }
start() { start() {
@@ -35,15 +35,24 @@ export class GameComponent {
} }
stop() { stop() {
this.visMain.start(); this.visMain.stop();
} }
step() { step() {
this.simMain.step(); this.simMain.step();
this.visMain.onRender();
} }
rewind() { 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() { fastForward() {

View File

@@ -1,11 +1,11 @@
import { GdRoot } from "../data/GdRoot"; import { GdRoot } from '../data/GdRoot';
import { EDirection } from "../util/EDirection"; import { EDirection } from '../util/EDirection';
import { Hex } from "../util/Hex"; import { Hex } from '../util/Hex';
import { PathFinding } from "../util/PathFinding"; import { PathFinding } from '../util/PathFinding';
import { ECellType } from "./ECellType"; import { ECellType } from './ECellType';
import { SimCell } from "./SimCell"; import { SimCell } from './SimCell';
import { SimEnemy } from "./SimEnemy"; import { SimEnemy } from './SimEnemy';
import { SimProjectile } from "./SimProjectile"; import { SimProjectile } from './SimProjectile';
export class SimLevel { export class SimLevel {
paused: boolean = true; paused: boolean = true;
@@ -23,159 +23,156 @@ export class SimLevel {
index: number = 0; index: number = 0;
radius: number = 0; radius: number = 0;
constructor(gdRoot: GdRoot, levelIdx: number) { constructor(gdRoot: GdRoot, levelIdx: number) {
const gdLevel = gdRoot.levels[levelIdx]; const gdLevel = gdRoot.levels[levelIdx];
this.index = levelIdx; this.index = levelIdx;
this.currency = gdLevel.currency; this.currency = gdLevel.currency;
this.enemiesLeftToSpawn = gdLevel.waves[0].amount; this.enemiesLeftToSpawn = gdLevel.waves[0].amount;
this.radius = gdLevel.radius; this.radius = gdLevel.radius;
this.stride = 2 * this.radius + 1; this.stride = 2 * this.radius + 1;
const h0 = new Hex(0, 0); const h0 = new Hex(0, 0);
for (let y = -this.radius; y <= this.radius; ++y) { for (let y = -this.radius; y <= this.radius; ++y) {
for (let x = -this.radius; x <= this.radius; ++x) { for (let x = -this.radius; x <= this.radius; ++x) {
const hex = new Hex(x, y); const hex = new Hex(x, y);
const distance = Hex.distance(hex, h0); const distance = Hex.distance(hex, h0);
const type = distance >= this.radius ? ECellType.Blocked : ECellType.Free; const type = distance >= this.radius ? ECellType.Blocked : ECellType.Free;
const cellIndex = this.getCellIndex(hex); const cellIndex = this.getCellIndex(hex);
this.cells[cellIndex] = new SimCell(hex, distance, type, cellIndex); this.cells[cellIndex] = new SimCell(hex, distance, type, cellIndex);
} }
} }
gdLevel.walls.forEach((wall: Hex) => { gdLevel.walls.forEach((wall: Hex) => {
const cellIndex = this.getCellIndex(wall); const cellIndex = this.getCellIndex(wall);
this.cells[cellIndex].type = ECellType.Blocked; this.cells[cellIndex].type = ECellType.Blocked;
}); });
gdLevel.enemySpawns.forEach((hex: Hex) => { gdLevel.enemySpawns.forEach((hex: Hex) => {
const cellIndex = this.getCellIndex(hex); const cellIndex = this.getCellIndex(hex);
this.cells[cellIndex].type = ECellType.Entry; this.cells[cellIndex].type = ECellType.Entry;
}); });
gdLevel.enemyTargets.forEach((hex: Hex) => { gdLevel.enemyTargets.forEach((hex: Hex) => {
const cellIndex = this.getCellIndex(hex); const cellIndex = this.getCellIndex(hex);
this.cells[cellIndex].type = ECellType.Entry; this.cells[cellIndex].type = ECellType.Entry;
}); });
this.cells.forEach((cell: SimCell) => { this.cells.forEach((cell: SimCell) => {
this.updateBlockedType(cell); this.updateBlockedType(cell);
}); });
this.updatePaths(gdRoot); this.updatePaths(gdRoot);
}; }
public getCellIndex(hex: Hex) { public getCellIndex(hex: Hex) {
const x = hex.col + this.radius; const x = hex.col + this.radius;
const y = hex.row + this.radius; const y = hex.row + this.radius;
if (x < 0 || x >= this.stride || y < 0 || y >= this.stride) { if (x < 0 || x >= this.stride || y < 0 || y >= this.stride) {
return -1; return -1;
} }
return y * this.stride + x; return y * this.stride + x;
} }
public getNeighbourCell(cell: SimCell, direction: EDirection) { public getNeighbourCell(cell: SimCell, direction: EDirection) {
const hex = cell.hex; const hex = cell.hex;
const neighbourHex = Hex.neighbour(hex, direction); const neighbourHex = Hex.neighbour(hex, direction);
const neighbourIndex = this.getCellIndex(neighbourHex); const neighbourIndex = this.getCellIndex(neighbourHex);
return this.cells[neighbourIndex]; return this.cells[neighbourIndex];
} }
public updateBlockedType(cell: SimCell) { public updateBlockedType(cell: SimCell) {
if (cell.type == ECellType.Free) { if (cell.type == ECellType.Free) {
cell.blockedType = -1; cell.blockedType = -1;
return; return;
} }
if (cell.type == ECellType.Entry && cell.blockedType != -1) { if (cell.type == ECellType.Entry && cell.blockedType != -1) {
return; return;
} }
let blockedType = 0; let blockedType = 0;
for (let direction = 0; direction < 6; ++direction) { for (let direction = 0; direction < 6; ++direction) {
const neighbourCell = this.getNeighbourCell(cell, direction); const neighbourCell = this.getNeighbourCell(cell, direction);
if (!!neighbourCell && neighbourCell.type == ECellType.Free) { if (!!neighbourCell && neighbourCell.type == ECellType.Free) {
blockedType |= 1 << direction; blockedType |= 1 << direction;
} }
} }
cell.blockedType = blockedType; cell.blockedType = blockedType;
} }
public reservePaths(gdRoot: GdRoot, hexToBlock: Hex | null): boolean { public reservePaths(gdRoot: GdRoot, hexToBlock: Hex | null): boolean {
try { try {
if (!!hexToBlock) { if (!!hexToBlock) {
const reservedCell = this.cells[this.getCellIndex(hexToBlock)]; const reservedCell = this.cells[this.getCellIndex(hexToBlock)];
if (reservedCell.type != ECellType.Free || !!reservedCell.tower) if (reservedCell.type != ECellType.Free || !!reservedCell.tower) return false;
return false;
reservedCell.type = ECellType.Reserved; reservedCell.type = ECellType.Reserved;
} }
return this.updatePaths(gdRoot); return this.updatePaths(gdRoot);
} } finally {
finally { if (!!hexToBlock) {
if (!!hexToBlock) { const reservedCell = this.cells[this.getCellIndex(hexToBlock)];
const reservedCell = this.cells[this.getCellIndex(hexToBlock)]; if (reservedCell.type == ECellType.Reserved) reservedCell.type = ECellType.Free;
if (reservedCell.type == ECellType.Reserved) }
reservedCell.type = ECellType.Free }
} }
}
}
public updatePaths(gdRoot: GdRoot): boolean { public updatePaths(gdRoot: GdRoot): boolean {
const newRoutePaths: number[][] = []; const newRoutePaths: number[][] = [];
const newEnemyPaths: number[][] = []; const newEnemyPaths: number[][] = [];
const gdLevel = gdRoot.levels[this.index]; const gdLevel = gdRoot.levels[this.index];
let invalid = false; let invalid = false;
for (const routeIdx in gdLevel.enemyRoutes) { for (const routeIdx in gdLevel.enemyRoutes) {
const route = gdLevel.enemyRoutes[routeIdx]; const route = gdLevel.enemyRoutes[routeIdx];
const enemySpawnHex = gdLevel.enemySpawns[route[0]]; const enemySpawnHex = gdLevel.enemySpawns[route[0]];
const enemySpawnCell = this.cells[this.getCellIndex(enemySpawnHex)]; const enemySpawnCell = this.cells[this.getCellIndex(enemySpawnHex)];
const enemyTargetHex = gdLevel.enemyTargets[route[1]]; const enemyTargetHex = gdLevel.enemyTargets[route[1]];
const enemyTargetCell = this.cells[this.getCellIndex(enemyTargetHex)]; const enemyTargetCell = this.cells[this.getCellIndex(enemyTargetHex)];
const path = PathFinding.bfs(this, enemySpawnCell.index, enemyTargetCell.index); const path = PathFinding.bfs(this, enemySpawnCell.index, enemyTargetCell.index);
if (path == null) { if (path == null) {
invalid = true; invalid = true;
break; break;
} }
newRoutePaths[routeIdx] = path; newRoutePaths[routeIdx] = path;
} }
if (invalid) { if (invalid) {
return false; return false;
} }
for (let idx in this.enemies) { for (let idx in this.enemies) {
const simEnemy = this.enemies[idx]; const simEnemy = this.enemies[idx];
const hex = Hex.fromWorld(simEnemy.position); const hex = Hex.fromWorld(simEnemy.position);
const startIndex = this.getCellIndex(hex); const startIndex = this.getCellIndex(hex);
const endIndex = this.getCellIndex(simEnemy.endHex); const endIndex = this.getCellIndex(simEnemy.endHex);
const path = PathFinding.bfs(this, startIndex, endIndex); const path = PathFinding.bfs(this, startIndex, endIndex);
if (path == null) { if (path == null) {
invalid = true; invalid = true;
break; break;
} }
newEnemyPaths[idx] = path; newEnemyPaths[idx] = path;
} }
if (invalid) { if (invalid) {
return false; return false;
} }
for (const routeIdx in gdLevel.enemyRoutes) { for (const routeIdx in gdLevel.enemyRoutes) {
const route = gdLevel.enemyRoutes[routeIdx]; const route = gdLevel.enemyRoutes[routeIdx];
const enemySpawnHex = gdLevel.enemySpawns[route[0]]; const enemySpawnHex = gdLevel.enemySpawns[route[0]];
const enemySpawnCell = this.cells[this.getCellIndex(enemySpawnHex)]; const enemySpawnCell = this.cells[this.getCellIndex(enemySpawnHex)];
enemySpawnCell.pathsToTarget[routeIdx] = newRoutePaths[routeIdx]; enemySpawnCell.pathsToTarget[routeIdx] = newRoutePaths[routeIdx];
} }
this.enemies.forEach((simEnemy: SimEnemy, idx: number) => { this.enemies.forEach((simEnemy: SimEnemy, idx: number) => {
simEnemy.path = newEnemyPaths[idx]; simEnemy.path = newEnemyPaths[idx];
simEnemy.currentPathIndex = 0; simEnemy.currentPathIndex = 0;
simEnemy.onPathUpdated(this); simEnemy.onPathUpdated(this);
}); });
return true; return true;
} }
} }

View File

@@ -36,29 +36,29 @@ export class SimMain {
} }
executeUntilStep(target: number) { executeUntilStep(target: number) {
this.currentStep = 0;
while (this.currentStep < target) { while (this.currentStep < target) {
this.step(); this.step();
} }
} }
step() { step() {
const nextStep = this.currentStep + 1; this.currentStep++;
if (this.currentLevel && !this.currentLevel.paused) {
this.currentLevel.currentStep++;
}
// Get all commands for this step
const commands = this.commandHistory.filter((c) => c.step === nextStep);
this.currentStep = nextStep;
for (const action of this.actions) { for (const action of this.actions) {
action.execute(this); action.execute(this);
} }
const commands = this.commandHistory.filter((c) => c.step === this.currentStep);
for (const cmd of commands) { for (const cmd of commands) {
cmd.execute(this); cmd.execute(this);
} }
} }
addCommand(command: SimCommand) { addCommand(command: SimCommand) {
command.step = this.currentStep + 1;
this.commandHistory = this.commandHistory.filter( this.commandHistory = this.commandHistory.filter(
(c) => c.step < command.step (c) => c.step < command.step
); );

View File

@@ -1,39 +1,45 @@
import { EEnemySize } from "../../data/EEnemySize"; import { EEnemySize } from '../../data/EEnemySize';
import { SimCommandStartNextWave } from "../commands/SimCommandStartNextWave"; import { SimCommandStartNextWave } from '../commands/SimCommandStartNextWave';
import { SimEnemy } from "../SimEnemy"; import { SimEnemy } from '../SimEnemy';
import { SimMain } from "../SimMain"; import { SimMain } from '../SimMain';
import { ISimAction } from "./ISimAction"; import { ISimAction } from './ISimAction';
export class SimActionSpawnEnemies implements ISimAction { export class SimActionSpawnEnemies implements ISimAction {
public execute(simMain: SimMain) { public execute(simMain: SimMain) {
const level = simMain.currentLevel; const level = simMain.currentLevel;
if (!level) return; if (!level) {
return;
}
const gdLevel = simMain.gdRoot.levels[level.index]; const gdLevel = simMain.gdRoot.levels[level.index];
const step = level.currentStep; const step = level.currentStep;
const gdWave = gdLevel.waves[level.currentWave]; const gdWave = gdLevel.waves[level.currentWave];
let spawnDelay = simMain.gdRoot.simulation.spawnDelay; if (!gdWave) {
switch (gdWave.size) { return;
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) { let spawnDelay = simMain.gdRoot.simulation.spawnDelay;
level.enemiesLeftToSpawn -= 1; switch (gdWave.size) {
level.lastEnemySpawnStep = step; case EEnemySize.Huge:
spawnDelay *= 2;
break;
case EEnemySize.Tiny:
spawnDelay *= 0.5;
break;
}
const route = Math.floor(Math.random() * gdLevel.enemyRoutes.length); if (level.enemiesLeftToSpawn > 0 && level.lastEnemySpawnStep + spawnDelay * simMain.gdRoot.simulation.stepsPerSecond <= level.currentStep) {
const enemy = new SimEnemy(gdWave.enemy, route); level.enemiesLeftToSpawn -= 1;
enemy.onPathUpdated(level); level.lastEnemySpawnStep = step;
level.enemies.push(enemy);
}
if (level.nextWaveStep == step && !!gdLevel.waves[level.currentWave + 1]) { const route = Math.floor(Math.random() * gdLevel.enemyRoutes.length);
simMain.addCommand(new SimCommandStartNextWave()); 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());
}
}
} }

View File

@@ -1,6 +1,7 @@
import { SimMain } from "../SimMain"; import { SimMain } from '../SimMain';
export abstract class SimCommand { export abstract class SimCommand {
step: number = -1; step: number = -1;
abstract execute(simMain: SimMain): void; abstract execute(simMain: SimMain): void;
abstract check(simMain: SimMain): boolean;
} }

View File

@@ -1,32 +1,46 @@
import { Hex } from "../../util/Hex"; import { Hex } from '../../util/Hex';
import { ECellType } from "../ECellType"; import { ECellType } from '../ECellType';
import { SimMain } from "../SimMain"; import { SimMain } from '../SimMain';
import { SimCommand } from "./SimCommand"; import { SimCommand } from './SimCommand';
export class SimCommandBlockTerrain extends SimCommand { export class SimCommandBlockTerrain extends SimCommand {
private hex: Hex; private hex: Hex;
constructor(hex: Hex) { constructor(hex: Hex) {
super(); super();
this.hex = hex; this.hex = hex;
} }
public execute(simMain: SimMain) { public execute(simMain: SimMain) {
const level = simMain.currentLevel; const level = simMain.currentLevel;
if (!level) return; if (!level) return;
const cellIndex = level.getCellIndex(this.hex); const cellIndex = level.getCellIndex(this.hex);
const cell = level.cells[cellIndex]; const cell = level.cells[cellIndex];
if (cell.type != ECellType.Free || cell.tower != null || !level.reservePaths(simMain.gdRoot, this.hex)) { cell.type = ECellType.Blocked;
return; level.updateBlockedType(cell);
} for (let i = 0; i < 6; ++i) {
cell.type = ECellType.Blocked; const neighbourHex = Hex.neighbour(cell.hex, i);
level.updateBlockedType(cell); const neighbourIndex = level.getCellIndex(neighbourHex);
for (let i = 0; i < 6; ++i) { level.updateBlockedType(level.cells[neighbourIndex]);
const neighbourHex = Hex.neighbour(cell.hex, i); }
const neighbourIndex = level.getCellIndex(neighbourHex); level.updatePaths(simMain.gdRoot);
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;
}
} }

View File

@@ -1,54 +1,75 @@
import { Hex } from "../../util/Hex"; import { Hex } from '../../util/Hex';
import { ECellType } from "../ECellType"; import { ECellType } from '../ECellType';
import { SimEnemy } from "../SimEnemy"; import { SimEnemy } from '../SimEnemy';
import { SimMain } from "../SimMain"; import { SimMain } from '../SimMain';
import { SimTower } from "../SimTower"; import { SimTower } from '../SimTower';
import { SimCommand } from "./SimCommand"; import { SimCommand } from './SimCommand';
export class SimCommandCreateTower extends SimCommand { export class SimCommandCreateTower extends SimCommand {
hex: Hex; hex: Hex;
index: number; index: number;
constructor(hex: Hex, index: number) { constructor(hex: Hex, index: number) {
super(); super();
this.hex = hex; this.hex = hex;
this.index = index; this.index = index;
} }
public execute(simMain: SimMain) { public execute(simMain: SimMain) {
const level = simMain.currentLevel; const level = simMain.currentLevel;
if (!level) { if (!level) {
return; return;
} }
const cellIndex = level.getCellIndex(this.hex); const cellIndex = level.getCellIndex(this.hex);
const cell = level.cells[cellIndex]; const cell = level.cells[cellIndex];
const towerData = simMain.gdRoot.towers[this.index]; const towerData = simMain.gdRoot.towers[this.index];
if (cell.type != ECellType.Free) { level.enemies.forEach((enemy: SimEnemy) => {
return; const hex = Hex.fromWorld(enemy.position);
} const enemyCellIndex = level.getCellIndex(hex);
if (cellIndex == enemyCellIndex) {
return;
}
});
if (cell.tower != null) { if (level.currency < towerData.cost) {
return; return;
} }
level.enemies.forEach((enemy: SimEnemy) => { if (level.reservePaths(simMain.gdRoot, this.hex)) {
const hex = Hex.fromWorld(enemy.position); level.currency -= towerData.cost;
const enemyCellIndex = level.getCellIndex(hex); cell.tower = new SimTower(this.index, Hex.toWorld(this.hex));
if (cellIndex == enemyCellIndex) { level.updatePaths(simMain.gdRoot);
return; }
} }
});
if (level.currency < towerData.cost) { public check(simMain: SimMain): boolean {
return; const level = simMain.currentLevel;
} if (!level) {
return false;
}
if (level.reservePaths(simMain.gdRoot, this.hex)) { const cellIndex = level.getCellIndex(this.hex);
level.currency -= towerData.cost; const cell = level.cells[cellIndex];
cell.tower = new SimTower(this.index, Hex.toWorld(this.hex)); if (!cell) {
level.updatePaths(simMain.gdRoot); 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;
}
} }

View File

@@ -1,22 +1,29 @@
import { GdRoot } from "../../data/GdRoot"; import { SimMain } from '../SimMain';
import { SimLevel } from "../SimLevel"; import { SimCommand } from './SimCommand';
import { SimMain } from "../SimMain";
import { SimCommand } from "./SimCommand";
export class SimCommandStartNextWave extends SimCommand { export class SimCommandStartNextWave extends SimCommand {
public execute(simMain: SimMain) {
const level = simMain.currentLevel;
if (!level) {
return;
}
execute(simMain: SimMain) { const data = simMain.gdRoot.levels[level.index];
const level = simMain.currentLevel; level.currentWave += 1;
if (!level) return; 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]; public check(simMain: SimMain): boolean {
level.currentWave += 1; const level = simMain.currentLevel;
if (!data.waves[level.currentWave]) { if (!level) {
level.paused = true; return false;
return; }
} return true;
level.nextWaveStep = level.currentStep + simMain.gdRoot.simulation.waveDuration * simMain.gdRoot.simulation.stepsPerSecond - 1; }
level.lastEnemySpawnStep = level.currentStep;
level.enemiesLeftToSpawn = data.waves[level.currentWave].amount;
}
} }

View File

@@ -27,6 +27,7 @@ export class VisLevel {
private simMain: SimMain; private simMain: SimMain;
private gdRoot: GdRoot; private gdRoot: GdRoot;
assets: AssetPreloaderService; assets: AssetPreloaderService;
private hoveredHex: Hex | null = null;
constructor(visMain: VisMain, simMain: SimMain, gdRoot: GdRoot, assets: AssetPreloaderService) { constructor(visMain: VisMain, simMain: SimMain, gdRoot: GdRoot, assets: AssetPreloaderService) {
this.assets = assets; this.assets = assets;
@@ -60,6 +61,15 @@ export class VisLevel {
this.drawBackground(); this.drawBackground();
ctx.globalCompositeOperation = "source-over"; 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) => { simLevel.cells.forEach((cell: SimCell) => {
if (cell.distance > gdLevel.radius) { if (cell.distance > gdLevel.radius) {
return; return;
@@ -161,6 +171,10 @@ export class VisLevel {
this.lastStep = currentStep; this.lastStep = currentStep;
} }
public setHoveredHex(hex: Hex | null) {
this.hoveredHex = hex;
}
public getScreenCoords(hex: Hex): Vector2 { public getScreenCoords(hex: Hex): Vector2 {
const coord = Hex.toPixel(hex, this.hexSize); const coord = Hex.toPixel(hex, this.hexSize);
return new Vector2(coord.x + this.screenXOffset - this.screenCellWidth / 2, coord.y + this.screenYOffset - this.screenCellHeight / 2); return new Vector2(coord.x + this.screenXOffset - this.screenCellWidth / 2, coord.y + this.screenYOffset - this.screenCellHeight / 2);

View File

@@ -1,115 +1,166 @@
import { AssetPreloaderService } from "../../../assetPreloaderService"; import { AssetPreloaderService } from '../../../assetPreloaderService';
import { SplashComponent } from "../../splash/splash.component"; import { SimMain } from '../sim/SimMain';
import { SimMain } from "../sim/SimMain"; import { VisLevel } from './VisLevel';
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 { export class VisMain {
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D; context: CanvasRenderingContext2D;
wallPattern: CanvasPattern; wallPattern: CanvasPattern;
visLevel: VisLevel; visLevel: VisLevel;
simMain: SimMain; simMain: SimMain;
private startTimestamp: number = 0; active: boolean = true;
private active: boolean = true; assets: AssetPreloaderService;
private ready: boolean = false; wrapper: HTMLDivElement;
private gap: number = 0; inputHandlers: InputHandler[] = [];
assets: AssetPreloaderService; private startTimestamp: number = 0;
wrapper: HTMLDivElement; private ready: boolean = false;
private gap: number = 0;
constructor(simMain: SimMain, assets: AssetPreloaderService, wrapper: HTMLDivElement, canvas: HTMLCanvasElement) { constructor(simMain: SimMain, assets: AssetPreloaderService, wrapper: HTMLDivElement, canvas: HTMLCanvasElement) {
this.assets = assets; this.assets = assets;
this.simMain = simMain; this.simMain = simMain;
this.canvas = canvas; this.canvas = canvas;
this.wrapper = wrapper; this.wrapper = wrapper;
this.context = this.canvas.getContext("2d")!; this.context = this.canvas.getContext('2d')!;
this.context.globalCompositeOperation = "source-over"; this.context.globalCompositeOperation = 'source-over';
this.wallPattern = this.createPattern(assets.getImage("images/wall.png"), 48); this.wallPattern = this.createPattern(assets.getImage('images/wall.png'), 48);
this.visLevel = new VisLevel(this, this.simMain, this.simMain.gdRoot, assets); this.visLevel = new VisLevel(this, this.simMain, this.simMain.gdRoot, assets);
this.start(); this.setupInput();
} this.start();
this.inputHandlers.push(new DefaultInputHandler(this));
}
private createPattern(image: HTMLImageElement, size: number): CanvasPattern { private createPattern(image: HTMLImageElement, size: number): CanvasPattern {
const tempCanvas = document.createElement("canvas"); const tempCanvas = document.createElement('canvas');
const tempContext = tempCanvas.getContext("2d")!; const tempContext = tempCanvas.getContext('2d')!;
tempCanvas.width = size; tempCanvas.width = size;
tempCanvas.height = size; tempCanvas.height = size;
tempContext.drawImage(image, 0, 0, image.width, image.height, 0, 0, size, 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) { private step(timestamp: number) {
if (!this.active) { if (!this.active) {
return; return;
} }
requestAnimationFrame((timestamp: number) => { requestAnimationFrame((timestamp: number) => {
this.step(timestamp); this.step(timestamp);
}); });
if (!this.startTimestamp) { const simLevel = this.simMain.currentLevel;
this.startTimestamp = timestamp; if (!simLevel) {
} return;
}
const simLevel = this.simMain.currentLevel; let targetStep = Math.floor(((timestamp - this.startTimestamp) * this.simMain.gdRoot.simulation.stepsPerSecond) / 1000 - this.gap);
if (!simLevel) { if (simLevel.paused) {
return; 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; public onResized() {
if (simLevel.paused) { // Always get the latest wrapper size
this.gap += targetStep - simLevel.currentStep; const width = this.wrapper.clientWidth;
targetStep = simLevel.currentStep; const height = this.wrapper.clientHeight;
} const ratio = window.devicePixelRatio;
this.simMain.executeUntilStep(targetStep);
this.visLevel.updateEveryFrame(targetStep);
this.onRender();
};
public onResized() { // Set canvas style to always fill the wrapper
// Always get the latest wrapper size this.canvas.width = width * ratio;
const width = this.wrapper.clientWidth; this.canvas.height = height * ratio;
const height = this.wrapper.clientHeight;
const ratio = window.devicePixelRatio;
// Set canvas style to always fill the wrapper // Reset transform before scaling to avoid compounding
this.canvas.width = width * ratio; this.context.setTransform(1, 0, 0, 1, 0, 0);
this.canvas.height = height * ratio; this.context.scale(ratio, ratio);
// Reset transform before scaling to avoid compounding this.visLevel.updateSize();
this.context.setTransform(1, 0, 0, 1, 0, 0); }
this.context.scale(ratio, ratio);
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() { private clear() {
this.clear(); this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
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() { stop() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); this.active = false;
} }
stop() { start() {
this.active = false; this.active = true;
} requestAnimationFrame((timestamp: number) => {
this.startTimestamp = timestamp - (this.simMain.currentStep / this.simMain.gdRoot.simulation.stepsPerSecond) * 1000;
this.step(timestamp);
});
}
start() { private setupInput() {
this.active = true; this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e));
requestAnimationFrame((timestamp: number) => { this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
this.step(timestamp); 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;
}
}
}
} }

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}