$units * @param array $deploymentZones * @param array $objectives */ public function __construct( public string $id, public string $name, public Battlefield $battlefield, public array $units, public array $deploymentZones, public array $objectives, public VictoryCondition $victoryCondition, public int $holdRoundsRequired, ) { } public function toScenario(): Scenario { return new Scenario( id: $this->id, name: $this->name, battlefield: $this->battlefield, units: $this->units, deploymentZones: $this->deploymentZones, objectives: $this->objectives, victoryCondition: $this->victoryCondition, holdRoundsRequired: $this->holdRoundsRequired, ); } /** @param array $post */ public static function fromPost(array $post): self { $id = self::stringField($post, 'id'); $name = self::stringField($post, 'name'); $width = self::intField($post, 'battlefieldWidth'); $height = self::intField($post, 'battlefieldHeight'); $terrain = []; foreach (($post['battlefieldTerrain'] ?? []) as $key => $value) { $terrain[(string) $key] = self::terrainField((string) $value); } $battlefield = new Battlefield($width, $height, $terrain); $alphaUnits = self::parseTeamUnits($post['teamA']['units'] ?? [], 'alpha'); $bravoUnits = self::parseTeamUnits($post['teamB']['units'] ?? [], 'bravo'); $units = [...$alphaUnits, ...$bravoUnits]; // The form does not yet expose a "deployment zone" picker (3b adds it). // For 3a we synthesize one zone per team containing the unit positions. // When 3b lands, the editor's POST will include explicit zone tiles. $deploymentZones = [ 'alpha' => new DeploymentZone('alpha', self::collectPositions($alphaUnits)), 'bravo' => new DeploymentZone('bravo', self::collectPositions($bravoUnits)), ]; $victory = self::victoryField(self::stringField($post, 'victoryCondition')); $holdRounds = self::intField($post, 'holdRoundsRequired'); $objectives = []; if ($victory === VictoryCondition::HoldObjective) { $objId = self::stringField($post, 'objectiveId'); $objX = self::intField($post, 'objectiveX'); $objY = self::intField($post, 'objectiveY'); $objectives[$objId] = new ObjectiveMarker($objId, new Position($objX, $objY)); } return new self( id: $id, name: $name, battlefield: $battlefield, units: $units, deploymentZones: $deploymentZones, objectives: $objectives, victoryCondition: $victory, holdRoundsRequired: $holdRounds, ); } /** * @param list> $rows * @return list */ private static function parseTeamUnits(array $rows, string $teamId): array { $units = []; foreach ($rows as $index => $row) { if (!is_array($row)) { // @phpstan-ignore function.alreadyNarrowedType (Runtime guard: PHPDoc is not a runtime contract for JSON-sourced arrays in Plan 3.) throw new InvalidArgumentException("Team {$teamId} unit row {$index} is malformed."); } $unitId = self::stringField($row, 'id'); $archetype = self::archetypeField(self::stringField($row, 'archetype')); $maxHealth = self::intField($row, 'maxHealth'); $attack = self::intField($row, 'attack'); $defense = self::intField($row, 'defense'); $speed = self::intField($row, 'speed'); $abilities = []; foreach (($row['abilities'] ?? []) as $ability) { $abilities[] = (string) $ability; } $position = new Position( self::intField($row, 'x'), self::intField($row, 'y'), ); $units[] = new UnitState( id: $unitId, teamId: $teamId, position: $position, maxHealth: $maxHealth, health: $maxHealth, attack: $attack, defense: $defense, speed: $speed, actionsRemaining: 2, hasAttacked: false, archetype: $archetype, abilities: $abilities, attackBonus: 0, hasUsedAbility: false, ); } return $units; } /** * @param list $units * @return list */ private static function collectPositions(array $units): array { $positions = []; foreach ($units as $unit) { $positions[] = $unit->position; } return $positions; } private static function archetypeField(string $value): Archetype { $archetype = Archetype::tryFrom($value); if ($archetype === null) { throw new InvalidArgumentException("Unknown archetype: {$value}."); } return $archetype; } private static function terrainField(string $value): Terrain { $terrain = Terrain::tryFrom($value); if ($terrain === null) { throw new InvalidArgumentException("Unknown terrain: {$value}."); } return $terrain; } private static function victoryField(string $value): VictoryCondition { $victory = VictoryCondition::tryFrom($value); if ($victory === null) { throw new InvalidArgumentException("Unknown victory condition: {$value}."); } return $victory; } /** * @param array $array */ private static function stringField(array $array, string $key): string { if (!isset($array[$key]) || !is_string($array[$key]) || $array[$key] === '') { throw new InvalidArgumentException("Field '{$key}' is required."); } return $array[$key]; } /** * @param array $array */ private static function intField(array $array, string $key): int { if (!isset($array[$key])) { throw new InvalidArgumentException("Field '{$key}' is required."); } return (int) $array[$key]; } }