Add Enemies section: D&D stat-block table + detail/edit pages

- Enemy entity: full 5e stat block (size/type/subtype/alignment, AC, HP,
  speed, six ability scores, CR/XP/prof, saves/skills/senses/languages,
  damage & condition modifiers, traits/actions/bonus/reactions/legendary,
  description, source) plus a DM game-notes field and updated_at.
- CreatureSize / CreatureType enums.
- /enemies: server-side DataTable (name, size, type, CR, AC, HP, notes),
  "Add enemy" button.
- /enemies/{id}: stat block rendered read-only + editable game notes.
- /enemies/new and /enemies/{id}/edit: the full stat-block form (no notes).
- "Enemies" nav item between NPC's and Storyline.
- Table left empty on purpose.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Frank
2026-09-06 16:36:17 +02:00
co-authored by Claude Sonnet 5
parent 574d903715
commit 7870be9dfb
12 changed files with 1234 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Form;
use App\Entity\Enemy;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* The only editable thing on the Enemy detail page: the DM's live play notes.
*/
class EnemyGameNotesType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('gameNotes', TextareaType::class, [
'required' => false,
'label' => false,
'attr' => ['rows' => 14, 'placeholder' => 'How this enemy played at the table…'],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['data_class' => Enemy::class]);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace App\Form;
use App\Entity\Enemy;
use App\Enum\CreatureSize;
use App\Enum\CreatureType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class EnemyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$enum = static fn (string $class) => [
'class' => $class,
'choice_label' => static fn ($case) => $case->value,
];
$opt = ['required' => false];
$ability = ['attr' => ['min' => 1, 'max' => 30]];
$block = ['required' => false, 'attr' => ['rows' => 3]];
$builder
// Identity
->add('name', TextType::class)
->add('size', EnumType::class, $enum(CreatureSize::class))
->add('type', EnumType::class, $enum(CreatureType::class))
->add('subtype', TextType::class, $opt + ['help' => 'e.g. goblinoid, shapechanger'])
->add('alignment', TextType::class, $opt + ['help' => 'e.g. chaotic evil, unaligned'])
// Defenses
->add('armorClass', IntegerType::class)
->add('armorClassNote', TextType::class, $opt + ['label' => 'AC note', 'help' => 'e.g. natural armor'])
->add('hitPoints', IntegerType::class)
->add('hitDice', TextType::class, $opt + ['help' => 'e.g. 7d8 + 14'])
->add('speed', TextType::class, $opt + ['help' => 'e.g. 30 ft., fly 60 ft.'])
// Abilities
->add('strength', IntegerType::class, $ability)
->add('dexterity', IntegerType::class, $ability)
->add('constitution', IntegerType::class, $ability)
->add('intelligence', IntegerType::class, $ability)
->add('wisdom', IntegerType::class, $ability)
->add('charisma', IntegerType::class, $ability)
// Challenge
->add('challengeRating', TextType::class, $opt + ['label' => 'CR', 'help' => 'e.g. 1/4, 5'])
->add('experiencePoints', IntegerType::class, $opt + ['label' => 'XP'])
->add('proficiencyBonus', IntegerType::class, $opt)
// Secondary stats
->add('savingThrows', TextType::class, $opt + ['help' => 'e.g. Dex +5, Con +6'])
->add('skills', TextType::class, $opt + ['help' => 'e.g. Perception +4, Stealth +6'])
->add('damageVulnerabilities', TextType::class, $opt)
->add('damageResistances', TextType::class, $opt)
->add('damageImmunities', TextType::class, $opt)
->add('conditionImmunities', TextType::class, $opt)
->add('senses', TextType::class, $opt + ['help' => 'e.g. darkvision 60 ft., passive Perception 12'])
->add('languages', TextType::class, $opt)
// Text blocks
->add('traits', TextareaType::class, $block + ['help' => 'Special abilities'])
->add('actions', TextareaType::class, $block)
->add('bonusActions', TextareaType::class, $block)
->add('reactions', TextareaType::class, $block)
->add('legendaryActions', TextareaType::class, $block)
->add('description', TextareaType::class, ['required' => false, 'attr' => ['rows' => 4], 'help' => 'Lore / flavour'])
->add('source', TextType::class, $opt + ['help' => 'e.g. Monster Manual p. 195, homebrew']);
// gameNotes lives on the detail page, not this form.
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['data_class' => Enemy::class]);
}
}