Add family tree page, linked from the NPC's family name
- /npcs/family/{family} (app_npc_family) renders a parent->child tree for
everyone sharing that family name; co-parents who are both in the family
are paired into one couple node, elders first
- FamilyTreeBuilder does the flat-list -> tree assembly
- the NPC detail page's family name (subtitle + Details row) now links there
- .family-tree CSS: nested list with connector lines, small portrait avatars
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Controller;
|
||||
use App\Entity\Npc;
|
||||
use App\Form\NpcGameNotesType;
|
||||
use App\Form\NpcType;
|
||||
use App\Npc\FamilyTreeBuilder;
|
||||
use App\Repository\NpcRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -66,6 +67,24 @@ class NpcController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Family tree for one family name.
|
||||
*/
|
||||
#[Route('/npcs/family/{family}', name: 'app_npc_family', requirements: ['family' => '[^/]+'], methods: ['GET'])]
|
||||
public function family(string $family, NpcRepository $repo, FamilyTreeBuilder $trees): Response
|
||||
{
|
||||
$members = $repo->findBy(['family' => $family], ['name' => 'ASC']);
|
||||
if (!$members) {
|
||||
throw $this->createNotFoundException(sprintf('No NPCs in family "%s".', $family));
|
||||
}
|
||||
|
||||
return $this->render('npc/family.html.twig', [
|
||||
'family' => $family,
|
||||
'count' => \count($members),
|
||||
'roots' => $trees->build($members),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* In-play detail page: everything read-only, plus the game-notes field.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Npc;
|
||||
|
||||
use App\Entity\Npc;
|
||||
|
||||
/**
|
||||
* Turns a flat list of NPCs (one family) into a parent -> child tree,
|
||||
* pairing co-parents who are both in the family into one couple node.
|
||||
*
|
||||
* A node is: array{npc: Npc, spouse: Npc|null, children: list<node>}
|
||||
*/
|
||||
final class FamilyTreeBuilder
|
||||
{
|
||||
/**
|
||||
* @param list<Npc> $members
|
||||
*
|
||||
* @return list<array{npc: Npc, spouse: Npc|null, children: list<array>}>
|
||||
*/
|
||||
public function build(array $members): array
|
||||
{
|
||||
/** @var array<int, Npc> $byId */
|
||||
$byId = [];
|
||||
foreach ($members as $m) {
|
||||
$byId[$m->getId()] = $m;
|
||||
}
|
||||
$inFamily = static fn (?Npc $n): bool => null !== $n && isset($byId[$n->getId()]);
|
||||
|
||||
// children under every in-family parent; couples via shared children
|
||||
$childrenOf = [];
|
||||
$spouseOf = [];
|
||||
foreach ($members as $m) {
|
||||
$f = $m->getFather();
|
||||
$mo = $m->getMother();
|
||||
foreach ([$f, $mo] as $p) {
|
||||
if ($inFamily($p)) {
|
||||
$childrenOf[$p->getId()][$m->getId()] = $m;
|
||||
}
|
||||
}
|
||||
if ($inFamily($f) && $inFamily($mo)) {
|
||||
$spouseOf[$f->getId()] = $mo;
|
||||
$spouseOf[$mo->getId()] = $f;
|
||||
}
|
||||
}
|
||||
|
||||
$done = []; // ids emitted anywhere in the tree
|
||||
$build = function (Npc $npc) use (&$build, $childrenOf, $spouseOf, $byId, &$done): array {
|
||||
$done[$npc->getId()] = true;
|
||||
$spouse = $spouseOf[$npc->getId()] ?? null;
|
||||
if ($spouse) {
|
||||
$done[$spouse->getId()] = true;
|
||||
}
|
||||
|
||||
$kids = ($childrenOf[$npc->getId()] ?? []) + ($spouse ? ($childrenOf[$spouse->getId()] ?? []) : []);
|
||||
uasort($kids, static fn (Npc $a, Npc $b) => ($b->getAge() ?? 0) <=> ($a->getAge() ?? 0));
|
||||
|
||||
$children = [];
|
||||
foreach ($kids as $kid) {
|
||||
if (!isset($done[$kid->getId()])) {
|
||||
$children[] = $build($kid);
|
||||
}
|
||||
}
|
||||
|
||||
return ['npc' => $npc, 'spouse' => $spouse, 'children' => $children];
|
||||
};
|
||||
|
||||
// Elders first: members with no in-family parent, oldest first.
|
||||
usort($members, static fn (Npc $a, Npc $b) => ($b->getAge() ?? 0) <=> ($a->getAge() ?? 0));
|
||||
|
||||
$roots = [];
|
||||
foreach ($members as $m) {
|
||||
if (isset($done[$m->getId()])) {
|
||||
continue;
|
||||
}
|
||||
if ($inFamily($m->getFather()) || $inFamily($m->getMother())) {
|
||||
continue;
|
||||
}
|
||||
$roots[] = $build($m);
|
||||
}
|
||||
// Anyone still unplaced (odd data): show as their own root.
|
||||
foreach ($members as $m) {
|
||||
if (!isset($done[$m->getId()])) {
|
||||
$roots[] = $build($m);
|
||||
}
|
||||
}
|
||||
|
||||
return $roots;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user