feat: catalog curated unit archetypes and templates
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BattleForge\Domain;
|
||||
|
||||
enum Archetype: string
|
||||
{
|
||||
case Defender = 'defender';
|
||||
case Striker = 'striker';
|
||||
case Support = 'support';
|
||||
case Scout = 'scout';
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BattleForge\Domain;
|
||||
|
||||
final class ArchetypeCatalog
|
||||
{
|
||||
/**
|
||||
* @return array<string, ArchetypeTemplate>
|
||||
*/
|
||||
public static function templates(): array
|
||||
{
|
||||
static $cache = null;
|
||||
|
||||
if ($cache !== null) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
$cache = [
|
||||
Archetype::Defender->value => new ArchetypeTemplate(
|
||||
minHealth: 10,
|
||||
maxHealth: 14,
|
||||
minAttack: 2,
|
||||
maxAttack: 4,
|
||||
minDefense: 3,
|
||||
maxDefense: 5,
|
||||
minSpeed: 1,
|
||||
maxSpeed: 3,
|
||||
allowedAbilities: ['buff'],
|
||||
),
|
||||
Archetype::Striker->value => new ArchetypeTemplate(
|
||||
minHealth: 6,
|
||||
maxHealth: 10,
|
||||
minAttack: 4,
|
||||
maxAttack: 6,
|
||||
minDefense: 1,
|
||||
maxDefense: 2,
|
||||
minSpeed: 2,
|
||||
maxSpeed: 4,
|
||||
allowedAbilities: ['area_damage'],
|
||||
),
|
||||
Archetype::Support->value => new ArchetypeTemplate(
|
||||
minHealth: 5,
|
||||
maxHealth: 9,
|
||||
minAttack: 1,
|
||||
maxAttack: 3,
|
||||
minDefense: 1,
|
||||
maxDefense: 3,
|
||||
minSpeed: 2,
|
||||
maxSpeed: 4,
|
||||
allowedAbilities: ['heal', 'buff'],
|
||||
),
|
||||
Archetype::Scout->value => new ArchetypeTemplate(
|
||||
minHealth: 4,
|
||||
maxHealth: 7,
|
||||
minAttack: 2,
|
||||
maxAttack: 4,
|
||||
minDefense: 0,
|
||||
maxDefense: 2,
|
||||
minSpeed: 4,
|
||||
maxSpeed: 6,
|
||||
allowedAbilities: [],
|
||||
),
|
||||
];
|
||||
|
||||
return $cache;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BattleForge\Domain;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final readonly class ArchetypeTemplate
|
||||
{
|
||||
/** @param list<string> $allowedAbilities */
|
||||
public function __construct(
|
||||
public int $minHealth,
|
||||
public int $maxHealth,
|
||||
public int $minAttack,
|
||||
public int $maxAttack,
|
||||
public int $minDefense,
|
||||
public int $maxDefense,
|
||||
public int $minSpeed,
|
||||
public int $maxSpeed,
|
||||
public array $allowedAbilities,
|
||||
) {
|
||||
if ($this->minHealth > $this->maxHealth) {
|
||||
throw new InvalidArgumentException('Health range is inverted.');
|
||||
}
|
||||
if ($this->minAttack > $this->maxAttack) {
|
||||
throw new InvalidArgumentException('Attack range is inverted.');
|
||||
}
|
||||
if ($this->minDefense > $this->maxDefense) {
|
||||
throw new InvalidArgumentException('Defense range is inverted.');
|
||||
}
|
||||
if ($this->minSpeed > $this->maxSpeed || $this->minSpeed < 1) {
|
||||
throw new InvalidArgumentException('Speed range must be at least 1 and non-inverted.');
|
||||
}
|
||||
if ($this->minHealth < 1) {
|
||||
throw new InvalidArgumentException('Minimum health must be at least 1.');
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array{maxHealth: int, attack: int, defense: int, speed: int} */
|
||||
public function defaultStats(): array
|
||||
{
|
||||
return [
|
||||
'maxHealth' => (int) ceil(($this->minHealth + $this->maxHealth) / 2),
|
||||
'attack' => (int) ceil(($this->minAttack + $this->maxAttack) / 2),
|
||||
'defense' => (int) ceil(($this->minDefense + $this->maxDefense) / 2),
|
||||
'speed' => (int) ceil(($this->minSpeed + $this->maxSpeed) / 2),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BattleForge\Tests\Unit\Domain;
|
||||
|
||||
use BattleForge\Domain\Archetype;
|
||||
use BattleForge\Domain\ArchetypeCatalog;
|
||||
use BattleForge\Domain\ArchetypeTemplate;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ArchetypeCatalogTest extends TestCase
|
||||
{
|
||||
public function testItExposesExactlyTheFourPlannedArchetypes(): void
|
||||
{
|
||||
$keys = array_keys(ArchetypeCatalog::templates());
|
||||
|
||||
sort($keys);
|
||||
|
||||
self::assertSame(['defender', 'scout', 'striker', 'support'], $keys);
|
||||
}
|
||||
|
||||
public function testEveryArchetypeTemplateCarriesAllFourStatRanges(): void
|
||||
{
|
||||
foreach (ArchetypeCatalog::templates() as $key => $template) {
|
||||
self::assertInstanceOf(ArchetypeTemplate::class, $template);
|
||||
self::assertLessThanOrEqual($template->maxHealth, $template->minHealth);
|
||||
self::assertLessThanOrEqual($template->maxAttack, $template->minAttack);
|
||||
self::assertLessThanOrEqual($template->maxDefense, $template->minDefense);
|
||||
self::assertLessThanOrEqual($template->maxSpeed, $template->minSpeed);
|
||||
self::assertGreaterThanOrEqual(1, $template->minSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
public function testEveryArchetypeTemplateDefaultsToTheCenterOfItsRanges(): void
|
||||
{
|
||||
foreach (ArchetypeCatalog::templates() as $template) {
|
||||
$defaults = $template->defaultStats();
|
||||
|
||||
self::assertSame((int) ceil(($template->minHealth + $template->maxHealth) / 2), $defaults['maxHealth']);
|
||||
self::assertSame((int) ceil(($template->minAttack + $template->maxAttack) / 2), $defaults['attack']);
|
||||
self::assertSame((int) ceil(($template->minDefense + $template->maxDefense) / 2), $defaults['defense']);
|
||||
self::assertSame((int) ceil(($template->minSpeed + $template->maxSpeed) / 2), $defaults['speed']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testDefaultStatsReturnAllFourKeys(): void
|
||||
{
|
||||
$template = ArchetypeCatalog::templates()[Archetype::Defender->value];
|
||||
|
||||
self::assertSame(
|
||||
['maxHealth', 'attack', 'defense', 'speed'],
|
||||
array_keys($template->defaultStats()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BattleForge\Tests\Unit\Domain;
|
||||
|
||||
use BattleForge\Domain\ArchetypeTemplate;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ArchetypeTemplateTest extends TestCase
|
||||
{
|
||||
public function testItRejectsInvertedStatRanges(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
new ArchetypeTemplate(
|
||||
minHealth: 10,
|
||||
maxHealth: 5,
|
||||
minAttack: 1,
|
||||
maxAttack: 3,
|
||||
minDefense: 0,
|
||||
maxDefense: 2,
|
||||
minSpeed: 1,
|
||||
maxSpeed: 4,
|
||||
allowedAbilities: [],
|
||||
);
|
||||
}
|
||||
|
||||
public function testItRejectsZeroMinimumSpeed(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
new ArchetypeTemplate(
|
||||
minHealth: 5,
|
||||
maxHealth: 10,
|
||||
minAttack: 1,
|
||||
maxAttack: 3,
|
||||
minDefense: 0,
|
||||
maxDefense: 2,
|
||||
minSpeed: 0,
|
||||
maxSpeed: 4,
|
||||
allowedAbilities: [],
|
||||
);
|
||||
}
|
||||
|
||||
public function testItExposesAllowedAbilitiesVerbatim(): void
|
||||
{
|
||||
$template = new ArchetypeTemplate(
|
||||
minHealth: 5,
|
||||
maxHealth: 10,
|
||||
minAttack: 1,
|
||||
maxAttack: 3,
|
||||
minDefense: 0,
|
||||
maxDefense: 2,
|
||||
minSpeed: 1,
|
||||
maxSpeed: 4,
|
||||
allowedAbilities: ['heal', 'buff'],
|
||||
);
|
||||
|
||||
self::assertSame(['heal', 'buff'], $template->allowedAbilities);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user