UI Razor component that renders the game's Info Booklet page in the lobby. It reads BookletState to build the left navigation (books, pages, entry list) and the right detailed sheet, handles hover/click actions that call BookletState and NZSound, and computes a small BuildHash from Showing and Version.
@using Sandbox;
@using Sandbox.UI;
@using System.Linq;
@using NZombies;
@inherits Panel
@*
INFO BOOKLET — a fullscreen page over the lobby, laid out like the Difficulty page and Map select (2026-10-05). The user: "i want
to build a new item in the menu called info booklet … the idea is to catalogue the game content so new players understand it".
Left: the title, the two books as tabs (Perks, Arsenal), the Arsenal's four pages as a second row of tabs, a line saying what the
book or page is, then its entries: every perk; the armor tiers; the rarities; the ammo mods; the weapon tech sets under their
four kinds. Right: the entry clicked. A perk's description and its majors and minors; a tier beside the others; a mod's trigger
and upgrades; a tech set's nodes by tier.
⛔ EVERY WORD AND NUMBER COMES FROM THE GAME'S CATALOGUES (`BookletState`), never from this file.
⚠️ BUILT ONLY WHILE OPEN, ONCE PER REDRAW: the entries and the sheet are taken at the top and read below, and the hash is
`Showing` and `Version`, two plain fields.
⚠️ A child Panel of the lobby, as Map select and the Difficulty page are: no GameObject, no scene edit.
*@
@if ( BookletState.Showing )
{
var entries = BookletState.Entries;
var chosen = BookletState.ChosenId( entries );
var sheet = BookletState.SheetFor( chosen );
<div class="bkpage">
<div class="bkbody">
<div class="bkleft">
<div class="bktitle">Info booklet</div>
<div class="bksub">What everything in the game does</div>
<div class="bktabs">
@foreach ( var b in BookletState.Books )
{
var book = b;
<div class="bktab @(BookletState.Current == book ? "on" : "")"
onmouseover=@Hover
onclick=@( () => PickBook( book ) )>@BookletState.NameOf( book )</div>
}
</div>
@if ( BookletState.Current == BookletState.Book.Arsenal )
{
<div class="bktabs bkpages">
@foreach ( var pg in BookletState.Pages )
{
var page = pg;
<div class="bktab @(BookletState.ArsenalPage == page ? "on" : "")"
onmouseover=@Hover
onclick=@( () => PickPage( page ) )>@BookletState.NameOf( page )</div>
}
</div>
}
<div class="bkhint">@BookletState.Hint</div>
@* ⛔ THE LIST SCROLLS, ITS ROWS DO NOT SHRINK (the scss) *@
<div class="bklist">
@foreach ( var en in entries )
{
var e = en;
if ( e.Heading )
{
<div class="bkheading">@e.Label</div>
}
else
{
<div class="bkrow @(e.Id == chosen ? "on" : "") @(e.Later ? "later" : "")"
onmouseover=@Hover
onclick=@( () => Pick( e.Id ) )>
@if ( !string.IsNullOrEmpty( e.Icon ) )
{
<img class="bkicon" src="@e.Icon" />
}
else if ( !string.IsNullOrEmpty( e.Colour ) )
{
<div class="bkswatch" style="background-color: @e.Colour;"></div>
}
<div class="bklabel">@e.Label</div>
@if ( !string.IsNullOrEmpty( e.Tag ) )
{
<div class="bktag">@e.Tag</div>
}
</div>
}
}
</div>
<div class="bkfoot">
<div class="bkbtn" onmouseover=@Hover onclick=@( () => Back() )>‹ Back</div>
</div>
</div>
@* ⚠️ THE RIGHT SIDE SCROLLS TOO: a perk's nine augments, or a class's tech across five tiers, are longer than the screen *@
<div class="bkright">
@if ( sheet is not null )
{
<div class="bkhead">
@if ( !string.IsNullOrEmpty( sheet.Icon ) )
{
<img class="bkbig" src="@sheet.Icon" />
}
<div class="bkheadtext">
<div class="bkname" style="@Tint( sheet.Colour )">@sheet.Name</div>
<div class="bkkicker">@sheet.Kicker</div>
</div>
</div>
<div class="bkdesc">@sheet.Text</div>
@if ( sheet.Cards.Length > 0 )
{
<div class="bkcards">
@foreach ( var cd in sheet.Cards )
{
var c = cd;
<div class="bkcard @(c.Id == sheet.CardOn ? "on" : "")"
onmouseover=@Hover
onclick=@( () => Pick( c.Id ) )>
<div class="bkcardname" style="@Tint( c.Colour )">@c.Name</div>
<div class="bkcardbig">@c.Big</div>
<div class="bkcardline">@c.Line</div>
@if ( !string.IsNullOrEmpty( c.Colour ) )
{
<div class="bkband" style="background-color: @c.Colour;"></div>
}
</div>
}
</div>
}
@foreach ( var sec in sheet.Sections )
{
var s = sec;
<div class="bksection">@s.Title</div>
if ( !string.IsNullOrEmpty( s.Note ) )
{
<div class="bksectionnote">@s.Note</div>
}
foreach ( var it in s.Items )
{
var i = it;
<div class="bkitem @(i.Later ? "later" : "")">
@if ( !string.IsNullOrEmpty( i.Chip ) )
{
<div class="bkchip">@i.Chip</div>
}
<div class="bkitemtext">
<div class="bkitemname">@i.Name</div>
<div class="bkitemdesc">@i.Text</div>
</div>
@if ( !string.IsNullOrEmpty( i.Cost ) )
{
<div class="bkitemcost">@i.Cost</div>
}
</div>
}
}
}
</div>
</div>
</div>
}
@code
{
/// <summary>A colour for a name, or nothing: a rarity's name and card are drawn in its own colour (`Rarity.HexFor`).</summary>
static string Tint( string hex ) => string.IsNullOrEmpty( hex ) ? "" : "color: " + hex + ";";
static void Hover() => NZSound.PlayUi( NZSound.UiHover );
static void PickBook( BookletState.Book book )
{
if ( BookletState.Current == book ) return;
NZSound.PlayUi( NZSound.UiClick );
BookletState.ChooseBook( book );
}
static void PickPage( BookletState.Page page )
{
if ( BookletState.ArsenalPage == page ) return;
NZSound.PlayUi( NZSound.UiClick );
BookletState.ChoosePage( page );
}
static void Pick( string id )
{
NZSound.PlayUi( NZSound.UiClick );
BookletState.Choose( id );
}
static void Back()
{
NZSound.PlayUi( NZSound.UiClick );
BookletState.Close();
}
// ⚠️ TWO PLAIN FIELDS: whether it is open, and the click counter. Nothing computed (the lobby's rule, and the page's).
protected override int BuildHash()
=> System.HashCode.Combine( BookletState.Showing, BookletState.Version );
}