diff --git a/angular.json b/angular.json index ef353e6..019cc53 100644 --- a/angular.json +++ b/angular.json @@ -32,7 +32,9 @@ } ], "styles": [ - "src/styles.scss" + "src/styles.scss", + "src/styles/_variables.scss", + "src/styles/_ui.scss" ], "scripts": [] }, diff --git a/public/sprites/buildings/harbour.png b/public/sprites/buildings/harbour.png new file mode 100644 index 0000000..dc61f9a Binary files /dev/null and b/public/sprites/buildings/harbour.png differ diff --git a/public/sprites/planets/aqualis.png b/public/sprites/planets/aqualis.png new file mode 100644 index 0000000..f9d159b Binary files /dev/null and b/public/sprites/planets/aqualis.png differ diff --git a/public/sprites/planets/ferron.png b/public/sprites/planets/ferron.png new file mode 100644 index 0000000..d7d475e Binary files /dev/null and b/public/sprites/planets/ferron.png differ diff --git a/public/sprites/planets/mechanica-prime.png b/public/sprites/planets/mechanica-prime.png new file mode 100644 index 0000000..459601f Binary files /dev/null and b/public/sprites/planets/mechanica-prime.png differ diff --git a/public/sprites/planets/novus-reach.png b/public/sprites/planets/novus-reach.png new file mode 100644 index 0000000..4bcd394 Binary files /dev/null and b/public/sprites/planets/novus-reach.png differ diff --git a/public/sprites/planets/planet-1.png b/public/sprites/planets/planet-1.png new file mode 100644 index 0000000..e40b1cc Binary files /dev/null and b/public/sprites/planets/planet-1.png differ diff --git a/public/sprites/planets/sm/aqualis.png b/public/sprites/planets/sm/aqualis.png new file mode 100644 index 0000000..36d0511 Binary files /dev/null and b/public/sprites/planets/sm/aqualis.png differ diff --git a/public/sprites/planets/sm/ferron.png b/public/sprites/planets/sm/ferron.png new file mode 100644 index 0000000..45fc1dc Binary files /dev/null and b/public/sprites/planets/sm/ferron.png differ diff --git a/public/sprites/planets/sm/mechanica-prime.png b/public/sprites/planets/sm/mechanica-prime.png new file mode 100644 index 0000000..7e287cf Binary files /dev/null and b/public/sprites/planets/sm/mechanica-prime.png differ diff --git a/public/sprites/planets/sm/novus-reach.png b/public/sprites/planets/sm/novus-reach.png new file mode 100644 index 0000000..8d07e5b Binary files /dev/null and b/public/sprites/planets/sm/novus-reach.png differ diff --git a/public/sprites/planets/sm/planet-1.png b/public/sprites/planets/sm/planet-1.png new file mode 100644 index 0000000..caa876e Binary files /dev/null and b/public/sprites/planets/sm/planet-1.png differ diff --git a/public/sprites/planets/sm/terra-nova.png b/public/sprites/planets/sm/terra-nova.png new file mode 100644 index 0000000..e20e5f0 Binary files /dev/null and b/public/sprites/planets/sm/terra-nova.png differ diff --git a/public/sprites/planets/terra-nova.png b/public/sprites/planets/terra-nova.png new file mode 100644 index 0000000..d0b2cc1 Binary files /dev/null and b/public/sprites/planets/terra-nova.png differ diff --git a/src/app/app.component.html b/src/app/app.component.html index 7a56999..0673847 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1 +1,5 @@ -
\ No newline at end of file +
+ +@if (gameService.showPlanetInfo) { + +} \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c738617..3559aa4 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,10 +3,12 @@ import Phaser from 'phaser'; import { MapScene } from './scene/map.scene'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { GameService } from './service/game.service'; +import { ShipDialogComponent } from './components/dialog/ship-dialog/ship-dialog.component'; +import { PlanetDialogComponent } from './components/dialog/planet-dialog/planet-dialog.component'; @Component({ selector: 'app-root', - imports: [MatDialogModule], + imports: [MatDialogModule, ShipDialogComponent, PlanetDialogComponent], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) @@ -18,7 +20,6 @@ export class AppComponent { @ViewChild('gameContainer', { static: true }) gameContainer!: ElementRef; ngAfterViewInit(): void { - this.gameService.dialog = this.dialog; const scene = new MapScene(); scene.gameService = this.gameService; diff --git a/src/app/components/dialog/planet-dialog/planet-dialog.component.html b/src/app/components/dialog/planet-dialog/planet-dialog.component.html index 62630a3..0258616 100644 --- a/src/app/components/dialog/planet-dialog/planet-dialog.component.html +++ b/src/app/components/dialog/planet-dialog/planet-dialog.component.html @@ -1,24 +1,45 @@ -@if (planet) { -

{{planet.name}}

- -
Bevölkerung: {{ planet.population | number:'0.0-0' }}
+
+
+
+
{{ planet.name }}
+
+ - -
-

Güter:

- @for (item of planet.getAllGoods(); track $index) { -
- {{ item.type }}: {{ item.amount }} +
+
👥 Bevölkerung: {{ population }} 🚀
+
+
🏭 Produktion:
+
    + @for (item of producedItems; track $index) { +
  • {{ item.type }}: +{{ item.productionRate | number:'0.0-2' }}/s
  • + } +
+
+ +
+
📦 Vorräte:
+ @for (item of storedItems; track $index) { +
+
{{ item.type }}: {{ item.amount | number:'0.0-1' }}
}
-
-

Angebotene Güter:

- @for (item of offeredItems(); track $index) { - {{ item.type }}: {{ item.amount | number }} / {{ item.productionStorage | number }} - } +
+
📦 Verbrauch:
+
    + @for (item of consumedItems; track $index) { +
  • {{ item.type }}: {{ item.demandRate * planet.population | number:'0.0-1' }}/s
  • + } +
+
- -} \ No newline at end of file +
+ +
+ + + +
+
\ 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 index e69de29..3c3bec3 100644 --- a/src/app/components/dialog/planet-dialog/planet-dialog.component.scss +++ b/src/app/components/dialog/planet-dialog/planet-dialog.component.scss @@ -0,0 +1,11 @@ +:host { + display: flex; + flex-direction: column; + position: absolute; + // top: 24px; + // left: 24px; + // width: 240px; + // height: 240px; + // background-color: var(--background-color); + // color: var(--primary-color); +} \ No newline at end of file diff --git a/src/app/components/dialog/planet-dialog/planet-dialog.component.ts b/src/app/components/dialog/planet-dialog/planet-dialog.component.ts index f3ae318..1415165 100644 --- a/src/app/components/dialog/planet-dialog/planet-dialog.component.ts +++ b/src/app/components/dialog/planet-dialog/planet-dialog.component.ts @@ -3,6 +3,8 @@ 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'; +import { GoodType } from '../../../model/goods/good-type.enum'; +import { GameService } from '../../../service/game.service'; @Component({ selector: 'app-planet-dialog', @@ -11,15 +13,49 @@ import { Good } from '../../../model/goods/good.interface'; styleUrl: './planet-dialog.component.scss' }) export class PlanetDialogComponent { - readonly planet = inject(MAT_DIALOG_DATA); + public gameService: GameService = inject(GameService) ngOnInit() { console.log(this.planet) } - offeredItems(): Good[] { + get planet(): Planet { + return this.gameService.showPlanetInfo!; + } + + get population(): number { + return Math.floor(this.planet.population); + } + + get producedItems(): Good[] { + if (!this.planet) { return []; } return this.planet.getAllGoods().filter(g => g.productionRate) } + + get storedItems(): Good[] { + return this.planet.getAllGoods() + } + + get consumedItems(): Good[] { + return this.planet.getAllGoods().filter(g => g.demandRate) + } + + requestedItems(): { + type: GoodType; + amount: number; + }[] { + if (!this.planet) { return []; } + return this.planet.requestedGoods + } + + getFillPercentange(item: Good): number { + return (item.amount / (item.demandRate * this.planet.population * 30)) * 100; + } + + + close() { + this.gameService.showPlanetInfo = undefined; + } } diff --git a/src/app/components/dialog/ship-dialog/ship-dialog.component.html b/src/app/components/dialog/ship-dialog/ship-dialog.component.html new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/dialog/ship-dialog/ship-dialog.component.scss b/src/app/components/dialog/ship-dialog/ship-dialog.component.scss new file mode 100644 index 0000000..3c3bec3 --- /dev/null +++ b/src/app/components/dialog/ship-dialog/ship-dialog.component.scss @@ -0,0 +1,11 @@ +:host { + display: flex; + flex-direction: column; + position: absolute; + // top: 24px; + // left: 24px; + // width: 240px; + // height: 240px; + // background-color: var(--background-color); + // color: var(--primary-color); +} \ No newline at end of file diff --git a/src/app/components/dialog/ship-dialog/ship-dialog.component.spec.ts b/src/app/components/dialog/ship-dialog/ship-dialog.component.spec.ts new file mode 100644 index 0000000..7230518 --- /dev/null +++ b/src/app/components/dialog/ship-dialog/ship-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ShipDialogComponent } from './ship-dialog.component'; + +describe('ShipDialogComponent', () => { + let component: ShipDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ShipDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ShipDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/dialog/ship-dialog/ship-dialog.component.ts b/src/app/components/dialog/ship-dialog/ship-dialog.component.ts new file mode 100644 index 0000000..7a450a4 --- /dev/null +++ b/src/app/components/dialog/ship-dialog/ship-dialog.component.ts @@ -0,0 +1,12 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-ship-dialog', + imports: [CommonModule], + templateUrl: './ship-dialog.component.html', + styleUrl: './ship-dialog.component.scss' +}) +export class ShipDialogComponent { + +} diff --git a/src/app/data/planets.data.ts b/src/app/data/planets.data.ts new file mode 100644 index 0000000..928b0ed --- /dev/null +++ b/src/app/data/planets.data.ts @@ -0,0 +1,74 @@ +import { GoodType } from "../model/goods/good-type.enum"; +import { PlanetInit } from "../model/planet.model"; + +export const PLANETCONFIGS: { x: number, y: number, texture: string, config: PlanetInit}[] = [ + { + x: 200, + y: 300, + texture: 'terra-nova', + config: { + name: 'Terra Nova', + initialGoods: [ + { type: GoodType.Wasser, amount: 120 }, + { type: GoodType.Nahrung, amount: 80, productionBonus: 1.3 }, + { type: GoodType.Erz, amount: 100 } + ] + } + }, + { + x: 600, + y: 200, + texture: 'mechanica-prime', + config: { + name: 'Mechanica Prime', + initialGoods: [ + { type: GoodType.Wasser, amount: 120 }, + { type: GoodType.Nahrung, amount: 80 }, + { type: GoodType.Metall, amount: 50, productionBonus: 1.5 }, + { type: GoodType.Bauteile, amount: 20, productionBonus: 1.3 }, + { type: GoodType.Elektronik, amount: 10 } + ] + } + }, + { + x: 400, + y: 700, + texture: 'aqualis', + config: { + name: 'Aqualis', + initialGoods: [ + { type: GoodType.Wasser, amount: 300, productionBonus: 2.0 }, + { type: GoodType.Nahrung, amount: 60 }, + { type: GoodType.Treibstoff, amount: 10 } + ] + } + }, + { + x: 900, + y: 500, + texture: 'planet', + config: { + name: 'Ferron', + initialGoods: [ + { type: GoodType.Wasser, amount: 120 }, + { type: GoodType.Nahrung, amount: 80 }, + { type: GoodType.Erz, amount: 200, productionBonus: 1.8 }, + { type: GoodType.Metall, amount: 40 }, + { type: GoodType.Treibstoff, amount: 20 } + ] + } + }, + { + x: 800, + y: 800, + texture: 'novus-reach', + config: { + name: 'Novus Reach', + initialGoods: [ + { type: GoodType.Nahrung, amount: 90, productionBonus: 1.5 }, + { type: GoodType.Wasser, amount: 50 }, + { type: GoodType.Bauteile, amount: 15 } + ] + } + } +] \ No newline at end of file diff --git a/src/app/model/goods/good-config.ts b/src/app/model/goods/good-config.ts index 3f2357a..58ec627 100644 --- a/src/app/model/goods/good-config.ts +++ b/src/app/model/goods/good-config.ts @@ -9,10 +9,10 @@ export interface GoodConfig { 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 } + [GoodType.Wasser]: { baseProduction: 1, baseDemand: 0.015, storageLimit: 300, isRawResource: true }, + [GoodType.Nahrung]: { baseProduction: 1, baseDemand: 0.01, storageLimit: 200, isRawResource: true }, + [GoodType.Metall]: { baseProduction: 1, baseDemand: 0, storageLimit: 300, isRawResource: false }, + [GoodType.Treibstoff]: { baseProduction: 1, baseDemand: 0, storageLimit: 150, isRawResource: false }, + [GoodType.Elektronik]: { baseProduction: 1, baseDemand: 0, storageLimit: 100, isRawResource: false }, + [GoodType.Bauteile]: { baseProduction: 1, baseDemand: 0, storageLimit: 100, isRawResource: false } }; \ No newline at end of file diff --git a/src/app/model/planet.model.ts b/src/app/model/planet.model.ts index b927082..8954d30 100644 --- a/src/app/model/planet.model.ts +++ b/src/app/model/planet.model.ts @@ -6,20 +6,39 @@ import { ShipUi } from "./ship"; import { Ship } from "./ships/ship.model"; export class Planet { - public population: number = 0; + public population: number = 100; public name: string = ""; + public image: string; private goods: Map = new Map(); private updateSubscription = interval(5000).subscribe(() => {this.update(5)}); // alle 5s public isGrowing: boolean = false; + private populationGrowthRate = 0.002; // Basiswachstum pro Tick (%) + private populationDeclineRate = 0.005; // Basisrückgang bei Mangel (%) + demandSecondsBuffer = 30; // Anfrage immer mindestens 30 Sekunden überleben + + private productionLevel: Map = new Map(); constructor(config: PlanetInit) { this.name = config.name; + this.image = config.name.toLowerCase().split(' ').join('-') + + /** set all empty */ + Object.values(GoodType).forEach(c => { + this.goods.set(c, { + amount: 0, + demandRate: 0, + productionRate: 0, + productionStorage: 0, + type: c + }) + }) + + config.initialGoods.forEach(good => { const base = GOODS_DATA[good.type]; - this.goods.set(good.type, { type: good.type, amount: good.amount, @@ -28,13 +47,15 @@ export class Planet { productionStorage: base.storageLimit }) }); - this.setIsGrowing(); + this.updatePopulation(0) + } private update(seconds: number): void { this.goods.forEach((good: Good, key: string) => { - good.amount += good.productionRate * seconds; + const lvlMultiplier = this.productionLevel.get(good.type) ?? 1; + good.amount += good.productionRate * seconds * lvlMultiplier; good.amount -= good.demandRate * seconds; // Min 0 @@ -44,7 +65,7 @@ export class Planet { } }); - this.setIsGrowing(); + this.updatePopulation(seconds) return; @@ -68,11 +89,6 @@ export class Planet { } - private setIsGrowing(): void { - this.isGrowing = !this.getAllGoods().some( g => g.amount == 0); - } - - getGood(type: GoodType): Good | undefined { return this.goods.get(type); } @@ -99,24 +115,135 @@ export class Planet { if (!good) { continue; } good.amount += transfer; offer.amount -= transfer; + console.log(`Ausgeladen: ${good.type}: ${transfer}`) } } + /** + * Gets a list of goods that the planet needs to import + * @returns Array of goods with their type and requested amount + * For each good that: + * - has no production + * - has demand + * - current amount is less than buffer demand + * Returns 3x the buffer demand amount + */ + get requestedGoods(): TradeInstance[] { + const result: TradeInstance[] = []; - 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 }}) + const goods = this.getAllGoods().filter(g => !g.productionRate && g.demandRate); + for (let good of goods) { + const demandPerSecond = good.demandRate * this.population; + const demand = this.demandSecondsBuffer * demandPerSecond; + if (demand < good.amount) { + continue; // Skip if we have enough in stock + } + + result.push({ + amount: demand * 3, // Request 3x buffer amount + type: good.type + }) + } + + return result; } - 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}}) + get offeredGoods(): TradeInstance[] { + return this.getAllGoods().filter(g => g.productionRate && g.amount).map(g => { return { type: g.type, amount: g.amount}}) } - request(ship: ShipUi) { + // request(ship: ShipUi) { + // const offers = this.offeredGoods; + // if (offers.length == 0) { return; } + // const loaded = ship.loadCargo(offers[0]); + // offers[0].amount -= loaded; + // const reduce = this.goods.get(offers[0].type) + // if (!reduce) { return; } + // reduce.amount = Math.max(0, reduce.amount - loaded); + // } + + request(demandedGood: {type: GoodType, amount: number}) { const offers = this.offeredGoods; - if (offers.length == 0) { return; } - const loaded = ship.loadCargo(offers[0]); - offers[0].amount -= loaded; + if (offers.length == 0) { return 0; } + const good = offers.find(o => o.type == demandedGood.type); + if (!good) { return 0; } + + const amount = Math.min(demandedGood.amount, good.amount); + + const store = this.getGood(demandedGood.type); + if (!store) { return 0; } + + store.amount -= amount; + return amount; + } + + + private updatePopulation(seconds: number): void { + const demands = this.calculateNaturalDemand(seconds); + let allSupplied = true; + + demands.forEach((amountNeeded, goodType) => { + const available = this.goods.get(goodType)?.amount ?? 0; + if (available < amountNeeded) { + allSupplied = false; + } + }); + + if (allSupplied) { + this.population += this.population * this.populationGrowthRate * seconds; + this.isGrowing = true; + } else { + this.isGrowing = false; + this.population -= this.population * this.populationDeclineRate * seconds; + this.population = Math.max(this.population, 100); + } + + // if (this.isGrowing) { + // console.log(`[${this.name}] Growing population: ${this.population.toFixed(2)}`); + // } else { + // console.log(`[${this.name}] Shrinking population: ${this.population.toFixed(2)}`); + // } + + // Verbrauch abziehen + demands.forEach((amountNeeded, goodType) => { + const good = this.goods.get(goodType); + if (good && good.amount >= amountNeeded) { + good.amount -= amountNeeded; + } + }); + } + + + /** + * Calculates the natural resource demand based on population. + * @param seconds Time interval in seconds + * @returns Map of good types to their demand quantities + * @private + */ + private calculateNaturalDemand(seconds: number): Map { + const demand = new Map(); + + for (const goodType in GOODS_DATA) { + const config = GOODS_DATA[goodType as GoodType]; + if (config.baseDemand > 0) { + demand.set(goodType as GoodType, this.population * config.baseDemand * seconds); + } + } + + return demand; + } + + getCriticalGoods(): GoodType[] { + const critical: GoodType[] = []; + this.getAllGoods().forEach(good => { + if (good && good.amount < 10 && good.demandRate) { // unter 10 Einheiten = kritisch + critical.push(good.type); + } + }) + + + return critical; } } @@ -128,4 +255,9 @@ export interface PlanetInit { amount: number; productionBonus?: number; // optional: z. B. 1.5 = 150% der Basis }[]; +} + +export interface TradeInstance { + type: GoodType; + amount: number; } \ No newline at end of file diff --git a/src/app/model/routes/trade-route.model.ts b/src/app/model/routes/trade-route.model.ts new file mode 100644 index 0000000..250b762 --- /dev/null +++ b/src/app/model/routes/trade-route.model.ts @@ -0,0 +1,54 @@ +import { Planet, TradeInstance } from "../planet.model" + +export class TradeRoute { + private route: ITradePlanet[] = []; + + private target!: ITradePlanet; + + constructor(route: Planet[]) { + const r: any[] = route.map(r => { return {target: r, next: null }}) + if (route.length < 2) { return;} + + for (let i = 1; i < route.length; i++) { + r[i-1].next = r[i]; + } + r[r.length - 1].next = r[0]; + this.target = r[0]; + this.route = r; + } + + get nextPlanetName(): string { + return this.target?.target.name ?? ''; + } + + routePointReached() { + if (!this.target) { return; } + this.target = this.target.next; + } + + + /** + * Returns an array of trade demands from all planets in the route except the current target + * @returns Array of TradeInstance representing goods demanded by planets in the route + */ + getTradeRouteDemands(): TradeInstance[] { + // Return empty array if route or target is not initialized + if (!this.route || !this.target) { return[] } + const demands = []; + + const max = this.route.length; + let target = this.target; + // Iterate through all planets in route except current target + for (let i = 1; i < max; i++) { + target = target.next; + // Add all requested goods from each planet to demands array + demands.push(...target.target.requestedGoods); + } + return demands; + } +} + +interface ITradePlanet { + target: Planet; + next: ITradePlanet; +} \ No newline at end of file diff --git a/src/app/model/ship.ts b/src/app/model/ship.ts index b2ce9ae..e5444ec 100644 --- a/src/app/model/ship.ts +++ b/src/app/model/ship.ts @@ -1,5 +1,7 @@ +import { MapScene } from "../scene/map.scene"; import { PlanetUi } from "../ui/planet.ui"; import { GoodType } from "./goods/good-type.enum"; +import { TradeInstance } from "./planet.model"; import { Ship } from "./ships/ship.model"; export class ShipUi extends Phaser.Physics.Arcade.Sprite { @@ -9,9 +11,11 @@ export class ShipUi extends Phaser.Physics.Arcade.Sprite { public model: Ship = new Ship(); - constructor(scene: Phaser.Scene, x: number, y: number) { + constructor(scene: MapScene, x: number, y: number) { super(scene, x, y, 'ship'); + + scene.add.existing(this); scene.physics.add.existing(this); this.setOrigin(0.5); @@ -22,9 +26,11 @@ export class ShipUi extends Phaser.Physics.Arcade.Sprite { useHandCursor: true } this.setInteractive(conf) + this.activateRoute(); } - moveTo(target: PlanetUi) { + moveTo(target: PlanetUi | undefined) { + if (!target) { return; } 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); @@ -70,13 +76,17 @@ export class ShipUi extends Phaser.Physics.Arcade.Sprite { } targetReached(target: PlanetUi) { - console.log(target) - - target.model.deliver(this.model); - target.model.request(this); + this.model.exchangeGoods(target.model) + if (this.model.route) { + this.model.route.routePointReached(); + const target = (this.scene as MapScene).getPlanetUiByName(this.model.route.nextPlanetName); + setTimeout(() => { + this.moveTo(target); + }, 1000) + } } - loadCargo(cargo: {type: GoodType, amount: number}): number { + loadCargo(cargo: TradeInstance): number { const loaded = this.model.cargoSpace.reduce((acc, succ) => acc + succ.amount, 0); if (loaded >= this.model.cargoSize) { return 0; } @@ -85,7 +95,7 @@ export class ShipUi extends Phaser.Physics.Arcade.Sprite { const exists = this.model.cargoSpace.find(c => c.type == cargo.type); if (exists) { - exists.amount += cargo.amount + exists.amount += load } else { this.model.cargoSpace.push({ amount: load, @@ -93,8 +103,16 @@ export class ShipUi extends Phaser.Physics.Arcade.Sprite { }) } - console.log(this.model.cargoSpace) + console.log(`Eingeladen: ${cargo.type}: ${load}`) + return load; } + activateRoute() { + if (!this.model.route) { return; } + + const planet = (this.scene as MapScene).getPlanetUiByName(this.model.route.nextPlanetName); + this.moveTo(planet) + } + } \ No newline at end of file diff --git a/src/app/model/ships/ship.model.ts b/src/app/model/ships/ship.model.ts index 7ffa765..88713d3 100644 --- a/src/app/model/ships/ship.model.ts +++ b/src/app/model/ships/ship.model.ts @@ -1,9 +1,52 @@ import { GoodType } from "../goods/good-type.enum"; +import { Planet, TradeInstance } from "../planet.model"; +import { TradeRoute } from "../routes/trade-route.model"; 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}[] = []; + public acceleration = 200; // Pixel pro Sekunde² + public maxSpeed = 4000; + public slowDownRadius = 2000; // Startet Bremsen, wenn Ziel nahe ist + public cargoSize = 100; + cargoSpace: TradeInstance[] = []; + public route: TradeRoute | undefined = new TradeRoute([]) + + exchangeGoods(planet: Planet) { + if (!this.route) { return; } + + planet.deliver(this); + + console.log(`\n[${planet.name}] Starting new Trade. Free cargospace: ${this.freeCargoSpace.toFixed(2)}`) + + const demands = this.route.getTradeRouteDemands(); + if (demands.length == 0) { return; } + console.log(JSON.parse(JSON.stringify(demands))) + + for (let demand of demands) { + // requested amount: demand - storage + const stored = this.cargoSpace.find(c => c.type == demand.type); + if (stored) { demand.amount -= stored.amount; }; + demand.amount = Math.min(demand.amount, this.freeCargoSpace); + demand.amount = Math.max(demand.amount, 0) + if (demand.amount > 0) { console.log(`[${planet.name}] Requesting ${demand.amount.toFixed(2)} ${demand.type}`); } + if (demand.amount == 0) { continue; } + const received = planet.request(demand); + this.addToCargoSpace({type: demand.type, amount: received}) + } + + console.log(`Cargo: ${this.cargoSpace.map(c => `${c.type}: ${c.amount.toFixed(2)}`).join(', ')}`); + } + + get freeCargoSpace(): number { + return this.cargoSize - this.cargoSpace.reduce((acc, succ) => acc + succ.amount, 0) + } + + addToCargoSpace(cargo: TradeInstance) { + const existing = this.cargoSpace.find(c => c.type == cargo.type); + + if (existing) { + existing.amount += cargo.amount; + } else { + this.cargoSpace.push(cargo); + } + } } \ No newline at end of file diff --git a/src/app/scene/map.scene.ts b/src/app/scene/map.scene.ts index e714184..48f014a 100644 --- a/src/app/scene/map.scene.ts +++ b/src/app/scene/map.scene.ts @@ -1,4 +1,6 @@ +import { PLANETCONFIGS } from "../data/planets.data"; import { GoodType } from "../model/goods/good-type.enum"; +import { TradeRoute } from "../model/routes/trade-route.model"; import { ShipUi } from "../model/ship"; import { GameService } from "../service/game.service"; import { PlanetUi } from "../ui/planet.ui"; @@ -9,6 +11,8 @@ export class MapScene extends Phaser.Scene { private isDragging = false; private dragStart = new Phaser.Math.Vector2(); private ship!: ShipUi; // Das Raumschiff + private ships: ShipUi[] = []; + private planets: PlanetUi[] = []; private lastZoomTime = 0; @@ -19,6 +23,13 @@ export class MapScene extends Phaser.Scene { preload() { this.load.image('ship', 'sprites/ships/01-starter.png'); + this.load.image('harbour', 'sprites/buildings/harbour.png'); + this.load.image('planet', 'sprites/planets/sm/planet-1.png'); + this.load.image('terra-nova', 'sprites/planets/sm/terra-nova.png'); + this.load.image('mechanica-prime', 'sprites/planets/sm/mechanica-prime.png'); + this.load.image('novus-reach', 'sprites/planets/sm/novus-reach.png'); + this.load.image('aqualis', 'sprites/planets/sm/aqualis.png'); + this.load.image('ferron', 'sprites/planets/sm/ferron.png'); } create() { @@ -45,7 +56,9 @@ export class MapScene extends Phaser.Scene { // Events für Panning this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => { + if (!this.gameService.canDrag) { return; } this.isDragging = true; + this.dragStart.set(pointer.x, pointer.y); }); @@ -76,11 +89,17 @@ export class MapScene extends Phaser.Scene { const minScale = Math.max(window.innerWidth / 10000, window.innerHeight / 10000) // Begrenze Zoom - this.camera.setZoom(Phaser.Math.Clamp(this.camera.zoom, minScale, 5)); + this.camera.setZoom(Phaser.Math.Clamp(this.camera.zoom, minScale, 1)); }); - this.ship = new ShipUi(this, 100, 100); - this.physics.world.enable(this.ship); + setTimeout(() => { + const s = new ShipUi(this, 100, 100); + s.model.route = new TradeRoute(this.planets.filter(planet => ['Terra Nova', 'Aqualis'].includes(planet.model.name)).map(planet => planet.model)) + this.ships.push(s) + this.physics.world.enable(s); + s.activateRoute(); + }, 1000) + } @@ -90,43 +109,23 @@ export class MapScene extends Phaser.Scene { this.ship.update(time, delta); } + + for (let ship of this.ships) { + 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) - }) + for (let config of PLANETCONFIGS) { + const planet = new PlanetUi(this, config.x, config.y, config.texture, config.config); + planet.rightClick.subscribe(planet => { + this.ship.moveTo(planet); + this.isDragging = false; + }); + planet.leftClick.subscribe(planet => { + this.gameService.showDialog(planet.model); + this.isDragging = false; + }); + this.planets.push(planet); } } @@ -137,4 +136,8 @@ export class MapScene extends Phaser.Scene { graphics.generateTexture('planet', 200, 200); graphics.destroy(); } + + getPlanetUiByName(name: string) { + return this.planets.find(p => p.model.name == name); + } } \ No newline at end of file diff --git a/src/app/service/game.service.ts b/src/app/service/game.service.ts index dbb8441..97064e9 100644 --- a/src/app/service/game.service.ts +++ b/src/app/service/game.service.ts @@ -1,5 +1,5 @@ import { inject, Injectable } from "@angular/core"; -import { MatDialog } from "@angular/material/dialog"; +import { MatDialog, MatDialogRef } from "@angular/material/dialog"; import { PlanetDialogComponent } from "../components/dialog/planet-dialog/planet-dialog.component"; import { Planet } from "../model/planet.model"; @@ -7,13 +7,15 @@ import { Planet } from "../model/planet.model"; providedIn: 'root', }) export class GameService { - public dialog!: MatDialog; + public showPlanetInfo: Planet | undefined; constructor() {} showDialog(planet: Planet) { - this.dialog.open(PlanetDialogComponent, { - data: planet - }) + this.showPlanetInfo = planet; + } + + get canDrag(): boolean { + return true; } } \ No newline at end of file diff --git a/src/app/ui/planet-status.ui.ts b/src/app/ui/planet-status.ui.ts new file mode 100644 index 0000000..7e54b1a --- /dev/null +++ b/src/app/ui/planet-status.ui.ts @@ -0,0 +1,54 @@ +import { GoodType } from "../model/goods/good-type.enum"; +import { PlanetUi } from "./planet.ui"; + +export class PlanetStatus { + private container: Phaser.GameObjects.Container; + private text: Phaser.GameObjects.Text; + + constructor( + private scene: Phaser.Scene, + private planetSprite: PlanetUi, + private getPopulation: () => number, + private getGrowthState: () => 'growing' | 'shrinking', + private getCriticalGoods: () => GoodType[] + ) { + this.text = scene.add.text(0, 0, '', { + fontSize: '12px', + color: '#ffffff', + backgroundColor: '#00000088', + padding: { x: 5, y: 3 }, + align: 'center' + }).setOrigin(0.5, 1); + + this.container = scene.add.container(planetSprite.getWorldPoint().x, planetSprite.getWorldPoint().y - (planetSprite.height / 2) - 5, [this.text]); + this.container.setDepth(100); // über Planeten + } + + update() { + const population = Math.floor(this.getPopulation()); + const growth = this.getGrowthState(); + const criticalGoods = this.getCriticalGoods(); + + let growthSymbol = ''; + if (growth === 'growing') { + growthSymbol = '🚀'; + } else if (growth === 'shrinking') { + growthSymbol = '⬇️'; + } + + let criticalText = ''; + if (criticalGoods.length > 0) { + criticalText = ' 🛑 ' + criticalGoods.map(g => g.toString().substring(0, 2)).join(','); + } + + this.text.setText(`${this.planetSprite.model.name}, 👥 ${population} ${growthSymbol}${criticalText}`); + + // Position immer über Planet aktualisieren + this.container.setPosition(this.planetSprite.x, this.planetSprite.y - (this.planetSprite.height / 2) - 5); + } + + destroy() { + this.text.destroy(); + this.container.destroy(); + } +} \ No newline at end of file diff --git a/src/app/ui/planet.ui.ts b/src/app/ui/planet.ui.ts index c7e9f7c..4e41fab 100644 --- a/src/app/ui/planet.ui.ts +++ b/src/app/ui/planet.ui.ts @@ -1,18 +1,21 @@ import { EventEmitter } from "@angular/core"; import { interval } from "rxjs"; import { Planet } from "../model/planet.model"; +import { PlanetStatus } from "./planet-status.ui"; 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 status: PlanetStatus | undefined; private updateInterval = interval(1000) - constructor(scene: Phaser.Scene, x: number, y: number, config: any) { - super(scene, x, y, 'planet'); + constructor(scene: Phaser.Scene, x: number, y: number, texture: string, config: any) { + super(scene, x, y, texture); + // this.setDisplaySize(200, 200); + this.model = new Planet(config); @@ -30,15 +33,17 @@ export class PlanetUi extends Phaser.Physics.Arcade.Sprite { } }); - 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.status = new PlanetStatus(scene, this, () => this.model.population, () => this.model.isGrowing ? 'growing' : 'shrinking', () => this.model.getCriticalGoods()) this.updateInterval.subscribe(() => this.update()) + + + const image = this.scene.add.image(this.getWorldPoint().x, this.getWorldPoint().y, 'harbour') + image.setDisplaySize(64, 96); + + // this.setInteractive(new Phaser.Geom.Circle(x, y, 200), Phaser.Geom.Circle.Contains); } @@ -58,13 +63,7 @@ export class PlanetUi extends Phaser.Physics.Arcade.Sprite { 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') - } + this.status?.update(); } } \ No newline at end of file diff --git a/src/styles.scss b/src/styles.scss index 07156ea..232473a 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -8,4 +8,5 @@ html { typography: Roboto, density: 0 )); -} \ No newline at end of file +} + diff --git a/src/styles/_ui.scss b/src/styles/_ui.scss new file mode 100644 index 0000000..888717c --- /dev/null +++ b/src/styles/_ui.scss @@ -0,0 +1,111 @@ +// _ui.scss + +// Farbdefinitionen +$color-background: #0b1120; +$color-panel: #1c243b; +$color-text-primary: #e0f0ff; +$color-text-secondary: #a0b8d8; +$color-water: #4fc1e9; +$color-food: #f7c06b; +$color-ore: #c1543a; +$color-success: #4CAF50; +$color-error: #f44336; +$color-button: #1c243b; +$color-button-hover: #3c8dbc; + +// Font-Einstellungen +$font-family: 'Roboto', sans-serif; +$font-size-base: 14px; + +// Mixin für Standardbutton +@mixin button { + background-color: $color-button; + color: $color-text-primary; + border: none; + padding: 10px 16px; + border-radius: 8px; + font-family: $font-family; + font-size: $font-size-base; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: $color-button-hover; + } + + &:disabled { + // background-color: darken($color-button, 10%); + background-color: $color-button; + cursor: not-allowed; + opacity: 0.6; + } +} + +// Anwendung für allgemeine UI-Elemente +.ui-panel { + background-color: $color-panel; + padding: 16px; + border-radius: 10px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + color: $color-text-primary; + font-family: $font-family; +} + +.ui-title { + font-size: 18px; + font-weight: bold; + margin-bottom: 12px; + display: flex; + flex-direction: column; + align-items: center; +} + +.ui-body { + border: 2px solid #202d3e; + border-radius: 8px;; +} + +.ui-text-secondary { + color: $color-text-secondary; +} + +.ui-progress-bar { + height: 14px; + border-radius: 5px; + // background-color: darken($color-panel, 10%); + background-color: $color-panel; + overflow: hidden; + margin: 4px 0; + font-size: 12px; + + .progress-fill { + height: 100%; + transition: width 0.3s; + } + + &.wasser .progress-fill { background-color: $color-water; } + &.nahrung .progress-fill { background-color: $color-food; } + &.erz .progress-fill { background-color: $color-ore; } +} + +.button { + @include button; +} + + +.ui-section { + padding: 16px; + border-top: 1px solid #202d3e; + + ul { + margin: 8px 0 0 16px; + padding: 0; + list-style-type: disc; + font-size: 13px; + } + + button { + margin-right: 8px; + margin-top: 8px; + } +} \ No newline at end of file diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss new file mode 100644 index 0000000..5f9a84d --- /dev/null +++ b/src/styles/_variables.scss @@ -0,0 +1,19 @@ + +:root { + --background-color: #0b1120; + --primary-color: #e0f0ff; + --secundary-color: #a0b8d8; + --water: #4fc1e9; + --food: #f7c06b; + --button-background: #1c243b; + --button-hover: #3c8dbc; +} + +button { + background-color: var(--button-background); + transition: background-color 0.3s ease-in-out; + + &:hover { + background-color: var(--button-hover); + } +} \ No newline at end of file