Storyline: chapter structure + formatted Campaign Overview

The Storyline section is now a set of hand-written chapters. /storyline
lists them; /storyline/{slug} renders one inside a shell with a sticky
chapter nav and prev/next pager. Chapters are declared in
StorylineController::CHAPTERS, each pointing at a Twig partial under
templates/storyline/chapters/.

First chapter is the Brackenford Campaign Overview: the campaign-bible
text laid out as a long-form reading page -- numbered section headers,
a "by the numbers" / "the cast" quick-reference strip, pull-quotes for
in-world lines, GM callouts for the ticking clocks and the red herring,
and the escalation ladder. Storyline styles added to app.css.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Frank
2026-09-08 17:28:21 +02:00
co-authored by Claude Sonnet 5
parent 373fa6f78b
commit 7fbf6f1569
5 changed files with 579 additions and 7 deletions
+39 -2
View File
@@ -10,9 +10,46 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class StorylineController extends AbstractController
{
#[Route('/storyline', name: 'app_storyline')]
/**
* The storyline is a set of hand-written chapters. Each entry points at a
* Twig partial under templates/storyline/chapters/. Add a chapter by adding
* a row here and the matching template; order in this array is reading order.
*
* @var array<string, array{title: string, summary: string, template: string}>
*/
private const CHAPTERS = [
'overview' => [
'title' => 'Campaign Overview',
'summary' => 'The whole shape of the Brackenford campaign, from the sealing of Kyuss four centuries ago to the final crisis — the truth behind the blood, the red herring around Sir Aldren, and how it can resolve.',
'template' => 'storyline/chapters/overview.html.twig',
],
];
#[Route('/storyline', name: 'app_storyline', methods: ['GET'])]
public function index(): Response
{
return $this->render('storyline/index.html.twig');
return $this->render('storyline/index.html.twig', [
'chapters' => self::CHAPTERS,
]);
}
#[Route('/storyline/{slug}', name: 'app_storyline_chapter', requirements: ['slug' => '[a-z0-9-]+'], methods: ['GET'])]
public function chapter(string $slug): Response
{
if (!isset(self::CHAPTERS[$slug])) {
throw $this->createNotFoundException('No such storyline chapter.');
}
$slugs = array_keys(self::CHAPTERS);
$pos = array_search($slug, $slugs, true);
return $this->render('storyline/chapter.html.twig', [
'chapters' => self::CHAPTERS,
'slug' => $slug,
'number' => $pos + 1,
'chapter' => self::CHAPTERS[$slug],
'prev' => $pos > 0 ? $slugs[$pos - 1] : null,
'next' => $pos < \count($slugs) - 1 ? $slugs[$pos + 1] : null,
]);
}
}