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.
@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;
}
}