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