Razor UI component for the main HUD of the Keyboard Warriors game. It renders selection menus, leaderboards, combat UI, tooltips, status badges, logs, and manages hover/selection state, periodic leaderboard refresh, simple animations and interactions with GameManager and ShopHUD.
@using Sandbox;
@using System;
@using Sandbox.Services;
@using System.Threading.Tasks;
@using Keyboard_Warriors_.Systems;
@using Keyboard_Warriors_.Models;
@using Keyboard_Warriors_.Managers;
@inherits PanelComponent
@attribute [Sandbox.UI.StyleSheet( "/ui/KeyboardWarriorsHUD.razor.scss" )]
<div class="animated-bg">
<!-- 🚀 Swapped units to vw and vh to bypass the integer-snapping limitation -->
<div class="bg-image-layer"
style="background-image: url('/@(Scene.GetAllComponents<GameManager>().FirstOrDefault()?.BackgroundImage)');
transform: translate( @(_currentX)vw, @(_currentY)vh ) scale( @_currentScale ) rotate( @(_currentRotation)deg );">
</div>
</div>
@if ( CurrentPhase == GamePhase.WeaponSelection )
{
<div class="selection-menu-wrapper">
<div class="selection-header">Choose Your Weapon</div>
<!-- Keep your grid completely separated from the leaderboard row -->
<div class="selection-grid">
@foreach ( var weapon in Weapons )
{
var localWeapon = weapon;
<button class="selection-button"
onclick="@(() => SelectWeapon(weapon))"
@onmouseover="@(() => TrackWeaponHover(weapon))"
@onmouseout="@(ClearHoverTelemetry)">
<span class="btn-title text-blue">@weapon.name.ToUpper()</span>
<span class="btn-desc">Equip this combat tool</span>
</button>
}
</div>
<!-- LEADERBOARDS ROW: Self-contained at the very bottom -->
<div class="leaderboard-panel-row">
<!-- Left Panel: Personal Highs -->
<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>
<!-- Right Panel: Global Cloud Rankings -->
<div class="leaderboard-panel global-leaderboard">
<div class="leaderboard-header">
<span class="leaderboard-icon">🌍</span>
<h2 class="leaderboard-title">GLOBAL PRESTIGE LEADERBOARD</h2>
</div>
<div class="leaderboard-rows scrollable-rows">
@if ( IsReadingGlobalBoard )
{
<div class="loading-text">Retrieving cloud rankings...</div>
}
else if ( GlobalPrestigeEntries != null && GlobalPrestigeEntries.Length > 0 )
{
@foreach ( var entry in GlobalPrestigeEntries )
{
bool isMe = entry.SteamId == Sandbox.Utility.Steam.SteamId;
<div class="leaderboard-row @(isMe ? "local-user-highlight" : "")">
<span class="stat-name">#@entry.Rank @entry.DisplayName.ToUpper()</span>
<span class="stat-value gold-glow">LV @entry.Value</span>
</div>
}
}
else
{
<!-- Displays an arcade style fallback string during local editor tests -->
<div class="loading-text">NO GLOBAL RECORDS FOUND [STANDBY]</div>
}
</div>
</div>
</div> <!-- Closes leaderboard-panel-row -->
</div> <!-- Closes selection-menu-wrapper -->
}
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))"
@onmouseover="@(() => TrackEnchantmentHover(enchantment))"
@onmouseout="@(ClearHoverTelemetry)">
<span class="btn-title text-gold">@GetEnchantName(enchantment).ToUpper()</span>
<span class="btn-desc">Solder code parameters</span>
</button>
}
</div>
<!-- DROP GLOBAL DATA DIRECTLY INTO THE ENCHANTMENT SCREEN TOO -->
<div class="leaderboard-panel-row">
<div class="leaderboard-panel global-leaderboard full-width-lb">
<div class="leaderboard-header">
<span class="leaderboard-icon">🌍</span>
<h2 class="leaderboard-title">GLOBAL PRESTIGE LEADERBOARD</h2>
</div>
<div class="leaderboard-rows scrollable-rows">
@if ( IsReadingGlobalBoard )
{
<div class="loading-text">Retrieving cloud rankings...</div>
}
else if ( GlobalPrestigeEntries != null )
{
@foreach ( var entry in GlobalPrestigeEntries )
{
// Fix: Compares the entry's SteamID to your local client profile safely
bool isMe = entry.SteamId == Sandbox.Utility.Steam.SteamId;
<div class="leaderboard-row @(isMe ? "local-user-highlight" : "")">
<span class="stat-name">#@entry.Rank @entry.DisplayName.ToUpper()</span>
<span class="stat-value gold-glow">LV @entry.Value</span>
</div>
}
}
</div>
</div>
</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>
<button class="shop-trigger-btn" onclick="@OpenShopMenu">
🛒 OPEN SHOP
</button>
</div>
<div class="stats-container">
<!-- ========================================== -->
<!-- 👤 PLAYER FIGHTER CARD -->
<!-- ========================================== -->
<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>
<!-- 📊 PLAYER ACTIVE STATUS MODIFIER BAR -->
<div class="status-effect-bar">
@{
var pManager = GetManager();
var playerObj = pManager?.LocalPlayer;
int pSessionStreak = CombatSession.WinStreak;
// 🌐 NEW DEFIANCE MATRIX VARIABLES (Update 1.5.8)
int currentPrestige = pManager != null ? pManager.CurrentPrestige : 0;
bool holdsBackupToken = pManager != null && !pManager.HasDefiedDeathThisMatch;
}
@if ( playerObj != null )
{
<div class="status-badge overclock @(playerObj.magic >= 75 ? "is-active" : "")">⚡ OVERCLOCK</div>
<div class="status-badge momentum @(pSessionStreak >= 3 ? "is-active" : "")">🔥 STREAK @pSessionStreak</div>
<div class="status-badge poison @(playerObj.cursed ? "is-active" : "")">🤢 POISONED</div>
<div class="status-badge event-buff @(playerObj.eventAttackBuff > 0 ? "is-active" : "")">🎲 +@playerObj.eventAttackBuff ATK</div>
<div class="status-badge weight-debuff @(playerObj.eventAttackBuff < 0 ? "is-active" : "")">📦 DEBUFF DRAG</div>
<!-- 🔒 PRESTIGE 3 TRACKER LAYER -->
@if ( currentPrestige >= 3 )
{
<!-- Glows radioactive emerald green when online, or fades to dark gray when consumed -->
<div class="status-badge defiance-core @(holdsBackupToken ? "is-active" : "")">
🔄 DEFIANCE @(holdsBackupToken ? "READY" : "OFFLINE")
</div>
}
<!-- 🔒 PRESTIGE 4 REQUIREMENT: Passive Counter-Strike Tracker -->
@if ( currentPrestige >= 4 )
{
<div class="status-badge malware-core is-active">👾 COUNTER-STRIKE</div>
}
<!-- 🔒 PRESTIGE 6 REQUIREMENT: Kinetic Rebound Shield Tracker -->
@if ( currentPrestige >= 6 )
{
<div class="status-badge rebound-core is-active">⚡ REBOUND ACTIVE</div>
}
@if ( currentPrestige >= 9 )
{
<div class="status-badge leak-core is-active">🔮 MAGIC STEAL</div>
}
}
</div>
</div>
</div>
<div class="vs-badge">VS</div>
<!-- ========================================== -->
<!-- 👹 AI OVERLORD FIGHTER CARD -->
<!-- ========================================== -->
<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>
<!-- 📊 AI ACTIVE STATUS MODIFIER BAR -->
<div class="status-effect-bar">
@{
var aManager = GetManager();
var aiObj = aManager?.AIEnemy;
int aiSessionStreak = CombatSession.LossStreak;
int playerPrestige = aManager != null ? aManager.CurrentPrestige : 0;
string currentStance = aManager?.AICurrentStance ?? "SECURE CONNECTION";
int hiddenAIAction = aManager?.AICurrentMoveIndex ?? 0;
bool isBreachSuccessful = aManager?.IsAIActionIntercepted ?? false;
// 🌐 AI DEFIANCE VARIABLE HANDSHAKE (Update 1.5.8)
bool holdsAIBackupToken = aManager != null && !aManager.HasAIDefiedDeathThisMatch;
}
@if ( aiObj != null )
{
<div class="status-badge overclock @(aiObj.magic >= 75 ? "is-active" : "")">⚡ OVERCLOCK</div>
<div class="status-badge momentum @(aiSessionStreak >= 3 ? "is-active" : "")">🔥 STREAK @aiSessionStreak</div>
<div class="status-badge poison @(aiObj.cursed ? "is-active" : "")">🤢 POISONED</div>
<div class="status-badge event-buff @(aiObj.eventAttackBuff > 0 ? "is-active" : "")">🎲 +@aiObj.eventAttackBuff ATK</div>
<!-- 🔒 AI PRESTIGE 3 TRACKER LAYER -->
@if ( playerPrestige >= 3 )
{
<!-- Reuses your exact defiance-core SCSS classes to keep your layout dimensions compact -->
<div class="status-badge defiance-core @(holdsAIBackupToken ? "is-active" : "")">
🔄 MAINFRAME @(holdsAIBackupToken ? "READY" : "OFFLINE")
</div>
}
@if ( playerPrestige >= 4 )
{
<div class="status-badge malware-core is-active">👾 MALWARE DETECT</div>
}
<!-- 🔒 AI PRESTIGE 6 REQUIREMENT: Kinetic Rebound Shield Tracker -->
@if ( playerPrestige >= 6 )
{
<div class="status-badge rebound-core is-active">⚡ REBOUND FIELD</div>
}
@if ( playerPrestige >= 1 )
{
<div class="scanner-text-line">
<span class="blink-dot">●</span> STANCE: <span class="stance-value">@currentStance</span>
<!-- 🚀 PRESTIGE 5 REWARD: Inline File Sniffer -->
@if ( playerPrestige >= 5 )
{
@if ( isBreachSuccessful )
{
<span class="action-value"> // PACKET: @(hiddenAIAction == 1 ? "ATTACK.EXE" :
hiddenAIAction == 2 ? "BLOCK.SYS" :
hiddenAIAction == 3 ? "REPAIR.BAT" :
hiddenAIAction == 4 ? "SPECIAL.EXE" :
hiddenAIAction == 5 ? "MANA.SYS" :
hiddenAIAction == 6 ? "FORTIFY.DRV" :
hiddenAIAction == 7 ? "REGEN.MSI" :
hiddenAIAction == 8 ? "PURGE.EXE" : "LOADING...")</span>
}
else
{
<span class="action-value blocked"> // PACKET: [ENCRYPTED]</span>
}
}
</div>
}
}
</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 )
{
<!-- Dynamically combines your base item styles with your custom color class tags -->
<div class="log-entry @log.ColorClass @log.AnimationClass">@log.Message</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>
}
<ShopHUD @ref="shopRef" />
@if ( GetManager() != null && GetManager().IsChoiceEventActive && GetManager().ActiveChoiceEvent != null )
{
var currentEvent = GetManager().ActiveChoiceEvent;
<div class="event-modal-backdrop">
<div class="event-choice-box">
<h2 class="event-title text-red">@currentEvent.Title</h2>
@if ( currentEvent.HasTimeLimit )
{
// Calculate current remaining percentage fraction (0.0 to 100.0)
float pct = (currentEvent.TimeRemaining / currentEvent.TotalDuration) * 100f;
<div class="timer-bar-wrapper">
<!-- Binding the width percentage directly into the style string updates it smoothly -->
<div class="timer-bar-fill" style="width: @(pct)%;"></div>
<span class="timer-text-overlay">FIREWALL CLOSING: @(((int)currentEvent.TimeRemaining) + 1)s</span>
</div>
}
<p class="event-prompt">@currentEvent.PromptText</p>
<!-- The flex container shifts layout automatically based on button counts -->
<div class="event-button-row">
@for ( int i = 0; i < currentEvent.Options.Count; i++ )
{
// Local variable capture is critical inside Razor loop callbacks!
int buttonIndex = i;
var currentButton = currentEvent.Options[i];
<button class="choice-option-btn dynamic-color-@buttonIndex"
onclick="@(() => GetManager().ResolveChoice(buttonIndex))">
@currentButton.ButtonText
</button>
}
</div>
</div>
</div>
}
@if ( ActiveHoverType != HoverType.None && Panel != null && (CurrentPhase == GamePhase.WeaponSelection || CurrentPhase == GamePhase.EnchantmentSelection) )
{
<div class="telemetry-tooltip-hud" style="left: @(Panel.MousePosition.x + 20)px; top: @(Panel.MousePosition.y + 15)px;">
@if ( ActiveHoverType == HoverType.Weapon && SelectedHoverWeapon != null )
{
<div class="tooltip-header text-blue">📊 WEAPON CORE TELEMETRY</div>
<!-- 🚀 THE DEFINITIVE DATA SYNC: Fallback properties check! -->
<!-- If enchantPower is 0 on the base template, we check your alternative damage fields automatically -->
@if ( SelectedHoverWeapon.enchantPower > 0 )
{
<div class="tooltip-row"><span class="label">💥 POWER RATING:</span> <span class="val">@SelectedHoverWeapon.enchantPower</span></div>
}
else
{
// 🛠️ Adjust this keyword to match your weapon class property name if needed (e.g., baseDamage, damage, power, attack)
<div class="tooltip-row"><span class="label">💥 BASE DAMAGE:</span> <span class="val">@SelectedHoverWeapon.damageBonus</span></div>
}
<div class="tooltip-row"><span class="label">❤️ HEAL BONUS:</span> <span class="val">@SelectedHoverWeapon.healBonus</span></div>
<div class="tooltip-divider"></div>
<div class="tooltip-sub">Asset: @SelectedHoverWeapon.name</div>
}
else if ( ActiveHoverType == HoverType.Enchantment )
{
<div class="tooltip-header text-gold">✨ CASING ANOMALY PROFILE</div>
<div class="tooltip-sub">@GetEnchantName(HoveredEnchantType.ToString())</div>
<div class="tooltip-body">@GetEnchantDescription(HoveredEnchantType.ToString())</div>
<div class="tooltip-divider"></div>
<div class="tooltip-row"><span class="label">⚡ TRIGGER ACCURACY:</span> <span class="val text-gold">@GetEnchantTriggerChance(HoveredEnchantType.ToString())</span></div>
}
</div>
}
@if ( GetManager() != null && GetManager().IsPerkEventActive && GetManager().ActivePerkChoices != null && GetManager().ActivePerkChoices.Count > 0 )
{
var currentPerkList = GetManager().ActivePerkChoices;
<div class="event-modal-backdrop">
<!-- Expanded width slightly to 700px to give your columns maximum elbow room -->
<div class="event-choice-box" style="width: 700px; max-width: 90vw; height: auto; display: flex; flex-direction: column; padding: 24px; box-sizing: border-box;">
<h2 class="event-title text-red" style="margin-bottom: 8px;">⚠️ MATRIX CORE UPGRADE</h2>
<p class="event-prompt" style="margin-bottom: 24px;">Select an offensive parameter injection routine. Modifications will scale permanently for both combatants.</p>
<!-- 🚀 THE COLUMNS FIX: We force the container wrapper to lay out horizontally across the card screen -->
<div class="perk-columns-container" style="display: flex; flex-direction: row; gap: 16px; width: 100%; justify-content: space-between; box-sizing: border-box;">
@for ( int i = 0; i < currentPerkList.Count; i++ )
{
int buttonIndex = i;
var currentPerk = currentPerkList[i];
<!-- 🚀 COLUMN CARDS: 'flex: 1;' forces s&box to make every option card equal width.
We use inline styling rules to force clean blocks and prevent font crunching. -->
<div class="choice-option-btn dynamic-color-@buttonIndex"
style="display: flex; flex-direction: column; align-items: stretch; text-align: center; padding: 18px 14px; min-height: 180px; gap: 8px; flex: 1; min-width: 0; pointer-events: all; cursor: pointer; box-sizing: border-box; background-color: #14141a;"
onclick="@(() => GetManager().ResolvePerkChoice(buttonIndex))">
<!-- Header Line: Emoji Icon + Title -->
<div style="font-weight: bold; font-size: 14px; display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%; text-transform: uppercase; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 6px;">
<span>@currentPerk.Icon</span>
<span class="text-blue">@currentPerk.Title</span>
</div>
<!-- Core Description Text (Perfect Paragraph Fluidity) -->
<div style="font-size: 11px; color: #b5b5c5; line-height: 1.4; width: 100%; white-space: normal !important; word-wrap: break-word !important; text-align: left; flex-grow: 1;">
@currentPerk.Description
</div>
<!-- Penalty Tag Info Row (Pushed down dynamically) -->
<div style="font-size: 10px; font-weight: bold; color: #ff0055; width: 100%; white-space: normal; text-align: left; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 4px; margin-top: auto;">
@currentPerk.DrawbackText
</div>
</div>
}
</div>
</div>
</div>
}
@code {
private enum HoverType { None, Weapon, Enchantment }
private HoverType ActiveHoverType = HoverType.None;
private Weapon SelectedHoverWeapon = null;
private Weapon.EnchantmentType HoveredEnchantType = Weapon.EnchantmentType.None;
private void TrackWeaponHover( Weapon targetWeapon )
{
// 🚀 THE PHASE GATE: Only allow the HUD to show if you are actively choosing a weapon!
if ( CurrentPhase != GamePhase.WeaponSelection )
{
ClearHoverTelemetry();
return;
}
SelectedHoverWeapon = targetWeapon;
ActiveHoverType = HoverType.Weapon;
StateHasChanged();
}
private void TrackEnchantmentHover( string enchantmentString )
{
// 🚀 THE PHASE GATE: Only allow the HUD to show if you are actively choosing an enchantment!
if ( CurrentPhase != GamePhase.EnchantmentSelection )
{
ClearHoverTelemetry();
return;
}
ActiveHoverType = HoverType.Enchantment;
if ( Enum.TryParse<Weapon.EnchantmentType>( enchantmentString, true, out var parsedType ) )
{
HoveredEnchantType = parsedType;
}
else
{
HoveredEnchantType = Weapon.EnchantmentType.None;
}
StateHasChanged();
}
private void ClearHoverTelemetry()
{
ActiveHoverType = HoverType.None;
SelectedHoverWeapon = null;
HoveredEnchantType = Weapon.EnchantmentType.None;
StateHasChanged();
}
// 🕊️ 1. Dynamic Casing Names
private string GetEnchantName( string enchantmentString )
{
if ( !Enum.TryParse<Weapon.EnchantmentType>( enchantmentString, true, out var type ) )
return "Null Kernel";
return type switch
{
Weapon.EnchantmentType.Fire => "Fire",
Weapon.EnchantmentType.Poison => "Poison Coating",
Weapon.EnchantmentType.Ice => "Ice",
Weapon.EnchantmentType.Sparks => "Sparks",
Weapon.EnchantmentType.DamageHP => "DamageHP",
Weapon.EnchantmentType.Stun => "Electrical Stun",
Weapon.EnchantmentType.LifeSteal => "Lifesteal Core",
Weapon.EnchantmentType.ShieldBreak => "ShieldBreak",
Weapon.EnchantmentType.None => "Null Kernel",
// 🩸 RESTORED: Given its true specialized name!
Weapon.EnchantmentType.Bleed => "Bleed Matrix",
_ => "Null Kernel"
};
}
private string GetEnchantDescription( string enchantmentString )
{
if ( !Enum.TryParse<Weapon.EnchantmentType>( enchantmentString, true, out var type ) )
return "Standard framework reinforcement module.";
return type switch
{
Weapon.EnchantmentType.Fire => "Ignites target for thermal damage. Deals 1.5x damage straight to armor shields.",
Weapon.EnchantmentType.Poison => "Injects permanent background toxin logic, dealing ticking damage over time.",
Weapon.EnchantmentType.Ice => "Freezes the core matrix, causing the AI's active defensive wall loops to fail completely.",
Weapon.EnchantmentType.Sparks => "Static current arcs backwards to recharge your local health framework grid.",
Weapon.EnchantmentType.DamageHP => "True kinetic puncture that completely bypasses armor grids to strike physical health.",
Weapon.EnchantmentType.Stun => "Overloads target processor threads, force-locking their client actions next turn.",
Weapon.EnchantmentType.LifeSteal => "Siphons health packets from weapon strikes, restoring your local grid stability.",
Weapon.EnchantmentType.ShieldBreak => "Catastrophic barrier rupture subroutine that triggers massive damage against active blocks.",
// 🩸 RESTORED: Direct code-logic description for bleeding wounds!
Weapon.EnchantmentType.Bleed => "Ruptures underlying system components on contact. Causes an ongoing hemorrhage loop that tears away physical health out-of-turn.",
Weapon.EnchantmentType.None => "Standard framework reinforcement module. No specialized status modifiers active.",
_ => "Standard framework reinforcement module."
};
}
private string GetEnchantTriggerChance( string enchantmentString )
{
if ( !Enum.TryParse<Weapon.EnchantmentType>( enchantmentString, true, out var type ) )
return "0%";
var manager = Sandbox.Game.ActiveScene.Get<GameManager>();
var currentWeaponContext = SelectedHoverWeapon ?? manager?.ChosenWeapon;
int powerValue = currentWeaponContext != null ? currentWeaponContext.enchantPower : 30;
return type switch
{
Weapon.EnchantmentType.Fire => "80%",
Weapon.EnchantmentType.Poison => "100%",
Weapon.EnchantmentType.Ice => manager?.LocalPlayer?.magic >= 75 ? "40%" : "25%",
Weapon.EnchantmentType.Sparks => "20%",
Weapon.EnchantmentType.DamageHP => "60%",
Weapon.EnchantmentType.Stun => "25%",
Weapon.EnchantmentType.LifeSteal => "100%",
Weapon.EnchantmentType.ShieldBreak => (manager != null && manager.WinStreak >= 3) ? $"{powerValue + 20}%" : $"{powerValue}%",
// 🩸 RESTORED: Scaled perfectly to match your weapon power rating criteria!
Weapon.EnchantmentType.Bleed => "80%",
Weapon.EnchantmentType.None => "0%",
_ => "0%"
};
}
private float _time = 0f;
private float _currentX = 0f;
private float _currentY = 0f;
private float _currentScale = 1.05f;
private float _currentRotation = 0f;
protected override void OnTreeBuilt()
{
base.OnTreeBuilt();
// 🚀 ACTIVATE RUNTIME OVERRIDE
// 1. Ensure the mouse cursor is configured to display normally
Mouse.Visibility = MouseVisibility.Auto;
// 2. Pass your registered Project Settings string key name
Mouse.CursorType = "KW Cursor";
}
protected override void OnUpdate()
{
_time += RealTime.Delta;
// 1. Calculate ultra-slow paths using viewport percentages instead of raw pixels
// 🐢 Shifting by 1.5vw means 1.5% of the total screen width.
float desiredX = MathF.Sin( _time * 0.08f ) * 1.5f;
float desiredY = MathF.Cos( _time * 0.06f ) * 1.0f;
float desiredScale = 1.06f + (MathF.Sin( _time * 0.04f ) * 0.015f);
float desiredRotation = MathF.Sin( _time * 0.02f ) * 0.8f;
// 2. Keep the smooth Lerp dampening intact
_currentX = MathX.Lerp( _currentX, desiredX, RealTime.Delta * 1.2f );
_currentY = MathX.Lerp( _currentY, desiredY, RealTime.Delta * 1.2f );
_currentScale = MathX.Lerp( _currentScale, desiredScale, RealTime.Delta * 1.2f );
_currentRotation = MathX.Lerp( _currentRotation, desiredRotation, RealTime.Delta * 1.2f );
StateHasChanged();
}
private Sandbox.Services.Leaderboards.Board2.Entry[] GlobalPrestigeEntries { get; set; }
private System.Threading.CancellationTokenSource leaderboardLoopToken;
public KeyboardWarriorsHUD()
{
var manager = GetManager();
if (manager != null)
{
// Subscribe using a delegate
manager.OnStateChanged += HandleStateChanged;
}
}
private ShopHUD shopRef { get; set; }
// Cloud Leaderboard Properties
private bool IsReadingGlobalBoard { get; set; } = true;
// FIXED: The old duplicate IReadOnlyList GlobalPrestigeEntries variable was removed from here!
private void OpenShopMenu()
{
shopRef?.Open();
}
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()
{
// Check if the component's main UI Panel canvas surface exists
if ( Panel != null )
{
Panel.AcceptsFocus = true;
Panel.Style.PointerEvents = Sandbox.UI.PointerEvents.All;
// FIXED: Spawns your child shop directly onto the main 'Panel' canvas surface
shopRef = Panel.AddChild<ShopHUD>();
}
// 2. Initialize the token system for the silent background loop
leaderboardLoopToken = new System.Threading.CancellationTokenSource();
// 3. Start the repeating 60-second auto-refresh thread
_ = StartLeaderboardAutoRefreshLoop( leaderboardLoopToken.Token );
StartNewGame();
}
private async System.Threading.Tasks.Task FetchGlobalLeaderboardData()
{
try
{
IsReadingGlobalBoard = true;
StateHasChanged();
// Pulls the board based on your game stat key
var cloudBoard = Sandbox.Services.Leaderboards.GetFromStat( "highest_prestige" );
// 🛡️ CRITICAL SAFETY FIX: If testing locally or game is unregistered, cloudBoard is null
if ( cloudBoard != null )
{
cloudBoard.MaxEntries = 10; // Cap at Top 10 to keep screens scannable
await cloudBoard.Refresh();
GlobalPrestigeEntries = cloudBoard.Entries;
}
else
{
// Create a fake placeholder array for local testing if the cloud is unreachable
GlobalPrestigeEntries = System.Array.Empty<Sandbox.Services.Leaderboards.Board2.Entry>();
}
}
catch ( System.Exception ex )
{
Log.Error( $"Cloud Leaderboard failed to load: {ex.Message}" );
}
finally
{
IsReadingGlobalBoard = false;
StateHasChanged(); // Force the HTML tree to draw the fresh layout
}
}
private async System.Threading.Tasks.Task StartLeaderboardAutoRefreshLoop( System.Threading.CancellationToken token )
{
while ( !token.IsCancellationRequested )
{
try
{
// Wait silently in the background for 60 seconds without dropping your frame rate
await System.Threading.Tasks.Task.Delay( TimeSpan.FromSeconds( 60 ), token );
if ( token.IsCancellationRequested ) break;
Log.Info( "🔄 Silent Auto-Refresh: Fetching live cloud leaderboard data..." );
// Calls your method to download fresh rows
await FetchGlobalLeaderboardData();
}
catch ( System.Threading.Tasks.TaskCanceledException )
{
break; // Normal exit when the game stops mid-sleep
}
catch ( System.Exception ex )
{
Log.Error( $"Leaderboard loop hit an issue: {ex.Message}" );
await System.Threading.Tasks.Task.Delay( TimeSpan.FromSeconds( 5 ), token ); // Avoid spamming errors
}
}
}
/// <summary>
/// Native s&box cleanup loop. Protects your project editor from slow background memory leaks.
/// </summary>
protected override void OnDestroy()
{
leaderboardLoopToken?.Cancel();
leaderboardLoopToken?.Dispose();
}
void StartNewGame()
{
ClearHoverTelemetry();
CurrentPhase = GamePhase.WeaponSelection;
SelectedWeapon = null;
SelectedEnchantment = string.Empty;
var manager = GetManager();
if ( manager != null )
{
manager.StartBattle();
}
}
void SelectWeapon( Weapon weapon )
{
ClearHoverTelemetry();
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 )
{
ClearHoverTelemetry();
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( new GameManager.CombatLogEntry
{
Message = $"⚔️ Combat initialized! Weapon: {activeWeapon?.name} ({SelectedEnchantment})",
ColorClass = "system-text"
} );
}
}
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( new GameManager.CombatLogEntry
{
Message = $"--- [ TURN {manager.Round} : {action.ToUpper()} ] ---",
ColorClass = "system-text"
} );
}
// 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();
// Bucket 1: Local Player Tracking Metrics (8/8 Max Capacity)
int playerHash = System.HashCode.Combine(
CurrentPhase,
manager?.LocalPlayer?.hp,
manager?.LocalPlayer?.defense,
manager?.LocalPlayer?.magic,
manager?.LocalPlayer?.cursed ?? false,
manager?.LocalPlayer?.eventAttackBuff ?? 0,
manager?.PlayerPoints,
IsGameOver ? 1 : 0
);
// Bucket 2: Enemy & Global Core Metrics (8/8 Max Capacity)
int enemyAndLogHash = System.HashCode.Combine(
manager?.AIEnemy?.hp,
manager?.AIEnemy?.defense,
manager?.AIEnemy?.magic,
manager?.AIEnemy?.cursed ?? false,
manager?.AIEnemy?.eventAttackBuff ?? 0,
CombatSession.WinStreak,
CombatSession.LossStreak,
manager?.CombatLogs?.Count
);
// Bucket 3: Leaderboard Sync Status (2/8 Capacity)
int cloudHash = System.HashCode.Combine( IsReadingGlobalBoard, GlobalPrestigeEntries?.Length ?? 0 );
int firewallHash = System.HashCode.Combine(
manager?.AICurrentStance,
manager?.IsAIActionIntercepted,
manager?.AICurrentMoveIndex
);
int perkTrackingHash = System.HashCode.Combine(
manager?.IsPerkEventActive ?? false,
manager?.ActivePerkChoices?.Count ?? 0
);
// Add all unified sub-hashes together cleanly to pass the UI rendering check
return playerHash + enemyAndLogHash + cloudHash + firewallHash + perkTrackingHash;
}
}