MainHud/KeyboardWarriorsHUD.razor

Razor UI component for the Keyboard Warriors HUD. Presents weapon/enchantment selection and the combat UI, reads game state from GameManager, calls manager methods for starting battles, processing turns, selecting weapons/enchantments, and renders logs, player/AI stats, and ASCII art.

NetworkingFile Access
@using Sandbox;
@using System;
@using Keyboard_Warriors_.Systems;
@using Keyboard_Warriors_.Models;
@using Keyboard_Warriors_.Managers;

@inherits PanelComponent

@if ( CurrentPhase == GamePhase.WeaponSelection )
{
    <div class="selection-menu-wrapper">
        <div class="selection-header">Choose Your Weapon</div>
        <div class="selection-grid">
            @foreach ( var weapon in Weapons )
            {
                <button class="selection-button" onclick="@(() => SelectWeapon(weapon))">
                    <span class="btn-title text-blue">@weapon.name.ToUpper()</span>
                    <span class="btn-desc">Equip this combat tool</span>
                </button>
            }
        </div>
        <div class="leaderboard-panel">
            <div class="leaderboard-header">
                <span class="leaderboard-icon">👑</span>
                <h2 class="leaderboard-title">PERSONAL ALL-TIME HIGHS</h2>
            </div>
        
            <div class="leaderboard-rows">
                <div class="leaderboard-row">
                    <span class="stat-name">✨ HIGHEST PRESTIGE</span>
                    <span class="stat-value gold-glow">RANK @(GetManager()?.MaxPrestige ?? 0)</span>
                </div>
            
                <div class="leaderboard-row">
                    <span class="stat-name">💰 MOST POINTS HELD</span>
                    <span class="stat-value cyan-glow">@(GetManager()?.MaxPoints ?? 0) PTS</span>
                </div>
            
                <div class="leaderboard-row">
                    <span class="stat-name">🔥 MAX WIN STREAK</span>
                    <span class="stat-value green-glow">@(GetManager()?.MaxWinStreak ?? 0) WINS</span>
                </div>
            
                <div class="leaderboard-row">
                    <span class="stat-name">💀 MAX LOSS STREAK</span>
                    <span class="stat-value red-glow">@(GetManager()?.MaxLossStreak ?? 0) LOSSES</span>
                </div>
            </div>
        </div>
    </div>
}
else if ( CurrentPhase == GamePhase.EnchantmentSelection )
{
    <div class="selection-menu-wrapper">
        <div class="selection-header">Select An Enchantment</div>
        <div class="selection-grid">
            @foreach ( var enchantment in Enchantments )
            {
                <button class="selection-button" onclick="@(() => SelectEnchantment(enchantment))">
                    <span class="btn-title text-red">@enchantment.ToUpper()</span>
                    <span class="btn-desc">Bind elemental energy</span>
                </button>
            }
        </div>
    </div>
}
else if ( CurrentPhase == GamePhase.Combat )
{
    <div class="keyboard-warriors">
        <div class="combat-layout-wrapper">
            
            <div class="game-header">
                <div class="prestige-panel">
                    <button class="prestige-btn @(GetManager()?.PlayerPoints >= GetManager()?.PointsRequiredForPrestige ? "ready-to-ascend" : "")" 
                            onclick="@(() => GetManager()?.TriggerPrestige())">
                        ⚡ PRESTIGE @(GetManager()?.CurrentPrestige ?? 0)
                    </button>

                    <div class="progression-tracker">
                        <div class="wallet-display">WALLET: <span class="wallet-pts">@(GetManager()?.PlayerPoints ?? 0) PTS</span></div>
                        
                        @if ((GetManager()?.PlayerPoints ?? 0) >= (GetManager()?.PointsRequiredForPrestige ?? 1000))
                        {
                            <div class="ascend-alert">READY TO ASCEND!</div>
                        }
                        else
                        {
                            var needed = (GetManager()?.PointsRequiredForPrestige ?? 1000) - (GetManager()?.PlayerPoints ?? 0);
                            <div class="requirement-alert">REQ: <span class="req-weight">@needed more PTS to advance</span></div>
                        }
                    </div>
                </div>

                <pre class="header-ascii player-weapon-art">@GetWeaponArt(GetManager()?.LocalPlayer?.equippedWeapon?.name)</pre>
                
                <div class="header-title-center-zone">
                    <pre class="header-ascii">
                 _                         _ __    __                _                
  /\ /\___ _   _| |__   ___   __ _ _ __ __| / / /\ \ \__ _ _ __ _ __(_) ___  _ __ ___ 
 / //_/ _ \ | | | '_ \ / _ \ / _` | '__/ _` \ \/  \/ / _` | '__| '__| |/ _ \| '__/ __|
/ __ \  __/ |_| | |_) | (_) | (_| | | | (_| |\  /\  / (_| | |  | |  | | (_) | |  \__ \
\/  \/\___|\__, |_.__/ \___/ \__,_|_|  \__,_| \/  \/ \__,_|_|  |_|  |_|\___/|_|  |___/
           |___/
                    </pre>
                </div>

                <pre class="header-ascii ai-weapon-art">@GetWeaponArt(GetManager()?.AIEnemy?.equippedWeapon?.name)</pre>
            </div>
            
            <div class="stats-container">
                <div class="fighter-card player">
                    <img class="avatar-placeholder player-side" src="avatar:@(Connection.Local?.SteamId ?? 0)" />
                    <div class="fighter-info">
                        <span class="name player-name">@(Connection.Local?.Name?.ToUpper() ?? "PLAYER")</span>
                        
                        <div class="stat-bar-row">
                            <span class="stat-lbl">HP</span>
                            <div class="hp-bar" style="width: @((GetManager()?.LocalPlayer?.hp * 100.0f / GetManager()?.LocalPlayer?.maxHp) ?? 0)%;"></div>
                            <span class="stat-val">@(GetManager()?.LocalPlayer?.hp ?? 0) / @(GetManager()?.LocalPlayer?.maxHp ?? 1)</span>
                        </div>
                        
                        <div class="stat-bar-row">
                            <span class="stat-lbl">ARMOR</span>
                            <div class="armor-bar" style="width: @((GetManager()?.LocalPlayer?.defense * 100.0f / GetManager()?.LocalPlayer?.maxDefense) ?? 0)%;"></div>
                            <span class="stat-val">@(GetManager()?.LocalPlayer?.defense ?? 0) / @(GetManager()?.LocalPlayer?.maxDefense ?? 1)</span>
                        </div>
                        
                        <div class="stat-bar-row">
                            <span class="stat-lbl">MP</span>
                            <div class="bar"><div class="mp-bar" style="width: @(GetManager()?.LocalPlayer?.magic ?? 0)%;"></div></div>
                            <span class="stat-val">@(GetManager()?.LocalPlayer?.magic ?? 0) / 100</span>
                        </div>
                    </div>
                </div>

                <div class="vs-badge">VS</div>

                <div class="fighter-card ai">
                    <div class="fighter-info">
                        <span class="name ai-name">@(GetManager()?.AIEnemy?.name ?? "AI OVERLORD")</span>
                        
                        <div class="stat-bar-row">
                            <span class="stat-lbl">HP</span>
                            <div class="hp-bar" style="width: @((GetManager()?.AIEnemy?.hp * 100.0f / GetManager()?.AIEnemy?.maxHp) ?? 0)%;"></div>
                            <span class="stat-val">@(GetManager()?.AIEnemy?.hp ?? 0) / @(GetManager()?.AIEnemy?.maxHp ?? 1)</span>
                        </div>
                        
                        <div class="stat-bar-row">
                            <span class="stat-lbl">ARMOR</span>
                            <div class="armor-bar" style="width: @((GetManager()?.AIEnemy?.defense * 100.0f / GetManager()?.AIEnemy?.maxDefense) ?? 0)%;"></div>
                            <span class="stat-val">@(GetManager()?.AIEnemy?.defense ?? 0) / @(GetManager()?.AIEnemy?.maxDefense ?? 1)</span>
                        </div>
                        
                        <div class="stat-bar-row">
                            <span class="stat-lbl">MP</span>
                            <div class="bar"><div class="mp-bar" style="width: @(GetManager()?.AIEnemy?.magic ?? 0)%;"></div></div>
                            <span class="stat-val">@(GetManager()?.AIEnemy?.magic ?? 0) / 100</span>
                        </div>
                    </div>
                    <div class="avatar-placeholder ai-side">👹</div>
                </div>
            </div>

            <div class="middle-layout-row">
                <div class="side-spell-panel player-spells">
                    <span class="panel-title text-blue">PLAYER LOADOUT</span>
                    <div class="weapon-status" style="border-bottom: 1px dashed #00d2ff; padding-bottom: 5px; margin-bottom: 10px; font-size: 11px; color: white;">
                        ⚔️ WEAPON: <span class="text-yellow" style="font-weight: bold;">@(GetManager()?.LocalPlayer?.equippedWeapon?.name ?? "None")</span>
                        <div>✨ ENCHANTMENT: <span class="highlight-red" style="font-weight: bold;">@(SelectedEnchantment ?? "None")</span></div>
                    </div>
                    <div class="spell-item offensive-glow">
                        <span class="spell-emoji">@GetSpellEmoji(GetManager()?.LocalPlayer?.offensiveSpells?.FirstOrDefault())</span> 
                        <span>@(GetManager()?.LocalPlayer?.offensiveSpells?.FirstOrDefault() ?? "Loading...")</span>
                    </div>
                    <div class="spell-item defensive-glow">
                        <span class="spell-emoji">@GetSpellEmoji(GetManager()?.LocalPlayer?.defensiveSpells?.FirstOrDefault())</span> 
                        <span>@(GetManager()?.LocalPlayer?.defensiveSpells?.FirstOrDefault() ?? "Loading...")</span>
                    </div>
                    <div class="spell-item healing-glow">
                        <span class="spell-emoji">@GetSpellEmoji(GetManager()?.LocalPlayer?.healingSpells?.FirstOrDefault())</span> 
                        <span>@(GetManager()?.LocalPlayer?.healingSpells?.FirstOrDefault() ?? "Loading...")</span>
                    </div>
                    <div class="spell-item ultimate-glow">
                        <span class="spell-emoji">@GetSpellEmoji(GetManager()?.LocalPlayer?.specialSpells?.FirstOrDefault())</span> 
                        <span>@(GetManager()?.LocalPlayer?.specialSpells?.FirstOrDefault() ?? "Loading...")</span>
                    </div>
                </div>

                <div class="log-container">
                    <div class="log-header">--- COMBAT LOG ---</div>
                    <div class="log-scroll-area" @ref="MyLogPanel">
                        <div class="log-entry system text-yellow">Welcome to Keyboard Warriors!</div>
                        @if ( GetManager()?.CombatLogs != null )
                        {
                            @foreach ( var log in GetManager().CombatLogs )
                            {
                                <div class="log-entry">@log</div>
                            }
                        }
                        else
                        {
                            <div class="log-entry system text-muted">Synchronizing with CombatLogs...</div>
                        }
                    </div>
                </div>

                <div class="side-spell-panel ai-spells">
                    <span class="panel-title text-red">AI LOADOUT</span>
                    <div class="weapon-status" style="border-bottom: 1px dashed #ff5500; padding-bottom: 5px; margin-bottom: 10px; font-size: 11px; color: white;">
                        ⚔️ WEAPON: <span class="text-yellow" style="font-weight: bold;">@(GetManager()?.AIEnemy?.equippedWeapon?.name ?? "None")</span>
                        <div>ENCHANTMENT: <span class="highlight-red" style="font-weight: bold; text-shadow: 0 0 8px #ff4444;">@(GetManager()?.AIEnemy.ActiveEnchantment.ToString())</span></div>
                    </div>
                    <div class="spell-item offensive-glow">
                        <span class="spell-emoji">@GetSpellEmoji(GetManager()?.AIEnemy?.offensiveSpells?.FirstOrDefault())</span> 
                        <span>@(GetManager()?.AIEnemy?.offensiveSpells?.FirstOrDefault() ?? "Loading...")</span>
                    </div>
                    <div class="spell-item defensive-glow">
                        <span class="spell-emoji">@GetSpellEmoji(GetManager()?.AIEnemy?.defensiveSpells?.FirstOrDefault())</span> 
                        <span>@(GetManager()?.AIEnemy?.defensiveSpells?.FirstOrDefault() ?? "Loading...")</span>
                    </div>
                    <div class="spell-item healing-glow">
                        <span class="spell-emoji">@GetSpellEmoji(GetManager()?.AIEnemy?.healingSpells?.FirstOrDefault())</span> 
                        <span>@(GetManager()?.AIEnemy?.healingSpells?.FirstOrDefault() ?? "Loading...")</span>
                    </div>
                    <div class="spell-item ultimate-glow">
                        <span class="spell-emoji">@GetSpellEmoji(GetManager()?.AIEnemy?.specialSpells?.FirstOrDefault())</span> 
                        <span>@(GetManager()?.AIEnemy?.specialSpells?.FirstOrDefault() ?? "Loading...")</span>
                    </div>
                </div>
            </div>

            <div class="actions-container">
                @if ( IsGameOver )
                {
                    <button class="btn rematch" onclick="@OnRematchPressed">Rematch</button>
                }
                else
                {
                    <div class="button-row">
                        <button class="combat-btn border-red" onclick="@(() => OnActionPressed("Attack"))">
                            <span class="btn-title text-red">ATTACK</span>
                            <span class="btn-desc">Deal damage to enemy</span>
                        </button>
                        <button class="combat-btn border-blue" onclick="@(() => OnActionPressed("Defend"))">
                            <span class="btn-title text-blue">DEFEND</span>
                            <span class="btn-desc">Reduce incoming damage</span>
                        </button>
                        <button class="combat-btn border-green" onclick="@(() => OnActionPressed("Heal"))">
                            <span class="btn-title text-green">HEAL</span>
                            <span class="btn-desc">Restore health</span>
                        </button>
                        <button class="combat-btn border-yellow" onclick="@(() => OnActionPressed("Special"))">
                            <span class="btn-title text-yellow">SPECIAL</span>
                            <span class="btn-desc">Use weapon special</span>
                        </button>
                    </div>

                    <div class="button-row">
                        <button class="combat-btn border-purple" onclick="@(() => OnActionPressed("Offensive Spell"))">
                            <span class="btn-title text-purple">OFFENSIVE SPELL</span>
                            <span class="btn-desc">Cast offensive spell</span>
                        </button>
                        <button class="combat-btn border-blue" onclick="@(() => OnActionPressed("Defensive Spell"))">
                            <span class="btn-title text-blue">DEFENSIVE SPELL</span>
                            <span class="btn-desc">Cast defensive spell</span>
                        </button>
                        <button class="combat-btn border-green" onclick="@(() => OnActionPressed("Healing Spell"))">
                            <span class="btn-title text-green">HEALING SPELL</span>
                            <span class="btn-desc">Cast healing spell</span>
                        </button>
                        <button class="combat-btn border-orange" onclick="@(() => OnActionPressed("Ultimate"))">
                            <span class="btn-title text-orange">ULTIMATE SPELL</span>
                            <span class="btn-desc">Cast ultimate spell</span>
                        </button>
                    </div>
                }
            </div>

            <div class="footer-hint">Press ESC to exit  |  Good luck, warrior!</div>
        </div>
    </div>
}

@code {
	public KeyboardWarriorsHUD()
    {
        var manager = GetManager();
        if (manager != null)
        {
            // Subscribe using a delegate
            manager.OnStateChanged += HandleStateChanged;
        }
    }

    private void HandleStateChanged()
    {
        StateHasChanged();
    }

    public enum GamePhase { WeaponSelection, EnchantmentSelection, Combat }
    public GamePhase CurrentPhase { get; set; } = GamePhase.WeaponSelection;

    public Weapon SelectedWeapon { get; set; }
    public string SelectedEnchantment { get; set; }

    // FIX: Kept the name 'Weapons' exactly as your markup expects, while using the Weapon object type
    private List<Weapon> Weapons = new() 
    { 
        new Weapon("Sword", 5, 1.0f, 5),
        new Weapon("Axe", 10, 0.8f, 2),
        new Weapon("Dagger", 3, 1.2f, 4),
        new Weapon("Shield", 0, 1.5f, 8)
    };

    private List<string> Enchantments = new() { "None", "Fire", "Poison", "Ice", "Sparks", "DamageHP", "Stun", "Bleed", "LifeSteal", "ShieldBreak" };

    private bool IsGameOver => GetManager() != null && !GetManager().IsBattleActive;

	public static string GetSpellEmoji(string spellName)
	{
		if (string.IsNullOrEmpty(spellName)) return "🔮";
		string lower = spellName.ToLower();

		if (lower.Contains("fireball")) return "🔥";
		if (lower.Contains("lightning")) return "⚡";
		if (lower.Contains("poison bolt")) return "🤢";
		
		if (lower.Contains("shield")) return "🛡️";
		if (lower.Contains("barrier")) return "🚧";
		if (lower.Contains("reflect")) return "🪞";
		if (lower.Contains("magic wall")) return "🧱";
		if (lower.Contains("fortify")) return "🏰";

		if (lower.Contains("heal")) return "💚";
		if (lower.Contains("regen")) return "⏳";
		if (lower.Contains("cleanse")) return "🧼";
		if (lower.Contains("divine light")) return "✨";
		if (lower.Contains("recovery pulse")) return "💗";

		if (lower.Contains("system overdrive")) return "🚀";
		if (lower.Contains("emp blast")) return "💥";
		if (lower.Contains("data corruption")) return "👾";

		return "🔮";
	}

    private GameManager GetManager()
    {
        if ( GameManager.Instance != null ) return GameManager.Instance;
        return Scene?.Components?.GetAll<GameManager>().FirstOrDefault();
    }

    protected override void OnStart()
    {
        if ( Panel != null )
        {
            Panel.AcceptsFocus = true;
            Panel.Style.PointerEvents = Sandbox.UI.PointerEvents.All;
        }
        StartNewGame();
    }

    void StartNewGame()
    {
        CurrentPhase = GamePhase.WeaponSelection;
        SelectedWeapon = null;
        SelectedEnchantment = string.Empty;

        var manager = GetManager();
        if ( manager != null )
        {
            manager.StartBattle();
        }
    }

    void SelectWeapon( Weapon weapon ) 
	{ 
		SelectedWeapon = weapon; 
    
		var manager = GetManager();
		if (manager != null)
			{
				// FIX: Assign it to ChosenWeapon so ProcessTurnSlice can see it!
				manager.ChosenWeapon = weapon;

				if (manager.LocalPlayer != null)
				{
					manager.LocalPlayer.equippedWeapon = weapon;
				}
			}

			GameManager.AddLog($"Equipped: {weapon.name}");
			CurrentPhase = GamePhase.EnchantmentSelection; 
	}
    
    void SelectEnchantment( string enchantment ) 
    { 
        SelectedEnchantment = enchantment; 
        
        // Find the active, live weapon reference inside the backend engine
        var manager = GetManager();
        var activeWeapon = manager?.ChosenWeapon ?? SelectedWeapon;

        if ( activeWeapon != null )
        {
            // Parse the clean UI text ("Fire", "Poison") straight into Weapon.EnchantmentType.Fire
            if ( Enum.TryParse<Weapon.EnchantmentType>( enchantment, true, out var parsedEnum ) )
            {
                activeWeapon.enchantment = parsedEnum;
                activeWeapon.enchantPower = 25; // Base probability rate used by your backend switch
            }
        }

        CurrentPhase = GamePhase.Combat; 
        
        if ( manager?.CombatLogs != null )
        {
            manager.CombatLogs.Add( $"⚔️ Combat initialized! Weapon: {activeWeapon?.name} ({SelectedEnchantment})" );
        }
    }

	private Sandbox.UI.Panel MyLogPanel { get; set; }

    private async void OnActionPressed( string action )
{
    var manager = GetManager();
    if ( manager == null ) return;

    // 1. Add the turn header to the log list immediately
    if ( manager.CombatLogs != null )
    {
        manager.CombatLogs.Add( $"--- [ TURN {manager.Round} : {action.ToUpper()} ] ---" );
    }

    // 2. Convert action strings to backend indices
    int moveIndex = action switch
    {
        "Attack"          => 1,
        "Defend"          => 2,
        "Heal"            => 3,
        "Special"         => 4,
        "Offensive Spell" => 5,
        "Defensive Spell" => 6,
        "Healing Spell"   => 7,
        "Ultimate"        => 8,
        _                 => 1
    };

    // 3. Process the backend combat loop simulation
    manager.ProcessTurnSlice( moveIndex );

    // 4. Tell Razor to update the UI tree elements
    StateHasChanged();

    // 5. PASS 1: Wait 30ms for s&box to generate the text strings, then snap down
    await System.Threading.Tasks.Task.Delay( 30 );
    if ( MyLogPanel != null )
    {
        MyLogPanel.TryScrollToBottom();
    }

    // 6. PASS 2: Wait another 30ms to catch layout changes (like game over banners popping in)
    await System.Threading.Tasks.Task.Delay( 30 );
    if ( MyLogPanel != null )
    {
        MyLogPanel.TryScrollToBottom();
    }
}
    
    private async void OnRematchPressed()
    {
        StartNewGame();
        StateHasChanged();
		await System.Threading.Tasks.Task.Delay( 50 );

		if ( MyLogPanel != null )
		{
			MyLogPanel.TryScrollToBottom();
		}
    }

	public static string GetWeaponArt( string weaponName )
	{
		
		if ( string.IsNullOrEmpty( weaponName ) ) return "";

		if ( weaponName.Equals( "Sword", StringComparison.OrdinalIgnoreCase ) )
		{
			return @"
     /\
    //\\
    ||||
    ||||
    ||||
    ||||
    ||||
    ||||
    ||||
    ||||
    ||||
    ||||
    ||||
    ||||
 _  ||||  _
/(  ||||  )\
\\\_[\/]_///
 \\_<()>_//
  \/[\\]\/
    [//]
    [\\]
    [//]
    [\\]
    [//]
    (__)
";
		}
		else if ( weaponName.Equals( "Axe", StringComparison.OrdinalIgnoreCase ) )
		{
			return @"
 //`-||-'\\\
(| -=||=- |)
 \\,-||-.//
  `  ||  '
     ||
     ||
     ||
     ||
     ||
     ()
";
		}

		else if ( weaponName.Equals( "Shield", StringComparison.OrdinalIgnoreCase ) )
		{
			return @"
  |`-._/\_.-`|
  |    ||    |
  |___o()o___|
  |__((<>))__|
  \   o\/o   /
   \   ||   /
    \  ||  /
     '.||.'
       ``
";
		}
		else if ( weaponName.Equals( "Dagger", StringComparison.OrdinalIgnoreCase ) )
		{
			return @"
       /\
      //\\
     //  \\
     \\  //    
      )  (
      )  (
      )  (
 ^    )  (    ^
/ \   )  (   / \
) (   )  (   ) (
\  \_/ /\ \_/  /
 \__  _)(_  __/
    \ \  / /
     ) \/ (
     | /\ |
     | )( |
     | )( |
     | \/ |
     )____(
    /      \
    \______/
";
		}

		return "";
	}

    protected override int BuildHash()
    {
        var manager = GetManager();

        int playerHash = System.HashCode.Combine(
            CurrentPhase,
            manager?.LocalPlayer?.hp,
            manager?.LocalPlayer?.defense,
            manager?.LocalPlayer?.magic,
            IsGameOver
        );

        int enemyAndLogHash = System.HashCode.Combine(
            manager?.AIEnemy?.hp,
            manager?.AIEnemy?.defense,
            manager?.AIEnemy?.magic,
            manager?.CombatLogs?.Count 
        );

        return playerHash + enemyAndLogHash;
    }
}