This commit is contained in:
2026-02-08 00:21:58 +01:00
parent dc427490a5
commit 0301716890
8 changed files with 197 additions and 444 deletions

View File

@@ -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'] ?? [];
}
}

View 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,
];
}
}

View File

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

View File

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