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
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);
}
)
}
}
};
}
}