This commit is contained in:
Bastian Wagner 2025-04-26 18:12:52 +02:00
parent 9007d94b0d
commit a5767a2c79
30 changed files with 683 additions and 91 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

@ -1,7 +1,17 @@
<div #gameContainer class="game-container"></div> <div #gameContainer class="game-container"></div>
@if (gameService.showShipInfo) { @if (gameService.showShipInfo) {
<app-ship-dialog cdkDrag cdkDragBoundary="html"/> <app-ship-dialog cdkDrag cdkDragBoundary="html" (click)="stopPropagation($event)" (mousedown)="stopPropagation($event)"/>
} }
@if (gameService.showPlanetInfo) { @if (gameService.showPlanetInfo) {
<app-planet-dialog cdkDrag cdkDragBoundary="html"></app-planet-dialog> <app-planet-dialog cdkDrag cdkDragBoundary="html" (click)="stopPropagation($event)" (mousedown)="stopPropagation($event)"></app-planet-dialog>
}
@if (gameService.showBuyShip) {
<app-buy cdkDrag (click)="stopPropagation($event)" (mousedown)="stopPropagation($event)"/>
}
<app-status-bar class="ui-panel" (click)="stopPropagation($event)" (mousedown)="stopPropagation($event)"/>
@if (gameService.showTutorial) {
<app-welcome (click)="stopPropagation($event)" (mousedown)="stopPropagation($event)" />
} }

View File

@ -1,4 +1,4 @@
import { Component, ElementRef, inject, ViewChild } from '@angular/core'; import { Component, ElementRef, inject, LOCALE_ID, ViewChild } from '@angular/core';
import Phaser from 'phaser'; import Phaser from 'phaser';
import { MapScene } from './scene/map.scene'; import { MapScene } from './scene/map.scene';
import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { MatDialog, MatDialogModule } from '@angular/material/dialog';
@ -6,10 +6,20 @@ import { GameService } from './service/game.service';
import { ShipDialogComponent } from './components/dialog/ship-dialog/ship-dialog.component'; import { ShipDialogComponent } from './components/dialog/ship-dialog/ship-dialog.component';
import { PlanetDialogComponent } from './components/dialog/planet-dialog/planet-dialog.component'; import { PlanetDialogComponent } from './components/dialog/planet-dialog/planet-dialog.component';
import { DragDropModule } from '@angular/cdk/drag-drop'; import { DragDropModule } from '@angular/cdk/drag-drop';
import { StatusBarComponent } from './components/status-bar/status-bar.component';
import { BuyComponent } from './components/ships/buy/buy.component';
import { registerLocaleData } from '@angular/common';
import localeDe from '@angular/common/locales/de';
import localeDeExtra from '@angular/common/locales/extra/de';
import { WelcomeComponent } from './components/tutorial/welcome/welcome.component';
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [MatDialogModule, ShipDialogComponent, PlanetDialogComponent, DragDropModule], imports: [MatDialogModule, ShipDialogComponent, PlanetDialogComponent, DragDropModule, StatusBarComponent, BuyComponent, WelcomeComponent],
providers: [{ provide: LOCALE_ID, useValue: 'de-DE' }],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss' styleUrl: './app.component.scss'
}) })
@ -53,4 +63,8 @@ export class AppComponent {
const game = new Phaser.Game(config); const game = new Phaser.Game(config);
} }
stopPropagation(event: any) {
event.stopPropagation();
}
} }

View File

@ -1,5 +1,5 @@
<div class="ui-panel"> <div class="ui-panel">
<div class="ui-title"> <div class="ui-title" cdkDragHandle>
<div class="image"><img [src]="'sprites/planets/sm/'+ planet.image +'.png'" alt=""></div> <div class="image"><img [src]="'sprites/planets/sm/'+ planet.image +'.png'" alt=""></div>
<div>{{ planet.name }}</div> <div>{{ planet.name }}</div>
</div> </div>
@ -7,6 +7,7 @@
<div class="ui-body"> <div class="ui-body">
@if (planet.hasHarbour) {
<div class="ui-text-secondary" style="padding: 16px;">👥 Bevölkerung: {{ population }} 🚀</div> <div class="ui-text-secondary" style="padding: 16px;">👥 Bevölkerung: {{ population }} 🚀</div>
<div class="ui-section"> <div class="ui-section">
<div>Landebuchten: {{ planet.dockCapacity }}</div> <div>Landebuchten: {{ planet.dockCapacity }}</div>
@ -21,7 +22,9 @@
</div> </div>
<div class="ui-section"> <div class="ui-section">
<div>📦 Vorräte:</div> <div style="display: inline-block;"
matTooltip="Jeder Planet braucht ausreichend Vorräte. Hat er genug Vorräte, ziehen Menschen in die Stadt des Raumhafens. Existieren nicht genug Rohstoffe, verlassen Menschen die Stadt. Je mehr Menschen hier leben, desto höher wird ihr Verbrauch."
>📦 Vorräte:</div>
@for (item of storedItems; track $index) { @for (item of storedItems; track $index) {
<div class="ui-progress-bar" [ngClass]="item.type.toLowerCase()"> <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 class="progress-fill" [style.width.%]="getFillPercentange(item)">{{ item.type }}: {{ item.amount | number:'0.0-1' }}</div>
@ -38,6 +41,7 @@
</ul> </ul>
</div> </div>
@if (goodsInTransit.length > 0) {
<div class="ui-section"> <div class="ui-section">
<div>📦 In Lieferung:</div> <div>📦 In Lieferung:</div>
<ul> <ul>
@ -46,11 +50,33 @@
} }
</ul> </ul>
</div> </div>
}
} @else {
<div class="ui-text-secondary" style="padding: 16px;">Baue einen Raumhafen um diesen Planeten zu erschließen, und auf die Ressourcen zuzugreifen. Es bildet sich eine kleine Siedlung um den Raumhafen. Je besser diese versorgt ist, desto mehr Menschen ziehen hin. Gibt es zu wenig Rohstoffe, verlassen die Menschen die Siedlung und ziehen ins Umland um sich selbst zu versorgen.</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> </div>
<div class="ui-section"> <div class="ui-section flex">
<button class="button" (click)="upgradeHarbour()" >Raumhafen upgraden</button> @if (planet.hasHarbour) {
<button class="button">Siedeln</button> <button class="button" (click)="upgradeHarbour()"
matTooltip="Stellt eine zusätzliche Landebucht zur Verfügung. Pro Landebucht kann ein Raumschiff verladen werden. Das Upgrade kostet allerdings Geld."
>Raumhafen upgraden</button>
} @else {
<button class="button" (click)="buildHarbour()"
matTooltip="Baue einen Raumhafen um den Planeten anfliegen zu können."
>Raumhafen bauen</button>
}
<button class="button" (click)="close()" >Schließen</button> <button class="button" (click)="close()" >Schließen</button>
</div> </div>
</div> </div>

View File

@ -4,4 +4,10 @@
position: absolute; position: absolute;
top: 24px; top: 24px;
left: 24px; left: 24px;
width: 500px;
}
img {
width: 128px;
height: 128px;
} }

View File

@ -5,11 +5,12 @@ import { CommonModule } from '@angular/common';
import { Good } from '../../../model/goods/good.interface'; import { Good } from '../../../model/goods/good.interface';
import { GoodType } from '../../../model/goods/good-type.enum'; import { GoodType } from '../../../model/goods/good-type.enum';
import { GameService } from '../../../service/game.service'; import { GameService } from '../../../service/game.service';
import {DragDropModule} from '@angular/cdk/drag-drop'; import {CdkDragHandle, DragDropModule} from '@angular/cdk/drag-drop';
import {MatTooltipModule} from '@angular/material/tooltip';
@Component({ @Component({
selector: 'app-planet-dialog', selector: 'app-planet-dialog',
imports: [CommonModule, MatDialogModule, DragDropModule], imports: [CommonModule, MatDialogModule, DragDropModule, MatTooltipModule, CdkDragHandle],
templateUrl: './planet-dialog.component.html', templateUrl: './planet-dialog.component.html',
styleUrl: './planet-dialog.component.scss' styleUrl: './planet-dialog.component.scss'
}) })
@ -17,7 +18,6 @@ export class PlanetDialogComponent {
public gameService: GameService = inject(GameService) public gameService: GameService = inject(GameService)
ngOnInit() { ngOnInit() {
console.log(this.planet)
} }
@ -67,5 +67,10 @@ export class PlanetDialogComponent {
this.planet.upgradeHarbour() this.planet.upgradeHarbour()
} }
buildHarbour() {
this.planet.buildHarbour()
}
} }

View File

@ -1,6 +1,6 @@
<div class="ui-panel" (click)="onClick($event)"> <div class="ui-panel" (click)="onClick($event)">
<div class="ui-title"> <div class="ui-title" cdkDragHandle>
<div class="image"><img [src]="'sprites/ships/01-starter.png'" alt=""></div> <div class="image"><img [src]="'sprites/ships/01-starter.png'" alt=""></div>
<div>{{ ship.name }}</div> <div>{{ ship.name }}</div>
</div> </div>
@ -70,7 +70,7 @@
</div> </div>
<div class="ui-section"> <div class="ui-section flex">
<button class="button">Produktion upgraden</button> <button class="button">Produktion upgraden</button>
<button class="button">Siedeln</button> <button class="button">Siedeln</button>
<button class="button" (click)="onClick($event); close();" >Schließen</button> <button class="button" (click)="onClick($event); close();" >Schließen</button>

View File

@ -2,12 +2,12 @@ import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { GameService } from '../../../service/game.service'; import { GameService } from '../../../service/game.service';
import { Ship } from '../../../model/ships/ship.model'; import { Ship } from '../../../model/ships/ship.model';
import { Good } from '../../../model/goods/good.interface';
import { TradeInstance } from '../../../model/planet.model'; import { TradeInstance } from '../../../model/planet.model';
import { CdkDragHandle } from '@angular/cdk/drag-drop';
@Component({ @Component({
selector: 'app-ship-dialog', selector: 'app-ship-dialog',
imports: [CommonModule], imports: [CommonModule, CdkDragHandle],
templateUrl: './ship-dialog.component.html', templateUrl: './ship-dialog.component.html',
styleUrl: './ship-dialog.component.scss' styleUrl: './ship-dialog.component.scss'
}) })

View File

@ -0,0 +1,86 @@
<div class="ui-panel">
<div class="ui-title" cdkDragHandle>
Raumschiff einsetzen
</div>
<div class="ui-section">
<div>🚀 Neues Schiff: Pioneer-1</div>
<ul>
<li class="flex">
<div>Kosten: </div>
<div>{{ config.buyCost | number:'0.0-2'}} Credits</div>
</li>
<li class="flex">
<div>Frachtraum: </div>
<div>{{ config.cargoSize | number: '0.0-2'}} t</div>
</li>
<li class="flex">
<div>Max Geschwindigkeit: </div>
<div>{{ config.maxSpeed | number: '0.0-2'}} km/s</div>
</li>
<li class="flex">
<div>Unterhalt: </div>
<div>{{ config.cost | number: '0.0-2'}} Credits</div>
</li>
</ul>
</div>
<div class="ui-section">
<div>🛣️ Route festlegen:</div>
@if (availablePlanets.length == 0) {
<div style="margin-top: 12px;">
Um Planeten anfliegen zu können, musst du einen Weltraumhafen bauen.
</div>
}
<div class="flex route-containers">
<div class="route-column">
<div class="route-header">Ausgewählte Planeten</div>
<div class="route-selection" cdkDropList #selected="cdkDropList"
[cdkDropListData]="selectedPlanets"
[cdkDropListConnectedTo]="[available]"
(cdkDropListDropped)="drop($event)">
@for (planet of selectedPlanets; track planet.name) {
<div class="planet-option" cdkDrag>
{{ planet.name }}
</div>
}
</div>
</div>
<div class="route-column">
<div class="route-header">Verfügbare Planeten</div>
<div class="route-selection" cdkDropList #available="cdkDropList"
[cdkDropListData]="availablePlanets"
[cdkDropListConnectedTo]="[selected]"
(cdkDropListDropped)="drop($event)">
@for (planet of availablePlanets; track planet.name) {
<div class="planet-option" cdkDrag>
{{ planet.name }}
</div>
}
</div>
</div>
</div>
</div>
<div class="ui-section">
<div>Ausgewählte Route:</div>
<div class="flex route">
@for (planet of selectedPlanets; track planet.name) {
<div>{{ planet.name }}</div>
}
</div>
</div>
<div class="ui-section flex">
<button class="button"
[disabled]="selectedPlanets.length < 2 || !canAffordShip()"
(click)="buyShip()">
Schiff kaufen
</button>
<button class="button" (click)="close()">Abbrechen</button>
</div>
</div>

View File

@ -0,0 +1,56 @@
:host {
position: absolute;
display: flex;
flex-direction: column;
position: absolute;
top: 24px;
left: 24px;
// width: 240px;
// height: 240px;
// background-color: var(--background-color);
// color: var(--primary-color);
width: 600px;
z-index: 1;
}
.route-containers {
gap: 16px;
margin: 16px 0;
}
.route-column {
flex: 1;
}
.route-header {
font-size: 14px;
color: var(--secundary-color);
margin-bottom: 8px;
}
.route-selection {
min-height: 100px;
border: 1px solid #202d3e;
border-radius: 8px;
padding: 8px;
}
.planet-option {
padding: 8px;
margin: 4px 0;
background-color: #1c243b;
border-radius: 4px;
cursor: move;
&:hover {
background-color: #3c8dbc;
}
}
.route {
display: flex;
gap: 8px;
padding: 8px;
border: 1px solid #202d3e;
border-radius: 4px;
}

View File

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

View File

@ -0,0 +1,59 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { GameService } from '../../../service/game.service';
import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
import { Planet } from '../../../model/planet.model';
import { ShipConfig } from '../../../model/ships/ship.model';
@Component({
selector: 'app-buy',
imports: [CommonModule, CdkDropList, CdkDrag, CdkDragHandle],
templateUrl: './buy.component.html',
styleUrl: './buy.component.scss'
})
export class BuyComponent {
private gameService: GameService = inject(GameService);
availablePlanets: Planet[] = this.gameService.planets.filter(p => p.hasHarbour);
selectedPlanets: Planet[] = [];
config: ShipConfig = {
name: 'Pioneer-01',
acceleration: 500,
cargoSize: 10,
cost: 0.6,
loadingSpeed: 25,
maxSpeed: 500,
planetRoute: [],
buyCost: 2000
}
canAffordShip() {
return this.gameService.money >= 3000;
}
buyShip() {
if (!this.canAffordShip()) return;
this.gameService.createShip({
...this.config,
planetRoute: this.selectedPlanets
})
this.close();
}
close() {
this.gameService.showBuyShip = false;
}
drop(event: CdkDragDrop<Planet[]>) {
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
} else {
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex,
);
}
}
}

View File

@ -0,0 +1,8 @@
<div (click)="onClick($event)" (mousedown)="onClick($event)" class="flex gamebar">
<div class="gold">
Credits: {{gameService.money | number:'0.0-0'}}
</div>
<button class="button" (click)="toggleBuy()" >Schiffe</button>
</div>

View File

@ -0,0 +1,11 @@
:host {
display: flex;
position: absolute;
top: 4px;
left: 40%;
pointer-events: auto;
}
.gamebar {
align-items: center;
}

View File

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

View File

@ -0,0 +1,26 @@
import { Component, inject } from '@angular/core';
import { GameService } from '../../service/game.service';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-status-bar',
imports: [CommonModule],
templateUrl: './status-bar.component.html',
styleUrl: './status-bar.component.scss'
})
export class StatusBarComponent {
public gameService: GameService = inject(GameService);
onClick(event: any) {
event.stopPropagation();
}
print() {
console.log("PRI")
}
toggleBuy() {
this.gameService.showBuyShip = !this.gameService.showBuyShip;
}
}

View File

@ -0,0 +1,57 @@
<div class="ui-panel welcome-container">
<div class="ui-title">
<h1>🚀 Willkommen bei StellarLines</h1>
</div>
<div class="ui-body">
<div class="ui-section">
<h2>Deine interplanetare Handelsgesellschaft</h2>
<p>
Willkommen im Jahr 2184!
<br>
<br>
Als aufstrebender Unternehmer ist es deine Aufgabe,
ein florierendes Handelsnetzwerk zwischen den Planeten aufzubauen. Gründe ein
interplanetares Frachtunternehmen, transportiere wichtige Güter zwischen den
Kolonien und erwirtschafte Profite mit jedem erfolgreichen Transport.
</p>
</div>
<div class="ui-section">
<h3>🎯 Deine Ziele:</h3>
<ul>
<li>Errichte Raumhäfen auf verschiedenen Planeten</li>
<li>Baue Handelsrouten zwischen den Planeten auf</li>
<li>Versorge die Siedlungen mit wichtigen Gütern</li>
<li>Erweitere deine Handelsflotte</li>
<li>Schaffe prosperierende Kolonien</li>
</ul>
</div>
<div class="ui-section">
<h3>🎮 So spielst du:</h3>
<ol>
<li>Wähle einen Planeten und baue einen Raumhafen</li>
<li>Kaufe dein erstes Handelsschiff (Hierfür brauchst du 2 Planeten)</li>
<li>Lege Handelsrouten zwischen Planeten fest</li>
<li>Versorge die Siedlungen mit Rohstoffen</li>
<li>Erwirtschafte Gewinne durch geschickten Handel</li>
</ol>
</div>
<div class="ui-section tips">
<h3>💡 Tipp:</h3>
<p>
Achte auf die Bedürfnisse der Siedlungen. Gut versorgte Kolonien wachsen und
erhöhen ihren Bedarf an Waren. Bei Mangel wandern die Siedler ab! Für den Anfang benötigen die Kolonien nur Nahrung und Wasser. Mit wachsender Bevölkerung steigt auch der Bedarf an weiteren Rohstoffen.
<br><br>
Du beginnst mit 10.000 Credits. Gib sie weise aus, Schiffe und Raumhäfen kosten Unterhalt. Je mehr Waren du transportierst, desto mehr Geld verdienst du auch. Planeten kaufen allerdings nur Waren, die sie auch brauchen.
Schiffe auf Handelsrouten fliegen die Planeten ab, und versorgen sie automatisch mit den Gütern die sie benötigen.
</p>
</div>
</div>
<div class="ui-section">
<button class="button" (click)="close()" >Spiel starten</button>
</div>
</div>

View File

@ -0,0 +1,20 @@
:host {
display: flex;
flex-direction: column;
position: absolute;
top: 24px;
left: 24px;
right: 24px;
bottom: 24px;
}
.ui-body{
overflow-y: auto;
}
.ui-panel {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}

View File

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

View File

@ -0,0 +1,18 @@
import { Component, inject } from '@angular/core';
import { GameService } from '../../../service/game.service';
@Component({
selector: 'app-welcome',
imports: [],
templateUrl: './welcome.component.html',
styleUrl: './welcome.component.scss'
})
export class WelcomeComponent {
gameService = inject(GameService);
close() {
this.gameService.showTutorial = false;
}
}

View File

@ -3,21 +3,20 @@ import { PlanetInit } from "../model/planet.model";
export const PLANETCONFIGS: { x: number, y: number, texture: string, config: PlanetInit}[] = [ export const PLANETCONFIGS: { x: number, y: number, texture: string, config: PlanetInit}[] = [
{ {
x: 200, x: 600,
y: 300, y: 800,
texture: 'terra-nova', texture: 'terra-nova',
config: { config: {
name: 'Terra Nova', name: 'Terra Nova',
initialGoods: [ initialGoods: [
{ type: GoodType.Wasser, amount: 120 }, { type: GoodType.Wasser, amount: 15 },
{ type: GoodType.Nahrung, amount: 80, productionBonus: 1.3 }, { type: GoodType.Nahrung, amount: 8, productionBonus: 1.3 },
{ type: GoodType.Erz, amount: 100 }
] ]
} }
}, },
{ {
x: 600, x: 3000,
y: 200, y: 900,
texture: 'mechanica-prime', texture: 'mechanica-prime',
config: { config: {
name: 'Mechanica Prime', name: 'Mechanica Prime',
@ -31,21 +30,21 @@ export const PLANETCONFIGS: { x: number, y: number, texture: string, config: Pla
} }
}, },
{ {
x: 400, x: 1800,
y: 700, y: 2800,
texture: 'aqualis', texture: 'aqualis',
config: { config: {
name: 'Aqualis', name: 'Aqualis',
initialGoods: [ initialGoods: [
{ type: GoodType.Wasser, amount: 300, productionBonus: 2.0 }, { type: GoodType.Wasser, amount: 30, productionBonus: 2.0 },
{ type: GoodType.Nahrung, amount: 60 }, { type: GoodType.Nahrung, amount: 15 },
{ type: GoodType.Treibstoff, amount: 10 } { type: GoodType.Treibstoff, amount: 10 }
] ]
} }
}, },
{ {
x: 900, x: 3900,
y: 500, y: 2500,
texture: 'planet', texture: 'planet',
config: { config: {
name: 'Ferron', name: 'Ferron',
@ -59,8 +58,8 @@ export const PLANETCONFIGS: { x: number, y: number, texture: string, config: Pla
} }
}, },
{ {
x: 800, x: 5400,
y: 800, y: 1400,
texture: 'novus-reach', texture: 'novus-reach',
config: { config: {
name: 'Novus Reach', name: 'Novus Reach',
@ -71,4 +70,31 @@ export const PLANETCONFIGS: { x: number, y: number, texture: string, config: Pla
] ]
} }
} }
,
{
x: 3700,
y: 4600,
texture: 'novus-reach',
config: {
name: 'Novus Reach 2',
initialGoods: [
{ type: GoodType.Nahrung, amount: 90, productionBonus: 1.5 },
{ type: GoodType.Wasser, amount: 50 },
{ type: GoodType.Bauteile, amount: 15 }
]
}
},
{
x: 6000,
y: 3600,
texture: 'terra-nova',
config: {
name: 'Terra Nova 2',
initialGoods: [
{ type: GoodType.Wasser, amount: 120 },
{ type: GoodType.Nahrung, amount: 80, productionBonus: 1.3 },
{ type: GoodType.Erz, amount: 100 }
]
}
}
] ]

View File

@ -9,8 +9,8 @@ export interface GoodConfig {
export const GOODS_DATA: Record<GoodType, GoodConfig> = { export const GOODS_DATA: Record<GoodType, GoodConfig> = {
[GoodType.Erz]: { baseProduction: 1.0, baseDemand: 0, storageLimit: 500, isRawResource: true }, [GoodType.Erz]: { baseProduction: 1.0, baseDemand: 0, storageLimit: 500, isRawResource: true },
[GoodType.Wasser]: { baseProduction: 1, baseDemand: 0.015, storageLimit: 300, isRawResource: true }, [GoodType.Wasser]: { baseProduction: 1, baseDemand: 0.0015, storageLimit: 300, isRawResource: true },
[GoodType.Nahrung]: { baseProduction: 1, baseDemand: 0.01, storageLimit: 200, isRawResource: true }, [GoodType.Nahrung]: { baseProduction: 1, baseDemand: 0.001, storageLimit: 200, isRawResource: true },
[GoodType.Metall]: { baseProduction: 1, baseDemand: 0, storageLimit: 300, isRawResource: false }, [GoodType.Metall]: { baseProduction: 1, baseDemand: 0, storageLimit: 300, isRawResource: false },
[GoodType.Treibstoff]: { baseProduction: 1, baseDemand: 0, storageLimit: 150, isRawResource: false }, [GoodType.Treibstoff]: { baseProduction: 1, baseDemand: 0, storageLimit: 150, isRawResource: false },
[GoodType.Elektronik]: { baseProduction: 1, baseDemand: 0, storageLimit: 100, isRawResource: false }, [GoodType.Elektronik]: { baseProduction: 1, baseDemand: 0, storageLimit: 100, isRawResource: false },

View File

@ -1,8 +1,7 @@
import { interval, Subscription } from "rxjs"; import { interval } from "rxjs";
import { GoodType } from "./goods/good-type.enum"; import { GoodType } from "./goods/good-type.enum";
import { GOODS_DATA } from "./goods/good-config"; import { GOODS_DATA } from "./goods/good-config";
import { Good } from "./goods/good.interface"; import { Good } from "./goods/good.interface";
import { ShipUi } from "./ship";
import { Ship } from "./ships/ship.model"; import { Ship } from "./ships/ship.model";
import { GameService } from "../service/game.service"; import { GameService } from "../service/game.service";
@ -17,7 +16,7 @@ export class Planet {
public isGrowing: boolean = false; public isGrowing: boolean = false;
private populationGrowthRate = 0.002; // Basiswachstum pro Tick (%) private populationGrowthRate = 0.002; // Basiswachstum pro Tick (%)
private populationDeclineRate = 0.005; // Basisrückgang bei Mangel (%) private populationDeclineRate = 0.005; // Basisrückgang bei Mangel (%)
demandSecondsBuffer = 30; // Anfrage immer mindestens 30 Sekunden überleben demandSecondsBuffer = 150; // Anfrage immer mindestens 30 Sekunden überleben
private productionLevel: Map<GoodType, number> = new Map(); private productionLevel: Map<GoodType, number> = new Map();
private dockedShips: Ship[] = []; private dockedShips: Ship[] = [];
@ -26,6 +25,8 @@ export class Planet {
private gameService: GameService; private gameService: GameService;
hasHarbour = false;
constructor(config: PlanetInit, gameService: GameService) { constructor(config: PlanetInit, gameService: GameService) {
this.name = config.name; this.name = config.name;
@ -133,12 +134,14 @@ export class Planet {
const inTransit = this.goodsInTransit.filter(g => g.type == good.type).reduce((acc, succ) => acc + succ.amount, 0); const inTransit = this.goodsInTransit.filter(g => g.type == good.type).reduce((acc, succ) => acc + succ.amount, 0);
if (demand < good.amount + inTransit) { if (demand < good.amount + inTransit) {
console.log(`[${this.name}] Skipping request for ${good.type}: ${inTransit} in transit + ${good.amount} > ${demand}`);
continue; // Es werden genug geliefert. continue; // Es werden genug geliefert.
} }
result.push({ result.push({
amount: demand * 3, // Request 3x buffer amount amount: demand * 5, // Request 3x buffer amount
type: good.type, type: good.type,
target: this.name, target: this.name,
}) })
@ -286,10 +289,7 @@ export class Planet {
} }
undock(ship: Ship) { undock(ship: Ship) {
console.log("undock", ship.name)
this.dockedShips = this.dockedShips.filter(d => d != ship); this.dockedShips = this.dockedShips.filter(d => d != ship);
console.log('Angedockt: ',this.dockedShips.map(d => d.name).join(','))
console.log('wartend: ',this.shipsWaitingForDocking.map(d => d.name).join(','))
} }
get goodsInTransit(): TradeInstance[] { get goodsInTransit(): TradeInstance[] {
@ -303,6 +303,12 @@ export class Planet {
this.dockCapacity++; this.dockCapacity++;
} }
buildHarbour() {
this.hasHarbour = true;
this.gameService.money -= 3000;
}
} }
export interface PlanetInit { export interface PlanetInit {

View File

@ -8,23 +8,23 @@ export class ShipUi extends Phaser.Physics.Arcade.Sprite {
private velocity = new Phaser.Math.Vector2(0, 0); private velocity = new Phaser.Math.Vector2(0, 0);
private target: PlanetUi | null = null; private target: PlanetUi | null = null;
private targetVector: Phaser.Math.Vector2 | null = null; // Später Planet! private targetVector: Phaser.Math.Vector2 | null = null; // Später Planet!
public model: Ship = new Ship(); public model: Ship;
public gameService: GameService; public gameService: GameService;
private orbitAngle: number = 0; private orbitAngle: number = 0;
private orbitRadius: number = 120; private orbitRadius: number = 120;
private orbitSpeed: number = Math.PI / 25; // Eine Runde in 2 Sekunden private orbitSpeed: number = Math.PI / 25; // Eine Runde in 2 Sekunden
constructor(scene: MapScene, x: number, y: number, gameService: GameService) { constructor(scene: MapScene, x: number, y: number, gameService: GameService, ship: Ship) {
super(scene, x, y, 'ship'); super(scene, x, y, 'ship');
this.gameService = gameService; this.gameService = gameService;
this.model = ship;
scene.add.existing(this); scene.add.existing(this);
scene.physics.add.existing(this); scene.physics.add.existing(this);
this.setOrigin(0.5); this.setOrigin(0.5);
this.setScale(1); // oder z.B. 0.5 je nach Bildgröße this.setScale(1); // oder z.B. 0.5 je nach Bildgröße
this.setDisplaySize(32, 32) this.setDisplaySize(96, 128)
this.setCollideWorldBounds(true); this.setCollideWorldBounds(true);
const conf: Phaser.Types.Input.InputConfiguration = { const conf: Phaser.Types.Input.InputConfiguration = {
useHandCursor: true useHandCursor: true
@ -62,7 +62,7 @@ export class ShipUi extends Phaser.Physics.Arcade.Sprite {
switch (this.model.flightMode) { switch (this.model.flightMode) {
case FlightMode.Normal: case FlightMode.Normal:
if (distance < 150) { if (distance < (this.target.height / 2) + 100) {
this.startOrbit(this.target); this.startOrbit(this.target);
return; return;
} }
@ -73,7 +73,7 @@ export class ShipUi extends Phaser.Physics.Arcade.Sprite {
break; break;
case FlightMode.Docking: case FlightMode.Docking:
this.flyDirectlyToTarget(to, dt); this.flyDirectlyToTarget(to, dt);
if (distance < 5) { if (distance < 150) {
this.land(); this.land();
} }
break; break;
@ -104,21 +104,21 @@ export class ShipUi extends Phaser.Physics.Arcade.Sprite {
this.velocity.set(Math.cos(angle) * this.model.currentSpeed, Math.sin(angle) * this.model.currentSpeed); this.velocity.set(Math.cos(angle) * this.model.currentSpeed, Math.sin(angle) * this.model.currentSpeed);
this.body.setVelocity(this.velocity.x, this.velocity.y); this.body.setVelocity(this.velocity.x, this.velocity.y);
this.setRotation(this.velocity.angle() + Phaser.Math.DegToRad(180)); this.setRotation(this.velocity.angle() + Phaser.Math.DegToRad(90));
} }
private updateOrbit(center: Phaser.Math.Vector2, dt: number): void { private updateOrbit(center: Phaser.Math.Vector2, dt: number): void {
if (!(this.body instanceof Phaser.Physics.Arcade.Body)) { return; } if (!(this.body instanceof Phaser.Physics.Arcade.Body)) { return; }
this.orbitAngle += this.orbitSpeed * dt; this.orbitAngle += this.orbitSpeed * dt;
const targetX = center.x + Math.cos(this.orbitAngle) * this.orbitRadius; const targetX = center.x + Math.cos(this.orbitAngle) * this.target!.height / 2;
const targetY = center.y + Math.sin(this.orbitAngle) * this.orbitRadius; const targetY = center.y + Math.sin(this.orbitAngle) * this.target!.height / 2;
const desiredVelocityX = (targetX - this.getWorldPoint().x) * 5; const desiredVelocityX = (targetX - this.getWorldPoint().x) * 5;
const desiredVelocityY = (targetY - this.getWorldPoint().y) * 5; const desiredVelocityY = (targetY - this.getWorldPoint().y) * 5;
this.body.setVelocity(desiredVelocityX, desiredVelocityY); this.body.setVelocity(desiredVelocityX, desiredVelocityY);
this.setRotation(this.body.velocity.angle() + Phaser.Math.DegToRad(180)); this.setRotation(this.body.velocity.angle() + Phaser.Math.DegToRad(90));
// Umdrehungen zählen // Umdrehungen zählen
if (this.orbitAngle >= Math.PI * 2) { if (this.orbitAngle >= Math.PI * 2) {

View File

@ -1,17 +1,49 @@
import { interval } from "rxjs";
import { Good } from "../goods/good.interface"; import { Good } from "../goods/good.interface";
import { Planet, TradeInstance } from "../planet.model"; import { Planet, TradeInstance } from "../planet.model";
import { TradeRoute } from "../routes/trade-route.model"; import { TradeRoute } from "../routes/trade-route.model";
import { GameService } from "../../service/game.service";
export interface ShipConfig {
name: string;
cargoSize: number;
acceleration: number;
maxSpeed: number;
loadingSpeed: number;
cost: number;
planetRoute: Planet[];
buyCost: number;
}
export class Ship { export class Ship {
public acceleration = 10; // Pixel pro Sekunde² public acceleration = 20; // Pixel pro Sekunde²
public maxSpeed = 100; public maxSpeed = 500;
public currentSpeed = 0; public currentSpeed = 0;
public cargoSize = 50; public cargoSize = 10;
cargoSpace: TradeInstance[] = []; cargoSpace: TradeInstance[] = [];
public route: TradeRoute | undefined = new TradeRoute([]) public route: TradeRoute | undefined = new TradeRoute([])
public name = "Pioneer-01-" + Math.round(Math.random() * 100); public name = "Pioneer-01-" + Math.round(Math.random() * 100);
public loadingSpeed = 3.5; public loadingSpeed = 3.5;
public cost = 0.5
private updateInterval = interval(1000);
private gameService: GameService;
constructor(gameService: GameService, config: ShipConfig) {
this.gameService = gameService;
this.acceleration = config.acceleration;
this.cargoSize = config.cargoSize;
this.maxSpeed = config.maxSpeed;
this.loadingSpeed = config.loadingSpeed;
this.cost = config.cost;
this.route = new TradeRoute(config.planetRoute)
this.updateInterval.subscribe(() => {
this.update();
})
}
public status: 'loading' | 'unloading' | 'waiting' | 'idle' | 'traveling' = 'idle'; public status: 'loading' | 'unloading' | 'waiting' | 'idle' | 'traveling' = 'idle';
public flightMode: FlightMode = FlightMode.Normal; public flightMode: FlightMode = FlightMode.Normal;
@ -89,6 +121,7 @@ export class Ship {
if (existing) { if (existing) {
existing.amount -= (cargo.amount / steps); existing.amount -= (cargo.amount / steps);
to.amount += (cargo.amount / steps); to.amount += (cargo.amount / steps);
this.gameService.money += 5;
} }
await this.waitForLoading(1) await this.waitForLoading(1)
} }
@ -126,6 +159,10 @@ export class Ship {
}, amount * 1000 / this.loadingSpeed) }, amount * 1000 / this.loadingSpeed)
}) })
} }
update() {
this.gameService.money -= this.cost;
}
} }
export enum FlightMode { export enum FlightMode {

View File

@ -21,14 +21,14 @@ export class MapScene extends Phaser.Scene {
} }
preload() { preload() {
this.load.image('ship', 'sprites/ships/01-starter.png'); this.load.image('ship', 'sprites/ships/swift-hauler.png');
this.load.image('harbour', 'sprites/buildings/harbour.png'); this.load.image('harbour', 'sprites/buildings/harbour.png');
this.load.image('planet', 'sprites/planets/sm/planet-1.png'); this.load.image('planet', 'sprites/planets/planet-1.png');
this.load.image('terra-nova', 'sprites/planets/sm/terra-nova.png'); this.load.image('terra-nova', 'sprites/planets/terra-nova.png');
this.load.image('mechanica-prime', 'sprites/planets/sm/mechanica-prime.png'); this.load.image('mechanica-prime', 'sprites/planets/mechanica-prime.png');
this.load.image('novus-reach', 'sprites/planets/sm/novus-reach.png'); this.load.image('novus-reach', 'sprites/planets/novus-reach.png');
this.load.image('aqualis', 'sprites/planets/sm/aqualis.png'); this.load.image('aqualis', 'sprites/planets/aqualis.png');
this.load.image('ferron', 'sprites/planets/sm/ferron.png'); this.load.image('ferron', 'sprites/planets/ferron.png');
} }
create() { create() {
@ -79,7 +79,7 @@ export class MapScene extends Phaser.Scene {
const now = Date.now(); const now = Date.now();
if (now - this.lastZoomTime < 50) return; // nur alle 100ms if (now - this.lastZoomTime < 50) return; // nur alle 100ms
this.lastZoomTime = now; this.lastZoomTime = now;
const factor = 1.05; const factor = 1.1;
if (deltaY > 0) { if (deltaY > 0) {
this.camera.setZoom(this.camera.zoom / factor); this.camera.setZoom(this.camera.zoom / factor);
} else { } else {
@ -91,23 +91,20 @@ export class MapScene extends Phaser.Scene {
this.camera.setZoom(Phaser.Math.Clamp(this.camera.zoom, minScale, 1)); this.camera.setZoom(Phaser.Math.Clamp(this.camera.zoom, minScale, 1));
}); });
this.createShip(); this.camera.setZoom(0.4)
setTimeout(() => {
this.createShip();
}, 100)
this.gameService.onShipCreate.subscribe(ship => {
const ui = new ShipUi(this, 100, 100, this.gameService, ship);
ui.activateRoute();
this.physics.world.enable(ui);
this.ships.push(ui);
})
// this.enableMouseLogging();
} }
createShip() {
const s = new ShipUi(this, 100, 100, this.gameService);
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();
this.gameService.ships.push(s.model)
}
override update(time: number, delta: number): void { override update(time: number, delta: number): void {
if (this.ship) { if (this.ship) {
@ -132,6 +129,7 @@ export class MapScene extends Phaser.Scene {
}); });
this.planets.push(planet); this.planets.push(planet);
this.gameService.planets.push(planet.model); this.gameService.planets.push(planet.model);
} }
} }
@ -146,4 +144,20 @@ export class MapScene extends Phaser.Scene {
getPlanetUiByName(name: string) { getPlanetUiByName(name: string) {
return this.planets.find(p => p.model.name == name); return this.planets.find(p => p.model.name == name);
} }
enableMouseLogging() {
let mouseText: Phaser.GameObjects.Text;
this.input.on('pointermove', (pointer: Phaser.Input.Pointer) => {
if (!mouseText) {
mouseText = this.add.text(30, 30, '', {
fontSize: '160px',
color: '#00ff00',
backgroundColor: '#000000',
padding: { x: 5, y: 2 }
}).setScrollFactor(0).setDepth(9999);
}
mouseText.setText(`X: ${pointer.worldX.toFixed(0)}\nY: ${pointer.worldY.toFixed(0)}`);
});
}
} }

View File

@ -1,8 +1,6 @@
import { inject, Injectable } from "@angular/core"; import { EventEmitter, Injectable } from "@angular/core";
import { MatDialog, MatDialogRef } from "@angular/material/dialog";
import { PlanetDialogComponent } from "../components/dialog/planet-dialog/planet-dialog.component";
import { Planet } from "../model/planet.model"; import { Planet } from "../model/planet.model";
import { Ship } from "../model/ships/ship.model"; import { Ship, ShipConfig } from "../model/ships/ship.model";
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -10,10 +8,17 @@ import { Ship } from "../model/ships/ship.model";
export class GameService { export class GameService {
public showPlanetInfo: Planet | undefined; public showPlanetInfo: Planet | undefined;
public showShipInfo: Ship | undefined; public showShipInfo: Ship | undefined;
public showBuyShip = false;
public ships: Ship[] = []; public ships: Ship[] = [];
public planets: Planet[] = []; public planets: Planet[] = [];
showTutorial = true;
public money = 10000;
onShipCreate: EventEmitter<Ship> = new EventEmitter();
constructor() {} constructor() {}
showDialog(planet: Planet) { showDialog(planet: Planet) {
@ -27,4 +32,15 @@ export class GameService {
get canDrag(): boolean { get canDrag(): boolean {
return this.showShipInfo == undefined && this.showPlanetInfo == undefined; return this.showShipInfo == undefined && this.showPlanetInfo == undefined;
} }
createShip(config: ShipConfig) {
if (this.money < config.buyCost) {
return;
}
this.money -= config.buyCost;
const ship = new Ship(this, config);
this.ships.push(ship);
this.onShipCreate.emit(ship);
}
} }

View File

@ -25,6 +25,7 @@ export class PlanetStatus {
} }
update() { update() {
this.container.setScale(1 / this.scene.cameras.main.zoom)
const population = Math.floor(this.getPopulation()); const population = Math.floor(this.getPopulation());
const growth = this.getGrowthState(); const growth = this.getGrowthState();
const criticalGoods = this.getCriticalGoods(); const criticalGoods = this.getCriticalGoods();
@ -41,7 +42,13 @@ export class PlanetStatus {
criticalText = ' 🛑 ' + criticalGoods.map(g => g.toString().substring(0, 2)).join(','); criticalText = ' 🛑 ' + criticalGoods.map(g => g.toString().substring(0, 2)).join(',');
} }
this.text.setText(`${this.planetSprite.model.name}, 👥 ${population} ${growthSymbol}${criticalText}`); let text = `${this.planetSprite.model.name}`;
if (this.planetSprite.model.hasHarbour) {
text += `, 👥 ${population} ${growthSymbol}${criticalText}`
}
this.text.setText(text);
// Position immer über Planet aktualisieren // Position immer über Planet aktualisieren
this.container.setPosition(this.planetSprite.x, this.planetSprite.y - (this.planetSprite.height / 2) - 5); this.container.setPosition(this.planetSprite.x, this.planetSprite.y - (this.planetSprite.height / 2) - 5);

View File

@ -10,6 +10,7 @@ export class PlanetUi extends Phaser.Physics.Arcade.Sprite {
public rightClick: EventEmitter<PlanetUi> = new EventEmitter(); public rightClick: EventEmitter<PlanetUi> = new EventEmitter();
public model: Planet; public model: Planet;
private status: PlanetStatus | undefined; private status: PlanetStatus | undefined;
private harbourImage!: any;
private updateInterval = interval(1000) private updateInterval = interval(1000)
@ -41,8 +42,11 @@ export class PlanetUi extends Phaser.Physics.Arcade.Sprite {
this.updateInterval.subscribe(() => this.update()) this.updateInterval.subscribe(() => this.update())
const image = this.scene.add.image(this.getWorldPoint().x, this.getWorldPoint().y, 'harbour') this.harbourImage = this.scene.add.image(this.getWorldPoint().x, this.getWorldPoint().y, 'harbour')
image.setDisplaySize(64, 96); this.harbourImage.setDisplaySize(164, 256);
this.harbourImage.setVisible(false);
// this.setScale(0.5)
// this.setInteractive(new Phaser.Geom.Circle(x, y, 200), Phaser.Geom.Circle.Contains); // this.setInteractive(new Phaser.Geom.Circle(x, y, 200), Phaser.Geom.Circle.Contains);
} }
@ -65,6 +69,8 @@ export class PlanetUi extends Phaser.Physics.Arcade.Sprite {
} }
this.status?.update(); this.status?.update();
this.harbourImage.setVisible(this.model.hasHarbour)
} }
} }

View File

@ -5,9 +5,13 @@ $color-background: #0b1120;
$color-panel: #1c243b; $color-panel: #1c243b;
$color-text-primary: #e0f0ff; $color-text-primary: #e0f0ff;
$color-text-secondary: #a0b8d8; $color-text-secondary: #a0b8d8;
$color-water: #4fc1e9; $color-water: #137092;
$color-food: #f7c06b; $color-food: #a87f41;
$color-ore: #c1543a; $color-ore: #c1543a;
$color-metal: #4d4d4d;
$color-fuel: #2e5c2d;
$color-electronics: #4d2d6b;
$color-workparts: #441631;
$color-success: #4CAF50; $color-success: #4CAF50;
$color-error: #f44336; $color-error: #f44336;
$color-button: #1c243b; $color-button: #1c243b;
@ -86,6 +90,10 @@ $font-size-base: 14px;
&.wasser .progress-fill { background-color: $color-water; } &.wasser .progress-fill { background-color: $color-water; }
&.nahrung .progress-fill { background-color: $color-food; } &.nahrung .progress-fill { background-color: $color-food; }
&.erz .progress-fill { background-color: $color-ore; } &.erz .progress-fill { background-color: $color-ore; }
&.metall .progress-fill { background-color: $color-metal; }
&.treibstoff .progress-fill { background-color: $color-fuel; }
&.elektronik .progress-fill { background-color: $color-electronics; }
&.bauteile .progress-fill { background-color: $color-workparts; }
} }
.button { .button {
@ -113,6 +121,7 @@ $font-size-base: 14px;
.flex { .flex {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 8px;
} }
.ui-body > .ui-section { .ui-body > .ui-section {