This commit is contained in:
Bastian Wagner 2025-10-09 19:48:54 +02:00
commit 8704e6aac7
46 changed files with 11154 additions and 0 deletions

17
.editorconfig Normal file
View File

@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

43
.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
__screenshots__/
# System files
.DS_Store
Thumbs.db

4
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

20
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

42
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

59
README.md Normal file
View File

@ -0,0 +1,59 @@
# KingdomFoundry
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.5.
## Development server
To start a local development server, run:
```bash
ng serve
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the project run:
```bash
ng build
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

94
angular.json Normal file
View File

@ -0,0 +1,94 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"kingdom-foundry": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "kingdom-foundry:build:production"
},
"development": {
"buildTarget": "kingdom-foundry:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
]
}
}
}
}
},
"cli": {
"analytics": "bd092848-0e42-4407-9e4a-d5885b181d65"
}
}

9680
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "kingdom-foundry",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"prettier": {
"printWidth": 100,
"singleQuote": true,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
},
"private": true,
"dependencies": {
"@angular/common": "^20.3.0",
"@angular/compiler": "^20.3.0",
"@angular/core": "^20.3.0",
"@angular/forms": "^20.3.0",
"@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0",
"phaser": "^3.90.0",
"rxjs": "~7.8.0",
"seedrandom": "^3.0.5",
"simplex-noise": "^4.0.3",
"tslib": "^2.3.0"
},
"devDependencies": {
"@angular/build": "^20.3.5",
"@angular/cli": "^20.3.5",
"@angular/compiler-cli": "^20.3.0",
"@types/jasmine": "~5.1.0",
"@types/seedrandom": "^3.0.8",
"jasmine-core": "~5.9.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.9.2"
}
}

BIN
public/assets/lumberhut.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

12
src/app/app.config.ts Normal file
View File

@ -0,0 +1,12 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideRouter(routes)
]
};

1
src/app/app.html Normal file
View File

@ -0,0 +1 @@
<app-game></app-game>

3
src/app/app.routes.ts Normal file
View File

@ -0,0 +1,3 @@
import { Routes } from '@angular/router';
export const routes: Routes = [];

0
src/app/app.scss Normal file
View File

25
src/app/app.spec.ts Normal file
View File

@ -0,0 +1,25 @@
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [provideZonelessChangeDetection()]
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, kingdom-foundry');
});
});

13
src/app/app.ts Normal file
View File

@ -0,0 +1,13 @@
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { GameComponent } from './game/game';
@Component({
selector: 'app-root',
imports: [RouterOutlet, GameComponent],
templateUrl: './app.html',
styleUrl: './app.scss'
})
export class App {
protected readonly title = signal('kingdom-foundry');
}

View File

@ -0,0 +1,49 @@
import { Building } from "./building/building";
import { LumberHut } from "./building/lumber-building";
import { ResouceBuilding } from "./building/resource-building";
export class BuildingRenderer {
private scene: Phaser.Scene;
private sprites = new Map<Building, Phaser.GameObjects.Image>();
constructor(scene: Phaser.Scene) {
this.scene = scene;
}
/** Wird aufgerufen, wenn ein neues Gebäude entsteht */
addBuilding(building: Building) {
let textureKey = 'default';
switch (building.constructor) {
case LumberHut:
textureKey = 'lumberhut';
break;
case Storage:
textureKey = 'storage';
break;
default:
textureKey = 'default';
}
const sprite = this.scene.add.image(
building.x * 32 + 16, // tileSize=32
building.y * 32 + 16,
textureKey
);
sprite.setOrigin(0.5);
sprite.setDepth(5);
this.sprites.set(building, sprite);
}
/** Wird jedes Frame aufgerufen */
update() {
// Hier kannst du Animation, Statusfarben, etc. updaten
for (const [b, s] of this.sprites) {
// z.B. "aktiv" Gebäude leicht heller rendern
if (b instanceof ResouceBuilding && b.assignedWorker != null) {
s.setTint(0xffffff);
} else {
s.setTint(0xaaaaaa);
}
}
}
}

View File

@ -0,0 +1,63 @@
import { Building, BuildingType } from "./building/building";
import { LumberHut } from "./building/lumber-building";
import { StorageBuilding } from "./building/storage.building";
import { GameMap } from "./game-map";
import { ResourceManager, ResourceType } from "./resource-manager";
import { Worker } from "./worker";
export class BuildingSystem {
private buildings: Building[] = [];
constructor(private map: GameMap, private res: ResourceManager) {}
getAll(): Building[] {
return this.buildings;
}
tryPlaceBuilding(x: number, y:number, type: BuildingType): Building | null {
const tile = this.map.getTile(x, y);
if (!tile || tile.type == 'water') { return null; }
const occupied = this.buildings.some(b => b.x == x && b.y == y);
if (occupied) {
return null;
}
const cost = this.getCost(type);
if (!this.res.pay(cost)) {
console.warn('Nicht genug Ressourcen!');
return null;
}
if (type == 'lumberjack') {
const b = new LumberHut(this.map, x, y, this.buildings);
this.buildings.push(b);
const w = new Worker(x, y);
b.assignWorker(w);
return b;
} else if (type == 'storage') {
const b = new StorageBuilding(this.map, x, y, this.buildings);
this.buildings.push(b)
const w = new Worker(x,y);
b.assignWorker(w);
return b;
}
return null;
}
private getCost(type: BuildingType): Partial<Record<ResourceType, number>> {
switch (type) {
case 'house': return { wood: 5, stone: 2 };
case 'lumberjack': return { wood: 3, stone: 1 };
case 'quarry': return { wood: 2, stone: 3 };
default: return {};
}
}
update(dt: number) {
for (const b of this.buildings) {
b.update(dt)
}
}
}

View File

@ -0,0 +1,22 @@
import { GameMap } from "../game-map";
export type BuildingType = 'house' | 'lumberjack' | 'quarry' | 'storage';
export abstract class Building {
type: BuildingType = 'house';
readonly map: GameMap;
buildings: Building[] = [];
readonly x: number;
readonly y: number;
constructor(map: GameMap, x: number, y: number, buildings: Building[]) {
this.map = map;
this.x = x;
this.y = y;
this.buildings = buildings;
}
abstract update(dt: number): void;
}

View File

@ -0,0 +1,60 @@
import { GameMap } from "../game-map";
import { CutTreeJob } from "../job/cut-tree.job";
import { ResourceType } from "../resource-manager";
import { Tile } from "../tile";
import { Worker } from "../worker";
import { Building } from "./building";
import { ProductionBuilding } from "./production-building";
export class LumberHut extends ProductionBuilding {
constructor(map: GameMap, x: number, y: number, buildings: Building[]) {
super(map, x, y, buildings);
this.type = 'lumberjack';
}
override update(dt: number): void {
super.update(dt);
if (!this.assignedWorker) { return; }
if (!this.assignedWorker.idle) { return; }
if (this.storageFull) { return; }
const forest = this.findNearestResourceNode();
if (!forest) { return; }
this.assignedWorker.setJob(new CutTreeJob(this.map, this.assignedWorker, forest, this))
}
findNearestResourceNode(): Tile | null {
let best: Tile | null = null;
let bestDist = Infinity;
for (let dy = -10; dy <= 10; dy++) {
for (let dx = -10; dx <= 10; dx++) {
const tile = this.map.getTile(this.x + dx, this.y + dy);
if (!tile || tile.type !== 'forest') continue;
const dist = dx * dx + dy * dy;
if (dist < bestDist) {
bestDist = dist;
best = tile;
}
}
}
return best;
}
assignWorker(worker: Worker): void {
this.assignedWorker = worker;
}
collectGoods(): ResourceType | null {
if (this.stored.wood && this.stored.wood > 0) {
this.stored.wood -= 1;
return "wood";
}
return null;
}
}

View File

@ -0,0 +1,13 @@
import { Tile } from "../tile";
import { Worker } from "../worker";
import { ResouceBuilding } from "./resource-building";
export abstract class ProductionBuilding extends ResouceBuilding {
public progress = 0; // Produktionsfortschritt (01)
public hasOutput = false; // True, wenn Ressource fertig ist
abstract findNearestResourceNode(): Tile | null;
abstract collectGoods(): any;
}

View File

@ -0,0 +1,27 @@
import { GameMap } from "../game-map";
import { ResourceType } from "../resource-manager";
import { Building } from "./building";
import { Worker } from "../worker";
export abstract class ResouceBuilding extends Building {
maxStorage = 10;
public stored: Partial<Record<ResourceType, number>> = {};
public assignedWorker: Worker | null = null;
constructor(map: GameMap, x: number, y: number, buildings: Building[]) {
super(map, x, y, buildings);
}
get storageFull(): boolean {
return Object.values(this.stored).reduce((a, b) => a+b, 0) >= this.maxStorage;
}
abstract assignWorker(worker: Worker): void;
update(dt: number) {
if (this.assignedWorker) {
this.assignedWorker.update(dt)
}
}
}

View File

@ -0,0 +1,43 @@
import { GameMap } from "../game-map";
import { CollectMaterialJob } from "../job/collect-material.job";
import { Worker } from "../worker";
import { Building } from "./building";
import { ProductionBuilding } from "./production-building";
import { ResouceBuilding } from "./resource-building";
export class StorageBuilding extends ResouceBuilding {
constructor(map: GameMap, x: number, y: number, buildings: Building[]) {
super(map, x, y, buildings);
this.type = 'storage';
}
assignWorker(worker: Worker): void {
this.assignedWorker = worker;
}
override update(dt: number): void {
super.update(dt);
if (!this.assignedWorker) { return; }
if (!this.assignedWorker.idle) { return; }
if (this.storageFull) { return; }
const target = this.findNearestProdBuilding();
if (!target) {
console.log("Not Target found");
return;
}
this.assignedWorker.setJob(new CollectMaterialJob(this.map, this.assignedWorker, target, this))
}
findNearestProdBuilding(): ProductionBuilding | null {
const prod = this.buildings.filter(b => b instanceof ProductionBuilding);
// FINDE WOOD;
const b = prod.find(b => b.type == "lumberjack" && b.stored.wood && b.stored.wood > 0);
if (!b) { return null; }
return b;
}
}

View File

@ -0,0 +1,47 @@
import { Tile, TileType } from "./tile";
import {createNoise2D } from 'simplex-noise';
import seedrandom from 'seedrandom';
export class GameMap {
private tiles: Tile[] = [];
constructor(
public readonly width: number,
public readonly height: number,
private readonly seed: number = Date.now()
) {
this.generate();
}
private generate(): void {
// 👇 create a deterministic RNG from the seed
const rng = seedrandom(this.seed.toString());
const noise2D = createNoise2D(rng); // ✅ expects a RandomFn
this.tiles = [];
const scale = 0.08;
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const n = noise2D(x * scale, y * scale); // [-1, 1]
const value = (n + 1) / 2; // [0, 1]
let type: TileType = 'grass';
if (value < 0.15) type = 'water';
else if (value < 0.25) type = 'stone';
else if (value > 0.65) type = 'forest';
this.tiles.push(new Tile({ x, y, type }));
}
}
}
getTile(x: number, y: number): Tile | null {
if (x < 0 || y < 0 || x >= this.width || y >= this.height) return null;
return this.tiles[y * this.width + x];
}
forEach(fn: (tile: Tile) => void): void {
for (const tile of this.tiles) fn(tile);
}
}

View File

@ -0,0 +1,36 @@
import { GameMap } from "../game-map";
import { Worker } from "../worker";
export type JobState = 'pending' | 'walking' | 'working' | 'returning' | 'done';
export abstract class Job {
state: JobState = 'pending';
progress = 0;
workDuration = 3; // sekunden
protected map: GameMap;
protected worker: Worker;
constructor(map: GameMap, worker: Worker) {
this.map = map;
this.worker = worker;
}
/** Initialisierung (z. B. Pfad setzen) */
abstract start(): void;
/** Wird pro Frame aufgerufen */
abstract update(dt: number): void;
/** Ob Job abgeschlossen ist */
get done(): boolean {
return this.state === 'done';
}
walk(dt: number) {
if (this.worker.followPath(dt)) {
this.state = 'working';
this.progress = 0;
}
}
}

View File

@ -0,0 +1,63 @@
import { GameMap } from "../game-map";
import { findPath } from "../pathfinding";
import { Job } from "./base-job";
import { Worker } from "../worker";
import { ProductionBuilding } from "../building/production-building";
import { ResourceType } from "../resource-manager";
import { ResouceBuilding } from "../building/resource-building";
export class CollectMaterialJob extends Job {
private good: ResourceType | null = null;
constructor(
map: GameMap,
worker: Worker,
private target: ProductionBuilding,
private home: ResouceBuilding
) {
super(map, worker);
this.workDuration = 1; // 4 Sekunden Hackzeit
}
override start(): void {
const start = this.map.getTile(Math.floor(this.worker.x), Math.floor(this.worker.y))!;
const target = this.map.getTile(this.target.x, this.target.y)!;
const path = findPath(this.map, start, target);
this.worker.setPath(path);
this.state = 'walking';
}
override update(dt: number): void {
switch (this.state) {
case 'walking':
this.walk(dt);
break;
case 'working':
this.progress += dt / this.workDuration;
if (this.progress < 1) {
break;
}
this.good = this.target.collectGoods();
if (this.good == null) {
this.state = 'done';
break;
}
const homeTile = this.map.getTile(this.home.x, this.home.y)!;
const start = this.map.getTile(Math.floor(this.worker.x), Math.floor(this.worker.y))!;
const path = findPath(this.map, start, homeTile);
this.worker.setPath(path);
this.state = 'returning';
break;
case 'returning':
if (this.worker.followPath(dt)) {
if (this.good) {
this.home.stored[this.good] = (this.home.stored[this.good] ?? 0) + 1;
}
this.state = 'done';
}
break;
}
}
}

View File

@ -0,0 +1,60 @@
import { GameMap } from "../game-map";
import { findPath } from "../pathfinding";
import { Tile } from "../tile";
import { Job } from "./base-job";
import { Worker } from "../worker";
import { LumberHut } from "../building/lumber-building";
export class CutTreeJob extends Job {
constructor(
map: GameMap,
worker: Worker,
private target: Tile,
private home: LumberHut
) {
super(map, worker);
this.workDuration = 4; // 4 Sekunden Hackzeit
}
start(): void {
const start = this.map.getTile(Math.floor(this.worker.x), Math.floor(this.worker.y))!;
const path = findPath(this.map, start, this.target);
this.worker.setPath(path);
this.state = 'walking';
}
update(dt: number): void {
switch (this.state) {
case 'walking':
this.walk(dt);
break;
case 'working':
this.progress += dt / this.workDuration;
if (this.progress >= 1) {
const wood = this.target.harvestWood(1);
if (wood > 0) {
this.worker.carrying = wood;
const homeTile = this.map.getTile(this.home.x, this.home.y)!;
const start = this.map.getTile(Math.floor(this.worker.x), Math.floor(this.worker.y))!;
const path = findPath(this.map, start, homeTile);
this.worker.setPath(path);
this.state = 'returning';
} else {
this.state = 'done';
}
}
break;
case 'returning':
if (this.worker.followPath(dt)) {
this.home.stored['wood'] = (this.home.stored['wood'] ?? 0) + this.worker.carrying;
this.worker.carrying = 0;
this.state = 'done';
}
break;
}
}
}

14
src/app/game/core/jobs.ts Normal file
View File

@ -0,0 +1,14 @@
import { Building } from "./building/building";
import { Tile } from "./tile";
export type JobType = 'cutTree' | 'returnWood' | 'idle';
export type JobPhase = 'walking' | 'working' | 'returning' | 'done';
export interface Job {
type: JobType;
targetTile: Tile;
homeBuilding: Building;
phase: JobPhase;
progress: number; // 01
workDuration: number; // Sekunden für die Arbeit
}

View File

@ -0,0 +1,67 @@
import { GameMap } from './game-map';
import { Tile, TileType } from './tile';
function heuristic(ax: number, ay: number, bx: number, by: number) {
// Diagonale Heuristik (Chebyshev-Distanz)
return Math.max(Math.abs(ax - bx), Math.abs(ay - by));
}
export function findPath(map: GameMap, start: Tile, goal: Tile): Tile[] {
const open: Tile[] = [start];
const cameFrom = new Map<Tile, Tile>();
const gScore = new Map<Tile, number>();
const fScore = new Map<Tile, number>();
gScore.set(start, 0);
fScore.set(start, heuristic(start.x, start.y, goal.x, goal.y));
const walkable = (tile: Tile | null) =>
tile && tile.type !== 'water' && tile.type !== 'stone';
while (open.length > 0) {
// Knoten mit niedrigstem fScore wählen
open.sort(
(a, b) => (fScore.get(a) ?? Infinity) - (fScore.get(b) ?? Infinity)
);
const current = open.shift()!;
if (current === goal) {
const path: Tile[] = [];
let c: Tile | undefined = current;
while (c) {
path.unshift(c);
c = cameFrom.get(c);
}
return path;
}
// 🔸 Alle 8 Nachbarn (inkl. Diagonalen)
const neighborOffsets = [
[1, 0], [-1, 0], [0, 1], [0, -1],
[1, 1], [-1, -1], [1, -1], [-1, 1]
];
for (const [dx, dy] of neighborOffsets) {
const n = map.getTile(current.x + dx, current.y + dy);
if (!n ||!isWalkable(n)) {
continue;
}
// Diagonalbewegung etwas teurer machen
const moveCost = (dx !== 0 && dy !== 0) ? 1.414 : 1;
const tentativeG = (gScore.get(current) ?? Infinity) + moveCost;
if (tentativeG < (gScore.get(n) ?? Infinity)) {
cameFrom.set(n, current);
gScore.set(n, tentativeG);
fScore.set(n, tentativeG + heuristic(n.x, n.y, goal.x, goal.y));
if (!open.includes(n)) open.push(n);
}
}
}
console.log("return empty")
return []; // kein Pfad gefunden
}
export function isWalkable(tile: Tile | null): boolean {
return !!tile && tile.type !== 'water' && tile.type !== 'stone';
}

View File

@ -0,0 +1,11 @@
import { Building } from "./building/building";
export class ProductionSystem {
constructor(private buildings: Building[]) {}
update(dt: number) {
for (const b of this.buildings) {
//b.updateProduction(dt)
}
}
}

View File

@ -0,0 +1,38 @@
export type ResourceType = 'wood' | 'stone' | 'food' | 'gold';
export class ResourceManager {
private store: Record<ResourceType, number> = {
wood: 20,
stone: 10,
food: 10,
gold: 0,
};
/** Prüft, ob Ressourcen ausreichen */
canAfford(cost: Partial<Record<ResourceType, number>>): boolean {
return Object.entries(cost).every(([type, amount]) =>
(this.store[type as ResourceType] ?? 0) >= (amount ?? 0)
);
}
/** Zieht Ressourcen ab */
pay(cost: Partial<Record<ResourceType, number>>): boolean {
if (!this.canAfford(cost)) return false;
for (const [type, amount] of Object.entries(cost)) {
this.store[type as ResourceType] -= amount ?? 0;
}
return true;
}
/** Fügt Ressourcen hinzu (z. B. Produktion) */
add(delta: Partial<Record<ResourceType, number>>): void {
for (const [type, amount] of Object.entries(delta)) {
this.store[type as ResourceType] += amount ?? 0;
}
}
/** Gibt aktuellen Stand zurück */
getAll(): Record<ResourceType, number> {
return { ...this.store };
}
}

36
src/app/game/core/tile.ts Normal file
View File

@ -0,0 +1,36 @@
export type TileType = 'grass' | 'forest' | 'stone' | 'water';
export class Tile implements ITile {
readonly x: number;
readonly y: number;
type: TileType;
public woodAmount = 0;
constructor({x, y, type}: ITile) {
this.x = x;
this.y = y;
this.type = type;
if (type === 'forest') {
this.woodAmount = Phaser.Math.Between(3, 6); // z. B. 36 Holz pro Baum
}
}
harvestWood(amount: number): number {
if (this.type !== 'forest') return 0;
const taken = Math.min(amount, this.woodAmount);
this.woodAmount -= taken;
if (this.woodAmount <= 0) {
this.type = 'grass';
}
return taken;
}
}
export interface ITile {
x: number;
y: number;
type: TileType;
}

View File

@ -0,0 +1,61 @@
import { Job } from "./job/base-job";
export class Worker {
x = 0;
y = 0;
speed = 1;
carrying = 0;
job: Job | null = null;
path: any[] = [];
pathIndex = 0;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
update(dt: number) {
if (this.job) {
this.job.update(dt);
if (this.job.done) this.job = null;
}
}
setJob(job: Job) {
this.job = job;
job.start();
}
setPath(path: any[]) {
this.path = path;
this.pathIndex = 0;
}
followPath(dt: number): boolean {
if (this.path.length === 0) return true;
const target = this.path[this.pathIndex];
const dx = target.x - this.x;
const dy = target.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.05) {
this.x = target.x;
this.y = target.y;
this.pathIndex++;
if (this.pathIndex >= this.path.length) {
this.path = [];
return true;
}
} else {
const nx = dx / dist;
const ny = dy / dist;
this.x += nx * this.speed * dt;
this.y += ny * this.speed * dt;
}
return false;
}
get idle(): boolean {
console.log(this.job == null)
return this.job == null;
}
}

1
src/app/game/game.html Normal file
View File

@ -0,0 +1 @@
<p>game works!</p>

0
src/app/game/game.scss Normal file
View File

23
src/app/game/game.spec.ts Normal file
View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Game } from './game';
describe('Game', () => {
let component: Game;
let fixture: ComponentFixture<Game>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Game]
})
.compileComponents();
fixture = TestBed.createComponent(Game);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

33
src/app/game/game.ts Normal file
View File

@ -0,0 +1,33 @@
import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from "@angular/core";
import Phaser from "phaser";
import { GameScene } from "./scenes/game.scene";
@Component({
selector: 'app-game',
template: `<div #gameContainer class="game-container"></div>`,
styleUrls: ['./game.scss']
})
export class GameComponent implements OnInit, OnDestroy {
@ViewChild('gameContainer', { static: true }) gameContainer!: ElementRef<HTMLDivElement>;
private game?: Phaser.Game;
ngOnInit(): void {
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: window.innerWidth,
height: window.innerHeight,
backgroundColor: '#0f111a',
parent: this.gameContainer.nativeElement,
scene: [GameScene],
physics: { default: 'arcade' }
};
this.game = new Phaser.Game(config);
// 👇 Debug: global im Fenster verfügbar machen
(window as any).game = this.game;
}
ngOnDestroy(): void {
this.game?.destroy(true);
}
}

View File

@ -0,0 +1,235 @@
import Phaser from 'phaser';
import { GameMap } from '../core/game-map';
import { Tile } from '../core/tile';
import { BuildingSystem } from '../core/building-system';
import { ResourceManager } from '../core/resource-manager';
import { Worker } from '../core/worker';
import { ProductionSystem } from '../core/production-system';
import { BuildingType } from '../core/building/building';
import { ResouceBuilding } from '../core/building/resource-building';
import { BuildingRenderer } from '../core/building-renderer';
export class GameScene extends Phaser.Scene {
private map!: GameMap;
private gfx!: Phaser.GameObjects.Graphics;
private buildingRenderer!: BuildingRenderer;
private hoveredTile: { x: number; y: number } | null = null;
private highlightGfx!: Phaser.GameObjects.Graphics;
private tileSize = 32;
private camSpeed = 600;
private zoom = 1;
private buildingSystem!: BuildingSystem;
private productionSystem!: ProductionSystem;
private selectedBuilding: BuildingType | null = null;
private resourceManager!: ResourceManager;
private workers: Worker[] = [];
constructor() {
super('GameScene');
}
preload() {
this.load.image('lumberhut', 'assets/lumberhut.png');
}
create() {
this.map = new GameMap(19, 10, 123);
this.gfx = this.add.graphics();
// Highlight-Layer (separate Graphics)
this.highlightGfx = this.add.graphics();
this.highlightGfx.setDepth(10); // über der Karte zeichnen
this.buildingRenderer = new BuildingRenderer(this);
// 📸 Kamera einstellen
const cam = this.cameras.main;
cam.setBounds(0, 0, this.map.width * this.tileSize, this.map.height * this.tileSize);
cam.centerOn(this.map.width * this.tileSize / 2, this.map.height * this.tileSize / 2);
cam.setZoom(this.zoom);
// 🕹️ Steuerung
this.setupCameraControls();
this.resourceManager = new ResourceManager();
this.buildingSystem = new BuildingSystem(this.map, this.resourceManager);
this.productionSystem = new ProductionSystem(this.buildingSystem.getAll())
// Erstes Rendern
this.renderMap();
// Testweise standardmäßig Haus als Werkzeug
this.selectedBuilding = 'lumberjack';
}
private setupCameraControls() {
const cam = this.cameras.main;
const keys = this.input.keyboard!.addKeys('W,A,S,D') as Record<string, Phaser.Input.Keyboard.Key>;
// Bewegung
this.events.on('update', (_: number, dtMs: number) => {
const dt = dtMs / 1000;
if (keys['W'].isDown) cam.scrollY -= this.camSpeed * dt;
if (keys['S'].isDown) cam.scrollY += this.camSpeed * dt;
if (keys['A'].isDown) cam.scrollX -= this.camSpeed * dt;
if (keys['D'].isDown) cam.scrollX += this.camSpeed * dt;
});
// Rechtsklick-Drag
this.input.on('pointermove', (p: Phaser.Input.Pointer) => {
if (p.rightButtonDown()) {
cam.scrollX -= p.velocity.x / cam.zoom;
cam.scrollY -= p.velocity.y / cam.zoom;
} else {
const worldPoint = this.cameras.main.getWorldPoint(p.x, p.y);
const tx = Math.floor(worldPoint.x / this.tileSize);
const ty = Math.floor(worldPoint.y / this.tileSize);
const tile = this.map.getTile(tx, ty);
this.hoveredTile = tile ? { x: tx, y: ty } : null;
}
});
// Mausrad-Zoom mit Fokus auf Mauszeiger
this.input.on(
'wheel',
(
pointer: Phaser.Input.Pointer,
over: any[],
dx: number,
dy: number,
dz: number
) => {
const cam = this.cameras.main;
const oldZoom = cam.zoom;
const newZoom = Phaser.Math.Clamp(oldZoom - dy * 0.001, 0.3, 3);
const worldPoint = cam.getWorldPoint(pointer.x, pointer.y);
cam.setZoom(newZoom);
const newWorldPoint = cam.getWorldPoint(pointer.x, pointer.y);
cam.scrollX += worldPoint.x - newWorldPoint.x;
cam.scrollY += worldPoint.y - newWorldPoint.y;
this.zoom = newZoom;
}
);
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
if (pointer.rightButtonDown()) return; // rechts: Kamera
if (!this.hoveredTile) return;
const tile = this.map.getTile(this.hoveredTile.x, this.hoveredTile.y);
this.clickOnTile(tile);
});
}
clickOnTile(tile: Tile | null) {
if (!tile) { return; }
console.log(`Tile [${tile.x}, ${tile.y}] → ${tile.type}`);
if (this.selectedBuilding != null) {
const succ = this.buildingSystem.tryPlaceBuilding(tile.x, tile.y, this.selectedBuilding);
if (succ) {
this.buildingRenderer.addBuilding(succ)
}
this.selectedBuilding = 'storage';
}
}
private renderMap() {
const g = this.gfx;
g.clear();
const cam = this.cameras.main;
const ts = this.tileSize;
// Nur sichtbare Tiles zeichnen
const left = Math.floor(cam.scrollX / ts) - 2;
const top = Math.floor(cam.scrollY / ts) - 2;
const right = Math.ceil((cam.scrollX + cam.width / cam.zoom) / ts) + 2;
const bottom = Math.ceil((cam.scrollY + cam.height / cam.zoom) / ts) + 2;
this.map.forEach((tile: Tile) => {
if (tile.x < left || tile.x > right || tile.y < top || tile.y > bottom) return;
g.fillStyle(this.tileColor(tile.type), 1);
g.fillRect(tile.x * ts, tile.y * ts, ts - 1, ts - 1);
});
// Gebäude zeichnen
for (const b of this.buildingSystem.getAll()) {
// g.fillStyle(0xffcc66, 1);
// g.fillRect(b.x * this.tileSize, b.y * this.tileSize, this.tileSize - 1, this.tileSize - 1);
if (b instanceof ResouceBuilding && b.assignedWorker) {
g.fillStyle(0xffffff, 1);
g.fillCircle(
b.assignedWorker.x * this.tileSize + this.tileSize / 2,
b.assignedWorker.y * this.tileSize + this.tileSize / 2,
this.tileSize / 4);
}
}
}
private tileColor(type: string): number {
switch (type) {
case 'grass': return 0x3a6b35;
case 'forest': return 0x285028;
case 'stone': return 0x8a8a8a;
case 'water': return 0x244b6b;
default: return 0xff00ff;
}
}
override update() {
this.renderMap();
// Highlight zeichnen
if (this.hoveredTile) {
const { x, y } = this.hoveredTile;
if (this.highlightGfx) {
this.highlightGfx.clear();
}
this.highlightGfx.lineStyle(2, 0xffffff, 0.8);
this.highlightGfx.strokeRect(
x * this.tileSize,
y * this.tileSize,
this.tileSize - 1,
this.tileSize - 1
);
} else {
if (this.highlightGfx) {
this.highlightGfx.clear();
}
}
const dt = this.game.loop.delta / 1000;
// Produktion tickt
this.productionSystem.update(dt);
this.buildingSystem.update(dt);
this.buildingRenderer.update();
}
// private tileColor(kind: TileKind): number {
// switch (kind) {
// case 'grass': return 0x3a6b35;
// case 'forest': return 0x285028;
// case 'stone': return 0x8a8a8a;
// case 'water': return 0x244b6b;
// }
// }
}

13
src/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>KingdomFoundry</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

6
src/main.ts Normal file
View File

@ -0,0 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));

6
src/styles.scss Normal file
View File

@ -0,0 +1,6 @@
/* You can add global styles to this file, and also import other style files */
html, body {
margin: 0;
padding: 0;
overflow: hidden;
}

15
tsconfig.app.json Normal file
View File

@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.spec.ts"
]
}

34
tsconfig.json Normal file
View File

@ -0,0 +1,34 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

14
tsconfig.spec.json Normal file
View File

@ -0,0 +1,14 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.ts"
]
}