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:
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user