The party sheets in assets/characters/ changed: Darrah_Oakwanderer.pdf is gone, replaced by Darragh Fermwyr (same halfling ranger, renamed and updated), Owayn Fermwyr (stout halfling fighter, BJ) and Dunnchadh (ghostwise halfling druid, Circle of the Blighted, Rick). - app:character:seed now carries those three sheets, read from the PDFs' form fields. Darragh's portrait is renamed to match his new name. - New skill expertise field (double proficiency bonus), since Darragh's Deft Explorer gives him +7 Stealth and the sheet had nowhere to show it. Expertise implies proficiency; the sheet marks it with two dots, the edit form gets a checkbox list next to skill proficiencies. - Migration adds skill_expertise, backfilling existing rows with [] so the NOT NULL JSON column works on tables that already have characters. Tests: expertise math on the entity, and a catalog test that every seed sheet hydrates into a Character that passes validation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
3.0 KiB
PHP
89 lines
3.0 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 testExpertiseDoublesProficiencyBonus(): void
|
|
{
|
|
$c = $this->darragh()->setSkillExpertise(['Stealth']);
|
|
|
|
// Dex 17 -> +3, expertise (2 x +2 prof) -> +7
|
|
self::assertSame(7, $c->skillModifier('Stealth'));
|
|
// Sleight of Hand stays plain proficient -> +5
|
|
self::assertSame(5, $c->skillModifier('Sleight of Hand'));
|
|
|
|
// Expertise alone implies proficiency
|
|
$c->setSkillProficiencies([])->setSkillExpertise(['Perception']);
|
|
self::assertSame(5, $c->skillModifier('Perception')); // Wis +1, 2 x +2
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|