Files
Project-M/Assets/_Project/Scripts/Client/UI/HowToPlayPanel.cs
T
kronic 77de740b63 LANTERN purge B7: rename CharacterId -> FrameKind (frame terminology)
Enum renamed in StatIds.cs (byte values unchanged - serialized definitions/saves
never re-mean); all call sites + doc mentions swept (ClassTraits, PlayerAuthoring,
CharacterStatsDefinition field type, menu/UI, tests). CharacterStatsDefinition SO
CLASS name kept (asset-binding risk; deferred per plan). 390 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 15:51:48 -07:00

136 lines
6.2 KiB
C#

using System;
using System.Collections.Generic;
using ProjectM.Simulation;
using UnityEngine;
using UnityEngine.UIElements;
namespace ProjectM.Client
{
/// <summary>
/// The replayable "How to Play" reference card (UI Toolkit), built on <see cref="MenuUi"/> like
/// <see cref="SettingsScreen"/> and reachable from BOTH the main menu and the in-game pause overlay. Tabbed,
/// one glanceable page each: Controls (for the chosen class), The Loop (the annotated win-condition diagram —
/// the single highest-value page, since the inverted goal "clear expeditions to win" is the #1 new-player
/// confusion), Build &amp; Economy, Threats, Win/Lose. Static + stateless: <see cref="Build"/> returns a fresh
/// full-screen panel each call; the caller owns its lifetime (RemoveFromHierarchy on close).
/// </summary>
public static class HowToPlayPanel
{
static readonly string[] Tabs = { "Controls", "The Loop", "Build & Economy", "Threats" };
public static VisualElement Build(Action onClose)
{
var root = MenuUi.FullScreenRoot(true);
var card = MenuUi.Card("HOW TO PLAY");
card.style.minWidth = 640;
card.style.maxWidth = 780;
root.Add(card);
var tabBar = new VisualElement();
tabBar.style.flexDirection = FlexDirection.Row;
tabBar.style.justifyContent = Justify.Center;
tabBar.style.marginBottom = 12;
card.Add(tabBar);
var content = new VisualElement();
content.style.minHeight = 230;
card.Add(content);
var tabButtons = new List<Button>();
void Show(int idx)
{
content.Clear();
BuildTab(content, idx);
for (int i = 0; i < tabButtons.Count; i++)
tabButtons[i].style.backgroundColor = i == idx
? new Color(0.16f, 0.34f, 0.42f, 1f)
: new Color(0.16f, 0.20f, 0.27f, 1f);
}
for (int i = 0; i < Tabs.Length; i++)
{
int idx = i;
var b = MenuUi.Button(Tabs[i], () => Show(idx));
b.style.height = 32; b.style.fontSize = 13;
b.style.marginLeft = 3; b.style.marginRight = 3;
b.style.flexGrow = 1;
tabButtons.Add(b);
tabBar.Add(b);
}
card.Add(MenuUi.Button("Back", () => onClose?.Invoke()));
Show(0);
return root;
}
static void BuildTab(VisualElement c, int idx)
{
switch (idx)
{
case 0: // Controls (chosen class)
bool ranger = WorldLauncher.SelectedClass == (byte)FrameKind.Ranger;
Head(c, ranger ? "CLASS: Ranger (ranged anchor)" : "CLASS: Warrior (melee anchor)");
Body(c, "Move — WASD / Left Stick");
Body(c, "Aim — Mouse cursor / Right Stick");
Body(c, "Attack — LMB / X (3-hit melee combo — the finisher hits hard)");
Body(c, ranger ? "Class ability — RMB / LT (aimed power shot)" : "Class ability — RMB / LT (cone slam with knockback)");
Body(c, "Dash — LShift / B (brief invulnerability — dodge THROUGH attacks)");
Body(c, "Build menu — Tab / Y");
Body(c, "Place / Cancel (in build) — LMB / RMB (A / B on gamepad)");
Body(c, "Deposit at base — G");
Body(c, "Ready up (launch a run) — T");
Body(c, "Boons / route map / shop — mouse click");
Body(c, "Inventory — I");
Body(c, "Pause — Esc");
break;
case 1: // The Loop
Head(c, "THE LOOP — descend, fight, bank");
Body(c, "1. BASE — pick your suit-frame, buy PREP buffs (this run only), spend Aether on PERMANENT upgrades.");
Body(c, "2. READY UP [T] — when everyone is ready, the party launches into a multi-room run together.");
Body(c, "3. RUN — clear each room, pick 1 of 3 BOONS (this run only), choose your path, mine the crystals.");
Body(c, "4. BOSS — fell it and the party returns with the haul. A wipe banks nothing but the depth record.");
break;
case 2: // Build & Economy
Head(c, "RESOURCES & BUILDING");
Body(c, "Ore — main currency. Attack the glowing crystals INSIDE runs and haul them home (they're scarce — spend well).");
Body(c, "Wall (Biomass) — a cheap barrier that blocks enemies.");
Body(c, "Aether — rare; fuels PERMANENT upgrades at the base between runs.");
Body(c, "Open Build with Tab (Y), pick a piece, click a green tile to place it.");
break;
default: // Threats
Head(c, "THREATS");
Body(c, "Runs escalate room by room — elites guard the deep paths, a boss guards the end.");
Body(c, "Enemy variety scales the deeper you push.");
Body(c, "What the light doesn't hold, the dark takes back.");
break;
}
}
static void Head(VisualElement c, string t)
{
var l = new Label(t);
l.style.color = MenuUi.Accent;
l.style.fontSize = 16;
l.style.unityFontStyleAndWeight = FontStyle.Bold;
l.style.marginBottom = 8;
l.style.whiteSpace = WhiteSpace.Normal;
var th = HudTheme.Get();
if (th != null) th.ApplyDisplay(l.style);
c.Add(l);
}
static void Body(VisualElement c, string t)
{
var l = new Label(t);
l.style.color = MenuUi.TextCol;
l.style.fontSize = 14;
l.style.marginBottom = 5;
l.style.whiteSpace = WhiteSpace.Normal;
var th = HudTheme.Get();
if (th != null) th.ApplyBody(l.style);
c.Add(l);
}
}
}