This commit is contained in:
Bastian Wagner 2025-04-26 11:19:54 +02:00
parent 8d42a63c76
commit ce7de1dc59
36 changed files with 753 additions and 122 deletions

View File

@ -32,7 +32,9 @@
}
],
"styles": [
"src/styles.scss"
"src/styles.scss",
"src/styles/_variables.scss",
"src/styles/_ui.scss"
],
"scripts": []
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1 +1,5 @@
<div #gameContainer class="game-container"></div>
<!-- <app-ship-dialog></app-ship-dialog> -->
@if (gameService.showPlanetInfo) {
<app-planet-dialog></app-planet-dialog>
}

View File

@ -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;

View File

@ -1,24 +1,45 @@
@if (planet) {
<h1 mat-dialog-title>{{planet.name}}</h1>
<mat-dialog-content>
<div>Bevölkerung: {{ planet.population | number:'0.0-0' }}</div>
<div class="ui-panel">
<div class="ui-title">
<div class="image"><img [src]="'sprites/planets/sm/'+ planet.image +'.png'" alt=""></div>
<div>{{ planet.name }}</div>
</div>
<div class="goods">
<h3>Güter:</h3>
@for (item of planet.getAllGoods(); track $index) {
<div>
{{ item.type }}: {{ item.amount }}
<div class="ui-body">
<div class="ui-text-secondary" style="padding: 16px;">👥 Bevölkerung: {{ population }} 🚀</div>
<div class="ui-section">
<div>🏭 Produktion:</div>
<ul>
@for (item of producedItems; track $index) {
<li>{{ item.type }}: +{{ item.productionRate | number:'0.0-2' }}/s</li>
}
</ul>
</div>
<div class="ui-section">
<div>📦 Vorräte:</div>
@for (item of storedItems; track $index) {
<div class="ui-progress-bar" [ngClass]="item.type.toLowerCase()">
<div class="progress-fill" [style.width.%]="getFillPercentange(item)">{{ item.type }}: {{ item.amount | number:'0.0-1' }}</div>
</div>
}
</div>
<div class="goods">
<h3>Angebotene Güter:</h3>
@for (item of offeredItems(); track $index) {
{{ item.type }}: {{ item.amount | number }} / {{ item.productionStorage | number }}
<div class="ui-section">
<div>📦 Verbrauch:</div>
<ul>
@for (item of consumedItems; track $index) {
<li>{{ item.type }}: {{ item.demandRate * planet.population | number:'0.0-1' }}/s</li>
}
</ul>
</div>
</div>
<div class="ui-section">
<button class="button">Produktion upgraden</button>
<button class="button">Siedeln</button>
<button class="button" (click)="close()" >Schließen</button>
</div>
</div>
</mat-dialog-content>
}

View File

@ -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);
}

View File

@ -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<Planet>(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;
}
}

View File

@ -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);
}

View File

@ -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<ShipDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ShipDialogComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ShipDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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 {
}

View File

@ -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 }
]
}
}
]

View File

@ -9,10 +9,10 @@ export interface GoodConfig {
export const GOODS_DATA: Record<GoodType, GoodConfig> = {
[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 }
};

View File

@ -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<string, Good> = 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<GoodType, number> = 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
}
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}})
result.push({
amount: demand * 3, // Request 3x buffer amount
type: good.type
})
}
request(ship: ShipUi) {
return result;
}
get offeredGoods(): TradeInstance[] {
return this.getAllGoods().filter(g => g.productionRate && g.amount).map(g => { return { type: g.type, amount: g.amount}})
}
// 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<GoodType, number> {
const demand = new Map<GoodType, number>();
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;
}
}
@ -129,3 +256,8 @@ export interface PlanetInit {
productionBonus?: number; // optional: z.B. 1.5 = 150% der Basis
}[];
}
export interface TradeInstance {
type: GoodType;
amount: number;
}

View File

@ -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;
}

View File

@ -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)
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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<PlanetUi> = new EventEmitter();
public rightClick: EventEmitter<PlanetUi> = 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();
}
}

View File

@ -9,3 +9,4 @@ html {
density: 0
));
}

111
src/styles/_ui.scss Normal file
View File

@ -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;
}
}

View File

@ -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);
}
}