feat: resolve attacks and elimination victory

This commit is contained in:
Keith Solomon
2026-07-04 15:53:57 -05:00
parent 3eba823e52
commit 3c6359243a
2 changed files with 301 additions and 6 deletions
+51 -5
View File
@@ -12,11 +12,7 @@ final class CombatEngine
{
$this->assertMatchActive($match);
try {
$unit = $match->unit($unitId);
} catch (InvalidArgumentException $exception) {
throw new CombatException("Unknown unit: {$unitId}.", previous: $exception);
}
$unit = $this->unitOrFail($match, $unitId);
$this->assertCanAct($match, $unit);
$occupied = [];
@@ -40,6 +36,56 @@ final class CombatEngine
return $next->copy(actionLog: $actionLog);
}
public function attack(MatchState $match, string $attackerId, string $targetId): MatchState
{
$this->assertMatchActive($match);
$attacker = $this->unitOrFail($match, $attackerId);
$this->assertCanAct($match, $attacker);
if ($attacker->hasAttacked) {
throw new CombatException('Unit has already attacked this turn.');
}
$target = $this->unitOrFail($match, $targetId);
if ($target->teamId === $attacker->teamId || $target->isDefeated()) {
throw new CombatException('Target must be an active enemy unit.');
}
$distance = abs($attacker->position->x - $target->position->x)
+ abs($attacker->position->y - $target->position->y);
if ($distance !== 1) {
throw new CombatException('Target is outside attack range.');
}
$terrainDefense = $match->battlefield->terrainAt($target->position)->defenseBonus();
$damage = max(1, $attacker->attack - $target->defense - $terrainDefense);
$updatedAttacker = $attacker->markAttacked()->spendAction();
$updatedTarget = $target->takeDamage($damage);
$next = $match->withUnit($updatedAttacker)->withUnit($updatedTarget);
$actionLog = [...$match->actionLog, "{$attacker->id} attacked {$target->id} for {$damage} damage"];
$next = $next->copy(actionLog: $actionLog);
foreach ($next->units as $unit) {
if ($unit->teamId !== $attacker->teamId && !$unit->isDefeated()) {
return $next;
}
}
return $next->withWinner($attacker->teamId);
}
private function unitOrFail(MatchState $match, string $unitId): UnitState
{
try {
return $match->unit($unitId);
} catch (InvalidArgumentException $exception) {
throw new CombatException("Unknown unit: {$unitId}.", previous: $exception);
}
}
private function assertMatchActive(MatchState $match): void
{
if ($match->winnerTeamId !== null) {