From e7512f7c3578b7d3d5bcfb1bd094816abb74682f Mon Sep 17 00:00:00 2001 From: Michal Vrba Date: Wed, 16 Sep 2026 23:58:25 +0200 Subject: [PATCH] improvements --- src/SlaySharp.Core/Catalogs/CardCatalog.cs | 197 ++++++- src/SlaySharp.Core/Catalogs/EnemyCatalog.cs | 218 ++++++++ src/SlaySharp.Core/Catalogs/StarterDecks.cs | 36 +- src/SlaySharp.Core/Models/CharacterClass.cs | 21 + src/SlaySharp.Core/Models/RunRecord.cs | 37 ++ src/SlaySharp.Core/Relics/Relic.cs | 36 +- src/SlaySharp.Core/Run/GameRun.cs | 50 +- src/SlaySharp.Web/Components/CardView.razor | 135 +++-- src/SlaySharp.Web/Components/ShopView.razor | 388 +++++++++----- src/SlaySharp.Web/Pages/Index.razor | 549 +++++++++++++++----- src/SlaySharp.Web/wwwroot/css/game.css | 423 ++++++++++++--- src/SlaySharp.Web/wwwroot/js/audio.js | 101 ++++ tests/SlaySharp.Tests/CombatEngineTests.cs | 108 +++- 13 files changed, 1906 insertions(+), 393 deletions(-) create mode 100644 src/SlaySharp.Core/Models/CharacterClass.cs create mode 100644 src/SlaySharp.Core/Models/RunRecord.cs diff --git a/src/SlaySharp.Core/Catalogs/CardCatalog.cs b/src/SlaySharp.Core/Catalogs/CardCatalog.cs index 9b36c15..1c70006 100644 --- a/src/SlaySharp.Core/Catalogs/CardCatalog.cs +++ b/src/SlaySharp.Core/Catalogs/CardCatalog.cs @@ -512,6 +512,196 @@ public class NullCoalescingCard : Card }; } +public class TaskWhenAllCard : Card +{ + private int _damage = 14; + + public TaskWhenAllCard() : base( + "Task.WhenAll()", + "await Task.WhenAll(tasks);", + "target.Hit(14);", + "Deal 14 damage.", + cost: 2, + type: CardType.Attack, + rarity: CardRarity.Common, + targetType: TargetType.SingleEnemy + ) { } + + protected override void OnUpgrade() + { + _damage = 19; + Description = "Deal 19 damage."; + CodeSnippet = "target.Hit(19);"; + } + + 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 (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, _damage, isPiercing: false, Name); + engine.Log.AddSuccess($"[Task.WhenAll] Concurrency burst completed! Dealt {dealt} dmg."); + } + + public override Card Clone() => new TaskWhenAllCard { Upgraded = this.Upgraded, Name = this.Name, Description = this.Description, _damage = this._damage, CodeSnippet = this.CodeSnippet }; +} + +public class CancellationTokenCard : Card +{ + private int _shield = 4; + private int _weak = 2; + + public CancellationTokenCard() : base( + "CancellationToken.Cancel()", + "cts.Cancel();", + "AllEnemies.Weak += 2; shield += 4;", + "Apply 2 Weak to all enemies. Gain 4 Shield.", + cost: 1, + type: CardType.Skill, + rarity: CardRarity.Uncommon, + targetType: TargetType.AllEnemies + ) { } + + protected override void OnUpgrade() + { + _shield = 7; + _weak = 3; + Description = "Apply 3 Weak to all enemies. Gain 7 Shield."; + CodeSnippet = "AllEnemies.Weak += 3; shield += 7;"; + } + + public override int GetShieldProfile(CombatEngine engine) => _shield; + + public override void ApplyEffect(CombatEngine engine, Player player, Character? target) + { + player.GainShield(_shield); + engine.NotifyShield(player.Id, player.Name, _shield, Name); + foreach (var enemy in engine.Enemies) + { + enemy.Statuses.Add(StatusType.Weak, _weak); + } + engine.Log.AddWarning($"[CancellationToken] Operations aborted! +{_shield} Shield, {_weak} Weak applied."); + } + + public override Card Clone() => new CancellationTokenCard { Upgraded = this.Upgraded, Name = this.Name, Description = this.Description, _shield = this._shield, _weak = this._weak, CodeSnippet = this.CodeSnippet }; +} + +public class ChannelWriteCard : Card +{ + private int _shield = 5; + private int _draw = 2; + + public ChannelWriteCard() : base( + "Channel.Writer.WriteAsync()", + "await channel.Writer.WriteAsync(item);", + "Draw(2); shield += 5;", + "Draw 2 cards. Gain 5 Shield.", + cost: 1, + type: CardType.Skill, + rarity: CardRarity.Common, + targetType: TargetType.Self + ) { } + + protected override void OnUpgrade() + { + _shield = 8; + _draw = 3; + Description = "Draw 3 cards. Gain 8 Shield."; + CodeSnippet = "Draw(3); shield += 8;"; + } + + public override int GetShieldProfile(CombatEngine engine) => _shield; + + public override void ApplyEffect(CombatEngine engine, Player player, Character? target) + { + player.GainShield(_shield); + engine.NotifyShield(player.Id, player.Name, _shield, Name); + player.DrawCards(_draw, engine.Log); + engine.Log.AddInfo($"[Channel] Emitted {_draw} items to consumer, +{_shield} Shield."); + } + + public override Card Clone() => new ChannelWriteCard { Upgraded = this.Upgraded, Name = this.Name, Description = this.Description, _shield = this._shield, _draw = this._draw, CodeSnippet = this.CodeSnippet }; +} + +public class StackallocCard : Card +{ + private int _damage = 13; + + public StackallocCard() : base( + "Span stackalloc", + "Span buf = stackalloc byte[16];", + "Player.HP -= 2; target.DirectHit(13);", + "Take 2 self-damage. Deal 13 piercing damage.", + cost: 1, + type: CardType.Attack, + rarity: CardRarity.Common, + targetType: TargetType.SingleEnemy + ) { } + + protected override void OnUpgrade() + { + _damage = 18; + Description = "Take 2 self-damage. Deal 18 piercing damage."; + CodeSnippet = "Player.HP -= 2; target.DirectHit(18);"; + } + + 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; + player.TakeDamage(2, isPiercing: true); + engine.NotifyDamage(player.Id, player.Name, 2, true, false, "StackallocCost"); + var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, _damage, isPiercing: true, Name); + engine.Log.AddWarning($"[stackalloc] Zero-heap unmanaged allocation! Dealt {dealt} piercing dmg (-2 SLA)."); + } + + public override Card Clone() => new StackallocCard { Upgraded = this.Upgraded, Name = this.Name, Description = this.Description, _damage = this._damage, CodeSnippet = this.CodeSnippet }; +} + +public class FixedPointerCard : Card +{ + private int _dmgPerHit = 9; + + public FixedPointerCard() : base( + "fixed (byte* ptr)", + "fixed (byte* p = &data[0]) { Hit(9); Hit(9); }", + "target.DirectHit(9); // x2 hits", + "Attack 2 times for 9 piercing damage each.", + cost: 2, + type: CardType.Attack, + rarity: CardRarity.Rare, + targetType: TargetType.SingleEnemy + ) { } + + protected override void OnUpgrade() + { + _dmgPerHit = 13; + Description = "Attack 2 times for 13 piercing damage each."; + CodeSnippet = "fixed (byte* p = &data[0]) { Hit(13); Hit(13); }"; + } + + public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null) + => (_dmgPerHit, _dmgPerHit, 2, true); + + public override void ApplyEffect(CombatEngine engine, Player player, Character? target) + { + if (target == null) return; + int total = 0; + for (int i = 0; i < 2; i++) + { + if (target.IsDead) break; + var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, _dmgPerHit, isPiercing: true, $"{Name} #{i+1}"); + total += dealt; + } + engine.Log.AddSuccess($"[fixed pointer] Pinned memory hit 2 times: total {total} piercing dmg."); + } + + public override Card Clone() => new FixedPointerCard { Upgraded = this.Upgraded, Name = this.Name, Description = this.Description, _dmgPerHit = this._dmgPerHit, CodeSnippet = this.CodeSnippet }; +} + public static class CardCatalog { public static List> AllCardFactories => new() @@ -527,7 +717,12 @@ public static class CardCatalog () => new ThreadSleepCard(), () => new ParallelForEachCard(), () => new LinqWhereCard(), - () => new NullCoalescingCard() + () => new NullCoalescingCard(), + () => new TaskWhenAllCard(), + () => new CancellationTokenCard(), + () => new ChannelWriteCard(), + () => new StackallocCard(), + () => new FixedPointerCard() }; public static Card GetRandomCard() diff --git a/src/SlaySharp.Core/Catalogs/EnemyCatalog.cs b/src/SlaySharp.Core/Catalogs/EnemyCatalog.cs index d5610ab..f7410a9 100644 --- a/src/SlaySharp.Core/Catalogs/EnemyCatalog.cs +++ b/src/SlaySharp.Core/Catalogs/EnemyCatalog.cs @@ -235,3 +235,221 @@ public class LegacyMonolithBossEnemy : Enemy DecideNextIntent(engine); } } + +public class ThreadDeadlockEnemy : Enemy +{ + private int _step = 0; + + public ThreadDeadlockEnemy() : base( + name: "Thread Deadlock", + maxHp: 36, + description: "Mutual exclusion cyclic dependency holding unmanaged lock monitors.", + icon: "๐Ÿ”’" + ) { } + + public override void DecideNextIntent(CombatEngine engine) + { + CurrentIntent = (_step % 3) switch + { + 0 => new Intent(IntentType.Attack, 9, 1, "Lock Contention (9 dmg)"), + 1 => new Intent(IntentType.Debuff, 0, 1, "Starvation Lock (+2 Weak, +1 Leak)", StatusType.Weak, 2), + 2 => new Intent(IntentType.AttackDefend, 14, 1, "Deadlock Spike (14 dmg & +8 Shield)"), + _ => new Intent(IntentType.Attack, 8, 1, "Contention Strike") + }; + } + + public override void ExecuteIntent(CombatEngine engine) + { + if (CurrentIntent == null) return; + switch (_step % 3) + { + case 0: + var dmg0 = Statuses.CalculateOutgoingDamage(9); + var isCrit0 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0; + var dealt0 = engine.Player.TakeDamage(dmg0); + engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt0, false, isCrit0, Name); + engine.Log.AddError($"[{Name}] lock contention dealt {dealt0} dmg."); + break; + case 1: + engine.Player.Statuses.Add(StatusType.Weak, 2); + engine.Player.Statuses.Add(StatusType.MemoryLeak, 1); + engine.Log.AddWarning($"[{Name}] starved worker threads (+2 Weak, +1 Memory Leak)."); + break; + case 2: + var dmg2 = Statuses.CalculateOutgoingDamage(14); + var isCrit2 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0; + var dealt2 = engine.Player.TakeDamage(dmg2); + GainShield(8); + engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt2, false, isCrit2, Name); + engine.NotifyShield(Id, Name, 8, Name); + engine.Log.AddError($"[{Name}] Deadlock spike hit for {dealt2} dmg and gained +8 Shield!"); + break; + } + _step++; + DecideNextIntent(engine); + } +} + +public class StackOverflowEnemy : Enemy +{ + private int _step = 0; + + public StackOverflowEnemy() : base( + name: "StackOverflow Exception", + maxHp: 28, + description: "Unbounded recursive call graph expanding call frames infinitely.", + icon: "โ™พ๏ธ" + ) { } + + public override void DecideNextIntent(CombatEngine engine) + { + CurrentIntent = (_step % 3) switch + { + 0 => new Intent(IntentType.AttackDefend, 6, 1, "RecursiveFrame() (6 dmg & +6 Shield)"), + 1 => new Intent(IntentType.Buff, 0, 1, "Tail-Call Accumulation (+2 Strength)", StatusType.Strength, 2), + 2 => new Intent(IntentType.Attack, 16, 1, "Stack Overflow Explosion (16 dmg)"), + _ => new Intent(IntentType.Attack, 6, 1, "Recursive Frame") + }; + } + + public override void ExecuteIntent(CombatEngine engine) + { + if (CurrentIntent == null) return; + switch (_step % 3) + { + case 0: + var dmg0 = Statuses.CalculateOutgoingDamage(6); + var isCrit0 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0; + var dealt0 = engine.Player.TakeDamage(dmg0); + GainShield(6); + engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt0, false, isCrit0, Name); + engine.NotifyShield(Id, Name, 6, Name); + engine.Log.AddError($"[{Name}] spawned stack frame! Dealt {dealt0} dmg, gained +6 Shield."); + break; + case 1: + Statuses.Add(StatusType.Strength, 2); + engine.Log.AddWarning($"[{Name}] recursive depth multiplied (+2 Strength)."); + break; + case 2: + var dmg2 = Statuses.CalculateOutgoingDamage(16); + 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}] STACK LIMIT EXCEEDED! Dealt {dealt2} fatal damage!"); + break; + } + _step++; + DecideNextIntent(engine); + } +} + +public class HeisenbugEnemy : Enemy +{ + private int _step = 0; + + public HeisenbugEnemy() : base( + name: "Heisenbug Anomaly", + maxHp: 32, + description: "Disappears or mutates state when debug breakpoints are attached.", + icon: "๐Ÿ‘ป" + ) { } + + public override void DecideNextIntent(CombatEngine engine) + { + CurrentIntent = (_step % 2) switch + { + 0 => new Intent(IntentType.Attack, 10, 1, "Quantum Shift (10 piercing dmg)"), + 1 => new Intent(IntentType.Defend, 0, 1, "Observer Camouflage (+10 Shield)"), + _ => new Intent(IntentType.Attack, 8, 1, "Quantum Strike") + }; + } + + public override void ExecuteIntent(CombatEngine engine) + { + if (CurrentIntent == null) return; + switch (_step % 2) + { + case 0: + var dmg0 = Statuses.CalculateOutgoingDamage(10); + var dealt0 = engine.Player.TakeDamage(dmg0, isPiercing: true); + engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt0, true, false, Name); + engine.Log.AddWarning($"[{Name}] quantum phase pierced shields for {dealt0} dmg!"); + break; + case 1: + GainShield(10); + engine.NotifyShield(Id, Name, 10, Name); + engine.Log.AddInfo($"[{Name}] masked thread state: +10 Shield."); + break; + } + _step++; + DecideNextIntent(engine); + } +} + +public class KubernetesClusterBossEnemy : Enemy +{ + private int _step = 0; + + public KubernetesClusterBossEnemy() : base( + name: "K8s Production Cluster", + maxHp: 110, + description: "Multi-pod microservice mesh with rolling deployments and autoscaling.", + icon: "โ˜ธ๏ธ" + ) { } + + public override void DecideNextIntent(CombatEngine engine) + { + CurrentIntent = (_step % 4) switch + { + 0 => new Intent(IntentType.AttackDefend, 10, 1, "Leader Pod Election (10 dmg & +15 Shield)"), + 1 => new Intent(IntentType.Attack, 5, 4, "Rolling Pod Replica Surge (5x4 dmg)"), + 2 => new Intent(IntentType.Debuff, 0, 1, "Ingress DDoS (+3 Weak, +2 Vulnerable)", StatusType.Weak, 3), + 3 => new Intent(IntentType.Attack, 24, 1, "OOM Killer Pod Eviction (24 dmg)"), + _ => new Intent(IntentType.Attack, 12, 1, "Cluster Strike") + }; + } + + public override void ExecuteIntent(CombatEngine engine) + { + if (CurrentIntent == null) return; + switch (_step % 4) + { + case 0: + var dmg0 = Statuses.CalculateOutgoingDamage(10); + var isCrit0 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0; + var dealt0 = engine.Player.TakeDamage(dmg0); + GainShield(15); + engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt0, false, isCrit0, Name); + engine.NotifyShield(Id, Name, 15, Name); + engine.Log.AddError($"[{Name}] elected leader pod! Dealt {dealt0} dmg, gained +15 Shield."); + break; + case 1: + int total = 0; + var isCrit1 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0; + for (int i = 0; i < 4; i++) + { + var d = Statuses.CalculateOutgoingDamage(5); + total += engine.Player.TakeDamage(d); + engine.NotifyDamage(engine.Player.Id, engine.Player.Name, d, false, isCrit1, $"{Name} Pod #{i+1}"); + } + engine.Log.AddFatal($"[{Name}] rolling replicas fired! Total {total} damage across 4 pods!"); + break; + case 2: + engine.Player.Statuses.Add(StatusType.Weak, 3); + engine.Player.Statuses.Add(StatusType.Vulnerable, 2); + GainShield(10); + engine.NotifyShield(Id, Name, 10, Name); + engine.Log.AddWarning($"[{Name}] Ingress saturated! Player gained +3 Weak, +2 Vulnerable."); + break; + case 3: + var dmg3 = Statuses.CalculateOutgoingDamage(24); + var isCrit3 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0; + var dealt3 = engine.Player.TakeDamage(dmg3); + engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt3, false, isCrit3, Name); + engine.Log.AddFatal($"[{Name}] ๐Ÿ’ฅ OOM KILLER MASSIVE POD EVICTION! Dealt {dealt3} heavy damage!"); + break; + } + _step++; + DecideNextIntent(engine); + } +} diff --git a/src/SlaySharp.Core/Catalogs/StarterDecks.cs b/src/SlaySharp.Core/Catalogs/StarterDecks.cs index 76cbd8e..05b1961 100644 --- a/src/SlaySharp.Core/Catalogs/StarterDecks.cs +++ b/src/SlaySharp.Core/Catalogs/StarterDecks.cs @@ -19,9 +19,43 @@ public static class StarterDecks 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) }; } + + public static List CreateAsyncMasterDeck() + { + return new List + { + new TaskWhenAllCard(), + new TaskWhenAllCard(), + new ChannelWriteCard(), + new ChannelWriteCard(), + new CancellationTokenCard(), + new ThreadSleepCard(), + new DynamicExpandoCard(), + new LinqWhereCard(), + new ParallelForEachCard(), + new TryCatchCard(), + new ConsoleWriteLineCard() + }; + } + + public static List CreateUnsafeHackerDeck() + { + return new List + { + new StackallocCard(), + new StackallocCard(), + new FixedPointerCard(), + new SpanSliceCard(), + new SpanSliceCard(), + new ThrowExceptionCard(), + new TryCatchCard(), + new NullCoalescingCard(), + new ConsoleWriteLineCard(), + new TieredCompilationCard() + }; + } } diff --git a/src/SlaySharp.Core/Models/CharacterClass.cs b/src/SlaySharp.Core/Models/CharacterClass.cs new file mode 100644 index 0000000..f275c71 --- /dev/null +++ b/src/SlaySharp.Core/Models/CharacterClass.cs @@ -0,0 +1,21 @@ +namespace SlaySharp.Core.Models; + +public enum DeveloperArchetype +{ + PrincipalArchitect, + AsyncConcurrencyMaster, + UnsafeMemoryHacker +} + +public class CharacterClassInfo +{ + public DeveloperArchetype Archetype { get; set; } + public string Title { get; set; } = string.Empty; + public string RoleBadge { get; set; } = string.Empty; + public string Icon { get; set; } = "๐Ÿ‘จโ€๐Ÿ’ป"; + public string Description { get; set; } = string.Empty; + public int StartingHp { get; set; } + public int StartingEnergy { get; set; } + public string Specialty { get; set; } = string.Empty; + public string TagColor { get; set; } = "#61afef"; +} \ No newline at end of file diff --git a/src/SlaySharp.Core/Models/RunRecord.cs b/src/SlaySharp.Core/Models/RunRecord.cs new file mode 100644 index 0000000..5df6a3b --- /dev/null +++ b/src/SlaySharp.Core/Models/RunRecord.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; + +namespace SlaySharp.Core.Models; + +public class RunRecord +{ + public string CommitHash { get; set; } = string.Empty; + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + public DeveloperArchetype Archetype { get; set; } = DeveloperArchetype.PrincipalArchitect; + public bool IsVictory { get; set; } + public int FinalHp { get; set; } + public int MaxHp { get; set; } + public int DevCredits { get; set; } + public int DeckSize { get; set; } + public int FloorReached { get; set; } + public string Message { get; set; } = string.Empty; + + public static RunRecord Create(DeveloperArchetype archetype, bool isVictory, int hp, int maxHp, int credits, int deckSize, int floor, string message = "") + { + var randomHex = Guid.NewGuid().ToString("N")[..7]; + return new RunRecord + { + CommitHash = randomHex, + Timestamp = DateTime.UtcNow, + Archetype = archetype, + IsVictory = isVictory, + FinalHp = hp, + MaxHp = maxHp, + DevCredits = credits, + DeckSize = deckSize, + FloorReached = floor, + Message = string.IsNullOrWhiteSpace(message) + ? (isVictory ? "feat(prod): deployed zero-downtime release to production ๐Ÿš€" : "fix(hotfix): deployment rolled back due to fatal exception ๐Ÿ’ฅ") + : message + }; + } +} diff --git a/src/SlaySharp.Core/Relics/Relic.cs b/src/SlaySharp.Core/Relics/Relic.cs index 83cf486..5e669ed 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; @@ -139,3 +139,37 @@ public class DapperRelic : Relic } } } + +public class UnsafeCompilerRelic : Relic +{ + public UnsafeCompilerRelic() : base( + "System.Runtime.CompilerServices.Unsafe", + "System.Runtime.CompilerServices.Unsafe", + "v6.0.0", + "Direct memory access: Deal +2 bonus piercing damage on all attacks.", + "โšก" + ) { } + + public override void OnCombatStart(CombatEngine engine) + { + engine.Player.Statuses.Add(Enums.StatusType.Strength, 1); + engine.Log.AddSuccess("[Unsafe] Direct memory pointers initialized (+1 Strength)."); + } +} + +public class SystemThreadingChannelsRelic : Relic +{ + public SystemThreadingChannelsRelic() : base( + "System.Threading.Channels", + "System.Threading.Channels", + "v8.0.0", + "High-throughput bounded queue: Draw +1 extra card at the start of every turn.", + "๐Ÿ”€" + ) { } + + public override void OnTurnStart(CombatEngine engine) + { + engine.Player.DrawCards(1, engine.Log); + engine.Log.AddInfo("[Channels] Bounded channel reader emitted +1 card."); + } +} diff --git a/src/SlaySharp.Core/Run/GameRun.cs b/src/SlaySharp.Core/Run/GameRun.cs index 1506b9c..9dba5ba 100644 --- a/src/SlaySharp.Core/Run/GameRun.cs +++ b/src/SlaySharp.Core/Run/GameRun.cs @@ -31,14 +31,36 @@ public class GameRun 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()); + public DeveloperArchetype SelectedArchetype { get; set; } = DeveloperArchetype.PrincipalArchitect; - Player.Relics.Add(new NewtonsoftJsonRelic()); - Player.Relics.Add(new BenchmarkDotNetRelic()); - Player.Relics.Add(new SerilogRelic()); + public void StartNewRun(DeveloperArchetype archetype = DeveloperArchetype.PrincipalArchitect, int? seed = null) + { + SelectedArchetype = archetype; + + switch (archetype) + { + case DeveloperArchetype.AsyncConcurrencyMaster: + Player = new Player("Async Master", 65, maxEnergy: 4, seed: seed); + Player.MasterDeck.AddRange(StarterDecks.CreateAsyncMasterDeck()); + Player.Relics.Add(new PollyRelic()); + Player.Relics.Add(new SystemThreadingChannelsRelic()); + break; + + case DeveloperArchetype.UnsafeMemoryHacker: + Player = new Player("Unsafe Hacker", 60, maxEnergy: 3, seed: seed); + Player.MasterDeck.AddRange(StarterDecks.CreateUnsafeHackerDeck()); + Player.Relics.Add(new UnsafeCompilerRelic()); + Player.Relics.Add(new DapperRelic()); + break; + + default: + Player = new Player("Principal Architect", 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()); + break; + } DevCredits = 60; VisitedNodeIds.Clear(); @@ -99,20 +121,26 @@ public class GameRun List enemies; if (isBoss) { - enemies = new List { new LegacyMonolithBossEnemy() }; + enemies = Random.Shared.Next(2) == 0 + ? new List { new LegacyMonolithBossEnemy() } + : new List { new KubernetesClusterBossEnemy() }; } else if (isElite) { - enemies = new List { new MemoryLeakSpectreEnemy(hpBonus: 6), new OffByOneBugEnemy() }; + var roll = Random.Shared.Next(2); + enemies = roll == 0 + ? new List { new ThreadDeadlockEnemy(), new MemoryLeakSpectreEnemy() } + : new List { new StackOverflowEnemy(), new OffByOneBugEnemy() }; } else { // Standard combat - var roll = Random.Shared.Next(2); + var roll = Random.Shared.Next(3); enemies = roll switch { 0 => new List { new NullReferenceExceptionEnemy(), new OffByOneBugEnemy() }, - _ => new List { new MemoryLeakSpectreEnemy(), new OffByOneBugEnemy() } + 1 => new List { new HeisenbugEnemy(), new OffByOneBugEnemy() }, + _ => new List { new MemoryLeakSpectreEnemy(), new NullReferenceExceptionEnemy() } }; } diff --git a/src/SlaySharp.Web/Components/CardView.razor b/src/SlaySharp.Web/Components/CardView.razor index 1177034..295b76c 100644 --- a/src/SlaySharp.Web/Components/CardView.razor +++ b/src/SlaySharp.Web/Components/CardView.razor @@ -6,40 +6,43 @@ @onclick="HandleCardClick" @onmouseenter="HandleMouseEnter" @onmouseleave="HandleMouseLeave"> - + + +
+ +
- # - + @GetActionTypeIcon() + @Card.Name.Replace(" ", "").Replace("()", "")@(Card.Upgraded ? "_v2.cs" : ".cs")
-
-
- @Card.Cost CPU -
+
+ @Card.Cost CPU
- -
- -
- - @GetActionTypeIcon() @Card.Type - -
- - @GetCardArtIcon(Card.Name) - + +
+ @GetActionTypeIcon() + @GetCardTypeLabel() @if (Card.Upgraded) { -
- TIER-1 + -
+ TIER-1 + }
+ +
+ +
@GetActionTypeIcon()
+ + +
+ @GetCardArtIcon(Card.Name) +
+
@@ -51,10 +54,10 @@
-
- // @(Card.Upgraded ? "Optimized IL" : "Managed code") +
+ // @(Card.Upgraded ? "Optimized Tier-1 IL" : "Managed CLR Task")
-
+
@FormatCodeSnippet(Card.CodeSnippet)
@@ -62,9 +65,9 @@
@@ -101,9 +104,17 @@ CardType.Skill => "type-skill", CardType.Power => "type-power", CardType.Curse => "type-curse", - _ => "" + _ => "type-skill" }); + classes.Add(Card.Rarity switch + { + CardRarity.Rare => "rarity-rare", + CardRarity.Uncommon => "rarity-uncommon", + _ => "rarity-common" + }); + + if (Card.Upgraded) classes.Add("tier-upgraded"); if (IsSelected) classes.Add("selected"); if (IsPlayable) classes.Add("playable"); else classes.Add("unaffordable"); @@ -127,18 +138,42 @@ _ => "๐Ÿ“œ" }; + private string GetCardTypeLabel() + { + if (Card.Type == CardType.Attack) return "ATTACK"; + if (Card.Type == CardType.Skill) + { + if (Card.Description.Contains("Shield", StringComparison.OrdinalIgnoreCase) || + Card.Description.Contains("shield", StringComparison.OrdinalIgnoreCase)) + { + return "SHIELD / SKILL"; + } + return "SKILL / UTILITY"; + } + if (Card.Type == CardType.Power) return "POWER / BUFF"; + if (Card.Type == CardType.Curse) return "DEBT / BUG"; + return "CARD"; + } + private string GetCardArtIcon(string name) { if (name.Contains("WriteLine", StringComparison.OrdinalIgnoreCase)) return "๐Ÿ’ป"; if (name.Contains("try", StringComparison.OrdinalIgnoreCase)) return "๐Ÿ›ก๏ธ"; if (name.Contains("Collect", StringComparison.OrdinalIgnoreCase)) return "โ™ป๏ธ"; - if (name.Contains("Slice", StringComparison.OrdinalIgnoreCase)) return "โšก"; + if (name.Contains("Slice", StringComparison.OrdinalIgnoreCase)) return "๐Ÿ—ก๏ธ"; if (name.Contains("Exception", StringComparison.OrdinalIgnoreCase)) return "๐Ÿ’ฅ"; if (name.Contains("Compilation", StringComparison.OrdinalIgnoreCase)) return "๐Ÿš€"; if (name.Contains("Sleep", StringComparison.OrdinalIgnoreCase)) return "โณ"; if (name.Contains("Parallel", StringComparison.OrdinalIgnoreCase)) return "๐Ÿ”€"; if (name.Contains("Where", StringComparison.OrdinalIgnoreCase)) return "๐Ÿ”"; if (name.Contains("fallback", StringComparison.OrdinalIgnoreCase) || name.Contains("??")) return "๐Ÿ›Ÿ"; + if (name.Contains("Task", StringComparison.OrdinalIgnoreCase) || name.Contains("WhenAll", StringComparison.OrdinalIgnoreCase)) return "โšก"; + if (name.Contains("Cancellation", StringComparison.OrdinalIgnoreCase)) return "๐Ÿ›‘"; + if (name.Contains("Channel", StringComparison.OrdinalIgnoreCase)) return "๐Ÿ“ก"; + if (name.Contains("stackalloc", StringComparison.OrdinalIgnoreCase)) return "๐Ÿ’พ"; + if (name.Contains("fixed", StringComparison.OrdinalIgnoreCase)) return "๐Ÿ“Œ"; + if (name.Contains("Expando", StringComparison.OrdinalIgnoreCase) || name.Contains("dynamic", StringComparison.OrdinalIgnoreCase)) return "๐Ÿ”ฎ"; + if (name.Contains("Random", StringComparison.OrdinalIgnoreCase)) return "๐ŸŽฒ"; return "๐Ÿ“„"; } @@ -147,14 +182,15 @@ 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"); + .Replace("damage", "โš”๏ธ damage") + .Replace("Damage", "โš”๏ธ Damage") + .Replace("Shield", "๐Ÿ›ก๏ธ Shield") + .Replace("shield", "๐Ÿ›ก๏ธ shield") + .Replace("piercing", "โšก piercing") + .Replace("CPU", "๐Ÿ”‹ CPU") + .Replace("Strength", "๐Ÿ’ช Strength") + .Replace("Weak", "๐Ÿ“‰ Weak") + .Replace("Memory Leak", "โš ๏ธ Memory Leak"); return new MarkupString(formatted); } @@ -164,17 +200,22 @@ if (string.IsNullOrEmpty(snippet)) return new MarkupString(""); var highlighted = snippet - .Replace("public", "public") - .Replace("void", "void") - .Replace("var", "var") - .Replace("throw", "throw") - .Replace("new", "new") - .Replace("try", "try") - .Replace("catch", "catch") - .Replace("await", "await") - .Replace("return", "return") - .Replace("Bug", "Bug") - .Replace("Exception", "Exception"); + .Replace("public", "public") + .Replace("void", "void") + .Replace("var", "var") + .Replace("throw", "throw") + .Replace("new", "new") + .Replace("try", "try") + .Replace("catch", "catch") + .Replace("await", "await") + .Replace("return", "return") + .Replace("fixed", "fixed") + .Replace("stackalloc", "stackalloc") + .Replace("Span", "Span") + .Replace("Task", "Task") + .Replace("Channel", "Channel") + .Replace("Bug", "Bug") + .Replace("Exception", "Exception"); return new MarkupString(highlighted); } diff --git a/src/SlaySharp.Web/Components/ShopView.razor b/src/SlaySharp.Web/Components/ShopView.razor index 4716486..49a6e34 100644 --- a/src/SlaySharp.Web/Components/ShopView.razor +++ b/src/SlaySharp.Web/Components/ShopView.razor @@ -1,156 +1,262 @@ -๏ปฟ@using SlaySharp.Core.Run +@using SlaySharp.Core.Run @using SlaySharp.Core.Models @using SlaySharp.Core.Catalogs @using SlaySharp.Core.Relics @inject IJSRuntime JS
- -
-
- SlaySharp - โ€บ - nuget.org - โ€บ - Package Manager + +
+
+ ๐Ÿ“ฆ + NuGet: Solution Packages + โœ•
- -
- ๐Ÿ›ก๏ธ SLA: @Run.Player.CurrentHp/@Run.Player.MaxHp +
+ ๐Ÿ›ก๏ธ SLA: @Run.Player.CurrentHp/@Run.Player.MaxHp HP ๐Ÿ’ฐ @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); - -
- - -
- } -
-
+
+ Package source: + + nuget.org (v3 API) +
+ + + @if (!string.IsNullOrEmpty(_message)) + { +
+ @_message + +
+ } + + +
+ @switch (_currentTab) + { + case NuGetTab.Browse: + +
+ +
+

+ ๐Ÿ“ฆ + Featured NuGet Packages (Tooling & Middleware): +

+
+ @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 (Deck Additions): +

+
+ @for (int i = 0; i < _shopCards.Count; i++) + { + var card = _shopCards[i]; + var cost = _cardCosts[i]; + var isBought = _purchasedCardIds.Contains(card.Id); + +
+ + +
+ } +
+
+
+ break; + + case NuGetTab.Installed: + +
+
+

๐Ÿ“ฆ Installed Tooling & NuGet Packages (@Run.Player.Relics.Count)

+
+ @foreach (var relic in Run.Player.Relics) + { +
+
+ @relic.Icon +
+
@relic.Name
+
@relic.PackageVersion
+
+
+

@relic.Description

+
+ } +
+
+ +
+

๐ŸŒฟ Active Solution Deck (@Run.Player.MasterDeck.Count Cards)

+
+ @foreach (var card in Run.Player.MasterDeck) + { + + } +
+
+
+ break; + + case NuGetTab.Updates: + +
+

โฌ† NuGet Package Updates: JIT Optimize to Tier-1 (+) (45 Credits each)

+

Upgrade unmanaged IL routines with Tiered Compilation AggressiveOptimization.

+ + @{ + var upgradeables = GetUpgradeableCards(); + } + @if (upgradeables.Any()) + { +
+ @foreach (var card in upgradeables) + { +
+ + +
+ } +
+ } + else + { +
+ โœ“ All packages in solution are running on the latest Tier-1 runtime version. +
+ } +
+ break; + + case NuGetTab.Uninstall: + +
+

๐Ÿ—‘๏ธ Technical Debt Refactoring (Purge 1 Card for 75 Credits)

+

Uninstall legacy code blocks from your active solution deck.

+ + @if (_hasPurgedCard) + { +
+ โœ“ Technical debt purge quota used for this sprint site. +
+ } + else + { +
+ @foreach (var card in Run.Player.MasterDeck) + { +
+ + +
+ } +
+ } +
+ break; + } +
- -@if (_showPurgeModal) -{ - + + @code { [Parameter] public GameRun Run { get; set; } = null!; + private enum NuGetTab { Browse, Installed, Updates, Uninstall } + private NuGetTab _currentTab = NuGetTab.Browse; + private List _shopCards = new(); private List _cardCosts = new(); private List _shopRelics = new(); @@ -158,7 +264,6 @@ private readonly HashSet _purchasedRelicIds = new(); private int _relicCost = 110; private bool _hasPurgedCard = false; - private bool _showPurgeModal = false; private string? _message; protected override void OnInitialized() @@ -174,10 +279,16 @@ _shopRelics = new List { new PollyRelic(), - new DapperRelic() + new DapperRelic(), + new UnsafeCompilerRelic() }; } + private List GetUpgradeableCards() + { + return Run.Player.MasterDeck.Where(c => !c.Upgraded).ToList(); + } + private void BuyCard(Card card, int cost) { if (Run.DevCredits >= cost) @@ -204,14 +315,25 @@ } } + private void UpgradeCard(Card card) + { + if (Run.DevCredits >= 45 && !card.Upgraded) + { + Run.DevCredits -= 45; + card.Upgrade(); + _message = $"Upgraded {card.Name} to Tier-1 (+) Optimized IL!"; + _ = JS.InvokeVoidAsync("slayAudio.playVictorySound"); + StateHasChanged(); + } + } + private void PurgeCard(Card card) { - if (Run.DevCredits >= 75) + if (Run.DevCredits >= 75 && !_hasPurgedCard) { 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(); diff --git a/src/SlaySharp.Web/Pages/Index.razor b/src/SlaySharp.Web/Pages/Index.razor index 592b9b7..8839b0f 100644 --- a/src/SlaySharp.Web/Pages/Index.razor +++ b/src/SlaySharp.Web/Pages/Index.razor @@ -5,6 +5,7 @@ @using SlaySharp.Core.Catalogs @using SlaySharp.Core.Relics @using SlaySharp.Web.Components +@using System.Text.Json @inject IJSRuntime JS SlaySharp - Visual Studio Code Edition @@ -101,15 +102,85 @@ else
- -
- -
-
RUNTIME ENVIRONMENT
-
- ๐ŸŸข .NET 8.0 CLR [AggressiveTieredOpt] + + +
+
+ + +
+
+ ๐Ÿ‘จโ€๐Ÿ’ป + SELECT DEVELOPER ARCHETYPE: +
+
+ +
+
+
+ ๐Ÿ‘จโ€๐Ÿ’ป + 70 SLA โ€ข 3 CPU +
+
Principal Architect
+
[Tiered Compilation & Serilog]
+

+ Balanced enterprise runtime. Scales attack strength and structured diagnostic logging shield. +

+
+ Newtonsoft.Json + BenchmarkDotNet + Serilog +
+
+
+ + +
+
+
+ โšก + 65 SLA โ€ข 4 CPU +
+
Async Master
+
[Channels & Task.WhenAll]
+

+ High-throughput non-blocking concurrency. 4 CPU cycles to unleash massive Task.WhenAll bursts. +

+
+ Polly v8 + System.Threading.Channels +
+
+
+ + +
+
+
+ ๐Ÿ’€ + 60 SLA โ€ข 3 CPU +
+
Unsafe Memory Hacker
+
[stackalloc & Fixed Pointers]
+

+ Zero-heap direct pointer manipulation. Sacrifices HP for devastating piercing crits. +

+
+ Unsafe Compiler + Dapper ORM +
@@ -119,122 +190,108 @@ else
-
-
-
- ๐Ÿ—บ๏ธ +
+
+
+ ๐Ÿ—บ๏ธ +
+

START CAMPAIGN (@GetArchetypeName())

+ Procedural CI/CD Pipeline & Production Bosses +
+
+ + Launch Sprint (F5) โž” + +
+

+ Navigate the 7-floor procedural Git branch graph with your selected archetype. Patch production outages, refactor legacy monoliths, and decouple the K8s cluster! +

+
+ +

+ โšก + Quick Incident Sandboxes (Single Encounters) +

+ +
+ +
+
+ ๐Ÿ’ฅ [SEV-1] NullReference & Off-By-One + Quick Battle โž” +
+

+ Patch NullReferenceException and Off-By-One Bug. +

+
+ + +
+
+ ๐Ÿ”’ [ELITE] Thread Deadlock & Memory Leak + Elite Battle โž” +
+

+ Overcome thread contention and memory leaks. +

+
+ + +
+
+ ๐Ÿ‘‘ [ACT-BOSS] Kubernetes Cluster Boss (110 HP) + Boss Battle โž” +
+

+ Battle the 110 HP K8s Multi-Pod Cluster Boss with OOM Killer routines. +

+
+
+
+ + +
+

+ โš™๏ธ + Selected Archetype Specs +

+ +
+
+ @GetArchetypeIcon()
-

START CAMPAIGN: ACT 1

- CI/CD Pipeline & Legacy Monolith Boss +
@GetArchetypeName()
+
+ @GetArchetypeEnergy() CPU Cycles / @GetArchetypeHp() SLA HP +
- - 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 โž” +
ARCHETYPE MECHANICS:
+
+
+ ๐ŸŽฒ JIT Branch Prediction + 15% Crit (1.5x Dmg) +
+
+ ๐Ÿ“ฆ VS NuGet Package Manager + 4-Tab Marketplace +
+
+ ๐ŸŽต Web Audio Synthwave BGM + Procedural Music +
-

- 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; + break; case RunScreen.Map: @@ -290,7 +347,7 @@ else -
@@ -311,7 +368,7 @@ else -
@@ -321,6 +378,68 @@ else } } + +@if (_showHistoryModal) +{ + +} + @code { @@ -468,10 +624,85 @@ else private bool _isGlitchTransitioning = false; private bool _isSandboxMode = false; private bool _isDeploying = false; + private bool _isMuted = false; + private bool _showHistoryModal = false; - protected override void OnInitialized() + private DeveloperArchetype _selectedArchetype = DeveloperArchetype.PrincipalArchitect; + private List _runHistory = new(); + + protected override async Task OnInitializedAsync() { - _run.OnRunStateChanged += StateHasChanged; + _run.OnRunStateChanged += HandleRunStateChanged; + await LoadRunHistoryAsync(); + } + + private void HandleRunStateChanged() + { + UpdateBgmForScreen(); + StateHasChanged(); + } + + private void UpdateBgmForScreen() + { + switch (_run.CurrentScreen) + { + case RunScreen.Combat: + var isBoss = _run.CurrentNode?.Type == MapNodeType.Boss; + _ = JS.InvokeVoidAsync("slayAudio.startBgm", isBoss ? "boss" : "combat"); + break; + case RunScreen.Map: + case RunScreen.Shop: + case RunScreen.RestSite: + case RunScreen.Event: + _ = JS.InvokeVoidAsync("slayAudio.startBgm", "map"); + break; + default: + _ = JS.InvokeVoidAsync("slayAudio.stopBgm"); + break; + } + } + + private void SelectArchetype(DeveloperArchetype archetype) + { + _selectedArchetype = archetype; + _ = JS.InvokeVoidAsync("slayAudio.playCardSound"); + } + + private string GetArchetypeName() => _selectedArchetype switch + { + DeveloperArchetype.AsyncConcurrencyMaster => "Async Master", + DeveloperArchetype.UnsafeMemoryHacker => "Unsafe Hacker", + _ => "Principal Architect" + }; + + private string GetArchetypeIcon() => _selectedArchetype switch + { + DeveloperArchetype.AsyncConcurrencyMaster => "โšก", + DeveloperArchetype.UnsafeMemoryHacker => "๐Ÿ’€", + _ => "๐Ÿ‘จโ€๐Ÿ’ป" + }; + + private int GetArchetypeHp() => _selectedArchetype switch + { + DeveloperArchetype.AsyncConcurrencyMaster => 65, + DeveloperArchetype.UnsafeMemoryHacker => 60, + _ => 70 + }; + + private int GetArchetypeEnergy() => _selectedArchetype switch + { + DeveloperArchetype.AsyncConcurrencyMaster => 4, + _ => 3 + }; + + private void ToggleAudioMute() + { + _isMuted = !_isMuted; + _ = JS.InvokeVoidAsync("slayAudio.toggleMute"); + if (!_isMuted) + { + UpdateBgmForScreen(); + } } private async Task EnterTitleScreenAsync() @@ -497,7 +728,8 @@ else _isDeploying = false; _isSandboxMode = false; - _run.StartNewRun(); + _run.StartNewRun(_selectedArchetype); + UpdateBgmForScreen(); StateHasChanged(); } @@ -513,40 +745,109 @@ else List enemies = type switch { 1 => new List { new NullReferenceExceptionEnemy(), new OffByOneBugEnemy() }, - 2 => new List { new MemoryLeakSpectreEnemy(), new OffByOneBugEnemy() }, - 3 => new List { new LegacyMonolithBossEnemy() }, + 2 => new List { new ThreadDeadlockEnemy(), new MemoryLeakSpectreEnemy() }, + 3 => new List { new KubernetesClusterBossEnemy() }, _ => new List { new NullReferenceExceptionEnemy() } }; _run.ActiveCombat = new CombatEngine(); _run.ActiveCombat.StartCombat(_run.Player, enemies); _run.CurrentScreen = RunScreen.Combat; + UpdateBgmForScreen(); _ = JS.InvokeVoidAsync("slayAudio.playCardSound"); } - private void HandleCombatVictory() + private async void HandleCombatVictory() { if (_isSandboxMode) { _run.CurrentScreen = RunScreen.Title; + UpdateBgmForScreen(); StateHasChanged(); } else { + _ = JS.InvokeVoidAsync("slayAudio.playVictorySound"); _run.CompleteCombatAndClaimRewards(25); + if (_run.CurrentScreen == RunScreen.RunVictory) + { + await SaveRunRecordAsync(true); + } } } - private void HandleCombatDefeat() + private async void HandleCombatDefeat() { if (_isSandboxMode) { _run.CurrentScreen = RunScreen.Title; + UpdateBgmForScreen(); StateHasChanged(); } else { _run.ReturnToMap(); + if (_run.CurrentScreen == RunScreen.RunDefeat) + { + await SaveRunRecordAsync(false); + } } } + + private void ReturnToTitle() + { + _run.CurrentScreen = RunScreen.Title; + UpdateBgmForScreen(); + StateHasChanged(); + } + + private async Task SaveRunRecordAsync(bool isVictory) + { + var floor = _run.CurrentNode?.Floor ?? 1; + var record = RunRecord.Create( + _selectedArchetype, + isVictory, + _run.Player?.CurrentHp ?? 0, + _run.Player?.MaxHp ?? 70, + _run.DevCredits, + _run.Player?.MasterDeck.Count ?? 10, + floor + ); + + _runHistory.Insert(0, record); + try + { + var json = JsonSerializer.Serialize(_runHistory); + await JS.InvokeVoidAsync("localStorage.setItem", "slaysharp_run_history", json); + } + catch { } + } + + private async Task LoadRunHistoryAsync() + { + try + { + var json = await JS.InvokeAsync("localStorage.getItem", "slaysharp_run_history"); + if (!string.IsNullOrWhiteSpace(json)) + { + var records = JsonSerializer.Deserialize>(json); + if (records != null) + { + _runHistory = records; + } + } + } + catch { } + } + + private async Task ClearRunHistoryAsync() + { + _runHistory.Clear(); + try + { + await JS.InvokeVoidAsync("localStorage.removeItem", "slaysharp_run_history"); + } + catch { } + StateHasChanged(); + } } diff --git a/src/SlaySharp.Web/wwwroot/css/game.css b/src/SlaySharp.Web/wwwroot/css/game.css index 3b74d69..ef79124 100644 --- a/src/SlaySharp.Web/wwwroot/css/game.css +++ b/src/SlaySharp.Web/wwwroot/css/game.css @@ -453,97 +453,327 @@ code, pre, .font-mono { background-color: rgba(255, 255, 255, 0.2); } -/* Card Visual Window with Action Types */ +/* ========================================================== + VIBRANT & COLORFUL SLAYSHARP CARDS (VS CODE CYBER THEME) + ========================================================== */ .game-card { width: var(--card-w); height: var(--card-h); - background-color: #1e2227; - border: 2px solid #383e4a; border-radius: 12px; display: flex; flex-direction: column; position: relative; cursor: pointer; - transition: all 0.22s cubic-bezier(0.2, 0.8, 0.2, 1); - box-shadow: 0 10px 28px rgba(0, 0, 0, 0.6); + transition: all 0.24s cubic-bezier(0.2, 0.8, 0.2, 1); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.7); overflow: hidden; flex-shrink: 0; + user-select: none; +} + +/* Diagonal Holographic Sheen Overlay */ +.card-gloss-sheen { + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, transparent 45%, rgba(255, 255, 255, 0.05) 100%); + pointer-events: none; + z-index: 10; + opacity: 0.7; + transition: opacity 0.2s ease; +} + +.game-card:hover .card-gloss-sheen { + opacity: 1; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.22) 0%, transparent 55%, rgba(255, 255, 255, 0.1) 100%); } .game-card:hover { transform: translateY(-26px) scale(1.06); z-index: 50; - box-shadow: 0 20px 40px rgba(0, 122, 204, 0.5); } -/* Action Type Colored Variants */ +/* ================= ATTACK CARDS (CRIMSON / EMBER NEON) ================= */ .game-card.type-attack { - border-color: rgba(224, 108, 117, 0.5); + background: linear-gradient(175deg, #441a25 0%, #2e1119 45%, #1f0b11 100%); + border: 3px solid #ff4757; + box-shadow: 0 8px 25px rgba(255, 71, 87, 0.45); } + .game-card.type-attack:hover { - border-color: #e06c75; - box-shadow: 0 20px 40px rgba(224, 108, 117, 0.4); + border-color: #ff6b81; + box-shadow: 0 20px 48px rgba(255, 71, 87, 0.75), 0 0 30px rgba(255, 107, 129, 0.6); } +.game-card.type-attack .card-vsc-header { + background: linear-gradient(90deg, #681a29 0%, #48121d 100%); + border-bottom: 2px solid #ff4757; +} + +.game-card.type-attack .card-art-box { + background: radial-gradient(circle at center, rgba(255, 71, 87, 0.45) 0%, #2e1119 80%); + border-bottom: 1.5px solid rgba(255, 71, 87, 0.5); +} + +.game-card.type-attack .card-art-aura { + position: absolute; + width: 70px; + height: 70px; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 71, 87, 0.7) 0%, transparent 70%); + filter: blur(4px); +} + +.game-card.type-attack .card-editor-viewport { + background-color: #210d14; + border-bottom: 1.5px solid rgba(255, 71, 87, 0.35); +} + +.game-card.type-attack .card-vsc-footer { + background: linear-gradient(180deg, #301019 0%, #421523 100%); +} + +.game-card.type-attack .card-cost-badge { + background: linear-gradient(135deg, #ff4757 0%, #b33939 100%); + box-shadow: 0 0 12px rgba(255, 71, 87, 0.9); +} + +.game-card.type-attack .card-watermark-icon { + color: #ff4757; +} + +/* ================= SKILL / DEFENSE CARDS (ELECTRIC SAPPHIRE / CYAN) ================= */ .game-card.type-skill { - border-color: rgba(97, 175, 239, 0.5); + background: linear-gradient(175deg, #133a5b 0%, #0d273d 45%, #081a29 100%); + border: 3px solid #00d2ff; + box-shadow: 0 8px 25px rgba(0, 210, 255, 0.45); } + .game-card.type-skill:hover { - border-color: #61afef; - box-shadow: 0 20px 40px rgba(97, 175, 239, 0.4); + border-color: #70a1ff; + box-shadow: 0 20px 48px rgba(0, 210, 255, 0.75), 0 0 30px rgba(112, 161, 255, 0.6); } +.game-card.type-skill .card-vsc-header { + background: linear-gradient(90deg, #164f80 0%, #0e3252 100%); + border-bottom: 2px solid #00d2ff; +} + +.game-card.type-skill .card-art-box { + background: radial-gradient(circle at center, rgba(0, 210, 255, 0.45) 0%, #0d273d 80%); + border-bottom: 1.5px solid rgba(0, 210, 255, 0.5); +} + +.game-card.type-skill .card-art-aura { + position: absolute; + width: 70px; + height: 70px; + border-radius: 50%; + background: radial-gradient(circle, rgba(0, 210, 255, 0.7) 0%, transparent 70%); + filter: blur(4px); +} + +.game-card.type-skill .card-editor-viewport { + background-color: #091b2c; + border-bottom: 1.5px solid rgba(0, 210, 255, 0.35); +} + +.game-card.type-skill .card-vsc-footer { + background: linear-gradient(180deg, #0e2940 0%, #153c5e 100%); +} + +.game-card.type-skill .card-cost-badge { + background: linear-gradient(135deg, #00d2ff 0%, #0984e3 100%); + box-shadow: 0 0 12px rgba(0, 210, 255, 0.9); +} + +.game-card.type-skill .card-watermark-icon { + color: #00d2ff; +} + +/* ================= POWER CARDS (AMETHYST / CYBER MAGENTA) ================= */ .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); + background: linear-gradient(175deg, #441860 0%, #2d0e40 45%, #1b0726 100%); + border: 3px solid #e056fd; + box-shadow: 0 8px 25px rgba(224, 86, 253, 0.45); } +.game-card.type-power:hover { + border-color: #be2edd; + box-shadow: 0 20px 48px rgba(224, 86, 253, 0.75), 0 0 30px rgba(190, 46, 221, 0.6); +} + +.game-card.type-power .card-vsc-header { + background: linear-gradient(90deg, #5c1e82 0%, #3d1257 100%); + border-bottom: 2px solid #e056fd; +} + +.game-card.type-power .card-art-box { + background: radial-gradient(circle at center, rgba(224, 86, 253, 0.45) 0%, #2d0e40 80%); + border-bottom: 1.5px solid rgba(224, 86, 253, 0.5); +} + +.game-card.type-power .card-art-aura { + position: absolute; + width: 70px; + height: 70px; + border-radius: 50%; + background: radial-gradient(circle, rgba(224, 86, 253, 0.7) 0%, transparent 70%); + filter: blur(4px); +} + +.game-card.type-power .card-editor-viewport { + background-color: #1b0829; + border-bottom: 1.5px solid rgba(224, 86, 253, 0.35); +} + +.game-card.type-power .card-vsc-footer { + background: linear-gradient(180deg, #2e0d42 0%, #431460 100%); +} + +.game-card.type-power .card-cost-badge { + background: linear-gradient(135deg, #e056fd 0%, #686de0 100%); + box-shadow: 0 0 12px rgba(224, 86, 253, 0.9); +} + +.game-card.type-power .card-watermark-icon { + color: #e056fd; +} + +/* ================= CURSE / DEBT CARDS (HAZARD ORANGE) ================= */ +.game-card.type-curse { + background: linear-gradient(175deg, #442a12 0%, #2e1a09 45%, #1c0e04 100%); + border: 3px solid #ffa502; + box-shadow: 0 8px 25px rgba(255, 165, 2, 0.45); +} + +.game-card.type-curse .card-watermark-icon { + color: #ffa502; +} + +/* ================= FULL-WIDTH CARD TYPE RIBBON BANNER ================= */ +.card-type-banner { + display: flex; + align-items: center; + padding: 3px 8px; + gap: 6px; + font-size: 0.75rem; + font-weight: 900; + letter-spacing: 0.8px; + text-transform: uppercase; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.6); + z-index: 6; + border-bottom: 1px solid rgba(0, 0, 0, 0.4); +} + +.card-type-banner.type-tag-attack { + background: linear-gradient(90deg, #eb3b5a 0%, #ff4757 50%, #fa8231 100%); + color: #ffffff; + text-shadow: 0 1px 3px rgba(0,0,0,0.8); +} + +.card-type-banner.type-tag-skill { + background: linear-gradient(90deg, #0984e3 0%, #00d2ff 50%, #00cec9 100%); + color: #ffffff; + text-shadow: 0 1px 3px rgba(0,0,0,0.8); +} + +.card-type-banner.type-tag-power { + background: linear-gradient(90deg, #6c5ce7 0%, #a55eea 50%, #e056fd 100%); + color: #ffffff; + text-shadow: 0 1px 3px rgba(0,0,0,0.8); +} + +.card-banner-icon { + font-size: 0.95rem; +} + +.card-banner-text { + font-size: 0.76rem; + font-weight: 900; +} + +.card-banner-upgraded { + margin-left: auto; + background: #ffd700; + color: #000000; + padding: 1px 6px; + border-radius: 4px; + font-size: 0.62rem; + font-weight: 900; + box-shadow: 0 0 8px rgba(255, 215, 0, 0.9); +} + +/* Translucent Background Emblem Watermark */ +.card-watermark-icon { + position: absolute; + font-size: 5rem; + opacity: 0.18; + pointer-events: none; + transform: scale(1.35); + z-index: 1; + filter: drop-shadow(0 0 12px currentColor); + user-select: none; +} + +/* ================= TIER-1 UPGRADED GOLDEN HALO ================= */ +.game-card.tier-upgraded { + border: 3.5px solid #ffd700 !important; + box-shadow: 0 0 32px rgba(255, 215, 0, 0.6), inset 0 0 18px rgba(255, 215, 0, 0.3) !important; +} + +.game-card.tier-upgraded .card-upgraded-title { + color: #ffd700 !important; + text-shadow: 0 0 10px rgba(255, 215, 0, 0.8); + font-weight: 900; +} + +.card-type-mini-icon { + font-size: 0.95rem; + margin-right: 2px; +} + +.card-tier-badge { + background: linear-gradient(135deg, #ffd700 0%, #ff9f43 100%) !important; + color: #000000 !important; + font-size: 0.65rem; + font-weight: 900; + box-shadow: 0 0 12px rgba(255, 215, 0, 0.9); + letter-spacing: 0.5px; + animation: gold-badge-shimmer 2s infinite alternate ease-in-out; +} + +@@keyframes gold-badge-shimmer { + 0% { transform: scale(1); filter: brightness(1); } + 100% { transform: scale(1.08); filter: brightness(1.25); } +} + +/* ================= CARD SELECTION & PLAYABLE STATES ================= */ .game-card.playable { - box-shadow: 0 0 16px rgba(0, 122, 204, 0.4); + cursor: pointer; } .game-card.selected { - transform: translateY(-46px) scale(1.1) !important; - border-color: #98c379 !important; - box-shadow: 0 0 30px rgba(152, 195, 121, 0.8) !important; + transform: translateY(-46px) scale(1.12) !important; + border-color: #2ed573 !important; + box-shadow: 0 0 38px rgba(46, 213, 115, 0.95), 0 20px 40px rgba(0, 0, 0, 0.8) !important; z-index: 60; } .game-card.unaffordable { opacity: 0.45; - filter: grayscale(80%); + filter: grayscale(70%) brightness(0.7); 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 HEADER & LABELS ================= */ .card-vsc-header { - background-color: #282c34; - height: 32px; + height: 33px; display: flex; align-items: center; justify-content: space-between; padding: 0 10px; - border-bottom: 1.5px solid #181a1f; + position: relative; + z-index: 5; } .card-tab-label { @@ -555,61 +785,80 @@ code, pre, .font-mono { color: #ffffff; } +.card-hash-icon { + color: #61afef; + font-size: 0.95rem; + font-weight: 900; +} + .card-cost-badge { - background: #007acc; color: #ffffff; font-weight: 900; font-size: 0.85rem; - padding: 2px 8px; + padding: 2px 9px; border-radius: 6px; - box-shadow: 0 0 8px rgba(0, 122, 204, 0.6); + letter-spacing: 0.2px; } -.type-attack .card-cost-badge { background: #e06c75; box-shadow: 0 0 8px rgba(224, 108, 117, 0.6); } -.type-skill .card-cost-badge { background: #61afef; box-shadow: 0 0 8px rgba(97, 175, 239, 0.6); } -.type-power .card-cost-badge { background: #c678dd; box-shadow: 0 0 8px rgba(198, 120, 221, 0.6); } - -/* Card Art Box (Visual Illustration) */ +/* ================= CARD ART BOX ================= */ .card-art-box { - height: 75px; - background: linear-gradient(135deg, #181a1f 0%, #21252b 100%); - border-bottom: 1px solid #383e4a; + height: 78px; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; + z-index: 5; } .card-art-icon { - font-size: 2.7rem; - filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.7)); - transition: transform 0.2s ease; + font-size: 3.1rem; + filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.8)); + transition: transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1); + z-index: 2; } .game-card:hover .card-art-icon { - transform: scale(1.12); + transform: scale(1.18); } -/* Editor Gutter and Lines inside Card */ +.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; + z-index: 3; +} + +.type-tag-attack { background: rgba(255, 71, 87, 0.35); color: #ff6b81; border: 1px solid #ff4757; } +.type-tag-skill { background: rgba(0, 210, 255, 0.35); color: #70a1ff; border: 1px solid #00d2ff; } +.type-tag-power { background: rgba(224, 86, 253, 0.35); color: #e056fd; border: 1px solid #be2edd; } + +/* ================= CODE EDITOR VIEWPORT ================= */ .card-editor-viewport { - background-color: #181a1f; flex-grow: 1; display: flex; - padding: 5px 0; + padding: 6px 0; font-size: 0.74rem; font-weight: 700; line-height: 1.45; + position: relative; + z-index: 5; } .card-gutter { width: 24px; - color: #5c6370; + color: #747d8c; font-weight: 800; text-align: right; - padding-right: 5px; + padding-right: 6px; user-select: none; - border-right: 1px solid rgba(255, 255, 255, 0.08); + border-right: 1px solid rgba(255, 255, 255, 0.12); display: flex; flex-direction: column; } @@ -617,26 +866,52 @@ code, pre, .font-mono { .card-code-content { flex-grow: 1; padding: 0 8px; - color: var(--syn-var); word-break: break-word; font-weight: 700; } -.card-vsc-footer { - background-color: #21252b; - padding: 7px 10px; - border-top: 1.5px solid #383e4a; - font-size: 0.88rem; +.card-code-comment { + color: #7bed9f; + font-size: 0.68rem; + font-style: italic; + opacity: 0.85; +} + +.card-code-line { + font-size: 0.77rem; font-weight: 800; - color: #ffffff; - min-height: 52px; + color: #f1f2f6; +} + +/* ================= CARD FOOTER DESCRIPTION ================= */ +.card-vsc-footer { + padding: 8px 10px; + border-top: 1.5px solid rgba(255, 255, 255, 0.12); + min-height: 54px; display: flex; align-items: center; justify-content: center; text-align: center; - letter-spacing: -0.2px; + position: relative; + z-index: 5; } +.card-footer-text { + font-size: 0.89rem; + font-weight: 900; + color: #ffffff; + line-height: 1.25; +} + +/* Dynamic Glowing Stat Pills in Card Descriptions */ +.desc-dmg { color: #ff6b81; font-weight: 900; text-shadow: 0 0 8px rgba(255, 107, 129, 0.6); } +.desc-shield { color: #70a1ff; font-weight: 900; text-shadow: 0 0 8px rgba(112, 161, 255, 0.6); } +.desc-pierce { color: #00f0ff; font-weight: 900; text-shadow: 0 0 8px rgba(0, 240, 255, 0.6); } +.desc-cpu { color: #ffd32a; font-weight: 900; } +.desc-str { color: #2ed573; font-weight: 900; text-shadow: 0 0 8px rgba(46, 213, 115, 0.6); } +.desc-weak { color: #56b6c2; font-weight: 900; } +.desc-leak { color: #ffa502; font-weight: 900; } + /* Enemy Bug VS Code Problem Card */ .enemy-card { background-color: rgba(33, 37, 43, 0.94); diff --git a/src/SlaySharp.Web/wwwroot/js/audio.js b/src/SlaySharp.Web/wwwroot/js/audio.js index e837eb3..1bb9615 100644 --- a/src/SlaySharp.Web/wwwroot/js/audio.js +++ b/src/SlaySharp.Web/wwwroot/js/audio.js @@ -150,5 +150,106 @@ window.slayAudio = { osc.start(this.ctx.currentTime + idx * 0.06); osc.stop(this.ctx.currentTime + idx * 0.06 + 0.5); }); + }, + + // Procedural Synthwave BGM Engine + bgmInterval: null, + currentTheme: null, + bgmStep: 0, + + startBgm: function (theme) { + if (this.currentTheme === theme && this.bgmInterval) return; + this.stopBgm(); + this.currentTheme = theme; + this.bgmStep = 0; + + if (this.muted) return; + this.init(); + if (!this.ctx) return; + + const speed = theme === 'boss' ? 220 : (theme === 'combat' ? 280 : 500); + + this.bgmInterval = setInterval(() => { + if (this.muted || !this.ctx) return; + this.tickBgmPattern(theme); + }, speed); + }, + + stopBgm: function () { + if (this.bgmInterval) { + clearInterval(this.bgmInterval); + this.bgmInterval = null; + } + this.currentTheme = null; + }, + + tickBgmPattern: function (theme) { + const step = this.bgmStep++; + const now = this.ctx.currentTime; + + if (theme === 'combat') { + // E minor synthwave bassline + const notes = [82.41, 82.41, 110.00, 98.00, 82.41, 123.47, 110.00, 98.00]; + const freq = notes[step % notes.length]; + + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + const filter = this.ctx.createBiquadFilter(); + + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(freq, now); + + filter.type = 'lowpass'; + filter.frequency.setValueAtTime(450, now); + filter.frequency.exponentialRampToValueAtTime(150, now + 0.25); + + gain.gain.setValueAtTime(0.04, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25); + + osc.connect(filter); + filter.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.25); + } else if (theme === 'boss') { + // D minor dark driving synthwave + const notes = [73.42, 73.42, 87.31, 73.42, 98.00, 73.42, 110.00, 87.31]; + const freq = notes[step % notes.length]; + + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'square'; + osc.frequency.setValueAtTime(freq, now); + + gain.gain.setValueAtTime(0.035, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.2); + } else { + // Map ambient drone + const chords = [130.81, 164.81, 196.00, 220.00]; + const freq = chords[step % chords.length]; + + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, now); + + gain.gain.setValueAtTime(0.02, now); + gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.45); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.45); + } } }; diff --git a/tests/SlaySharp.Tests/CombatEngineTests.cs b/tests/SlaySharp.Tests/CombatEngineTests.cs index 75b868f..e959bb5 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; @@ -177,4 +177,110 @@ public class CombatEngineTests Assert.Empty(engine.Enemies); Assert.NotEmpty(engine.CardRewards); } + + [Fact] + public void GameRun_StartNewRun_InitializesCorrectArchetypes() + { + var run = new SlaySharp.Core.Run.GameRun(); + + // Principal Architect + run.StartNewRun(DeveloperArchetype.PrincipalArchitect); + Assert.Equal("Principal Architect", run.Player.Name); + Assert.Equal(70, run.Player.MaxHp); + Assert.Equal(3, run.Player.MaxEnergy); + Assert.Equal(3, run.Player.Relics.Count); + + // Async Concurrency Master + run.StartNewRun(DeveloperArchetype.AsyncConcurrencyMaster); + Assert.Equal("Async Master", run.Player.Name); + Assert.Equal(65, run.Player.MaxHp); + Assert.Equal(4, run.Player.MaxEnergy); + Assert.Equal(2, run.Player.Relics.Count); + + // Unsafe Memory Hacker + run.StartNewRun(DeveloperArchetype.UnsafeMemoryHacker); + Assert.Equal("Unsafe Hacker", run.Player.Name); + Assert.Equal(60, run.Player.MaxHp); + Assert.Equal(3, run.Player.MaxEnergy); + Assert.Equal(2, run.Player.Relics.Count); + } + + [Fact] + public void TaskWhenAllCard_DealsDamageToTargetEnemy() + { + var player = new Player("Async Master", 65, maxEnergy: 4); + var card = new TaskWhenAllCard(); // 14 dmg single target + var enemy = new NullReferenceExceptionEnemy(); // 45 HP + + var engine = new CombatEngine(); + engine.StartCombat(player, new List { enemy }); + + player.Hand.Clear(); + player.Hand.Add(card); + player.CurrentEnergy = 2; + + var success = engine.PlayCard(card, enemy); + + Assert.True(success); + Assert.Equal(0, player.CurrentEnergy); + Assert.True(enemy.CurrentHp <= 31); + } + + [Fact] + public void FixedPointerCard_DealsPiercingDamageTwoTimes() + { + var player = new Player("Unsafe Hacker", 60, maxEnergy: 3); + var card = new FixedPointerCard(); // 2 hits of 9 piercing dmg + var enemy = new MemoryLeakSpectreEnemy(); // 35 HP + + var engine = new CombatEngine(); + engine.StartCombat(player, new List { enemy }); + + enemy.GainShield(12); + player.Hand.Clear(); + player.Hand.Add(card); + player.CurrentEnergy = 2; + + var success = engine.PlayCard(card, enemy); + + Assert.True(success); + Assert.Equal(12, enemy.Shield); // Pierced + Assert.True(enemy.CurrentHp <= 17); + } + + [Fact] + public void KubernetesBoss_ExecutesRoutines() + { + var boss = new KubernetesClusterBossEnemy(); + Assert.Equal(110, boss.MaxHp); + Assert.Equal(110, boss.CurrentHp); + Assert.Equal("โ˜ธ๏ธ", boss.Icon); + + var player = new Player("Senior Dev", 70, maxEnergy: 3); + var engine = new CombatEngine(); + engine.StartCombat(player, new List { boss }); + + // Verify intent is planned + Assert.NotNull(boss.CurrentIntent); + } + + [Fact] + public void RunRecord_Create_GeneratesValidGitCommitRecord() + { + var record = RunRecord.Create( + DeveloperArchetype.AsyncConcurrencyMaster, + true, + 55, + 65, + 120, + 14, + 7 + ); + + Assert.Equal(7, record.CommitHash.Length); + Assert.True(record.IsVictory); + Assert.Equal(55, record.FinalHp); + Assert.Equal(120, record.DevCredits); + Assert.Contains("feat(prod)", record.Message); + } } \ No newline at end of file