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="main-area">
|
||||||
<div class="canvas-wrapper" #canvasWrapper>
|
<div class="canvas-wrapper" #canvasWrapper>
|
||||||
<canvas #gameCanvas></canvas>
|
<canvas #gameCanvas (click)="onCanvasClick($event)"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom-panel">
|
<div class="bottom-panel">
|
||||||
<button (click)="rewind()">⏮ Rewind</button>
|
<button (click)="rewind()">⏮ Rewind</button>
|
||||||
@@ -11,8 +11,7 @@
|
|||||||
<button (click)="stop()">⏸ Pause</button>
|
<button (click)="stop()">⏸ Pause</button>
|
||||||
<button (click)="step()">Step</button>
|
<button (click)="step()">Step</button>
|
||||||
<button (click)="fastForward()">⏭ Fast Forward</button>
|
<button (click)="fastForward()">⏭ Fast Forward</button>
|
||||||
<div>Step: {{ engine.currentStep }}</div>
|
<div>Step: {{ simMain.currentStep }}</div>
|
||||||
<button (click)="issueMove()">🚶 Move Unit</button>
|
|
||||||
<button (click)="openOptions()">Options</button>
|
<button (click)="openOptions()">Options</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||||
import { OptionsComponent } from '../options/options.component';
|
import { OptionsComponent } from '../options/options.component';
|
||||||
import { GameState } from './gameState';
|
import { SimMain } from './sim/SimMain';
|
||||||
import { SimulationEngine } from './simulationEngine';
|
import { GdRoot } from './data/GdRoot';
|
||||||
import { GameRules } from './gameRules';
|
import { SimCommand } from './sim/commands/SimCommand';
|
||||||
import { Command } from './command';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-game',
|
selector: 'app-game',
|
||||||
@@ -12,10 +11,11 @@ import { Command } from './command';
|
|||||||
imports: [OptionsComponent],
|
imports: [OptionsComponent],
|
||||||
})
|
})
|
||||||
export class GameComponent implements OnInit {
|
export class GameComponent implements OnInit {
|
||||||
engine!: SimulationEngine;
|
simMain!: SimMain;
|
||||||
private optionsOpen = false;
|
private optionsOpen = false;
|
||||||
private isPaused = false;
|
private isPaused = false;
|
||||||
private lastFrameTime = 0;
|
private lastFrameTime = 0;
|
||||||
|
private tileSize = 10; // Size of each tile in pixels
|
||||||
|
|
||||||
@ViewChild('gameCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
|
@ViewChild('gameCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||||
@ViewChild('canvasWrapper') wrapperRef!: ElementRef<HTMLDivElement>;
|
@ViewChild('canvasWrapper') wrapperRef!: ElementRef<HTMLDivElement>;
|
||||||
@@ -26,93 +26,78 @@ export class GameComponent implements OnInit {
|
|||||||
pixelScale: number = 1;
|
pixelScale: number = 1;
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const initialState: GameState = {
|
const gdRoot = await this.loadGdRoot();
|
||||||
tick: 0,
|
this.simMain = new SimMain(gdRoot);
|
||||||
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);
|
|
||||||
requestAnimationFrame(this.gameLoop.bind(this));
|
requestAnimationFrame(this.gameLoop.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
this.engine.start();
|
this.simMain.start(performance.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
this.engine.stop();
|
this.simMain.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
step() {
|
step() {
|
||||||
this.engine.step();
|
this.simMain.step();
|
||||||
}
|
}
|
||||||
|
|
||||||
rewind() {
|
rewind() {
|
||||||
this.engine.rewindToZero();
|
//this.simMain.rewindToZero();
|
||||||
}
|
}
|
||||||
|
|
||||||
fastForward() {
|
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() {
|
async reloadGameData() {
|
||||||
const json = await fetch('/assets/gameData.json').then((r) => r.json());
|
this.simMain.setGdRoot(await this.loadGdRoot());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.resizeCanvas();
|
this.resizeCanvas();
|
||||||
requestAnimationFrame(this.render);
|
requestAnimationFrame(this.update);
|
||||||
window.addEventListener('resize', this.resizeCanvas.bind(this));
|
window.addEventListener('resize', this.resizeCanvas.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
render = (now: number) => {
|
update = (now: number) => {
|
||||||
if (!this.engine) return;
|
if (!this.simMain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [prevState, currState] = this.engine.getRenderStates();
|
this.simMain.update(now)
|
||||||
|
|
||||||
// Compute interpolation factor (0..1)
|
const lastStepTime = this.simMain.isRunning
|
||||||
const lastStepTime = this.engine.isRunning ? this.engine.lastStepTime : now;
|
? this.simMain.lastStepTime
|
||||||
const t = this.engine.isRunning
|
: now;
|
||||||
? Math.max(Math.min((now - lastStepTime) / this.engine.interval, 1), 0)
|
const t = this.simMain.isRunning
|
||||||
: 1; // fully render the last known state
|
? 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');
|
const ctx = this.canvasRef.nativeElement.getContext('2d');
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
const canvas = this.canvasRef.nativeElement;
|
const canvas = this.canvasRef.nativeElement;
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
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 {
|
resizeCanvas(): void {
|
||||||
@@ -149,6 +134,18 @@ export class GameComponent implements OnInit {
|
|||||||
return [px / this.pixelScale, py / this.pixelScale];
|
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 {
|
private gameLoop(currentTime: number): void {
|
||||||
if (this.lastFrameTime === 0) {
|
if (this.lastFrameTime === 0) {
|
||||||
this.lastFrameTime = currentTime;
|
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'],
|
styleUrls: ['./splash.component.css'],
|
||||||
})
|
})
|
||||||
export class SplashComponent {
|
export class SplashComponent {
|
||||||
|
static assetPreloader: AssetPreloaderService = new AssetPreloaderService();
|
||||||
show: boolean = true;
|
show: boolean = true;
|
||||||
assetPreloader: AssetPreloaderService = new AssetPreloaderService();
|
progress: number = 0;
|
||||||
progress: number = 0;
|
|
||||||
|
|
||||||
constructor(private router: Router) {
|
constructor(private router: Router) {
|
||||||
this.assetPreloader.progress$.subscribe(p => this.progress = p);
|
SplashComponent.assetPreloader.progress$.subscribe(
|
||||||
this.assetPreloader.preload().then(() => this.router.navigate(['/menu']));
|
(p) => (this.progress = p)
|
||||||
|
);
|
||||||
|
SplashComponent.assetPreloader
|
||||||
|
.preload()
|
||||||
|
.then(() => this.router.navigate(['/menu']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user