This commit is contained in:
Michal Vrba
2026-09-16 23:37:16 +02:00
parent 121d013d14
commit d3c419f8ce
30 changed files with 3151 additions and 350 deletions
+163 -20
View File
@@ -26,13 +26,13 @@ public class ConsoleWriteLineCard : Card
CodeSnippet = "target.Hit(9);";
}
public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
=> (_damage, _damage, 1, false);
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);
var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, _damage, isPiercing: false, Name);
engine.Log.AddInfo($"[Console.WriteLine] {target.Name} took {dealt} dmg.");
}
@@ -68,6 +68,8 @@ public class TryCatchCard : Card
CodeSnippet = "shield += 8;";
}
public override int GetShieldProfile(CombatEngine engine) => _shield;
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
player.GainShield(_shield);
@@ -85,6 +87,91 @@ public class TryCatchCard : Card
};
}
public class RandomNextCard : Card
{
private int _min = 3;
private int _max = 14;
public RandomNextCard() : base(
"Random.Shared.Next",
"Random.Shared.Next(3, 15);",
"target.Hit(Random(3..14));",
"Deal 3 to 14 random damage.",
cost: 1,
type: CardType.Attack,
rarity: CardRarity.Common,
targetType: TargetType.SingleEnemy
) { }
protected override void OnUpgrade()
{
_min = 6;
_max = 20;
Description = "Deal 6 to 20 random damage.";
CodeSnippet = "target.Hit(Random(6..20));";
}
public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
=> (_min, _max, 1, false);
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
if (target == null) return;
var rolledDmg = Random.Shared.Next(_min, _max + 1);
var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, rolledDmg, isPiercing: false, Name);
engine.Log.AddSuccess($"[Random.Shared] 🎲 ROLLED {rolledDmg} base dmg! Dealt {dealt} total to {target.Name}.");
}
public override Card Clone() => new RandomNextCard
{
Upgraded = this.Upgraded,
Name = this.Name,
Description = this.Description,
_min = this._min,
_max = this._max,
CodeSnippet = this.CodeSnippet
};
}
public class DynamicExpandoCard : Card
{
public DynamicExpandoCard() : base(
"dynamic ExpandoObject",
"dynamic obj = new ExpandoObject();",
"Hand.Add(RandomCard(Cost: 0));",
"Add a random 0-cost card to your hand.",
cost: 1,
type: CardType.Skill,
rarity: CardRarity.Uncommon,
targetType: TargetType.Self
) { }
protected override void OnUpgrade()
{
Description = "Add a random Upgraded 0-cost card to hand.";
CodeSnippet = "Hand.Add(RandomUpgradedCard(0));";
}
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
var randomCard = CardCatalog.GetRandomCard();
if (Upgraded) randomCard.Upgrade();
var cloned = randomCard.Clone();
cloned.SetCostOverride(0);
player.Hand.Add(cloned);
engine.Log.AddSuccess($"[dynamic ExpandoObject] 🎲 Runtime IL Emit generated '{cloned.Name}' (0 CPU) into hand!");
}
public override Card Clone() => new DynamicExpandoCard
{
Upgraded = this.Upgraded,
Name = this.Name,
Description = this.Description
};
}
public class GcCollectCard : Card
{
private int _baseDamage = 8;
@@ -107,15 +194,19 @@ public class GcCollectCard : Card
CodeSnippet = "target.Hit(12 + Discard * 2);";
}
public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
{
var discardCount = engine.Player.DiscardPile.Count;
var dmg = _baseDamage + (discardCount * 2);
return (dmg, dmg, 1, false);
}
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);
var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, rawDmg, isPiercing: false, Name);
engine.Log.AddSuccess($"[GC.Collect] Swept {discardCount} objects! Dealt {dealt} dmg.");
}
@@ -151,13 +242,13 @@ public class SpanSliceCard : Card
CodeSnippet = "target.DirectHit(11);";
}
public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
=> (_damage, _damage, 1, true);
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);
var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, _damage, isPiercing: true, Name);
engine.Log.AddSuccess($"[Span.Slice] Pierced shield! Dealt {dealt} dmg.");
}
@@ -193,14 +284,14 @@ public class ThrowExceptionCard : Card
CodeSnippet = "target.Hit(22); // +1 Leak";
}
public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
=> (_damage, _damage, 1, false);
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);
var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, _damage, isPiercing: false, Name);
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.");
}
@@ -311,19 +402,19 @@ public class ParallelForEachCard : Card
CodeSnippet = "target.Hit(5); // x3 hits";
}
public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
=> (_dmgPerHit, _dmgPerHit, 3, false);
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);
var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, _dmgPerHit, isPiercing: false, $"{Name} #{i+1}");
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.");
}
@@ -360,6 +451,8 @@ public class LinqWhereCard : Card
CodeSnippet = "shield += 9; Draw(1);";
}
public override int GetShieldProfile(CombatEngine engine) => _shield;
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
player.GainShield(_shield);
@@ -397,6 +490,8 @@ public class NullCoalescingCard : Card
CodeSnippet = "ClearDebuffs(); shield += 8;";
}
public override int GetShieldProfile(CombatEngine engine) => Upgraded ? 8 : 6;
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
{
var stacksToRemove = Upgraded ? 2 : 1;
@@ -416,3 +511,51 @@ public class NullCoalescingCard : Card
Description = this.Description
};
}
public static class CardCatalog
{
public static List<Func<Card>> AllCardFactories => new()
{
() => new ConsoleWriteLineCard(),
() => new TryCatchCard(),
() => new RandomNextCard(),
() => new DynamicExpandoCard(),
() => new GcCollectCard(),
() => new SpanSliceCard(),
() => new ThrowExceptionCard(),
() => new TieredCompilationCard(),
() => new ThreadSleepCard(),
() => new ParallelForEachCard(),
() => new LinqWhereCard(),
() => new NullCoalescingCard()
};
public static Card GetRandomCard()
{
var factories = AllCardFactories;
var index = Random.Shared.Next(factories.Count);
return factories[index]();
}
public static List<Card> RollThreeCardRewards()
{
var pool = AllCardFactories;
var chosen = new HashSet<int>();
var result = new List<Card>();
while (result.Count < 3 && chosen.Count < pool.Count)
{
var idx = Random.Shared.Next(pool.Count);
if (chosen.Add(idx))
{
var card = pool[idx]();
if (Random.Shared.Next(100) < 25)
{
card.Upgrade();
}
result.Add(card);
}
}
return result;
}
}
+74 -6
View File
@@ -8,8 +8,8 @@ public class NullReferenceExceptionEnemy : Enemy
{
private int _patternStep = 0;
public NullReferenceExceptionEnemy()
: base("NullReferenceException", 45, "💥", "Unhandled null pointer reference causing sudden application crashes.")
public NullReferenceExceptionEnemy(int hpBonus = 0)
: base("NullReferenceException", 45 + hpBonus, "💥", "Unhandled null pointer reference causing sudden application crashes.")
{
}
@@ -71,8 +71,8 @@ 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 OffByOneBugEnemy(int hpBonus = 0)
: base("Off-By-One Bug", 22 + hpBonus, "🐛", "Iterative boundary bug indexing `for (int i = 0; i <= arr.Length; i++)`.")
{
}
@@ -121,8 +121,8 @@ 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 MemoryLeakSpectreEnemy(int hpBonus = 0)
: base("Memory Leak Spectre", 34 + hpBonus, "👻", "Unmanaged IDisposable and dangling event handlers slowly devouring RAM.")
{
}
@@ -167,3 +167,71 @@ public class MemoryLeakSpectreEnemy : Enemy
DecideNextIntent(engine);
}
}
public class LegacyMonolithBossEnemy : Enemy
{
private int _step = 0;
public LegacyMonolithBossEnemy()
: base("Legacy .NET 2.0 Monolith", 85, "🏢", "Ancient monolithic server running on Windows Server 2003 with zero async code.")
{
}
public override void DecideNextIntent(CombatEngine engine)
{
CurrentIntent = (_step % 4) switch
{
0 => new Intent(IntentType.AttackDefend, 8, 1, "Synchronous Lock (8 dmg & +12 Shield)"),
1 => new Intent(IntentType.Debuff, 0, 1, "Thread Starvation (+2 Leak, +1 Weak)", StatusType.MemoryLeak, 2),
2 => new Intent(IntentType.Attack, 18, 1, "GC Gen-2 Global Lockout (18 dmg)"),
3 => new Intent(IntentType.Attack, 5, 3, "COM Interop Cascade (5x3 dmg)"),
_ => new Intent(IntentType.Attack, 10, 1, "Monolith Strike")
};
}
public override void ExecuteIntent(CombatEngine engine)
{
if (CurrentIntent == null) return;
switch (_step % 4)
{
case 0:
var dmg0 = Statuses.CalculateOutgoingDamage(8);
var isCrit0 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
var dealt0 = engine.Player.TakeDamage(dmg0);
GainShield(12);
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt0, false, isCrit0, Name);
engine.NotifyShield(Id, Name, 12, Name);
engine.Log.AddError($"[{Name}] locked main UI thread! Dealt {dealt0} dmg and gained +12 Shield.");
break;
case 1:
engine.Player.Statuses.Add(StatusType.MemoryLeak, 2);
engine.Player.Statuses.Add(StatusType.Weak, 1);
GainShield(8);
engine.NotifyShield(Id, Name, 8, Name);
engine.Log.AddFatal($"[{Name}] starved worker thread pool! Player gained +2 Memory Leak and 1 Weak.");
break;
case 2:
var dmg2 = Statuses.CalculateOutgoingDamage(18);
var isCrit2 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
var dealt2 = engine.Player.TakeDamage(dmg2);
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt2, false, isCrit2, Name);
engine.Log.AddFatal($"[{Name}] FULL CLR CRASH WAVE! Dealt massive {dealt2} damage!");
break;
case 3:
int total = 0;
var isCrit3 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
for (int i = 0; i < 3; i++)
{
var d = Statuses.CalculateOutgoingDamage(5);
total += engine.Player.TakeDamage(d);
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, d, false, isCrit3, $"{Name} #{i+1}");
}
engine.Log.AddError($"[{Name}] COM interop DLLs cascaded! Dealt total {total} damage!");
break;
}
_step++;
DecideNextIntent(engine);
}
}
+14 -12
View File
@@ -8,18 +8,20 @@ public static class StarterDecks
{
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()
new ConsoleWriteLineCard(), // 1 CPU
new ConsoleWriteLineCard(), // 1 CPU
new ConsoleWriteLineCard(), // 1 CPU
new TryCatchCard(), // 1 CPU
new TryCatchCard(), // 1 CPU
new TryCatchCard(), // 1 CPU
new RandomNextCard(), // 1 CPU - RNG (3-14 dmg)
new DynamicExpandoCard(), // 1 CPU - RNG (Generate random 0-cost card)
new ThreadSleepCard(), // 0 CPU - Ramp (+2 CPU)
new SpanSliceCard(), // 1 CPU - Piercing
new LinqWhereCard(), // 1 CPU - Shield & Draw
new TieredCompilationCard(), // 1 CPU - Power (+2 Strength)
new GcCollectCard(), // 2 CPU - Scaler
new ThrowExceptionCard() // 2 CPU - Heavy Burst (16 dmg)
};
}
}
+86 -1
View File
@@ -1,3 +1,4 @@
using SlaySharp.Core.Catalogs;
using SlaySharp.Core.Enums;
using SlaySharp.Core.Models;
@@ -11,6 +12,7 @@ public class CombatEngine
public CombatPhase Phase { get; private set; } = CombatPhase.PlayerTurn;
public CombatLog Log { get; } = new();
public string? ActiveActingEnemyId { get; private set; }
public List<Card> CardRewards { get; private set; } = new();
public event Action? OnStateChanged;
public event Action<DamageEvent>? OnDamageDealt;
@@ -27,6 +29,79 @@ public class CombatEngine
OnShieldGained?.Invoke(new ShieldEvent(targetId, targetName, amount, sourceName));
}
public (int dealtDamage, bool isCrit) CalculateAndDealDamage(Player player, Character target, int baseDamage, bool isPiercing, string sourceName)
{
var targetIsVulnerable = target.Statuses.Get(StatusType.Vulnerable) > 0;
var rngCrit = Random.Shared.Next(100) < 15; // 15% JIT Branch Prediction Crit chance!
var isCrit = targetIsVulnerable || rngCrit;
var modifiedDmg = player.Statuses.CalculateOutgoingDamage(baseDamage);
if (rngCrit && !targetIsVulnerable)
{
modifiedDmg = (int)Math.Round(modifiedDmg * 1.5, MidpointRounding.AwayFromZero);
Log.AddSuccess($"[JIT Branch Prediction] 🎲 CRITICAL HIT on {target.Name} (1.5x damage)!");
}
var dealt = target.TakeDamage(modifiedDmg, isPiercing);
NotifyDamage(target.Id, target.Name, dealt, isPiercing, isCrit, sourceName);
return (dealt, isCrit);
}
public DamagePreviewInfo CalculateDamagePreview(Card card, Enemy enemy)
{
var (minRaw, maxRaw, hits, isPiercing) = card.GetDamageProfile(this, enemy);
if (minRaw == 0 && maxRaw == 0)
{
return new DamagePreviewInfo(0, 0, 0, false, 0, 0, 0, enemy.CurrentHp, false, false);
}
var isCrit = enemy.Statuses.Get(StatusType.Vulnerable) > 0;
int minTotal = 0;
int maxTotal = 0;
for (int i = 0; i < hits; i++)
{
var minHit = Player.Statuses.CalculateOutgoingDamage(minRaw);
var maxHit = Player.Statuses.CalculateOutgoingDamage(maxRaw);
minTotal += minHit;
maxTotal += maxHit;
}
int shieldAbsorbed = 0;
int minHpLoss = 0;
int maxHpLoss = 0;
if (isPiercing)
{
minHpLoss = minTotal;
maxHpLoss = maxTotal;
}
else
{
shieldAbsorbed = Math.Min(enemy.Shield, minTotal);
minHpLoss = Math.Max(0, minTotal - enemy.Shield);
maxHpLoss = Math.Max(0, maxTotal - enemy.Shield);
}
int projectedRemaining = Math.Max(0, enemy.CurrentHp - minHpLoss);
bool isLethal = minHpLoss >= enemy.CurrentHp;
return new DamagePreviewInfo(
minTotal,
maxTotal,
hits,
isPiercing,
shieldAbsorbed,
minHpLoss,
maxHpLoss,
projectedRemaining,
isLethal,
isCrit
);
}
public void StartCombat(Player player, List<Enemy> enemies)
{
Player = player;
@@ -34,6 +109,7 @@ public class CombatEngine
TurnNumber = 1;
Phase = CombatPhase.PlayerTurn;
ActiveActingEnemyId = null;
CardRewards.Clear();
Log.Clear();
Log.AddInfo($"=== Starting Incident Resolution: {enemies.Count} Bugs Detected ===");
@@ -301,9 +377,18 @@ public class CombatEngine
else if (Enemies.Count == 0)
{
Phase = CombatPhase.Victory;
CardRewards = CardCatalog.RollThreeCardRewards();
Log.AddSuccess("=== ALL BUGS RESOLVED! Build Deployed Successfully! ===");
}
}
public void ClaimCardReward(Card card)
{
Player.MasterDeck.Add(card.Clone());
Log.AddSuccess($"[Pull Request Merged] Added '{card.Name}' to Master Deck!");
CardRewards.Clear();
NotifyStateChanged();
}
public void NotifyStateChanged() => OnStateChanged?.Invoke();
}
}
+16 -1
View File
@@ -44,8 +44,23 @@ public abstract class Card
return engine.Player.CurrentEnergy >= Cost;
}
public void SetCostOverride(int newCost)
{
Cost = newCost;
}
public abstract void ApplyEffect(CombatEngine engine, Player player, Character? target);
public virtual (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
{
return (0, 0, 1, false);
}
public virtual int GetShieldProfile(CombatEngine engine)
{
return 0;
}
public virtual void Upgrade()
{
if (Upgraded) return;
@@ -57,4 +72,4 @@ public abstract class Card
protected virtual void OnUpgrade() { }
public abstract Card Clone();
}
}
@@ -0,0 +1,14 @@
namespace SlaySharp.Core.Models;
public record DamagePreviewInfo(
int MinRawDamage,
int MaxRawDamage,
int Hits,
bool IsPiercing,
int ShieldAbsorbed,
int MinHpLoss,
int MaxHpLoss,
int ProjectedRemainingHp,
bool IsLethal,
bool IsCrit
);
+57 -1
View File
@@ -1,4 +1,4 @@
using SlaySharp.Core.Combat;
using SlaySharp.Core.Combat;
namespace SlaySharp.Core.Relics;
@@ -83,3 +83,59 @@ public class SerilogRelic : Relic
}
}
}
public class PollyRelic : Relic
{
private bool _triggered;
public PollyRelic() : base(
"Polly",
"Polly",
"v8.3.0",
"Once per combat: When taking damage, trigger resilience policy for +12 Shield.",
"🛡️"
) { }
public override void OnCombatStart(CombatEngine engine)
{
_triggered = false;
}
public override void OnTurnStart(CombatEngine engine)
{
if (!_triggered && engine.Player.CurrentHp < (engine.Player.MaxHp / 2))
{
_triggered = true;
engine.Player.GainShield(12);
engine.Log.AddInfo("[Polly] Circuit breaker triggered! Resilience policy deployed +12 Shield.");
}
}
}
public class DapperRelic : Relic
{
private bool _attackPlayedThisTurn;
public DapperRelic() : base(
"Dapper",
"Dapper",
"v2.1.35",
"Micro-ORM caching: The first Attack card played each turn grants +3 Shield.",
"⚡"
) { }
public override void OnTurnStart(CombatEngine engine)
{
_attackPlayedThisTurn = false;
}
public override void OnCardPlayed(CombatEngine engine, Models.Card card)
{
if (!_attackPlayedThisTurn && card.Type == Enums.CardType.Attack)
{
_attackPlayedThisTurn = true;
engine.Player.GainShield(3);
engine.Log.AddInfo("[Dapper] High-speed micro-ORM query: +3 Shield gained.");
}
}
}
+163
View File
@@ -0,0 +1,163 @@
using SlaySharp.Core.Catalogs;
using SlaySharp.Core.Combat;
using SlaySharp.Core.Models;
using SlaySharp.Core.Relics;
namespace SlaySharp.Core.Run;
public enum RunScreen
{
Title,
Map,
Combat,
Event,
RestSite,
Shop,
RunVictory,
RunDefeat
}
public class GameRun
{
public Player Player { get; set; } = null!;
public MapGraph Map { get; set; } = null!;
public MapNode? CurrentNode { get; set; }
public HashSet<string> VisitedNodeIds { get; } = new();
public int DevCredits { get; set; } = 60;
public RunScreen CurrentScreen { get; set; } = RunScreen.Title;
public CombatEngine? ActiveCombat { get; set; }
public StoryEvent? ActiveEvent { get; set; }
public event Action? OnRunStateChanged;
public void StartNewRun(int? seed = null)
{
Player = new Player("Senior Dev", 70, maxEnergy: 3, seed: seed);
Player.MasterDeck.AddRange(StarterDecks.CreateCsharpArchitectDeck());
Player.Relics.Add(new NewtonsoftJsonRelic());
Player.Relics.Add(new BenchmarkDotNetRelic());
Player.Relics.Add(new SerilogRelic());
DevCredits = 60;
VisitedNodeIds.Clear();
CurrentNode = null;
ActiveCombat = null;
ActiveEvent = null;
Map = MapGraph.GenerateAct1Map(seed);
CurrentScreen = RunScreen.Map;
NotifyChange();
}
public bool CanVisitNode(MapNode node)
{
if (CurrentNode == null)
{
// Can visit any floor 0 node
return node.Floor == 0;
}
return CurrentNode.NextNodeIds.Contains(node.Id);
}
public void SelectNode(MapNode node)
{
if (!CanVisitNode(node)) return;
CurrentNode = node;
VisitedNodeIds.Add(node.Id);
switch (node.Type)
{
case MapNodeType.Combat:
StartCombatEncounter(isElite: false, isBoss: false);
break;
case MapNodeType.Elite:
StartCombatEncounter(isElite: true, isBoss: false);
break;
case MapNodeType.Boss:
StartCombatEncounter(isElite: false, isBoss: true);
break;
case MapNodeType.RestSite:
CurrentScreen = RunScreen.RestSite;
break;
case MapNodeType.Shop:
CurrentScreen = RunScreen.Shop;
break;
case MapNodeType.MysteryEvent:
StartRandomEvent();
break;
}
NotifyChange();
}
private void StartCombatEncounter(bool isElite, bool isBoss)
{
List<Enemy> enemies;
if (isBoss)
{
enemies = new List<Enemy> { new LegacyMonolithBossEnemy() };
}
else if (isElite)
{
enemies = new List<Enemy> { new MemoryLeakSpectreEnemy(hpBonus: 6), new OffByOneBugEnemy() };
}
else
{
// Standard combat
var roll = Random.Shared.Next(2);
enemies = roll switch
{
0 => new List<Enemy> { new NullReferenceExceptionEnemy(), new OffByOneBugEnemy() },
_ => new List<Enemy> { new MemoryLeakSpectreEnemy(), new OffByOneBugEnemy() }
};
}
ActiveCombat = new CombatEngine();
ActiveCombat.StartCombat(Player, enemies);
CurrentScreen = RunScreen.Combat;
}
private void StartRandomEvent()
{
var events = StoryEvent.GetAct1Events();
var index = Random.Shared.Next(events.Count);
ActiveEvent = events[index];
CurrentScreen = RunScreen.Event;
}
public void CompleteCombatAndClaimRewards(int bonusCredits = 25)
{
DevCredits += bonusCredits;
if (CurrentNode?.Type == MapNodeType.Boss)
{
CurrentScreen = RunScreen.RunVictory;
}
else
{
CurrentScreen = RunScreen.Map;
}
ActiveCombat = null;
NotifyChange();
}
public void ReturnToMap()
{
if (Player.IsDead)
{
CurrentScreen = RunScreen.RunDefeat;
}
else
{
CurrentScreen = RunScreen.Map;
}
ActiveCombat = null;
ActiveEvent = null;
NotifyChange();
}
public void NotifyChange() => OnRunStateChanged?.Invoke();
}
+99
View File
@@ -0,0 +1,99 @@
namespace SlaySharp.Core.Run;
public class MapGraph
{
public List<List<MapNode>> Floors { get; } = new();
public static MapGraph GenerateAct1Map(int? seed = null)
{
var rng = seed.HasValue ? new Random(seed.Value) : Random.Shared;
var map = new MapGraph();
// Floor 0: Starter Pull Requests (2 Combats)
var floor0 = new List<MapNode>
{
new MapNode { Floor = 0, Column = 0, Type = MapNodeType.Combat, Title = "PR #101: Code Review", Description = "Resolve unit test failures on branch feature/auth." },
new MapNode { Floor = 0, Column = 1, Type = MapNodeType.Combat, Title = "PR #102: API Gateway Glitch", Description = "Fix routing exceptions in reverse proxy." }
};
map.Floors.Add(floor0);
// Floor 1: 3 Nodes (Mystery, Combat, Mystery)
var floor1 = new List<MapNode>
{
new MapNode { Floor = 1, Column = 0, Type = MapNodeType.MysteryEvent, Title = "Stack Overflow Thread", Description = "An enigmatic developer answered an old issue." },
new MapNode { Floor = 1, Column = 1, Type = MapNodeType.Combat, Title = "PR #108: Cache Invalidation", Description = "Memory corruption in Redis cache layer." },
new MapNode { Floor = 1, Column = 2, Type = MapNodeType.MysteryEvent, Title = "Junior Dev PR", Description = "A pull request with 8000 lines of code changes." }
};
map.Floors.Add(floor1);
// Floor 2: 2 Nodes (Shop, Elite)
var floor2 = new List<MapNode>
{
new MapNode { Floor = 2, Column = 0, Type = MapNodeType.Shop, Title = "NuGet Marketplace Feed", Description = "Procure enterprise diagnostic libraries." },
new MapNode { Floor = 2, Column = 1, Type = MapNodeType.Elite, Title = "SEV-1 Critical Outage", Description = "Heavy memory leak threatening pod stability." }
};
map.Floors.Add(floor2);
// Floor 3: 2 Nodes (RestSite, RestSite)
var floor3 = new List<MapNode>
{
new MapNode { Floor = 3, Column = 0, Type = MapNodeType.RestSite, Title = "Espresso & Refactor Bar", Description = "Recharge SLA or optimize Roslyn AST cards." },
new MapNode { Floor = 3, Column = 1, Type = MapNodeType.RestSite, Title = "Dev Lounge Coffee Machine", Description = "Quick break before staging deployment." }
};
map.Floors.Add(floor3);
// Floor 4: 2 Nodes (Mystery, Elite)
var floor4 = new List<MapNode>
{
new MapNode { Floor = 4, Column = 0, Type = MapNodeType.MysteryEvent, Title = "Legacy COM Discovery", Description = "Ancient unmanaged DLL found in codebase." },
new MapNode { Floor = 4, Column = 1, Type = MapNodeType.Elite, Title = "Deadlock Demon Outage", Description = "Thread pool starvation across microservices." }
};
map.Floors.Add(floor4);
// Floor 5: 1 Node (RestSite before Boss)
var floor5 = new List<MapNode>
{
new MapNode { Floor = 5, Column = 0, Type = MapNodeType.RestSite, Title = "Final Staging Gate Review", Description = "Last chance to refactor cards before Release Gate." }
};
map.Floors.Add(floor5);
// Floor 6: 1 Node (Boss)
var floor6 = new List<MapNode>
{
new MapNode { Floor = 6, Column = 0, Type = MapNodeType.Boss, Title = "BOSS: Friday 5 PM Monolith", Description = "The ultimate legacy .NET Framework 2.0 Monolith release." }
};
map.Floors.Add(floor6);
// Connect Nodes
// Floor 0 -> Floor 1
floor0[0].NextNodeIds.AddRange(new[] { floor1[0].Id, floor1[1].Id });
floor0[1].NextNodeIds.AddRange(new[] { floor1[1].Id, floor1[2].Id });
// Floor 1 -> Floor 2
floor1[0].NextNodeIds.Add(floor2[0].Id);
floor1[1].NextNodeIds.AddRange(new[] { floor2[0].Id, floor2[1].Id });
floor1[2].NextNodeIds.Add(floor2[1].Id);
// Floor 2 -> Floor 3
floor2[0].NextNodeIds.AddRange(new[] { floor3[0].Id, floor3[1].Id });
floor2[1].NextNodeIds.AddRange(new[] { floor3[0].Id, floor3[1].Id });
// Floor 3 -> Floor 4
floor3[0].NextNodeIds.AddRange(new[] { floor4[0].Id, floor4[1].Id });
floor3[1].NextNodeIds.AddRange(new[] { floor4[0].Id, floor4[1].Id });
// Floor 4 -> Floor 5
floor4[0].NextNodeIds.Add(floor5[0].Id);
floor4[1].NextNodeIds.Add(floor5[0].Id);
// Floor 5 -> Floor 6
floor5[0].NextNodeIds.Add(floor6[0].Id);
return map;
}
public MapNode? FindNode(string id)
{
return Floors.SelectMany(f => f).FirstOrDefault(n => n.Id == id);
}
}
+44
View File
@@ -0,0 +1,44 @@
namespace SlaySharp.Core.Run;
public enum MapNodeType
{
Combat,
Elite,
RestSite,
Shop,
MysteryEvent,
Boss
}
public class MapNode
{
public string Id { get; set; } = Guid.NewGuid().ToString("N")[..8];
public int Floor { get; set; }
public int Column { get; set; }
public MapNodeType Type { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public List<string> NextNodeIds { get; set; } = new();
public string Icon => Type switch
{
MapNodeType.Combat => "⚔️",
MapNodeType.Elite => "☠️",
MapNodeType.RestSite => "☕",
MapNodeType.Shop => "🛒",
MapNodeType.MysteryEvent => "❓",
MapNodeType.Boss => "👑",
_ => "📍"
};
public string BadgeColor => Type switch
{
MapNodeType.Combat => "#61afef",
MapNodeType.Elite => "#e06c75",
MapNodeType.RestSite => "#98c379",
MapNodeType.Shop => "#e5c07b",
MapNodeType.MysteryEvent => "#c678dd",
MapNodeType.Boss => "#ff4757",
_ => "#abb2bf"
};
}
+140
View File
@@ -0,0 +1,140 @@
using SlaySharp.Core.Catalogs;
using SlaySharp.Core.Models;
namespace SlaySharp.Core.Run;
public record StoryOption(
string ChoiceText,
string OutcomeText,
Action<GameRun> ApplyEffect
);
public class StoryEvent
{
public string Id { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Icon { get; set; } = "❓";
public string Lore { get; set; } = string.Empty;
public List<StoryOption> Options { get; set; } = new();
public static List<StoryEvent> GetAct1Events()
{
return new List<StoryEvent>
{
new StoryEvent
{
Id = "stackoverflow",
Title = "The 12-Year-Old Stack Overflow Answer",
Icon = "🌐",
Lore = "While investigating a rare memory access bug, you find a forum answer from 2014 by a developer named 'JonSkeetFan99'. The solution uses dangerous unsafe pointer arithmetic.",
Options = new List<StoryOption>
{
new StoryOption(
"Copy-Paste Without Reading [Gain Upgraded Span.Slice+, lose 8 Max SLA]",
"You paste the snippet into production. It executes at blinding speed, but technical debt accrues!",
run =>
{
var card = new SpanSliceCard();
card.Upgrade();
run.Player.MasterDeck.Add(card);
run.Player.MaxHp = Math.Max(20, run.Player.MaxHp - 8);
run.Player.CurrentHp = Math.Min(run.Player.CurrentHp, run.Player.MaxHp);
}
),
new StoryOption(
"Read Documentation & Write Unit Tests [Heal 15 SLA, Gain 30 Dev Credits]",
"You spend 20 minutes understanding the root cause. Clean architecture restored!",
run =>
{
run.Player.Heal(15);
run.DevCredits += 30;
}
),
new StoryOption(
"Downvote & Close Browser [Gain 20 Dev Credits]",
"You decide not to gamble with legacy hacks and move forward.",
run =>
{
run.DevCredits += 20;
}
)
}
},
new StoryEvent
{
Id = "junior_pr",
Title = "The 8,000-Line 'Minor Bugfix' PR",
Icon = "🌿",
Lore = "A junior developer submits a pull request titled 'Refactored auth module (quick fix)'. It modifies 142 files across the entire solution without a single unit test.",
Options = new List<StoryOption>
{
new StoryOption(
"Approve & Ship to Staging [Gain Upgraded throw Exception+, take 10 damage]",
"You click 'LGTM' and merge. The build explodes in flames, but unlocks sheer destructive power!",
run =>
{
var card = new ThrowExceptionCard();
card.Upgrade();
run.Player.MasterDeck.Add(card);
run.Player.TakeDamage(10, isPiercing: true);
}
),
new StoryOption(
"Pair-Program & Teach Clean Code [Upgrade a Card, +35 Dev Credits]",
"You mentor the developer. A card in your Master Deck is refactored to production perfection!",
run =>
{
var upgradable = run.Player.MasterDeck.FirstOrDefault(c => !c.Upgraded);
upgradable?.Upgrade();
run.DevCredits += 35;
}
),
new StoryOption(
"Close PR and Request Smaller Diffs [Heal 10 SLA]",
"You enforce code hygiene policies and drink some tea.",
run =>
{
run.Player.Heal(10);
}
)
}
},
new StoryEvent
{
Id = "legacy_com",
Title = "Ancient C++ COM Interop Wrapper",
Icon = "💾",
Lore = "In the deep bowels of the repository, you uncover a binary file compiled in 2003 with zero source code and unmanaged memory allocations.",
Options = new List<StoryOption>
{
new StoryOption(
"Wrap in Dynamic JIT Optimization [Gain Tiered Compilation+]",
"You wrap the legacy binary in a high-performance Roslyn wrapper!",
run =>
{
var card = new TieredCompilationCard();
card.Upgrade();
run.Player.MasterDeck.Add(card);
}
),
new StoryOption(
"Decompile and Liquidate [Gain 80 Dev Credits]",
"You extract the proprietary business logic and earn huge company bonuses!",
run =>
{
run.DevCredits += 80;
}
),
new StoryOption(
"Seal Behind Deprecated Attribute [Heal 20 SLA]",
"You mark it [Obsolete] and isolate it from the runtime.",
run =>
{
run.Player.Heal(20);
}
)
}
}
};
}
}