impl
This commit is contained in:
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user