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