init
This commit is contained in:
commit
8704e6aac7
17
.editorconfig
Normal file
17
.editorconfig
Normal 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
43
.gitignore
vendored
Normal 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
4
.vscode/extensions.json
vendored
Normal 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
20
.vscode/launch.json
vendored
Normal 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
42
.vscode/tasks.json
vendored
Normal 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
59
README.md
Normal 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
94
angular.json
Normal 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
9680
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
package.json
Normal file
51
package.json
Normal 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
BIN
public/assets/lumberhut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
BIN
public/assets/original/lumberhut.png
Normal file
BIN
public/assets/original/lumberhut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
12
src/app/app.config.ts
Normal file
12
src/app/app.config.ts
Normal 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
1
src/app/app.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<app-game></app-game>
|
||||||
3
src/app/app.routes.ts
Normal file
3
src/app/app.routes.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
export const routes: Routes = [];
|
||||||
0
src/app/app.scss
Normal file
0
src/app/app.scss
Normal file
25
src/app/app.spec.ts
Normal file
25
src/app/app.spec.ts
Normal 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
13
src/app/app.ts
Normal 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');
|
||||||
|
}
|
||||||
49
src/app/game/core/building-renderer.ts
Normal file
49
src/app/game/core/building-renderer.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/app/game/core/building-system.ts
Normal file
63
src/app/game/core/building-system.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/app/game/core/building/building.ts
Normal file
22
src/app/game/core/building/building.ts
Normal 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;
|
||||||
|
}
|
||||||
60
src/app/game/core/building/lumber-building.ts
Normal file
60
src/app/game/core/building/lumber-building.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
13
src/app/game/core/building/production-building.ts
Normal file
13
src/app/game/core/building/production-building.ts
Normal 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 (0–1)
|
||||||
|
public hasOutput = false; // True, wenn Ressource fertig ist
|
||||||
|
|
||||||
|
abstract findNearestResourceNode(): Tile | null;
|
||||||
|
|
||||||
|
abstract collectGoods(): any;
|
||||||
|
|
||||||
|
}
|
||||||
27
src/app/game/core/building/resource-building.ts
Normal file
27
src/app/game/core/building/resource-building.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/app/game/core/building/storage.building.ts
Normal file
43
src/app/game/core/building/storage.building.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/app/game/core/game-map.ts
Normal file
47
src/app/game/core/game-map.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/app/game/core/job/base-job.ts
Normal file
36
src/app/game/core/job/base-job.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/app/game/core/job/collect-material.job.ts
Normal file
63
src/app/game/core/job/collect-material.job.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/app/game/core/job/cut-tree.job.ts
Normal file
60
src/app/game/core/job/cut-tree.job.ts
Normal 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
14
src/app/game/core/jobs.ts
Normal 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; // 0–1
|
||||||
|
workDuration: number; // Sekunden für die Arbeit
|
||||||
|
}
|
||||||
67
src/app/game/core/pathfinding.ts
Normal file
67
src/app/game/core/pathfinding.ts
Normal 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';
|
||||||
|
}
|
||||||
11
src/app/game/core/production-system.ts
Normal file
11
src/app/game/core/production-system.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/app/game/core/resource-manager.ts
Normal file
38
src/app/game/core/resource-manager.ts
Normal 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
36
src/app/game/core/tile.ts
Normal 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. 3–6 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;
|
||||||
|
}
|
||||||
61
src/app/game/core/worker.ts
Normal file
61
src/app/game/core/worker.ts
Normal 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
1
src/app/game/game.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<p>game works!</p>
|
||||||
0
src/app/game/game.scss
Normal file
0
src/app/game/game.scss
Normal file
23
src/app/game/game.spec.ts
Normal file
23
src/app/game/game.spec.ts
Normal 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
33
src/app/game/game.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
235
src/app/game/scenes/game.scene.ts
Normal file
235
src/app/game/scenes/game.scene.ts
Normal 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
13
src/index.html
Normal 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
6
src/main.ts
Normal 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
6
src/styles.scss
Normal 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
15
tsconfig.app.json
Normal 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
34
tsconfig.json
Normal 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
14
tsconfig.spec.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user