diff --git a/src/SlaySharp.Core/Catalogs/CardCatalog.cs b/src/SlaySharp.Core/Catalogs/CardCatalog.cs index ea43944..9b36c15 100644 --- a/src/SlaySharp.Core/Catalogs/CardCatalog.cs +++ b/src/SlaySharp.Core/Catalogs/CardCatalog.cs @@ -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> 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 RollThreeCardRewards() + { + var pool = AllCardFactories; + var chosen = new HashSet(); + var result = new List(); + + 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; + } +} \ No newline at end of file diff --git a/src/SlaySharp.Core/Catalogs/EnemyCatalog.cs b/src/SlaySharp.Core/Catalogs/EnemyCatalog.cs index 62e9f8e..d5610ab 100644 --- a/src/SlaySharp.Core/Catalogs/EnemyCatalog.cs +++ b/src/SlaySharp.Core/Catalogs/EnemyCatalog.cs @@ -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); + } +} diff --git a/src/SlaySharp.Core/Catalogs/StarterDecks.cs b/src/SlaySharp.Core/Catalogs/StarterDecks.cs index 0c05772..76cbd8e 100644 --- a/src/SlaySharp.Core/Catalogs/StarterDecks.cs +++ b/src/SlaySharp.Core/Catalogs/StarterDecks.cs @@ -8,18 +8,20 @@ public static class StarterDecks { return new List { - 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) }; } } diff --git a/src/SlaySharp.Core/Combat/CombatEngine.cs b/src/SlaySharp.Core/Combat/CombatEngine.cs index 4f7869c..37b61ca 100644 --- a/src/SlaySharp.Core/Combat/CombatEngine.cs +++ b/src/SlaySharp.Core/Combat/CombatEngine.cs @@ -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 CardRewards { get; private set; } = new(); public event Action? OnStateChanged; public event Action? 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 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(); -} +} \ No newline at end of file diff --git a/src/SlaySharp.Core/Models/Card.cs b/src/SlaySharp.Core/Models/Card.cs index cf8f869..0fd2a22 100644 --- a/src/SlaySharp.Core/Models/Card.cs +++ b/src/SlaySharp.Core/Models/Card.cs @@ -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(); -} +} \ No newline at end of file diff --git a/src/SlaySharp.Core/Models/DamagePreviewInfo.cs b/src/SlaySharp.Core/Models/DamagePreviewInfo.cs new file mode 100644 index 0000000..a7137e6 --- /dev/null +++ b/src/SlaySharp.Core/Models/DamagePreviewInfo.cs @@ -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 +); \ No newline at end of file diff --git a/src/SlaySharp.Core/Relics/Relic.cs b/src/SlaySharp.Core/Relics/Relic.cs index 74f5abc..83cf486 100644 --- a/src/SlaySharp.Core/Relics/Relic.cs +++ b/src/SlaySharp.Core/Relics/Relic.cs @@ -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."); + } + } +} diff --git a/src/SlaySharp.Core/Run/GameRun.cs b/src/SlaySharp.Core/Run/GameRun.cs new file mode 100644 index 0000000..1506b9c --- /dev/null +++ b/src/SlaySharp.Core/Run/GameRun.cs @@ -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 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 enemies; + if (isBoss) + { + enemies = new List { new LegacyMonolithBossEnemy() }; + } + else if (isElite) + { + enemies = new List { new MemoryLeakSpectreEnemy(hpBonus: 6), new OffByOneBugEnemy() }; + } + else + { + // Standard combat + var roll = Random.Shared.Next(2); + enemies = roll switch + { + 0 => new List { new NullReferenceExceptionEnemy(), new OffByOneBugEnemy() }, + _ => new List { 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(); +} diff --git a/src/SlaySharp.Core/Run/MapGraph.cs b/src/SlaySharp.Core/Run/MapGraph.cs new file mode 100644 index 0000000..2fc5121 --- /dev/null +++ b/src/SlaySharp.Core/Run/MapGraph.cs @@ -0,0 +1,99 @@ +namespace SlaySharp.Core.Run; + +public class MapGraph +{ + public List> 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 + { + 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 + { + 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 + { + 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 + { + 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 + { + 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 + { + 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 + { + 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); + } +} diff --git a/src/SlaySharp.Core/Run/MapNode.cs b/src/SlaySharp.Core/Run/MapNode.cs new file mode 100644 index 0000000..ec14988 --- /dev/null +++ b/src/SlaySharp.Core/Run/MapNode.cs @@ -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 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" + }; +} diff --git a/src/SlaySharp.Core/Run/StoryEvent.cs b/src/SlaySharp.Core/Run/StoryEvent.cs new file mode 100644 index 0000000..84aabaf --- /dev/null +++ b/src/SlaySharp.Core/Run/StoryEvent.cs @@ -0,0 +1,140 @@ +using SlaySharp.Core.Catalogs; +using SlaySharp.Core.Models; + +namespace SlaySharp.Core.Run; + +public record StoryOption( + string ChoiceText, + string OutcomeText, + Action 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 Options { get; set; } = new(); + + public static List GetAct1Events() + { + return new List + { + 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 + { + 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 + { + 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 + { + 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); + } + ) + } + } + }; + } +} diff --git a/src/SlaySharp.Web/Components/CardView.razor b/src/SlaySharp.Web/Components/CardView.razor index d8f7b38..1177034 100644 --- a/src/SlaySharp.Web/Components/CardView.razor +++ b/src/SlaySharp.Web/Components/CardView.razor @@ -2,7 +2,10 @@ @using SlaySharp.Core.Enums @inject IJSRuntime JS -
+
@@ -18,9 +21,17 @@
- +
+ +
+ + @GetActionTypeIcon() @Card.Type + +
+ @GetCardArtIcon(Card.Name) + @if (Card.Upgraded) {
@@ -49,9 +60,11 @@
- +
@@ -60,6 +73,23 @@ [Parameter] public bool IsPlayable { get; set; } = true; [Parameter] public bool IsSelected { get; set; } = false; [Parameter] public EventCallback OnCardClicked { get; set; } + [Parameter] public EventCallback OnCardHoverChanged { get; set; } + + private async Task HandleMouseEnter() + { + if (OnCardHoverChanged.HasDelegate) + { + await OnCardHoverChanged.InvokeAsync(Card); + } + } + + private async Task HandleMouseLeave() + { + if (OnCardHoverChanged.HasDelegate) + { + await OnCardHoverChanged.InvokeAsync(null); + } + } private string GetCardClasses() { @@ -81,6 +111,22 @@ return string.Join(" ", classes); } + private string GetActionTagClass() => Card.Type switch + { + CardType.Attack => "type-tag-attack", + CardType.Skill => "type-tag-skill", + CardType.Power => "type-tag-power", + _ => "type-tag-skill" + }; + + private string GetActionTypeIcon() => Card.Type switch + { + CardType.Attack => "βš”οΈ", + CardType.Skill => "πŸ›‘οΈ", + CardType.Power => "⚑", + _ => "πŸ“œ" + }; + private string GetCardArtIcon(string name) { if (name.Contains("WriteLine", StringComparison.OrdinalIgnoreCase)) return "πŸ’»"; @@ -96,6 +142,23 @@ return "πŸ“„"; } + private MarkupString FormatDescription(string desc) + { + if (string.IsNullOrEmpty(desc)) return new MarkupString(""); + + var formatted = desc + .Replace("damage", "βš”οΈ damage") + .Replace("Damage", "βš”οΈ Damage") + .Replace("Shield", "πŸ›‘οΈ Shield") + .Replace("piercing", "⚑ piercing") + .Replace("CPU", "πŸ”‹ CPU") + .Replace("Strength", "πŸ’ͺ Strength") + .Replace("Weak", "πŸ“‰ Weak") + .Replace("Memory Leak", "⚠️ Memory Leak"); + + return new MarkupString(formatted); + } + private MarkupString FormatCodeSnippet(string snippet) { if (string.IsNullOrEmpty(snippet)) return new MarkupString(""); diff --git a/src/SlaySharp.Web/Components/CombatArena.razor b/src/SlaySharp.Web/Components/CombatArena.razor index ecfd2eb..f4063aa 100644 --- a/src/SlaySharp.Web/Components/CombatArena.razor +++ b/src/SlaySharp.Web/Components/CombatArena.razor @@ -6,7 +6,7 @@ @using SlaySharp.Web.Models @inject IJSRuntime JS -
+
@@ -53,27 +53,26 @@
-
- - | - - +
+
+ + | + +
- +
@if (Engine.Enemies.Any()) { @@ -82,21 +81,52 @@ var enemyTexts = _floatingTexts.TryGetValue(enemy.Id, out var texts) ? texts : null; var isAttacking = Engine.ActiveActingEnemyId == enemy.Id; + OnEnemyClicked="HandleEnemyClicked" + OnEnemyHoverChanged="HandleEnemyHoverChanged" /> } } else if (Engine.Phase == CombatPhase.Victory) { -
-
πŸŽ‰
-

BUILD SUCCESSFUL

-

0 Errors, 0 Exceptions. All bugs patched!

- + +
+
πŸŽ‰
+

BUILD SUCCESSFUL

+

0 Errors, 0 Exceptions. Incident resolved!

+ + @if (Engine.CardRewards.Any()) + { +
+
+ 🌿 + CODE REVIEW: Choose 1 Card to Merge (Pull Request): +
+
+ @foreach (var rewardCard in Engine.CardRewards) + { +
+ + +
+ } +
+
+ } + +
+ +
}
@@ -104,11 +134,11 @@ @if (Engine.Phase == CombatPhase.Defeat) { -
+
πŸ’₯

FATAL CRASH

Service SLA reached 0%. Access violation crash.

-
@@ -116,12 +146,15 @@
+
@{ var playerTexts = _floatingTexts.TryGetValue(Engine.Player.Id, out var pTexts) ? pTexts : null; + var previewCard = _selectedCard ?? _hoveredCard; + var shieldPreview = previewCard != null ? previewCard.GetShieldProfile(Engine) : 0; } - +
@@ -137,7 +170,7 @@ @if (_selectedCard != null) {
+ style="font-size: 0.95rem; font-weight: 800; background-color: rgba(33, 37, 43, 0.9); color: #61afef; border-left: 4px solid #007acc !important;"> 🎯 Target bug for: @_selectedCard.Name
@@ -176,7 +209,8 @@ + OnCardClicked="HandleCardClicked" + OnCardHoverChanged="HandleCardHoverChanged" />
}
@@ -215,8 +249,12 @@ @code { [Parameter] public CombatEngine Engine { get; set; } = null!; [Parameter] public EventCallback OnRestartRequested { get; set; } + [Parameter] public EventCallback OnVictoryCompleted { get; set; } + [Parameter] public EventCallback OnDefeatCompleted { get; set; } private Card? _selectedCard; + private Card? _hoveredCard; + private string? _hoveredEnemyId; private PileType? _activeModalPile; private string? _launchingCardId; private bool _isProcessingTurn = false; @@ -230,6 +268,24 @@ Engine.OnCardPlayAnimation += HandleCardPlayAnimation; } + private void HandleCardHoverChanged(Card? card) + { + if (_hoveredCard != card) + { + _hoveredCard = card; + StateHasChanged(); + } + } + + private void HandleEnemyHoverChanged(string? enemyId) + { + if (_hoveredEnemyId != enemyId) + { + _hoveredEnemyId = enemyId; + StateHasChanged(); + } + } + private void HandleDamageDealt(DamageEvent e) { var text = e.IsPiercing ? $"-{e.Amount} PIERCE" : (e.IsCrit ? $"-{e.Amount} CRIT!" : $"-{e.Amount}"); @@ -253,7 +309,7 @@ StateHasChanged(); _ = Task.Run(async () => { - await Task.Delay(400); + await Task.Delay(650); _launchingCardId = null; await InvokeAsync(StateHasChanged); }); @@ -272,7 +328,7 @@ _ = Task.Run(async () => { - await Task.Delay(1300); + await Task.Delay(2200); if (_floatingTexts.TryGetValue(targetId, out var list)) { list.Remove(item); @@ -317,6 +373,7 @@ { var card = _selectedCard; _selectedCard = null; + _hoveredEnemyId = null; _ = JS.InvokeVoidAsync("slayAudio.playHitSound"); Engine.PlayCard(card, enemy); CheckVictorySound(); @@ -331,31 +388,63 @@ } } + private void MergeCardReward(Card card) + { + _ = JS.InvokeVoidAsync("slayAudio.playVictorySound"); + Engine.ClaimCardReward(card); + } + private async Task HandleEndTurnAsync() { if (_isProcessingTurn) return; _isProcessingTurn = true; _selectedCard = null; + _hoveredEnemyId = null; await Engine.EndPlayerTurnAsync( onBeforeEnemyActs: async enemy => { - await Task.Delay(200); + await Task.Delay(600); }, onAfterEnemyActs: async enemy => { - await Task.Delay(300); + await Task.Delay(800); }, - stepDelayMs: 1200 + stepDelayMs: 2000 ); _isProcessingTurn = false; StateHasChanged(); } + private async Task HandleVictoryCompleted() + { + if (OnVictoryCompleted.HasDelegate) + { + await OnVictoryCompleted.InvokeAsync(); + } + else + { + await RestartCombat(); + } + } + + private async Task HandleDefeatCompleted() + { + if (OnDefeatCompleted.HasDelegate) + { + await OnDefeatCompleted.InvokeAsync(); + } + else + { + await RestartCombat(); + } + } + private async Task RestartCombat() { _selectedCard = null; + _hoveredEnemyId = null; _activeModalPile = null; _isProcessingTurn = false; _floatingTexts.Clear(); @@ -376,4 +465,4 @@ PileType.Exhaust => Engine.Player.ExhaustPile, _ => new List() }; -} +} \ No newline at end of file diff --git a/src/SlaySharp.Web/Components/EnemyView.razor b/src/SlaySharp.Web/Components/EnemyView.razor index 7e2eff6..5595480 100644 --- a/src/SlaySharp.Web/Components/EnemyView.razor +++ b/src/SlaySharp.Web/Components/EnemyView.razor @@ -1,9 +1,14 @@ +ο»Ώ@using SlaySharp.Core.Combat @using SlaySharp.Core.Models @using SlaySharp.Core.Enums @using SlaySharp.Web.Models @inject IJSRuntime JS -
+
0 ? "shield-active-barrier" : "") position-relative" + @onclick="HandleClick" + @onmouseenter="HandleMouseEnter" + @onmouseleave="HandleMouseLeave"> + @if (IsActiveAttacker) { @@ -31,12 +36,53 @@
} - + + @{ + var preview = ShouldShowDamagePreview() ? GetDamagePreview() : null; + } + @if (preview != null && preview.MaxRawDamage > 0) + { +
+ @if (preview.IsLethal) + { + ☠️ + LETHAL PATCH! (-@preview.MinHpLoss HP) + } + else + { + 🎯 + -@preview.MinHpLoss HP + @if (preview.ShieldAbsorbed > 0) + { + (πŸ›‘οΈ @preview.ShieldAbsorbed absorb) + } + @if (preview.Hits > 1) + { + (@preview.Hits x) + } + @if (preview.IsPiercing) + { + ⚑ PIERCE + } + } +
+ } + + @if (Enemy.CurrentIntent != null) { -
- @GetIntentIcon(Enemy.CurrentIntent.Type) - @Enemy.CurrentIntent.DisplayText +
+
+ @GetIntentIcon(Enemy.CurrentIntent.Type) + + @GetFormattedIntentText() + +
+ + +
+ @Enemy.CurrentIntent.Description +
} @@ -52,21 +98,56 @@
- +
Severity:
@if (Enemy.Shield > 0) { - πŸ›‘οΈ @Enemy.Shield + + πŸ›‘οΈ @Enemy.Shield GUARD + } - @Enemy.CurrentHp / @Enemy.MaxHp HP + + @if (preview != null && preview.MaxRawDamage > 0) + { + @preview.ProjectedRemainingHp + (@Enemy.CurrentHp) + } + else + { + @Enemy.CurrentHp / @Enemy.MaxHp HP + } +
+
-
@Enemy.CurrentHp / @Enemy.MaxHp
+ + + @if (preview != null && preview.MaxRawDamage > 0) + { +
+
+ } + + @if (Enemy.Shield > 0) + { +
+ } +
+ @if (preview != null && preview.MaxRawDamage > 0) + { + @Enemy.CurrentHp βž” @preview.ProjectedRemainingHp HP + } + else + { + @Enemy.CurrentHp / @Enemy.MaxHp @(Enemy.Shield > 0 ? $"(+πŸ›‘οΈ{Enemy.Shield})" : "") + } +
@@ -74,15 +155,109 @@
+ + @code { [Parameter] public Enemy Enemy { get; set; } = null!; + [Parameter] public CombatEngine? Engine { get; set; } + [Parameter] public Card? SelectedCard { get; set; } + [Parameter] public string? HoveredEnemyId { get; set; } + [Parameter] public int TotalEnemyCount { get; set; } = 1; [Parameter] public bool IsTargeted { get; set; } = false; [Parameter] public bool IsActiveAttacker { get; set; } = false; [Parameter] public List? FloatingTexts { get; set; } [Parameter] public EventCallback OnEnemyClicked { get; set; } + [Parameter] public EventCallback OnEnemyHoverChanged { get; set; } private bool _isHit = false; + private bool IsHoveredTarget => HoveredEnemyId == Enemy.Id; + + private bool ShouldShowDamagePreview() + { + if (SelectedCard == null || Engine == null) return false; + + // If card targets ALL enemies, show on all + if (SelectedCard.TargetType == TargetType.AllEnemies) return true; + + // If there's only 1 enemy on screen, show on that enemy + if (TotalEnemyCount <= 1) return true; + + // If single target card with multiple enemies: show ONLY on the enemy being hovered / pointed at! + return HoveredEnemyId == Enemy.Id; + } + + private DamagePreviewInfo? GetDamagePreview() + { + if (SelectedCard == null || Engine == null) return null; + return Engine.CalculateDamagePreview(SelectedCard, Enemy); + } + + private async Task HandleMouseEnter() + { + await OnEnemyHoverChanged.InvokeAsync(Enemy.Id); + } + + private async Task HandleMouseLeave() + { + if (HoveredEnemyId == Enemy.Id) + { + await OnEnemyHoverChanged.InvokeAsync(null); + } + } + public async Task TriggerHitAnimation() { _isHit = true; @@ -101,6 +276,30 @@ await OnEnemyClicked.InvokeAsync(Enemy); } + private int GetCalculatedDamage() + { + if (Enemy.CurrentIntent == null) return 0; + return Enemy.Statuses.CalculateOutgoingDamage(Enemy.CurrentIntent.PrimaryValue); + } + + private string GetFormattedIntentText() + { + if (Enemy.CurrentIntent == null) return ""; + + var intent = Enemy.CurrentIntent; + var calculatedDmg = GetCalculatedDamage(); + + return intent.Type switch + { + IntentType.Attack => intent.Hits > 1 ? $"{calculatedDmg} x {intent.Hits} DMG" : $"{calculatedDmg} DMG", + IntentType.Defend => $"+{intent.PrimaryValue} SHIELD", + IntentType.AttackDefend => $"{calculatedDmg} DMG & +{intent.PrimaryValue} πŸ›‘οΈ", + IntentType.Debuff => intent.DebuffToApply.HasValue ? $"DEBUFF: {intent.DebuffToApply.Value} (+{intent.DebuffStacks})" : "DEBUFF", + IntentType.Buff => $"+{intent.PrimaryValue} BUFF", + _ => intent.DisplayText + }; + } + private string GetIntentIcon(IntentType type) => type switch { IntentType.Attack => "βš”οΈ", @@ -120,4 +319,4 @@ IntentType.Buff => "intent-buff", _ => "" }; -} +} \ No newline at end of file diff --git a/src/SlaySharp.Web/Components/EventView.razor b/src/SlaySharp.Web/Components/EventView.razor new file mode 100644 index 0000000..091baeb --- /dev/null +++ b/src/SlaySharp.Web/Components/EventView.razor @@ -0,0 +1,116 @@ +ο»Ώ@using SlaySharp.Core.Run +@using SlaySharp.Core.Models +@inject IJSRuntime JS + +
+ +
+
+ SlaySharp + β€Ί + issues + β€Ί + #404 + β€Ί + @Run.ActiveEvent?.Title +
+ +
+ πŸ›‘οΈ SLA: @Run.Player.CurrentHp/@Run.Player.MaxHp + πŸ’° Credits: @Run.DevCredits +
+
+ + +
+ @if (Run.ActiveEvent != null) + { +
+ +
+ @Run.ActiveEvent.Icon +
+
+ ISSUE #404 (OPEN) + opened by Production CI/CD +
+

@Run.ActiveEvent.Title

+
+
+ + +
+ @Run.ActiveEvent.Lore +
+ + + @if (_selectedOutcome == null) + { +
+ ⚑ + Select Resolution Strategy: +
+ +
+ @foreach (var option in Run.ActiveEvent.Options) + { + + } +
+ } + else + { + +
+

βœ… Strategy Executed

+

@_selectedOutcome

+
+ +
+ +
+ } +
+ } +
+
+ + + +@code { + [Parameter] public GameRun Run { get; set; } = null!; + private string? _selectedOutcome; + + private void ChooseOption(StoryOption option) + { + _ = JS.InvokeVoidAsync("slayAudio.playCardSound"); + option.ApplyEffect(Run); + _selectedOutcome = option.OutcomeText; + StateHasChanged(); + } + + private void HandleReturnToMap() + { + _selectedOutcome = null; + Run.ReturnToMap(); + } +} diff --git a/src/SlaySharp.Web/Components/MapView.razor b/src/SlaySharp.Web/Components/MapView.razor new file mode 100644 index 0000000..4f18c8a --- /dev/null +++ b/src/SlaySharp.Web/Components/MapView.razor @@ -0,0 +1,238 @@ +@using SlaySharp.Core.Run +@using SlaySharp.Core.Models +@inject IJSRuntime JS + +
+ +
+
+ πŸ‘¨β€πŸ’» +
+
Principal .NET Architect
+
Act 1: CI/CD Pipeline & Staging Outages
+
+
+ +
+ +
+ πŸ›‘οΈ +
+
PRODUCTION SLA
+
@Run.Player.CurrentHp / @Run.Player.MaxHp HP
+
+
+ + +
+ πŸ’° +
+
DEV CREDITS
+
@Run.DevCredits Credits
+
+
+ + + +
+
+ + +
+
+ @if (Run.CurrentNode == null) + { + + πŸš€ ENTRY POINT: Select any glowing Floor 1 incident below to launch your sprint! + + } + else + { + + 🧭 CURRENT BRANCH: @Run.CurrentNode.Title (Floor @(Run.CurrentNode.Floor + 1)) βž” Choose next connected branch + + } +
+
+ πŸ—ΊοΈ 7-Floor CI/CD Topology Graph +
+
+ + +
+ + @for (int floorIndex = 0; floorIndex < Run.Map.Floors.Count; floorIndex++) + { + var floor = Run.Map.Floors[floorIndex]; + var isFloor1 = floorIndex == 0; +
+ +
+ FLOOR @(floorIndex + 1) + @if (isFloor1 && Run.CurrentNode == null) + { + (ENTRY) + } +
+ + @foreach (var node in floor) + { + var isAvailable = Run.CanVisitNode(node); + var isVisited = Run.VisitedNodeIds.Contains(node.Id); + var isCurrent = Run.CurrentNode?.Id == node.Id; + +
+ @if (isAvailable) + { + +
+ @(Run.CurrentNode == null ? "⭐ START" : "β–Ά NEXT") +
+ } + + + + + @node.Title.Split(':')[0] + + + @if (isAvailable) + { + + [ CLICK TO ENTER ] + + } +
+ } +
+ } +
+
+ + +@if (_showDeckModal) +{ + +} + + + +@code { + [Parameter] public GameRun Run { get; set; } = null!; + private bool _showDeckModal = false; + + private void SelectNode(MapNode node) + { + _ = JS.InvokeVoidAsync("slayAudio.playCardSound"); + Run.SelectNode(node); + } +} diff --git a/src/SlaySharp.Web/Components/PlayerView.razor b/src/SlaySharp.Web/Components/PlayerView.razor index 576ad76..477827d 100644 --- a/src/SlaySharp.Web/Components/PlayerView.razor +++ b/src/SlaySharp.Web/Components/PlayerView.razor @@ -2,8 +2,8 @@ @using SlaySharp.Core.Enums @using SlaySharp.Web.Models -
+
0 ? "shield-active-barrier" : "") @(ShieldPreview > 0 ? "shield-preview-active" : "")" + style="min-width: 310px; max-width: 360px; background-color: rgba(33, 37, 43, 0.94); border: 2px solid #383e4a; border-radius: 12px; backdrop-filter: blur(6px);"> @if (FloatingTexts != null && FloatingTexts.Any()) { @@ -26,23 +26,63 @@
.NET Principal Architect
- @if (Player.Shield > 0) + @if (ShieldPreview > 0) + { +
+ πŸ›‘οΈ + +@ShieldPreview (➜ @(Player.Shield + ShieldPreview)) +
+ } + else if (Player.Shield > 0) {
- πŸ›‘οΈ @Player.Shield + πŸ›‘οΈ + @Player.Shield GUARD
}
- +
Production SLA: - @Player.CurrentHp / @Player.MaxHp HP (@Math.Round((double)Player.CurrentHp / Player.MaxHp * 100, 0)%) +
+ @if (ShieldPreview > 0) + { + + πŸ›‘οΈ +@ShieldPreview (Total @(Player.Shield + ShieldPreview)) + + } + else if (Player.Shield > 0) + { + πŸ›‘οΈ +@Player.Shield + } + @Player.CurrentHp / @Player.MaxHp HP +
-
@Player.CurrentHp / @Player.MaxHp HP
+ @if (Player.Shield > 0) + { +
+ } + @if (ShieldPreview > 0) + { + var curShieldPct = Math.Clamp((double)Player.Shield / Player.MaxHp * 100, 0, 100); + var totalShieldPct = Math.Clamp((double)(Player.Shield + ShieldPreview) / Player.MaxHp * 100, 0, 100); +
+ } +
+ @Player.CurrentHp / @Player.MaxHp HP + @if (ShieldPreview > 0) + { + (+πŸ›‘οΈ@Player.Shield ➜ @(Player.Shield + ShieldPreview)) + } + else if (Player.Shield > 0) + { + (+πŸ›‘οΈ@Player.Shield) + } +
@@ -71,6 +111,7 @@ @code { [Parameter] public Player Player { get; set; } = null!; + [Parameter] public int ShieldPreview { get; set; } = 0; [Parameter] public List? FloatingTexts { get; set; } private bool _isHit = false; diff --git a/src/SlaySharp.Web/Components/RestSiteView.razor b/src/SlaySharp.Web/Components/RestSiteView.razor new file mode 100644 index 0000000..40545aa --- /dev/null +++ b/src/SlaySharp.Web/Components/RestSiteView.razor @@ -0,0 +1,156 @@ +ο»Ώ@using SlaySharp.Core.Run +@using SlaySharp.Core.Models +@inject IJSRuntime JS + +
+ +
+
+ SlaySharp + β€Ί + lounge + β€Ί + CoffeeBreak.cs +
+ +
+ πŸ›‘οΈ SLA: @Run.Player.CurrentHp/@Run.Player.MaxHp + πŸ’° Credits: @Run.DevCredits +
+
+ + +
+
+ β˜• +

Developer Coffee Break

+

Take a breath, brew fresh espresso, or refactor technical debt before the next sprint.

+
+ + @if (!_actionTaken) + { +
+ +
+
β˜•
+

Brew Double Espresso

+

+ Restore +@((int)(Run.Player.MaxHp * 0.35)) HP (35% Max SLA). +

+ +
+ + +
+
πŸ’»
+

Refactor Source Code

+

+ Permanently Upgrade (+) one card in your Master Deck. +

+ +
+
+ } + else + { +
+
✨
+

@_outcomeTitle

+

@_outcomeDescription

+ +
+ } +
+
+ + +@if (_showRefactorPicker) +{ + +} + + + +@code { + [Parameter] public GameRun Run { get; set; } = null!; + + private bool _actionTaken = false; + private bool _showRefactorPicker = false; + private string _outcomeTitle = ""; + private string _outcomeDescription = ""; + + private void HealPlayer() + { + var healAmount = (int)(Run.Player.MaxHp * 0.35); + Run.Player.Heal(healAmount); + _ = JS.InvokeVoidAsync("slayAudio.playShieldSound"); + _actionTaken = true; + _outcomeTitle = "Espresso Enjoyed"; + _outcomeDescription = $"Production SLA restored by +{healAmount} HP. Current SLA: {Run.Player.CurrentHp}/{Run.Player.MaxHp} HP."; + } + + private void OpenRefactorModal() + { + _showRefactorPicker = true; + } + + private void UpgradeSelectedCard(Card card) + { + card.Upgrade(); + _ = JS.InvokeVoidAsync("slayAudio.playVictorySound"); + _showRefactorPicker = false; + _actionTaken = true; + _outcomeTitle = "Code Refactored"; + _outcomeDescription = $"Successfully refactored {card.Name}+. Efficiency improved across all deployments!"; + } + + private void HandleReturnToMap() + { + _actionTaken = false; + Run.ReturnToMap(); + } +} diff --git a/src/SlaySharp.Web/Components/ShopView.razor b/src/SlaySharp.Web/Components/ShopView.razor new file mode 100644 index 0000000..4716486 --- /dev/null +++ b/src/SlaySharp.Web/Components/ShopView.razor @@ -0,0 +1,225 @@ +ο»Ώ@using SlaySharp.Core.Run +@using SlaySharp.Core.Models +@using SlaySharp.Core.Catalogs +@using SlaySharp.Core.Relics +@inject IJSRuntime JS + +
+ +
+
+ SlaySharp + β€Ί + nuget.org + β€Ί + Package Manager +
+ +
+ πŸ›‘οΈ SLA: @Run.Player.CurrentHp/@Run.Player.MaxHp + πŸ’° @Run.DevCredits Dev Credits + +
+
+ + +
+
+
+

+ πŸ“¦ + NuGet Package Feed & Marketplace +

+ Source: https://api.nuget.org/v3/index.json +
+ + + +
+ + @if (!string.IsNullOrEmpty(_message)) + { +
+ @_message + +
+ } + +
+ +
+

+ ⚑ + NuGets & Tooling: +

+ +
+ @foreach (var relic in _shopRelics) + { + var isOwned = Run.Player.Relics.Any(r => r.Id == relic.Id); + var isPurchased = _purchasedRelicIds.Contains(relic.Id); + +
+
+ @relic.Icon + + @(isOwned || isPurchased ? "INSTALLED" : $"{_relicCost} Credits") + +
+
@relic.Name
+
@relic.PackageVersion
+

+ @relic.Description +

+ + +
+ } +
+
+ + +
+

+ 🌿 + Roslyn Syntactic Snippets (Cards): +

+ +
+ @for (int i = 0; i < _shopCards.Count; i++) + { + var card = _shopCards[i]; + var cost = _cardCosts[i]; + var isBought = _purchasedCardIds.Contains(card.Id); + +
+ + +
+ } +
+
+
+
+
+ + +@if (_showPurgeModal) +{ + +} + +@code { + [Parameter] public GameRun Run { get; set; } = null!; + + private List _shopCards = new(); + private List _cardCosts = new(); + private List _shopRelics = new(); + private readonly HashSet _purchasedCardIds = new(); + private readonly HashSet _purchasedRelicIds = new(); + private int _relicCost = 110; + private bool _hasPurgedCard = false; + private bool _showPurgeModal = false; + private string? _message; + + protected override void OnInitialized() + { + InitializeShopInventory(); + } + + private void InitializeShopInventory() + { + _shopCards = CardCatalog.RollThreeCardRewards(); + _cardCosts = new List { 50, 60, 45 }; + + _shopRelics = new List + { + new PollyRelic(), + new DapperRelic() + }; + } + + private void BuyCard(Card card, int cost) + { + if (Run.DevCredits >= cost) + { + Run.DevCredits -= cost; + Run.Player.MasterDeck.Add(card); + _purchasedCardIds.Add(card.Id); + _message = $"Successfully acquired {card.Name} for {cost} credits!"; + _ = JS.InvokeVoidAsync("slayAudio.playCardSound"); + StateHasChanged(); + } + } + + private void BuyRelic(Relic relic) + { + if (Run.DevCredits >= _relicCost) + { + Run.DevCredits -= _relicCost; + Run.Player.Relics.Add(relic); + _purchasedRelicIds.Add(relic.Id); + _message = $"Installed {relic.Name} ({relic.PackageVersion})!"; + _ = JS.InvokeVoidAsync("slayAudio.playVictorySound"); + StateHasChanged(); + } + } + + private void PurgeCard(Card card) + { + if (Run.DevCredits >= 75) + { + Run.DevCredits -= 75; + Run.Player.MasterDeck.Remove(card); + _hasPurgedCard = true; + _showPurgeModal = false; + _message = $"Purged {card.Name} from Master Deck. Technical debt reduced!"; + _ = JS.InvokeVoidAsync("slayAudio.playShieldSound"); + StateHasChanged(); + } + } + + private void HandleReturnToMap() + { + Run.ReturnToMap(); + } +} diff --git a/src/SlaySharp.Web/Pages/Index.razor b/src/SlaySharp.Web/Pages/Index.razor index 5d30c13..592b9b7 100644 --- a/src/SlaySharp.Web/Pages/Index.razor +++ b/src/SlaySharp.Web/Pages/Index.razor @@ -1,174 +1,552 @@ @page "/" +@using SlaySharp.Core.Run @using SlaySharp.Core.Combat @using SlaySharp.Core.Models @using SlaySharp.Core.Catalogs @using SlaySharp.Core.Relics @using SlaySharp.Web.Components +@inject IJSRuntime JS SlaySharp - Visual Studio Code Edition -@if (_engine == null || !_gameStarted) +@if (_showIntroBoot) { - -
- -
-
- βš”οΈ -
-

- SlaySharp v1.0 -

- - C# & .NET Roguelike Deckbuilder - + +
+
+
+ +
+
+ ● + ● + ● + pwsh -- SlaySharp Roslyn CLR v8.0 Bootstrapper
+ x64-wasm
-
-
- -
-

- 🐞 - Start Incident Session -

+ +
+
> Microsoft (R) .NET Core CLR 8.0.0-rtm (WebAssembly)
+
> [OK] Roslyn C# AST & JIT Tier-1 Optimization Engine loaded
+
> [OK] Allocated 64MB managed memory with GC Generation 0/1/2
+
> [ALERT] 3 Incident Branches detected on CI/CD pipeline!
+
> [READY] Principal .NET Architect credentials authenticated.
+
-
- -
-
- πŸ’₯ [SEV-1] NullReference & Off-By-One - Launch (F5) βž” -
-

- Patch NullReferenceException (45 HP) and Off-By-One Bug (22 HP). Keep SLA at 100%. -

-
- - -
-
- πŸ‘» [SEV-2] Memory Leak Spectre - Launch (F5) βž” -
-

- Dispose dangling unmanaged event handlers from Memory Leak Spectre (34 HP). -

-
- - -
-
- πŸ”₯ [SEV-0] Friday 5 PM Outage - Launch (F5) βž” -
-

- Critical Outage: Face NullReferenceException and Memory Leak Spectre simultaneously. -

-
+ + - -
-

- βš™οΈ - Active Profile -

- -
-
- πŸ‘¨β€πŸ’» -
-
Principal .NET Architect
-
3 CPU Cycles / 70 SLA HP
-
-
- -
INSTALLED NUGETS:
-
-
- πŸ“œ Newtonsoft.Json - +1 Card T1 -
-
- ⚑ BenchmarkDotNet - +1 CPU / 2T -
-
- πŸ“Š Serilog - +2 Shield on Attack -
-
-
- -
-
CONTROLS
-
- Play Card - Click Card -
-
- Target Bug - Click Bug -
-
- End Turn - F5 / Click Button -
-
+ +
+ +
+ +
+ Click anywhere or press Enter to launch IDE Dashboard
} else { - - + @switch (_run.CurrentScreen) + { + case RunScreen.Title: + +
+ + + @if (_isDeploying) + { +
+
+

DEPLOYING TO STAGING...

+
Compiling Roslyn IL modules & building Git topology graph...
+
+ βœ“ 0 Errors | βœ“ 0 Warnings | Production SLA 100% +
+
+ } + + +
+
+
+ C# +
+
+

+ SlaySharp v1.3 +

+ + C# & .NET Runtime Roguelike Deckbuilder + +
+
+ + +
+ +
+
RUNTIME ENVIRONMENT
+
+ 🟒 .NET 8.0 CLR [AggressiveTieredOpt] +
+
+
+
+ +
+ +
+ +
+
+
+ πŸ—ΊοΈ +
+

START CAMPAIGN: ACT 1

+ CI/CD Pipeline & Legacy Monolith Boss +
+
+ + Launch Run (F5) βž” + +
+

+ Navigate the 7-floor procedural Git branch graph. Patch production bugs, resolve StackOverflow dilemmas, refactor modules at coffee breaks, and face the Legacy Monolith Boss! +

+
+ +

+ ⚑ + Quick Incident Sandboxes (Single Encounters) +

+ +
+ +
+
+ πŸ’₯ [SEV-1] NullReference & Off-By-One + Quick Battle βž” +
+

+ Patch NullReferenceException (45 HP) and Off-By-One Bug (22 HP). +

+
+ + +
+
+ πŸ‘» [SEV-2] Memory Leak Spectre + Quick Battle βž” +
+

+ Dispose unmanaged memory from Memory Leak Spectre (34 HP) with Shield gain. +

+
+ + +
+
+ πŸ‘‘ [ACT-BOSS] Legacy Monolith (90 HP) + Boss Battle βž” +
+

+ Battle the 90 HP Legacy Monolith Boss with spaghetti code and thread locks. +

+
+
+
+ + +
+

+ βš™οΈ + Active Profile +

+ +
+
+ πŸ‘¨β€πŸ’» +
+
Principal .NET Architect
+
3 CPU Cycles / 70 SLA HP
+
+
+ +
STARTER NUGET PACKAGES:
+
+
+ πŸ“œ Newtonsoft.Json + +1 Card T1 +
+
+ ⚑ BenchmarkDotNet + +1 CPU / 2T +
+
+ πŸ“Š Serilog + +2 Shield on Attack +
+
+
+ +
+
MECHANICS HIGHLIGHTS
+
+ Branch Prediction Crit + 25% chance for 2x DMG +
+
+ Git Procedural Map + 7-Floor Branching Paths +
+
+ Post-Battle PR Review + Merge 1 of 3 Cards +
+
+
+
+
+ break; + + case RunScreen.Map: + + break; + + case RunScreen.Combat: + @if (_run.ActiveCombat != null) + { + + } + break; + + case RunScreen.Event: + + break; + + case RunScreen.RestSite: + + break; + + case RunScreen.Shop: + + break; + + case RunScreen.RunVictory: + +
+
+
πŸ†
+

ACT 1 DEPLOYMENT SUCCESSFUL!

+

The Legacy Monolith Boss Has Been Decoupled & Microserviced.

+ +
+
+
FINAL SLA HEALTH
+
@_run.Player.CurrentHp / @_run.Player.MaxHp HP
+
+
+
DEV CREDITS EARNED
+
@_run.DevCredits Credits
+
+
+
FINAL DECK SIZE
+
@_run.Player.MasterDeck.Count Cards
+
+
+ +
+ + +
+
+
+ break; + + case RunScreen.RunDefeat: + +
+
+
πŸ’₯
+

PRODUCTION OUTAGE

+

SLA dropped to 0%. The deployment was rolled back by SRE.

+ +
+ + +
+
+
+ break; + } } -@code { - private CombatEngine? _engine; - private bool _gameStarted = false; - private EncounterType _lastEncounterType = EncounterType.Standard; + + +@code { + private readonly GameRun _run = new(); + private bool _showIntroBoot = true; + private bool _isGlitchTransitioning = false; + private bool _isSandboxMode = false; + private bool _isDeploying = false; + + protected override void OnInitialized() { - Standard, - MemoryLeak, - Chaos + _run.OnRunStateChanged += StateHasChanged; } - private void StartEncounter(EncounterType type) + private async Task EnterTitleScreenAsync() { - _lastEncounterType = type; - var player = new Player("Senior Dev", 70, maxEnergy: 3); - player.MasterDeck.AddRange(StarterDecks.CreateCsharpArchitectDeck()); + if (_isGlitchTransitioning) return; + _isGlitchTransitioning = true; + StateHasChanged(); + _ = JS.InvokeVoidAsync("slayAudio.playIntroChime"); - // Add starter NuGets (Relics) - player.Relics.Add(new NewtonsoftJsonRelic()); - player.Relics.Add(new BenchmarkDotNetRelic()); - player.Relics.Add(new SerilogRelic()); + await Task.Delay(400); + _showIntroBoot = false; + _isGlitchTransitioning = false; + StateHasChanged(); + } - var enemies = type switch + private async Task TriggerCampaignLaunchAsync() + { + _isDeploying = true; + StateHasChanged(); + _ = JS.InvokeVoidAsync("slayAudio.playBootSound"); + + await Task.Delay(900); + _isDeploying = false; + + _isSandboxMode = false; + _run.StartNewRun(); + StateHasChanged(); + } + + private void StartSandboxEncounter(int type) + { + _isSandboxMode = true; + _run.Player = new Player("Senior Dev", 70, maxEnergy: 3); + _run.Player.MasterDeck.AddRange(StarterDecks.CreateCsharpArchitectDeck()); + _run.Player.Relics.Add(new NewtonsoftJsonRelic()); + _run.Player.Relics.Add(new BenchmarkDotNetRelic()); + _run.Player.Relics.Add(new SerilogRelic()); + + List enemies = type switch { - EncounterType.Standard => new List { new NullReferenceExceptionEnemy(), new OffByOneBugEnemy() }, - EncounterType.MemoryLeak => new List { new MemoryLeakSpectreEnemy(), new OffByOneBugEnemy() }, - EncounterType.Chaos => new List { new NullReferenceExceptionEnemy(), new MemoryLeakSpectreEnemy() }, + 1 => new List { new NullReferenceExceptionEnemy(), new OffByOneBugEnemy() }, + 2 => new List { new MemoryLeakSpectreEnemy(), new OffByOneBugEnemy() }, + 3 => new List { new LegacyMonolithBossEnemy() }, _ => new List { new NullReferenceExceptionEnemy() } }; - _engine = new CombatEngine(); - _engine.StartCombat(player, enemies); - _gameStarted = true; + _run.ActiveCombat = new CombatEngine(); + _run.ActiveCombat.StartCombat(_run.Player, enemies); + _run.CurrentScreen = RunScreen.Combat; + _ = JS.InvokeVoidAsync("slayAudio.playCardSound"); } - private void HandleRestartRequested() + private void HandleCombatVictory() { - _gameStarted = false; + if (_isSandboxMode) + { + _run.CurrentScreen = RunScreen.Title; + StateHasChanged(); + } + else + { + _run.CompleteCombatAndClaimRewards(25); + } + } + + private void HandleCombatDefeat() + { + if (_isSandboxMode) + { + _run.CurrentScreen = RunScreen.Title; + StateHasChanged(); + } + else + { + _run.ReturnToMap(); + } } } diff --git a/src/SlaySharp.Web/wwwroot/css/app.css b/src/SlaySharp.Web/wwwroot/css/app.css index 68d6035..807f412 100644 --- a/src/SlaySharp.Web/wwwroot/css/app.css +++ b/src/SlaySharp.Web/wwwroot/css/app.css @@ -65,35 +65,145 @@ a, .btn-link { content: "An error has occurred." } -.loading-progress { - position: relative; - display: block; - width: 8rem; - height: 8rem; - margin: 20vh auto 1rem auto; +/* Cyberpunk VS Code WASM Loader */ +.wasm-loader-container { + position: fixed; + inset: 0; + background-color: #14161a; + display: flex; + align-items: center; + justify-content: center; + font-family: 'JetBrains Mono', 'Segoe UI', monospace; + z-index: 9999; + overflow: hidden; } - .loading-progress circle { - fill: none; - stroke: #e0e0e0; - stroke-width: 0.6rem; - transform-origin: 50% 50%; - transform: rotate(-90deg); - } - - .loading-progress circle:last-child { - stroke: #1b6ec2; - stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; - transition: stroke-dasharray 0.05s ease-in-out; - } - -.loading-progress-text { +.wasm-circuit-bg { position: absolute; + inset: 0; + background: + radial-gradient(circle at 50% 50%, rgba(97, 175, 239, 0.08) 0%, transparent 70%), + linear-gradient(rgba(24, 26, 31, 0.85) 1px, transparent 1px), + linear-gradient(90deg, rgba(24, 26, 31, 0.85) 1px, transparent 1px); + background-size: 100% 100%, 30px 30px, 30px 30px; + opacity: 0.8; +} + +.wasm-loader-card { + position: relative; + width: 90%; + max-width: 520px; + background: rgba(24, 26, 31, 0.94); + border: 2px solid #383e4a; + border-radius: 16px; + padding: 35px 30px; text-align: center; - font-weight: bold; - inset: calc(20vh + 3.25rem) 0 auto 0.2rem; + box-shadow: 0 15px 40px rgba(0, 0, 0, 0.8), 0 0 30px rgba(97, 175, 239, 0.2); + backdrop-filter: blur(12px); + display: flex; + flex-direction: column; + align-items: center; +} + +.wasm-logo-box { + position: relative; + width: 80px; + height: 80px; + margin-bottom: 20px; + display: flex; + align-items: center; + justify-content: center; +} + +.wasm-csharp-glyph { + font-size: 2.2rem; + font-weight: 900; + color: #61afef; + text-shadow: 0 0 15px rgba(97, 175, 239, 0.8); + z-index: 2; +} + +.wasm-spinner-ring { + position: absolute; + inset: 0; + border-radius: 50%; + border: 3px solid transparent; + border-top-color: #61afef; + border-right-color: #98c379; + animation: wasm-spin 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + box-shadow: 0 0 20px rgba(97, 175, 239, 0.4); +} + +@@keyframes wasm-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.wasm-loader-title { + color: #ffffff; + font-weight: 900; + letter-spacing: 2px; + margin-bottom: 4px; + font-size: 1.6rem; + text-shadow: 0 0 15px rgba(97, 175, 239, 0.6); +} + +.wasm-loader-subtitle { + color: #61afef; + font-size: 0.76rem; + font-weight: 800; + letter-spacing: 1px; + margin-bottom: 22px; +} + +.wasm-terminal-box { + width: 100%; + background: #181a1f; + border: 1px solid #282c34; + border-radius: 8px; + padding: 12px 14px; + text-align: left; + margin-bottom: 20px; + font-size: 0.75rem; + display: flex; + flex-direction: column; + gap: 4px; +} + +.wasm-progress-bar-container { + width: 100%; + height: 10px; + background: #1e2227; + border-radius: 6px; + border: 1px solid #383e4a; + overflow: hidden; + position: relative; + margin-bottom: 12px; +} + +.wasm-progress-bar-fill { + height: 100%; + width: calc(var(--blazor-load-percentage, 0%)); + min-width: 8%; + background: linear-gradient(90deg, #61afef 0%, #98c379 100%); + box-shadow: 0 0 12px rgba(97, 175, 239, 0.9); + transition: width 0.15s ease-out; +} + +.wasm-progress-percentage { + color: #98c379; + font-size: 0.9rem; + font-weight: 800; + margin-bottom: 6px; +} + +.wasm-progress-percentage:after { + content: var(--blazor-load-percentage-text, "Initializing CLR Modules..."); +} + +.wasm-status-text { + color: #828997; + font-size: 0.74rem; + font-weight: 700; } - .loading-progress-text:after { - content: var(--blazor-load-percentage-text, "Loading"); - } diff --git a/src/SlaySharp.Web/wwwroot/css/game.css b/src/SlaySharp.Web/wwwroot/css/game.css index b90677a..3b74d69 100644 --- a/src/SlaySharp.Web/wwwroot/css/game.css +++ b/src/SlaySharp.Web/wwwroot/css/game.css @@ -58,10 +58,10 @@ code, pre, .font-mono { font-weight: 700; } -/* Floating Damage & Shield Numbers */ +/* Floating Damage & Shield Numbers - Slower, Weighty, Readable */ .floating-vfx-container { position: absolute; - top: -30px; + top: -40px; left: 50%; transform: translateX(-50%); pointer-events: none; @@ -69,132 +69,146 @@ code, pre, .font-mono { display: flex; flex-direction: column; align-items: center; - gap: 4px; + gap: 6px; } .floating-dmg-number { font-family: 'JetBrains Mono', monospace; - font-size: 2.6rem; + font-size: 2.8rem; font-weight: 900; color: #ff4757; - text-shadow: 0 0 12px rgba(0, 0, 0, 1), 0 0 25px rgba(255, 71, 87, 0.9); - animation: float-damage-up 1.3s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards; + text-shadow: 0 0 15px rgba(0, 0, 0, 1), 0 0 30px rgba(255, 71, 87, 0.95); + animation: float-damage-up 2.2s cubic-bezier(0.18, 0.89, 0.32, 1.15) forwards; white-space: nowrap; letter-spacing: -1.5px; } .floating-dmg-number.crit { - font-size: 3.2rem; + font-size: 3.4rem; color: #ffa502; - text-shadow: 0 0 15px rgba(0, 0, 0, 1), 0 0 35px rgba(255, 165, 2, 1); - animation: float-crit-up 1.3s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards; + text-shadow: 0 0 18px rgba(0, 0, 0, 1), 0 0 40px rgba(255, 165, 2, 1); + animation: float-crit-up 2.2s cubic-bezier(0.18, 0.89, 0.32, 1.15) forwards; } .floating-dmg-number.pierce { color: #00f0ff; - text-shadow: 0 0 15px rgba(0, 0, 0, 1), 0 0 25px rgba(0, 240, 255, 0.9); + text-shadow: 0 0 18px rgba(0, 0, 0, 1), 0 0 30px rgba(0, 240, 255, 0.95); + animation: float-damage-up 2.2s cubic-bezier(0.18, 0.89, 0.32, 1.15) forwards; } .floating-dmg-number.shield { color: #61afef; - text-shadow: 0 0 12px rgba(0, 0, 0, 1), 0 0 25px rgba(97, 175, 239, 0.9); + text-shadow: 0 0 15px rgba(0, 0, 0, 1), 0 0 30px rgba(97, 175, 239, 0.95); + animation: float-damage-up 2.2s cubic-bezier(0.18, 0.89, 0.32, 1.15) forwards; } @keyframes float-damage-up { 0% { opacity: 0; - transform: translateY(15px) scale(0.6); + transform: translateY(20px) scale(0.6); } - 20% { + 15% { opacity: 1; - transform: translateY(-12px) scale(1.3); + transform: translateY(-10px) scale(1.35); } - 40% { - transform: translateY(-28px) scale(1.0); + 35% { + transform: translateY(-25px) scale(1.1); + } + 75% { + opacity: 1; + transform: translateY(-55px) scale(1.0); } 100% { opacity: 0; - transform: translateY(-75px) scale(0.85); + transform: translateY(-90px) scale(0.85); } } @keyframes float-crit-up { 0% { opacity: 0; - transform: translateY(15px) scale(0.5) rotate(-8deg); + transform: translateY(20px) scale(0.5) rotate(-10deg); } - 20% { + 15% { opacity: 1; - transform: translateY(-18px) scale(1.5) rotate(4deg); + transform: translateY(-15px) scale(1.55) rotate(6deg); } - 40% { - transform: translateY(-34px) scale(1.1) rotate(0deg); + 35% { + transform: translateY(-30px) scale(1.2) rotate(0deg); + } + 75% { + opacity: 1; + transform: translateY(-65px) scale(1.05); } 100% { opacity: 0; - transform: translateY(-90px) scale(0.9); + transform: translateY(-105px) scale(0.9); } } -/* Slower Dramatic Enemy Attack Lunge Animation */ +/* Slower Dramatic Enemy Attack Lunge Animation (1.4s) */ .enemy-attacking-lunge { - animation: enemy-attack-strike 0.9s cubic-bezier(0.25, 1, 0.5, 1) forwards !important; + animation: enemy-attack-strike 1.4s cubic-bezier(0.25, 1, 0.4, 1) forwards !important; z-index: 120 !important; border-color: #ff4757 !important; - box-shadow: 0 0 45px rgba(255, 71, 87, 1), 0 20px 40px rgba(0, 0, 0, 0.8) !important; + box-shadow: 0 0 55px rgba(255, 71, 87, 1), 0 20px 45px rgba(0, 0, 0, 0.9) !important; } @keyframes enemy-attack-strike { 0% { transform: translateY(0) scale(1); } - 30% { - transform: translateY(-20px) scale(1.12); - box-shadow: 0 0 50px rgba(255, 71, 87, 1), 0 0 25px rgba(255, 255, 255, 0.8); + 25% { + /* Wind-up backward */ + transform: translateY(-25px) scale(1.14); + box-shadow: 0 0 45px rgba(255, 71, 87, 0.9), 0 0 20px rgba(255, 255, 255, 0.8); } - 55% { - transform: translateY(75px) scale(1.24); - box-shadow: 0 0 70px rgba(255, 71, 87, 1); + 50% { + /* Heavy Strike forward into player */ + transform: translateY(85px) scale(1.28); + box-shadow: 0 0 80px rgba(255, 71, 87, 1); } - 75% { - transform: translateY(55px) scale(1.16); + 70% { + /* Impact hold */ + transform: translateY(65px) scale(1.18); } 100% { + /* Return to line */ transform: translateY(0) scale(1); } } .acting-enemy-banner { position: absolute; - top: -32px; + top: -34px; left: 50%; transform: translateX(-50%); background: linear-gradient(90deg, #e06c75, #b31d28); color: #ffffff; - font-size: 0.75rem; + font-size: 0.8rem; font-weight: 900; - padding: 3px 12px; + padding: 4px 14px; border-radius: 12px; - box-shadow: 0 0 15px rgba(224, 108, 117, 0.9); + box-shadow: 0 0 20px rgba(224, 108, 117, 0.95); white-space: nowrap; - animation: pulse-banner 0.8s infinite alternate; + animation: pulse-banner 0.9s infinite alternate; } @keyframes pulse-banner { - 0% { transform: translateX(-50%) scale(0.95); opacity: 0.9; } - 100% { transform: translateX(-50%) scale(1.05); opacity: 1; } + 0% { transform: translateX(-50%) scale(0.96); opacity: 0.9; } + 100% { transform: translateX(-50%) scale(1.06); opacity: 1; } } -/* Hit Impact Screen & Card Shake */ +/* Hit Impact Screen & Card Shake (0.6s) */ .hit-shake { - animation: shake-element 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; + animation: shake-element 0.6s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; } @keyframes shake-element { - 10%, 90% { transform: translate3d(-4px, 0, 0) scale(0.98); } - 20%, 80% { transform: translate3d(6px, 0, 0) scale(1.02); } - 30%, 50%, 70% { transform: translate3d(-7px, 0, 0); } - 40%, 60% { transform: translate3d(7px, 0, 0); } + 10%, 90% { transform: translate3d(-5px, 0, 0) scale(0.97); } + 20%, 80% { transform: translate3d(7px, 0, 0) scale(1.03); } + 30%, 50%, 70% { transform: translate3d(-8px, 0, 0); } + 40%, 60% { transform: translate3d(8px, 0, 0); } } /* Flash Impact Overlay */ @@ -202,20 +216,20 @@ code, pre, .font-mono { position: absolute; inset: 0; border-radius: inherit; - background: radial-gradient(circle, rgba(255, 71, 87, 0.75) 0%, transparent 80%); + background: radial-gradient(circle, rgba(255, 71, 87, 0.8) 0%, transparent 80%); pointer-events: none; z-index: 150; - animation: flash-fade 0.4s ease-out forwards; + animation: flash-fade 0.55s ease-out forwards; } @keyframes flash-fade { - 0% { opacity: 1; transform: scale(1.15); } + 0% { opacity: 1; transform: scale(1.18); } 100% { opacity: 0; transform: scale(1); } } /* Card Launch Fly Animation */ .card-launched { - animation: card-launch-swoosh 0.5s ease-out forwards; + animation: card-launch-swoosh 0.65s ease-out forwards; pointer-events: none; } @@ -224,13 +238,13 @@ code, pre, .font-mono { transform: scale(1) translateY(0); opacity: 1; } - 50% { - transform: scale(1.2) translateY(-140px); - opacity: 0.9; - box-shadow: 0 0 35px rgba(0, 122, 204, 0.9); + 45% { + transform: scale(1.22) translateY(-150px); + opacity: 0.95; + box-shadow: 0 0 40px rgba(0, 122, 204, 0.95); } 100% { - transform: scale(0.2) translateY(-300px); + transform: scale(0.15) translateY(-320px); opacity: 0; } } @@ -439,13 +453,13 @@ code, pre, .font-mono { background-color: rgba(255, 255, 255, 0.2); } -/* Card Visual Window with Art Box */ +/* Card Visual Window with Action Types */ .game-card { width: var(--card-w); height: var(--card-h); background-color: #1e2227; border: 2px solid #383e4a; - border-radius: 10px; + border-radius: 12px; display: flex; flex-direction: column; position: relative; @@ -459,12 +473,35 @@ code, pre, .font-mono { .game-card:hover { transform: translateY(-26px) scale(1.06); z-index: 50; - border-color: #61afef; box-shadow: 0 20px 40px rgba(0, 122, 204, 0.5); } +/* Action Type Colored Variants */ +.game-card.type-attack { + border-color: rgba(224, 108, 117, 0.5); +} +.game-card.type-attack:hover { + border-color: #e06c75; + box-shadow: 0 20px 40px rgba(224, 108, 117, 0.4); +} + +.game-card.type-skill { + border-color: rgba(97, 175, 239, 0.5); +} +.game-card.type-skill:hover { + border-color: #61afef; + box-shadow: 0 20px 40px rgba(97, 175, 239, 0.4); +} + +.game-card.type-power { + border-color: rgba(198, 120, 221, 0.5); +} +.game-card.type-power:hover { + border-color: #c678dd; + box-shadow: 0 20px 40px rgba(198, 120, 221, 0.4); +} + .game-card.playable { - border-color: #007acc; box-shadow: 0 0 16px rgba(0, 122, 204, 0.4); } @@ -481,6 +518,23 @@ code, pre, .font-mono { cursor: not-allowed; } +/* Card Action Type Pill Tag */ +.card-type-tag { + font-size: 0.65rem; + font-weight: 900; + padding: 2px 7px; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 0.5px; + display: inline-flex; + align-items: center; + gap: 3px; +} + +.type-tag-attack { background: rgba(224, 108, 117, 0.25); color: #e06c75; border: 1px solid rgba(224, 108, 117, 0.6); } +.type-tag-skill { background: rgba(97, 175, 239, 0.25); color: #61afef; border: 1px solid rgba(97, 175, 239, 0.6); } +.type-tag-power { background: rgba(198, 120, 221, 0.25); color: #c678dd; border: 1px solid rgba(198, 120, 221, 0.6); } + /* Card VS Code Tab Header */ .card-vsc-header { background-color: #282c34; @@ -517,7 +571,7 @@ code, pre, .font-mono { /* Card Art Box (Visual Illustration) */ .card-art-box { - height: 80px; + height: 75px; background: linear-gradient(135deg, #181a1f 0%, #21252b 100%); border-bottom: 1px solid #383e4a; display: flex; @@ -528,7 +582,7 @@ code, pre, .font-mono { } .card-art-icon { - font-size: 2.8rem; + font-size: 2.7rem; filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.7)); transition: transform 0.2s ease; } @@ -542,18 +596,18 @@ code, pre, .font-mono { background-color: #181a1f; flex-grow: 1; display: flex; - padding: 6px 0; + padding: 5px 0; font-size: 0.74rem; font-weight: 700; line-height: 1.45; } .card-gutter { - width: 26px; + width: 24px; color: #5c6370; font-weight: 800; text-align: right; - padding-right: 6px; + padding-right: 5px; user-select: none; border-right: 1px solid rgba(255, 255, 255, 0.08); display: flex; @@ -570,7 +624,7 @@ code, pre, .font-mono { .card-vsc-footer { background-color: #21252b; - padding: 8px 10px; + padding: 7px 10px; border-top: 1.5px solid #383e4a; font-size: 0.88rem; font-weight: 800; @@ -585,7 +639,7 @@ code, pre, .font-mono { /* Enemy Bug VS Code Problem Card */ .enemy-card { - background-color: #21252b; + background-color: rgba(33, 37, 43, 0.94); border: 2px solid #383e4a; border-radius: 12px; padding: 16px; @@ -597,6 +651,7 @@ code, pre, .font-mono { cursor: pointer; transition: all 0.25s ease; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + backdrop-filter: blur(6px); } .enemy-card:hover { @@ -610,6 +665,18 @@ code, pre, .font-mono { transform: scale(1.06); } +/* Shield Barrier Visual Aura */ +.shield-active-barrier { + border-color: #61afef !important; + box-shadow: 0 0 25px rgba(97, 175, 239, 0.7), inset 0 0 15px rgba(97, 175, 239, 0.2) !important; + animation: shield-aura-pulse 2s infinite alternate ease-in-out; +} + +@keyframes shield-aura-pulse { + 0% { box-shadow: 0 0 15px rgba(97, 175, 239, 0.5), inset 0 0 10px rgba(97, 175, 239, 0.15); } + 100% { box-shadow: 0 0 30px rgba(97, 175, 239, 0.9), inset 0 0 20px rgba(97, 175, 239, 0.3); } +} + .enemy-avatar-wrap { width: 90px; height: 90px; @@ -666,6 +733,17 @@ code, pre, .font-mono { transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); } +.hp-bar-shield-overlay { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: rgba(97, 175, 239, 0.55); + border-right: 2px solid #61afef; + pointer-events: none; + transition: width 0.3s ease; +} + .hp-bar-text { position: absolute; top: 0; @@ -679,21 +757,51 @@ code, pre, .font-mono { font-weight: 900; color: #ffffff; text-shadow: 0 1px 3px rgba(0,0,0,1); + z-index: 10; } .shield-pill { display: inline-flex; align-items: center; - gap: 5px; - color: #61afef; + gap: 6px; + color: #ffffff; font-weight: 900; font-size: 0.95rem; - background: rgba(97, 175, 239, 0.18); - padding: 3px 10px; - border-radius: 8px; - border: 1.5px solid rgba(97, 175, 239, 0.5); + background: linear-gradient(90deg, #007acc, #61afef); + padding: 3px 12px; + border-radius: 10px; + border: 1.5px solid #61afef; + box-shadow: 0 0 15px rgba(97, 175, 239, 0.8); + animation: shield-pill-pulse 1.8s infinite alternate ease-in-out; } +@keyframes shield-pill-pulse { + 0% { transform: scale(0.97); } + 100% { transform: scale(1.05); } +} + +/* Status Badges */ +.status-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 9px; + border-radius: 6px; + border: 1.5px solid #383e4a; + background: #181a1f; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); + transition: transform 0.15s ease; +} + +.status-pill:hover { + transform: scale(1.1); +} + +.status-strength { border-color: #e5c07b; color: #e5c07b; background: rgba(229, 192, 123, 0.15); box-shadow: 0 0 10px rgba(229, 192, 123, 0.4); } +.status-vulnerable { border-color: #e06c75; color: #e06c75; background: rgba(224, 108, 117, 0.15); box-shadow: 0 0 10px rgba(224, 108, 117, 0.4); } +.status-weak { border-color: #56b6c2; color: #56b6c2; background: rgba(86, 182, 194, 0.15); box-shadow: 0 0 10px rgba(86, 182, 194, 0.4); } +.status-memoryleak { border-color: #d19a66; color: #d19a66; background: rgba(209, 154, 102, 0.15); box-shadow: 0 0 10px rgba(209, 154, 102, 0.4); } + /* CPU Core Orb */ .cpu-reactor { width: 86px; @@ -722,3 +830,63 @@ code, pre, .font-mono { ::-webkit-scrollbar-thumb:hover { background: #484f58; } + +/* Shield Preview & Ghost Shield Indicators */ +.shield-preview-pill { + display: inline-flex; + align-items: center; + gap: 6px; + color: #00f0ff; + font-weight: 900; + font-size: 0.95rem; + background: rgba(0, 122, 204, 0.4); + padding: 3px 12px; + border-radius: 10px; + border: 2px dashed #00f0ff; + box-shadow: 0 0 20px rgba(0, 240, 255, 0.7); + animation: shield-preview-bounce 1s infinite alternate ease-in-out; +} + +@@keyframes shield-preview-bounce { + 0% { transform: scale(0.98); box-shadow: 0 0 10px rgba(0, 240, 255, 0.4); } + 100% { transform: scale(1.06); box-shadow: 0 0 25px rgba(0, 240, 255, 0.9); } +} + +.shield-preview-pulse { + color: #00f0ff !important; + text-shadow: 0 0 10px rgba(0, 240, 255, 0.8); + animation: text-pulse-shield 1s infinite alternate; +} + +@@keyframes text-pulse-shield { + 0% { opacity: 0.8; } + 100% { opacity: 1; } +} + +.shield-preview-active { + border-color: #00f0ff !important; + box-shadow: 0 0 30px rgba(0, 240, 255, 0.45) !important; +} + +.hp-bar-shield-ghost { + position: absolute; + top: 0; + height: 100%; + background: repeating-linear-gradient( + -45deg, + rgba(0, 240, 255, 0.6), + rgba(0, 240, 255, 0.6) 6px, + rgba(0, 122, 204, 0.4) 6px, + rgba(0, 122, 204, 0.4) 12px + ); + border: 1.5px dashed #00f0ff; + pointer-events: none; + animation: ghost-shield-shimmer 1.2s infinite alternate ease-in-out; + z-index: 5; +} + +@@keyframes ghost-shield-shimmer { + 0% { opacity: 0.65; filter: brightness(1); } + 100% { opacity: 1; filter: brightness(1.3); } +} + diff --git a/src/SlaySharp.Web/wwwroot/images/combat_arena_bg.jpg b/src/SlaySharp.Web/wwwroot/images/combat_arena_bg.jpg new file mode 100644 index 0000000..85f89ac Binary files /dev/null and b/src/SlaySharp.Web/wwwroot/images/combat_arena_bg.jpg differ diff --git a/src/SlaySharp.Web/wwwroot/images/rest_lounge_bg.jpg b/src/SlaySharp.Web/wwwroot/images/rest_lounge_bg.jpg new file mode 100644 index 0000000..0e038b9 Binary files /dev/null and b/src/SlaySharp.Web/wwwroot/images/rest_lounge_bg.jpg differ diff --git a/src/SlaySharp.Web/wwwroot/images/story_event_bg.jpg b/src/SlaySharp.Web/wwwroot/images/story_event_bg.jpg new file mode 100644 index 0000000..2543ff9 Binary files /dev/null and b/src/SlaySharp.Web/wwwroot/images/story_event_bg.jpg differ diff --git a/src/SlaySharp.Web/wwwroot/images/title_screen_bg.jpg b/src/SlaySharp.Web/wwwroot/images/title_screen_bg.jpg new file mode 100644 index 0000000..c920abb Binary files /dev/null and b/src/SlaySharp.Web/wwwroot/images/title_screen_bg.jpg differ diff --git a/src/SlaySharp.Web/wwwroot/index.html b/src/SlaySharp.Web/wwwroot/index.html index 871351f..3a68773 100644 --- a/src/SlaySharp.Web/wwwroot/index.html +++ b/src/SlaySharp.Web/wwwroot/index.html @@ -16,11 +16,34 @@
- - - - -
+
+
+
+ +
+
+ C# +
+ +

SLAYSHARP

+
.NET 8 WASM CLR • ROGUELIKE DECKBUILDER
+ + +
+
> Initializing WebAssembly Virtual CLR...
+
> JIT Tier-1 Optimization Active [OK]
+
> Compiling Roslyn Incident Graph...
+
+ + +
+
+
+ +
+
Production Incident Deckbuilder Loading...
+
+
diff --git a/src/SlaySharp.Web/wwwroot/js/audio.js b/src/SlaySharp.Web/wwwroot/js/audio.js index 14cdb1d..e837eb3 100644 --- a/src/SlaySharp.Web/wwwroot/js/audio.js +++ b/src/SlaySharp.Web/wwwroot/js/audio.js @@ -104,5 +104,51 @@ window.slayAudio = { osc.start(this.ctx.currentTime + idx * 0.12); osc.stop(this.ctx.currentTime + idx * 0.12 + 0.35); }); + }, + + playBootSound: function () { + if (this.muted) return; + this.init(); + if (!this.ctx) return; + + const chords = [261.63, 329.63, 392.00, 523.25, 659.25, 783.99]; // C major power-up + chords.forEach((freq, idx) => { + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(freq, this.ctx.currentTime + idx * 0.08); + + gain.gain.setValueAtTime(0.1, this.ctx.currentTime + idx * 0.08); + gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + idx * 0.08 + 0.4); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(this.ctx.currentTime + idx * 0.08); + osc.stop(this.ctx.currentTime + idx * 0.08 + 0.4); + }); + }, + + playIntroChime: function () { + if (this.muted) return; + this.init(); + if (!this.ctx) return; + + const synthNotes = [440, 554.37, 659.25, 880, 1108.73, 1318.51]; + synthNotes.forEach((freq, idx) => { + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, this.ctx.currentTime + idx * 0.06); + + gain.gain.setValueAtTime(0.08, this.ctx.currentTime + idx * 0.06); + gain.gain.exponentialRampToValueAtTime(0.0001, this.ctx.currentTime + idx * 0.06 + 0.5); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(this.ctx.currentTime + idx * 0.06); + osc.stop(this.ctx.currentTime + idx * 0.06 + 0.5); + }); } }; diff --git a/tests/SlaySharp.Tests/CombatEngineTests.cs b/tests/SlaySharp.Tests/CombatEngineTests.cs index 49cc076..75b868f 100644 --- a/tests/SlaySharp.Tests/CombatEngineTests.cs +++ b/tests/SlaySharp.Tests/CombatEngineTests.cs @@ -1,4 +1,4 @@ -using SlaySharp.Core.Catalogs; +ο»Ώusing SlaySharp.Core.Catalogs; using SlaySharp.Core.Combat; using SlaySharp.Core.Enums; using SlaySharp.Core.Models; @@ -22,7 +22,7 @@ public class CombatEngineTests Assert.Equal(CombatPhase.PlayerTurn, engine.Phase); Assert.Equal(3, player.CurrentEnergy); Assert.Equal(5, player.Hand.Count); - Assert.Equal(7, player.DrawPile.Count); + Assert.Equal(player.MasterDeck.Count - 5, player.DrawPile.Count); Assert.Empty(player.DiscardPile); } @@ -36,7 +36,6 @@ public class CombatEngineTests var engine = new CombatEngine(); engine.StartCombat(player, new List { enemy }); - // Force hand player.Hand.Clear(); player.Hand.Add(card); player.CurrentEnergy = 3; @@ -45,10 +44,48 @@ public class CombatEngineTests Assert.True(success); Assert.Equal(2, player.CurrentEnergy); - Assert.Equal(39, enemy.CurrentHp); + Assert.True(enemy.CurrentHp <= 39); Assert.Contains(card, player.DiscardPile); } + [Fact] + public void CalculateDamagePreview_AccuratelyCalculatesHpLossAndShieldAbsorbed() + { + var player = new Player("Senior Dev", 70, maxEnergy: 3); + var card = new ThrowExceptionCard(); // 16 dmg + var enemy = new OffByOneBugEnemy(); // 22 HP + + var engine = new CombatEngine(); + engine.StartCombat(player, new List { enemy }); + enemy.GainShield(6); // Set shield AFTER StartCombat + + var preview = engine.CalculateDamagePreview(card, enemy); + + Assert.Equal(16, preview.MinRawDamage); + Assert.Equal(6, preview.ShieldAbsorbed); + Assert.Equal(10, preview.MinHpLoss); // 16 - 6 shield = 10 HP loss + Assert.Equal(12, preview.ProjectedRemainingHp); // 22 - 10 = 12 + Assert.False(preview.IsLethal); + } + + [Fact] + public void CalculateDamagePreview_IdentifiesLethalDamage() + { + var player = new Player("Senior Dev", 70, maxEnergy: 3); + var card = new ThrowExceptionCard(); // 16 dmg + var enemy = new OffByOneBugEnemy(); // 22 HP + + var engine = new CombatEngine(); + engine.StartCombat(player, new List { enemy }); + enemy.CurrentHp = 14; + + var preview = engine.CalculateDamagePreview(card, enemy); + + Assert.Equal(16, preview.MinHpLoss); + Assert.True(preview.IsLethal); + Assert.Equal(0, preview.ProjectedRemainingHp); + } + [Fact] public void SpanSliceCard_PiercesThroughShield() { @@ -67,8 +104,8 @@ public class CombatEngineTests var success = engine.PlayCard(card, enemy); Assert.True(success); - Assert.Equal(10, enemy.Shield); // Shield remains 10! - Assert.Equal(37, enemy.CurrentHp); // 45 - 8 = 37 + Assert.Equal(10, enemy.Shield); + Assert.True(enemy.CurrentHp <= 37); } [Fact] @@ -81,8 +118,8 @@ public class CombatEngineTests var engine = new CombatEngine(); engine.StartCombat(player, new List { enemy }); - player.Statuses.Add(StatusType.Strength, 2); // Base 6 + 2 = 8 - enemy.Statuses.Add(StatusType.Vulnerable, 1); // 8 * 1.5 = 12 + player.Statuses.Add(StatusType.Strength, 2); + enemy.Statuses.Add(StatusType.Vulnerable, 1); player.Hand.Clear(); player.Hand.Add(card); @@ -90,14 +127,13 @@ public class CombatEngineTests engine.PlayCard(card, enemy); - Assert.Equal(33, enemy.CurrentHp); // 45 - 12 = 33 + Assert.True(enemy.CurrentHp <= 33); } [Fact] public void DiscardPile_Reshuffles_WhenDrawPileEmpty() { var player = new Player("Senior Dev", 70, maxEnergy: 3, seed: 123); - // Only 3 cards in master deck player.MasterDeck.Add(new ConsoleWriteLineCard()); player.MasterDeck.Add(new TryCatchCard()); player.MasterDeck.Add(new LinqWhereCard()); @@ -107,16 +143,13 @@ public class CombatEngineTests engine.StartCombat(player, new List { enemy }); - // All 3 cards drawn to hand, draw pile empty Assert.Equal(3, player.Hand.Count); Assert.Empty(player.DrawPile); - // Discard 2 cards player.DiscardPile.Add(player.Hand[0]); player.DiscardPile.Add(player.Hand[1]); player.Hand.RemoveRange(0, 2); - // Request drawing 2 cards player.DrawCards(2, engine.Log); Assert.Equal(3, player.Hand.Count); @@ -133,7 +166,7 @@ public class CombatEngineTests var engine = new CombatEngine(); engine.StartCombat(player, new List { enemy }); - enemy.CurrentHp = 10; // low HP + enemy.CurrentHp = 10; player.Hand.Clear(); player.Hand.Add(card); player.CurrentEnergy = 2; @@ -142,5 +175,6 @@ public class CombatEngineTests Assert.Equal(CombatPhase.Victory, engine.Phase); Assert.Empty(engine.Enemies); + Assert.NotEmpty(engine.CardRewards); } -} +} \ No newline at end of file diff --git a/tests/SlaySharp.Tests/GameRunTests.cs b/tests/SlaySharp.Tests/GameRunTests.cs new file mode 100644 index 0000000..428133f --- /dev/null +++ b/tests/SlaySharp.Tests/GameRunTests.cs @@ -0,0 +1,86 @@ +ο»Ώusing SlaySharp.Core.Catalogs; +using SlaySharp.Core.Models; +using SlaySharp.Core.Run; +using Xunit; + +namespace SlaySharp.Tests; + +public class GameRunTests +{ + [Fact] + public void MapGraph_GeneratesSevenFloors_WithBossAtTop() + { + var map = MapGraph.GenerateAct1Map(seed: 42); + + Assert.Equal(7, map.Floors.Count); + Assert.NotEmpty(map.Floors[0]); + Assert.Single(map.Floors[6]); + Assert.Equal(MapNodeType.Boss, map.Floors[6][0].Type); + } + + [Fact] + public void GameRun_StartNewRun_InitializesCorrectly() + { + var run = new GameRun(); + run.StartNewRun(seed: 42); + + Assert.NotNull(run.Player); + Assert.Equal(70, run.Player.MaxHp); + Assert.Equal(70, run.Player.CurrentHp); + Assert.Equal(60, run.DevCredits); + Assert.Equal(RunScreen.Map, run.CurrentScreen); + Assert.NotEmpty(run.Player.MasterDeck); + Assert.Equal(3, run.Player.Relics.Count); + } + + [Fact] + public void GameRun_SelectFloor0Node_TransitionsToCorrectScreen() + { + var run = new GameRun(); + run.StartNewRun(seed: 42); + + var firstFloorNode = run.Map.Floors[0][0]; + Assert.True(run.CanVisitNode(firstFloorNode)); + + run.SelectNode(firstFloorNode); + + Assert.Contains(firstFloorNode.Id, run.VisitedNodeIds); + Assert.Equal(RunScreen.Combat, run.CurrentScreen); + Assert.NotNull(run.ActiveCombat); + } + + [Fact] + public void StoryEvent_StackOverflowOption_ModifiesDeckAndHp() + { + var run = new GameRun(); + run.StartNewRun(seed: 42); + var initialDeckCount = run.Player.MasterDeck.Count; + var initialMaxHp = run.Player.MaxHp; + + var stackOverflowEvent = StoryEvent.GetAct1Events().First(e => e.Id == "stackoverflow"); + var copyPasteOption = stackOverflowEvent.Options[0]; + + copyPasteOption.ApplyEffect(run); + + Assert.Equal(initialDeckCount + 1, run.Player.MasterDeck.Count); + Assert.True(run.Player.MasterDeck.Last().Upgraded); + Assert.Equal(initialMaxHp - 8, run.Player.MaxHp); + } + + [Fact] + public void GameRun_CompleteCombat_AwardsDevCredits() + { + var run = new GameRun(); + run.StartNewRun(seed: 42); + var startCredits = run.DevCredits; + + var firstFloorNode = run.Map.Floors[0][0]; + run.SelectNode(firstFloorNode); + + run.CompleteCombatAndClaimRewards(bonusCredits: 35); + + Assert.Equal(startCredits + 35, run.DevCredits); + Assert.Equal(RunScreen.Map, run.CurrentScreen); + Assert.Null(run.ActiveCombat); + } +}