Compare commits
10 Commits
23e5707e98
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| fae633ea54 | |||
| 337a3d8213 | |||
| d26999ace9 | |||
| 9b81bef804 | |||
| c61a10d79a | |||
| 61b86b7a60 | |||
| 102c1f59dc | |||
| e0797d2cfc | |||
| c6a3056dcd | |||
| 6ffa7e9c68 |
@@ -3,11 +3,13 @@ root = true
|
|||||||
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = tab
|
||||||
indent_size = 2
|
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# Add this to help VSCode and Copilot use tabs for indentation
|
||||||
|
indent_size = tab
|
||||||
|
|
||||||
[*.ts]
|
[*.ts]
|
||||||
quote_type = single
|
quote_type = single
|
||||||
ij_typescript_use_double_quotes = false
|
ij_typescript_use_double_quotes = false
|
||||||
|
|||||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"json.schemas": [
|
"json.schemas": [
|
||||||
{
|
{
|
||||||
"fileMatch": [ "src/assets/data/*.json" ],
|
"fileMatch": ["src/assets/data/*.json"],
|
||||||
"url": "./schemas/gdRoot.json"
|
"url": "./schemas/gdRoot.json"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"editor.insertSpaces": false,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.tabSize": 4
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"tabWidth": 4,
|
"useTabs": true,
|
||||||
"useTabs": true
|
"printWidth": 9999
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,13 +29,36 @@ export class AssetPreloaderService {
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
if (entry.type === 'image') {
|
if (entry.type === 'image') {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
const loadedPromise = new Promise<void>((resolve, reject) => {
|
||||||
|
img.onload = () => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
img.onerror = (e) => {
|
||||||
|
console.error(`Failed to load image: ${entry.path}`, e);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
reject(e);
|
||||||
|
};
|
||||||
|
});
|
||||||
img.src = url;
|
img.src = url;
|
||||||
|
return loadedPromise.then(() => {
|
||||||
this.images.set(entry.path, img);
|
this.images.set(entry.path, img);
|
||||||
|
});
|
||||||
} else if (entry.type === 'audio') {
|
} else if (entry.type === 'audio') {
|
||||||
const audio = new Audio();
|
const audio = new Audio();
|
||||||
|
const loadedPromise = new Promise<void>((resolve, reject) => {
|
||||||
|
audio.oncanplaythrough = () => resolve();
|
||||||
|
audio.onerror = (e) => {
|
||||||
|
console.error(`Failed to load audio: ${entry.path}`, e);
|
||||||
|
resolve(); // Don't reject to avoid blocking all
|
||||||
|
};
|
||||||
|
});
|
||||||
audio.src = url;
|
audio.src = url;
|
||||||
|
return loadedPromise.then(() => {
|
||||||
this.audio.set(entry.path, audio);
|
this.audio.set(entry.path, audio);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
return Promise.resolve();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -44,21 +67,24 @@ export class AssetPreloaderService {
|
|||||||
|
|
||||||
private async loadAsset(entry: AssetEntry): Promise<Blob> {
|
private async loadAsset(entry: AssetEntry): Promise<Blob> {
|
||||||
const response = await fetch(`/assets/${entry.path}`);
|
const response = await fetch(`/assets/${entry.path}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch asset: ${entry.path}`);
|
||||||
|
}
|
||||||
|
const contentType = response.headers.get('Content-Type') || undefined;
|
||||||
const reader = response.body?.getReader();
|
const reader = response.body?.getReader();
|
||||||
let loaded = 0;
|
let loaded = 0;
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader!.read();
|
const { done, value } = await reader!.read();
|
||||||
if (done)
|
if (done) break;
|
||||||
break;
|
|
||||||
chunks.push(value);
|
chunks.push(value);
|
||||||
loaded += value.length;
|
loaded += value.length;
|
||||||
this.loadedBytes += value.length;
|
this.loadedBytes += value.length;
|
||||||
this._progress.next(this.loadedBytes / this.totalBytes);
|
this._progress.next(this.loadedBytes / this.totalBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Blob(chunks);
|
return new Blob(chunks, contentType ? { type: contentType } : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
getImage(name: string): HTMLImageElement {
|
getImage(name: string): HTMLImageElement {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -24,10 +24,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: black;
|
background: black;
|
||||||
|
overflow: hidden; /* Prevent canvas from causing wrapper to grow */
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
display: block;
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-panel {
|
.bottom-panel {
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bottom-panel">
|
<div class="bottom-panel">
|
||||||
<button (click)="rewind()">⏮ Rewind</button>
|
<button (click)="rewind()">⏮ Rewind</button>
|
||||||
<button (click)="start()">▶ Play</button>
|
<button (click)="start()" [disabled]="visMain?.active">▶ Play</button>
|
||||||
<button (click)="stop()">⏸ Pause</button>
|
<button (click)="stop()" [disabled]="!visMain?.active">⏸ Pause</button>
|
||||||
<button (click)="step()">Step</button>
|
<button (click)="step()" [disabled]="visMain?.active">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)="startNextWave()">Start next wave</button>
|
||||||
<button (click)="openOptions()">Options</button>
|
<button (click)="openOptions()">Options</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
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';
|
import { VisMain } from './vis/VisMain';
|
||||||
|
import { SplashComponent } from '../splash/splash.component';
|
||||||
|
import { SimCommandStartNextWave } from './sim/commands/SimCommandStartNextWave';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-game',
|
selector: 'app-game',
|
||||||
@@ -11,182 +13,79 @@ import { Command } from './command';
|
|||||||
styleUrls: ['./game.component.css'],
|
styleUrls: ['./game.component.css'],
|
||||||
imports: [OptionsComponent],
|
imports: [OptionsComponent],
|
||||||
})
|
})
|
||||||
export class GameComponent implements OnInit {
|
export class GameComponent {
|
||||||
engine!: SimulationEngine;
|
simMain: SimMain = new SimMain();
|
||||||
private optionsOpen = false;
|
visMain!: VisMain;
|
||||||
private isPaused = false;
|
|
||||||
private lastFrameTime = 0;
|
|
||||||
|
|
||||||
@ViewChild('gameCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
|
@ViewChild('gameCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||||
@ViewChild('canvasWrapper') wrapperRef!: ElementRef<HTMLDivElement>;
|
@ViewChild('canvasWrapper') wrapperRef!: ElementRef<HTMLDivElement>;
|
||||||
private logicalWidth = 800;
|
|
||||||
private logicalHeight = 600;
|
|
||||||
scaleX: number = 1;
|
scaleX: number = 1;
|
||||||
scaleY: number = 1;
|
scaleY: number = 1;
|
||||||
pixelScale: number = 1;
|
pixelScale: number = 1;
|
||||||
|
optionsOpen: boolean = false;
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngAfterViewInit() {
|
||||||
const initialState: GameState = {
|
const gdRoot = await this.loadGdRoot();
|
||||||
tick: 0,
|
this.simMain.setGdRoot(gdRoot);
|
||||||
units: [{ id: 'u1', x: 0, y: 0 }],
|
this.visMain = new VisMain(this.simMain, SplashComponent.assetPreloader, this.wrapperRef.nativeElement, this.canvasRef.nativeElement);
|
||||||
commandHistory: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const json = await fetch('/assets/gameData.json').then((r) => r.json());
|
|
||||||
this.engine = new SimulationEngine(initialState, GameRules, json);
|
|
||||||
requestAnimationFrame(this.gameLoop.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
this.engine.start();
|
this.visMain.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
this.engine.stop();
|
this.visMain.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
step() {
|
step() {
|
||||||
this.engine.step();
|
this.simMain.step();
|
||||||
|
this.visMain.onRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
rewind() {
|
rewind() {
|
||||||
this.engine.rewindToZero();
|
const wasActive = this.visMain.active;
|
||||||
|
if (wasActive)
|
||||||
|
this.visMain.stop();
|
||||||
|
this.simMain.rewind();
|
||||||
|
if (wasActive)
|
||||||
|
this.visMain.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
startNextWave() {
|
||||||
|
this.simMain.addCommand(new SimCommandStartNextWave());
|
||||||
}
|
}
|
||||||
|
|
||||||
fastForward() {
|
fastForward() {
|
||||||
this.engine.fastForwardToEnd();
|
const wasActive = this.visMain.active;
|
||||||
|
if (wasActive)
|
||||||
|
this.visMain.stop();
|
||||||
|
this.simMain.executeToEnd();
|
||||||
|
if (wasActive)
|
||||||
|
this.visMain.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadGdRoot(): Promise<GdRoot> {
|
||||||
|
const data = await fetch('/assets/data/gdRoot.json').then((r) =>
|
||||||
|
r.json()
|
||||||
|
);
|
||||||
|
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 {
|
|
||||||
this.resizeCanvas();
|
|
||||||
requestAnimationFrame(this.render);
|
|
||||||
window.addEventListener('resize', this.resizeCanvas.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
render = (now: number) => {
|
|
||||||
if (!this.engine) return;
|
|
||||||
|
|
||||||
const [prevState, currState] = this.engine.getRenderStates();
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
this.renderFrame(prevState, currState, t);
|
|
||||||
|
|
||||||
requestAnimationFrame(this.render);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderFrame(prev: GameState, curr: GameState, 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 {
|
|
||||||
const wrapper = this.wrapperRef.nativeElement;
|
|
||||||
const canvas = this.canvasRef.nativeElement;
|
|
||||||
|
|
||||||
const targetAspect = this.logicalWidth / this.logicalHeight;
|
|
||||||
const maxW = wrapper.clientWidth;
|
|
||||||
const maxH = wrapper.clientHeight;
|
|
||||||
|
|
||||||
let width = maxW;
|
|
||||||
let height = width / targetAspect;
|
|
||||||
|
|
||||||
if (height > maxH) {
|
|
||||||
height = maxH;
|
|
||||||
width = height * targetAspect;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
|
|
||||||
this.scaleX = canvas.width / this.logicalWidth; // e.g. 800
|
|
||||||
this.scaleY = canvas.height / this.logicalHeight; // e.g. 600
|
|
||||||
|
|
||||||
// Optionally use the smaller of the two for uniform scaling
|
|
||||||
this.pixelScale = Math.min(this.scaleX, this.scaleY);
|
|
||||||
}
|
|
||||||
|
|
||||||
toScreen(x: number, y: number): [number, number] {
|
|
||||||
return [x * this.pixelScale, y * this.pixelScale];
|
|
||||||
}
|
|
||||||
|
|
||||||
fromScreen(px: number, py: number): [number, number] {
|
|
||||||
return [px / this.pixelScale, py / this.pixelScale];
|
|
||||||
}
|
|
||||||
|
|
||||||
private gameLoop(currentTime: number): void {
|
|
||||||
if (this.lastFrameTime === 0) {
|
|
||||||
this.lastFrameTime = currentTime;
|
|
||||||
}
|
|
||||||
const deltaTime = (currentTime - this.lastFrameTime) / 1000;
|
|
||||||
this.lastFrameTime = currentTime;
|
|
||||||
|
|
||||||
// Only update the game state if not paused
|
|
||||||
if (!this.isPaused) {
|
|
||||||
this.updateGame(deltaTime);
|
|
||||||
requestAnimationFrame(this.gameLoop.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateGame(deltaTime: number): void {
|
|
||||||
// Implement your game update logic here using deltaTime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when opening options: pause the game
|
// Called when opening options: pause the game
|
||||||
public openOptions(): void {
|
public openOptions(): void {
|
||||||
this.optionsOpen = true;
|
this.optionsOpen = true;
|
||||||
this.pauseGame();
|
this.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when closing options: resume the game
|
// Called when closing options: resume the game
|
||||||
public closeOptions(): void {
|
public closeOptions(): void {
|
||||||
this.optionsOpen = false;
|
this.optionsOpen = false;
|
||||||
this.resumeGame();
|
this.start();
|
||||||
}
|
|
||||||
|
|
||||||
private pauseGame(): void {
|
|
||||||
this.isPaused = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private resumeGame(): void {
|
|
||||||
this.isPaused = false;
|
|
||||||
// Reset the lastFrameTime to avoid a huge delta on resume
|
|
||||||
this.lastFrameTime = performance.now();
|
|
||||||
requestAnimationFrame(this.gameLoop.bind(this));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/app/components/game/sim/SimEnemy.ts
Normal file
94
src/app/components/game/sim/SimEnemy.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { EEnemySize } from '../data/EEnemySize';
|
||||||
|
import { EProjectileEffectType } from '../data/EProjectileEffectType';
|
||||||
|
import { GdEnemy } from '../data/GdEnemy';
|
||||||
|
import { GdLevel } from '../data/GdLevel';
|
||||||
|
import { GdProjectileEffect } from '../data/GdProjectileEffect';
|
||||||
|
import { Hex } from '../util/Hex';
|
||||||
|
import { PathFinding } from '../util/PathFinding';
|
||||||
|
import { Vector2 } from '../util/Vector2';
|
||||||
|
import { SimLevel } from './SimLevel';
|
||||||
|
import { SimMain } from './SimMain';
|
||||||
|
|
||||||
|
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, step: number, simMain: SimMain) {
|
||||||
|
const level = simMain.currentLevel!;
|
||||||
|
const gdLevel = simMain.gdRoot.levels[level.index];
|
||||||
|
const gdWave = gdLevel.waves[level.currentWave];
|
||||||
|
const gdEnemy = simMain.gdRoot.enemies[gdWave.enemy];
|
||||||
|
|
||||||
|
this.index = index;
|
||||||
|
this.routeIdx = routeIdx;
|
||||||
|
this.startHex = gdLevel.enemySpawns[gdLevel.enemyRoutes[routeIdx][0]];
|
||||||
|
this.endHex = gdLevel.enemyTargets[gdLevel.enemyRoutes[routeIdx][1]];
|
||||||
|
const startIndex = level.getCellIndex(this.startHex);
|
||||||
|
this.path = [...level.cells[startIndex].pathsToTarget[routeIdx]];
|
||||||
|
this.currentPathIndex = 0;
|
||||||
|
this.currentPathStep = step;
|
||||||
|
this.speed = gdEnemy.speed;
|
||||||
|
this.hitPonts = 10; // TODO gd
|
||||||
|
this.size = EEnemySize.Tiny; // TODO gd
|
||||||
|
this.gain = 10; // TODO gd
|
||||||
|
this.onPathUpdated(level, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, initPosition: boolean = false) {
|
||||||
|
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(this.currentPathPosition);
|
||||||
|
const myLen = myDir.magnitude();
|
||||||
|
const hexDir = pos2.subtract(pos1);
|
||||||
|
const hexLen = hexDir.magnitude();
|
||||||
|
if (myLen > hexLen) {
|
||||||
|
this.path.unshift(this.path[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initPosition)
|
||||||
|
{
|
||||||
|
this.currentPathPosition = pos1.clone();
|
||||||
|
this.prevPosition = pos1.clone();
|
||||||
|
this.position = pos1.clone();
|
||||||
|
}
|
||||||
|
this.direction = myDir.multiplyScalar(1 / myLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
213
src/app/components/game/sim/SimLevel.ts
Normal file
213
src/app/components/game/sim/SimLevel.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import { EEnemySize } from '../data/EEnemySize';
|
||||||
|
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 { SimMain } from './SimMain';
|
||||||
|
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;
|
||||||
|
spawnDelay: 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 startNextWave(simMain: SimMain) {
|
||||||
|
const data = simMain.gdRoot.levels[this.index];
|
||||||
|
this.currentWave += 1;
|
||||||
|
const gdWave = data.waves[this.currentWave];
|
||||||
|
if (!gdWave) {
|
||||||
|
this.paused = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastEnemySpawnStep = this.currentStep;
|
||||||
|
this.enemiesLeftToSpawn = data.waves[this.currentWave].amount;
|
||||||
|
this.paused = false;
|
||||||
|
this.spawnDelay = simMain.gdRoot.simulation.spawnDelay;
|
||||||
|
switch (gdWave.size) {
|
||||||
|
case EEnemySize.Huge:
|
||||||
|
this.spawnDelay *= 2;
|
||||||
|
break;
|
||||||
|
case EEnemySize.Tiny:
|
||||||
|
this.spawnDelay *= 0.5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.nextWaveStep = this.currentStep + (simMain.gdRoot.simulation.waveDuration + gdWave.amount * this.spawnDelay) * simMain.gdRoot.simulation.stepsPerSecond - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (const routeIdx in gdLevel.enemyRoutes) {
|
||||||
|
const route = gdLevel.enemyRoutes[routeIdx];
|
||||||
|
const enemySpawnHex = gdLevel.enemySpawns[route[0]];
|
||||||
|
const enemySpawnCell = this.cells[this.getCellIndex(enemySpawnHex)];
|
||||||
|
enemySpawnCell.pathsToTarget[routeIdx] = newRoutePaths[routeIdx];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let idx in this.enemies) {
|
||||||
|
const simEnemy = this.enemies[idx];
|
||||||
|
const currentIndex = simEnemy.path[simEnemy.currentPathIndex];
|
||||||
|
const nextIndex = simEnemy.path[simEnemy.currentPathIndex + 1];
|
||||||
|
const nextCell = this.cells[nextIndex];
|
||||||
|
const endIndex = this.getCellIndex(simEnemy.endHex);
|
||||||
|
let startIndex = nextCell.type != ECellType.Free ? currentIndex : nextIndex;
|
||||||
|
let path = PathFinding.bfs(this, startIndex, endIndex);
|
||||||
|
if (path == null) {
|
||||||
|
invalid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (nextCell.type != ECellType.Free) {
|
||||||
|
path = [nextIndex, ...path];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
path = [currentIndex, ...path];
|
||||||
|
}
|
||||||
|
newEnemyPaths[idx] = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.enemies.forEach((simEnemy: SimEnemy, idx: number) => {
|
||||||
|
const newPath = newEnemyPaths[idx];
|
||||||
|
simEnemy.path = newPath;
|
||||||
|
simEnemy.currentPathIndex = 0;
|
||||||
|
simEnemy.onPathUpdated(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/app/components/game/sim/SimMain.ts
Normal file
72
src/app/components/game/sim/SimMain.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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 {
|
||||||
|
currentStep = -1;
|
||||||
|
maxStep = -1;
|
||||||
|
currentLevel: SimLevel | null = null;
|
||||||
|
commandHistory: SimCommand[] = [];
|
||||||
|
gdRoot: GdRoot = null!;
|
||||||
|
interval: number = 0;
|
||||||
|
private actions: ISimAction[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
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;
|
||||||
|
this.currentLevel = new SimLevel(this.gdRoot, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
executeToEnd() {
|
||||||
|
this.executeUntilStep(this.maxStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
executeUntilStep(target: number) {
|
||||||
|
while (this.currentStep < target) {
|
||||||
|
this.step();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rewind() {
|
||||||
|
this.currentLevel = new SimLevel(this.gdRoot, this.currentLevel?.index ?? 0);
|
||||||
|
this.currentStep = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
step() {
|
||||||
|
this.currentStep++;
|
||||||
|
this.maxStep = Math.max(this.maxStep, this.currentStep);
|
||||||
|
if (this.currentLevel && !this.currentLevel.paused) {
|
||||||
|
this.currentLevel.currentStep++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const action of this.actions) {
|
||||||
|
action.execute(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
const commands = this.commandHistory.filter((c) => c.step === this.currentStep);
|
||||||
|
for (const cmd of commands) {
|
||||||
|
cmd.execute(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addCommand(command: SimCommand) {
|
||||||
|
command.step = this.currentStep + 1;
|
||||||
|
this.commandHistory = this.commandHistory.filter(
|
||||||
|
(c) => c.step < command.step
|
||||||
|
);
|
||||||
|
this.commandHistory.push(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/app/components/game/sim/actions/SimActionMoveEnemies.ts
Normal file
46
src/app/components/game/sim/actions/SimActionMoveEnemies.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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 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);
|
||||||
|
|
||||||
|
const segmentLength = pos2.subtract(pos1).magnitude();
|
||||||
|
const stepsToTraverse = (segmentLength * simMain.gdRoot.simulation.stepsPerSecond) / simEnemy.speed;
|
||||||
|
const t = (level.currentStep - simEnemy.currentPathStep) / stepsToTraverse;
|
||||||
|
|
||||||
|
simEnemy.prevPosition = simEnemy.position;
|
||||||
|
simEnemy.position = Vector2.lerp(pos1, pos2, t);
|
||||||
|
if (t >= 1) {
|
||||||
|
simEnemy.currentPathIndex += 1;
|
||||||
|
simEnemy.currentPathStep = level.currentStep;
|
||||||
|
simEnemy.currentPathPosition = pos2.clone();
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/app/components/game/sim/actions/SimActionSpawnEnemies.ts
Normal file
35
src/app/components/game/sim/actions/SimActionSpawnEnemies.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { EEnemySize } from '../../data/EEnemySize';
|
||||||
|
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];
|
||||||
|
if (!gdWave) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level.enemiesLeftToSpawn > 0 && level.lastEnemySpawnStep + level.spawnDelay * simMain.gdRoot.simulation.stepsPerSecond <= level.currentStep) {
|
||||||
|
level.enemiesLeftToSpawn -= 1;
|
||||||
|
level.lastEnemySpawnStep = step;
|
||||||
|
|
||||||
|
const route = Math.floor(Math.random() * gdLevel.enemyRoutes.length);
|
||||||
|
const simEnemy = new SimEnemy(gdWave.enemy, route, step, simMain);
|
||||||
|
|
||||||
|
level.enemies.push(simEnemy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level.nextWaveStep == step) {
|
||||||
|
if (!!gdLevel.waves[level.currentWave + 1])
|
||||||
|
level.startNextWave(simMain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/app/components/game/sim/commands/SimCommand.ts
Normal file
7
src/app/components/game/sim/commands/SimCommand.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { SimMain } from '../SimMain';
|
||||||
|
|
||||||
|
export abstract class SimCommand {
|
||||||
|
step: number = -1;
|
||||||
|
abstract execute(simMain: SimMain): void;
|
||||||
|
abstract check(simMain: SimMain): boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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];
|
||||||
|
cell.type = ECellType.Blocked;
|
||||||
|
level.updateBlockedType(cell);
|
||||||
|
for (let i = 0; i < 6; ++i) {
|
||||||
|
const neighbourHex = Hex.neighbour(cell.hex, i);
|
||||||
|
const neighbourIndex = level.getCellIndex(neighbourHex);
|
||||||
|
level.updateBlockedType(level.cells[neighbourIndex]);
|
||||||
|
}
|
||||||
|
level.updatePaths(simMain.gdRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public check(simMain: SimMain): boolean {
|
||||||
|
const level = simMain.currentLevel;
|
||||||
|
if (!level) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const cellIndex = level.getCellIndex(this.hex);
|
||||||
|
const cell = level.cells[cellIndex];
|
||||||
|
if (!cell) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cell.type != ECellType.Free || cell.tower != null || !level.reservePaths(simMain.gdRoot, this.hex)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { Hex } from '../../util/Hex';
|
||||||
|
import { ECellType } from '../ECellType';
|
||||||
|
import { SimEnemy } from '../SimEnemy';
|
||||||
|
import { SimMain } from '../SimMain';
|
||||||
|
import { SimTower } from '../SimTower';
|
||||||
|
import { SimCommand } from './SimCommand';
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public check(simMain: SimMain): boolean {
|
||||||
|
const level = simMain.currentLevel;
|
||||||
|
if (!level) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cellIndex = level.getCellIndex(this.hex);
|
||||||
|
const cell = level.cells[cellIndex];
|
||||||
|
if (!cell) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const towerData = simMain.gdRoot.towers[this.index];
|
||||||
|
|
||||||
|
if (cell.type != ECellType.Free) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cell.tower != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level.currency < towerData.cost) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { SimMain } from '../SimMain';
|
||||||
|
import { SimCommand } from './SimCommand';
|
||||||
|
|
||||||
|
export class SimCommandStartNextWave extends SimCommand {
|
||||||
|
public execute(simMain: SimMain) {
|
||||||
|
const level = simMain.currentLevel;
|
||||||
|
if (!level) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
level.startNextWave(simMain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public check(simMain: SimMain): boolean {
|
||||||
|
const level = simMain.currentLevel;
|
||||||
|
if (!level) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@ export class VisEnemy {
|
|||||||
ctx.scale(2, 2);
|
ctx.scale(2, 2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ctx.drawImage(this.assets.getImage("enemy-" + (this.simEnemy.index | 0) + ".svg"), -this.image.width / 2, -this.image.height / 2, this.image.width, this.image.height);
|
ctx.drawImage(this.assets.getImage("images/enemy-" + (this.simEnemy.index | 0) + ".svg"), -this.image.width / 2, -this.image.height / 2, this.image.width, this.image.height);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { AssetPreloaderService } from "../../../assetPreloaderService";
|
import { AssetPreloaderService } from '../../../assetPreloaderService';
|
||||||
import { GdRoot } from "../data/GdRoot";
|
import { GdRoot } from '../data/GdRoot';
|
||||||
import { ECellType } from "../sim/ECellType";
|
import { ECellType } from '../sim/ECellType';
|
||||||
import { SimCell } from "../sim/SimCell";
|
import { SimCell } from '../sim/SimCell';
|
||||||
import { SimEnemy } from "../sim/SimEnemy";
|
import { SimEnemy } from '../sim/SimEnemy';
|
||||||
import { SimLevel } from "../sim/SimLevel";
|
import { SimLevel } from '../sim/SimLevel';
|
||||||
import { SimMain } from "../sim/SimMain";
|
import { SimMain } from '../sim/SimMain';
|
||||||
import { SimProjectile } from "../sim/SimProjectile";
|
import { SimProjectile } from '../sim/SimProjectile';
|
||||||
import { Hex } from "../util/Hex";
|
import { Hex } from '../util/Hex';
|
||||||
import { Vector2 } from "../util/Vector2";
|
import { Vector2 } from '../util/Vector2';
|
||||||
import { VisEnemy } from "./VisEnemy";
|
import { VisEnemy } from './VisEnemy';
|
||||||
import { VisMain } from "./VisMain";
|
import { VisMain } from './VisMain';
|
||||||
import { VisProjectile } from "./VisProjectile";
|
import { VisProjectile } from './VisProjectile';
|
||||||
|
|
||||||
export class VisLevel {
|
export class VisLevel {
|
||||||
private screenCellWidth: number = -1;
|
private screenCellWidth: number = -1;
|
||||||
@@ -27,12 +27,13 @@ export class VisLevel {
|
|||||||
private simMain: SimMain;
|
private simMain: SimMain;
|
||||||
private gdRoot: GdRoot;
|
private gdRoot: GdRoot;
|
||||||
assets: AssetPreloaderService;
|
assets: AssetPreloaderService;
|
||||||
|
private hoveredHex: Hex | null = null;
|
||||||
|
|
||||||
constructor(visMain: VisMain, simMain: SimMain, gdRoot: GdRoot, assets: AssetPreloaderService) {
|
constructor(visMain: VisMain, simMain: SimMain, gdRoot: GdRoot, assets: AssetPreloaderService) {
|
||||||
this.assets = assets;
|
this.assets = assets;
|
||||||
this.visMain = visMain;
|
this.visMain = visMain;
|
||||||
this.simMain = simMain;
|
this.simMain = simMain;
|
||||||
this.gdRoot = gdRoot
|
this.gdRoot = gdRoot;
|
||||||
this.enemyMap = new Map<SimEnemy, VisEnemy>();
|
this.enemyMap = new Map<SimEnemy, VisEnemy>();
|
||||||
this.projectileMap = new Map<SimProjectile, VisProjectile>();
|
this.projectileMap = new Map<SimProjectile, VisProjectile>();
|
||||||
this.reset();
|
this.reset();
|
||||||
@@ -58,7 +59,16 @@ export class VisLevel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.drawBackground();
|
this.drawBackground();
|
||||||
ctx.globalCompositeOperation = "source-over";
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
|
|
||||||
|
// Highlight hovered cell first (under everything else)
|
||||||
|
if (this.hoveredHex) {
|
||||||
|
const hoveredIdx = simLevel.getCellIndex(this.hoveredHex);
|
||||||
|
const hoveredCell = simLevel.cells[hoveredIdx];
|
||||||
|
if (hoveredCell && hoveredCell.distance <= gdLevel.radius) {
|
||||||
|
this.drawCellImage(ctx, hoveredCell, 'cell-highlighted.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
simLevel.cells.forEach((cell: SimCell) => {
|
simLevel.cells.forEach((cell: SimCell) => {
|
||||||
if (cell.distance > gdLevel.radius) {
|
if (cell.distance > gdLevel.radius) {
|
||||||
@@ -75,11 +85,11 @@ export class VisLevel {
|
|||||||
this.drawProjectile(projectile);
|
this.drawProjectile(projectile);
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.fillStyle = "white";
|
ctx.fillStyle = 'white';
|
||||||
ctx.fillText("Currency: " + simLevel.currency, 5, 15);
|
ctx.fillText('Currency: ' + simLevel.currency, 5, 15);
|
||||||
ctx.fillText("Current wave: " + simLevel.currentWave, 5, 35);
|
ctx.fillText('Current wave: ' + simLevel.currentWave, 5, 35);
|
||||||
ctx.fillText("Enemies left: " + simLevel.enemiesLeftToSpawn, 5, 55);
|
ctx.fillText('Enemies left: ' + simLevel.enemiesLeftToSpawn, 5, 55);
|
||||||
ctx.fillText("Current step: " + simLevel.currentStep, 5, 75);
|
ctx.fillText('Current step: ' + simLevel.currentStep, 5, 75);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateSize() {
|
public updateSize() {
|
||||||
@@ -89,11 +99,11 @@ export class VisLevel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gdLevel = this.gdRoot.levels[simLevel.index];
|
const gdLevel = this.gdRoot.levels[simLevel.index];
|
||||||
const minSize = Math.min(this.visMain.canvas.height, this.visMain.canvas.width * Math.sqrt(3) / 2);
|
const minSize = Math.min(this.visMain.canvas.height, (this.visMain.canvas.width * Math.sqrt(3)) / 2);
|
||||||
this.hexSize = Math.floor(minSize / (4 * (gdLevel.radius - 1) - 4));
|
this.hexSize = Math.floor(minSize / (4 * (gdLevel.radius - 1) - 4));
|
||||||
this.screenCellHeight = Math.ceil(2 * this.hexSize);
|
this.screenCellHeight = Math.ceil(2 * this.hexSize);
|
||||||
this.screenCellHeight = Math.ceil(this.screenCellHeight * 0.5) * 2;
|
this.screenCellHeight = Math.ceil(this.screenCellHeight * 0.5) * 2;
|
||||||
this.screenCellWidth = Math.ceil(Math.sqrt(3) / 2 * this.screenCellHeight);
|
this.screenCellWidth = Math.ceil((Math.sqrt(3) / 2) * this.screenCellHeight);
|
||||||
this.screenCellWidth = Math.ceil(this.screenCellWidth * 0.5) * 2;
|
this.screenCellWidth = Math.ceil(this.screenCellWidth * 0.5) * 2;
|
||||||
this.screenXOffset = this.screenCellWidth * (gdLevel.radius + 0.5);
|
this.screenXOffset = this.screenCellWidth * (gdLevel.radius + 0.5);
|
||||||
this.screenYOffset = this.screenCellHeight * (gdLevel.radius + 0.5);
|
this.screenYOffset = this.screenCellHeight * (gdLevel.radius + 0.5);
|
||||||
@@ -110,6 +120,11 @@ export class VisLevel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public updateEveryFrame(currentStep: number) {
|
public updateEveryFrame(currentStep: number) {
|
||||||
|
if (currentStep < 0) {
|
||||||
|
this.lastStep = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const simLevel = this.simMain.currentLevel;
|
const simLevel = this.simMain.currentLevel;
|
||||||
if (simLevel == null) {
|
if (simLevel == null) {
|
||||||
return;
|
return;
|
||||||
@@ -127,11 +142,9 @@ export class VisLevel {
|
|||||||
const visEnemy = this.enemyMap.get(simEnemy);
|
const visEnemy = this.enemyMap.get(simEnemy);
|
||||||
if (!visEnemy) {
|
if (!visEnemy) {
|
||||||
this.enemyMap.set(simEnemy, new VisEnemy(this.gdRoot, this.assets, simEnemy, this.screenCellWidth, this.screenCellHeight));
|
this.enemyMap.set(simEnemy, new VisEnemy(this.gdRoot, this.assets, simEnemy, this.screenCellWidth, this.screenCellHeight));
|
||||||
}
|
} else if (Math.floor(currentStep) != Math.floor(this.lastStep)) {
|
||||||
else if (Math.floor(currentStep) != Math.floor(this.lastStep)) {
|
|
||||||
visEnemy.advanceStep();
|
visEnemy.advanceStep();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
visEnemy.update(t);
|
visEnemy.update(t);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -149,8 +162,7 @@ export class VisLevel {
|
|||||||
const visProjectile = this.projectileMap.get(simProjectile);
|
const visProjectile = this.projectileMap.get(simProjectile);
|
||||||
if (!visProjectile) {
|
if (!visProjectile) {
|
||||||
this.projectileMap.set(simProjectile, new VisProjectile(simProjectile));
|
this.projectileMap.set(simProjectile, new VisProjectile(simProjectile));
|
||||||
}
|
} else if (Math.floor(currentStep) != Math.floor(this.lastStep)) {
|
||||||
else if (Math.floor(currentStep) != Math.floor(this.lastStep)) {
|
|
||||||
visProjectile.advanceStep();
|
visProjectile.advanceStep();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -161,6 +173,10 @@ export class VisLevel {
|
|||||||
this.lastStep = currentStep;
|
this.lastStep = currentStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setHoveredHex(hex: Hex | null) {
|
||||||
|
this.hoveredHex = hex;
|
||||||
|
}
|
||||||
|
|
||||||
public getScreenCoords(hex: Hex): Vector2 {
|
public getScreenCoords(hex: Hex): Vector2 {
|
||||||
const coord = Hex.toPixel(hex, this.hexSize);
|
const coord = Hex.toPixel(hex, this.hexSize);
|
||||||
return new Vector2(coord.x + this.screenXOffset - this.screenCellWidth / 2, coord.y + this.screenYOffset - this.screenCellHeight / 2);
|
return new Vector2(coord.x + this.screenXOffset - this.screenCellWidth / 2, coord.y + this.screenYOffset - this.screenCellHeight / 2);
|
||||||
@@ -182,34 +198,34 @@ export class VisLevel {
|
|||||||
const gdLevel = this.gdRoot.levels[simLevel.index];
|
const gdLevel = this.gdRoot.levels[simLevel.index];
|
||||||
if (this.background == null) {
|
if (this.background == null) {
|
||||||
const backgroundCanvas = this.visMain.canvas.cloneNode() as HTMLCanvasElement;
|
const backgroundCanvas = this.visMain.canvas.cloneNode() as HTMLCanvasElement;
|
||||||
const backgroundContext = backgroundCanvas.getContext("2d")!;
|
const backgroundContext = backgroundCanvas.getContext('2d')!;
|
||||||
this.background = backgroundCanvas;
|
this.background = backgroundCanvas;
|
||||||
simLevel.cells.forEach((cell: SimCell) => {
|
simLevel.cells.forEach((cell: SimCell) => {
|
||||||
if (cell.distance > gdLevel.radius) {
|
if (cell.distance > gdLevel.radius) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (cell.blockedType != -1 && cell.type == ECellType.Blocked) {
|
if (cell.blockedType != -1 && cell.type == ECellType.Blocked) {
|
||||||
this.drawCellImage(backgroundContext, cell, "cell-blocked-" + (cell.blockedType | 0) + ".svg");
|
this.drawCellImage(backgroundContext, cell, 'cell-blocked-' + (cell.blockedType | 0) + '.svg');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
backgroundContext.globalCompositeOperation = "source-atop";
|
backgroundContext.globalCompositeOperation = 'source-atop';
|
||||||
backgroundContext.fillStyle = this.visMain.wallPattern;
|
backgroundContext.fillStyle = this.visMain.wallPattern;
|
||||||
backgroundContext.fillRect(0, 0, this.visMain.canvas.width, this.visMain.canvas.height);
|
backgroundContext.fillRect(0, 0, this.visMain.canvas.width, this.visMain.canvas.height);
|
||||||
|
|
||||||
const cellCanvas = this.visMain.canvas.cloneNode() as HTMLCanvasElement;
|
const cellCanvas = this.visMain.canvas.cloneNode() as HTMLCanvasElement;
|
||||||
const cellContext = cellCanvas.getContext("2d")!;
|
const cellContext = cellCanvas.getContext('2d')!;
|
||||||
simLevel.cells.forEach((cell: SimCell) => {
|
simLevel.cells.forEach((cell: SimCell) => {
|
||||||
if (cell.distance > gdLevel.radius) {
|
if (cell.distance > gdLevel.radius) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (cell.type != ECellType.Entry) {
|
if (cell.type != ECellType.Entry) {
|
||||||
this.drawCellImage(cellContext, cell, "cell.svg");
|
this.drawCellImage(cellContext, cell, 'cell.svg');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
backgroundContext.globalCompositeOperation = "destination-over";
|
backgroundContext.globalCompositeOperation = 'destination-over';
|
||||||
backgroundContext.drawImage(cellCanvas, 0, 0);
|
backgroundContext.drawImage(cellCanvas, 0, 0);
|
||||||
backgroundContext.globalCompositeOperation = "source-over";
|
backgroundContext.globalCompositeOperation = 'source-over';
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.drawImage(this.background, 0, 0);
|
ctx.drawImage(this.background, 0, 0);
|
||||||
@@ -228,11 +244,7 @@ export class VisLevel {
|
|||||||
const pos = Vector2.lerp(positions[0], positions[1], t);
|
const pos = Vector2.lerp(positions[0], positions[1], t);
|
||||||
const width = this.screenCellWidth * simProjectile.size;
|
const width = this.screenCellWidth * simProjectile.size;
|
||||||
const height = this.screenCellHeight * simProjectile.size;
|
const height = this.screenCellHeight * simProjectile.size;
|
||||||
this.visMain.context.drawImage(this.assets.getImage("projectile.svg"),
|
this.visMain.context.drawImage(this.assets.getImage('images/projectile.svg'), this.screenXOffset + pos.x * this.hexSize - width / 2, this.screenYOffset + pos.y * this.hexSize - height / 2, width, height);
|
||||||
this.screenXOffset + pos.x * this.hexSize - width / 2,
|
|
||||||
this.screenYOffset + pos.y * this.hexSize - height / 2,
|
|
||||||
width,
|
|
||||||
height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawEnemy(simEnemy: SimEnemy) {
|
private drawEnemy(simEnemy: SimEnemy) {
|
||||||
@@ -246,12 +258,7 @@ export class VisLevel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pos = Vector2.lerp(positions[0], positions[1], t);
|
const pos = Vector2.lerp(positions[0], positions[1], t);
|
||||||
this.visMain.context.drawImage(
|
this.visMain.context.drawImage(visEnemy.image, this.screenXOffset + pos.x * this.hexSize - this.screenCellWidth / 2, this.screenYOffset + pos.y * this.hexSize - this.screenCellHeight / 2, this.screenCellWidth, this.screenCellHeight);
|
||||||
visEnemy.image,
|
|
||||||
this.screenXOffset + pos.x * this.hexSize - this.screenCellWidth / 2,
|
|
||||||
this.screenYOffset + pos.y * this.hexSize - this.screenCellHeight / 2,
|
|
||||||
this.screenCellWidth,
|
|
||||||
this.screenCellHeight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawCell(cell: SimCell) {
|
private drawCell(cell: SimCell) {
|
||||||
@@ -260,9 +267,9 @@ export class VisLevel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.visMain.context.fillStyle = "rgba(192, 192, 192, 0.25)";
|
this.visMain.context.fillStyle = 'rgba(192, 192, 192, 0.25)';
|
||||||
if (cell.type == ECellType.Entry) {
|
if (cell.type == ECellType.Entry) {
|
||||||
this.drawCellImage(this.visMain.context, cell, "cell-entry-" + (cell.blockedType | 0) + ".svg");
|
this.drawCellImage(this.visMain.context, cell, 'cell-entry-' + (cell.blockedType | 0) + '.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
const highlightedCell = simLevel.cells[simLevel.highlightedIndex];
|
const highlightedCell = simLevel.cells[simLevel.highlightedIndex];
|
||||||
@@ -279,20 +286,20 @@ export class VisLevel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (draw) {
|
if (draw) {
|
||||||
this.drawCellImage(this.visMain.context, highlightedCell, "cell-highlighted.svg");
|
this.drawCellImage(this.visMain.context, highlightedCell, 'cell-highlighted.svg');
|
||||||
this.visMain.context.fillStyle = "rgba(0, 0, 0, 1)";
|
this.visMain.context.fillStyle = 'rgba(0, 0, 0, 1)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cell.tower != null) {
|
if (cell.tower != null) {
|
||||||
this.drawCellImage(this.visMain.context, cell, "tower-" + (cell.tower.index | 0) + ".svg");
|
this.drawCellImage(this.visMain.context, cell, 'tower-' + (cell.tower.index | 0) + '.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
const coords = this.getScreenCoords(cell.hex);
|
const coords = this.getScreenCoords(cell.hex);
|
||||||
this.visMain.context.fillText("(" + cell.hex.col + ", " + cell.hex.row + ")", coords.x + 10, coords.y + this.screenCellHeight / 2 + 5);
|
this.visMain.context.fillText('(' + cell.hex.col + ', ' + cell.hex.row + ')', coords.x + 10, coords.y + this.screenCellHeight / 2 + 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawCellImage(context: CanvasRenderingContext2D, cell: SimCell, name: string) {
|
private drawCellImage(context: CanvasRenderingContext2D, cell: SimCell, name: string) {
|
||||||
const coords = this.getScreenCoords(cell.hex);
|
const coords = this.getScreenCoords(cell.hex);
|
||||||
context.drawImage(this.assets.getImage(name), coords.x, coords.y, this.screenCellWidth, this.screenCellHeight);
|
context.drawImage(this.assets.getImage('images/' + name), coords.x, coords.y, this.screenCellWidth, this.screenCellHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { AssetPreloaderService } from "../../../assetPreloaderService";
|
import { AssetPreloaderService } from '../../../assetPreloaderService';
|
||||||
import { SplashComponent } from "../../splash/splash.component";
|
import { SimMain } from '../sim/SimMain';
|
||||||
import { SimMain } from "../sim/SimMain";
|
import { VisLevel } from './VisLevel';
|
||||||
import { VisLevel } from "./VisLevel";
|
import { Vector2 } from '../util/Vector2';
|
||||||
|
import { InputHandler } from './input/InputHandler';
|
||||||
|
import { Hex } from '../util/Hex';
|
||||||
|
import { DefaultInputHandler } from './input/DefaultInputHandler';
|
||||||
|
|
||||||
export class VisMain {
|
export class VisMain {
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
@@ -9,29 +12,32 @@ export class VisMain {
|
|||||||
wallPattern: CanvasPattern;
|
wallPattern: CanvasPattern;
|
||||||
visLevel: VisLevel;
|
visLevel: VisLevel;
|
||||||
simMain: SimMain;
|
simMain: SimMain;
|
||||||
|
active: boolean = true;
|
||||||
|
assets: AssetPreloaderService;
|
||||||
|
wrapper: HTMLDivElement;
|
||||||
|
inputHandlers: InputHandler[] = [];
|
||||||
private startTimestamp: number = 0;
|
private startTimestamp: number = 0;
|
||||||
private active: boolean = true;
|
|
||||||
private ready: boolean = false;
|
private ready: boolean = false;
|
||||||
private gap: number = 0;
|
private gap: number = 0;
|
||||||
assets: AssetPreloaderService;
|
private animationFrameId: number | null = null;
|
||||||
|
|
||||||
constructor(simMain: SimMain, assets: AssetPreloaderService, canvas: HTMLCanvasElement) {
|
constructor(simMain: SimMain, assets: AssetPreloaderService, wrapper: HTMLDivElement, canvas: HTMLCanvasElement) {
|
||||||
this.assets = assets;
|
this.assets = assets;
|
||||||
this.simMain = simMain;
|
this.simMain = simMain;
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
this.context = this.canvas.getContext("2d")!;
|
this.wrapper = wrapper;
|
||||||
this.context.globalCompositeOperation = "source-over";
|
this.context = this.canvas.getContext('2d')!;
|
||||||
this.wallPattern = this.createPattern(assets.getImage("wall.png"), 48);
|
this.context.globalCompositeOperation = 'source-over';
|
||||||
|
this.wallPattern = this.createPattern(assets.getImage('images/wall.png'), 48);
|
||||||
this.visLevel = new VisLevel(this, this.simMain, this.simMain.gdRoot, assets);
|
this.visLevel = new VisLevel(this, this.simMain, this.simMain.gdRoot, assets);
|
||||||
const host = this;
|
this.setupInput();
|
||||||
requestAnimationFrame(function step(timestamp) {
|
this.start();
|
||||||
host.step(timestamp);
|
this.inputHandlers.push(new DefaultInputHandler(this));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createPattern(image: HTMLImageElement, size: number): CanvasPattern {
|
private createPattern(image: HTMLImageElement, size: number): CanvasPattern {
|
||||||
const tempCanvas = document.createElement("canvas");
|
const tempCanvas = document.createElement('canvas');
|
||||||
const tempContext = tempCanvas.getContext("2d")!;
|
const tempContext = tempCanvas.getContext('2d')!;
|
||||||
|
|
||||||
tempCanvas.width = size;
|
tempCanvas.width = size;
|
||||||
tempCanvas.height = size;
|
tempCanvas.height = size;
|
||||||
@@ -45,47 +51,45 @@ export class VisMain {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = this;
|
this.animationFrameId = requestAnimationFrame((timestamp: number) => {
|
||||||
requestAnimationFrame((timestamp: number) => {
|
this.step(timestamp);
|
||||||
host.step(timestamp);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.startTimestamp) {
|
|
||||||
this.startTimestamp = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
const simLevel = this.simMain.currentLevel;
|
const simLevel = this.simMain.currentLevel;
|
||||||
if (!simLevel) {
|
if (!simLevel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetStep = (timestamp - this.startTimestamp) * this.simMain.gdRoot.simulation.stepsPerSecond / 1000 - this.gap;
|
let targetStep = Math.floor(((timestamp - this.startTimestamp) * this.simMain.gdRoot.simulation.stepsPerSecond) / 1000 - this.gap);
|
||||||
if (simLevel.paused) {
|
if (simLevel.paused) {
|
||||||
this.gap += targetStep - simLevel.currentStep;
|
this.gap += targetStep - this.simMain.currentStep;
|
||||||
targetStep = simLevel.currentStep;
|
|
||||||
}
|
}
|
||||||
this.simMain.executeUntilStep(targetStep);
|
this.simMain.executeUntilStep(targetStep);
|
||||||
this.visLevel.updateEveryFrame(targetStep);
|
this.visLevel.updateEveryFrame(targetStep);
|
||||||
this.onRender();
|
this.onRender();
|
||||||
};
|
}
|
||||||
|
|
||||||
public onResized() {
|
public onResized() {
|
||||||
const gameHost = document.getElementById("game-host") as HTMLDivElement;
|
// Always get the latest wrapper size
|
||||||
const width = gameHost.clientWidth;
|
const width = this.wrapper.clientWidth;
|
||||||
const height = gameHost.clientHeight;
|
const height = this.wrapper.clientHeight;
|
||||||
const ratio = window.devicePixelRatio;
|
const ratio = window.devicePixelRatio;
|
||||||
|
|
||||||
|
// Set canvas style to always fill the wrapper
|
||||||
this.canvas.width = width * ratio;
|
this.canvas.width = width * ratio;
|
||||||
this.canvas.height = height * ratio;
|
this.canvas.height = height * ratio;
|
||||||
this.canvas.style.width = width + "px";
|
|
||||||
this.canvas.style.height = height + "px";
|
|
||||||
this.context.scale(ratio, ratio);
|
|
||||||
this.visLevel.updateSize();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRender() {
|
// Reset transform before scaling to avoid compounding
|
||||||
|
this.context.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
this.context.scale(ratio, ratio);
|
||||||
|
|
||||||
|
this.visLevel.updateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
onRender() {
|
||||||
this.clear();
|
this.clear();
|
||||||
const ctx = this.context;
|
const ctx = this.context;
|
||||||
ctx.font = "12px Tahoma";
|
ctx.font = '12px Tahoma';
|
||||||
const simLevel = this.simMain.currentLevel;
|
const simLevel = this.simMain.currentLevel;
|
||||||
if (!!simLevel) {
|
if (!!simLevel) {
|
||||||
if (!this.ready) {
|
if (!this.ready) {
|
||||||
@@ -94,13 +98,76 @@ export class VisMain {
|
|||||||
}
|
}
|
||||||
this.visLevel.draw();
|
this.visLevel.draw();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
private clear() {
|
private clear() {
|
||||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private stop() {
|
stop() {
|
||||||
this.active = false;
|
this.active = false;
|
||||||
|
this.startTimestamp = 0;
|
||||||
|
this.visLevel.updateEveryFrame(-1);
|
||||||
|
if (this.animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
|
this.animationFrameId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.active = true;
|
||||||
|
this.animationFrameId = requestAnimationFrame((timestamp: number) => {
|
||||||
|
this.startTimestamp = timestamp - (this.simMain.currentStep / this.simMain.gdRoot.simulation.stepsPerSecond) * 1000;
|
||||||
|
this.step(timestamp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupInput() {
|
||||||
|
this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e));
|
||||||
|
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
|
||||||
|
window.addEventListener('keydown', (e) => this.handleKeyDown(e));
|
||||||
|
window.addEventListener('resize', (_) => this.onResized());
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHexFromMouseCoords(event: MouseEvent): Hex {
|
||||||
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
|
const ratio = this.canvas.width / rect.width;
|
||||||
|
const x = ((event.clientX - rect.left) * ratio) / window.devicePixelRatio;
|
||||||
|
const y = ((event.clientY - rect.top) * ratio) / window.devicePixelRatio;
|
||||||
|
const coords = new Vector2(x, y);
|
||||||
|
const hex = this.visLevel.getHexFromScreenCoords(coords);
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseDown(event: MouseEvent) {
|
||||||
|
const hex = this.getHexFromMouseCoords(event);
|
||||||
|
for (let i = this.inputHandlers.length - 1; i >= 0; i--) {
|
||||||
|
const handled = this.inputHandlers[i].onMouseDown?.(event, hex);
|
||||||
|
if (handled) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseMove(event: MouseEvent) {
|
||||||
|
const hex = this.getHexFromMouseCoords(event);
|
||||||
|
for (let i = this.inputHandlers.length - 1; i >= 0; i--) {
|
||||||
|
const handled = this.inputHandlers[i].onMouseMove?.(event, hex);
|
||||||
|
if (handled) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown(event: KeyboardEvent) {
|
||||||
|
for (let i = this.inputHandlers.length - 1; i >= 0; i--) {
|
||||||
|
const handled = this.inputHandlers[i].onKeyDown?.(event);
|
||||||
|
if (handled) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/app/components/game/vis/input/DefaultInputHandler.ts
Normal file
47
src/app/components/game/vis/input/DefaultInputHandler.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { SimCommandBlockTerrain } from '../../sim/commands/SimCommandBlockTerrain';
|
||||||
|
import { SimCommandCreateTower } from '../../sim/commands/SimCommandCreateTower';
|
||||||
|
import { Hex } from '../../util/Hex';
|
||||||
|
import { VisMain } from '../VisMain';
|
||||||
|
import { HexInteractionInputHandler } from './HexInteractionInputHandler';
|
||||||
|
import { InputHandler } from './InputHandler';
|
||||||
|
|
||||||
|
export class DefaultInputHandler implements InputHandler {
|
||||||
|
visMain: VisMain;
|
||||||
|
|
||||||
|
constructor(visMain: VisMain) {
|
||||||
|
this.visMain = visMain;
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(event: KeyboardEvent): boolean {
|
||||||
|
if (event.code === 'Escape') {
|
||||||
|
if (this.visMain.inputHandlers.length > 1) {
|
||||||
|
this.visMain.inputHandlers.pop();
|
||||||
|
this.visMain.visLevel.setHoveredHex(null);
|
||||||
|
event.preventDefault();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (event.code === 'Space') {
|
||||||
|
if (this.visMain.active) {
|
||||||
|
this.visMain.stop();
|
||||||
|
} else {
|
||||||
|
this.visMain.start();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (event.code === 'KeyT') {
|
||||||
|
this.visMain.inputHandlers.push(new HexInteractionInputHandler(this.visMain, (hex) => new SimCommandCreateTower(hex, 0)));
|
||||||
|
return true;
|
||||||
|
} else if (event.code === 'KeyB') {
|
||||||
|
this.visMain.inputHandlers.push(new HexInteractionInputHandler(this.visMain, (hex) => new SimCommandBlockTerrain(hex)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown(event: MouseEvent, hex: Hex): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(event: MouseEvent, hex: Hex): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { SimCommand } from '../../sim/commands/SimCommand';
|
||||||
|
import { Hex } from '../../util/Hex';
|
||||||
|
import { VisMain } from '../VisMain';
|
||||||
|
import { InputHandler } from './InputHandler';
|
||||||
|
|
||||||
|
export class HexInteractionInputHandler implements InputHandler {
|
||||||
|
private hexCallback: (hex: Hex) => SimCommand;
|
||||||
|
private command: SimCommand | null = null;
|
||||||
|
visMain: VisMain;
|
||||||
|
|
||||||
|
constructor(visMain: VisMain, hexCallback: (hex: Hex) => SimCommand) {
|
||||||
|
this.visMain = visMain;
|
||||||
|
this.hexCallback = hexCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(event: KeyboardEvent): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown(event: MouseEvent, hex: Hex): boolean {
|
||||||
|
if (!this.command) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.visMain.simMain.addCommand(this.command);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(event: MouseEvent, hex: Hex): boolean {
|
||||||
|
this.command = this.hexCallback(hex);
|
||||||
|
if (this.command && this.command.check(this.visMain.simMain)) {
|
||||||
|
this.visMain.visLevel.setHoveredHex(hex);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.visMain.visLevel.setHoveredHex(null);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/app/components/game/vis/input/InputHandler.ts
Normal file
7
src/app/components/game/vis/input/InputHandler.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Hex } from "../../util/Hex";
|
||||||
|
|
||||||
|
export interface InputHandler {
|
||||||
|
onKeyDown?: (event: KeyboardEvent) => boolean;
|
||||||
|
onMouseDown?: (event: MouseEvent, hex: Hex) => boolean;
|
||||||
|
onMouseMove?: (event: MouseEvent, hex: Hex) => boolean;
|
||||||
|
}
|
||||||
@@ -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']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,18 +24,16 @@
|
|||||||
{
|
{
|
||||||
"radius": 9,
|
"radius": 9,
|
||||||
"currency": 1500,
|
"currency": 1500,
|
||||||
"walls": [
|
"walls": [{ "col": 0, "row": 0 }],
|
||||||
{ "col": 0, "row": 0 }
|
|
||||||
],
|
|
||||||
"enemySpawns": [
|
"enemySpawns": [
|
||||||
{ "col": -9, "row": -1},
|
{ "col": -9, "row": -1 },
|
||||||
{ "col": -9, "row": 0},
|
{ "col": -9, "row": 0 },
|
||||||
{ "col": -9, "row": 1}
|
{ "col": -9, "row": 1 }
|
||||||
],
|
],
|
||||||
"enemyTargets": [
|
"enemyTargets": [
|
||||||
{ "col": 8, "row": -1},
|
{ "col": 8, "row": -1 },
|
||||||
{ "col": 9, "row": 0},
|
{ "col": 9, "row": 0 },
|
||||||
{ "col": 8, "row": 1}
|
{ "col": 8, "row": 1 }
|
||||||
],
|
],
|
||||||
"enemyRoutes": [
|
"enemyRoutes": [
|
||||||
[0, 0],
|
[0, 0],
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user