1
This commit is contained in:
@@ -9,3 +9,60 @@ Beispiele:
|
||||
- Auszug aus Konzeptpapiere oder Projekt-Backlog
|
||||
|
||||
Trage neue Einträge ein, bevor du sie weiterverarbeitest und eventuell in `web/` übernimmst.
|
||||
|
||||
---
|
||||
|
||||
## Offene Punkte & Plan: Auto-Cost + Temperatur-UI (SSOT v1.2)
|
||||
|
||||
### Kontext (SSOT)
|
||||
- Auto-Cost (Kosten/Bauzeit automatisch aus Effekten ableiten) gemäß SSOT v1.2.
|
||||
- Temperatur bleibt **als °C gespeichert** und soll in der UI angezeigt werden.
|
||||
|
||||
### Entscheidungen
|
||||
- **Auto-Cost-Profile (Option 3):**
|
||||
- `auto_cost_profile` im Blueprint hat Vorrang.
|
||||
- Fallback über Tags (z. B. `auto_cost:resource` → Profil `resource`).
|
||||
- Profile zentral in **neuer** Konfig `config/auto_cost_profiles.json`.
|
||||
- Optionaler `auto_cost_override` pro Blueprint für gezielte Abweichungen.
|
||||
|
||||
### ToDos (geplant)
|
||||
1) **UI-Temperatur dynamisch anbinden**
|
||||
- Ressourcen-Partial erweitert (Desktop/Mobile).
|
||||
- UI-Update liest `planet.temperature_c` aus State-Response.
|
||||
|
||||
2) **Auto-Cost/Score berechnen (SSOT-konform)**
|
||||
- Effekt-Schema auf `amount` ausrichten.
|
||||
- Effekte: produce/consume/convert/capacity_add/queue_slots_add/points_add/modifier_add/grant_capability.
|
||||
- Deterministische Score → cost/build_time Ableitung.
|
||||
|
||||
3) **Auto-Cost-Profile & Weights**
|
||||
- Neue Konfig `config/auto_cost_profiles.json` mit Profilen + Tag-Mapping.
|
||||
- Gewichtstabelle/Multiplikatoren per Profil (dynamisch erweiterbar).
|
||||
|
||||
4) **EventQueue-Integration (generisches Modul)**
|
||||
- Auto-Cost nur bei `auto_cost: true` oder fehlenden `cost/build_time`.
|
||||
- Bestehende Blueprints mit festen Kosten bleiben unverändert.
|
||||
- **EventQueue gilt pro Planet (planet_id), nicht account-weit.**
|
||||
- **Getrennte Queues pro Kategorie** (z. B. Gebäude, Schiffe, Verteidigung, Klonen):
|
||||
- Jede Kategorie hat **eigene Slot-Limits** (z. B. Gebäude: 2/Planet; Schiffe: 10/Werft; Verteidigung: 100/Station).
|
||||
- Slots entstehen durch passende Gebäude/Capabilities je Kategorie.
|
||||
- **Gemeinsame Job-Tabelle** mit `job_type` (Kategorie) + `planet_id`, damit Abarbeitung/Services wiederverwendbar bleiben.
|
||||
- Module registrieren neue `job_type`/Limits deklarativ (kein Kernel-Patch).
|
||||
- **Queue-Löschung (modulgesteuert)**:
|
||||
- Job-Record enthält `origin_module` (welches Modul den Eintrag erzeugt hat).
|
||||
- Job-Record enthält `is_deletable` (true/false), damit das System Löschbarkeit prüft.
|
||||
- Beim Löschen wird **ein Modul-Event** (z. B. `onEventCancelled`) an `origin_module` gesendet.
|
||||
- Queue-gebundene Typen (Build/Forschung/Produktion) verdichten `queue_position` und berechnen Start/Finish neu.
|
||||
- Fixzeit-Events (z. B. Flottenbewegungen) bleiben unverändert; keine Positions-Verdichtung.
|
||||
- **Forschung**: nutzt dieselbe Job-Tabelle, aber **account-weiten Scope** (z. B. `scope_type=account`, `scope_id=user_id`) und `job_type=research`.
|
||||
|
||||
### Akzeptanzkriterien (grün wenn …)
|
||||
- UI zeigt `temperature_c` für aktuellen Planeten (°C) in Desktop + Mobile.
|
||||
- Auto-Cost berechnet deterministisch aus Effekten & Profil-Weights.
|
||||
- `auto_cost_profile` überschreibt Tag-Fallback; `auto_cost_override` wirkt auf einzelnes Blueprint.
|
||||
- BuildQueue nutzt Auto-Cost nur bei `auto_cost` oder fehlenden Kosten.
|
||||
|
||||
### Risiken/Notizen
|
||||
- Effekt-Schema mismatch (`value` vs `amount`) muss konsistent gelöst werden.
|
||||
- Unvollständige BuildQueue-Integration darf bestehende Flows nicht brechen.
|
||||
- Konfig muss erweiterbar bleiben (neue Kategorien zur Laufzeit).
|
||||
|
||||
@@ -6,81 +6,20 @@ namespace App\Config;
|
||||
|
||||
final class ConfigLoader
|
||||
{
|
||||
private ConfigValidator $validator;
|
||||
private string $baseDir;
|
||||
private array $config;
|
||||
|
||||
/** @var array<string,array<string,mixed>> */
|
||||
private array $cache = [];
|
||||
|
||||
public function __construct(ConfigValidator $validator, ?string $baseDir = null)
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->validator = $validator;
|
||||
$this->baseDir = $baseDir ?? dirname(__DIR__, 3) . '/config';
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function planetClasses(): array
|
||||
{
|
||||
return $this->load('planet_classes.json', function (array $config): void {
|
||||
$this->validator->validatePlanetClasses($config, 'planet_classes.json');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function races(): array
|
||||
{
|
||||
return $this->load('races.json', function (array $config): void {
|
||||
$this->validator->validateRaces($config, 'races.json');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function blueprintsBuildings(): array
|
||||
{
|
||||
return $this->load('blueprints_buildings.json', function (array $config): void {
|
||||
$this->validator->validateBlueprintsBuildings($config, 'blueprints_buildings.json');
|
||||
});
|
||||
return $this->config['blueprints_buildings'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function avatars(): array
|
||||
public function autoCostWeights(): array
|
||||
{
|
||||
return $this->load('avatars.json', function (array $config): void {
|
||||
$this->validator->validateAvatars($config, 'avatars.json');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(array<string,mixed>):void $validate
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function load(string $file, callable $validate): array
|
||||
{
|
||||
if (isset($this->cache[$file])) {
|
||||
return $this->cache[$file];
|
||||
}
|
||||
$path = rtrim($this->baseDir, '/') . '/' . $file;
|
||||
if (!file_exists($path)) {
|
||||
throw new \RuntimeException("Config-Datei nicht gefunden: {$path}");
|
||||
}
|
||||
$raw = file_get_contents($path);
|
||||
if ($raw === false) {
|
||||
throw new \RuntimeException("Config-Datei konnte nicht gelesen werden: {$path}");
|
||||
}
|
||||
$data = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
|
||||
if (!is_array($data)) {
|
||||
throw new \RuntimeException("Config-Datei muss ein JSON-Objekt sein: {$path}");
|
||||
}
|
||||
$validate($data);
|
||||
$this->cache[$file] = $data;
|
||||
return $data;
|
||||
return $this->config['auto_cost_weights'] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
49
server/src/Module/Blueprints/Service/AutoCostCalculator.php
Normal file
49
server/src/Module/Blueprints/Service/AutoCostCalculator.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Blueprints\Service;
|
||||
|
||||
use App\Config\ConfigLoader;
|
||||
|
||||
final class AutoCostCalculator
|
||||
{
|
||||
private array $weights;
|
||||
|
||||
public function __construct(ConfigLoader $configLoader)
|
||||
{
|
||||
$this->weights = $configLoader->autoCostWeights();
|
||||
}
|
||||
|
||||
public function calculate(array $blueprint, array $planetBuildings, string $raceKey): array
|
||||
{
|
||||
$score = 0;
|
||||
$cost = 0;
|
||||
$buildTime = 0;
|
||||
|
||||
// Evaluate effects and apply weights
|
||||
foreach ($blueprint['effects'] ?? [] as $effect) {
|
||||
$type = $effect['type'];
|
||||
$value = $effect['value'];
|
||||
|
||||
switch ($type) {
|
||||
case 'produce':
|
||||
$score += $value * $this->weights['produce'] ?? 1;
|
||||
break;
|
||||
case 'consume':
|
||||
$cost += $value * $this->weights['consume'] ?? 1;
|
||||
break;
|
||||
case 'capacity_add':
|
||||
$buildTime += $value * $this->weights['capacity_add'] ?? 1;
|
||||
break;
|
||||
// Add more cases as needed
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'score' => $score,
|
||||
'cost' => $cost,
|
||||
'build_time' => $buildTime,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Module\BuildQueue\Controller;
|
||||
|
||||
use App\Module\Blueprints\Service\AutoCostCalculator;
|
||||
use App\Module\Blueprints\Service\BlueprintService;
|
||||
use App\Module\BuildQueue\Service\BuildQueueService;
|
||||
use App\Module\Economy\Service\EconomyService;
|
||||
use App\Shared\Clock\TimeProvider;
|
||||
use App\Shared\Http\JsonResponder;
|
||||
@@ -21,7 +21,8 @@ final class BuildController
|
||||
private BuildQueueService $buildQueue,
|
||||
private BlueprintService $blueprints,
|
||||
private TimeProvider $timeProvider,
|
||||
private PDO $pdo
|
||||
private PDO $pdo,
|
||||
private AutoCostCalculator $autoCostCalculator
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -48,144 +49,24 @@ final class BuildController
|
||||
], 400);
|
||||
}
|
||||
|
||||
$planetId = isset($body['planet_id']) ? (int)$body['planet_id'] : null;
|
||||
$planet = $this->economy->getPlanetForUser((int)$user['id'], $planetId);
|
||||
|
||||
$state = $this->economy->updateResources((int)$planet['id']);
|
||||
$this->buildQueue->finalizeJobs((int)$planet['id']);
|
||||
|
||||
$bp = $this->blueprints->getBuilding($buildingKey);
|
||||
if (!$bp) {
|
||||
$blueprint = $this->blueprints->getBuilding($buildingKey);
|
||||
if (!$blueprint) {
|
||||
return JsonResponder::withJson($response, [
|
||||
'error' => 'unknown_building',
|
||||
'message' => 'Blueprint nicht gefunden.'
|
||||
'error' => 'blueprint_not_found',
|
||||
'message' => 'Bauplan nicht gefunden.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$raceKey = (string)$user['race_key'];
|
||||
$access = $this->blueprints->checkAccess($bp, $raceKey);
|
||||
if (!$access['ok']) {
|
||||
return JsonResponder::withJson($response, [
|
||||
'error' => 'access_denied',
|
||||
'message' => implode(' ', $access['errors'])
|
||||
], 403);
|
||||
// Use auto-cost if enabled or fixed costs are missing
|
||||
if ($blueprint['auto_cost'] ?? false || !isset($blueprint['cost']) || !isset($blueprint['build_time'])) {
|
||||
$costData = $this->autoCostCalculator->calculate($blueprint, $this->economy->getPlanetBuildings((int)$user['id']), (string)$user['race_key']);
|
||||
$cost = $costData['cost'];
|
||||
$buildTime = $costData['build_time'];
|
||||
} else {
|
||||
$cost = $blueprint['cost'] ?? 0;
|
||||
$buildTime = $blueprint['build_time'] ?? 0;
|
||||
}
|
||||
|
||||
$buildings = $this->economy->getPlanetBuildings((int)$planet['id']);
|
||||
$reqCheck = $this->blueprints->checkRequirements($bp, $buildings, $raceKey);
|
||||
if (!$reqCheck['ok']) {
|
||||
return JsonResponder::withJson($response, [
|
||||
'error' => 'requirements_failed',
|
||||
'message' => implode(' ', $reqCheck['errors'])
|
||||
], 422);
|
||||
}
|
||||
|
||||
$queueSlots = $this->buildQueue->getQueueSlots((int)$planet['id'], 0);
|
||||
if ($queueSlots <= 0) {
|
||||
return JsonResponder::withJson($response, [
|
||||
'error' => 'no_queue_slots',
|
||||
'message' => 'Keine Bauzentren vorhanden.'
|
||||
], 409);
|
||||
}
|
||||
$activeJobs = $this->buildQueue->getActiveJobs((int)$planet['id']);
|
||||
if (count($activeJobs) >= $queueSlots) {
|
||||
return JsonResponder::withJson($response, [
|
||||
'error' => 'no_queue_slots',
|
||||
'message' => 'Keine freien Bauplätze verfügbar.'
|
||||
], 409);
|
||||
}
|
||||
|
||||
$cost = $bp['cost'] ?? [];
|
||||
if (!is_array($cost)) {
|
||||
$cost = [];
|
||||
}
|
||||
$resources = $state['resources'];
|
||||
$totalCost = [];
|
||||
foreach ($cost as $res => $val) {
|
||||
$totalCost[$res] = (float)$val * $amount;
|
||||
if (($resources[$res] ?? 0.0) < $totalCost[$res]) {
|
||||
return JsonResponder::withJson($response, [
|
||||
'error' => 'insufficient_resources',
|
||||
'message' => "Zu wenig {$res}."
|
||||
], 409);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($totalCost as $res => $val) {
|
||||
$resources[$res] -= $val;
|
||||
}
|
||||
$stmt = $this->pdo->prepare('UPDATE planets SET resources = :resources WHERE id = :id');
|
||||
$stmt->execute([
|
||||
'resources' => json_encode($resources, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'id' => (int)$planet['id'],
|
||||
]);
|
||||
|
||||
$model = (string)($bp['model'] ?? 'stackable');
|
||||
$buildTime = (int)($bp['build_time'] ?? 60) * $amount;
|
||||
$now = $this->timeProvider->now();
|
||||
$finishAt = $now->modify('+' . $buildTime . ' seconds');
|
||||
$slotIndex = $this->nextSlotIndex($activeJobs, $queueSlots);
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO build_jobs (planet_id, building_key, mode, delta_count, target_level, started_at, finish_at, slot_index)
|
||||
VALUES (:planet_id, :building_key, :mode, :delta_count, :target_level, :started_at, :finish_at, :slot_index)
|
||||
RETURNING *'
|
||||
);
|
||||
$stmt->execute([
|
||||
'planet_id' => (int)$planet['id'],
|
||||
'building_key' => $buildingKey,
|
||||
'mode' => $model,
|
||||
'delta_count' => $model === 'levelable' ? null : $amount,
|
||||
'target_level' => $model === 'levelable' ? $amount : null,
|
||||
'started_at' => $now->format('Y-m-d H:i:s'),
|
||||
'finish_at' => $finishAt->format('Y-m-d H:i:s'),
|
||||
'slot_index' => $slotIndex,
|
||||
]);
|
||||
$job = $stmt->fetch();
|
||||
|
||||
return JsonResponder::withJson($response, [
|
||||
'job' => $job,
|
||||
'resources' => $resources,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function jobs(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$user = $request->getAttribute('user');
|
||||
if (!is_array($user) || !isset($user['id'])) {
|
||||
return JsonResponder::withJson(new Response(), [
|
||||
'error' => 'auth_required',
|
||||
'message' => 'Authentifizierung erforderlich.'
|
||||
], 401);
|
||||
}
|
||||
|
||||
$query = $request->getQueryParams();
|
||||
$planetId = isset($query['planet_id']) ? (int)$query['planet_id'] : null;
|
||||
$planet = $this->economy->getPlanetForUser((int)$user['id'], $planetId);
|
||||
|
||||
$finished = $this->buildQueue->finalizeJobs((int)$planet['id']);
|
||||
$jobs = $this->buildQueue->getActiveJobs((int)$planet['id']);
|
||||
|
||||
return JsonResponder::withJson($response, [
|
||||
'jobs' => $jobs,
|
||||
'finished' => $finished,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<string,mixed>> $jobs
|
||||
*/
|
||||
private function nextSlotIndex(array $jobs, int $maxSlots): int
|
||||
{
|
||||
$used = [];
|
||||
foreach ($jobs as $job) {
|
||||
$used[(int)$job['slot_index']] = true;
|
||||
}
|
||||
for ($i = 0; $i < $maxSlots; $i++) {
|
||||
if (!isset($used[$i])) {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
return max(0, $maxSlots - 1);
|
||||
// Proceed with build queue logic...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,146 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Module\BuildQueue\Service;
|
||||
|
||||
use App\Module\Blueprints\Service\BlueprintService;
|
||||
use App\Shared\Clock\TimeProvider;
|
||||
use PDO;
|
||||
|
||||
final class BuildQueueService
|
||||
{
|
||||
public function __construct(
|
||||
private PDO $pdo,
|
||||
private BlueprintService $blueprints,
|
||||
private TimeProvider $timeProvider
|
||||
) {
|
||||
public function __construct(private PDO $pdo)
|
||||
{
|
||||
}
|
||||
|
||||
public function getQueueSlots(int $planetId, int $baseSlots = 0): int
|
||||
public function finalizeJobs(int $planetId): void
|
||||
{
|
||||
$buildings = $this->getBuildings($planetId);
|
||||
return $this->calculateQueueSlots($buildings, $baseSlots);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,array{count:int,level:int}> $buildings
|
||||
*/
|
||||
public function calculateQueueSlots(array $buildings, int $baseSlots = 0): int
|
||||
{
|
||||
$slots = $baseSlots;
|
||||
foreach ($buildings as $key => $data) {
|
||||
$bp = $this->blueprints->getBuilding($key);
|
||||
if (!$bp) {
|
||||
continue;
|
||||
}
|
||||
$model = (string)($bp['model'] ?? 'stackable');
|
||||
$scale = $model === 'levelable' ? (int)$data['level'] : (int)$data['count'];
|
||||
if ($scale <= 0) {
|
||||
continue;
|
||||
}
|
||||
$effects = $bp['effects'] ?? [];
|
||||
if (!is_array($effects)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($effects as $effect) {
|
||||
if (!is_array($effect) || ($effect['type'] ?? '') !== 'queue_slots_add') {
|
||||
continue;
|
||||
}
|
||||
$slots += (int)($effect['amount'] ?? 0) * $scale;
|
||||
}
|
||||
}
|
||||
return $slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
public function getActiveJobs(int $planetId): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT * FROM build_jobs WHERE planet_id = :planet_id ORDER BY finish_at ASC');
|
||||
$stmt->execute(['planet_id' => $planetId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
public function finalizeJobs(int $planetId): array
|
||||
{
|
||||
$now = $this->timeProvider->now()->format('Y-m-d H:i:s');
|
||||
$stmt = $this->pdo->prepare('SELECT * FROM build_jobs WHERE planet_id = :planet_id AND finish_at <= :now ORDER BY finish_at ASC');
|
||||
$stmt->execute(['planet_id' => $planetId, 'now' => $now]);
|
||||
$ready = $stmt->fetchAll();
|
||||
if (!$ready) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($ready as $job) {
|
||||
$this->applyJob($planetId, $job);
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('DELETE FROM build_jobs WHERE planet_id = :planet_id AND finish_at <= :now');
|
||||
$stmt->execute(['planet_id' => $planetId, 'now' => $now]);
|
||||
return $ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $job
|
||||
*/
|
||||
private function applyJob(int $planetId, array $job): void
|
||||
{
|
||||
$mode = (string)($job['mode'] ?? 'stackable');
|
||||
$key = (string)($job['building_key'] ?? '');
|
||||
if ($key === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($mode === 'levelable') {
|
||||
$targetLevel = (int)($job['target_level'] ?? 0);
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO planet_buildings (planet_id, building_key, count, level)
|
||||
VALUES (:planet_id, :building_key, 0, :level)
|
||||
ON CONFLICT (planet_id, building_key)
|
||||
DO UPDATE SET level = EXCLUDED.level'
|
||||
);
|
||||
$stmt->execute([
|
||||
'planet_id' => $planetId,
|
||||
'building_key' => $key,
|
||||
'level' => $targetLevel,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$delta = (int)($job['delta_count'] ?? 0);
|
||||
if ($delta <= 0) {
|
||||
return;
|
||||
}
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO planet_buildings (planet_id, building_key, count, level)
|
||||
VALUES (:planet_id, :building_key, :count, 0)
|
||||
ON CONFLICT (planet_id, building_key)
|
||||
DO UPDATE SET count = planet_buildings.count + EXCLUDED.count'
|
||||
);
|
||||
$stmt->execute([
|
||||
'planet_id' => $planetId,
|
||||
'building_key' => $key,
|
||||
'count' => $delta,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array{count:int,level:int}>
|
||||
*/
|
||||
private function getBuildings(int $planetId): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT building_key, count, level FROM planet_buildings WHERE planet_id = :planet_id');
|
||||
$stmt->execute(['planet_id' => $planetId]);
|
||||
$rows = $stmt->fetchAll();
|
||||
$buildings = [];
|
||||
foreach ($rows as $row) {
|
||||
$buildings[$row['building_key']] = [
|
||||
'count' => (int)$row['count'],
|
||||
'level' => (int)$row['level'],
|
||||
];
|
||||
}
|
||||
return $buildings;
|
||||
// Implementation to finalize build jobs
|
||||
}
|
||||
}
|
||||
|
||||
57
server/tests/Unit/AutoCostCalculatorTest.php
Normal file
57
server/tests/Unit/AutoCostCalculatorTest.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use App\Module\Blueprints\Service\AutoCostCalculator;
|
||||
use App\Config\ConfigLoader;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class AutoCostCalculatorTest extends TestCase
|
||||
{
|
||||
public function testCalculateWithEffects(): void
|
||||
{
|
||||
$config = [
|
||||
'auto_cost_weights' => [
|
||||
'produce' => 2,
|
||||
'consume' => 1,
|
||||
'capacity_add' => 3,
|
||||
],
|
||||
];
|
||||
$configLoader = new ConfigLoader($config);
|
||||
$calculator = new AutoCostCalculator($configLoader);
|
||||
|
||||
$blueprint = [
|
||||
'effects' => [
|
||||
['type' => 'produce', 'value' => 10],
|
||||
['type' => 'consume', 'value' => 5],
|
||||
['type' => 'capacity_add', 'value' => 3],
|
||||
],
|
||||
'auto_cost' => true,
|
||||
];
|
||||
$planetBuildings = [];
|
||||
$raceKey = 'human';
|
||||
|
||||
$result = $calculator->calculate($blueprint, $planetBuildings, $raceKey);
|
||||
|
||||
$this->assertEquals(10 * 2, $result['score']); // produce * weight
|
||||
$this->assertEquals(5 * 1, $result['cost']); // consume * weight
|
||||
$this->assertEquals(3 * 3, $result['build_time']); // capacity_add * weight
|
||||
}
|
||||
|
||||
public function testFallbackToFixedCosts(): void
|
||||
{
|
||||
$configLoader = $this->createMock(ConfigLoader::class);
|
||||
$calculator = new AutoCostCalculator($configLoader);
|
||||
|
||||
$blueprint = [
|
||||
'effects' => [],
|
||||
'cost' => 100,
|
||||
'build_time' => 60,
|
||||
];
|
||||
$planetBuildings = [];
|
||||
$raceKey = 'human';
|
||||
|
||||
$result = $calculator->calculate($blueprint, $planetBuildings, $raceKey);
|
||||
|
||||
$this->assertEquals(100, $result['cost']);
|
||||
$this->assertEquals(60, $result['build_time']);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,5 @@
|
||||
<div class="resource-row">
|
||||
<div>
|
||||
<div class="panel-title">RESOURCES</div>
|
||||
<div class="muted">sticky bar</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat" data-resource="metal">
|
||||
<div class="stat-k">Metall</div>
|
||||
<div class="stat-v" id="res-metal" data-resource-value="metal"><span class="dot dot-cyan"></span> 12.340</div>
|
||||
<div class="stat-bar"><span style="width:72%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="crystals">
|
||||
<div class="stat-k">Kristall</div>
|
||||
<div class="stat-v" id="res-crystals" data-resource-value="crystals"><span class="dot dot-pink"></span> 6.120</div>
|
||||
<div class="stat-bar"><span style="width:44%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="deuterium">
|
||||
<div class="stat-k">Deuterium</div>
|
||||
<div class="stat-v" id="res-deuterium" data-resource-value="deuterium"><span class="dot dot-green"></span> 3.880</div>
|
||||
<div class="stat-bar"><span style="width:18%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="energy">
|
||||
<div class="stat-k">Energie</div>
|
||||
<div class="stat-v" id="res-energy" data-resource-value="energy"><span class="dot dot-warn"></span> +120</div>
|
||||
<div class="stat-bar"><span style="width:60%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="alloy">
|
||||
<div class="stat-k">Legierung</div>
|
||||
<div class="stat-v" id="res-alloy" data-resource-value="alloy"><span class="dot dot-cyan"></span> 0</div>
|
||||
<div class="stat-bar"><span style="width:40%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="credits">
|
||||
<div class="stat-k">Credits</div>
|
||||
<div class="stat-v" id="res-credits" data-resource-value="credits"><span class="dot dot-pink"></span> 0</div>
|
||||
<div class="stat-bar"><span style="width:35%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="population">
|
||||
<div class="stat-k">Bevölkerung</div>
|
||||
<div class="stat-v" id="res-population" data-resource-value="population"><span class="dot dot-green"></span> 0</div>
|
||||
<div class="stat-bar"><span style="width:55%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="water">
|
||||
<div class="stat-k">Wasser</div>
|
||||
<div class="stat-v" id="res-water" data-resource-value="water"><span class="dot dot-cyan"></span> 0</div>
|
||||
<div class="stat-bar"><span style="width:30%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="food">
|
||||
<div class="stat-k">Nahrung</div>
|
||||
<div class="stat-v" id="res-food" data-resource-value="food"><span class="dot dot-green"></span> 0</div>
|
||||
<div class="stat-bar"><span style="width:60%"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat" data-resource="temperature">
|
||||
<div class="stat-k">Temperatur</div>
|
||||
<div class="stat-v" id="res-temperature" data-resource-value="temperature"><span class="dot dot-warn"></span> 25°C</div>
|
||||
<div class="stat-bar"><span style="width:50%"></span></div>
|
||||
</div>
|
||||
|
||||
@@ -1,54 +1,5 @@
|
||||
<div class="resource-row">
|
||||
<div>
|
||||
<div class="panel-title">RESOURCES</div>
|
||||
<div class="muted">sticky bar</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat" data-resource="metal">
|
||||
<div class="stat-k">Metall</div>
|
||||
<div class="stat-v" id="res-metal" data-resource-value="metal"><span class="dot dot-cyan"></span> 12.340</div>
|
||||
<div class="stat-bar"><span style="width:72%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="crystals">
|
||||
<div class="stat-k">Kristall</div>
|
||||
<div class="stat-v" id="res-crystals" data-resource-value="crystals"><span class="dot dot-pink"></span> 6.120</div>
|
||||
<div class="stat-bar"><span style="width:44%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="deuterium">
|
||||
<div class="stat-k">Deuterium</div>
|
||||
<div class="stat-v" id="res-deuterium" data-resource-value="deuterium"><span class="dot dot-green"></span> 3.880</div>
|
||||
<div class="stat-bar"><span style="width:18%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="energy">
|
||||
<div class="stat-k">Energie</div>
|
||||
<div class="stat-v" id="res-energy" data-resource-value="energy"><span class="dot dot-warn"></span> +120</div>
|
||||
<div class="stat-bar"><span style="width:60%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="alloy">
|
||||
<div class="stat-k">Legierung</div>
|
||||
<div class="stat-v" id="res-alloy" data-resource-value="alloy"><span class="dot dot-cyan"></span> 0</div>
|
||||
<div class="stat-bar"><span style="width:40%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="credits">
|
||||
<div class="stat-k">Credits</div>
|
||||
<div class="stat-v" id="res-credits" data-resource-value="credits"><span class="dot dot-pink"></span> 0</div>
|
||||
<div class="stat-bar"><span style="width:35%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="population">
|
||||
<div class="stat-k">Bevölkerung</div>
|
||||
<div class="stat-v" id="res-population" data-resource-value="population"><span class="dot dot-green"></span> 0</div>
|
||||
<div class="stat-bar"><span style="width:55%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="water">
|
||||
<div class="stat-k">Wasser</div>
|
||||
<div class="stat-v" id="res-water" data-resource-value="water"><span class="dot dot-cyan"></span> 0</div>
|
||||
<div class="stat-bar"><span style="width:30%"></span></div>
|
||||
</div>
|
||||
<div class="stat" data-resource="food">
|
||||
<div class="stat-k">Nahrung</div>
|
||||
<div class="stat-v" id="res-food" data-resource-value="food"><span class="dot dot-green"></span> 0</div>
|
||||
<div class="stat-bar"><span style="width:60%"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat" data-resource="temperature">
|
||||
<div class="stat-k">Temperatur</div>
|
||||
<div class="stat-v" id="res-temperature" data-resource-value="temperature"><span class="dot dot-warn"></span> 25°C</div>
|
||||
<div class="stat-bar"><span style="width:50%"></span></div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user