Characters: build the page around Darragh Oakwanderer's sheet
Replaces the Characters placeholder with a real 5e character sheet feature, following the same read-only-detail/edit-split/game-notes pattern as NPCs and Enemies: - Character entity: identity, combat vitals, six ability scores, saving throw and skill proficiencies (computed modifiers, not stored), a small attacks table, and the sheet's freeform boxes (features, equipment, personality, appearance, backstory, allies, treasure). - Detail page shows the portrait uncropped, then the sheet - genuine <table>s for ability scores, skills and attacks, since that's the one part of a character sheet that actually is a table; everything else reuses the prose helpers from the enemy stat block. Game notes are the only editable field here, same as Enemy/NPC. - Edit page covers every field via CharacterType; the attack rows are six plain inputs handled by hand (not a Symfony collection field), since it's a short fixed-shape table, not a dynamic list. - Portraits follow the NPC convention: assets/image/characters/<NameNo Spaces>.png, computed path, not a DB column. - Imported the example sheet from assets/characters/ (Darragh Oakwanderer, halfling ranger) directly into the database. Tests: entity math (ability/save/skill modifiers, passive perception, portrait path), template rendering for show/edit/index (catches broken Twig/macro/path()/asset() calls), and the login-gate redirect. 21/21 passing; schema validate clean. 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\Character;
|
||||
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 Character detail page: the DM's live play notes.
|
||||
*/
|
||||
class CharacterGameNotesType 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 character played at the table…'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(['data_class' => Character::class]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Character;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The full character sheet, minus game notes (that lives on the detail page)
|
||||
* and minus attacks (a small repeatable table handled by hand in the
|
||||
* template/controller, since it's a list of rows rather than a single value).
|
||||
*/
|
||||
class CharacterType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$opt = ['required' => false];
|
||||
$ability = ['attr' => ['min' => 1, 'max' => 30]];
|
||||
$block = ['required' => false, 'attr' => ['rows' => 3]];
|
||||
$note = ['required' => false, 'attr' => ['rows' => 2]];
|
||||
|
||||
$builder
|
||||
// Identity
|
||||
->add('name', TextType::class)
|
||||
->add('playerName', TextType::class, $opt + ['label' => 'Player name'])
|
||||
->add('race', TextType::class, $opt + ['help' => 'e.g. Halfling, stout'])
|
||||
->add('characterClass', TextType::class, $opt + ['label' => 'Class', 'help' => 'e.g. Ranger'])
|
||||
->add('level', IntegerType::class, ['attr' => ['min' => 1, 'max' => 20]])
|
||||
->add('background', TextType::class, $opt)
|
||||
->add('alignment', TextType::class, $opt + ['help' => 'e.g. CN'])
|
||||
->add('experiencePoints', IntegerType::class, $opt + ['label' => 'XP'])
|
||||
// Combat
|
||||
->add('armorClass', IntegerType::class, $opt + ['label' => 'AC'])
|
||||
->add('initiative', IntegerType::class, $opt)
|
||||
->add('speed', TextType::class, $opt + ['help' => 'e.g. 25 ft.'])
|
||||
->add('hitPointMaximum', IntegerType::class, $opt + ['label' => 'HP maximum'])
|
||||
->add('currentHitPoints', IntegerType::class, $opt + ['label' => 'Current HP'])
|
||||
->add('temporaryHitPoints', IntegerType::class, $opt + ['label' => 'Temporary HP'])
|
||||
->add('hitDice', TextType::class, $opt + ['help' => 'e.g. 2d10'])
|
||||
->add('proficiencyBonus', IntegerType::class)
|
||||
->add('inspiration', CheckboxType::class, ['required' => false])
|
||||
// Ability scores
|
||||
->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)
|
||||
// Proficiencies
|
||||
->add('savingThrowProficiencies', ChoiceType::class, [
|
||||
'choices' => array_combine(Character::ABILITIES, Character::ABILITIES),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
'label' => 'Saving throw proficiencies',
|
||||
])
|
||||
->add('skillProficiencies', ChoiceType::class, [
|
||||
'choices' => array_combine(array_keys(Character::SKILLS), array_keys(Character::SKILLS)),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
'label' => 'Skill proficiencies',
|
||||
])
|
||||
// Text blocks
|
||||
->add('otherProficienciesLanguages', TextareaType::class, $block + ['label' => 'Other proficiencies & languages'])
|
||||
->add('equipment', TextareaType::class, $block)
|
||||
->add('featuresTraits', TextareaType::class, $block + ['label' => 'Features & traits'])
|
||||
->add('spellcasting', TextareaType::class, $block)
|
||||
->add('personalityTraits', TextareaType::class, $note + ['label' => 'Personality traits'])
|
||||
->add('ideals', TextareaType::class, $note)
|
||||
->add('bonds', TextareaType::class, $note)
|
||||
->add('flaws', TextareaType::class, $note)
|
||||
->add('appearance', TextareaType::class, $block + ['help' => 'Age, height, weight, eyes, skin, hair, description'])
|
||||
->add('backstory', TextareaType::class, $block)
|
||||
->add('alliesOrganizations', TextareaType::class, $block + ['label' => 'Allies & organizations'])
|
||||
->add('treasure', TextareaType::class, $block);
|
||||
// attacks and gameNotes are not part of this form.
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(['data_class' => Character::class]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user