Add app:enemy:seed - 8 SRD monsters for testing the Enemies section

Goblin, Wolf, Zombie, Bandit Captain, Ogre, Owlbear, Wight and Adult Red
Dragon (CR 1/4 to 17; humanoid, beast, undead, giant, monstrosity,
dragon). Idempotent by name; --purge removes them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Frank
2026-09-06 19:04:43 +02:00
co-authored by Claude Sonnet 5
parent ed22b81e97
commit e417ff9655
+180
View File
@@ -0,0 +1,180 @@
<?php
namespace App\Command;
use App\Entity\Enemy;
use App\Enum\CreatureSize;
use App\Enum\CreatureType;
use App\Repository\EnemyRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:enemy:seed',
description: 'Add a handful of SRD monsters to the enemies table for testing.',
)]
class SeedEnemiesCommand extends Command
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly EnemyRepository $enemies,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addOption('purge', null, InputOption::VALUE_NONE, 'Remove the seeded monsters instead of adding them');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$data = $this->monsters();
if ($input->getOption('purge')) {
$removed = 0;
foreach ($data as $row) {
if ($e = $this->enemies->findOneBy(['name' => $row['name']])) {
$this->em->remove($e);
++$removed;
}
}
$this->em->flush();
$io->success(sprintf('Removed %d seeded monster(s).', $removed));
return Command::SUCCESS;
}
$added = $skipped = 0;
foreach ($data as $row) {
if ($this->enemies->findOneBy(['name' => $row['name']])) {
++$skipped;
continue;
}
$e = new Enemy();
foreach ($row as $prop => $value) {
$e->{'set'.ucfirst($prop)}(
'size' === $prop ? CreatureSize::from($value)
: ('type' === $prop ? CreatureType::from($value) : $value)
);
}
$this->em->persist($e);
++$added;
}
$this->em->flush();
$io->success(sprintf('%d monster(s) added, %d already present.', $added, $skipped));
return Command::SUCCESS;
}
/**
* SRD 5.1 stat blocks (open content). Trimmed to what the app stores.
*
* @return list<array<string, mixed>>
*/
private function monsters(): array
{
return [
[
'name' => 'Goblin', 'size' => 'Small', 'type' => 'Humanoid', 'subtype' => 'goblinoid',
'alignment' => 'neutral evil', 'armorClass' => 15, 'armorClassNote' => 'leather armor, shield',
'hitPoints' => 7, 'hitDice' => '2d6', 'speed' => '30 ft.',
'strength' => 8, 'dexterity' => 14, 'constitution' => 10, 'intelligence' => 10, 'wisdom' => 8, 'charisma' => 8,
'challengeRating' => '1/4', 'experiencePoints' => 50, 'proficiencyBonus' => 2,
'skills' => 'Stealth +6', 'senses' => 'darkvision 60 ft., passive Perception 9',
'languages' => 'Common, Goblin',
'traits' => "Nimble Escape. The goblin can take the Disengage or Hide action as a bonus action on each of its turns.",
'actions' => "Scimitar. Melee Weapon Attack: +4 to hit, reach 5 ft., one target. Hit: 5 (1d6 + 2) slashing damage.\nShortbow. Ranged Weapon Attack: +4 to hit, range 80/320 ft., one target. Hit: 5 (1d6 + 2) piercing damage.",
'source' => 'SRD 5.1',
],
[
'name' => 'Wolf', 'size' => 'Medium', 'type' => 'Beast', 'alignment' => 'unaligned',
'armorClass' => 13, 'armorClassNote' => 'natural armor', 'hitPoints' => 11, 'hitDice' => '2d8 + 2', 'speed' => '40 ft.',
'strength' => 12, 'dexterity' => 15, 'constitution' => 12, 'intelligence' => 3, 'wisdom' => 12, 'charisma' => 6,
'challengeRating' => '1/4', 'experiencePoints' => 50, 'proficiencyBonus' => 2,
'skills' => 'Perception +3, Stealth +4', 'senses' => 'passive Perception 13',
'traits' => "Keen Hearing and Smell. The wolf has advantage on Wisdom (Perception) checks that rely on hearing or smell.\nPack Tactics. The wolf has advantage on an attack roll against a creature if at least one of the wolf's allies is within 5 ft. of the creature and the ally isn't incapacitated.",
'actions' => "Bite. Melee Weapon Attack: +4 to hit, reach 5 ft., one target. Hit: 7 (2d4 + 2) piercing damage. If the target is a creature, it must succeed on a DC 11 Strength saving throw or be knocked prone.",
'source' => 'SRD 5.1',
],
[
'name' => 'Zombie', 'size' => 'Medium', 'type' => 'Undead', 'alignment' => 'neutral evil',
'armorClass' => 8, 'hitPoints' => 22, 'hitDice' => '3d8 + 9', 'speed' => '20 ft.',
'strength' => 13, 'dexterity' => 6, 'constitution' => 16, 'intelligence' => 3, 'wisdom' => 6, 'charisma' => 5,
'challengeRating' => '1/4', 'experiencePoints' => 50, 'proficiencyBonus' => 2,
'savingThrows' => 'Wis +0', 'damageImmunities' => 'poison', 'conditionImmunities' => 'poisoned',
'senses' => 'darkvision 60 ft., passive Perception 8',
'languages' => "understands the languages it knew in life but can't speak",
'traits' => "Undead Fortitude. If damage reduces the zombie to 0 hit points, it must make a Constitution saving throw with a DC of 5 + the damage taken, unless the damage is radiant or from a critical hit. On a success, the zombie drops to 1 hit point instead.",
'actions' => "Slam. Melee Weapon Attack: +3 to hit, reach 5 ft., one target. Hit: 4 (1d6 + 1) bludgeoning damage.",
'source' => 'SRD 5.1',
],
[
'name' => 'Bandit Captain', 'size' => 'Medium', 'type' => 'Humanoid', 'alignment' => 'any non-lawful alignment',
'armorClass' => 15, 'armorClassNote' => 'studded leather', 'hitPoints' => 65, 'hitDice' => '10d8 + 20', 'speed' => '30 ft.',
'strength' => 15, 'dexterity' => 16, 'constitution' => 14, 'intelligence' => 14, 'wisdom' => 11, 'charisma' => 14,
'challengeRating' => '2', 'experiencePoints' => 450, 'proficiencyBonus' => 2,
'savingThrows' => 'Str +4, Dex +5, Wis +2', 'skills' => 'Athletics +4, Deception +4',
'senses' => 'passive Perception 10', 'languages' => 'any two languages',
'actions' => "Multiattack. The captain makes three melee attacks: two with its scimitar and one with its dagger. Or the captain makes two ranged attacks with its daggers.\nScimitar. Melee Weapon Attack: +5 to hit, reach 5 ft., one target. Hit: 6 (1d6 + 3) slashing damage.\nDagger. Melee or Ranged Weapon Attack: +5 to hit, reach 5 ft. or range 20/60 ft., one target. Hit: 5 (1d4 + 3) piercing damage.",
'reactions' => "Parry. The captain adds 2 to its AC against one melee attack that would hit it. To do so, the captain must see the attacker and be wielding a melee weapon.",
'source' => 'SRD 5.1',
],
[
'name' => 'Ogre', 'size' => 'Large', 'type' => 'Giant', 'alignment' => 'chaotic evil',
'armorClass' => 11, 'armorClassNote' => 'hide armor', 'hitPoints' => 59, 'hitDice' => '7d10 + 21', 'speed' => '40 ft.',
'strength' => 19, 'dexterity' => 8, 'constitution' => 16, 'intelligence' => 5, 'wisdom' => 7, 'charisma' => 7,
'challengeRating' => '2', 'experiencePoints' => 450, 'proficiencyBonus' => 2,
'senses' => 'darkvision 60 ft., passive Perception 8', 'languages' => 'Common, Giant',
'actions' => "Greatclub. Melee Weapon Attack: +6 to hit, reach 5 ft., one target. Hit: 13 (2d8 + 4) bludgeoning damage.\nJavelin. Melee or Ranged Weapon Attack: +6 to hit, reach 5 ft. or range 30/120 ft., one target. Hit: 11 (2d6 + 4) piercing damage.",
'source' => 'SRD 5.1',
],
[
'name' => 'Owlbear', 'size' => 'Large', 'type' => 'Monstrosity', 'alignment' => 'unaligned',
'armorClass' => 13, 'armorClassNote' => 'natural armor', 'hitPoints' => 59, 'hitDice' => '7d10 + 21', 'speed' => '40 ft.',
'strength' => 20, 'dexterity' => 12, 'constitution' => 17, 'intelligence' => 3, 'wisdom' => 12, 'charisma' => 7,
'challengeRating' => '3', 'experiencePoints' => 700, 'proficiencyBonus' => 2,
'skills' => 'Perception +3', 'senses' => 'darkvision 60 ft., passive Perception 13',
'traits' => "Keen Sight and Smell. The owlbear has advantage on Wisdom (Perception) checks that rely on sight or smell.",
'actions' => "Multiattack. The owlbear makes two attacks: one with its beak and one with its claws.\nBeak. Melee Weapon Attack: +7 to hit, reach 5 ft., one creature. Hit: 10 (1d10 + 5) piercing damage.\nClaws. Melee Weapon Attack: +7 to hit, reach 5 ft., one target. Hit: 14 (2d8 + 5) slashing damage.",
'source' => 'SRD 5.1',
],
[
'name' => 'Wight', 'size' => 'Medium', 'type' => 'Undead', 'alignment' => 'neutral evil',
'armorClass' => 14, 'armorClassNote' => 'studded leather', 'hitPoints' => 45, 'hitDice' => '6d8 + 18', 'speed' => '30 ft.',
'strength' => 15, 'dexterity' => 14, 'constitution' => 16, 'intelligence' => 10, 'wisdom' => 13, 'charisma' => 15,
'challengeRating' => '3', 'experiencePoints' => 700, 'proficiencyBonus' => 2,
'skills' => 'Perception +3, Stealth +4',
'damageResistances' => "necrotic; bludgeoning, piercing, and slashing from nonmagical attacks that aren't silvered",
'damageImmunities' => 'poison', 'conditionImmunities' => 'exhaustion, poisoned',
'senses' => 'darkvision 60 ft., passive Perception 13',
'languages' => 'the languages it knew in life',
'traits' => "Sunlight Sensitivity. While in sunlight, the wight has disadvantage on attack rolls, as well as on Wisdom (Perception) checks that rely on sight.",
'actions' => "Multiattack. The wight makes two longsword attacks or two longbow attacks. It can use its Life Drain in place of one longsword attack.\nLife Drain. Melee Weapon Attack: +4 to hit, reach 5 ft., one creature. Hit: 5 (1d6 + 2) necrotic damage. The target must succeed on a DC 13 Constitution saving throw or its hit point maximum is reduced by an amount equal to the damage taken. This reduction lasts until the target finishes a long rest. The target dies if this effect reduces its hit point maximum to 0.\nLongsword. Melee Weapon Attack: +4 to hit, reach 5 ft., one target. Hit: 6 (1d8 + 2) slashing damage, or 7 (1d10 + 2) slashing damage if used with two hands.\nLongbow. Ranged Weapon Attack: +4 to hit, range 150/600 ft., one target. Hit: 6 (1d8 + 2) piercing damage.",
'source' => 'SRD 5.1',
],
[
'name' => 'Adult Red Dragon', 'size' => 'Huge', 'type' => 'Dragon', 'alignment' => 'chaotic evil',
'armorClass' => 19, 'armorClassNote' => 'natural armor', 'hitPoints' => 256, 'hitDice' => '19d12 + 133',
'speed' => '40 ft., climb 40 ft., fly 80 ft.',
'strength' => 27, 'dexterity' => 10, 'constitution' => 25, 'intelligence' => 16, 'wisdom' => 13, 'charisma' => 21,
'challengeRating' => '17', 'experiencePoints' => 18000, 'proficiencyBonus' => 6,
'savingThrows' => 'Dex +6, Con +13, Wis +7, Cha +11', 'skills' => 'Perception +13, Stealth +6',
'damageImmunities' => 'fire',
'senses' => 'blindsight 60 ft., darkvision 120 ft., passive Perception 23',
'languages' => 'Common, Draconic',
'traits' => "Legendary Resistance (3/Day). If the dragon fails a saving throw, it can choose to succeed instead.",
'actions' => "Multiattack. The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws.\nBite. Melee Weapon Attack: +14 to hit, reach 10 ft., one target. Hit: 19 (2d10 + 8) piercing damage plus 7 (2d6) fire damage.\nClaw. Melee Weapon Attack: +14 to hit, reach 5 ft., one target. Hit: 15 (2d6 + 8) slashing damage.\nTail. Melee Weapon Attack: +14 to hit, reach 15 ft., one target. Hit: 17 (2d8 + 8) bludgeoning damage.\nFrightful Presence. Each creature of the dragon's choice within 120 ft. and aware of it must succeed on a DC 19 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the save at the end of each of its turns; on a success it is immune to this dragon's Frightful Presence for 24 hours.\nFire Breath (Recharge 5-6). The dragon exhales fire in a 60-foot cone. Each creature in that area must make a DC 21 Dexterity saving throw, taking 63 (18d6) fire damage on a failed save, or half as much on a success.",
'legendaryActions' => "The dragon can take 3 legendary actions, choosing from the options below. Only one legendary action can be used at a time and only at the end of another creature's turn. The dragon regains spent legendary actions at the start of its turn.\nDetect. The dragon makes a Wisdom (Perception) check.\nTail Attack. The dragon makes a tail attack.\nWing Attack (Costs 2 Actions). The dragon beats its wings. Each creature within 10 ft. must succeed on a DC 22 Dexterity saving throw or take 15 (2d6 + 8) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed.",
'source' => 'SRD 5.1',
],
];
}
}