*/ private const CHAPTERS = [ 'overview' => [ 'title' => 'Campaign Overview', 'eyebrow' => 'Overview', 'tag' => 'Ov', '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', ], 'prologue-the-long-road' => [ 'title' => 'Prologue — The Long Road to Brackenford', 'eyebrow' => 'Prologue', 'tag' => 'Pr', 'summary' => "The courier job that carries the party to Brackenford: hired in Bellhaven to deliver Edric Vale's estate to his nephew Eamon, twenty days on the road, and one night in a guest cottage that won't let anyone sleep. Levels 1 to 2, about 2.5 hours.", 'template' => 'storyline/chapters/prologue-the-long-road.html.twig', ], 'session-1-outside-blood' => [ 'title' => 'Session 1 — Outside Blood', 'eyebrow' => 'Session 1', 'tag' => 'S1', 'summary' => "The party arrives in Brackenford two hours after Eamon Vale met Aldren. A warm welcome from the Thorn beekeepers, a family hearing of Edric's death, a signature nobody can get because the council has Eamon — and, late at night, something dripping from the collection cart. Level 2, about 2.5 hours.", 'template' => 'storyline/chapters/session-1-outside-blood.html.twig', ], ]; #[Route('/storyline', name: 'app_storyline', methods: ['GET'])] public function index(): Response { return $this->render('storyline/index.html.twig', [ 'chapters' => self::CHAPTERS, ]); } #[Route('/storyline/{slug}', name: 'app_storyline_chapter', requirements: ['slug' => '[a-z0-9-]+'], methods: ['GET', 'POST'])] public function chapter(string $slug, Request $request, ChapterNoteRepository $notes, EntityManagerInterface $em, ChapterOutline $outline): Response { if (!isset(self::CHAPTERS[$slug])) { throw $this->createNotFoundException('No such storyline chapter.'); } $note = $notes->findOrCreate($slug); $form = $this->createForm(ChapterNoteType::class, $note); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { if (null === $note->getId()) { $em->persist($note); } $em->flush(); $this->addFlash('success', 'Chapter notes saved.'); return $this->redirectToRoute('app_storyline_chapter', ['slug' => $slug]); } // Render every chapter once: the current one is shown (with anchors), // the rest only feed the sidebar's per-section jump lists. $sections = []; $body = ''; foreach (self::CHAPTERS as $s => $c) { $processed = $outline->process($this->renderView($c['template'])); $sections[$s] = $processed['sections']; if ($s === $slug) { $body = $processed['html']; } } $slugs = array_keys(self::CHAPTERS); $pos = array_search($slug, $slugs, true); return $this->render('storyline/chapter.html.twig', [ 'chapters' => self::CHAPTERS, 'slug' => $slug, 'chapter' => self::CHAPTERS[$slug], 'body' => $body, 'sections' => $sections, 'note' => $note, 'form' => $form, 'prev' => $pos > 0 ? $slugs[$pos - 1] : null, 'next' => $pos < \count($slugs) - 1 ? $slugs[$pos + 1] : null, ]); } }