A GameManager component for a small turn-based combat game. It tracks player/AI state, weapons, spells, rounds, prestige/points, saves/loads progress to disk, runs turn logic, logs combat events, and exposes methods for starting battles and processing player moves.
using Keyboard_Warriors_.Managers;
using Keyboard_Warriors_.Models;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using static Sandbox.MainHud.KeyboardWarriorsHUD;
namespace Keyboard_Warriors_.Systems;
public class GameManager : Component
{
public bool HasPlayerTakenAnyDamage { get; set; } = false;
public static GameManager Instance { get; private set; }
private static Random rand = new Random();
// Persistent engine tracking properties - Fully bound to your Razor HUD
[Property] public Player LocalPlayer;
[Property] public Player AIEnemy;
[Property] public Weapon ChosenWeapon;
[Property] public Weapon AIWeapon;
[Property] public int PlayerPoints { get; set; } = 0;
[Property] public int CurrentPrestige { get; set; } = 0;
[Property] public int MaxWinStreak { get; set; } = 0;
[Property] public int MaxLossStreak { get; set; } = 0;
[Property] public int MaxPrestige { get; set; } = 0;
[Property] public int MaxPoints { get; set; } = 0;
public int PointsRequiredForPrestige { get; set; } = 1000;
public float AIHealthMultiplier => 1.0f + (CurrentPrestige * 0.25f);
public GamePhase CurrentPhase { get; set; } = GamePhase.StartMenu;
// Core State Variables
public int Round = 1;
public bool IsBattleActive { get; set; } = false;
// Stats Tracking
public int Wins { get; set; } = 0;
public int Losses { get; set; } = 0;
public int WinStreak { get; set; } = 0;
public int LossStreak { get; set; } = 0;
// Active UI logs parsed by your Razor log-scroll area
public List<string> CombatLogs { get; set; } = new List<string>();
public string SelectedWeapon => ChosenWeapon?.name ?? "None";
// Legacy pattern counters for AI Memory evaluation
private int lastPlayerMove = 0;
private int repeatCount = 0;
private int attackCount = 0;
private int blockCount = 0;
private int healCount = 0;
private int specialCount = 0;
public void InitializeGameSetup()
{
// s&box utilizes Game.Random internally; local rand instantiation is usually unneeded
int prestige = GameManager.Instance.CurrentPrestige;
float multiplier = 1.0f + (prestige * 0.25f);
int startingHp = (int)System.Math.Ceiling( 100 * multiplier );
int startingArmor = prestige * 10;
Player player = new Player( "You", 100, 15, 0, false );
Player ai = new Player( "AI Opponent", startingHp, 15, startingArmor, false );
ai.defense = startingArmor;
char playAgain;
int wins = 0;
int losses = 0;
int winStreak = 0;
int lossStreak = 0;
List<Weapon> weapons = new List<Weapon>()
{
new Weapon("Sword", 5, 1.0f, 5),
new Weapon("Axe", 10, 0.8f, 2),
new Weapon("Shield", 0, 1.5f, 8),
new Weapon("Dagger", 3, 1.2f, 4)
};
GameManager.AddLog( "Choose your weapon:" );
for ( int i = 0; i < weapons.Count; i++ )
{
GameManager.AddLog(
$"{i + 1}. {weapons[i].name} " +
$"(Damage +{weapons[i].damageBonus}, " +
$"Block x{weapons[i].blockModifier}, " +
$"Heal +{weapons[i].healBonus})" );
}
// NOTE: In s&box, weaponChoice and enchantChoice will be set via
// your Razor UI UI buttons clicking into an event method,
// replacing the blocking while(true) Console.ReadLine() loops.
}
protected override void OnAwake()
{
Instance = this;
PointsRequiredForPrestige = 1000;
LoadProgress();
ApplyPrestigeScaling();
InitializeGameSetup();
}
// Method sits outside OnAwake, but still inside the GameManager class
public static void AddLog( string message )
{
if ( Instance.CombatLogs == null)
{
Instance.CombatLogs = new List<string>();
}
Instance.CombatLogs.Add( message );
Log.Info( message );
}
public static string GetSpellEmoji( string spellName )
{
if ( string.IsNullOrEmpty( spellName ) ) return "❓";
return spellName switch
{
// --- Offensive ---
"Fireball" => "🔥",
"Lightning" => "⚡",
"Poison Bolt" => "🤢",
// --- Defensive ---
"Shield" => "🛡️",
"Barrier" => "🔮",
"Reflect" => "🪞",
"Magic Wall" => "🧱",
"Fortify" => "🏋️",
// --- Healing ---
"Heal" => "❤️",
"Regen" => "⏳",
"Cleanse" => "✨",
"Divine Light" => "☀️",
"Recovery Pulse" => "💗",
// --- Special ---
"System Overdrive" => "🚀",
"EMP Blast" => "💥",
"Data Corruption" => "👾",
_ => "🔮"
};
}
public void ResetBattle()
{
int nextPrestige = CurrentPrestige + 1;
float multiplier = 1.0f + (CurrentPrestige * 0.25f);
// AI Scaling
AIEnemy.maxHp = (int)System.Math.Ceiling( 100 * multiplier );
AIEnemy.hp = AIEnemy.maxHp;
AIEnemy.maxDefense = 100 + CurrentPrestige * 10;
AIEnemy.defense = AIEnemy.maxDefense;
AIEnemy.magic = 100; // Or whatever your base magic should be
// Player Scaling (Keep at 100, or scale if you wish)
LocalPlayer.maxHp = 100 + (CurrentPrestige * 15);
LocalPlayer.hp = LocalPlayer.maxHp;
LocalPlayer.maxDefense = 100 + CurrentPrestige * 5; // Or keep as is
LocalPlayer.defense = LocalPlayer.maxDefense;
LocalPlayer.magic = 100;
AddLog( $"Battle Reset! AI HP: {AIEnemy.hp}, Armor: {AIEnemy.defense}" );
LocalPlayer.cursed = false;
AIEnemy.cursed = false;
SpellManager.generateSpells( ref LocalPlayer );
SpellManager.generateSpells( ref AIEnemy );
}
public enum GamePhase
{
StartMenu,
WeaponSelection,
Battle,
AnomalySelection,
GameOver
}
public List<Weapon> AvailableWeapons { get; set; } = new List<Weapon>()
{
new Weapon("Sword", 5, 1.0f, 5),
new Weapon("Axe", 10, 0.8f, 2),
new Weapon("Shield", 0, 1.5f, 8),
new Weapon("Dagger", 3, 1.2f, 4)
};
public void ApplyEnchantmentSelection( Weapon chosenWeapon, int enchantChoice )
{
switch ( enchantChoice )
{
case 1:
chosenWeapon.enchantment = Weapon.EnchantmentType.Fire;
break;
case 2:
chosenWeapon.enchantment = Weapon.EnchantmentType.Poison;
break;
case 3:
chosenWeapon.enchantment = Weapon.EnchantmentType.Ice;
break;
case 4:
chosenWeapon.enchantment = Weapon.EnchantmentType.Sparks;
break;
case 5:
chosenWeapon.enchantment = Weapon.EnchantmentType.DamageHP;
chosenWeapon.enchantPower = 60;
break;
case 6:
chosenWeapon.enchantment = Weapon.EnchantmentType.Stun;
chosenWeapon.enchantPower = 25;
break;
case 7:
chosenWeapon.enchantment = Weapon.EnchantmentType.Bleed;
break;
case 8:
chosenWeapon.enchantment = Weapon.EnchantmentType.LifeSteal;
break;
case 9:
chosenWeapon.enchantment = Weapon.EnchantmentType.ShieldBreak;
chosenWeapon.enchantPower = 50;
break;
default:
chosenWeapon.enchantment = Weapon.EnchantmentType.None;
break;
}
}
public void StartBattle( int chosenWeaponIndex = 0 )
{
LoadProgress();
HasPlayerTakenAnyDamage = false;
// 1. Calculate the scaled stats here based on the current prestige
float multiplier = 1.0f + (CurrentPrestige * 0.25f);
int scaledAiHp = (int)System.Math.Ceiling( 100 * multiplier );
int scaledAiArmor = CurrentPrestige * 10;
// 2. Initialize Objects with the CALCULATED values
LocalPlayer = new Player( "Hero", 100, 15, 0, false ); // 100 HP, 15 Atk, 0 Def
// Use the scaled stats instead of hardcoded 100/15!
AIEnemy = new Player( "AI Overlord", scaledAiHp, 15, scaledAiArmor, true );
// 🚀 FIX: Pull your real weapon stats from the pool safely instead of hardcoding text strings!
if ( chosenWeaponIndex >= 0 && chosenWeaponIndex < AvailableWeapons.Count )
{
ChosenWeapon = AvailableWeapons[chosenWeaponIndex];
}
else
{
ChosenWeapon = AvailableWeapons[0]; // Fallback to Sword
}
// AI picks a random weapon from your exact list!
// 1. Pick the weapon
int aiWeaponIndex = System.Random.Shared.Next( AvailableWeapons.Count );
AIWeapon = AvailableWeapons[aiWeaponIndex];
// 2. Assign the weapon to the player/AI
LocalPlayer.equippedWeapon = ChosenWeapon;
AIEnemy.equippedWeapon = AIWeapon;
// 3. REPLACED: Use your existing logic method to configure the weapon properly
int aiEnchantChoice = Random.Shared.Next( 1, 10 ); // Picks cases 1-9
ApplyEnchantmentSelection( AIWeapon, aiEnchantChoice );
// 4. Update the UI property so the Razor HUD displays the right name
AIEnemy.ActiveEnchantment = AIWeapon.enchantment;
ResetBattle();
Round = 1;
IsBattleActive = true;
// Reset tracking counters
attackCount = 0; blockCount = 0; healCount = 0; specialCount = 0;
repeatCount = 0; lastPlayerMove = 0;
CombatLogs.Clear();
CombatLogs.Add( $"⚔️ Combat Begins! You equipped {ChosenWeapon.name}. AI equipped {AIWeapon.name}." );
// Generate real spellbooks directly without temp-swaps overwriting them
SpellManager.generateSpells( ref LocalPlayer );
SpellManager.generateSpells( ref AIEnemy );
AddLog( $"AI: \"A challenger approaches... I see you have chosen the {ChosenWeapon.name}. Prepare yourself.\"" );
}
public static bool ShouldStunAI { get; set; } = false;
/// <summary>
/// Executes a complete real-time turn step. Called instantly by UI button clicks.
/// </summary>
public void ProcessTurnSlice( int playerMove )
{
if ( !IsBattleActive || LocalPlayer == null || AIEnemy == null ) return;
// --- 1. CORE TICK EFFECTS (CURSES) ---
if ( LocalPlayer.cursed )
{
LocalPlayer.hp = Math.Max( 0, LocalPlayer.hp - 5 );
AddLog( "⚠️ The demon's curse drains 5 HP from YOU!" );
}
if ( AIEnemy.cursed )
{
AIEnemy.hp = Math.Max( 0, AIEnemy.hp - 5 );
AddLog( "⚠️ The demon's curse drains 5 HP from the AI!" );
}
// Increment tracking moves
if ( playerMove == 1 ) attackCount++;
else if ( playerMove == 2 ) blockCount++;
else if ( playerMove == 3 ) healCount++;
else if ( playerMove == 4 ) specialCount++;
// --- 2. RANDOM WORLD EVENTS ---
int playerBuff = 0;
int aiBuff = 0;
// Restored passing variables directly into original ecosystem rules
if ( System.Random.Shared.Next( 10 ) == 0 )
{
// Roll the container structure
Keyboard_Warriors_.Managers.RandomEventManager.randomEvent( ref LocalPlayer, ref AIEnemy, ref playerBuff, ref aiBuff );
}
// --- 3. EXECUTE SPELLS MANUALLY (RESTORED SECTIONS 5, 6, 7, 8) ---
ExecutePlayerSpellLogic( playerMove );
// --- 4. AI INTELLIGENT MOVE EVALUATION ---
int aiMove = Game.ChooseAIAction( LocalPlayer, AIEnemy, attackCount, blockCount, healCount, specialCount, Round );
LocalPlayer.isBlocking = (playerMove == 2);
AIEnemy.isBlocking = (aiMove == 2);
bool tookDamage = false;
// --- 5. ORIGINAL TURN EVALUATIONS ---
Game.HandlePlayerTurn( ref LocalPlayer, ref AIEnemy, ChosenWeapon, playerMove, playerBuff, ref aiBuff );
Game.HandleAITurn( ref LocalPlayer, ref AIEnemy, ChosenWeapon, AIWeapon, aiMove, ref playerBuff, ref aiBuff, ref tookDamage );
// If the battle is still going or the AI just hit 0/died, fire the dialogue tracker
if ( IsBattleActive )
{
Game.aiDialogue( AIEnemy.hp );
Game.aiMemoryDialogue( WinStreak, LossStreak, tookDamage, Round );
}
// Clean up fields and step up rounds natively
Game.CleanupRound( ref LocalPlayer, ref AIEnemy, ref playerBuff, ref aiBuff, ref Round );
// --- 6. CHECK GAME OVER STATES ---
CheckBattleEndConditions( tookDamage );
}
private void ExecutePlayerSpellLogic( int playerMove )
{
// Move 5: Offensive Spells
if ( playerMove == 5 )
{
if ( LocalPlayer.magic >= 20 )
{
string spell = LocalPlayer.offensiveSpells[rand.Next( LocalPlayer.offensiveSpells.Count )];
AddLog( $"🔮 You cast {spell}!" );
int spellDamage = 15 + (LocalPlayer.magic / 2);
LocalPlayer.magic -= 20;
if ( spell == "Fireball" ) spellDamage += 5;
if ( spell == "Lightning" ) spellDamage += 10;
if ( spell == "Poison Bolt" )
{
AIEnemy.cursed = true;
AddLog( "🧪 AI is poisoned!" );
}
CombatManager.applyDamage( ref AIEnemy, spellDamage );
AddLog( $"💥 {spell} hits AI for {spellDamage} damage!" );
}
else AddLog( "❌ Not enough magic!" );
}
// Move 6: Defensive Spells
else if ( playerMove == 6 )
{
if ( LocalPlayer.magic >= 15 )
{
string spell = LocalPlayer.defensiveSpells[rand.Next( LocalPlayer.defensiveSpells.Count )];
AddLog( $"🔮 You cast {spell}!" );
if ( spell == "Shield" || spell == "Barrier" ) LocalPlayer.defense += 15;
else if ( spell == "Reflect" )
{
LocalPlayer.isBlocking = true;
AddLog( "🛡️ Next attack will be blocked!" );
}
else if ( spell == "Magic Wall" )
{
LocalPlayer.isMagicBlocking = true;
LocalPlayer.activeMagicShield = "Magic Wall";
AddLog( "✨ A magical barrier surrounds you!" );
}
else if ( spell == "Fortify" )
{
LocalPlayer.isMagicBlocking = true;
LocalPlayer.activeMagicShield = "Fortify";
AddLog( "💎 Your defenses are magically reinforced!" );
}
LocalPlayer.magic -= 15;
}
else AddLog( "❌ Not enough magic!" );
}
// Move 7: Healing Spells
else if ( playerMove == 7 )
{
if ( LocalPlayer.magic >= 15 )
{
string spell = LocalPlayer.healingSpells[rand.Next( LocalPlayer.healingSpells.Count )];
AddLog( $"🔮 You cast {spell}!" );
int healAmount = 20;
if ( spell == "Heal" ) healAmount = 20;
else if ( spell == "Regen" ) healAmount = 15;
else if ( spell == "Cleanse" )
{
healAmount = 10;
LocalPlayer.cursed = false;
AddLog( "✨ Negative effects removed!" );
}
else if ( spell == "Divine Light" ) healAmount = 40;
else if ( spell == "Recovery Pulse" ) healAmount = 30;
LocalPlayer.hp = Math.Min( 100, LocalPlayer.hp + healAmount );
LocalPlayer.magic -= 15;
AddLog( $"💚 You recover {healAmount} HP!" );
}
else AddLog( "❌ Not enough magic!" );
}
// Move 8: Special Spells
else if ( playerMove == 8 )
{
if ( LocalPlayer.magic >= 30 )
{
string spell = LocalPlayer.specialSpells[rand.Next( LocalPlayer.specialSpells.Count )];
string emoji = GetSpellEmoji( spell );
AddLog( $"{emoji} You cast Ultimate Spell: {spell}!" );
LocalPlayer.magic = Math.Max( 0, LocalPlayer.magic - 30 );
if (spell == "System Overdrive" )
{
int damageAmount = 30 + LocalPlayer.magic;// + is the remaining magic points left out of 100
CombatManager.applyDamage( ref AIEnemy, damageAmount );
AddLog( "⚡ You enter System Overdrive, bypassing the AI's firewall for massive damage!" );
}
else if ( spell == "EMP Blast")
{
int empDamage = LocalPlayer.attack *= 2;
CombatManager.applyDamage( ref AIEnemy, empDamage );
AddLog( "💥 An EMP Blast de-calibrates the AI's framework, doubling your attack power!" );
}
else if ( spell == "Data Corruption")
{
int corruptionDamage = 40;
CombatManager.applyDamage( ref AIEnemy, corruptionDamage );
ShouldStunAI = true;
AddLog( "👾 You inject Data Corruption into the AI's system routines! Causing -40 HP!" );
}
}
else AddLog( "❌ Not enough magic!" );
}
// Clamp values safely
LocalPlayer.defense = Math.Min( 100, LocalPlayer.defense );
LocalPlayer.magic = Math.Min( 100, LocalPlayer.magic );
}
private void CheckBattleEndConditions( bool tookDamage )
{
if ( LocalPlayer.hp <= 0 || AIEnemy.hp <= 0 )
{
IsBattleActive = false;
AddLog( "\n=== Game Over ===" );
// 1. Run Achievements
bool hitJackpotDummy = false;
List<string> newAchievements = Keyboard_Warriors_.Managers.AchievementManager.CheckAchievements(
tookDamage,
WinStreak,
LossStreak,
ChosenWeapon,
LocalPlayer,
AIEnemy,
ref hitJackpotDummy
);
foreach ( var logLine in newAchievements )
{
AddLog( logLine );
}
// 3. Evaluate Match States
if ( LocalPlayer.hp <= 0 && AIEnemy.hp <= 0 )
{
AddLog( "🤝 It's a tie!" );
WinStreak = 0;
LossStreak = 0;
}
else if ( LocalPlayer.hp <= 0 )
{
AddLog( "💀 AI wins!" );
Losses++;
LossStreak++;
WinStreak = 0;
PlayerPoints += 50;
AddLog( "💰 +50 Points added (Consolation)." );
}
else
{
AddLog( "AI: \"**INITIATING DEFEAT PROCESS.. 3..2..1.. *BOOM*\"" );
AddLog( "🏆 You win!" );
Wins++;
WinStreak++;
LossStreak = 0;
double prestigeExponent = Math.Pow( 1.1, CurrentPrestige );
int winReward = (int)((100 + (WinStreak * 10)) * prestigeExponent);
int prestigeMultiplier = 1 + (CurrentPrestige * 2); // Each prestige level adds a 2x bonus
winReward = winReward * prestigeMultiplier;
PlayerPoints += winReward;
AddLog( $"💰 +{winReward} Points added! (Streak Bonus included)" );
}
SaveProgress();// Save immediately just in case of a sudden crash.
}
}
public event Action OnStateChanged;
public void TriggerPrestige()
{
if (PlayerPoints >= PointsRequiredForPrestige)
{
PlayerPoints = 0;
CurrentPrestige++;
ApplyPrestigeScaling();
ResetBattle();
SaveProgress();// Lock in their new ascension status.
OnStateChanged?.Invoke();
AddLog( $"\n✨ PRESTIGE INCREASED TO LEVEL {CurrentPrestige}! ✨" );
AddLog( "⚠️ WARNING: The AI Overlord has adapted. Its vital capacities have evolved!" );
WinStreak = 0;
LossStreak = 0;
}
else
{
AddLog( $"❌ Not enough points! You need {PointsRequiredForPrestige} points to prestige." );
}
}
public void ApplyPrestigeScaling()
{
// 1. Exponential Requirement Scaling
// 1000, 2000, 4000, 8000...
PointsRequiredForPrestige = 1000 * (int)Math.Pow( 2.0, CurrentPrestige );
if ( AIEnemy != null )
{
// Scale max health based on prestige level (+25% per level)
float multiplier = 1.0f + (CurrentPrestige * 0.25f);
int scaledMaxHp = (int)System.Math.Ceiling( 100 * multiplier );
// If your Player model tracks a MaxHp property, set that too!
AIEnemy.maxHp = scaledMaxHp;
// Boost AI start Armor subtly (+10 per prestige level)
AIEnemy.defense = 100 + CurrentPrestige * 10;
}
}
public struct SaveData
{
public int Points { get; set; }
public int Prestige { get; set; }
public int MaxWinStreak { get; set; }
public int MaxLossStreak { get; set; }
public int MaxPrestige { get; set; }
public int MaxPoints { get; set; }
}
private const string SaveFileName = "player_progress.json";
public void SaveProgress()
{
if ( WinStreak > MaxWinStreak ) MaxWinStreak = WinStreak;
if ( LossStreak > MaxLossStreak ) MaxLossStreak = LossStreak;
if ( CurrentPrestige > MaxPrestige ) MaxPrestige = CurrentPrestige;
if ( PlayerPoints > MaxPoints ) MaxPoints = PlayerPoints;
var data = new SaveData
{
Points = PlayerPoints,
Prestige = CurrentPrestige,
MaxWinStreak = MaxWinStreak,
MaxLossStreak = MaxLossStreak,
MaxPrestige = MaxPrestige,
MaxPoints = MaxPoints
};
FileSystem.Data.WriteJson( SaveFileName, data );
Log.Info( "💾 Progress saved to disk!" );
}
public void LoadProgress()
{
if ( FileSystem.Data.FileExists( SaveFileName ))
{
var data = FileSystem.Data.ReadJson<SaveData>( SaveFileName );
PlayerPoints = data.Points;
CurrentPrestige = data.Prestige;
Log.Info( $"📂 Loaded Progress: {PlayerPoints} PTS, Prestige {CurrentPrestige}" );
MaxWinStreak = data.MaxWinStreak;
MaxLossStreak = data.MaxLossStreak;
MaxPrestige = data.MaxPrestige;
MaxPoints = data.MaxPoints;
ApplyPrestigeScaling();// Make sure AI updates its baseline stats right away to match player.
}
}
}