angular ui
This commit is contained in:
5
src/app/components/game/command.ts
Normal file
5
src/app/components/game/command.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Command {
|
||||
step: number;
|
||||
type: string;
|
||||
payload: any;
|
||||
}
|
||||
40
src/app/components/game/game.component.css
Normal file
40
src/app/components/game/game.component.css
Normal file
@@ -0,0 +1,40 @@
|
||||
.game-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
width: 250px; /* resizable width later */
|
||||
background-color: #222;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.main-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: black;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
height: 80px;
|
||||
background-color: #444;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
}
|
||||
25
src/app/components/game/game.component.html
Normal file
25
src/app/components/game/game.component.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<div class="game-layout">
|
||||
<div class="left-panel">Sidebar content</div>
|
||||
|
||||
<div class="main-area">
|
||||
<div class="canvas-wrapper" #canvasWrapper>
|
||||
<canvas #gameCanvas></canvas>
|
||||
</div>
|
||||
<div class="bottom-panel">
|
||||
<button (click)="rewind()">⏮ Rewind</button>
|
||||
<button (click)="start()">▶ Play</button>
|
||||
<button (click)="stop()">⏸ Pause</button>
|
||||
<button (click)="step()">Step</button>
|
||||
<button (click)="fastForward()">⏭ Fast Forward</button>
|
||||
<div>Step: {{ engine.currentStep }}</div>
|
||||
<button (click)="issueMove()">🚶 Move Unit</button>
|
||||
<button (click)="openOptions()">Options</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-options
|
||||
*ngIf="optionsOpen"
|
||||
(closed)="closeOptions()"
|
||||
[mode]="'modal'"
|
||||
></app-options>
|
||||
154
src/app/components/game/game.component.ts
Normal file
154
src/app/components/game/game.component.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { OptionsComponent } from '../options/options.component';
|
||||
import { GameState } from './gameState';
|
||||
import { SimulationEngine } from './simulationEngine';
|
||||
import { GameRules } from './gameRules';
|
||||
import { Command } from './command';
|
||||
|
||||
@Component({
|
||||
selector: 'app-game',
|
||||
templateUrl: './game.component.html',
|
||||
styleUrls: ['./game.component.css'],
|
||||
imports: [OptionsComponent],
|
||||
})
|
||||
export class GameComponent implements OnInit {
|
||||
engine!: SimulationEngine;
|
||||
private optionsOpen = false;
|
||||
private isPaused = false;
|
||||
private lastFrameTime = 0;
|
||||
|
||||
@ViewChild('gameCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
@ViewChild('canvasWrapper') wrapperRef!: ElementRef<HTMLDivElement>;
|
||||
private logicalWidth = 800;
|
||||
private logicalHeight = 600;
|
||||
scaleX: number = 1;
|
||||
scaleY: number = 1;
|
||||
pixelScale: number = 1;
|
||||
|
||||
async ngOnInit() {
|
||||
const initialState: GameState = {
|
||||
tick: 0,
|
||||
units: [{ id: 'u1', x: 0, y: 0 }],
|
||||
commandHistory: []
|
||||
};
|
||||
|
||||
const json = await fetch('/assets/gameData.json').then(r => r.json());
|
||||
this.engine = new SimulationEngine(initialState, GameRules, json);
|
||||
requestAnimationFrame(this.gameLoop.bind(this));
|
||||
}
|
||||
|
||||
start() {
|
||||
this.engine.start();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.engine.stop();
|
||||
}
|
||||
|
||||
step() {
|
||||
this.engine.step();
|
||||
}
|
||||
|
||||
rewind() {
|
||||
this.engine.rewindToZero();
|
||||
}
|
||||
|
||||
fastForward() {
|
||||
this.engine.fastForwardToEnd();
|
||||
}
|
||||
|
||||
async reloadGameData() {
|
||||
const json = await fetch('/assets/gameData.json').then(r => r.json());
|
||||
this.engine.reinitializeWithNewData(json);
|
||||
}
|
||||
|
||||
issueMove() {
|
||||
const cmd: Command = {
|
||||
step: this.engine.currentStep + 1,
|
||||
type: 'move',
|
||||
payload: { id: 'u1', dx: 1, dy: 0 }
|
||||
};
|
||||
this.engine.issueCommand(cmd);
|
||||
}
|
||||
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.resizeCanvas();
|
||||
window.addEventListener('resize', this.resizeCanvas.bind(this));
|
||||
}
|
||||
|
||||
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
|
||||
public openOptions(): void {
|
||||
this.optionsOpen = true;
|
||||
this.pauseGame();
|
||||
}
|
||||
|
||||
// Called when closing options: resume the game
|
||||
public closeOptions(): void {
|
||||
this.optionsOpen = false;
|
||||
this.resumeGame();
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
6
src/app/components/game/gameData.ts
Normal file
6
src/app/components/game/gameData.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface GameData {
|
||||
unitSpeed: number;
|
||||
maxUnits: number;
|
||||
enemySpawnRate: number;
|
||||
// anything else that's tunable
|
||||
}
|
||||
37
src/app/components/game/gameRules.ts
Normal file
37
src/app/components/game/gameRules.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
7
src/app/components/game/gameState.ts
Normal file
7
src/app/components/game/gameState.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Command } from "./command";
|
||||
|
||||
export interface GameState {
|
||||
tick: number;
|
||||
units: { id: string; x: number; y: number }[];
|
||||
commandHistory: Command[];
|
||||
}
|
||||
97
src/app/components/game/simulationEngine.ts
Normal file
97
src/app/components/game/simulationEngine.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Command } from './command';
|
||||
import { GameData } from './gameData';
|
||||
import { GameRules } from './gameRules';
|
||||
import { GameState } from './gameState';
|
||||
|
||||
export class SimulationEngine {
|
||||
private interval = 50; // ms per step
|
||||
private lastTime = 0;
|
||||
private frameRequestId: number | null = null;
|
||||
private data: GameData;
|
||||
|
||||
currentStep = 0;
|
||||
initialState: GameState;
|
||||
state: GameState;
|
||||
isRunning = false;
|
||||
|
||||
constructor(
|
||||
initialState: GameState,
|
||||
private rules: typeof GameRules,
|
||||
initialData: GameData
|
||||
) {
|
||||
this.initialState = JSON.parse(JSON.stringify(initialState));
|
||||
this.state = JSON.parse(JSON.stringify(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.state = this.rules.step(this.state, this.data);
|
||||
this.currentStep++;
|
||||
}
|
||||
|
||||
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 = JSON.parse(JSON.stringify(this.initialState));
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
0
src/app/components/menu/menu.component.css
Normal file
0
src/app/components/menu/menu.component.css
Normal file
4
src/app/components/menu/menu.component.html
Normal file
4
src/app/components/menu/menu.component.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<h1>Menu Page</h1>
|
||||
<p>Select an option:</p>
|
||||
<button (click)="navigateToGame()">Play Game</button>
|
||||
<button (click)="navigateToOptions()">Options</button>
|
||||
19
src/app/components/menu/menu.component.ts
Normal file
19
src/app/components/menu/menu.component.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu',
|
||||
templateUrl: './menu.component.html',
|
||||
styleUrls: ['./menu.component.css'],
|
||||
})
|
||||
export class MenuComponent {
|
||||
constructor(private router: Router) {}
|
||||
|
||||
navigateToGame(): void {
|
||||
this.router.navigate(['/game']);
|
||||
}
|
||||
|
||||
navigateToOptions(): void {
|
||||
this.router.navigate(['/options'], { state: { mode: 'full' } });
|
||||
}
|
||||
}
|
||||
0
src/app/components/options/options.component.css
Normal file
0
src/app/components/options/options.component.css
Normal file
2
src/app/components/options/options.component.html
Normal file
2
src/app/components/options/options.component.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Options Page</h1>
|
||||
<p>Configure your preferences here.</p>
|
||||
19
src/app/components/options/options.component.ts
Normal file
19
src/app/components/options/options.component.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-options',
|
||||
templateUrl: './options.component.html',
|
||||
styleUrls: ['./options.component.css'],
|
||||
})
|
||||
export class OptionsComponent {
|
||||
@Input() mode: 'modal' | 'full' = 'full';
|
||||
|
||||
constructor(private router: Router) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const nav = this.router.getCurrentNavigation();
|
||||
const state = nav?.extras?.state as { mode: 'modal' | 'full' };
|
||||
this.mode = state?.mode || 'full';
|
||||
}
|
||||
}
|
||||
4
src/app/components/splash/splash.component.css
Normal file
4
src/app/components/splash/splash.component.css
Normal file
@@ -0,0 +1,4 @@
|
||||
h1 {
|
||||
color: blue;
|
||||
text-align: center;
|
||||
}
|
||||
1
src/app/components/splash/splash.component.html
Normal file
1
src/app/components/splash/splash.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>Welcome to the Splash Page!</h1>
|
||||
19
src/app/components/splash/splash.component.ts
Normal file
19
src/app/components/splash/splash.component.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AssetPreloaderService } from '../../assetPreloaderService';
|
||||
|
||||
@Component({
|
||||
selector: 'app-splash',
|
||||
templateUrl: './splash.component.html',
|
||||
styleUrls: ['./splash.component.css'],
|
||||
})
|
||||
export class SplashComponent {
|
||||
show: boolean = true;
|
||||
assetPreloader: AssetPreloaderService = new AssetPreloaderService();
|
||||
progress: number = 0;
|
||||
|
||||
constructor(private router: Router) {
|
||||
this.assetPreloader.progress$.subscribe(p => this.progress = p);
|
||||
this.assetPreloader.preload().then(() => this.router.navigate(['/menu']));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user