288 lines
9.9 KiB
PHP
288 lines
9.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace BattleForge\Application;
|
|
|
|
use BattleForge\Domain\Archetype;
|
|
use BattleForge\Domain\ArchetypeCatalog;
|
|
use BattleForge\Domain\Battlefield;
|
|
use BattleForge\Domain\DeploymentZone;
|
|
use BattleForge\Domain\MatchState;
|
|
use BattleForge\Domain\ObjectiveMarker;
|
|
use BattleForge\Domain\Position;
|
|
use BattleForge\Domain\Scenario;
|
|
use BattleForge\Domain\Terrain;
|
|
use BattleForge\Domain\UnitState;
|
|
use BattleForge\Domain\VictoryCondition;
|
|
use InvalidArgumentException;
|
|
|
|
final class ScenarioSerializer
|
|
{
|
|
/** @return array<string, mixed> */
|
|
public static function scenarioToArray(Scenario $scenario): array
|
|
{
|
|
$terrainMap = [];
|
|
|
|
for ($y = 0; $y < $scenario->battlefield->height; $y++) {
|
|
for ($x = 0; $x < $scenario->battlefield->width; $x++) {
|
|
$position = new Position($x, $y);
|
|
$tile = $scenario->battlefield->terrainAt($position);
|
|
if ($tile === Terrain::Open) {
|
|
continue;
|
|
}
|
|
$terrainMap[$position->key()] = $tile->value;
|
|
}
|
|
}
|
|
|
|
$units = [];
|
|
foreach ($scenario->units as $unit) {
|
|
$units[] = self::unitToArray($unit);
|
|
}
|
|
|
|
$deploymentZones = [];
|
|
foreach ($scenario->deploymentZones as $teamId => $zone) {
|
|
$deploymentZones[$teamId] = self::deploymentZoneToArray($zone);
|
|
}
|
|
|
|
$objectives = [];
|
|
foreach ($scenario->objectives as $id => $objective) {
|
|
$objectives[$id] = ['id' => $objective->id, 'position' => self::positionToArray($objective->position)];
|
|
}
|
|
|
|
return [
|
|
'id' => $scenario->id,
|
|
'name' => $scenario->name,
|
|
'battlefield' => [
|
|
'width' => $scenario->battlefield->width,
|
|
'height' => $scenario->battlefield->height,
|
|
'terrain' => $terrainMap,
|
|
],
|
|
'units' => $units,
|
|
'deploymentZones' => $deploymentZones,
|
|
'objectives' => $objectives,
|
|
'victoryCondition' => $scenario->victoryCondition->value,
|
|
'holdRoundsRequired' => $scenario->holdRoundsRequired,
|
|
];
|
|
}
|
|
|
|
/** @param array<string, mixed> $data */
|
|
public static function scenarioFromArray(array $data): Scenario
|
|
{
|
|
$terrainMap = [];
|
|
foreach (($data['battlefield']['terrain'] ?? []) as $key => $value) {
|
|
$terrainMap[(string) $key] = Terrain::from((string) $value);
|
|
}
|
|
|
|
$battlefield = new Battlefield(
|
|
(int) $data['battlefield']['width'],
|
|
(int) $data['battlefield']['height'],
|
|
$terrainMap,
|
|
);
|
|
|
|
$units = [];
|
|
foreach (($data['units'] ?? []) as $row) {
|
|
$units[] = self::unitFromArray($row);
|
|
}
|
|
|
|
$deploymentZones = [];
|
|
foreach (($data['deploymentZones'] ?? []) as $teamId => $row) {
|
|
$deploymentZones[(string) $teamId] = self::deploymentZoneFromArray($row);
|
|
}
|
|
|
|
$objectives = [];
|
|
foreach (($data['objectives'] ?? []) as $id => $row) {
|
|
$objectives[(string) $id] = new ObjectiveMarker(
|
|
(string) $row['id'],
|
|
self::positionFromArray($row['position']),
|
|
);
|
|
}
|
|
|
|
return new Scenario(
|
|
id: (string) $data['id'],
|
|
name: (string) $data['name'],
|
|
battlefield: $battlefield,
|
|
units: $units,
|
|
deploymentZones: $deploymentZones,
|
|
objectives: $objectives,
|
|
victoryCondition: VictoryCondition::from((string) $data['victoryCondition']),
|
|
holdRoundsRequired: (int) $data['holdRoundsRequired'],
|
|
);
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
public static function matchToArray(MatchState $match): array
|
|
{
|
|
$terrainMap = [];
|
|
for ($y = 0; $y < $match->battlefield->height; $y++) {
|
|
for ($x = 0; $x < $match->battlefield->width; $x++) {
|
|
$position = new Position($x, $y);
|
|
$tile = $match->battlefield->terrainAt($position);
|
|
if ($tile === Terrain::Open) {
|
|
continue;
|
|
}
|
|
$terrainMap[$position->key()] = $tile->value;
|
|
}
|
|
}
|
|
|
|
$units = [];
|
|
foreach ($match->units as $unit) {
|
|
$units[] = self::unitToArray($unit);
|
|
}
|
|
|
|
$objectives = [];
|
|
foreach ($match->objectives as $id => $objective) {
|
|
$objectives[(string) $id] = [
|
|
'id' => $objective->id,
|
|
'position' => self::positionToArray($objective->position),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'battlefield' => [
|
|
'width' => $match->battlefield->width,
|
|
'height' => $match->battlefield->height,
|
|
'terrain' => $terrainMap,
|
|
],
|
|
'units' => $units,
|
|
'activeTeamId' => $match->activeTeamId,
|
|
'round' => $match->round,
|
|
'winnerTeamId' => $match->winnerTeamId,
|
|
'actionLog' => $match->actionLog,
|
|
'objectives' => $objectives,
|
|
'victoryCondition' => $match->victoryCondition->value,
|
|
'holdRoundsRequired' => $match->holdRoundsRequired,
|
|
'objectiveControl' => $match->objectiveControl,
|
|
];
|
|
}
|
|
|
|
/** @param array<string, mixed> $data */
|
|
public static function matchFromArray(array $data): MatchState
|
|
{
|
|
$terrainMap = [];
|
|
foreach (($data['battlefield']['terrain'] ?? []) as $key => $value) {
|
|
$terrainMap[(string) $key] = Terrain::from((string) $value);
|
|
}
|
|
|
|
$battlefield = new Battlefield(
|
|
(int) $data['battlefield']['width'],
|
|
(int) $data['battlefield']['height'],
|
|
$terrainMap,
|
|
);
|
|
|
|
$units = [];
|
|
foreach (($data['units'] ?? []) as $row) {
|
|
$units[] = self::unitFromArray($row);
|
|
}
|
|
|
|
$objectives = [];
|
|
foreach (($data['objectives'] ?? []) as $id => $row) {
|
|
$objectives[(string) $id] = new ObjectiveMarker(
|
|
(string) $row['id'],
|
|
self::positionFromArray($row['position']),
|
|
);
|
|
}
|
|
|
|
return new MatchState(
|
|
battlefield: $battlefield,
|
|
units: $units,
|
|
activeTeamId: (string) $data['activeTeamId'],
|
|
round: (int) $data['round'],
|
|
winnerTeamId: $data['winnerTeamId'] ?? null,
|
|
actionLog: array_map(static fn (mixed $entry): string => (string) $entry, $data['actionLog'] ?? []),
|
|
objectives: $objectives,
|
|
victoryCondition: VictoryCondition::from((string) $data['victoryCondition']),
|
|
holdRoundsRequired: (int) $data['holdRoundsRequired'],
|
|
objectiveControl: $data['objectiveControl'] ?? [],
|
|
);
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
private static function unitToArray(UnitState $unit): array
|
|
{
|
|
return [
|
|
'id' => $unit->id,
|
|
'teamId' => $unit->teamId,
|
|
'position' => self::positionToArray($unit->position),
|
|
'maxHealth' => $unit->maxHealth,
|
|
'health' => $unit->health,
|
|
'attack' => $unit->attack,
|
|
'defense' => $unit->defense,
|
|
'speed' => $unit->speed,
|
|
'archetype' => $unit->archetype->value,
|
|
'abilities' => $unit->abilities,
|
|
];
|
|
}
|
|
|
|
/** @param array<string, mixed> $row */
|
|
private static function unitFromArray(array $row): UnitState
|
|
{
|
|
$archetype = Archetype::from((string) $row['archetype']);
|
|
// Validate the ability allowlist against the catalog before constructing,
|
|
// because the runtime guard in UnitState only catches non-string entries.
|
|
$template = ArchetypeCatalog::templates()[$archetype->value] ?? null;
|
|
if ($template !== null) {
|
|
$abilities = array_map(static fn (mixed $a): string => (string) $a, $row['abilities'] ?? []);
|
|
foreach ($abilities as $ability) {
|
|
if (!in_array($ability, $template->allowedAbilities, true)) {
|
|
throw new InvalidArgumentException("Ability {$ability} is not in archetype {$archetype->value}'s allowlist.");
|
|
}
|
|
}
|
|
}
|
|
|
|
return new UnitState(
|
|
id: (string) $row['id'],
|
|
teamId: (string) $row['teamId'],
|
|
position: self::positionFromArray($row['position']),
|
|
maxHealth: (int) $row['maxHealth'],
|
|
health: (int) $row['health'],
|
|
attack: (int) $row['attack'],
|
|
defense: (int) $row['defense'],
|
|
speed: (int) $row['speed'],
|
|
actionsRemaining: 2,
|
|
hasAttacked: false,
|
|
archetype: $archetype,
|
|
abilities: $abilities ?? [],
|
|
attackBonus: 0,
|
|
hasUsedAbility: false,
|
|
);
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
private static function positionToArray(Position $position): array
|
|
{
|
|
return ['x' => $position->x, 'y' => $position->y];
|
|
}
|
|
|
|
private static function positionFromArray(mixed $row): Position
|
|
{
|
|
if (!is_array($row)) {
|
|
throw new InvalidArgumentException('Expected position to be an array.');
|
|
}
|
|
|
|
return new Position((int) $row['x'], (int) $row['y']);
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
private static function deploymentZoneToArray(DeploymentZone $zone): array
|
|
{
|
|
$positions = [];
|
|
foreach ($zone->positions as $position) {
|
|
$positions[] = self::positionToArray($position);
|
|
}
|
|
|
|
return ['teamId' => $zone->teamId, 'positions' => $positions];
|
|
}
|
|
|
|
/** @param array<string, mixed> $row */
|
|
private static function deploymentZoneFromArray(array $row): DeploymentZone
|
|
{
|
|
$positions = [];
|
|
foreach (($row['positions'] ?? []) as $positionRow) {
|
|
$positions[] = self::positionFromArray($positionRow);
|
|
}
|
|
|
|
return new DeploymentZone((string) $row['teamId'], $positions);
|
|
}
|
|
}
|