diff --git a/package-lock.json b/package-lock.json
index 6cad3d0..418b4d6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,9 +12,11 @@
"@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0",
"@angular/forms": "^19.2.0",
+ "@angular/material": "^19.2.11",
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0",
+ "phaser": "^3.88.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
@@ -493,6 +495,22 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/@angular/cdk": {
+ "version": "19.2.11",
+ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.11.tgz",
+ "integrity": "sha512-G568yWIJlnsuS563WxvCofmxc1405+wRQvDGQ32+qWOblJScFkHgr4jeDkZGcyt/r8OudaW0H0/rNeg1dzdnIQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "parse5": "^7.1.2",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "^19.0.0 || ^20.0.0",
+ "@angular/core": "^19.0.0 || ^20.0.0",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
"node_modules/@angular/cli": {
"version": "19.2.9",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.9.tgz",
@@ -666,6 +684,23 @@
"rxjs": "^6.5.3 || ^7.4.0"
}
},
+ "node_modules/@angular/material": {
+ "version": "19.2.11",
+ "resolved": "https://registry.npmjs.org/@angular/material/-/material-19.2.11.tgz",
+ "integrity": "sha512-0OWwv55Il25mit7oGTloMeKVi0v/q1tr13wUJj0KJOcvICA6JCEW6VEc9zqYmkMPstDCx96cSJgPKxkHjKYyqg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/cdk": "19.2.11",
+ "@angular/common": "^19.0.0 || ^20.0.0",
+ "@angular/core": "^19.0.0 || ^20.0.0",
+ "@angular/forms": "^19.0.0 || ^20.0.0",
+ "@angular/platform-browser": "^19.0.0 || ^20.0.0",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
"node_modules/@angular/platform-browser": {
"version": "19.2.8",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.8.tgz",
@@ -11161,7 +11196,6 @@
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
@@ -11202,7 +11236,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
"integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
- "dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@@ -11302,6 +11335,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/phaser": {
+ "version": "3.88.2",
+ "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.88.2.tgz",
+ "integrity": "sha512-UBgd2sAFuRJbF2xKaQ5jpMWB8oETncChLnymLGHcrnT53vaqiGrQWbUKUDBawKLm24sghjKo4Bf+/xfv8espZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^5.0.1"
+ }
+ },
+ "node_modules/phaser/node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "license": "MIT"
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
diff --git a/package.json b/package.json
index 594e898..6287245 100644
--- a/package.json
+++ b/package.json
@@ -14,9 +14,11 @@
"@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0",
"@angular/forms": "^19.2.0",
+ "@angular/material": "^19.2.11",
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0",
+ "phaser": "^3.88.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
diff --git a/public/sprites/ships/01-starter.png b/public/sprites/ships/01-starter.png
new file mode 100644
index 0000000..7a7f36d
Binary files /dev/null and b/public/sprites/ships/01-starter.png differ
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 36093e1..7a56999 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,336 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Hello, {{ title }}
-
Congratulations! Your app is running. 🎉
-
-
-
-
- @for (item of [
- { title: 'Explore the Docs', link: 'https://angular.dev' },
- { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
- { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
- { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
- { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
- ]; track item.title) {
-
- {{ item.title }}
-
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index e69de29..1fa9853 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -0,0 +1,8 @@
+.game-container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+}
\ No newline at end of file
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 54fadbf..c738617 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,12 +1,54 @@
-import { Component } from '@angular/core';
-import { RouterOutlet } from '@angular/router';
+import { Component, ElementRef, inject, ViewChild } from '@angular/core';
+import Phaser from 'phaser';
+import { MapScene } from './scene/map.scene';
+import { MatDialog, MatDialogModule } from '@angular/material/dialog';
+import { GameService } from './service/game.service';
@Component({
selector: 'app-root',
- imports: [RouterOutlet],
+ imports: [MatDialogModule],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
- title = 'stellar-lines';
+
+ public dialog = inject(MatDialog);
+ public gameService = inject(GameService);
+
+ @ViewChild('gameContainer', { static: true }) gameContainer!: ElementRef;
+
+ ngAfterViewInit(): void {
+ this.gameService.dialog = this.dialog;
+ const scene = new MapScene();
+ scene.gameService = this.gameService;
+
+ const config: Phaser.Types.Core.GameConfig = {
+ type: Phaser.AUTO,
+ width: window.innerWidth,
+ height: window.innerHeight,
+ disableContextMenu: true,
+ scale: {
+ mode: Phaser.Scale.EXPAND,
+ autoCenter: Phaser.Scale.CENTER_BOTH,
+
+ },
+ parent: this.gameContainer.nativeElement,
+ backgroundColor: '#0c0e1a',
+ scene: [scene],
+ render: {
+ pixelArt: false,
+ antialias: true,
+ },
+
+ physics: {
+ default: 'arcade',
+ arcade: {
+ debug: true,
+ timeScale: 1
+ }
+ }
+ };
+
+ const game = new Phaser.Game(config);
+ }
}
diff --git a/src/app/components/dialog/planet-dialog/planet-dialog.component.html b/src/app/components/dialog/planet-dialog/planet-dialog.component.html
new file mode 100644
index 0000000..62630a3
--- /dev/null
+++ b/src/app/components/dialog/planet-dialog/planet-dialog.component.html
@@ -0,0 +1,24 @@
+@if (planet) {
+ {{planet.name}}
+
+ Bevölkerung: {{ planet.population | number:'0.0-0' }}
+
+
+
+
+
Güter:
+ @for (item of planet.getAllGoods(); track $index) {
+
+ {{ item.type }}: {{ item.amount }}
+
+ }
+
+
+
+
Angebotene Güter:
+ @for (item of offeredItems(); track $index) {
+ {{ item.type }}: {{ item.amount | number }} / {{ item.productionStorage | number }}
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/app/components/dialog/planet-dialog/planet-dialog.component.scss b/src/app/components/dialog/planet-dialog/planet-dialog.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/components/dialog/planet-dialog/planet-dialog.component.spec.ts b/src/app/components/dialog/planet-dialog/planet-dialog.component.spec.ts
new file mode 100644
index 0000000..ba06a1d
--- /dev/null
+++ b/src/app/components/dialog/planet-dialog/planet-dialog.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PlanetDialogComponent } from './planet-dialog.component';
+
+describe('PlanetDialogComponent', () => {
+ let component: PlanetDialogComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [PlanetDialogComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(PlanetDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/dialog/planet-dialog/planet-dialog.component.ts b/src/app/components/dialog/planet-dialog/planet-dialog.component.ts
new file mode 100644
index 0000000..f3ae318
--- /dev/null
+++ b/src/app/components/dialog/planet-dialog/planet-dialog.component.ts
@@ -0,0 +1,25 @@
+import { Component, inject, InjectionToken } from '@angular/core';
+import { Planet } from '../../../model/planet.model';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { CommonModule } from '@angular/common';
+import { Good } from '../../../model/goods/good.interface';
+
+@Component({
+ selector: 'app-planet-dialog',
+ imports: [CommonModule, MatDialogModule],
+ templateUrl: './planet-dialog.component.html',
+ styleUrl: './planet-dialog.component.scss'
+})
+export class PlanetDialogComponent {
+ readonly planet = inject(MAT_DIALOG_DATA);
+
+ ngOnInit() {
+ console.log(this.planet)
+ }
+
+
+ offeredItems(): Good[] {
+ return this.planet.getAllGoods().filter(g => g.productionRate)
+ }
+}
+
diff --git a/src/app/model/goods/good-config.ts b/src/app/model/goods/good-config.ts
new file mode 100644
index 0000000..3f2357a
--- /dev/null
+++ b/src/app/model/goods/good-config.ts
@@ -0,0 +1,18 @@
+import { GoodType } from "./good-type.enum";
+
+export interface GoodConfig {
+ baseProduction: number; // pro Sekunde
+ baseDemand: number; // pro Sekunde
+ storageLimit: number;
+ isRawResource: boolean;
+}
+
+export const GOODS_DATA: Record = {
+ [GoodType.Erz]: { baseProduction: 1.0, baseDemand: 0, storageLimit: 500, isRawResource: true },
+ [GoodType.Wasser]: { baseProduction: 0.8, baseDemand: 0.2, storageLimit: 300, isRawResource: true },
+ [GoodType.Nahrung]: { baseProduction: 5, baseDemand: 0.5, storageLimit: 200, isRawResource: true },
+ [GoodType.Metall]: { baseProduction: 0, baseDemand: 0.6, storageLimit: 300, isRawResource: false },
+ [GoodType.Treibstoff]: { baseProduction: 0, baseDemand: 0.3, storageLimit: 150, isRawResource: false },
+ [GoodType.Elektronik]: { baseProduction: 0, baseDemand: 0.4, storageLimit: 100, isRawResource: false },
+ [GoodType.Bauteile]: { baseProduction: 0, baseDemand: 0.2, storageLimit: 100, isRawResource: false }
+};
\ No newline at end of file
diff --git a/src/app/model/goods/good-type.enum.ts b/src/app/model/goods/good-type.enum.ts
new file mode 100644
index 0000000..60c7299
--- /dev/null
+++ b/src/app/model/goods/good-type.enum.ts
@@ -0,0 +1,9 @@
+export enum GoodType {
+ Erz = 'Erz',
+ Wasser = 'Wasser',
+ Nahrung = 'Nahrung',
+ Metall = 'Metall',
+ Treibstoff = 'Treibstoff',
+ Elektronik = 'Elektronik',
+ Bauteile = 'Bauteile'
+}
\ No newline at end of file
diff --git a/src/app/model/goods/good.interface.ts b/src/app/model/goods/good.interface.ts
new file mode 100644
index 0000000..71fad34
--- /dev/null
+++ b/src/app/model/goods/good.interface.ts
@@ -0,0 +1,9 @@
+import { GoodType } from "./good-type.enum";
+
+export interface Good {
+ type: GoodType,
+ amount: number;
+ productionRate: number;
+ demandRate: number;
+ productionStorage: number;
+}
\ No newline at end of file
diff --git a/src/app/model/planet.model.ts b/src/app/model/planet.model.ts
new file mode 100644
index 0000000..b927082
--- /dev/null
+++ b/src/app/model/planet.model.ts
@@ -0,0 +1,131 @@
+import { interval, Subscription } from "rxjs";
+import { GoodType } from "./goods/good-type.enum";
+import { GOODS_DATA } from "./goods/good-config";
+import { Good } from "./goods/good.interface";
+import { ShipUi } from "./ship";
+import { Ship } from "./ships/ship.model";
+
+export class Planet {
+ public population: number = 0;
+ public name: string = "";
+
+ private goods: Map = new Map();
+ private updateSubscription = interval(5000).subscribe(() => {this.update(5)}); // alle 5s
+
+ public isGrowing: boolean = false;
+
+
+ constructor(config: PlanetInit) {
+ this.name = config.name;
+ config.initialGoods.forEach(good => {
+ const base = GOODS_DATA[good.type];
+
+ this.goods.set(good.type, {
+ type: good.type,
+ amount: good.amount,
+ demandRate: base.baseDemand,
+ productionRate: base.baseProduction * (good.productionBonus ?? 0),
+ productionStorage: base.storageLimit
+ })
+ });
+ this.setIsGrowing();
+ }
+
+
+ private update(seconds: number): void {
+ this.goods.forEach((good: Good, key: string) => {
+ good.amount += good.productionRate * seconds;
+ good.amount -= good.demandRate * seconds;
+
+ // Min 0
+ good.amount = Math.max(0, good.amount);
+ if (good.productionRate && good.productionStorage) {
+ good.amount = Math.min(good.amount, good.productionStorage)
+ }
+ });
+
+ this.setIsGrowing();
+
+ return;
+
+ // Debug-Ausgabe (später ersetzen durch Events/Callback/Service)
+ console.log(`[${this.name}] Wirtschaft aktualisiert:`);
+ this.goods.forEach(good => {
+ console.log(` ${good.type}: ${good.amount.toFixed(2)}`);
+ });
+
+ console.log("Angeboten:")
+ for (let good of this.offeredGoods) {
+ console.log(` ${good.type}: ${good.amount.toFixed(2)}`)
+ }
+
+ console.log("Nachgefragt:")
+ for (let good of this.requestedGoods) {
+ console.log(` ${good.type}: ${good.amount.toFixed(2)}`)
+ }
+
+
+
+ }
+
+ private setIsGrowing(): void {
+ this.isGrowing = !this.getAllGoods().some( g => g.amount == 0);
+ }
+
+
+ getGood(type: GoodType): Good | undefined {
+ return this.goods.get(type);
+ }
+
+ getAllGoods(): Good[] {
+ return Array.from(this.goods.values());
+ }
+
+ addGood(good: Good): void {
+ this.goods.set(good.type, { ...good });
+ }
+
+
+ deliver(ship: Ship) {
+ if (!ship || ship.cargoSpace.length == 0) { return; }
+
+ const requests = this.requestedGoods;
+ if (requests.length == 0) { return; }
+ for (let request of requests) {
+ const offer = ship.cargoSpace.find(cargo => cargo.type == request?.type);
+ if (!offer) { continue; }
+ const transfer = Math.min(offer.amount, request.amount);
+ const good = this.getGood(request.type);
+ if (!good) { continue; }
+ good.amount += transfer;
+ offer.amount -= transfer;
+ }
+ }
+
+
+ get requestedGoods(): {type: GoodType, amount: number}[] {
+ return this.getAllGoods().filter(g => !g.productionRate && g.amount < g.demandRate * 30).map(g => { return { type: g.type, amount: g.demandRate * 100 }})
+ }
+
+ get offeredGoods(): {type: GoodType, amount: number}[] {
+ return this.getAllGoods().filter(g => g.productionRate && g.amount > g.demandRate * 10).map(g => { return { type: g.type, amount: g.amount - g.demandRate * 20}})
+ }
+
+ request(ship: ShipUi) {
+ const offers = this.offeredGoods;
+ if (offers.length == 0) { return; }
+ const loaded = ship.loadCargo(offers[0]);
+ offers[0].amount -= loaded;
+
+ }
+
+}
+
+export interface PlanetInit {
+ name: string;
+ initialGoods: {
+ type: GoodType;
+ amount: number;
+ productionBonus?: number; // optional: z. B. 1.5 = 150% der Basis
+ }[];
+}
\ No newline at end of file
diff --git a/src/app/model/ship.ts b/src/app/model/ship.ts
new file mode 100644
index 0000000..b2ce9ae
--- /dev/null
+++ b/src/app/model/ship.ts
@@ -0,0 +1,100 @@
+import { PlanetUi } from "../ui/planet.ui";
+import { GoodType } from "./goods/good-type.enum";
+import { Ship } from "./ships/ship.model";
+
+export class ShipUi extends Phaser.Physics.Arcade.Sprite {
+ private velocity = new Phaser.Math.Vector2(0, 0);
+ private target: PlanetUi | null = null;
+ private targetVector: Phaser.Math.Vector2 | null = null; // Später Planet!
+ public model: Ship = new Ship();
+
+
+ constructor(scene: Phaser.Scene, x: number, y: number) {
+ super(scene, x, y, 'ship');
+
+ scene.add.existing(this);
+ scene.physics.add.existing(this);
+ this.setOrigin(0.5);
+ this.setScale(1); // oder z. B. 0.5 je nach Bildgröße
+ this.setDisplaySize(32, 32)
+ this.setCollideWorldBounds(true);
+ const conf: Phaser.Types.Input.InputConfiguration = {
+ useHandCursor: true
+ }
+ this.setInteractive(conf)
+ }
+
+ moveTo(target: PlanetUi) {
+ this.target = target;
+ this.targetVector = new Phaser.Math.Vector2(target.getWorldPoint().x, target.getWorldPoint().y);
+ const angle = Phaser.Math.Angle.Between(this.getWorldPoint().x, this.getWorldPoint().y, target.getWorldPoint().x, target.getWorldPoint().y);
+ // this.setRotation(angle); // Zeigt Schiff zur Zielrichtung
+ if (!this.body) { return; }
+ }
+
+ override update(time: number, delta: number): void {
+ if (!this.targetVector) return;
+ if (!(this.body instanceof Phaser.Physics.Arcade.Body)) { return; }
+ const dt = delta / 1000; // ms → Sekunden
+
+ const toTarget = new Phaser.Math.Vector2(this.targetVector.x - this.getWorldPoint().x, this.targetVector.y - this.getWorldPoint().y);
+ const distance = toTarget.length();
+
+ this.setRotation(this.velocity.angle() + Phaser.Math.DegToRad(180)); // Zeigt Schiff zur Zielrichtung
+
+ if (distance < 4) {
+ this.body.setVelocity(0, 0);
+ this.velocity.set(0, 0);
+ if (this.targetVector && this.target) {
+ this.targetReached(this.target);
+ }
+ this.targetVector = null;
+ return;
+ }
+
+ // Richtung zum Ziel
+ const desiredDirection = toTarget.normalize();
+
+ // Zielgeschwindigkeit abhängig von Entfernung (langsamer bremsen)
+ const targetSpeed = (distance < this.model.slowDownRadius)
+ ? this.model.maxSpeed * (distance / this.model.slowDownRadius)
+ : this.model.maxSpeed;
+
+ const desiredVelocity = desiredDirection.scale(targetSpeed);
+
+ // Steuerung per "Lerp" zur Zielgeschwindigkeit (weiches Beschleunigen)
+ this.velocity.lerp(desiredVelocity, this.model.acceleration * dt / this.model.maxSpeed);
+
+ this.body.setVelocity(this.velocity.x, this.velocity.y);
+
+ }
+
+ targetReached(target: PlanetUi) {
+ console.log(target)
+
+ target.model.deliver(this.model);
+ target.model.request(this);
+ }
+
+ loadCargo(cargo: {type: GoodType, amount: number}): number {
+ const loaded = this.model.cargoSpace.reduce((acc, succ) => acc + succ.amount, 0);
+ if (loaded >= this.model.cargoSize) { return 0; }
+
+ const remaining = this.model.cargoSize - loaded;
+ const load = Math.min(remaining, cargo.amount);
+
+ const exists = this.model.cargoSpace.find(c => c.type == cargo.type);
+ if (exists) {
+ exists.amount += cargo.amount
+ } else {
+ this.model.cargoSpace.push({
+ amount: load,
+ type: cargo.type
+ })
+ }
+
+ console.log(this.model.cargoSpace)
+ return load;
+ }
+
+}
\ No newline at end of file
diff --git a/src/app/model/ships/ship.model.ts b/src/app/model/ships/ship.model.ts
new file mode 100644
index 0000000..7ffa765
--- /dev/null
+++ b/src/app/model/ships/ship.model.ts
@@ -0,0 +1,9 @@
+import { GoodType } from "../goods/good-type.enum";
+
+export class Ship {
+ public acceleration = 100; // Pixel pro Sekunde²
+ public maxSpeed = 200;
+ public slowDownRadius = 1000; // Startet Bremsen, wenn Ziel nahe ist
+ public cargoSize = 20;
+ cargoSpace: {type: GoodType, amount: number}[] = [];
+}
\ No newline at end of file
diff --git a/src/app/scene/map.scene.ts b/src/app/scene/map.scene.ts
new file mode 100644
index 0000000..e714184
--- /dev/null
+++ b/src/app/scene/map.scene.ts
@@ -0,0 +1,140 @@
+import { GoodType } from "../model/goods/good-type.enum";
+import { ShipUi } from "../model/ship";
+import { GameService } from "../service/game.service";
+import { PlanetUi } from "../ui/planet.ui";
+
+export class MapScene extends Phaser.Scene {
+ public gameService: GameService = new GameService();
+ private camera!: Phaser.Cameras.Scene2D.Camera;
+ private isDragging = false;
+ private dragStart = new Phaser.Math.Vector2();
+ private ship!: ShipUi; // Das Raumschiff
+
+
+ private lastZoomTime = 0;
+
+ constructor() {
+ super({ key: 'MapScene '})
+ }
+
+ preload() {
+ this.load.image('ship', 'sprites/ships/01-starter.png');
+ }
+
+ create() {
+ this.camera = this.cameras.main;
+
+ this.createPlaceHolderGraphic();
+ // this.camera.setBackgroundColor('0x99ccff')
+
+ // Weltgröße groß setzen, z. B. 5000x5000 Pixel
+ this.camera.setBounds(0, 0, 10000, 10000);
+ this.physics.world.setBounds(0, 0, 10000, 10000);
+ this.cameras.main.setBounds(0, 0, 10000, 10000);
+
+
+ /* Sterne */
+ for (let i = 0; i< 1000; i++) {
+ const x = Phaser.Math.Between(0, 10000);
+ const y = Phaser.Math.Between(0, 10000);
+ this.add.circle(x, y, Phaser.Math.Between(1, 3), 0x88ccff)
+ }
+
+ // Beispiel: ein paar Kreise (Planeten) zeichnen
+ this.buildPlanets();
+
+ // Events für Panning
+ this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
+ this.isDragging = true;
+ this.dragStart.set(pointer.x, pointer.y);
+ });
+
+ this.input.on('pointerup', () => {
+ this.isDragging = false;
+ });
+
+ this.input.on('pointermove', (pointer: Phaser.Input.Pointer) => {
+ if (!this.isDragging) return;
+ const dragX = pointer.x - this.dragStart.x;
+ const dragY = pointer.y - this.dragStart.y;
+ this.camera.scrollX -= dragX / this.camera.zoom;
+ this.camera.scrollY -= dragY / this.camera.zoom;
+ this.dragStart.set(pointer.x, pointer.y);
+ });
+
+ // Zoom via Mausrad
+ this.input.on('wheel', (_: any, __: number, ___: number, deltaY: number) => {
+ const now = Date.now();
+ if (now - this.lastZoomTime < 50) return; // nur alle 100ms
+ this.lastZoomTime = now;
+ const factor = 1.05;
+ if (deltaY > 0) {
+ this.camera.setZoom(this.camera.zoom / factor);
+ } else {
+ this.camera.setZoom(this.camera.zoom * factor);
+ }
+
+ const minScale = Math.max(window.innerWidth / 10000, window.innerHeight / 10000)
+ // Begrenze Zoom
+ this.camera.setZoom(Phaser.Math.Clamp(this.camera.zoom, minScale, 5));
+ });
+
+ this.ship = new ShipUi(this, 100, 100);
+ this.physics.world.enable(this.ship);
+
+
+ }
+
+ override update(time: number, delta: number): void {
+ if (this.ship) {
+
+ this.ship.update(time, delta);
+ }
+ }
+ private buildPlanets() {
+ for (let i = 0; i < 1; i++) {
+ const x = Phaser.Math.Between(200, 300);
+ const y = Phaser.Math.Between(200, 300);
+
+ const conf = {
+ name: 'Terra Nova',
+ initialGoods: [
+ {type: GoodType.Nahrung, amount: 10, productionBonus: 1 },
+ {type: GoodType.Wasser, amount: 10 },
+ ]
+ }
+ const p = new PlanetUi(this, x, y, conf);
+ p.rightClick.subscribe(n => {
+ this.ship.moveTo(p)
+ })
+
+ p.leftClick.subscribe(n => {
+ this.gameService.showDialog(p.model);
+ })
+ }
+
+ for (let i = 0; i < 1; i++) {
+ const conf = {
+ name: 'Waterloo',
+ initialGoods: [
+ {type: GoodType.Nahrung, amount: 10 },
+ {type: GoodType.Wasser, amount: 10, productionBonus: 1 },
+ ]
+ }
+ const x = Phaser.Math.Between(400, 500);
+ const y = Phaser.Math.Between(400, 500);
+ const p = new PlanetUi(this, x, y, conf);
+ p.rightClick.subscribe(n => {
+ this.ship.moveTo(p)
+ })
+ }
+ }
+
+ private createPlaceHolderGraphic() {
+ const graphics = this.add.graphics();
+ graphics.fillStyle(0x88ccff, 1);
+ graphics.fillCircle(100, 100, 100);
+ graphics.generateTexture('planet', 200, 200);
+ graphics.destroy();
+ }
+}
\ No newline at end of file
diff --git a/src/app/service/game.service.ts b/src/app/service/game.service.ts
new file mode 100644
index 0000000..dbb8441
--- /dev/null
+++ b/src/app/service/game.service.ts
@@ -0,0 +1,19 @@
+import { inject, Injectable } from "@angular/core";
+import { MatDialog } from "@angular/material/dialog";
+import { PlanetDialogComponent } from "../components/dialog/planet-dialog/planet-dialog.component";
+import { Planet } from "../model/planet.model";
+
+@Injectable({
+ providedIn: 'root',
+})
+export class GameService {
+ public dialog!: MatDialog;
+
+ constructor() {}
+
+ showDialog(planet: Planet) {
+ this.dialog.open(PlanetDialogComponent, {
+ data: planet
+ })
+ }
+}
\ No newline at end of file
diff --git a/src/app/ui/planet.ui.ts b/src/app/ui/planet.ui.ts
new file mode 100644
index 0000000..c7e9f7c
--- /dev/null
+++ b/src/app/ui/planet.ui.ts
@@ -0,0 +1,70 @@
+import { EventEmitter } from "@angular/core";
+import { interval } from "rxjs";
+import { Planet } from "../model/planet.model";
+
+export class PlanetUi extends Phaser.Physics.Arcade.Sprite {
+
+ public leftClick: EventEmitter = new EventEmitter();
+ public rightClick: EventEmitter = new EventEmitter();
+ public model: Planet;
+ private growingIndicator!: Phaser.GameObjects.Text;
+
+ private updateInterval = interval(1000)
+
+ constructor(scene: Phaser.Scene, x: number, y: number, config: any) {
+ super(scene, x, y, 'planet');
+
+ this.model = new Planet(config);
+
+ scene.add.existing(this);
+ this.setInteractive({ useHandCursor: true });
+
+ this.leftClick.emit();
+
+
+ this.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
+ if (pointer.button == Phaser.Input.MOUSE_DOWN) {
+ this.leftClick.emit(this)
+ } else if (pointer.button == Phaser.Input.MOUSE_UP) {
+ this.rightClick.emit(this);
+ }
+ });
+
+ this.growingIndicator = scene.add.text(x, y - 100, this.model.name, {
+ fontSize: '20px',
+ color: '#ffffff',
+ fontFamily: 'Arial',
+ backgroundColor: '#00000080',
+ padding: { x: 10, y: 5 },
+ }).setOrigin(0.5, 1);
+
+ this.updateInterval.subscribe(() => this.update())
+ }
+
+
+ override update(...args: any[]): void {
+
+ const offers = this.model.offeredGoods;
+
+
+ let text = `${this.model.name}`;
+ if (offers.length > 0) {
+ text += `\nAngebot: ${offers[0].type}: ${offers[0].amount.toFixed(2)}`
+ }
+
+
+ const request = this.model.requestedGoods;
+ if (request.length > 0) {
+ text += `\nNachgefragt: ${request[0].type}: ${request[0].amount.toFixed(2)}`
+ }
+
+ this.growingIndicator.setText(text);
+
+ if (this.model.isGrowing) {
+ this.growingIndicator.setColor('green')
+ } else {
+ this.growingIndicator.setColor('red')
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/styles.scss b/src/styles.scss
index 90d4ee0..07156ea 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -1 +1,11 @@
/* You can add global styles to this file, and also import other style files */
+@use '@angular/material' as mat;
+
+html {
+ color-scheme: light dark;
+ @include mat.theme((
+ color: mat.$violet-palette,
+ typography: Roboto,
+ density: 0
+ ));
+}
\ No newline at end of file