Manager class that runs a small UNO-like duel mini-game between a player and an AI. It deals simulated cards, constructs a popup with two choices, resolves the player's and AI's actions, applies HP changes via CombatManager, and logs events to GameManager.
using Keyboard_Warriors_.Models;
using Keyboard_Warriors_.Systems;
using Sandbox;
using System;
namespace Keyboard_Warriors_.Managers;
public static class UnoDuelEventRunner
{
private static Player cachedPlayer;
private static Player cachedAI;
private static readonly string[] colors = { "Red", "Blue", "Green", "Yellow" };
// Class properties to securely preserve the deck variables between button clicks
private static int topColor;
private static int topNumber;
private static int topType;
private static int playerColor;
private static int playerNumber;
private static int playerType;
private static int aiColor;
private static int aiNumber;
private static int aiType;
private static bool playerCanPlay;
private static int DrawCardType()
{
int roll = Random.Shared.Next( 0, 100 ); // 0 to 99 inclusive
if ( roll < 65 ) return 0; // Number
if ( roll < 77 ) return 1; // Reverse
if ( roll < 92 ) return 2; // Draw Two
return 3; // Wild
}
private static string GetCardTypeName( int t )
{
if ( t == 0 ) return "Number";
if ( t == 1 ) return "Reverse";
if ( t == 2 ) return "Draw Two";
return "Wild";
}
// Safe helper method to fetch the GameManager directly from the active engine layout context
private static GameManager GetManager()
{
return Sandbox.Game.ActiveScene?.Get<GameManager>();
}
public static void StartEvent( Player player, Player ai )
{
// Defensive guard assertion prevents multi-popup panel UI thread registration crashes
var manager = GetManager();
if ( manager == null )
{
Log.Error( "[UNO DUEL CRITICAL] Failed to resolve GameManager from active scene tree setup!" );
return;
}
// ⚙️ SYSTEM CHANNEL: Outer announcement barriers stay neutral white
GameManager.AddSystemLog( "\n=== UNO DUEL MATCH PROTOCOL ===" );
cachedPlayer = player;
cachedAI = ai;
// Generate random positions safely using Random.Shared
topColor = Random.Shared.Next( 0, 4 ); // 0 to 3
topNumber = Random.Shared.Next( 0, 10 ); // 0 to 9
topType = DrawCardType();
playerColor = Random.Shared.Next( 0, 4 );
playerNumber = Random.Shared.Next( 0, 10 );
playerType = DrawCardType();
aiColor = Random.Shared.Next( 0, 4 );
aiNumber = Random.Shared.Next( 0, 10 );
aiType = DrawCardType();
GameManager.AddSystemLog( $"\n[DECK STACK] Top Discard Pile: {colors[topColor]} {topNumber} ({GetCardTypeName( topType )})" );
GameManager.AddPlayerLog( $"[YOUR HAND] Dealt Profile: {colors[playerColor]} {playerNumber} ({GetCardTypeName( playerType )})" );
GameManager.AddAILog( $"[AI BUFFER] Dealt Profile: {colors[aiColor]} {aiNumber} ({GetCardTypeName( aiType )})" );
GameManager.AddSystemLog( "⚠️ AWAITING SECURITY HANDSHAKE DISCARD PROTOCOL..." );
// Set the rule lookup cache flag based on standard matching matrices
playerCanPlay =
playerType == 3 ||
playerType == topType ||
playerColor == topColor ||
playerNumber == topNumber;
var unoEvent = new ChoiceEvent
{
Title = "🃏 PROTOCOL CARD DECK DUEL",
PromptText = $"The system has dealt an encryption card profile. Top Card: {colors[topColor]} {topNumber} ({GetCardTypeName( topType )}). Your Card: {colors[playerColor]} {playerNumber} ({GetCardTypeName( playerType )}). Evaluate your data packets. Will you match the sector or pull from the core stack?",
};
// --- BUTTON 1: PLAY CARD ---
unoEvent.Options.Add( new EventOption
{
ButtonText = "🃏 DISCARD / PLAY CARD",
ActionToExecute = () =>
{
ResolveUnoDuel( 1 );
}
} );
// --- BUTTON 2: DRAW AND SKIP ---
unoEvent.Options.Add( new EventOption
{
ButtonText = "➕ DRAW PACKET & SKIP",
ActionToExecute = () =>
{
ResolveUnoDuel( 2 );
}
} );
// Execute popup utilizing the secure found scene reference
manager.TriggerChoicePopup( unoEvent );
}
private static void ResolveUnoDuel( int playerChoice )
{
var manager = GetManager();
int prestige = manager != null ? manager.CurrentPrestige : 0;
// Calculate scaled prestige modifications (+1 to +3 lineally per prestige level)
int scaledReverseDamage = 10 + (prestige * 1);
int scaledDrawTwoDamage = 20 + (prestige * 3);
int scaledPenaltyDamage = 15 + (prestige * 2);
// --- PLAYER ACTION REACTION TREE ---
if ( playerChoice == 1 )
{
if ( playerCanPlay )
{
// 🔵 PLAYER HIGHLIGHT: Valid card matches turn blue and print a clear explanation
GameManager.AddPlayerLog( $"🟢 CARD MATCHED! You successfully play your {colors[playerColor]} {GetCardTypeName( playerType )}!" );
if ( playerType == 1 ) // REVERSE CARD
{
int finalHeal = scaledReverseDamage + cachedPlayer.eventHealingBuff;
GameManager.AddPlayerLog( $"🔄 REVERSE DIRECTION! Data stream inverted! AI loses {scaledReverseDamage} HP | You heal +{finalHeal} HP!" );
CombatManager.applyDamage( ref cachedAI, scaledReverseDamage );
cachedPlayer.hp += finalHeal;
cachedPlayer.eventHealingBuff = 0; // Safe flush
}
else if ( playerType == 2 ) // DRAW TWO CARD
{
GameManager.AddPlayerLog( $"➕ DRAW TWO! Forcing stack overflow on AI registers! AI loses {scaledDrawTwoDamage} HP!" );
CombatManager.applyDamage( ref cachedAI, scaledDrawTwoDamage );
}
else if ( playerType == 3 ) // WILD CARD
{
topColor = Random.Shared.Next( 0, 4 );
GameManager.AddPlayerLog( $"🌈 [WILD CARD ACTIVATE] Firewalls re-encrypted! Mainframe color shifted to {colors[topColor]}!" );
}
else // STANDARD NUMBER CARD
{
GameManager.AddPlayerLog( $"💾 Data segment discarded cleanly: {colors[playerColor]} {playerNumber} overrides the stack." );
}
// The card you played now safely becomes the fresh Top Card of the deck for the AI's turn!
topColor = playerColor;
topNumber = playerNumber;
topType = playerType;
}
else
{
// 🔵 PLAYER HIGHLIGHT: Mismatched cards throw a clear warning log explaining why you failed!
GameManager.AddPlayerLog( $"❌ INVALID SEQUENCE! Your {colors[playerColor]} {GetCardTypeName( playerType )} does not match the pile color or number! Card rejected!" );
GameManager.AddPlayerLog( $"🚨 UNO MISPLAY PENALTY! System feedback loop hits you for -{scaledPenaltyDamage} HP!" );
CombatManager.applyDamage( ref cachedPlayer, scaledPenaltyDamage );
}
}
else
{
// 🔵 PLAYER HIGHLIGHT: Your voluntary skip turns turn blue
GameManager.AddPlayerLog( "📥 Packet pulled. You draw from the core stack and skip your action." );
}
// --- AI TURNING REACTION TREE ---
bool aiCanPlay =
aiType == 3 ||
aiType == topType ||
aiColor == topColor ||
aiNumber == topNumber;
// 🔴 AI HIGHLIGHT: The AI's turn announcement headers turn red
GameManager.AddAILog( "\nAI's turn!" );
if ( aiCanPlay && cachedAI.hp > 15 )
{
// 🔴 AI HIGHLIGHT: AI card deployments turn red
GameManager.AddAILog( $"🔴 AI MATCHED! AI plays its {colors[aiColor]} {GetCardTypeName( aiType )} onto the pile!" );
if ( aiType == 1 )
{
int finalAIHeal = scaledReverseDamage + cachedAI.eventHealingBuff;
GameManager.AddAILog( $"🔄 AI REVERSE! AI steals health! (You lose {scaledReverseDamage} HP | AI heals +{finalAIHeal} HP!)" );
CombatManager.applyDamage( ref cachedPlayer, scaledReverseDamage );
cachedAI.hp += finalAIHeal;
cachedAI.eventHealingBuff = 0; // Safe flush
}
else if ( aiType == 2 )
{
GameManager.AddAILog( $"➕ AI DRAW TWO! You lose {scaledDrawTwoDamage} HP!" );
CombatManager.applyDamage( ref cachedPlayer, scaledDrawTwoDamage );
}
else if ( aiType == 3 )
{
topColor = Random.Shared.Next( 0, 4 );
GameManager.AddAILog( $"🌈 AI uses WILD! Mainframe color shifted to {colors[topColor]}" );
}
else
{
GameManager.AddAILog( $"💾 AI discards cleanly: {colors[aiColor]} {aiNumber} overrides the stack." );
}
topColor = aiColor;
topNumber = aiNumber;
topType = aiType;
}
else
{
// 🔴 AI HIGHLIGHT: AI cards skips or failures turn red
GameManager.AddAILog( "AI draws a card from the core stack." );
if ( !aiCanPlay )
{
GameManager.AddAILog( $"🚨 AI MISPLAY PENALTY! AI failed to match parameters! Feedback loop hits AI for -{scaledPenaltyDamage} HP!" );
CombatManager.applyDamage( ref cachedAI, scaledPenaltyDamage );
}
}
// Clamp logic using prestige-scaled maximum health pools safely
if ( cachedPlayer.hp < 0 ) cachedPlayer.hp = 0;
if ( cachedAI.hp < 0 ) cachedAI.hp = 0;
if ( manager != null )
{
if ( manager.LocalPlayer != null && cachedPlayer.hp > manager.LocalPlayer.maxHp )
cachedPlayer.hp = manager.LocalPlayer.maxHp;
if ( manager.AIEnemy != null && cachedAI.hp > manager.AIEnemy.maxHp )
cachedAI.hp = manager.AIEnemy.maxHp;
// Force state diagnostics to redraw the health bars immediately on the viewport panel
manager.NotifyStateChanged();
}
else
{
if ( cachedPlayer.hp > 100 ) cachedPlayer.hp = 100;
if ( cachedAI.hp > 100 ) cachedAI.hp = 100;
}
// ⚙️ SYSTEM CHANNEL: Displays the final score and results of the mini-game clearly
GameManager.AddSystemLog( "\n=== END OF UNO DUEL ===" );
GameManager.AddPlayerLog( $"STATUS REPORT -> {cachedPlayer.name} | HP: {cachedPlayer.hp} | Armor: {cachedPlayer.defense}" );
GameManager.AddAILog( $"STATUS REPORT -> {cachedAI.name} | HP: {cachedAI.hp} | Armor: {cachedAI.defense}" );
}
}