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
+612
View File
@@ -0,0 +1,612 @@
<?php
namespace App\Entity;
use App\Enum\CreatureSize;
use App\Enum\CreatureType;
use App\Repository\EnemyRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: EnemyRepository::class)]
#[ORM\Table(name: 'enemy')]
#[ORM\UniqueConstraint(name: 'uniq_enemy_name', columns: ['name'])]
#[ORM\HasLifecycleCallbacks]
class Enemy
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
// --- Identity ---------------------------------------------------------
#[ORM\Column(length: 160)]
#[Assert\NotBlank]
private string $name = '';
#[ORM\Column(enumType: CreatureSize::class)]
private CreatureSize $size = CreatureSize::Medium;
#[ORM\Column(enumType: CreatureType::class)]
private CreatureType $type = CreatureType::Humanoid;
#[ORM\Column(length: 120, nullable: true)]
private ?string $subtype = null;
#[ORM\Column(length: 120, nullable: true)]
private ?string $alignment = null;
// --- Defenses / core ------------------------------------------------
#[ORM\Column(type: Types::SMALLINT)]
#[Assert\PositiveOrZero]
private int $armorClass = 10;
#[ORM\Column(length: 120, nullable: true)]
private ?string $armorClassNote = null;
#[ORM\Column(type: Types::SMALLINT)]
#[Assert\PositiveOrZero]
private int $hitPoints = 1;
#[ORM\Column(length: 60, nullable: true)]
private ?string $hitDice = null;
#[ORM\Column(length: 180, nullable: true)]
private ?string $speed = null;
// --- Ability scores ------------------------------------------------
#[ORM\Column(type: Types::SMALLINT)]
#[Assert\Range(min: 1, max: 30)]
private int $strength = 10;
#[ORM\Column(type: Types::SMALLINT)]
#[Assert\Range(min: 1, max: 30)]
private int $dexterity = 10;
#[ORM\Column(type: Types::SMALLINT)]
#[Assert\Range(min: 1, max: 30)]
private int $constitution = 10;
#[ORM\Column(type: Types::SMALLINT)]
#[Assert\Range(min: 1, max: 30)]
private int $intelligence = 10;
#[ORM\Column(type: Types::SMALLINT)]
#[Assert\Range(min: 1, max: 30)]
private int $wisdom = 10;
#[ORM\Column(type: Types::SMALLINT)]
#[Assert\Range(min: 1, max: 30)]
private int $charisma = 10;
// --- Challenge ----------------------------------------------------
/** Fractional for weak creatures ("1/8", "1/4", "1/2"), so a string. */
#[ORM\Column(length: 12, nullable: true)]
private ?string $challengeRating = null;
#[ORM\Column(type: Types::INTEGER, nullable: true)]
private ?int $experiencePoints = null;
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
private ?int $proficiencyBonus = null;
// --- Secondary stats (free text) --------------------------------
#[ORM\Column(length: 180, nullable: true)]
private ?string $savingThrows = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $skills = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $damageVulnerabilities = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $damageResistances = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $damageImmunities = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $conditionImmunities = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $senses = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $languages = null;
// --- Text blocks ------------------------------------------------
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $traits = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $actions = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $bonusActions = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $reactions = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $legendaryActions = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 180, nullable: true)]
private ?string $source = null;
// --- DM ------------------------------------------------------
/** DM's live notes, written during play. Never touched by an importer. */
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $gameNotes = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
#[ORM\PrePersist]
#[ORM\PreUpdate]
public function touch(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
/** e.g. "Large Fiend (demon), chaotic evil". */
public function getMetaLine(): string
{
$type = $this->type->value;
if ($this->subtype) {
$type .= ' ('.$this->subtype.')';
}
$line = $this->size->value.' '.$type;
if ($this->alignment) {
$line .= ', '.$this->alignment;
}
return $line;
}
public function abilityModifier(int $score): int
{
return (int) floor(($score - 10) / 2);
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getSize(): CreatureSize
{
return $this->size;
}
public function setSize(CreatureSize $size): static
{
$this->size = $size;
return $this;
}
public function getType(): CreatureType
{
return $this->type;
}
public function setType(CreatureType $type): static
{
$this->type = $type;
return $this;
}
public function getSubtype(): ?string
{
return $this->subtype;
}
public function setSubtype(?string $subtype): static
{
$this->subtype = $subtype;
return $this;
}
public function getAlignment(): ?string
{
return $this->alignment;
}
public function setAlignment(?string $alignment): static
{
$this->alignment = $alignment;
return $this;
}
public function getArmorClass(): int
{
return $this->armorClass;
}
public function setArmorClass(int $armorClass): static
{
$this->armorClass = $armorClass;
return $this;
}
public function getArmorClassNote(): ?string
{
return $this->armorClassNote;
}
public function setArmorClassNote(?string $armorClassNote): static
{
$this->armorClassNote = $armorClassNote;
return $this;
}
public function getHitPoints(): int
{
return $this->hitPoints;
}
public function setHitPoints(int $hitPoints): static
{
$this->hitPoints = $hitPoints;
return $this;
}
public function getHitDice(): ?string
{
return $this->hitDice;
}
public function setHitDice(?string $hitDice): static
{
$this->hitDice = $hitDice;
return $this;
}
public function getSpeed(): ?string
{
return $this->speed;
}
public function setSpeed(?string $speed): static
{
$this->speed = $speed;
return $this;
}
public function getStrength(): int
{
return $this->strength;
}
public function setStrength(int $strength): static
{
$this->strength = $strength;
return $this;
}
public function getDexterity(): int
{
return $this->dexterity;
}
public function setDexterity(int $dexterity): static
{
$this->dexterity = $dexterity;
return $this;
}
public function getConstitution(): int
{
return $this->constitution;
}
public function setConstitution(int $constitution): static
{
$this->constitution = $constitution;
return $this;
}
public function getIntelligence(): int
{
return $this->intelligence;
}
public function setIntelligence(int $intelligence): static
{
$this->intelligence = $intelligence;
return $this;
}
public function getWisdom(): int
{
return $this->wisdom;
}
public function setWisdom(int $wisdom): static
{
$this->wisdom = $wisdom;
return $this;
}
public function getCharisma(): int
{
return $this->charisma;
}
public function setCharisma(int $charisma): static
{
$this->charisma = $charisma;
return $this;
}
public function getChallengeRating(): ?string
{
return $this->challengeRating;
}
public function setChallengeRating(?string $challengeRating): static
{
$this->challengeRating = $challengeRating;
return $this;
}
public function getExperiencePoints(): ?int
{
return $this->experiencePoints;
}
public function setExperiencePoints(?int $experiencePoints): static
{
$this->experiencePoints = $experiencePoints;
return $this;
}
public function getProficiencyBonus(): ?int
{
return $this->proficiencyBonus;
}
public function setProficiencyBonus(?int $proficiencyBonus): static
{
$this->proficiencyBonus = $proficiencyBonus;
return $this;
}
public function getSavingThrows(): ?string
{
return $this->savingThrows;
}
public function setSavingThrows(?string $savingThrows): static
{
$this->savingThrows = $savingThrows;
return $this;
}
public function getSkills(): ?string
{
return $this->skills;
}
public function setSkills(?string $skills): static
{
$this->skills = $skills;
return $this;
}
public function getDamageVulnerabilities(): ?string
{
return $this->damageVulnerabilities;
}
public function setDamageVulnerabilities(?string $damageVulnerabilities): static
{
$this->damageVulnerabilities = $damageVulnerabilities;
return $this;
}
public function getDamageResistances(): ?string
{
return $this->damageResistances;
}
public function setDamageResistances(?string $damageResistances): static
{
$this->damageResistances = $damageResistances;
return $this;
}
public function getDamageImmunities(): ?string
{
return $this->damageImmunities;
}
public function setDamageImmunities(?string $damageImmunities): static
{
$this->damageImmunities = $damageImmunities;
return $this;
}
public function getConditionImmunities(): ?string
{
return $this->conditionImmunities;
}
public function setConditionImmunities(?string $conditionImmunities): static
{
$this->conditionImmunities = $conditionImmunities;
return $this;
}
public function getSenses(): ?string
{
return $this->senses;
}
public function setSenses(?string $senses): static
{
$this->senses = $senses;
return $this;
}
public function getLanguages(): ?string
{
return $this->languages;
}
public function setLanguages(?string $languages): static
{
$this->languages = $languages;
return $this;
}
public function getTraits(): ?string
{
return $this->traits;
}
public function setTraits(?string $traits): static
{
$this->traits = $traits;
return $this;
}
public function getActions(): ?string
{
return $this->actions;
}
public function setActions(?string $actions): static
{
$this->actions = $actions;
return $this;
}
public function getBonusActions(): ?string
{
return $this->bonusActions;
}
public function setBonusActions(?string $bonusActions): static
{
$this->bonusActions = $bonusActions;
return $this;
}
public function getReactions(): ?string
{
return $this->reactions;
}
public function setReactions(?string $reactions): static
{
$this->reactions = $reactions;
return $this;
}
public function getLegendaryActions(): ?string
{
return $this->legendaryActions;
}
public function setLegendaryActions(?string $legendaryActions): static
{
$this->legendaryActions = $legendaryActions;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getSource(): ?string
{
return $this->source;
}
public function setSource(?string $source): static
{
$this->source = $source;
return $this;
}
public function getGameNotes(): ?string
{
return $this->gameNotes;
}
public function setGameNotes(?string $gameNotes): static
{
$this->gameNotes = $gameNotes;
return $this;
}
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
}