improvements

This commit is contained in:
Michal Vrba
2026-09-16 23:58:25 +02:00
parent d3c419f8ce
commit e7512f7c35
13 changed files with 1906 additions and 393 deletions
+196 -1
View File
@@ -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<byte> stackalloc",
"Span<byte> 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<Func<Card>> 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()
+218
View File
@@ -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);
}
}
+35 -1
View File
@@ -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<Card> CreateAsyncMasterDeck()
{
return new List<Card>
{
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<Card> CreateUnsafeHackerDeck()
{
return new List<Card>
{
new StackallocCard(),
new StackallocCard(),
new FixedPointerCard(),
new SpanSliceCard(),
new SpanSliceCard(),
new ThrowExceptionCard(),
new TryCatchCard(),
new NullCoalescingCard(),
new ConsoleWriteLineCard(),
new TieredCompilationCard()
};
}
}
@@ -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";
}
+37
View File
@@ -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
};
}
}
+35 -1
View File
@@ -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.");
}
}
+36 -8
View File
@@ -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;
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<Enemy> enemies;
if (isBoss)
{
enemies = new List<Enemy> { new LegacyMonolithBossEnemy() };
enemies = Random.Shared.Next(2) == 0
? new List<Enemy> { new LegacyMonolithBossEnemy() }
: new List<Enemy> { new KubernetesClusterBossEnemy() };
}
else if (isElite)
{
enemies = new List<Enemy> { new MemoryLeakSpectreEnemy(hpBonus: 6), new OffByOneBugEnemy() };
var roll = Random.Shared.Next(2);
enemies = roll == 0
? new List<Enemy> { new ThreadDeadlockEnemy(), new MemoryLeakSpectreEnemy() }
: new List<Enemy> { 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<Enemy> { new NullReferenceExceptionEnemy(), new OffByOneBugEnemy() },
_ => new List<Enemy> { new MemoryLeakSpectreEnemy(), new OffByOneBugEnemy() }
1 => new List<Enemy> { new HeisenbugEnemy(), new OffByOneBugEnemy() },
_ => new List<Enemy> { new MemoryLeakSpectreEnemy(), new NullReferenceExceptionEnemy() }
};
}
+86 -45
View File
@@ -6,40 +6,43 @@
@onclick="HandleCardClick"
@onmouseenter="HandleMouseEnter"
@onmouseleave="HandleMouseLeave">
<!-- VS Code Tab Header -->
<!-- Holographic / Gloss Highlight Sheen -->
<div class="card-gloss-sheen"></div>
<!-- Prominent Top Header with File Name & Cost -->
<div class="card-vsc-header">
<div class="card-tab-label font-mono overflow-hidden">
<span style="color: #61afef; font-size: 0.95rem; font-weight: 900;">#</span>
<span class="text-truncate" title="@Card.Name" style="font-weight: 800; font-size: 0.82rem;">
<span class="card-type-mini-icon">@GetActionTypeIcon()</span>
<span class="text-truncate card-filename @(Card.Upgraded ? "card-upgraded-title" : "")" title="@Card.Name">
@Card.Name.Replace(" ", "").Replace("()", "")@(Card.Upgraded ? "_v2.cs" : ".cs")
</span>
</div>
<div class="d-flex align-items-center gap-1">
<div class="card-cost-badge font-mono" title="@($"{Card.Cost} CPU Cycles")">
@Card.Cost CPU
</div>
</div>
</div>
<!-- Card Visual Art Window with Action Type Badge -->
<div class="card-art-box">
<!-- Action Type Badge Overlay -->
<div class="position-absolute top-0 start-0 m-1">
<span class="card-type-tag @GetActionTagClass() font-mono">
@GetActionTypeIcon() @Card.Type
</span>
</div>
<span class="card-art-icon">@GetCardArtIcon(Card.Name)</span>
<!-- Extra Bold Card Type Banner Ribbon -->
<div class="card-type-banner @GetActionTagClass() font-mono">
<span class="card-banner-icon">@GetActionTypeIcon()</span>
<span class="card-banner-text">@GetCardTypeLabel()</span>
@if (Card.Upgraded)
{
<div class="position-absolute top-0 end-0 m-1 badge bg-warning text-dark font-mono" style="font-size: 0.62rem; font-weight: 900;">
TIER-1 +
</div>
<span class="card-banner-upgraded">TIER-1 +</span>
}
</div>
<!-- Card Visual Art Window with Themed Watermark Background -->
<div class="card-art-box">
<!-- Giant Translucent Background Watermark Emblem -->
<div class="card-watermark-icon">@GetActionTypeIcon()</div>
<!-- Ambient Glow Aura behind icon -->
<div class="card-art-aura"></div>
<span class="card-art-icon">@GetCardArtIcon(Card.Name)</span>
</div>
<!-- Mini Code Editor with Gutter and Line Numbers -->
<div class="card-editor-viewport font-mono">
<!-- Line Numbers Gutter -->
@@ -51,10 +54,10 @@
<!-- C# Code Content with Syntax Coloring -->
<div class="card-code-content">
<div style="color: var(--syn-comment); font-size: 0.68rem;">
// @(Card.Upgraded ? "Optimized IL" : "Managed code")
<div class="card-code-comment">
// @(Card.Upgraded ? "Optimized Tier-1 IL" : "Managed CLR Task")
</div>
<div style="font-size: 0.76rem; font-weight: 800;">
<div class="card-code-line">
@FormatCodeSnippet(Card.CodeSnippet)
</div>
</div>
@@ -62,9 +65,9 @@
<!-- Punchy, Large, Bold Card Effect Footer with Action Visuals -->
<div class="card-vsc-footer font-mono">
<span style="font-size: 0.92rem; font-weight: 900; line-height: 1.25;">
<div class="card-footer-text">
@FormatDescription(Card.Description)
</span>
</div>
</div>
</div>
@@ -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", "<span style='color: #ff6b81;'>⚔️ damage</span>")
.Replace("Damage", "<span style='color: #ff6b81;'>⚔️ Damage</span>")
.Replace("Shield", "<span style='color: #70a1ff;'>🛡️ Shield</span>")
.Replace("piercing", "<span style='color: #00f0ff;'>⚡ piercing</span>")
.Replace("CPU", "<span style='color: #e5c07b;'>🔋 CPU</span>")
.Replace("Strength", "<span style='color: #2ed573;'>💪 Strength</span>")
.Replace("Weak", "<span style='color: #56b6c2;'>📉 Weak</span>")
.Replace("Memory Leak", "<span style='color: #ffa502;'>⚠️ Memory Leak</span>");
.Replace("damage", "<span class='desc-dmg'>⚔️ damage</span>")
.Replace("Damage", "<span class='desc-dmg'>⚔️ Damage</span>")
.Replace("Shield", "<span class='desc-shield'>🛡️ Shield</span>")
.Replace("shield", "<span class='desc-shield'>🛡️ shield</span>")
.Replace("piercing", "<span class='desc-pierce'>⚡ piercing</span>")
.Replace("CPU", "<span class='desc-cpu'>🔋 CPU</span>")
.Replace("Strength", "<span class='desc-str'>💪 Strength</span>")
.Replace("Weak", "<span class='desc-weak'>📉 Weak</span>")
.Replace("Memory Leak", "<span class='desc-leak'>⚠️ Memory Leak</span>");
return new MarkupString(formatted);
}
@@ -164,17 +200,22 @@
if (string.IsNullOrEmpty(snippet)) return new MarkupString("");
var highlighted = snippet
.Replace("public", "<span style='color: var(--syn-keyword);'>public</span>")
.Replace("void", "<span style='color: var(--syn-keyword);'>void</span>")
.Replace("var", "<span style='color: var(--syn-keyword);'>var</span>")
.Replace("throw", "<span style='color: var(--syn-control);'>throw</span>")
.Replace("new", "<span style='color: var(--syn-keyword);'>new</span>")
.Replace("try", "<span style='color: var(--syn-control);'>try</span>")
.Replace("catch", "<span style='color: var(--syn-control);'>catch</span>")
.Replace("await", "<span style='color: var(--syn-control);'>await</span>")
.Replace("return", "<span style='color: var(--syn-control);'>return</span>")
.Replace("Bug", "<span style='color: var(--syn-type);'>Bug</span>")
.Replace("Exception", "<span style='color: var(--syn-type);'>Exception</span>");
.Replace("public", "<span style='color: #ff79c6; font-weight: 900;'>public</span>")
.Replace("void", "<span style='color: #8be9fd; font-weight: 900;'>void</span>")
.Replace("var", "<span style='color: #8be9fd; font-weight: 900;'>var</span>")
.Replace("throw", "<span style='color: #ff5555; font-weight: 900;'>throw</span>")
.Replace("new", "<span style='color: #ff79c6; font-weight: 900;'>new</span>")
.Replace("try", "<span style='color: #ff79c6; font-weight: 900;'>try</span>")
.Replace("catch", "<span style='color: #ff79c6; font-weight: 900;'>catch</span>")
.Replace("await", "<span style='color: #bd93f9; font-weight: 900;'>await</span>")
.Replace("return", "<span style='color: #ff79c6; font-weight: 900;'>return</span>")
.Replace("fixed", "<span style='color: #ff79c6; font-weight: 900;'>fixed</span>")
.Replace("stackalloc", "<span style='color: #ffb86c; font-weight: 900;'>stackalloc</span>")
.Replace("Span", "<span style='color: #50fa7b; font-weight: 900;'>Span</span>")
.Replace("Task", "<span style='color: #50fa7b; font-weight: 900;'>Task</span>")
.Replace("Channel", "<span style='color: #50fa7b; font-weight: 900;'>Channel</span>")
.Replace("Bug", "<span style='color: #ff5555; font-weight: 900;'>Bug</span>")
.Replace("Exception", "<span style='color: #ff5555; font-weight: 900;'>Exception</span>");
return new MarkupString(highlighted);
}
+183 -61
View File
@@ -1,64 +1,73 @@
@using SlaySharp.Core.Run
@using SlaySharp.Core.Run
@using SlaySharp.Core.Models
@using SlaySharp.Core.Catalogs
@using SlaySharp.Core.Relics
@inject IJSRuntime JS
<div class="d-flex flex-column h-100 font-mono" style="background-color: var(--vscode-bg-editor);">
<!-- Breadcrumbs -->
<div class="d-flex align-items-center justify-content-between vsc-breadcrumbs font-mono">
<div class="d-flex align-items-center gap-2">
<span>SlaySharp</span>
<span>›</span>
<span>nuget.org</span>
<span>›</span>
<span style="color: #61afef; font-weight: 800;">Package Manager</span>
<!-- VS Code Tab Header -->
<div class="vsc-tabs-bar font-mono">
<div class="vsc-tab active">
<span style="color: #61afef; font-weight: 900; font-size: 1rem;">📦</span>
<span>NuGet: Solution Packages</span>
<span style="color: #828997; font-size: 0.75rem;">✕</span>
</div>
<div class="d-flex align-items-center gap-3">
<span class="text-success fw-bold">🛡️ SLA: @Run.Player.CurrentHp/@Run.Player.MaxHp</span>
<div class="ms-auto d-flex align-items-center gap-3 pe-3">
<span class="text-success fw-bolder" style="font-size: 0.85rem;">🛡️ SLA: @Run.Player.CurrentHp/@Run.Player.MaxHp HP</span>
<span class="badge bg-warning text-dark fs-6 px-3 py-1 fw-bolder">💰 @Run.DevCredits Dev Credits</span>
<button class="btn btn-sm btn-outline-success font-mono fw-bold" @onclick="HandleReturnToMap">
Return to Graph ➔
<button class="btn btn-sm btn-success font-mono fw-bold px-3" @onclick="HandleReturnToMap">
▶ Return to Sprint Map
</button>
</div>
</div>
<!-- Shop Main Area -->
<div class="flex-grow-1 p-4 overflow-y-auto">
<div class="d-flex justify-content-between align-items-center mb-4 pb-2 border-bottom border-secondary">
<div>
<h2 class="text-white fw-bolder mb-0 d-flex align-items-center gap-2">
<span style="color: #007acc;">📦</span>
<span>NuGet Package Feed & Marketplace</span>
</h2>
<span class="text-muted" style="font-size: 0.85rem;">Source: https://api.nuget.org/v3/index.json</span>
</div>
<!-- Purge Card Service -->
<button class="btn btn-outline-danger font-mono fw-bold px-3 py-2"
disabled="@(Run.DevCredits < 75 || _hasPurgedCard)"
@onclick="() => _showPurgeModal = true">
🗑️ Clean Tech Debt (-1 Card) - 75 Credits
<!-- NuGet Sub-Navigation & Package Feed Tabs -->
<div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom border-secondary" style="background-color: #21252b;">
<div class="d-flex align-items-center gap-2">
<button class="nuget-tab-btn @(_currentTab == NuGetTab.Browse ? "active" : "")" @onclick="() => _currentTab = NuGetTab.Browse">
🔍 Browse
</button>
<button class="nuget-tab-btn @(_currentTab == NuGetTab.Installed ? "active" : "")" @onclick="() => _currentTab = NuGetTab.Installed">
📥 Installed (@(Run.Player.Relics.Count + Run.Player.MasterDeck.Count))
</button>
<button class="nuget-tab-btn @(_currentTab == NuGetTab.Updates ? "active" : "")" @onclick="() => _currentTab = NuGetTab.Updates">
⬆ Updates (@GetUpgradeableCards().Count)
</button>
<button class="nuget-tab-btn @(_currentTab == NuGetTab.Uninstall ? "active" : "")" @onclick="() => _currentTab = NuGetTab.Uninstall">
🗑️ Clean Debt
</button>
</div>
<div class="d-flex align-items-center gap-2">
<span class="text-muted" style="font-size: 0.75rem;">Package source:</span>
<span class="badge bg-dark border border-secondary text-info font-mono" style="font-size: 0.75rem;">
nuget.org (v3 API)
</span>
</div>
</div>
<!-- Feedback Message Banner -->
@if (!string.IsNullOrEmpty(_message))
{
<div class="alert alert-info py-2 px-3 mb-4 font-mono fw-bold d-flex justify-content-between align-items-center">
<div class="alert alert-info py-2 px-3 m-2 font-mono fw-bold d-flex justify-content-between align-items-center border-0 shadow-sm" style="background-color: #1f2a3d; color: #61afef;">
<span>@_message</span>
<button class="btn-close btn-close-white" @onclick="() => _message = null"></button>
</div>
}
<div class="row g-4 mb-4">
<!-- Section 1: Relics (NuGet Packages) -->
<div class="col-12">
<!-- Tab Content Area -->
<div class="flex-grow-1 p-4 overflow-y-auto" style="min-height: 0;">
@switch (_currentTab)
{
case NuGetTab.Browse:
<!-- Browse Packages & Syntax Modules -->
<div class="d-flex flex-column gap-4">
<!-- NuGet Packages (Relics) -->
<div>
<h4 class="text-info fw-bold mb-3 d-flex align-items-center gap-2">
<span>⚡</span>
<span>NuGets & Tooling:</span>
<span>📦</span>
<span>Featured NuGet Packages (Tooling &amp; Middleware):</span>
</h4>
<div class="d-flex gap-3 flex-wrap">
@foreach (var relic in _shopRelics)
{
@@ -88,13 +97,12 @@
</div>
</div>
<!-- Section 2: Cards (Roslyn Snippets) -->
<div class="col-12 mt-4">
<!-- Roslyn Syntax Cards -->
<div class="mt-2">
<h4 class="text-success fw-bold mb-3 d-flex align-items-center gap-2">
<span>🌿</span>
<span>Roslyn Syntactic Snippets (Cards):</span>
<span>Roslyn Syntactic Snippets (Deck Additions):</span>
</h4>
<div class="d-flex gap-4 flex-wrap justify-content-start">
@for (int i = 0; i < _shopCards.Count; i++)
{
@@ -114,43 +122,141 @@
</div>
</div>
</div>
break;
case NuGetTab.Installed:
<!-- Installed Packages & Cards -->
<div class="d-flex flex-column gap-4">
<div>
<h4 class="text-info fw-bold mb-3">📦 Installed Tooling &amp; NuGet Packages (@Run.Player.Relics.Count)</h4>
<div class="d-flex flex-wrap gap-3">
@foreach (var relic in Run.Player.Relics)
{
<div class="card p-3" style="min-width: 260px; max-width: 340px; background-color: #21252b; border: 1px solid #383e4a; border-radius: 8px;">
<div class="d-flex align-items-center gap-2 mb-2">
<span style="font-size: 1.8rem;">@relic.Icon</span>
<div>
<div class="text-white fw-bold">@relic.Name</div>
<div class="text-muted" style="font-size: 0.72rem;">@relic.PackageVersion</div>
</div>
</div>
<p class="text-light mb-0" style="font-size: 0.82rem;">@relic.Description</p>
</div>
}
</div>
</div>
<!-- Card Purge Modal -->
@if (_showPurgeModal)
<div>
<h4 class="text-success fw-bold mb-3">🌿 Active Solution Deck (@Run.Player.MasterDeck.Count Cards)</h4>
<div class="d-flex flex-wrap gap-3">
@foreach (var card in Run.Player.MasterDeck)
{
<div class="modal d-block" tabindex="-1" style="background: rgba(0,0,0,0.85); backdrop-filter: blur(6px);">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content text-white" style="background-color: #181a1f; border: 2px solid #383e4a; border-radius: 12px;">
<div class="modal-header border-secondary py-3" style="background-color: #21252b;">
<h5 class="modal-title font-mono fw-bold text-danger">
🗑️ Select Technical Debt Card to Purge (-75 Credits)
</h5>
<button type="button" class="btn-close btn-close-white" @onclick="() => _showPurgeModal = false"></button>
<CardView Card="@card" IsPlayable="false" />
}
</div>
<div class="modal-body p-4">
<div class="d-flex flex-wrap justify-content-center gap-3">
</div>
</div>
break;
case NuGetTab.Updates:
<!-- Upgrade Cards to Tier-1 '+' -->
<div>
<h4 class="text-warning fw-bold mb-2">⬆ NuGet Package Updates: JIT Optimize to Tier-1 (+) (45 Credits each)</h4>
<p class="text-muted mb-4" style="font-size: 0.85rem;">Upgrade unmanaged IL routines with Tiered Compilation AggressiveOptimization.</p>
@{
var upgradeables = GetUpgradeableCards();
}
@if (upgradeables.Any())
{
<div class="d-flex flex-wrap gap-4">
@foreach (var card in upgradeables)
{
<div class="d-flex flex-column align-items-center gap-2">
<CardView Card="@card" IsPlayable="false" />
<button class="btn btn-warning font-mono fw-bold px-3 py-1 shadow"
disabled="@(Run.DevCredits < 45)"
@onclick="() => UpgradeCard(card)">
⬆ Optimize Card (45 C)
</button>
</div>
}
</div>
}
else
{
<div class="card p-4 text-center text-muted" style="background-color: #21252b; border: 1px dashed #383e4a;">
✓ All packages in solution are running on the latest Tier-1 runtime version.
</div>
}
</div>
break;
case NuGetTab.Uninstall:
<!-- Purge / Refactor Debt -->
<div>
<h4 class="text-danger fw-bold mb-2">🗑️ Technical Debt Refactoring (Purge 1 Card for 75 Credits)</h4>
<p class="text-muted mb-4" style="font-size: 0.85rem;">Uninstall legacy code blocks from your active solution deck.</p>
@if (_hasPurgedCard)
{
<div class="alert alert-secondary py-3 text-center">
✓ Technical debt purge quota used for this sprint site.
</div>
}
else
{
<div class="d-flex flex-wrap gap-3">
@foreach (var card in Run.Player.MasterDeck)
{
<div class="d-flex flex-column align-items-center gap-2">
<CardView Card="@card" IsPlayable="false" />
<button class="btn btn-danger btn-sm font-mono fw-bold px-3 py-1 shadow"
disabled="@(Run.DevCredits < 75)"
@onclick="() => PurgeCard(card)">
🗑️ Purge @card.Name
</button>
</div>
}
</div>
</div>
</div>
</div>
</div>
}
</div>
break;
}
</div>
</div>
<style>
.nuget-tab-btn {
background: transparent;
border: none;
color: #abb2bf;
font-size: 0.88rem;
font-weight: 800;
padding: 6px 16px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.nuget-tab-btn:hover {
background: #282c34;
color: #ffffff;
}
.nuget-tab-btn.active {
background: #007acc;
color: #ffffff;
box-shadow: 0 0 12px rgba(0, 122, 204, 0.6);
}
</style>
@code {
[Parameter] public GameRun Run { get; set; } = null!;
private enum NuGetTab { Browse, Installed, Updates, Uninstall }
private NuGetTab _currentTab = NuGetTab.Browse;
private List<Card> _shopCards = new();
private List<int> _cardCosts = new();
private List<Relic> _shopRelics = new();
@@ -158,7 +264,6 @@
private readonly HashSet<string> _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<Relic>
{
new PollyRelic(),
new DapperRelic()
new DapperRelic(),
new UnsafeCompilerRelic()
};
}
private List<Card> 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();
+356 -55
View File
@@ -5,6 +5,7 @@
@using SlaySharp.Core.Catalogs
@using SlaySharp.Core.Relics
@using SlaySharp.Web.Components
@using System.Text.Json
@inject IJSRuntime JS
<PageTitle>SlaySharp - Visual Studio Code Edition</PageTitle>
@@ -101,15 +102,85 @@ else
</div>
</div>
<!-- Animated Terminal Boot Badge & Replay Intro CTA -->
<div class="d-flex align-items-center gap-3">
<button class="btn btn-outline-secondary btn-sm font-mono fw-bold px-3 py-2" @onclick="() => _showIntroBoot = true">
📺 Replay Boot Intro
<!-- Animated Terminal Boot Badge, Audio Toggle & History CTA -->
<div class="d-flex align-items-center gap-2">
<button class="btn btn-outline-warning btn-sm font-mono fw-bold px-3 py-2" @onclick="() => _showHistoryModal = true">
📜 Git Log (@_runHistory.Count)
</button>
<div class="card p-2 px-3 text-start shadow" style="background-color: rgba(24, 26, 31, 0.92); border: 1px solid #383e4a; border-radius: 8px;">
<div class="text-muted" style="font-size: 0.72rem; font-weight: 800;">RUNTIME ENVIRONMENT</div>
<div class="text-success font-mono" style="font-size: 0.85rem; font-weight: 800;">
🟢 .NET 8.0 CLR [AggressiveTieredOpt]
<button class="btn btn-outline-info btn-sm font-mono fw-bold px-3 py-2" @onclick="ToggleAudioMute">
@(_isMuted ? "🔇 Audio OFF" : "🔊 Audio ON")
</button>
<button class="btn btn-outline-secondary btn-sm font-mono fw-bold px-3 py-2" @onclick="() => _showIntroBoot = true">
📺 Replay Boot
</button>
</div>
</div>
<!-- Character Archetype Selection Bar -->
<div class="mb-4">
<h5 class="text-white fw-bold mb-3 d-flex align-items-center gap-2">
<span style="color: #61afef;">👨‍💻</span>
<span>SELECT DEVELOPER ARCHETYPE:</span>
</h5>
<div class="row g-3">
<!-- Class 1: Principal Architect -->
<div class="col-md-4">
<div class="card p-3 h-100 class-select-card @(_selectedArchetype == DeveloperArchetype.PrincipalArchitect ? "class-selected" : "")"
@onclick="() => SelectArchetype(DeveloperArchetype.PrincipalArchitect)">
<div class="d-flex justify-content-between align-items-center mb-2">
<span style="font-size: 2rem;">👨‍💻</span>
<span class="badge bg-primary text-white font-mono">70 SLA • 3 CPU</span>
</div>
<div class="fw-bolder text-white fs-5 mb-1">Principal Architect</div>
<div class="text-info font-mono mb-2" style="font-size: 0.76rem; font-weight: 800;">[Tiered Compilation &amp; Serilog]</div>
<p class="text-light mb-2" style="font-size: 0.82rem;">
Balanced enterprise runtime. Scales attack strength and structured diagnostic logging shield.
</p>
<div class="d-flex flex-wrap gap-1 mt-auto">
<span class="badge bg-dark border border-secondary text-muted" style="font-size: 0.72rem;">Newtonsoft.Json</span>
<span class="badge bg-dark border border-secondary text-muted" style="font-size: 0.72rem;">BenchmarkDotNet</span>
<span class="badge bg-dark border border-secondary text-muted" style="font-size: 0.72rem;">Serilog</span>
</div>
</div>
</div>
<!-- Class 2: Async Concurrency Master -->
<div class="col-md-4">
<div class="card p-3 h-100 class-select-card @(_selectedArchetype == DeveloperArchetype.AsyncConcurrencyMaster ? "class-selected" : "")"
@onclick="() => SelectArchetype(DeveloperArchetype.AsyncConcurrencyMaster)">
<div class="d-flex justify-content-between align-items-center mb-2">
<span style="font-size: 2rem;">⚡</span>
<span class="badge bg-success text-white font-mono">65 SLA • 4 CPU</span>
</div>
<div class="fw-bolder text-white fs-5 mb-1">Async Master</div>
<div class="text-success font-mono mb-2" style="font-size: 0.76rem; font-weight: 800;">[Channels &amp; Task.WhenAll]</div>
<p class="text-light mb-2" style="font-size: 0.82rem;">
High-throughput non-blocking concurrency. 4 CPU cycles to unleash massive Task.WhenAll bursts.
</p>
<div class="d-flex flex-wrap gap-1 mt-auto">
<span class="badge bg-dark border border-secondary text-muted" style="font-size: 0.72rem;">Polly v8</span>
<span class="badge bg-dark border border-secondary text-muted" style="font-size: 0.72rem;">System.Threading.Channels</span>
</div>
</div>
</div>
<!-- Class 3: Low-Level Unsafe Hacker -->
<div class="col-md-4">
<div class="card p-3 h-100 class-select-card @(_selectedArchetype == DeveloperArchetype.UnsafeMemoryHacker ? "class-selected" : "")"
@onclick="() => SelectArchetype(DeveloperArchetype.UnsafeMemoryHacker)">
<div class="d-flex justify-content-between align-items-center mb-2">
<span style="font-size: 2rem;">💀</span>
<span class="badge bg-danger text-white font-mono">60 SLA • 3 CPU</span>
</div>
<div class="fw-bolder text-white fs-5 mb-1">Unsafe Memory Hacker</div>
<div class="text-danger font-mono mb-2" style="font-size: 0.76rem; font-weight: 800;">[stackalloc &amp; Fixed Pointers]</div>
<p class="text-light mb-2" style="font-size: 0.82rem;">
Zero-heap direct pointer manipulation. Sacrifices HP for devastating piercing crits.
</p>
<div class="d-flex flex-wrap gap-1 mt-auto">
<span class="badge bg-dark border border-secondary text-muted" style="font-size: 0.72rem;">Unsafe Compiler</span>
<span class="badge bg-dark border border-secondary text-muted" style="font-size: 0.72rem;">Dapper ORM</span>
</div>
</div>
</div>
</div>
@@ -126,16 +197,16 @@ else
<div class="d-flex align-items-center gap-3">
<span style="font-size: 2.5rem;" class="pulse-icon">🗺️</span>
<div>
<h3 class="text-success fw-bolder mb-0">START CAMPAIGN: ACT 1</h3>
<span class="text-muted" style="font-size: 0.82rem; font-weight: 800;">CI/CD Pipeline & Legacy Monolith Boss</span>
<h3 class="text-success fw-bolder mb-0">START CAMPAIGN (@GetArchetypeName())</h3>
<span class="text-muted" style="font-size: 0.82rem; font-weight: 800;">Procedural CI/CD Pipeline &amp; Production Bosses</span>
</div>
</div>
<span class="badge bg-success text-white fs-6 px-4 py-3 fw-bolder shadow launch-btn">
Launch Run (F5) ➔
Launch Sprint (F5) ➔
</span>
</div>
<p class="text-white mb-0" style="font-size: 0.95rem; font-weight: 700; line-height: 1.5;">
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!
Navigate the 7-floor procedural Git branch graph with your selected archetype. Patch production outages, refactor legacy monoliths, and decouple the K8s cluster!
</p>
</div>
@@ -149,11 +220,11 @@ else
<div class="card p-3 shadow-sm sandbox-card" style="background-color: rgba(33, 37, 43, 0.92); border: 2px solid #383e4a; border-radius: 10px; cursor: pointer; backdrop-filter: blur(6px);"
@onclick="() => StartSandboxEncounter(1)">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="text-danger fw-bolder fs-6">💥 [SEV-1] NullReference & Off-By-One</span>
<span class="text-danger fw-bolder fs-6">💥 [SEV-1] NullReference &amp; Off-By-One</span>
<span class="badge bg-danger text-white fs-6 px-3 py-1">Quick Battle ➔</span>
</div>
<p class="text-white mb-0" style="font-size: 0.85rem; font-weight: 700;">
Patch NullReferenceException (45 HP) and Off-By-One Bug (22 HP).
Patch NullReferenceException and Off-By-One Bug.
</p>
</div>
@@ -161,11 +232,11 @@ else
<div class="card p-3 shadow-sm sandbox-card" style="background-color: rgba(33, 37, 43, 0.92); border: 2px solid #383e4a; border-radius: 10px; cursor: pointer; backdrop-filter: blur(6px);"
@onclick="() => StartSandboxEncounter(2)">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="text-warning fw-bolder fs-6">👻 [SEV-2] Memory Leak Spectre</span>
<span class="badge bg-warning text-dark fs-6 px-3 py-1">Quick Battle ➔</span>
<span class="text-warning fw-bolder fs-6">🔒 [ELITE] Thread Deadlock &amp; Memory Leak</span>
<span class="badge bg-warning text-dark fs-6 px-3 py-1">Elite Battle ➔</span>
</div>
<p class="text-white mb-0" style="font-size: 0.85rem; font-weight: 700;">
Dispose unmanaged memory from Memory Leak Spectre (34 HP) with Shield gain.
Overcome thread contention and memory leaks.
</p>
</div>
@@ -173,11 +244,11 @@ else
<div class="card p-3 shadow-sm sandbox-card" style="background-color: rgba(33, 37, 43, 0.92); border: 2px solid #383e4a; border-radius: 10px; cursor: pointer; backdrop-filter: blur(6px);"
@onclick="() => StartSandboxEncounter(3)">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="text-info fw-bolder fs-6">👑 [ACT-BOSS] Legacy Monolith (90 HP)</span>
<span class="text-info fw-bolder fs-6">👑 [ACT-BOSS] Kubernetes Cluster Boss (110 HP)</span>
<span class="badge bg-primary text-white fs-6 px-3 py-1">Boss Battle ➔</span>
</div>
<p class="text-white mb-0" style="font-size: 0.85rem; font-weight: 700;">
Battle the 90 HP Legacy Monolith Boss with spaghetti code and thread locks.
Battle the 110 HP K8s Multi-Pod Cluster Boss with OOM Killer routines.
</p>
</div>
</div>
@@ -187,50 +258,36 @@ else
<div class="col-lg-5">
<h4 class="text-white mb-3 d-flex align-items-center gap-2 fw-bold">
<span style="color: #56b6c2; font-size: 1.5rem;">⚙️</span>
<span>Active Profile</span>
<span>Selected Archetype Specs</span>
</h4>
<div class="card p-4 mb-3" style="background-color: rgba(33, 37, 43, 0.92); border: 2px solid #383e4a; border-radius: 12px; backdrop-filter: blur(6px);">
<div class="d-flex align-items-center gap-2 mb-3">
<span style="font-size: 2.2rem;">👨‍💻</span>
<span style="font-size: 2.2rem;">@GetArchetypeIcon()</span>
<div>
<div class="text-info fw-bolder fs-5">Principal .NET Architect</div>
<div class="text-white" style="font-size: 0.85rem; font-weight: 800;">3 CPU Cycles / 70 SLA HP</div>
<div class="text-info fw-bolder fs-5">@GetArchetypeName()</div>
<div class="text-white" style="font-size: 0.85rem; font-weight: 800;">
@GetArchetypeEnergy() CPU Cycles / @GetArchetypeHp() SLA HP
</div>
</div>
</div>
<div class="text-muted fw-bold mb-1" style="font-size: 0.78rem;">STARTER NUGET PACKAGES:</div>
<div class="text-muted fw-bold mb-1" style="font-size: 0.78rem;">ARCHETYPE MECHANICS:</div>
<div class="d-flex flex-column gap-2 mb-2">
<div class="badge bg-dark border border-secondary text-white p-2 d-flex align-items-center justify-content-between" style="font-size: 0.85rem; font-weight: 800;">
<span>📜 Newtonsoft.Json</span>
<span class="text-info">+1 Card T1</span>
<span>🎲 JIT Branch Prediction</span>
<span class="text-warning">15% Crit (1.5x Dmg)</span>
</div>
<div class="badge bg-dark border border-secondary text-white p-2 d-flex align-items-center justify-content-between" style="font-size: 0.85rem; font-weight: 800;">
<span>⚡ BenchmarkDotNet</span>
<span class="text-warning">+1 CPU / 2T</span>
<span>📦 VS NuGet Package Manager</span>
<span class="text-info">4-Tab Marketplace</span>
</div>
<div class="badge bg-dark border border-secondary text-white p-2 d-flex align-items-center justify-content-between" style="font-size: 0.85rem; font-weight: 800;">
<span>📊 Serilog</span>
<span class="text-success">+2 Shield on Attack</span>
<span>🎵 Web Audio Synthwave BGM</span>
<span class="text-success">Procedural Music</span>
</div>
</div>
</div>
<div class="card p-3" style="background-color: rgba(33, 37, 43, 0.92); border: 2px solid #383e4a; border-radius: 12px; backdrop-filter: blur(6px);">
<div class="text-white fw-bolder mb-2" style="font-size: 0.88rem;">MECHANICS HIGHLIGHTS</div>
<div class="d-flex justify-content-between text-white" style="font-size: 0.85rem; font-weight: 800;">
<span>Branch Prediction Crit</span>
<span class="text-warning fw-bolder">25% chance for 2x DMG</span>
</div>
<div class="d-flex justify-content-between text-white mt-1" style="font-size: 0.85rem; font-weight: 800;">
<span>Git Procedural Map</span>
<span class="text-info fw-bolder">7-Floor Branching Paths</span>
</div>
<div class="d-flex justify-content-between text-white mt-1" style="font-size: 0.85rem; font-weight: 800;">
<span>Post-Battle PR Review</span>
<span class="text-success fw-bolder">Merge 1 of 3 Cards</span>
</div>
</div>
</div>
</div>
</div>
@@ -290,7 +347,7 @@ else
<button class="btn btn-success btn-lg font-mono fw-bold px-5 py-3 shadow" @onclick="TriggerCampaignLaunchAsync">
🚀 Start New Sprint / Run
</button>
<button class="btn btn-outline-secondary btn-lg font-mono fw-bold px-4 py-3" @onclick="() => _run.CurrentScreen = RunScreen.Title">
<button class="btn btn-outline-secondary btn-lg font-mono fw-bold px-4 py-3" @onclick="ReturnToTitle">
Main Dashboard
</button>
</div>
@@ -311,7 +368,7 @@ else
<button class="btn btn-danger btn-lg font-mono fw-bold px-5 py-3 shadow" @onclick="TriggerCampaignLaunchAsync">
🔄 Retrying Deployment (New Run)
</button>
<button class="btn btn-outline-secondary btn-lg font-mono fw-bold px-4 py-3" @onclick="() => _run.CurrentScreen = RunScreen.Title">
<button class="btn btn-outline-secondary btn-lg font-mono fw-bold px-4 py-3" @onclick="ReturnToTitle">
Main Dashboard
</button>
</div>
@@ -321,6 +378,68 @@ else
}
}
<!-- Git Commit Log / Run History Modal -->
@if (_showHistoryModal)
{
<div class="modal-backdrop-custom d-flex align-items-center justify-content-center" @onclick="() => _showHistoryModal = false">
<div class="card bg-dark text-light border border-secondary shadow-lg p-4 font-mono git-modal-box" @onclick:stopPropagation="true">
<div class="d-flex justify-content-between align-items-center mb-3 pb-2 border-bottom border-secondary">
<h4 class="text-warning mb-0 fw-bolder d-flex align-items-center gap-2">
<span>📜</span>
<span>Git Commit History (Sprint Runs)</span>
</h4>
<button class="btn btn-sm btn-outline-secondary" @onclick="() => _showHistoryModal = false">✕</button>
</div>
@if (_runHistory.Count == 0)
{
<div class="text-center py-5 text-muted">
<div style="font-size: 3rem;">📂</div>
<p class="mt-2 mb-1">No deployment records found in local git tree.</p>
<p class="small text-secondary">Complete a Sprint to log deployment telemetry!</p>
</div>
}
else
{
<div class="overflow-y-auto mb-3 pe-1" style="max-height: 420px;">
@foreach (var record in _runHistory.OrderByDescending(r => r.Timestamp))
{
<div class="card mb-2 p-3 border @(record.IsVictory ? "border-success bg-dark" : "border-danger bg-dark") shadow-sm">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="badge @(record.IsVictory ? "bg-success" : "bg-danger") font-mono">
@(record.IsVictory ? "✓ DEPLOYED" : "✗ ROLLED BACK")
</span>
<span class="text-muted font-mono" style="font-size: 0.75rem;">
@record.Timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")
</span>
</div>
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-secondary font-mono">commit @record.CommitHash</span>
<span class="text-info fw-bold">@record.Archetype</span>
<span class="text-muted font-mono" style="font-size: 0.8rem;">(Floor @record.FloorReached)</span>
</div>
<div class="text-light small mb-2">@record.Message</div>
<div class="d-flex gap-3 text-muted font-mono" style="font-size: 0.78rem;">
<span>HP: <strong class="text-white">@record.FinalHp/@record.MaxHp</strong></span>
<span>Credits: <strong class="text-warning">@record.DevCredits</strong></span>
<span>Deck: <strong class="text-info">@record.DeckSize cards</strong></span>
</div>
</div>
}
</div>
<div class="d-flex justify-content-between align-items-center pt-2 border-top border-secondary">
<button class="btn btn-outline-danger btn-sm font-mono" @onclick="ClearRunHistoryAsync">
🗑️ Purge Commit Log
</button>
<button class="btn btn-secondary btn-sm px-4 font-mono" @onclick="() => _showHistoryModal = false">
Close
</button>
</div>
}
</div>
</div>
}
<style>
/* Intro Boot Screen & Terminal Styles */
.intro-boot-screen {
@@ -438,6 +557,26 @@ else
border-color: #61afef !important;
}
.class-select-card {
background-color: rgba(33, 37, 43, 0.85);
border: 2px solid #383e4a;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
backdrop-filter: blur(6px);
}
.class-select-card:hover {
transform: translateY(-3px);
border-color: #61afef;
}
.class-selected {
border-color: #98c379 !important;
background-color: rgba(33, 50, 43, 0.9) !important;
box-shadow: 0 0 20px rgba(152, 195, 121, 0.35) !important;
}
.deploying-overlay {
position: absolute;
inset: 0;
@@ -460,6 +599,23 @@ else
0% { transform: scale(1); }
100% { transform: scale(1.15); }
}
.modal-backdrop-custom {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(6px);
z-index: 20000;
animation: fade-in 0.2s ease-out;
}
.git-modal-box {
width: 90%;
max-width: 650px;
border-radius: 12px;
background: #1e222b !important;
border: 2px solid #4b5263 !important;
}
</style>
@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<RunRecord> _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<Enemy> enemies = type switch
{
1 => new List<Enemy> { new NullReferenceExceptionEnemy(), new OffByOneBugEnemy() },
2 => new List<Enemy> { new MemoryLeakSpectreEnemy(), new OffByOneBugEnemy() },
3 => new List<Enemy> { new LegacyMonolithBossEnemy() },
2 => new List<Enemy> { new ThreadDeadlockEnemy(), new MemoryLeakSpectreEnemy() },
3 => new List<Enemy> { new KubernetesClusterBossEnemy() },
_ => new List<Enemy> { 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<string?>("localStorage.getItem", "slaysharp_run_history");
if (!string.IsNullOrWhiteSpace(json))
{
var records = JsonSerializer.Deserialize<List<RunRecord>>(json);
if (records != null)
{
_runHistory = records;
}
}
}
catch { }
}
private async Task ClearRunHistoryAsync()
{
_runHistory.Clear();
try
{
await JS.InvokeVoidAsync("localStorage.removeItem", "slaysharp_run_history");
}
catch { }
StateHasChanged();
}
}
+349 -74
View File
@@ -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);
+101
View File
@@ -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);
}
}
};
+107 -1
View File
@@ -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> { 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 });
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<Enemy> { 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);
}
}