This commit is contained in:
Michal Vrba
2026-09-16 22:20:44 +02:00
commit 121d013d14
53 changed files with 10410 additions and 0 deletions
+418
View File
@@ -0,0 +1,418 @@
using SlaySharp.Core.Combat;
using SlaySharp.Core.Enums;
using SlaySharp.Core.Models;
namespace SlaySharp.Core.Catalogs;
public class ConsoleWriteLineCard : Card
{
private int _damage = 6;
public ConsoleWriteLineCard() : base(
"Console.WriteLine",
"Console.WriteLine(target);",
"target.Hit(6);",
"Deal 6 damage.",
cost: 1,
type: CardType.Attack,
rarity: CardRarity.Basic,
targetType: TargetType.SingleEnemy
) { }
protected override void OnUpgrade()
{
_damage = 9;
Description = "Deal 9 damage.";
CodeSnippet = "target.Hit(9);";
}
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
if (target == null) return;
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
var totalDmg = player.Statuses.CalculateOutgoingDamage(_damage);
var dealt = target.TakeDamage(totalDmg);
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: false, isCrit: isCrit, Name);
engine.Log.AddInfo($"[Console.WriteLine] {target.Name} took {dealt} dmg.");
}
public override Card Clone() => new ConsoleWriteLineCard
{
Upgraded = this.Upgraded,
Name = this.Name,
Description = this.Description,
_damage = this._damage,
CodeSnippet = this.CodeSnippet
};
}
public class TryCatchCard : Card
{
private int _shield = 5;
public TryCatchCard() : base(
"try { ... } catch",
"try { Guard(); } catch { }",
"shield += 5;",
"Gain 5 Shield.",
cost: 1,
type: CardType.Skill,
rarity: CardRarity.Basic,
targetType: TargetType.Self
) { }
protected override void OnUpgrade()
{
_shield = 8;
Description = "Gain 8 Shield.";
CodeSnippet = "shield += 8;";
}
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
player.GainShield(_shield);
engine.NotifyShield(player.Id, player.Name, _shield, Name);
engine.Log.AddInfo($"[try/catch] +{_shield} Shield.");
}
public override Card Clone() => new TryCatchCard
{
Upgraded = this.Upgraded,
Name = this.Name,
Description = this.Description,
_shield = this._shield,
CodeSnippet = this.CodeSnippet
};
}
public class GcCollectCard : Card
{
private int _baseDamage = 8;
public GcCollectCard() : base(
"GC.Collect()",
"GC.Collect(2);",
"target.Hit(8 + Discard * 2);",
"Deal 8 damage + 2 per Discard card.",
cost: 2,
type: CardType.Attack,
rarity: CardRarity.Common,
targetType: TargetType.SingleEnemy
) { }
protected override void OnUpgrade()
{
_baseDamage = 12;
Description = "Deal 12 damage + 2 per Discard card.";
CodeSnippet = "target.Hit(12 + Discard * 2);";
}
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
if (target == null) return;
var discardCount = player.DiscardPile.Count;
var rawDmg = _baseDamage + (discardCount * 2);
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
var totalDmg = player.Statuses.CalculateOutgoingDamage(rawDmg);
var dealt = target.TakeDamage(totalDmg);
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: false, isCrit: isCrit, Name);
engine.Log.AddSuccess($"[GC.Collect] Swept {discardCount} objects! Dealt {dealt} dmg.");
}
public override Card Clone() => new GcCollectCard
{
Upgraded = this.Upgraded,
Name = this.Name,
Description = this.Description,
_baseDamage = this._baseDamage,
CodeSnippet = this.CodeSnippet
};
}
public class SpanSliceCard : Card
{
private int _damage = 8;
public SpanSliceCard() : base(
"Span<T>.Slice()",
"span.Slice(0, 8);",
"target.DirectHit(8);",
"Deal 8 piercing damage (ignores shield).",
cost: 1,
type: CardType.Attack,
rarity: CardRarity.Common,
targetType: TargetType.SingleEnemy
) { }
protected override void OnUpgrade()
{
_damage = 11;
Description = "Deal 11 piercing damage (ignores shield).";
CodeSnippet = "target.DirectHit(11);";
}
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
if (target == null) return;
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
var totalDmg = player.Statuses.CalculateOutgoingDamage(_damage);
var dealt = target.TakeDamage(totalDmg, isPiercing: true);
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: true, isCrit: isCrit, Name);
engine.Log.AddSuccess($"[Span.Slice] Pierced shield! Dealt {dealt} dmg.");
}
public override Card Clone() => new SpanSliceCard
{
Upgraded = this.Upgraded,
Name = this.Name,
Description = this.Description,
_damage = this._damage,
CodeSnippet = this.CodeSnippet
};
}
public class ThrowExceptionCard : Card
{
private int _damage = 16;
public ThrowExceptionCard() : base(
"throw Exception()",
"throw new PanicException();",
"target.Hit(16); // +1 Leak",
"Deal 16 damage. Gain 1 Memory Leak.",
cost: 2,
type: CardType.Attack,
rarity: CardRarity.Uncommon,
targetType: TargetType.SingleEnemy
) { }
protected override void OnUpgrade()
{
_damage = 22;
Description = "Deal 22 damage. Gain 1 Memory Leak.";
CodeSnippet = "target.Hit(22); // +1 Leak";
}
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
if (target == null) return;
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
var totalDmg = player.Statuses.CalculateOutgoingDamage(_damage);
var dealt = target.TakeDamage(totalDmg);
player.Statuses.Add(StatusType.MemoryLeak, 1);
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: false, isCrit: isCrit, Name);
engine.Log.AddWarning($"[throw Exception] Dealt {dealt} dmg. +1 Memory Leak.");
}
public override Card Clone() => new ThrowExceptionCard
{
Upgraded = this.Upgraded,
Name = this.Name,
Description = this.Description,
_damage = this._damage,
CodeSnippet = this.CodeSnippet
};
}
public class TieredCompilationCard : Card
{
private int _strength = 2;
public TieredCompilationCard() : base(
"Tiered Compilation",
"[AggressiveOptimization]",
"Player.Strength += 2;",
"Power: +2 Strength (All attacks deal +2 damage).",
cost: 1,
type: CardType.Power,
rarity: CardRarity.Rare,
targetType: TargetType.Self,
exhausts: true
) { }
protected override void OnUpgrade()
{
_strength = 3;
Description = "Power: +3 Strength (All attacks deal +3 damage).";
CodeSnippet = "Player.Strength += 3;";
}
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
player.Statuses.Add(StatusType.Strength, _strength);
engine.Log.AddSuccess($"[JIT] +{_strength} Strength permanently.");
}
public override Card Clone() => new TieredCompilationCard
{
Upgraded = this.Upgraded,
Name = this.Name,
Description = this.Description,
_strength = this._strength,
CodeSnippet = this.CodeSnippet
};
}
public class ThreadSleepCard : Card
{
public ThreadSleepCard() : base(
"Thread.Sleep()",
"await Task.Yield();",
"CPU += 2; Weak += 1;",
"Gain +2 CPU this turn. Gain 1 Weak.",
cost: 0,
type: CardType.Skill,
rarity: CardRarity.Common,
targetType: TargetType.Self
) { }
protected override void OnUpgrade()
{
Description = "Gain +3 CPU this turn. Gain 1 Weak.";
CodeSnippet = "CPU += 3; Weak += 1;";
}
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
var energyBonus = Upgraded ? 3 : 2;
player.CurrentEnergy += energyBonus;
player.Statuses.Add(StatusType.Weak, 1);
engine.Log.AddInfo($"[Thread.Sleep] +{energyBonus} CPU, +1 Weak.");
}
public override Card Clone() => new ThreadSleepCard
{
Upgraded = this.Upgraded,
Name = this.Name,
Description = this.Description,
CodeSnippet = this.CodeSnippet
};
}
public class ParallelForEachCard : Card
{
private int _dmgPerHit = 4;
public ParallelForEachCard() : base(
"Parallel.ForEach()",
"Parallel.For(0, 3);",
"target.Hit(4); // x3 hits",
"Attack 3 times for 4 damage each.",
cost: 2,
type: CardType.Attack,
rarity: CardRarity.Uncommon,
targetType: TargetType.SingleEnemy
) { }
protected override void OnUpgrade()
{
_dmgPerHit = 5;
Description = "Attack 3 times for 5 damage each.";
CodeSnippet = "target.Hit(5); // x3 hits";
}
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
if (target == null) return;
int totalHits = 3;
int totalDamage = 0;
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
for (int i = 0; i < totalHits; i++)
{
if (target.IsDead) break;
var hitDmg = player.Statuses.CalculateOutgoingDamage(_dmgPerHit);
var dealt = target.TakeDamage(hitDmg);
totalDamage += dealt;
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: false, isCrit: isCrit, $"{Name} #{i+1}");
}
engine.Log.AddSuccess($"[Parallel.ForEach] 3 hits: total {totalDamage} dmg.");
}
public override Card Clone() => new ParallelForEachCard
{
Upgraded = this.Upgraded,
Name = this.Name,
Description = this.Description,
_dmgPerHit = this._dmgPerHit,
CodeSnippet = this.CodeSnippet
};
}
public class LinqWhereCard : Card
{
private int _shield = 6;
public LinqWhereCard() : base(
"collection.Where()",
"data.Where(x => x.IsSafe);",
"shield += 6; Draw(1);",
"Gain 6 Shield. Draw 1 card.",
cost: 1,
type: CardType.Skill,
rarity: CardRarity.Common,
targetType: TargetType.Self
) { }
protected override void OnUpgrade()
{
_shield = 9;
Description = "Gain 9 Shield. Draw 1 card.";
CodeSnippet = "shield += 9; Draw(1);";
}
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
player.GainShield(_shield);
engine.NotifyShield(player.Id, player.Name, _shield, Name);
player.DrawCards(1, engine.Log);
engine.Log.AddInfo($"[LINQ .Where()] +{_shield} Shield, drew 1 card.");
}
public override Card Clone() => new LinqWhereCard
{
Upgraded = this.Upgraded,
Name = this.Name,
Description = this.Description,
_shield = this._shield,
CodeSnippet = this.CodeSnippet
};
}
public class NullCoalescingCard : Card
{
public NullCoalescingCard() : base(
"fallback ?? default",
"val ?? FallbackState;",
"ClearDebuffs(); shield += 6;",
"Remove all debuffs. Gain 6 Shield.",
cost: 1,
type: CardType.Skill,
rarity: CardRarity.Uncommon,
targetType: TargetType.Self
) { }
protected override void OnUpgrade()
{
Description = "Remove all debuffs. Gain 8 Shield.";
CodeSnippet = "ClearDebuffs(); shield += 8;";
}
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
var stacksToRemove = Upgraded ? 2 : 1;
var shield = Upgraded ? 8 : 6;
player.Statuses.Remove(StatusType.Vulnerable, stacksToRemove);
player.Statuses.Remove(StatusType.Weak, stacksToRemove);
player.Statuses.Remove(StatusType.MemoryLeak, stacksToRemove);
player.GainShield(shield);
engine.NotifyShield(player.Id, player.Name, shield, Name);
engine.Log.AddSuccess($"[Null Coalescing ??] Cleared debuffs, +{shield} Shield.");
}
public override Card Clone() => new NullCoalescingCard
{
Upgraded = this.Upgraded,
Name = this.Name,
Description = this.Description
};
}
+169
View File
@@ -0,0 +1,169 @@
using SlaySharp.Core.Combat;
using SlaySharp.Core.Enums;
using SlaySharp.Core.Models;
namespace SlaySharp.Core.Catalogs;
public class NullReferenceExceptionEnemy : Enemy
{
private int _patternStep = 0;
public NullReferenceExceptionEnemy()
: base("NullReferenceException", 45, "💥", "Unhandled null pointer reference causing sudden application crashes.")
{
}
public override void DecideNextIntent(CombatEngine engine)
{
CurrentIntent = _patternStep switch
{
0 => new Intent(IntentType.Attack, 7, 1, "Dereference Null Pointer (7 dmg)"),
1 => new Intent(IntentType.AttackDefend, 6, 1, "Corrupt Stack Frame (6 dmg & +7 Shield)"),
2 => new Intent(IntentType.Debuff, 0, 1, "Expose Null Pointer (+2 Vulnerable)", StatusType.Vulnerable, 2),
3 => new Intent(IntentType.Attack, 14, 1, "FATAL PROCESS TERMINATION (14 dmg)"),
_ => new Intent(IntentType.Attack, 8, 1, "Null Strike (8 dmg)")
};
}
public override void ExecuteIntent(CombatEngine engine)
{
if (CurrentIntent == null) return;
switch (_patternStep)
{
case 0:
var dmg0 = Statuses.CalculateOutgoingDamage(CurrentIntent.PrimaryValue);
var isCrit0 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
var dealt0 = engine.Player.TakeDamage(dmg0);
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt0, isPiercing: false, isCrit: isCrit0, Name);
engine.Log.AddError($"[{Name}] dereferenced null pointer! Dealt {dealt0} damage to Player.");
break;
case 1:
var dmg1 = Statuses.CalculateOutgoingDamage(CurrentIntent.PrimaryValue);
var isCrit1 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
var dealt1 = engine.Player.TakeDamage(dmg1);
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt1, isPiercing: false, isCrit: isCrit1, Name);
GainShield(7);
engine.NotifyShield(Id, Name, 7, Name);
engine.Log.AddError($"[{Name}] corrupted call stack! Dealt {dealt1} damage and gained +7 Shield.");
break;
case 2:
engine.Player.Statuses.Add(StatusType.Vulnerable, 2);
GainShield(5);
engine.NotifyShield(Id, Name, 5, Name);
engine.Log.AddWarning($"[{Name}] exposed unhandled pointer! Player is now Vulnerable for 2 turns.");
break;
case 3:
var dmg3 = Statuses.CalculateOutgoingDamage(CurrentIntent.PrimaryValue);
var isCrit3 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
var dealt3 = engine.Player.TakeDamage(dmg3);
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt3, isPiercing: false, isCrit: isCrit3, Name);
engine.Log.AddFatal($"[{Name}] TRIGGERED PROCESS CRASH! Massive {dealt3} damage dealt!");
break;
}
_patternStep = (_patternStep + 1) % 4;
DecideNextIntent(engine);
}
}
public class OffByOneBugEnemy : Enemy
{
private int _patternStep = 0;
public OffByOneBugEnemy()
: base("Off-By-One Bug", 22, "🐛", "Iterative boundary bug indexing `for (int i = 0; i <= arr.Length; i++)`.")
{
}
public override void DecideNextIntent(CombatEngine engine)
{
CurrentIntent = _patternStep switch
{
0 => new Intent(IntentType.Attack, 3, 3, "IndexOutOfRangeException (3x3 dmg)"),
1 => new Intent(IntentType.Defend, 6, 1, "Loop Boundary Shield (+6 Shield)"),
_ => new Intent(IntentType.Attack, 4, 2, "Array Overflow (4x2 dmg)")
};
}
public override void ExecuteIntent(CombatEngine engine)
{
if (CurrentIntent == null) return;
switch (_patternStep)
{
case 0:
int totalDmg = 0;
var isCrit = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
for (int i = 0; i < 3; i++)
{
var dmg = Statuses.CalculateOutgoingDamage(3);
var dealt = engine.Player.TakeDamage(dmg);
totalDmg += dealt;
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt, isPiercing: false, isCrit: isCrit, $"{Name} #{i+1}");
}
engine.Log.AddWarning($"[{Name}] looped past boundary! 3 hits for total {totalDmg} damage!");
break;
case 1:
GainShield(6);
Statuses.Add(StatusType.Strength, 1);
engine.NotifyShield(Id, Name, 6, Name);
engine.Log.AddInfo($"[{Name}] reinforced iteration bound! Gained +6 Shield and +1 Strength.");
break;
}
_patternStep = (_patternStep + 1) % 2;
DecideNextIntent(engine);
}
}
public class MemoryLeakSpectreEnemy : Enemy
{
private int _turn = 0;
public MemoryLeakSpectreEnemy()
: base("Memory Leak Spectre", 34, "👻", "Unmanaged IDisposable and dangling event handlers slowly devouring RAM.")
{
}
public override void DecideNextIntent(CombatEngine engine)
{
CurrentIntent = (_turn % 3) switch
{
0 => new Intent(IntentType.Attack, 6, 1, "Heap Allocation Spurt (6 dmg)"),
1 => new Intent(IntentType.Debuff, 0, 1, "Dangling Event Handler (+1 Memory Leak)", StatusType.MemoryLeak, 1),
2 => new Intent(IntentType.Attack, 11, 1, "Out Of Memory Wave (11 dmg)"),
_ => new Intent(IntentType.Attack, 6, 1, "Heap Bloat (6 dmg)")
};
}
public override void ExecuteIntent(CombatEngine engine)
{
if (CurrentIntent == null) return;
switch (_turn % 3)
{
case 0:
var dmg0 = Statuses.CalculateOutgoingDamage(6);
var isCrit0 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
var dealt0 = engine.Player.TakeDamage(dmg0);
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt0, isPiercing: false, isCrit: isCrit0, Name);
engine.Log.AddWarning($"[{Name}] allocated 200MB unmanaged memory! Dealt {dealt0} dmg.");
break;
case 1:
engine.Player.Statuses.Add(StatusType.MemoryLeak, 1);
engine.Log.AddError($"[{Name}] hooked persistent event handler! Player gained +1 Memory Leak.");
break;
case 2:
var dmg2 = Statuses.CalculateOutgoingDamage(11);
var isCrit2 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
var dealt2 = engine.Player.TakeDamage(dmg2);
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt2, isPiercing: false, isCrit: isCrit2, Name);
engine.Log.AddFatal($"[{Name}] triggered GC Thrashing OOM! Dealt {dealt2} damage.");
break;
}
_turn++;
DecideNextIntent(engine);
}
}
@@ -0,0 +1,25 @@
using SlaySharp.Core.Models;
namespace SlaySharp.Core.Catalogs;
public static class StarterDecks
{
public static List<Card> CreateCsharpArchitectDeck()
{
return new List<Card>
{
new ConsoleWriteLineCard(),
new ConsoleWriteLineCard(),
new ConsoleWriteLineCard(),
new ConsoleWriteLineCard(),
new TryCatchCard(),
new TryCatchCard(),
new TryCatchCard(),
new TryCatchCard(),
new GcCollectCard(),
new SpanSliceCard(),
new LinqWhereCard(),
new TieredCompilationCard()
};
}
}
+309
View File
@@ -0,0 +1,309 @@
using SlaySharp.Core.Enums;
using SlaySharp.Core.Models;
namespace SlaySharp.Core.Combat;
public class CombatEngine
{
public Player Player { get; private set; } = null!;
public List<Enemy> Enemies { get; private set; } = new();
public int TurnNumber { get; private set; }
public CombatPhase Phase { get; private set; } = CombatPhase.PlayerTurn;
public CombatLog Log { get; } = new();
public string? ActiveActingEnemyId { get; private set; }
public event Action? OnStateChanged;
public event Action<DamageEvent>? OnDamageDealt;
public event Action<ShieldEvent>? OnShieldGained;
public event Action<CardPlayAnimationEvent>? OnCardPlayAnimation;
public void NotifyDamage(string targetId, string targetName, int amount, bool isPiercing, bool isCrit, string sourceName)
{
OnDamageDealt?.Invoke(new DamageEvent(targetId, targetName, amount, isPiercing, isCrit, sourceName));
}
public void NotifyShield(string targetId, string targetName, int amount, string sourceName)
{
OnShieldGained?.Invoke(new ShieldEvent(targetId, targetName, amount, sourceName));
}
public void StartCombat(Player player, List<Enemy> enemies)
{
Player = player;
Enemies = enemies;
TurnNumber = 1;
Phase = CombatPhase.PlayerTurn;
ActiveActingEnemyId = null;
Log.Clear();
Log.AddInfo($"=== Starting Incident Resolution: {enemies.Count} Bugs Detected ===");
Player.InitCombatDeck();
foreach (var enemy in Enemies)
{
enemy.Statuses.Clear();
enemy.Shield = 0;
enemy.DecideNextIntent(this);
}
foreach (var relic in Player.Relics)
{
relic.OnCombatStart(this);
}
StartPlayerTurn(isFirstTurn: true);
}
public void StartPlayerTurn(bool isFirstTurn = false)
{
Phase = CombatPhase.PlayerTurn;
ActiveActingEnemyId = null;
Player.ResetShield();
Player.ResetTurnEnergy();
Log.AddInfo($"--- Turn {TurnNumber}: Player Execution Phase ---");
foreach (var relic in Player.Relics)
{
relic.OnTurnStart(this);
}
int drawCount = 5;
Player.DrawCards(drawCount, Log);
NotifyStateChanged();
}
public bool PlayCard(Card card, Character? target = null)
{
if (Phase != CombatPhase.PlayerTurn)
{
Log.AddWarning("Cannot play cards outside of player turn.");
return false;
}
if (!Player.Hand.Contains(card))
{
Log.AddError($"Card '{card.Name}' is not in player hand.");
return false;
}
if (Player.CurrentEnergy < card.Cost)
{
Log.AddWarning($"Insufficient CPU cycles! Card requires {card.Cost} CPU, available: {Player.CurrentEnergy}.");
return false;
}
if (card.TargetType == TargetType.SingleEnemy)
{
if (target is not Enemy enemyTarget || enemyTarget.IsDead || !Enemies.Contains(enemyTarget))
{
Log.AddWarning("Please select a valid bug target for this card.");
return false;
}
}
Player.CurrentEnergy -= card.Cost;
Player.Hand.Remove(card);
OnCardPlayAnimation?.Invoke(new CardPlayAnimationEvent(
card.Id,
card.Name,
target?.Id,
card.TargetType == TargetType.AllEnemies
));
try
{
card.ApplyEffect(this, Player, target);
}
catch (Exception ex)
{
Log.AddFatal($"Runtime Exception while executing card effect: {ex.Message}");
}
foreach (var relic in Player.Relics)
{
relic.OnCardPlayed(this, card);
}
if (card.Exhausts)
{
Player.ExhaustPile.Add(card);
Log.AddInfo($"[Exhaust] Card '{card.Name}' was finalized and unallocated (Exhaust Pile).");
}
else
{
Player.DiscardPile.Add(card);
}
CleanupDeadEnemies();
CheckBattleConditions();
NotifyStateChanged();
return true;
}
public void EndPlayerTurn()
{
_ = EndPlayerTurnAsync(stepDelayMs: 0);
}
public async Task EndPlayerTurnAsync(
Func<Enemy, Task>? onBeforeEnemyActs = null,
Func<Enemy, Task>? onAfterEnemyActs = null,
int stepDelayMs = 1000)
{
if (Phase != CombatPhase.PlayerTurn) return;
Log.AddInfo("Ending player turn. Discarding active memory...");
Player.DiscardHand(Log);
NotifyStateChanged();
if (stepDelayMs > 0)
{
await Task.Delay(300);
}
Player.Statuses.OnTurnEnd(
logMsg => Log.AddWarning(logMsg),
dmg =>
{
var taken = Player.TakeDamage(dmg, isPiercing: true);
NotifyDamage(Player.Id, Player.Name, taken, isPiercing: true, isCrit: false, "MemoryLeak");
}
);
foreach (var relic in Player.Relics)
{
relic.OnTurnEnd(this);
}
if (Player.IsDead)
{
Phase = CombatPhase.Defeat;
Log.AddFatal("=== PRODUCTION CRASH: Uptime 0%, All Services Offline ===");
NotifyStateChanged();
return;
}
CheckBattleConditions();
if (Phase == CombatPhase.Victory) return;
await ExecuteEnemyTurnsAsync(onBeforeEnemyActs, onAfterEnemyActs, stepDelayMs);
}
private async Task ExecuteEnemyTurnsAsync(
Func<Enemy, Task>? onBeforeEnemyActs = null,
Func<Enemy, Task>? onAfterEnemyActs = null,
int stepDelayMs = 1000)
{
Phase = CombatPhase.EnemyTurn;
Log.AddInfo($"--- Turn {TurnNumber}: Bug Execution Phase ---");
NotifyStateChanged();
if (stepDelayMs > 0)
{
await Task.Delay(400);
}
foreach (var enemy in Enemies.ToList())
{
if (enemy.IsDead) continue;
ActiveActingEnemyId = enemy.Id;
NotifyStateChanged();
if (onBeforeEnemyActs != null)
{
await onBeforeEnemyActs(enemy);
}
if (stepDelayMs > 0)
{
await Task.Delay(stepDelayMs / 2);
}
enemy.ResetShield();
enemy.Statuses.OnTurnEnd(
logMsg => Log.AddInfo($"[{enemy.Name}] {logMsg}"),
dmg =>
{
var taken = enemy.TakeDamage(dmg, isPiercing: true);
NotifyDamage(enemy.Id, enemy.Name, taken, isPiercing: true, isCrit: false, "Debuff");
}
);
if (enemy.IsDead)
{
CleanupDeadEnemies();
ActiveActingEnemyId = null;
NotifyStateChanged();
continue;
}
enemy.ExecuteIntent(this);
NotifyStateChanged();
if (onAfterEnemyActs != null)
{
await onAfterEnemyActs(enemy);
}
if (stepDelayMs > 0)
{
await Task.Delay(stepDelayMs / 2);
}
if (Player.IsDead)
{
Phase = CombatPhase.Defeat;
ActiveActingEnemyId = null;
Log.AddFatal("=== PRODUCTION CRASH: Uptime 0%, All Services Offline ===");
NotifyStateChanged();
return;
}
}
ActiveActingEnemyId = null;
CleanupDeadEnemies();
CheckBattleConditions();
if (Phase != CombatPhase.Victory && Phase != CombatPhase.Defeat)
{
if (stepDelayMs > 0)
{
await Task.Delay(350);
}
TurnNumber++;
StartPlayerTurn();
}
}
private void CleanupDeadEnemies()
{
for (int i = Enemies.Count - 1; i >= 0; i--)
{
if (Enemies[i].IsDead)
{
Log.AddSuccess($"[RESOLVED] Bug '{Enemies[i].Name}' was successfully patched and disposed!");
Enemies.RemoveAt(i);
}
}
}
private void CheckBattleConditions()
{
if (Player.IsDead)
{
Phase = CombatPhase.Defeat;
}
else if (Enemies.Count == 0)
{
Phase = CombatPhase.Victory;
Log.AddSuccess("=== ALL BUGS RESOLVED! Build Deployed Successfully! ===");
}
}
public void NotifyStateChanged() => OnStateChanged?.Invoke();
}
+24
View File
@@ -0,0 +1,24 @@
namespace SlaySharp.Core.Combat;
public record DamageEvent(
string TargetId,
string TargetName,
int Amount,
bool IsPiercing,
bool IsCrit,
string SourceName
);
public record ShieldEvent(
string TargetId,
string TargetName,
int Amount,
string SourceName
);
public record CardPlayAnimationEvent(
string CardId,
string CardName,
string? TargetId,
bool IsAoe
);
+41
View File
@@ -0,0 +1,41 @@
namespace SlaySharp.Core.Combat;
public enum LogLevel
{
Info,
Success,
Warning,
Error,
Fatal
}
public record LogEntry(DateTime Timestamp, LogLevel Level, string Message)
{
public string FormattedTime => Timestamp.ToString("HH:mm:ss.fff");
}
public class CombatLog
{
private readonly List<LogEntry> _entries = new();
public IReadOnlyList<LogEntry> Entries => _entries;
public event Action? OnLogAdded;
public void Add(LogLevel level, string message)
{
_entries.Add(new LogEntry(DateTime.Now, level, message));
OnLogAdded?.Invoke();
}
public void AddInfo(string message) => Add(LogLevel.Info, message);
public void AddSuccess(string message) => Add(LogLevel.Success, message);
public void AddWarning(string message) => Add(LogLevel.Warning, message);
public void AddError(string message) => Add(LogLevel.Error, message);
public void AddFatal(string message) => Add(LogLevel.Fatal, message);
public void Clear()
{
_entries.Clear();
OnLogAdded?.Invoke();
}
}
+55
View File
@@ -0,0 +1,55 @@
namespace SlaySharp.Core.Enums;
public enum CardType
{
Attack,
Skill,
Power,
Curse
}
public enum CardRarity
{
Basic,
Common,
Uncommon,
Rare,
Curse
}
public enum TargetType
{
SingleEnemy,
AllEnemies,
Self,
None
}
public enum StatusType
{
Strength,
Vulnerable,
Weak,
MemoryLeak,
JitTier,
ShieldRetain,
Overclocked
}
public enum IntentType
{
Attack,
Defend,
Buff,
Debuff,
AttackDefend,
Special
}
public enum CombatPhase
{
PlayerTurn,
EnemyTurn,
Victory,
Defeat
}
+60
View File
@@ -0,0 +1,60 @@
using SlaySharp.Core.Combat;
using SlaySharp.Core.Enums;
namespace SlaySharp.Core.Models;
public abstract class Card
{
public string Id { get; } = Guid.NewGuid().ToString("N")[..8];
public string Name { get; protected set; }
public string SyntaxSignature { get; protected set; }
public string CodeSnippet { get; protected set; }
public string Description { get; protected set; }
public int Cost { get; protected set; }
public CardType Type { get; }
public CardRarity Rarity { get; }
public TargetType TargetType { get; }
public bool Upgraded { get; protected set; }
public bool Exhausts { get; protected set; }
protected Card(
string name,
string syntaxSignature,
string codeSnippet,
string description,
int cost,
CardType type,
CardRarity rarity,
TargetType targetType,
bool exhausts = false)
{
Name = name;
SyntaxSignature = syntaxSignature;
CodeSnippet = codeSnippet;
Description = description;
Cost = cost;
Type = type;
Rarity = rarity;
TargetType = targetType;
Exhausts = exhausts;
}
public virtual bool CanPlay(CombatEngine engine)
{
return engine.Player.CurrentEnergy >= Cost;
}
public abstract void ApplyEffect(CombatEngine engine, Player player, Character? target);
public virtual void Upgrade()
{
if (Upgraded) return;
Upgraded = true;
Name += "+";
OnUpgrade();
}
protected virtual void OnUpgrade() { }
public abstract Card Clone();
}
+67
View File
@@ -0,0 +1,67 @@
using SlaySharp.Core.Enums;
namespace SlaySharp.Core.Models;
public abstract class Character
{
public string Id { get; } = Guid.NewGuid().ToString("N")[..8];
public string Name { get; set; } = string.Empty;
public int MaxHp { get; set; }
public int CurrentHp { get; set; }
public int Shield { get; set; }
public StatusCollection Statuses { get; } = new();
public bool IsDead => CurrentHp <= 0;
protected Character(string name, int maxHp)
{
Name = name;
MaxHp = maxHp;
CurrentHp = maxHp;
}
public virtual int TakeDamage(int rawAmount, bool isPiercing = false)
{
if (IsDead || rawAmount <= 0) return 0;
var modifiedAmount = Statuses.CalculateIncomingDamage(rawAmount);
var damageToHp = modifiedAmount;
if (!isPiercing && Shield > 0)
{
if (Shield >= modifiedAmount)
{
Shield -= modifiedAmount;
damageToHp = 0;
}
else
{
damageToHp = modifiedAmount - Shield;
Shield = 0;
}
}
CurrentHp = Math.Max(0, CurrentHp - damageToHp);
return damageToHp;
}
public virtual void GainShield(int amount)
{
if (IsDead || amount <= 0) return;
Shield += amount;
}
public virtual void ResetShield()
{
if (Statuses.Get(StatusType.ShieldRetain) == 0)
{
Shield = 0;
}
}
public virtual void Heal(int amount)
{
if (IsDead || amount <= 0) return;
CurrentHp = Math.Min(MaxHp, CurrentHp + amount);
}
}
+20
View File
@@ -0,0 +1,20 @@
using SlaySharp.Core.Combat;
namespace SlaySharp.Core.Models;
public abstract class Enemy : Character
{
public string Description { get; set; } = string.Empty;
public string Icon { get; set; } = "👾";
public Intent? CurrentIntent { get; protected set; }
protected Enemy(string name, int maxHp, string icon = "👾", string description = "")
: base(name, maxHp)
{
Icon = icon;
Description = description;
}
public abstract void DecideNextIntent(CombatEngine engine);
public abstract void ExecuteIntent(CombatEngine engine);
}
+30
View File
@@ -0,0 +1,30 @@
using SlaySharp.Core.Enums;
namespace SlaySharp.Core.Models;
public record Intent(
IntentType Type,
int PrimaryValue,
int Hits = 1,
string Description = "",
StatusType? DebuffToApply = null,
int DebuffStacks = 0
)
{
public string DisplayText
{
get
{
return Type switch
{
IntentType.Attack => Hits > 1 ? $"{PrimaryValue} x {Hits} dmg" : $"{PrimaryValue} dmg",
IntentType.Defend => $"+{PrimaryValue} Shield",
IntentType.AttackDefend => $"{PrimaryValue} dmg & +{PrimaryValue} Shield",
IntentType.Debuff => DebuffToApply.HasValue ? $"Debuff: {DebuffToApply.Value} ({DebuffStacks})" : "Debuff",
IntentType.Buff => $"+{PrimaryValue} Buff",
IntentType.Special => Description,
_ => Description
};
}
}
}
+102
View File
@@ -0,0 +1,102 @@
using SlaySharp.Core.Combat;
using SlaySharp.Core.Enums;
using SlaySharp.Core.Relics;
namespace SlaySharp.Core.Models;
public class Player : Character
{
public int MaxEnergy { get; set; } = 3;
public int CurrentEnergy { get; set; } = 3;
public List<Card> MasterDeck { get; } = new();
public List<Card> DrawPile { get; } = new();
public List<Card> Hand { get; } = new();
public List<Card> DiscardPile { get; } = new();
public List<Card> ExhaustPile { get; } = new();
public List<Relic> Relics { get; } = new();
private readonly Random _rng;
public Player(string name, int maxHp, int maxEnergy = 3, int? seed = null)
: base(name, maxHp)
{
MaxEnergy = maxEnergy;
CurrentEnergy = maxEnergy;
_rng = seed.HasValue ? new Random(seed.Value) : new Random();
}
public void InitCombatDeck()
{
DrawPile.Clear();
Hand.Clear();
DiscardPile.Clear();
ExhaustPile.Clear();
Shield = 0;
Statuses.Clear();
foreach (var card in MasterDeck)
{
DrawPile.Add(card.Clone());
}
ShuffleDrawPile();
}
public void ShuffleDrawPile()
{
int n = DrawPile.Count;
while (n > 1)
{
n--;
int k = _rng.Next(n + 1);
(DrawPile[k], DrawPile[n]) = (DrawPile[n], DrawPile[k]);
}
}
public void DrawCards(int count, CombatLog? log = null)
{
for (int i = 0; i < count; i++)
{
if (Hand.Count >= 10)
{
log?.AddWarning("Hand buffer overflow! Max 10 cards in hand.");
break;
}
if (DrawPile.Count == 0)
{
if (DiscardPile.Count == 0)
{
break;
}
log?.AddInfo("Recompiling draw pile from deallocated heap (Discard Pile)...");
DrawPile.AddRange(DiscardPile);
DiscardPile.Clear();
ShuffleDrawPile();
}
if (DrawPile.Count > 0)
{
var card = DrawPile[0];
DrawPile.RemoveAt(0);
Hand.Add(card);
}
}
}
public void DiscardHand(CombatLog? log = null)
{
foreach (var card in Hand)
{
DiscardPile.Add(card);
}
Hand.Clear();
}
public void ResetTurnEnergy()
{
CurrentEnergy = MaxEnergy;
}
}
@@ -0,0 +1,79 @@
using SlaySharp.Core.Enums;
namespace SlaySharp.Core.Models;
public class StatusCollection
{
private readonly Dictionary<StatusType, int> _statuses = new();
public IReadOnlyDictionary<StatusType, int> All => _statuses;
public int Get(StatusType type) => _statuses.TryGetValue(type, out var val) ? val : 0;
public void Add(StatusType type, int amount)
{
if (amount <= 0) return;
_statuses[type] = Get(type) + amount;
}
public void Remove(StatusType type, int amount)
{
if (!_statuses.TryGetValue(type, out var current)) return;
var newVal = current - amount;
if (newVal <= 0)
{
_statuses.Remove(type);
}
else
{
_statuses[type] = newVal;
}
}
public void Clear() => _statuses.Clear();
public int CalculateOutgoingDamage(int baseDamage)
{
var dmg = (double)baseDamage;
dmg += Get(StatusType.Strength);
if (Get(StatusType.Weak) > 0)
{
dmg *= 0.75;
}
return Math.Max(0, (int)Math.Round(dmg, MidpointRounding.AwayFromZero));
}
public int CalculateIncomingDamage(int incomingDamage)
{
var dmg = (double)incomingDamage;
if (Get(StatusType.Vulnerable) > 0)
{
dmg *= 1.5;
}
return Math.Max(0, (int)Math.Round(dmg, MidpointRounding.AwayFromZero));
}
public void OnTurnEnd(Action<string>? logAction = null, Action<int>? takeDamageAction = null)
{
// Decay standard turn debuffs
if (Get(StatusType.Vulnerable) > 0)
{
Remove(StatusType.Vulnerable, 1);
}
if (Get(StatusType.Weak) > 0)
{
Remove(StatusType.Weak, 1);
}
// Memory leak causes damage over time
var leaks = Get(StatusType.MemoryLeak);
if (leaks > 0)
{
logAction?.Invoke($"MemoryLeak triggered! Taking {leaks} damage from unmanaged resources.");
takeDamageAction?.Invoke(leaks);
}
}
}
+85
View File
@@ -0,0 +1,85 @@
using SlaySharp.Core.Combat;
namespace SlaySharp.Core.Relics;
public abstract class Relic
{
public string Id { get; }
public string Name { get; }
public string PackageVersion { get; }
public string Description { get; }
public string Icon { get; }
protected Relic(string id, string name, string packageVersion, string description, string icon = "📦")
{
Id = id;
Name = name;
PackageVersion = packageVersion;
Description = description;
Icon = icon;
}
public virtual void OnCombatStart(CombatEngine engine) { }
public virtual void OnTurnStart(CombatEngine engine) { }
public virtual void OnTurnEnd(CombatEngine engine) { }
public virtual void OnCardPlayed(CombatEngine engine, Models.Card card) { }
}
public class NewtonsoftJsonRelic : Relic
{
public NewtonsoftJsonRelic() : base(
"Newtonsoft.Json",
"Newtonsoft.Json",
"v13.0.3",
"Draw +1 extra card at the start of combat (Memory deserialization buffer).",
"📜"
) { }
public override void OnCombatStart(CombatEngine engine)
{
engine.Player.DrawCards(1, engine.Log);
}
}
public class BenchmarkDotNetRelic : Relic
{
private int _turnCounter;
public BenchmarkDotNetRelic() : base(
"BenchmarkDotNet",
"BenchmarkDotNet",
"v0.13.12",
"Every 2nd turn, micro-benchmark optimizations grant +1 extra CPU Cycle.",
"⚡"
) { }
public override void OnTurnStart(CombatEngine engine)
{
_turnCounter++;
if (_turnCounter % 2 == 0)
{
engine.Player.CurrentEnergy += 1;
engine.Log.AddInfo($"[BenchmarkDotNet] Micro-benchmark boost! +1 CPU Cycle (Available: {engine.Player.CurrentEnergy})");
}
}
}
public class SerilogRelic : Relic
{
public SerilogRelic() : base(
"Serilog",
"Serilog",
"v4.0.0",
"Whenever you play an Attack card, gain +2 Shield (Structured diagnostic logging).",
"📊"
) { }
public override void OnCardPlayed(CombatEngine engine, Models.Card card)
{
if (card.Type == Enums.CardType.Attack)
{
engine.Player.GainShield(2);
engine.Log.AddInfo("[Serilog] Diagnostic trace emitted: +2 Shield.");
}
}
}
+9
View File
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>