Game/2D/UI/MainMenuTutorial.razor

Main menu tutorial UI component (Razor) for the game. Renders an optional debug card, dim layer, highlight rectangles and instruction box for sequential tutorial steps, advances steps, persists progress to PlayerSave, and triggers MainMenu navigation and actions (start game, grant fragments).

File AccessNetworking
@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 10XPLOSION!",
            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);
}