Files
dmtools/tests/Entity/CharacterTest.php
T
FrankandClaude Sonnet 5 9052218927 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>
2026-09-22 08:55:28 +02:00

75 lines
2.4 KiB
PHP

<?php
namespace App\Tests\Entity;
use App\Entity\Character;
use PHPUnit\Framework\TestCase;
class CharacterTest extends TestCase
{
private function darragh(): Character
{
return (new Character())
->setName('Darragh Oakwanderer')
->setCharacterClass('Ranger')
->setLevel(2)
->setRace('Halfling, stout')
->setProficiencyBonus(2)
->setStrength(12)
->setDexterity(17)
->setWisdom(13)
->setSavingThrowProficiencies(['Strength', 'Dexterity'])
->setSkillProficiencies(['Sleight of Hand', 'Stealth']);
}
public function testAbilityModifierRoundsDown(): void
{
$c = new Character();
self::assertSame(-1, $c->abilityModifier(8));
self::assertSame(0, $c->abilityModifier(10));
self::assertSame(0, $c->abilityModifier(11));
self::assertSame(3, $c->abilityModifier(17));
}
public function testSkillModifierAddsProficiencyOnlyWhenProficient(): void
{
$c = $this->darragh();
// Dex 17 -> +3, proficient (+2 prof) -> +5
self::assertSame(5, $c->skillModifier('Stealth'));
// Wis 13 -> +1, not proficient in Perception
self::assertSame(1, $c->skillModifier('Perception'));
}
public function testSaveModifierAddsProficiencyOnlyWhenProficient(): void
{
$c = $this->darragh();
self::assertSame(3, $c->saveModifier('Strength')); // Str +1, proficient +2 => +3
self::assertSame(5, $c->saveModifier('Dexterity')); // Dex +3, proficient +2 => +5
self::assertSame(1, $c->saveModifier('Wisdom')); // Wis +1, not proficient
}
public function testPassivePerceptionIsTenPlusPerceptionModifier(): void
{
$c = $this->darragh(); // Wis +1, not proficient in Perception
self::assertSame(11, $c->passivePerception());
}
public function testPortraitPathStripsSpacesFromName(): void
{
$c = (new Character())->setName('Darragh Oakwanderer');
self::assertSame('image/characters/DarraghOakwanderer.png', $c->getPortraitPath());
}
public function testMetaLineCombinesRaceClassAndLevel(): void
{
self::assertSame('Halfling, stout Ranger 2', $this->darragh()->getMetaLine());
self::assertSame('', (new Character())->getMetaLine());
self::assertSame('Halfling', (new Character())->setRace('Halfling')->getMetaLine());
}
}