Game/2D/UI/MainMenuTutorial.razor
@using System;
@using Sandbox;
@using Sandbox.Game.Menu
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox
<root>
@if(_active)
{
@* ── Debug card ── *@
@if(_debugMode)
{
<div class="tut-debug-card">
<div class="tut-debug-title">MM TUT — Step @_step</div>
<div class="tut-debug-row">
<span>BL: @_debugBL.ToString("F1")%</span>
<button class="tut-debug-btn" onclick=@(() => { _debugBL -= 1f; StateHasChanged(); })>◀</button>
<button class="tut-debug-btn" onclick=@(() => { _debugBL += 1f; StateHasChanged(); })>▶</button>
</div>
<div class="tut-debug-row">
<span>BY: @_debugBY.ToString("F0")px</span>
<button class="tut-debug-btn" onclick=@(() => { _debugBY -= 10f; StateHasChanged(); })>▲</button>
<button class="tut-debug-btn" onclick=@(() => { _debugBY += 10f; StateHasChanged(); })>▼</button>
</div>
<div class="tut-debug-row">
<span>BR: @_debugBR.ToString("F1")%</span>
<button class="tut-debug-btn" onclick=@(() => { _debugBR -= 1f; StateHasChanged(); })>◀</button>
<button class="tut-debug-btn" onclick=@(() => { _debugBR += 1f; StateHasChanged(); })>▶</button>
</div>
<div class="tut-debug-row">
<span>HX: @_debugHX.ToString("F1")%</span>
<button class="tut-debug-btn" onclick=@(() => { _debugHX -= 1f; StateHasChanged(); })>◀</button>
<button class="tut-debug-btn" onclick=@(() => { _debugHX += 1f; StateHasChanged(); })>▶</button>
</div>
<div class="tut-debug-row">
<span>HY: @_debugHY.ToString("F0")px</span>
<button class="tut-debug-btn" onclick=@(() => { _debugHY -= 10f; StateHasChanged(); })>▲</button>
<button class="tut-debug-btn" onclick=@(() => { _debugHY += 10f; StateHasChanged(); })>▼</button>
</div>
<div class="tut-debug-row">
<span>HW: @_debugHW.ToString("F0")px</span>
<button class="tut-debug-btn" onclick=@(() => { _debugHW -= 10f; StateHasChanged(); })>◀</button>
<button class="tut-debug-btn" onclick=@(() => { _debugHW += 10f; StateHasChanged(); })>▶</button>
</div>
<div class="tut-debug-row">
<span>HH: @_debugHH.ToString("F0")px</span>
<button class="tut-debug-btn" onclick=@(() => { _debugHH -= 10f; StateHasChanged(); })>▲</button>
<button class="tut-debug-btn" onclick=@(() => { _debugHH += 10f; StateHasChanged(); })>▼</button>
</div>
<button class="tut-debug-log" onclick=@LogCurrentValues>📋 Log</button>
</div>
}
@* ── Dim layer ── *@
<div class="tut-dim"></div>
@* ── Highlight ── *@
@if(_step < _steps.Length && _steps[_step].HW > 0)
{
var s = _steps[_step];
float hx = _debugHX != 0f ? _debugHX : s.HX;
float hy = _debugHY != 0f ? _debugHY : s.HY;
float hw = _debugHW != 0f ? _debugHW : s.HW;
float hh = _debugHH != 0f ? _debugHH : s.HH;
<div class="tut-highlight"
style="left:@hx.ToString("F1")%;top:@hy.ToString("F0")px;width:@hw.ToString("F0")px;height:@hh.ToString("F0")px;">
</div>
}
@* ── Instruction box ── *@
@if(_step < _steps.Length)
{
var s = _steps[_step];
float bl = _debugBL != 0f ? _debugBL : s.BL;
float br = _debugBR != 0f ? _debugBR : s.BR;
float by = _debugBY != 0f ? _debugBY : s.BY;
float boxWidthPct = s.BW > 0f ? (s.BW / 1920f * 100f) : (380f / 1920f * 100f);
string posStyle = br != 0f
? $"left:{(100f - br - boxWidthPct).ToString("F1")}%;"
: $"left:{bl.ToString("F1")}%;";
string boxStyle = $"{posStyle}top:{by.ToString("F0")}px;{(s.BW > 0f ? $"width:{s.BW.ToString("F0")}px;" : "")}";
<div class="tut-box" style="@boxStyle">
<div class="tut-step-count">@(_step + 1) / @_steps.Length</div>
<div class="tut-title">@s.Title</div>
<div class="tut-text">@s.Text</div>
@* Action hint — shown when player must act *@
@if(s.Trigger == MMTutTrigger.HeartPurchased)
{
<div class="tut-action-hint">👆 @s.ActionHint</div>
}
@* Next button — shown on Any steps *@
@if(s.Trigger == MMTutTrigger.Any)
{
<button class="tut-btn-next" onclick=@NextStep>Got it →</button>
}
@* Final step — Play button *@
@if(s.Trigger == MMTutTrigger.Done)
{
<button class="tut-btn-next" onclick=@OnPlayAndComplete>▶ Let's play!</button>
}
</div>
}
}
</root>
@code {
public static MainMenuTutorial Instance { get; private set; }
public bool IsActive => _active;
bool _active = false;
public int Step => _step;
int _step = 0;
// ── Debug mode ──────────────────────────────────────────────────── //
const bool _debugMode = false; // set false before release
float _debugBL = 0f;
float _debugBR = 0f;
float _debugBY = 0f;
float _debugHX = 0f;
float _debugHY = 0f;
float _debugHW = 0f;
float _debugHH = 0f;
void LogCurrentValues()
{
Log.Info($"// MM Step {_step} — {_steps[_step].Title}");
Log.Info($"BL={_debugBL:F1}f, BY={_debugBY:F0}f, BR={_debugBR:F1}f,");
Log.Info($"HX={_debugHX:F1}f, HY={_debugHY:F0}f, HW={_debugHW:F0}f, HH={_debugHH:F0}f");
}
// ── Trigger types ────────────────────────────────────────────────── //
public enum MMTutTrigger
{
Any, // advances via Got it button
HeartPurchased, // advances when player buys Heart consumable
Done, // final step — shows Play button
}
// ── Step data ────────────────────────────────────────────────────── //
public struct MMTutStep
{
public string Title;
public string Text;
public string ActionHint;
public MMTutTrigger Trigger;
public float HX, HY, HW, HH; // highlight
public float BL, BR, BY, BW; // box position
}
// ── Steps ────────────────────────────────────────────────────────── //
// Positions are placeholder — tune with debug card in-game
static readonly MMTutStep[] _steps = new MMTutStep[]
{
// Step 1 — Welcome
new MMTutStep {
Title = "Welcome to Chain Reaction!",
Text = "Here's a quick tour of how the game works. It'll take 30 seconds.",
Trigger = MMTutTrigger.Any,
HW = 0,
BL = 47f, BY = 370f
},
// Step 2 — Play button
new MMTutStep {
Title = "Play runs to earn 💠 Fragments",
Text = "Fragments are the game's currency. The further you go each run, the more you earn.",
Trigger = MMTutTrigger.Any,
HW = 0,
BL = 47f, BY = 370f
},
// Step 3 — Shop → Modifiers
new MMTutStep {
Title = "The Shop — Modifiers 🎲",
Text = "Spend fragments to unlock Modifiers. These permanently strengthen every run you play. Unlock once, keep forever.",
Trigger = MMTutTrigger.Any,
HX = 40f, HY = 200f, HW = 110f, HH = 40f,
BR = 61f, BY = 120f
},
// Step 4 — Consumables explanation
new MMTutStep {
Title = "Consumables 🎁",
Text = "Consumables are single-use items you bring into a run. Buy them once, use them at any time during a run. A bar will appear in-game when you own one.",
Trigger = MMTutTrigger.Any,
HX = 46f, HY = 200f, HW = 130f, HH = 40f,
BR = 61f, BY = 110
},
// Step 5 — Buy Heart (action step)
new MMTutStep {
Title = "Get yourself a Heart ❤️",
Text = "We've added 100 💠 to your balance. Buy the Heart consumable — it gives you an extra life mid-run when you need it most.",
ActionHint = "Click the buy button on the Heart card",
Trigger = MMTutTrigger.HeartPurchased,
HX = 65f, HY = 860f, HW = 80f, HH = 70f,
BR = 7f, BY = 800f
},
// Step 6 — Pet tab
new MMTutStep {
Title = "Your secret package 📦",
Text = "Play 3 runs to unlock what's inside. This section is for cosmetic skins — coming soon.",
Trigger = MMTutTrigger.Any,
HX = 53f, HY = 360f, HW = 70f, HH = 40f,
BR = 50f, BY = 340f
},
// Step 7 — Bombs
new MMTutStep {
Title = "Unlock new Bombs 💣",
Text = "Different bomb types open up new chain reaction strategies. Mix and match to find your playstyle.",
Trigger = MMTutTrigger.Any,
HX = 57f, HY = 220f, HW = 90f, HH = 40f,
BR = 5f, BY = 130f
},
// Step 8 — Leaderboard
new MMTutStep {
Title = "Compete Globally 🏅",
Text = "The leaderboard tracks your best score, chain reactions, and pet level. How far can you go?",
Trigger = MMTutTrigger.Any,
HX = 75f, HY = 400f, HW = 10f, HH = 10f,
BR = 5f, BY = 160f
},
// Step 9 — Done
new MMTutStep {
Title = "You're ready. 💥",
Text = "Play runs → earn fragments → unlock things → become stronger. Good luck!",
Trigger = MMTutTrigger.Done,
BR = 20f, BY = 370f
},
};
// ── Lifecycle ────────────────────────────────────────────────────── //
protected override void OnStart()
{
Instance = this;
_active = false;
}
public void Activate(PlayerSave save)
{
//var save = ChainReactionGame.Save ?? PlayerSave.Load();
if (save?.MenuTutorialCompleted == true) return;
_step = save?.MenuTutorialStep ?? 0;
_active = true;
ResetDebug();
// If resuming at step 4 (Buy Heart) but heart already purchased, skip it
if (_step == 4 && (save?.GetConsumableCount(ConsumableId.Heart) ?? 0) > 0)
_step = 5;
// Step 3 requires shop/mods to be visible — navigate there
ApplySideEffect(_step);
StateHasChanged();
}
// ── Called from MainMenu when Heart is bought ────────────────────── //
public void OnHeartPurchased()
{
if (!_active || _step >= _steps.Length) return;
if (_steps[_step].Trigger != MMTutTrigger.HeartPurchased) return;
NextStep();
}
// ── Step advancement ─────────────────────────────────────────────── //
void NextStep()
{
_step++;
ResetDebug();
if (_step >= _steps.Length)
{
Complete();
return;
}
// Persist current step so we can resume after crash - save tuto step
var save = PlayerSave.Load();
if (save != null)
{
save.MenuTutorialStep = _step;
save.Save();
}
ApplySideEffect(_step);
Sound.Play("UI_Button");
StateHasChanged();
}
// ── Side effects — navigate tabs automatically ───────────────────── //
void ApplySideEffect(int step)
{
var mm = MainMenu.Instance;
if (mm == null) return;
switch(step)
{
case 2: // Show shop modifiers
mm.SetTabPublic("shop");
mm.SetShopTabPublic("mods");
// Grant frags on step before heart purchase
break;
case 3: // Show consumables
mm.SetTabPublic("shop");
mm.SetShopTabPublic("consumables");
break;
case 4: // Buy Heart — shop consumables + grant frags if not already granted
var save = ChainReactionGame.Save ?? PlayerSave.Load();
if (save != null && !save.TutorialHeartFragsGranted)
{
save.TotalFragments += 100;
save.TutorialHeartFragsGranted = true;
save.Save();
}
mm.SetTabPublic("shop");
mm.SetShopTabPublic("consumables");
mm.ReloadSave();
break;
case 5: // Pet tab
mm.SetTabPublic("shop");
mm.SetShopTabPublic("pets");
break;
case 6: // Bombs
mm.SetTabPublic("shop");
mm.SetShopTabPublic("bombs");
break;
case 7: // Leaderboard
mm.SetTabPublic("lb");
break;
case 8: // Done — no navigation needed
break;
}
}
void Skip() => Complete();
void OnPlayAndComplete()
{
Complete();
MainMenu.Instance?.StartGamePublic();
}
void Complete()
{
_active = false;
// Get save from MainMenu directly
var mm = MainMenu.Instance;
// just use ChainReactionGame.Save or load fresh
var save = PlayerSave.Load();
if (save != null)
{
save.MenuTutorialCompleted = true;
save.MenuTutorialStep = 0;
save.Save();
}
StateHasChanged();
}
void ResetDebug()
{
_debugBL = 0f; _debugBR = 0f; _debugBY = 0f;
_debugHX = 0f; _debugHY = 0f; _debugHW = 0f; _debugHH = 0f;
}
public void ForceReset()
{
_active = false;
_step = 0;
ResetDebug();
StateHasChanged();
}
protected override int BuildHash() =>
System.HashCode.Combine(_active, _step, _debugBL, _debugBY, _debugHX, _debugHY, _debugHW, _debugHH);
}