impl
This commit is contained in:
@@ -26,13 +26,13 @@ public class ConsoleWriteLineCard : Card
|
||||
CodeSnippet = "target.Hit(9);";
|
||||
}
|
||||
|
||||
public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
|
||||
=> (_damage, _damage, 1, false);
|
||||
|
||||
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||
{
|
||||
if (target == null) return;
|
||||
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||
var totalDmg = player.Statuses.CalculateOutgoingDamage(_damage);
|
||||
var dealt = target.TakeDamage(totalDmg);
|
||||
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: false, isCrit: isCrit, Name);
|
||||
var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, _damage, isPiercing: false, Name);
|
||||
engine.Log.AddInfo($"[Console.WriteLine] {target.Name} took {dealt} dmg.");
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ public class TryCatchCard : Card
|
||||
CodeSnippet = "shield += 8;";
|
||||
}
|
||||
|
||||
public override int GetShieldProfile(CombatEngine engine) => _shield;
|
||||
|
||||
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||
{
|
||||
player.GainShield(_shield);
|
||||
@@ -85,6 +87,91 @@ public class TryCatchCard : Card
|
||||
};
|
||||
}
|
||||
|
||||
public class RandomNextCard : Card
|
||||
{
|
||||
private int _min = 3;
|
||||
private int _max = 14;
|
||||
|
||||
public RandomNextCard() : base(
|
||||
"Random.Shared.Next",
|
||||
"Random.Shared.Next(3, 15);",
|
||||
"target.Hit(Random(3..14));",
|
||||
"Deal 3 to 14 random damage.",
|
||||
cost: 1,
|
||||
type: CardType.Attack,
|
||||
rarity: CardRarity.Common,
|
||||
targetType: TargetType.SingleEnemy
|
||||
) { }
|
||||
|
||||
protected override void OnUpgrade()
|
||||
{
|
||||
_min = 6;
|
||||
_max = 20;
|
||||
Description = "Deal 6 to 20 random damage.";
|
||||
CodeSnippet = "target.Hit(Random(6..20));";
|
||||
}
|
||||
|
||||
public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
|
||||
=> (_min, _max, 1, false);
|
||||
|
||||
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||
{
|
||||
if (target == null) return;
|
||||
var rolledDmg = Random.Shared.Next(_min, _max + 1);
|
||||
var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, rolledDmg, isPiercing: false, Name);
|
||||
engine.Log.AddSuccess($"[Random.Shared] 🎲 ROLLED {rolledDmg} base dmg! Dealt {dealt} total to {target.Name}.");
|
||||
}
|
||||
|
||||
public override Card Clone() => new RandomNextCard
|
||||
{
|
||||
Upgraded = this.Upgraded,
|
||||
Name = this.Name,
|
||||
Description = this.Description,
|
||||
_min = this._min,
|
||||
_max = this._max,
|
||||
CodeSnippet = this.CodeSnippet
|
||||
};
|
||||
}
|
||||
|
||||
public class DynamicExpandoCard : Card
|
||||
{
|
||||
public DynamicExpandoCard() : base(
|
||||
"dynamic ExpandoObject",
|
||||
"dynamic obj = new ExpandoObject();",
|
||||
"Hand.Add(RandomCard(Cost: 0));",
|
||||
"Add a random 0-cost card to your hand.",
|
||||
cost: 1,
|
||||
type: CardType.Skill,
|
||||
rarity: CardRarity.Uncommon,
|
||||
targetType: TargetType.Self
|
||||
) { }
|
||||
|
||||
protected override void OnUpgrade()
|
||||
{
|
||||
Description = "Add a random Upgraded 0-cost card to hand.";
|
||||
CodeSnippet = "Hand.Add(RandomUpgradedCard(0));";
|
||||
}
|
||||
|
||||
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||
{
|
||||
var randomCard = CardCatalog.GetRandomCard();
|
||||
if (Upgraded) randomCard.Upgrade();
|
||||
|
||||
var cloned = randomCard.Clone();
|
||||
cloned.SetCostOverride(0);
|
||||
|
||||
player.Hand.Add(cloned);
|
||||
engine.Log.AddSuccess($"[dynamic ExpandoObject] 🎲 Runtime IL Emit generated '{cloned.Name}' (0 CPU) into hand!");
|
||||
}
|
||||
|
||||
public override Card Clone() => new DynamicExpandoCard
|
||||
{
|
||||
Upgraded = this.Upgraded,
|
||||
Name = this.Name,
|
||||
Description = this.Description
|
||||
};
|
||||
}
|
||||
|
||||
public class GcCollectCard : Card
|
||||
{
|
||||
private int _baseDamage = 8;
|
||||
@@ -107,15 +194,19 @@ public class GcCollectCard : Card
|
||||
CodeSnippet = "target.Hit(12 + Discard * 2);";
|
||||
}
|
||||
|
||||
public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
|
||||
{
|
||||
var discardCount = engine.Player.DiscardPile.Count;
|
||||
var dmg = _baseDamage + (discardCount * 2);
|
||||
return (dmg, dmg, 1, false);
|
||||
}
|
||||
|
||||
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||
{
|
||||
if (target == null) return;
|
||||
var discardCount = player.DiscardPile.Count;
|
||||
var rawDmg = _baseDamage + (discardCount * 2);
|
||||
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||
var totalDmg = player.Statuses.CalculateOutgoingDamage(rawDmg);
|
||||
var dealt = target.TakeDamage(totalDmg);
|
||||
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: false, isCrit: isCrit, Name);
|
||||
var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, rawDmg, isPiercing: false, Name);
|
||||
engine.Log.AddSuccess($"[GC.Collect] Swept {discardCount} objects! Dealt {dealt} dmg.");
|
||||
}
|
||||
|
||||
@@ -151,13 +242,13 @@ public class SpanSliceCard : Card
|
||||
CodeSnippet = "target.DirectHit(11);";
|
||||
}
|
||||
|
||||
public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
|
||||
=> (_damage, _damage, 1, true);
|
||||
|
||||
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||
{
|
||||
if (target == null) return;
|
||||
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||
var totalDmg = player.Statuses.CalculateOutgoingDamage(_damage);
|
||||
var dealt = target.TakeDamage(totalDmg, isPiercing: true);
|
||||
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: true, isCrit: isCrit, Name);
|
||||
var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, _damage, isPiercing: true, Name);
|
||||
engine.Log.AddSuccess($"[Span.Slice] Pierced shield! Dealt {dealt} dmg.");
|
||||
}
|
||||
|
||||
@@ -193,14 +284,14 @@ public class ThrowExceptionCard : Card
|
||||
CodeSnippet = "target.Hit(22); // +1 Leak";
|
||||
}
|
||||
|
||||
public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
|
||||
=> (_damage, _damage, 1, false);
|
||||
|
||||
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||
{
|
||||
if (target == null) return;
|
||||
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||
var totalDmg = player.Statuses.CalculateOutgoingDamage(_damage);
|
||||
var dealt = target.TakeDamage(totalDmg);
|
||||
var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, _damage, isPiercing: false, Name);
|
||||
player.Statuses.Add(StatusType.MemoryLeak, 1);
|
||||
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: false, isCrit: isCrit, Name);
|
||||
engine.Log.AddWarning($"[throw Exception] Dealt {dealt} dmg. +1 Memory Leak.");
|
||||
}
|
||||
|
||||
@@ -311,19 +402,19 @@ public class ParallelForEachCard : Card
|
||||
CodeSnippet = "target.Hit(5); // x3 hits";
|
||||
}
|
||||
|
||||
public override (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
|
||||
=> (_dmgPerHit, _dmgPerHit, 3, false);
|
||||
|
||||
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||
{
|
||||
if (target == null) return;
|
||||
int totalHits = 3;
|
||||
int totalDamage = 0;
|
||||
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||
for (int i = 0; i < totalHits; i++)
|
||||
{
|
||||
if (target.IsDead) break;
|
||||
var hitDmg = player.Statuses.CalculateOutgoingDamage(_dmgPerHit);
|
||||
var dealt = target.TakeDamage(hitDmg);
|
||||
var (dealt, isCrit) = engine.CalculateAndDealDamage(player, target, _dmgPerHit, isPiercing: false, $"{Name} #{i+1}");
|
||||
totalDamage += dealt;
|
||||
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: false, isCrit: isCrit, $"{Name} #{i+1}");
|
||||
}
|
||||
engine.Log.AddSuccess($"[Parallel.ForEach] 3 hits: total {totalDamage} dmg.");
|
||||
}
|
||||
@@ -360,6 +451,8 @@ public class LinqWhereCard : Card
|
||||
CodeSnippet = "shield += 9; Draw(1);";
|
||||
}
|
||||
|
||||
public override int GetShieldProfile(CombatEngine engine) => _shield;
|
||||
|
||||
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||
{
|
||||
player.GainShield(_shield);
|
||||
@@ -397,6 +490,8 @@ public class NullCoalescingCard : Card
|
||||
CodeSnippet = "ClearDebuffs(); shield += 8;";
|
||||
}
|
||||
|
||||
public override int GetShieldProfile(CombatEngine engine) => Upgraded ? 8 : 6;
|
||||
|
||||
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||
{
|
||||
var stacksToRemove = Upgraded ? 2 : 1;
|
||||
@@ -416,3 +511,51 @@ public class NullCoalescingCard : Card
|
||||
Description = this.Description
|
||||
};
|
||||
}
|
||||
|
||||
public static class CardCatalog
|
||||
{
|
||||
public static List<Func<Card>> AllCardFactories => new()
|
||||
{
|
||||
() => new ConsoleWriteLineCard(),
|
||||
() => new TryCatchCard(),
|
||||
() => new RandomNextCard(),
|
||||
() => new DynamicExpandoCard(),
|
||||
() => new GcCollectCard(),
|
||||
() => new SpanSliceCard(),
|
||||
() => new ThrowExceptionCard(),
|
||||
() => new TieredCompilationCard(),
|
||||
() => new ThreadSleepCard(),
|
||||
() => new ParallelForEachCard(),
|
||||
() => new LinqWhereCard(),
|
||||
() => new NullCoalescingCard()
|
||||
};
|
||||
|
||||
public static Card GetRandomCard()
|
||||
{
|
||||
var factories = AllCardFactories;
|
||||
var index = Random.Shared.Next(factories.Count);
|
||||
return factories[index]();
|
||||
}
|
||||
|
||||
public static List<Card> RollThreeCardRewards()
|
||||
{
|
||||
var pool = AllCardFactories;
|
||||
var chosen = new HashSet<int>();
|
||||
var result = new List<Card>();
|
||||
|
||||
while (result.Count < 3 && chosen.Count < pool.Count)
|
||||
{
|
||||
var idx = Random.Shared.Next(pool.Count);
|
||||
if (chosen.Add(idx))
|
||||
{
|
||||
var card = pool[idx]();
|
||||
if (Random.Shared.Next(100) < 25)
|
||||
{
|
||||
card.Upgrade();
|
||||
}
|
||||
result.Add(card);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ public class NullReferenceExceptionEnemy : Enemy
|
||||
{
|
||||
private int _patternStep = 0;
|
||||
|
||||
public NullReferenceExceptionEnemy()
|
||||
: base("NullReferenceException", 45, "💥", "Unhandled null pointer reference causing sudden application crashes.")
|
||||
public NullReferenceExceptionEnemy(int hpBonus = 0)
|
||||
: base("NullReferenceException", 45 + hpBonus, "💥", "Unhandled null pointer reference causing sudden application crashes.")
|
||||
{
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ public class OffByOneBugEnemy : Enemy
|
||||
{
|
||||
private int _patternStep = 0;
|
||||
|
||||
public OffByOneBugEnemy()
|
||||
: base("Off-By-One Bug", 22, "🐛", "Iterative boundary bug indexing `for (int i = 0; i <= arr.Length; i++)`.")
|
||||
public OffByOneBugEnemy(int hpBonus = 0)
|
||||
: base("Off-By-One Bug", 22 + hpBonus, "🐛", "Iterative boundary bug indexing `for (int i = 0; i <= arr.Length; i++)`.")
|
||||
{
|
||||
}
|
||||
|
||||
@@ -121,8 +121,8 @@ public class MemoryLeakSpectreEnemy : Enemy
|
||||
{
|
||||
private int _turn = 0;
|
||||
|
||||
public MemoryLeakSpectreEnemy()
|
||||
: base("Memory Leak Spectre", 34, "👻", "Unmanaged IDisposable and dangling event handlers slowly devouring RAM.")
|
||||
public MemoryLeakSpectreEnemy(int hpBonus = 0)
|
||||
: base("Memory Leak Spectre", 34 + hpBonus, "👻", "Unmanaged IDisposable and dangling event handlers slowly devouring RAM.")
|
||||
{
|
||||
}
|
||||
|
||||
@@ -167,3 +167,71 @@ public class MemoryLeakSpectreEnemy : Enemy
|
||||
DecideNextIntent(engine);
|
||||
}
|
||||
}
|
||||
|
||||
public class LegacyMonolithBossEnemy : Enemy
|
||||
{
|
||||
private int _step = 0;
|
||||
|
||||
public LegacyMonolithBossEnemy()
|
||||
: base("Legacy .NET 2.0 Monolith", 85, "🏢", "Ancient monolithic server running on Windows Server 2003 with zero async code.")
|
||||
{
|
||||
}
|
||||
|
||||
public override void DecideNextIntent(CombatEngine engine)
|
||||
{
|
||||
CurrentIntent = (_step % 4) switch
|
||||
{
|
||||
0 => new Intent(IntentType.AttackDefend, 8, 1, "Synchronous Lock (8 dmg & +12 Shield)"),
|
||||
1 => new Intent(IntentType.Debuff, 0, 1, "Thread Starvation (+2 Leak, +1 Weak)", StatusType.MemoryLeak, 2),
|
||||
2 => new Intent(IntentType.Attack, 18, 1, "GC Gen-2 Global Lockout (18 dmg)"),
|
||||
3 => new Intent(IntentType.Attack, 5, 3, "COM Interop Cascade (5x3 dmg)"),
|
||||
_ => new Intent(IntentType.Attack, 10, 1, "Monolith Strike")
|
||||
};
|
||||
}
|
||||
|
||||
public override void ExecuteIntent(CombatEngine engine)
|
||||
{
|
||||
if (CurrentIntent == null) return;
|
||||
|
||||
switch (_step % 4)
|
||||
{
|
||||
case 0:
|
||||
var dmg0 = Statuses.CalculateOutgoingDamage(8);
|
||||
var isCrit0 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||
var dealt0 = engine.Player.TakeDamage(dmg0);
|
||||
GainShield(12);
|
||||
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt0, false, isCrit0, Name);
|
||||
engine.NotifyShield(Id, Name, 12, Name);
|
||||
engine.Log.AddError($"[{Name}] locked main UI thread! Dealt {dealt0} dmg and gained +12 Shield.");
|
||||
break;
|
||||
case 1:
|
||||
engine.Player.Statuses.Add(StatusType.MemoryLeak, 2);
|
||||
engine.Player.Statuses.Add(StatusType.Weak, 1);
|
||||
GainShield(8);
|
||||
engine.NotifyShield(Id, Name, 8, Name);
|
||||
engine.Log.AddFatal($"[{Name}] starved worker thread pool! Player gained +2 Memory Leak and 1 Weak.");
|
||||
break;
|
||||
case 2:
|
||||
var dmg2 = Statuses.CalculateOutgoingDamage(18);
|
||||
var isCrit2 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||
var dealt2 = engine.Player.TakeDamage(dmg2);
|
||||
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt2, false, isCrit2, Name);
|
||||
engine.Log.AddFatal($"[{Name}] FULL CLR CRASH WAVE! Dealt massive {dealt2} damage!");
|
||||
break;
|
||||
case 3:
|
||||
int total = 0;
|
||||
var isCrit3 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var d = Statuses.CalculateOutgoingDamage(5);
|
||||
total += engine.Player.TakeDamage(d);
|
||||
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, d, false, isCrit3, $"{Name} #{i+1}");
|
||||
}
|
||||
engine.Log.AddError($"[{Name}] COM interop DLLs cascaded! Dealt total {total} damage!");
|
||||
break;
|
||||
}
|
||||
|
||||
_step++;
|
||||
DecideNextIntent(engine);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,20 @@ public static class StarterDecks
|
||||
{
|
||||
return new List<Card>
|
||||
{
|
||||
new ConsoleWriteLineCard(),
|
||||
new ConsoleWriteLineCard(),
|
||||
new ConsoleWriteLineCard(),
|
||||
new ConsoleWriteLineCard(),
|
||||
new TryCatchCard(),
|
||||
new TryCatchCard(),
|
||||
new TryCatchCard(),
|
||||
new TryCatchCard(),
|
||||
new GcCollectCard(),
|
||||
new SpanSliceCard(),
|
||||
new LinqWhereCard(),
|
||||
new TieredCompilationCard()
|
||||
new ConsoleWriteLineCard(), // 1 CPU
|
||||
new ConsoleWriteLineCard(), // 1 CPU
|
||||
new ConsoleWriteLineCard(), // 1 CPU
|
||||
new TryCatchCard(), // 1 CPU
|
||||
new TryCatchCard(), // 1 CPU
|
||||
new TryCatchCard(), // 1 CPU
|
||||
new RandomNextCard(), // 1 CPU - RNG (3-14 dmg)
|
||||
new DynamicExpandoCard(), // 1 CPU - RNG (Generate random 0-cost card)
|
||||
new ThreadSleepCard(), // 0 CPU - Ramp (+2 CPU)
|
||||
new SpanSliceCard(), // 1 CPU - Piercing
|
||||
new LinqWhereCard(), // 1 CPU - Shield & Draw
|
||||
new TieredCompilationCard(), // 1 CPU - Power (+2 Strength)
|
||||
new GcCollectCard(), // 2 CPU - Scaler
|
||||
new ThrowExceptionCard() // 2 CPU - Heavy Burst (16 dmg)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using SlaySharp.Core.Catalogs;
|
||||
using SlaySharp.Core.Enums;
|
||||
using SlaySharp.Core.Models;
|
||||
|
||||
@@ -11,6 +12,7 @@ public class CombatEngine
|
||||
public CombatPhase Phase { get; private set; } = CombatPhase.PlayerTurn;
|
||||
public CombatLog Log { get; } = new();
|
||||
public string? ActiveActingEnemyId { get; private set; }
|
||||
public List<Card> CardRewards { get; private set; } = new();
|
||||
|
||||
public event Action? OnStateChanged;
|
||||
public event Action<DamageEvent>? OnDamageDealt;
|
||||
@@ -27,6 +29,79 @@ public class CombatEngine
|
||||
OnShieldGained?.Invoke(new ShieldEvent(targetId, targetName, amount, sourceName));
|
||||
}
|
||||
|
||||
public (int dealtDamage, bool isCrit) CalculateAndDealDamage(Player player, Character target, int baseDamage, bool isPiercing, string sourceName)
|
||||
{
|
||||
var targetIsVulnerable = target.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||
var rngCrit = Random.Shared.Next(100) < 15; // 15% JIT Branch Prediction Crit chance!
|
||||
var isCrit = targetIsVulnerable || rngCrit;
|
||||
|
||||
var modifiedDmg = player.Statuses.CalculateOutgoingDamage(baseDamage);
|
||||
if (rngCrit && !targetIsVulnerable)
|
||||
{
|
||||
modifiedDmg = (int)Math.Round(modifiedDmg * 1.5, MidpointRounding.AwayFromZero);
|
||||
Log.AddSuccess($"[JIT Branch Prediction] 🎲 CRITICAL HIT on {target.Name} (1.5x damage)!");
|
||||
}
|
||||
|
||||
var dealt = target.TakeDamage(modifiedDmg, isPiercing);
|
||||
NotifyDamage(target.Id, target.Name, dealt, isPiercing, isCrit, sourceName);
|
||||
return (dealt, isCrit);
|
||||
}
|
||||
|
||||
public DamagePreviewInfo CalculateDamagePreview(Card card, Enemy enemy)
|
||||
{
|
||||
var (minRaw, maxRaw, hits, isPiercing) = card.GetDamageProfile(this, enemy);
|
||||
if (minRaw == 0 && maxRaw == 0)
|
||||
{
|
||||
return new DamagePreviewInfo(0, 0, 0, false, 0, 0, 0, enemy.CurrentHp, false, false);
|
||||
}
|
||||
|
||||
var isCrit = enemy.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||
|
||||
int minTotal = 0;
|
||||
int maxTotal = 0;
|
||||
|
||||
for (int i = 0; i < hits; i++)
|
||||
{
|
||||
var minHit = Player.Statuses.CalculateOutgoingDamage(minRaw);
|
||||
var maxHit = Player.Statuses.CalculateOutgoingDamage(maxRaw);
|
||||
|
||||
minTotal += minHit;
|
||||
maxTotal += maxHit;
|
||||
}
|
||||
|
||||
int shieldAbsorbed = 0;
|
||||
int minHpLoss = 0;
|
||||
int maxHpLoss = 0;
|
||||
|
||||
if (isPiercing)
|
||||
{
|
||||
minHpLoss = minTotal;
|
||||
maxHpLoss = maxTotal;
|
||||
}
|
||||
else
|
||||
{
|
||||
shieldAbsorbed = Math.Min(enemy.Shield, minTotal);
|
||||
minHpLoss = Math.Max(0, minTotal - enemy.Shield);
|
||||
maxHpLoss = Math.Max(0, maxTotal - enemy.Shield);
|
||||
}
|
||||
|
||||
int projectedRemaining = Math.Max(0, enemy.CurrentHp - minHpLoss);
|
||||
bool isLethal = minHpLoss >= enemy.CurrentHp;
|
||||
|
||||
return new DamagePreviewInfo(
|
||||
minTotal,
|
||||
maxTotal,
|
||||
hits,
|
||||
isPiercing,
|
||||
shieldAbsorbed,
|
||||
minHpLoss,
|
||||
maxHpLoss,
|
||||
projectedRemaining,
|
||||
isLethal,
|
||||
isCrit
|
||||
);
|
||||
}
|
||||
|
||||
public void StartCombat(Player player, List<Enemy> enemies)
|
||||
{
|
||||
Player = player;
|
||||
@@ -34,6 +109,7 @@ public class CombatEngine
|
||||
TurnNumber = 1;
|
||||
Phase = CombatPhase.PlayerTurn;
|
||||
ActiveActingEnemyId = null;
|
||||
CardRewards.Clear();
|
||||
|
||||
Log.Clear();
|
||||
Log.AddInfo($"=== Starting Incident Resolution: {enemies.Count} Bugs Detected ===");
|
||||
@@ -301,9 +377,18 @@ public class CombatEngine
|
||||
else if (Enemies.Count == 0)
|
||||
{
|
||||
Phase = CombatPhase.Victory;
|
||||
CardRewards = CardCatalog.RollThreeCardRewards();
|
||||
Log.AddSuccess("=== ALL BUGS RESOLVED! Build Deployed Successfully! ===");
|
||||
}
|
||||
}
|
||||
|
||||
public void ClaimCardReward(Card card)
|
||||
{
|
||||
Player.MasterDeck.Add(card.Clone());
|
||||
Log.AddSuccess($"[Pull Request Merged] Added '{card.Name}' to Master Deck!");
|
||||
CardRewards.Clear();
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
public void NotifyStateChanged() => OnStateChanged?.Invoke();
|
||||
}
|
||||
@@ -44,8 +44,23 @@ public abstract class Card
|
||||
return engine.Player.CurrentEnergy >= Cost;
|
||||
}
|
||||
|
||||
public void SetCostOverride(int newCost)
|
||||
{
|
||||
Cost = newCost;
|
||||
}
|
||||
|
||||
public abstract void ApplyEffect(CombatEngine engine, Player player, Character? target);
|
||||
|
||||
public virtual (int minDamage, int maxDamage, int hits, bool isPiercing) GetDamageProfile(CombatEngine engine, Character? target = null)
|
||||
{
|
||||
return (0, 0, 1, false);
|
||||
}
|
||||
|
||||
public virtual int GetShieldProfile(CombatEngine engine)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public virtual void Upgrade()
|
||||
{
|
||||
if (Upgraded) return;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace SlaySharp.Core.Models;
|
||||
|
||||
public record DamagePreviewInfo(
|
||||
int MinRawDamage,
|
||||
int MaxRawDamage,
|
||||
int Hits,
|
||||
bool IsPiercing,
|
||||
int ShieldAbsorbed,
|
||||
int MinHpLoss,
|
||||
int MaxHpLoss,
|
||||
int ProjectedRemainingHp,
|
||||
bool IsLethal,
|
||||
bool IsCrit
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
using SlaySharp.Core.Combat;
|
||||
using SlaySharp.Core.Combat;
|
||||
|
||||
namespace SlaySharp.Core.Relics;
|
||||
|
||||
@@ -83,3 +83,59 @@ public class SerilogRelic : Relic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PollyRelic : Relic
|
||||
{
|
||||
private bool _triggered;
|
||||
|
||||
public PollyRelic() : base(
|
||||
"Polly",
|
||||
"Polly",
|
||||
"v8.3.0",
|
||||
"Once per combat: When taking damage, trigger resilience policy for +12 Shield.",
|
||||
"🛡️"
|
||||
) { }
|
||||
|
||||
public override void OnCombatStart(CombatEngine engine)
|
||||
{
|
||||
_triggered = false;
|
||||
}
|
||||
|
||||
public override void OnTurnStart(CombatEngine engine)
|
||||
{
|
||||
if (!_triggered && engine.Player.CurrentHp < (engine.Player.MaxHp / 2))
|
||||
{
|
||||
_triggered = true;
|
||||
engine.Player.GainShield(12);
|
||||
engine.Log.AddInfo("[Polly] Circuit breaker triggered! Resilience policy deployed +12 Shield.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DapperRelic : Relic
|
||||
{
|
||||
private bool _attackPlayedThisTurn;
|
||||
|
||||
public DapperRelic() : base(
|
||||
"Dapper",
|
||||
"Dapper",
|
||||
"v2.1.35",
|
||||
"Micro-ORM caching: The first Attack card played each turn grants +3 Shield.",
|
||||
"⚡"
|
||||
) { }
|
||||
|
||||
public override void OnTurnStart(CombatEngine engine)
|
||||
{
|
||||
_attackPlayedThisTurn = false;
|
||||
}
|
||||
|
||||
public override void OnCardPlayed(CombatEngine engine, Models.Card card)
|
||||
{
|
||||
if (!_attackPlayedThisTurn && card.Type == Enums.CardType.Attack)
|
||||
{
|
||||
_attackPlayedThisTurn = true;
|
||||
engine.Player.GainShield(3);
|
||||
engine.Log.AddInfo("[Dapper] High-speed micro-ORM query: +3 Shield gained.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
using SlaySharp.Core.Catalogs;
|
||||
using SlaySharp.Core.Combat;
|
||||
using SlaySharp.Core.Models;
|
||||
using SlaySharp.Core.Relics;
|
||||
|
||||
namespace SlaySharp.Core.Run;
|
||||
|
||||
public enum RunScreen
|
||||
{
|
||||
Title,
|
||||
Map,
|
||||
Combat,
|
||||
Event,
|
||||
RestSite,
|
||||
Shop,
|
||||
RunVictory,
|
||||
RunDefeat
|
||||
}
|
||||
|
||||
public class GameRun
|
||||
{
|
||||
public Player Player { get; set; } = null!;
|
||||
public MapGraph Map { get; set; } = null!;
|
||||
public MapNode? CurrentNode { get; set; }
|
||||
public HashSet<string> VisitedNodeIds { get; } = new();
|
||||
public int DevCredits { get; set; } = 60;
|
||||
public RunScreen CurrentScreen { get; set; } = RunScreen.Title;
|
||||
|
||||
public CombatEngine? ActiveCombat { get; set; }
|
||||
public StoryEvent? ActiveEvent { get; set; }
|
||||
|
||||
public event Action? OnRunStateChanged;
|
||||
|
||||
public void StartNewRun(int? seed = null)
|
||||
{
|
||||
Player = new Player("Senior Dev", 70, maxEnergy: 3, seed: seed);
|
||||
Player.MasterDeck.AddRange(StarterDecks.CreateCsharpArchitectDeck());
|
||||
|
||||
Player.Relics.Add(new NewtonsoftJsonRelic());
|
||||
Player.Relics.Add(new BenchmarkDotNetRelic());
|
||||
Player.Relics.Add(new SerilogRelic());
|
||||
|
||||
DevCredits = 60;
|
||||
VisitedNodeIds.Clear();
|
||||
CurrentNode = null;
|
||||
ActiveCombat = null;
|
||||
ActiveEvent = null;
|
||||
|
||||
Map = MapGraph.GenerateAct1Map(seed);
|
||||
CurrentScreen = RunScreen.Map;
|
||||
NotifyChange();
|
||||
}
|
||||
|
||||
public bool CanVisitNode(MapNode node)
|
||||
{
|
||||
if (CurrentNode == null)
|
||||
{
|
||||
// Can visit any floor 0 node
|
||||
return node.Floor == 0;
|
||||
}
|
||||
|
||||
return CurrentNode.NextNodeIds.Contains(node.Id);
|
||||
}
|
||||
|
||||
public void SelectNode(MapNode node)
|
||||
{
|
||||
if (!CanVisitNode(node)) return;
|
||||
|
||||
CurrentNode = node;
|
||||
VisitedNodeIds.Add(node.Id);
|
||||
|
||||
switch (node.Type)
|
||||
{
|
||||
case MapNodeType.Combat:
|
||||
StartCombatEncounter(isElite: false, isBoss: false);
|
||||
break;
|
||||
case MapNodeType.Elite:
|
||||
StartCombatEncounter(isElite: true, isBoss: false);
|
||||
break;
|
||||
case MapNodeType.Boss:
|
||||
StartCombatEncounter(isElite: false, isBoss: true);
|
||||
break;
|
||||
case MapNodeType.RestSite:
|
||||
CurrentScreen = RunScreen.RestSite;
|
||||
break;
|
||||
case MapNodeType.Shop:
|
||||
CurrentScreen = RunScreen.Shop;
|
||||
break;
|
||||
case MapNodeType.MysteryEvent:
|
||||
StartRandomEvent();
|
||||
break;
|
||||
}
|
||||
|
||||
NotifyChange();
|
||||
}
|
||||
|
||||
private void StartCombatEncounter(bool isElite, bool isBoss)
|
||||
{
|
||||
List<Enemy> enemies;
|
||||
if (isBoss)
|
||||
{
|
||||
enemies = new List<Enemy> { new LegacyMonolithBossEnemy() };
|
||||
}
|
||||
else if (isElite)
|
||||
{
|
||||
enemies = new List<Enemy> { new MemoryLeakSpectreEnemy(hpBonus: 6), new OffByOneBugEnemy() };
|
||||
}
|
||||
else
|
||||
{
|
||||
// Standard combat
|
||||
var roll = Random.Shared.Next(2);
|
||||
enemies = roll switch
|
||||
{
|
||||
0 => new List<Enemy> { new NullReferenceExceptionEnemy(), new OffByOneBugEnemy() },
|
||||
_ => new List<Enemy> { new MemoryLeakSpectreEnemy(), new OffByOneBugEnemy() }
|
||||
};
|
||||
}
|
||||
|
||||
ActiveCombat = new CombatEngine();
|
||||
ActiveCombat.StartCombat(Player, enemies);
|
||||
CurrentScreen = RunScreen.Combat;
|
||||
}
|
||||
|
||||
private void StartRandomEvent()
|
||||
{
|
||||
var events = StoryEvent.GetAct1Events();
|
||||
var index = Random.Shared.Next(events.Count);
|
||||
ActiveEvent = events[index];
|
||||
CurrentScreen = RunScreen.Event;
|
||||
}
|
||||
|
||||
public void CompleteCombatAndClaimRewards(int bonusCredits = 25)
|
||||
{
|
||||
DevCredits += bonusCredits;
|
||||
if (CurrentNode?.Type == MapNodeType.Boss)
|
||||
{
|
||||
CurrentScreen = RunScreen.RunVictory;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentScreen = RunScreen.Map;
|
||||
}
|
||||
ActiveCombat = null;
|
||||
NotifyChange();
|
||||
}
|
||||
|
||||
public void ReturnToMap()
|
||||
{
|
||||
if (Player.IsDead)
|
||||
{
|
||||
CurrentScreen = RunScreen.RunDefeat;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentScreen = RunScreen.Map;
|
||||
}
|
||||
ActiveCombat = null;
|
||||
ActiveEvent = null;
|
||||
NotifyChange();
|
||||
}
|
||||
|
||||
public void NotifyChange() => OnRunStateChanged?.Invoke();
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
namespace SlaySharp.Core.Run;
|
||||
|
||||
public class MapGraph
|
||||
{
|
||||
public List<List<MapNode>> Floors { get; } = new();
|
||||
|
||||
public static MapGraph GenerateAct1Map(int? seed = null)
|
||||
{
|
||||
var rng = seed.HasValue ? new Random(seed.Value) : Random.Shared;
|
||||
var map = new MapGraph();
|
||||
|
||||
// Floor 0: Starter Pull Requests (2 Combats)
|
||||
var floor0 = new List<MapNode>
|
||||
{
|
||||
new MapNode { Floor = 0, Column = 0, Type = MapNodeType.Combat, Title = "PR #101: Code Review", Description = "Resolve unit test failures on branch feature/auth." },
|
||||
new MapNode { Floor = 0, Column = 1, Type = MapNodeType.Combat, Title = "PR #102: API Gateway Glitch", Description = "Fix routing exceptions in reverse proxy." }
|
||||
};
|
||||
map.Floors.Add(floor0);
|
||||
|
||||
// Floor 1: 3 Nodes (Mystery, Combat, Mystery)
|
||||
var floor1 = new List<MapNode>
|
||||
{
|
||||
new MapNode { Floor = 1, Column = 0, Type = MapNodeType.MysteryEvent, Title = "Stack Overflow Thread", Description = "An enigmatic developer answered an old issue." },
|
||||
new MapNode { Floor = 1, Column = 1, Type = MapNodeType.Combat, Title = "PR #108: Cache Invalidation", Description = "Memory corruption in Redis cache layer." },
|
||||
new MapNode { Floor = 1, Column = 2, Type = MapNodeType.MysteryEvent, Title = "Junior Dev PR", Description = "A pull request with 8000 lines of code changes." }
|
||||
};
|
||||
map.Floors.Add(floor1);
|
||||
|
||||
// Floor 2: 2 Nodes (Shop, Elite)
|
||||
var floor2 = new List<MapNode>
|
||||
{
|
||||
new MapNode { Floor = 2, Column = 0, Type = MapNodeType.Shop, Title = "NuGet Marketplace Feed", Description = "Procure enterprise diagnostic libraries." },
|
||||
new MapNode { Floor = 2, Column = 1, Type = MapNodeType.Elite, Title = "SEV-1 Critical Outage", Description = "Heavy memory leak threatening pod stability." }
|
||||
};
|
||||
map.Floors.Add(floor2);
|
||||
|
||||
// Floor 3: 2 Nodes (RestSite, RestSite)
|
||||
var floor3 = new List<MapNode>
|
||||
{
|
||||
new MapNode { Floor = 3, Column = 0, Type = MapNodeType.RestSite, Title = "Espresso & Refactor Bar", Description = "Recharge SLA or optimize Roslyn AST cards." },
|
||||
new MapNode { Floor = 3, Column = 1, Type = MapNodeType.RestSite, Title = "Dev Lounge Coffee Machine", Description = "Quick break before staging deployment." }
|
||||
};
|
||||
map.Floors.Add(floor3);
|
||||
|
||||
// Floor 4: 2 Nodes (Mystery, Elite)
|
||||
var floor4 = new List<MapNode>
|
||||
{
|
||||
new MapNode { Floor = 4, Column = 0, Type = MapNodeType.MysteryEvent, Title = "Legacy COM Discovery", Description = "Ancient unmanaged DLL found in codebase." },
|
||||
new MapNode { Floor = 4, Column = 1, Type = MapNodeType.Elite, Title = "Deadlock Demon Outage", Description = "Thread pool starvation across microservices." }
|
||||
};
|
||||
map.Floors.Add(floor4);
|
||||
|
||||
// Floor 5: 1 Node (RestSite before Boss)
|
||||
var floor5 = new List<MapNode>
|
||||
{
|
||||
new MapNode { Floor = 5, Column = 0, Type = MapNodeType.RestSite, Title = "Final Staging Gate Review", Description = "Last chance to refactor cards before Release Gate." }
|
||||
};
|
||||
map.Floors.Add(floor5);
|
||||
|
||||
// Floor 6: 1 Node (Boss)
|
||||
var floor6 = new List<MapNode>
|
||||
{
|
||||
new MapNode { Floor = 6, Column = 0, Type = MapNodeType.Boss, Title = "BOSS: Friday 5 PM Monolith", Description = "The ultimate legacy .NET Framework 2.0 Monolith release." }
|
||||
};
|
||||
map.Floors.Add(floor6);
|
||||
|
||||
// Connect Nodes
|
||||
// Floor 0 -> Floor 1
|
||||
floor0[0].NextNodeIds.AddRange(new[] { floor1[0].Id, floor1[1].Id });
|
||||
floor0[1].NextNodeIds.AddRange(new[] { floor1[1].Id, floor1[2].Id });
|
||||
|
||||
// Floor 1 -> Floor 2
|
||||
floor1[0].NextNodeIds.Add(floor2[0].Id);
|
||||
floor1[1].NextNodeIds.AddRange(new[] { floor2[0].Id, floor2[1].Id });
|
||||
floor1[2].NextNodeIds.Add(floor2[1].Id);
|
||||
|
||||
// Floor 2 -> Floor 3
|
||||
floor2[0].NextNodeIds.AddRange(new[] { floor3[0].Id, floor3[1].Id });
|
||||
floor2[1].NextNodeIds.AddRange(new[] { floor3[0].Id, floor3[1].Id });
|
||||
|
||||
// Floor 3 -> Floor 4
|
||||
floor3[0].NextNodeIds.AddRange(new[] { floor4[0].Id, floor4[1].Id });
|
||||
floor3[1].NextNodeIds.AddRange(new[] { floor4[0].Id, floor4[1].Id });
|
||||
|
||||
// Floor 4 -> Floor 5
|
||||
floor4[0].NextNodeIds.Add(floor5[0].Id);
|
||||
floor4[1].NextNodeIds.Add(floor5[0].Id);
|
||||
|
||||
// Floor 5 -> Floor 6
|
||||
floor5[0].NextNodeIds.Add(floor6[0].Id);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public MapNode? FindNode(string id)
|
||||
{
|
||||
return Floors.SelectMany(f => f).FirstOrDefault(n => n.Id == id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace SlaySharp.Core.Run;
|
||||
|
||||
public enum MapNodeType
|
||||
{
|
||||
Combat,
|
||||
Elite,
|
||||
RestSite,
|
||||
Shop,
|
||||
MysteryEvent,
|
||||
Boss
|
||||
}
|
||||
|
||||
public class MapNode
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N")[..8];
|
||||
public int Floor { get; set; }
|
||||
public int Column { get; set; }
|
||||
public MapNodeType Type { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public List<string> NextNodeIds { get; set; } = new();
|
||||
|
||||
public string Icon => Type switch
|
||||
{
|
||||
MapNodeType.Combat => "⚔️",
|
||||
MapNodeType.Elite => "☠️",
|
||||
MapNodeType.RestSite => "☕",
|
||||
MapNodeType.Shop => "🛒",
|
||||
MapNodeType.MysteryEvent => "❓",
|
||||
MapNodeType.Boss => "👑",
|
||||
_ => "📍"
|
||||
};
|
||||
|
||||
public string BadgeColor => Type switch
|
||||
{
|
||||
MapNodeType.Combat => "#61afef",
|
||||
MapNodeType.Elite => "#e06c75",
|
||||
MapNodeType.RestSite => "#98c379",
|
||||
MapNodeType.Shop => "#e5c07b",
|
||||
MapNodeType.MysteryEvent => "#c678dd",
|
||||
MapNodeType.Boss => "#ff4757",
|
||||
_ => "#abb2bf"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
using SlaySharp.Core.Catalogs;
|
||||
using SlaySharp.Core.Models;
|
||||
|
||||
namespace SlaySharp.Core.Run;
|
||||
|
||||
public record StoryOption(
|
||||
string ChoiceText,
|
||||
string OutcomeText,
|
||||
Action<GameRun> ApplyEffect
|
||||
);
|
||||
|
||||
public class StoryEvent
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = "❓";
|
||||
public string Lore { get; set; } = string.Empty;
|
||||
public List<StoryOption> Options { get; set; } = new();
|
||||
|
||||
public static List<StoryEvent> GetAct1Events()
|
||||
{
|
||||
return new List<StoryEvent>
|
||||
{
|
||||
new StoryEvent
|
||||
{
|
||||
Id = "stackoverflow",
|
||||
Title = "The 12-Year-Old Stack Overflow Answer",
|
||||
Icon = "🌐",
|
||||
Lore = "While investigating a rare memory access bug, you find a forum answer from 2014 by a developer named 'JonSkeetFan99'. The solution uses dangerous unsafe pointer arithmetic.",
|
||||
Options = new List<StoryOption>
|
||||
{
|
||||
new StoryOption(
|
||||
"Copy-Paste Without Reading [Gain Upgraded Span.Slice+, lose 8 Max SLA]",
|
||||
"You paste the snippet into production. It executes at blinding speed, but technical debt accrues!",
|
||||
run =>
|
||||
{
|
||||
var card = new SpanSliceCard();
|
||||
card.Upgrade();
|
||||
run.Player.MasterDeck.Add(card);
|
||||
run.Player.MaxHp = Math.Max(20, run.Player.MaxHp - 8);
|
||||
run.Player.CurrentHp = Math.Min(run.Player.CurrentHp, run.Player.MaxHp);
|
||||
}
|
||||
),
|
||||
new StoryOption(
|
||||
"Read Documentation & Write Unit Tests [Heal 15 SLA, Gain 30 Dev Credits]",
|
||||
"You spend 20 minutes understanding the root cause. Clean architecture restored!",
|
||||
run =>
|
||||
{
|
||||
run.Player.Heal(15);
|
||||
run.DevCredits += 30;
|
||||
}
|
||||
),
|
||||
new StoryOption(
|
||||
"Downvote & Close Browser [Gain 20 Dev Credits]",
|
||||
"You decide not to gamble with legacy hacks and move forward.",
|
||||
run =>
|
||||
{
|
||||
run.DevCredits += 20;
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
new StoryEvent
|
||||
{
|
||||
Id = "junior_pr",
|
||||
Title = "The 8,000-Line 'Minor Bugfix' PR",
|
||||
Icon = "🌿",
|
||||
Lore = "A junior developer submits a pull request titled 'Refactored auth module (quick fix)'. It modifies 142 files across the entire solution without a single unit test.",
|
||||
Options = new List<StoryOption>
|
||||
{
|
||||
new StoryOption(
|
||||
"Approve & Ship to Staging [Gain Upgraded throw Exception+, take 10 damage]",
|
||||
"You click 'LGTM' and merge. The build explodes in flames, but unlocks sheer destructive power!",
|
||||
run =>
|
||||
{
|
||||
var card = new ThrowExceptionCard();
|
||||
card.Upgrade();
|
||||
run.Player.MasterDeck.Add(card);
|
||||
run.Player.TakeDamage(10, isPiercing: true);
|
||||
}
|
||||
),
|
||||
new StoryOption(
|
||||
"Pair-Program & Teach Clean Code [Upgrade a Card, +35 Dev Credits]",
|
||||
"You mentor the developer. A card in your Master Deck is refactored to production perfection!",
|
||||
run =>
|
||||
{
|
||||
var upgradable = run.Player.MasterDeck.FirstOrDefault(c => !c.Upgraded);
|
||||
upgradable?.Upgrade();
|
||||
run.DevCredits += 35;
|
||||
}
|
||||
),
|
||||
new StoryOption(
|
||||
"Close PR and Request Smaller Diffs [Heal 10 SLA]",
|
||||
"You enforce code hygiene policies and drink some tea.",
|
||||
run =>
|
||||
{
|
||||
run.Player.Heal(10);
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
new StoryEvent
|
||||
{
|
||||
Id = "legacy_com",
|
||||
Title = "Ancient C++ COM Interop Wrapper",
|
||||
Icon = "💾",
|
||||
Lore = "In the deep bowels of the repository, you uncover a binary file compiled in 2003 with zero source code and unmanaged memory allocations.",
|
||||
Options = new List<StoryOption>
|
||||
{
|
||||
new StoryOption(
|
||||
"Wrap in Dynamic JIT Optimization [Gain Tiered Compilation+]",
|
||||
"You wrap the legacy binary in a high-performance Roslyn wrapper!",
|
||||
run =>
|
||||
{
|
||||
var card = new TieredCompilationCard();
|
||||
card.Upgrade();
|
||||
run.Player.MasterDeck.Add(card);
|
||||
}
|
||||
),
|
||||
new StoryOption(
|
||||
"Decompile and Liquidate [Gain 80 Dev Credits]",
|
||||
"You extract the proprietary business logic and earn huge company bonuses!",
|
||||
run =>
|
||||
{
|
||||
run.DevCredits += 80;
|
||||
}
|
||||
),
|
||||
new StoryOption(
|
||||
"Seal Behind Deprecated Attribute [Heal 20 SLA]",
|
||||
"You mark it [Obsolete] and isolate it from the runtime.",
|
||||
run =>
|
||||
{
|
||||
run.Player.Heal(20);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
@using SlaySharp.Core.Enums
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="game-card @GetCardClasses()" @onclick="HandleCardClick">
|
||||
<div class="game-card @GetCardClasses()"
|
||||
@onclick="HandleCardClick"
|
||||
@onmouseenter="HandleMouseEnter"
|
||||
@onmouseleave="HandleMouseLeave">
|
||||
<!-- VS Code Tab Header -->
|
||||
<div class="card-vsc-header">
|
||||
<div class="card-tab-label font-mono overflow-hidden">
|
||||
@@ -18,9 +21,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Visual Art Window -->
|
||||
<!-- 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>
|
||||
|
||||
@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;">
|
||||
@@ -49,9 +60,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Punchy, Large, Bold Card Effect Footer -->
|
||||
<!-- 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;">@Card.Description</span>
|
||||
<span style="font-size: 0.92rem; font-weight: 900; line-height: 1.25;">
|
||||
@FormatDescription(Card.Description)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,6 +73,23 @@
|
||||
[Parameter] public bool IsPlayable { get; set; } = true;
|
||||
[Parameter] public bool IsSelected { get; set; } = false;
|
||||
[Parameter] public EventCallback<Card> OnCardClicked { get; set; }
|
||||
[Parameter] public EventCallback<Card?> OnCardHoverChanged { get; set; }
|
||||
|
||||
private async Task HandleMouseEnter()
|
||||
{
|
||||
if (OnCardHoverChanged.HasDelegate)
|
||||
{
|
||||
await OnCardHoverChanged.InvokeAsync(Card);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleMouseLeave()
|
||||
{
|
||||
if (OnCardHoverChanged.HasDelegate)
|
||||
{
|
||||
await OnCardHoverChanged.InvokeAsync(null);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCardClasses()
|
||||
{
|
||||
@@ -81,6 +111,22 @@
|
||||
return string.Join(" ", classes);
|
||||
}
|
||||
|
||||
private string GetActionTagClass() => Card.Type switch
|
||||
{
|
||||
CardType.Attack => "type-tag-attack",
|
||||
CardType.Skill => "type-tag-skill",
|
||||
CardType.Power => "type-tag-power",
|
||||
_ => "type-tag-skill"
|
||||
};
|
||||
|
||||
private string GetActionTypeIcon() => Card.Type switch
|
||||
{
|
||||
CardType.Attack => "⚔️",
|
||||
CardType.Skill => "🛡️",
|
||||
CardType.Power => "⚡",
|
||||
_ => "📜"
|
||||
};
|
||||
|
||||
private string GetCardArtIcon(string name)
|
||||
{
|
||||
if (name.Contains("WriteLine", StringComparison.OrdinalIgnoreCase)) return "💻";
|
||||
@@ -96,6 +142,23 @@
|
||||
return "📄";
|
||||
}
|
||||
|
||||
private MarkupString FormatDescription(string desc)
|
||||
{
|
||||
if (string.IsNullOrEmpty(desc)) return new MarkupString("");
|
||||
|
||||
var formatted = desc
|
||||
.Replace("damage", "<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>");
|
||||
|
||||
return new MarkupString(formatted);
|
||||
}
|
||||
|
||||
private MarkupString FormatCodeSnippet(string snippet)
|
||||
{
|
||||
if (string.IsNullOrEmpty(snippet)) return new MarkupString("");
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@using SlaySharp.Web.Models
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="d-flex flex-column h-100" style="background-color: var(--vscode-bg-editor);">
|
||||
<div class="d-flex flex-column h-100" style="background: linear-gradient(rgba(24, 26, 31, 0.82), rgba(24, 26, 31, 0.88)), url('images/combat_arena_bg.jpg') no-repeat center center; background-size: cover;">
|
||||
<!-- VS Code Editor Tabs Bar -->
|
||||
<div class="vsc-tabs-bar font-mono">
|
||||
<div class="vsc-tab active">
|
||||
@@ -53,6 +53,7 @@
|
||||
</div>
|
||||
|
||||
<!-- VS Code Floating Debug Toolbar -->
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="vsc-debug-toolbar font-mono">
|
||||
<button class="vsc-debug-btn"
|
||||
disabled="@(Engine.Phase != CombatPhase.PlayerTurn || _isProcessingTurn)"
|
||||
@@ -65,15 +66,13 @@
|
||||
<button class="vsc-debug-btn" @onclick="RestartCombat" title="Restart Incident (Ctrl+Shift+F5)">
|
||||
<span style="color: #61afef; font-size: 1.2rem;">↻</span>
|
||||
</button>
|
||||
<button class="vsc-debug-btn" @onclick="RestartCombat" title="Stop Incident (Shift+F5)">
|
||||
<span style="color: #e06c75; font-size: 1.2rem;">⏹</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Battle Arena Area -->
|
||||
<div class="flex-grow-1 d-flex flex-column justify-content-between p-3 overflow-y-auto" style="min-height: 0;">
|
||||
<!-- Enemies Area with Floating Damage Popups -->
|
||||
<!-- Enemies Area with Point-and-Target Live Damage Previews -->
|
||||
<div class="d-flex justify-content-center align-items-center gap-4 my-auto py-2">
|
||||
@if (Engine.Enemies.Any())
|
||||
{
|
||||
@@ -82,33 +81,64 @@
|
||||
var enemyTexts = _floatingTexts.TryGetValue(enemy.Id, out var texts) ? texts : null;
|
||||
var isAttacking = Engine.ActiveActingEnemyId == enemy.Id;
|
||||
<EnemyView Enemy="@enemy"
|
||||
Engine="@Engine"
|
||||
SelectedCard="@_selectedCard"
|
||||
HoveredEnemyId="@_hoveredEnemyId"
|
||||
TotalEnemyCount="@Engine.Enemies.Count"
|
||||
IsTargeted="@(_selectedCard != null && _selectedCard.TargetType == TargetType.SingleEnemy)"
|
||||
IsActiveAttacker="@isAttacking"
|
||||
FloatingTexts="@enemyTexts"
|
||||
OnEnemyClicked="HandleEnemyClicked" />
|
||||
OnEnemyClicked="HandleEnemyClicked"
|
||||
OnEnemyHoverChanged="HandleEnemyHoverChanged" />
|
||||
}
|
||||
}
|
||||
else if (Engine.Phase == CombatPhase.Victory)
|
||||
{
|
||||
<div class="card p-5 text-center shadow-lg" style="border-radius: 14px; max-width: 580px; background-color: #21252b; border: 2px solid #98c379;">
|
||||
<div style="font-size: 4rem;">🎉</div>
|
||||
<h1 class="text-success font-mono fw-bold mb-2">BUILD SUCCESSFUL</h1>
|
||||
<p class="text-white font-mono fs-5">0 Errors, 0 Exceptions. All bugs patched!</p>
|
||||
<button class="btn btn-success btn-lg font-mono fw-bold mt-3 px-5 py-3" @onclick="RestartCombat">
|
||||
🚀 Next Incident
|
||||
<!-- Victory & Post-Battle Code Review (Pull Request) Card Draft -->
|
||||
<div class="card p-4 text-center shadow-lg my-auto" style="border-radius: 14px; max-width: 820px; background-color: rgba(33, 37, 43, 0.95); border: 2px solid #98c379; backdrop-filter: blur(8px);">
|
||||
<div style="font-size: 3rem;">🎉</div>
|
||||
<h2 class="text-success font-mono fw-bold mb-1">BUILD SUCCESSFUL</h2>
|
||||
<p class="text-white font-mono fs-6 mb-3">0 Errors, 0 Exceptions. Incident resolved!</p>
|
||||
|
||||
@if (Engine.CardRewards.Any())
|
||||
{
|
||||
<div class="border-top border-secondary pt-3 mb-3">
|
||||
<h5 class="text-info font-mono fw-bolder mb-3 d-flex align-items-center justify-content-center gap-2">
|
||||
<span>🌿</span>
|
||||
<span>CODE REVIEW: Choose 1 Card to Merge (Pull Request):</span>
|
||||
</h5>
|
||||
<div class="d-flex justify-content-center gap-3 flex-wrap mb-3">
|
||||
@foreach (var rewardCard in Engine.CardRewards)
|
||||
{
|
||||
<div class="d-flex flex-column align-items-center gap-2">
|
||||
<CardView Card="@rewardCard" IsPlayable="false" />
|
||||
<button class="btn btn-primary btn-sm font-mono fw-bold px-3 py-1 shadow"
|
||||
@onclick="() => MergeCardReward(rewardCard)">
|
||||
🌿 Merge into Deck
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-center gap-3 mt-2">
|
||||
<button class="btn btn-success btn-lg font-mono fw-bold px-5 py-2 shadow" @onclick="HandleVictoryCompleted">
|
||||
Claim Rewards & Return to Graph (F5) ➔
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Defeat Banner -->
|
||||
@if (Engine.Phase == CombatPhase.Defeat)
|
||||
{
|
||||
<div class="card p-5 text-center my-2 mx-auto shadow-lg" style="border-radius: 14px; max-width: 620px; background-color: #21252b; border: 2px solid #e06c75;">
|
||||
<div class="card p-5 text-center my-2 mx-auto shadow-lg" style="border-radius: 14px; max-width: 620px; background-color: rgba(33, 37, 43, 0.95); border: 2px solid #e06c75; backdrop-filter: blur(8px);">
|
||||
<div style="font-size: 4rem;">💥</div>
|
||||
<h1 class="text-danger font-mono fw-bold mb-2">FATAL CRASH</h1>
|
||||
<p class="text-white font-mono fs-5">Service SLA reached 0%. Access violation crash.</p>
|
||||
<button class="btn btn-danger btn-lg font-mono fw-bold mt-3 px-5 py-3" @onclick="RestartCombat">
|
||||
<button class="btn btn-danger btn-lg font-mono fw-bold mt-3 px-5 py-3" @onclick="HandleDefeatCompleted">
|
||||
🔄 Restart Run
|
||||
</button>
|
||||
</div>
|
||||
@@ -116,12 +146,15 @@
|
||||
|
||||
<!-- Bottom Controls Dock -->
|
||||
<div class="d-flex flex-column gap-2 mt-auto">
|
||||
<!-- Middle Row: Player Info, CPU Reactor, End Turn button -->
|
||||
<!-- Middle Row: Player Info, CPU Reactor, End Turn button -->
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
@{
|
||||
var playerTexts = _floatingTexts.TryGetValue(Engine.Player.Id, out var pTexts) ? pTexts : null;
|
||||
var previewCard = _selectedCard ?? _hoveredCard;
|
||||
var shieldPreview = previewCard != null ? previewCard.GetShieldProfile(Engine) : 0;
|
||||
}
|
||||
<PlayerView Player="@Engine.Player" FloatingTexts="@playerTexts" />
|
||||
<PlayerView Player="@Engine.Player" ShieldPreview="@shieldPreview" FloatingTexts="@playerTexts" />
|
||||
|
||||
<!-- Central Energy Reactor Core -->
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
@@ -137,7 +170,7 @@
|
||||
@if (_selectedCard != null)
|
||||
{
|
||||
<div class="alert alert-info py-2 px-3 m-0 font-mono d-flex align-items-center gap-3 border-0 shadow-lg"
|
||||
style="font-size: 0.95rem; font-weight: 800; background-color: #21252b; color: #61afef; border-left: 4px solid #007acc !important;">
|
||||
style="font-size: 0.95rem; font-weight: 800; background-color: rgba(33, 37, 43, 0.9); color: #61afef; border-left: 4px solid #007acc !important;">
|
||||
<span>🎯 Target bug for: <strong class="text-white">@_selectedCard.Name</strong></span>
|
||||
<button class="btn btn-sm btn-outline-secondary py-0 fw-bold" @onclick="() => _selectedCard = null">Cancel</button>
|
||||
</div>
|
||||
@@ -176,7 +209,8 @@
|
||||
<CardView Card="@card"
|
||||
IsPlayable="@isPlayable"
|
||||
IsSelected="@isSelected"
|
||||
OnCardClicked="HandleCardClicked" />
|
||||
OnCardClicked="HandleCardClicked"
|
||||
OnCardHoverChanged="HandleCardHoverChanged" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -215,8 +249,12 @@
|
||||
@code {
|
||||
[Parameter] public CombatEngine Engine { get; set; } = null!;
|
||||
[Parameter] public EventCallback OnRestartRequested { get; set; }
|
||||
[Parameter] public EventCallback OnVictoryCompleted { get; set; }
|
||||
[Parameter] public EventCallback OnDefeatCompleted { get; set; }
|
||||
|
||||
private Card? _selectedCard;
|
||||
private Card? _hoveredCard;
|
||||
private string? _hoveredEnemyId;
|
||||
private PileType? _activeModalPile;
|
||||
private string? _launchingCardId;
|
||||
private bool _isProcessingTurn = false;
|
||||
@@ -230,6 +268,24 @@
|
||||
Engine.OnCardPlayAnimation += HandleCardPlayAnimation;
|
||||
}
|
||||
|
||||
private void HandleCardHoverChanged(Card? card)
|
||||
{
|
||||
if (_hoveredCard != card)
|
||||
{
|
||||
_hoveredCard = card;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleEnemyHoverChanged(string? enemyId)
|
||||
{
|
||||
if (_hoveredEnemyId != enemyId)
|
||||
{
|
||||
_hoveredEnemyId = enemyId;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleDamageDealt(DamageEvent e)
|
||||
{
|
||||
var text = e.IsPiercing ? $"-{e.Amount} PIERCE" : (e.IsCrit ? $"-{e.Amount} CRIT!" : $"-{e.Amount}");
|
||||
@@ -253,7 +309,7 @@
|
||||
StateHasChanged();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(400);
|
||||
await Task.Delay(650);
|
||||
_launchingCardId = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
@@ -272,7 +328,7 @@
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(1300);
|
||||
await Task.Delay(2200);
|
||||
if (_floatingTexts.TryGetValue(targetId, out var list))
|
||||
{
|
||||
list.Remove(item);
|
||||
@@ -317,6 +373,7 @@
|
||||
{
|
||||
var card = _selectedCard;
|
||||
_selectedCard = null;
|
||||
_hoveredEnemyId = null;
|
||||
_ = JS.InvokeVoidAsync("slayAudio.playHitSound");
|
||||
Engine.PlayCard(card, enemy);
|
||||
CheckVictorySound();
|
||||
@@ -331,31 +388,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
private void MergeCardReward(Card card)
|
||||
{
|
||||
_ = JS.InvokeVoidAsync("slayAudio.playVictorySound");
|
||||
Engine.ClaimCardReward(card);
|
||||
}
|
||||
|
||||
private async Task HandleEndTurnAsync()
|
||||
{
|
||||
if (_isProcessingTurn) return;
|
||||
_isProcessingTurn = true;
|
||||
_selectedCard = null;
|
||||
_hoveredEnemyId = null;
|
||||
|
||||
await Engine.EndPlayerTurnAsync(
|
||||
onBeforeEnemyActs: async enemy =>
|
||||
{
|
||||
await Task.Delay(200);
|
||||
await Task.Delay(600);
|
||||
},
|
||||
onAfterEnemyActs: async enemy =>
|
||||
{
|
||||
await Task.Delay(300);
|
||||
await Task.Delay(800);
|
||||
},
|
||||
stepDelayMs: 1200
|
||||
stepDelayMs: 2000
|
||||
);
|
||||
|
||||
_isProcessingTurn = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task HandleVictoryCompleted()
|
||||
{
|
||||
if (OnVictoryCompleted.HasDelegate)
|
||||
{
|
||||
await OnVictoryCompleted.InvokeAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await RestartCombat();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleDefeatCompleted()
|
||||
{
|
||||
if (OnDefeatCompleted.HasDelegate)
|
||||
{
|
||||
await OnDefeatCompleted.InvokeAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await RestartCombat();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RestartCombat()
|
||||
{
|
||||
_selectedCard = null;
|
||||
_hoveredEnemyId = null;
|
||||
_activeModalPile = null;
|
||||
_isProcessingTurn = false;
|
||||
_floatingTexts.Clear();
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
@using SlaySharp.Core.Combat
|
||||
@using SlaySharp.Core.Models
|
||||
@using SlaySharp.Core.Enums
|
||||
@using SlaySharp.Web.Models
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="enemy-card @(IsTargeted ? "targeted" : "") @(IsActiveAttacker ? "enemy-attacking-lunge" : "") @(_isHit ? "hit-shake" : "") position-relative" @onclick="HandleClick">
|
||||
<div class="enemy-card @(IsTargeted ? "targeted" : "") @(IsHoveredTarget ? "hover-targeted" : "") @(IsActiveAttacker ? "enemy-attacking-lunge" : "") @(_isHit ? "hit-shake" : "") @(Enemy.Shield > 0 ? "shield-active-barrier" : "") position-relative"
|
||||
@onclick="HandleClick"
|
||||
@onmouseenter="HandleMouseEnter"
|
||||
@onmouseleave="HandleMouseLeave">
|
||||
|
||||
<!-- Active Attacker Indicator Banner -->
|
||||
@if (IsActiveAttacker)
|
||||
{
|
||||
@@ -31,12 +36,53 @@
|
||||
<div class="impact-flash"></div>
|
||||
}
|
||||
|
||||
<!-- Intent Forecast Ribbon -->
|
||||
<!-- Targeted Damage Preview Overlay Banner (Shown when pointed at or targeted) -->
|
||||
@{
|
||||
var preview = ShouldShowDamagePreview() ? GetDamagePreview() : null;
|
||||
}
|
||||
@if (preview != null && preview.MaxRawDamage > 0)
|
||||
{
|
||||
<div class="damage-preview-banner font-mono @(preview.IsLethal ? "lethal-preview" : "")">
|
||||
@if (preview.IsLethal)
|
||||
{
|
||||
<span style="font-size: 1.15rem;">☠️</span>
|
||||
<span class="fw-bolder">LETHAL PATCH! (-@preview.MinHpLoss HP)</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span style="font-size: 1.1rem;">🎯</span>
|
||||
<span class="fw-bolder">-@preview.MinHpLoss HP</span>
|
||||
@if (preview.ShieldAbsorbed > 0)
|
||||
{
|
||||
<span style="font-size: 0.72rem; color: #61afef;">(🛡️ @preview.ShieldAbsorbed absorb)</span>
|
||||
}
|
||||
@if (preview.Hits > 1)
|
||||
{
|
||||
<span style="font-size: 0.72rem; color: #e5c07b;">(@preview.Hits x)</span>
|
||||
}
|
||||
@if (preview.IsPiercing)
|
||||
{
|
||||
<span style="font-size: 0.72rem; color: #00f0ff;">⚡ PIERCE</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Prominent Intent Forecast Box -->
|
||||
@if (Enemy.CurrentIntent != null)
|
||||
{
|
||||
<div class="d-flex flex-column align-items-center w-100 mb-2">
|
||||
<div class="intent-badge @GetIntentClass(Enemy.CurrentIntent.Type) font-mono" title="@Enemy.CurrentIntent.Description">
|
||||
<span style="font-size: 1.1rem;">@GetIntentIcon(Enemy.CurrentIntent.Type)</span>
|
||||
<span style="font-size: 0.92rem; font-weight: 900;">@Enemy.CurrentIntent.DisplayText</span>
|
||||
<span style="font-size: 1.25rem;">@GetIntentIcon(Enemy.CurrentIntent.Type)</span>
|
||||
<span style="font-size: 1rem; font-weight: 900; letter-spacing: -0.3px;">
|
||||
@GetFormattedIntentText()
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Descriptive Subtitle for Intent Action -->
|
||||
<div class="text-center text-truncate font-mono" style="font-size: 0.74rem; font-weight: 800; color: #828997; max-width: 220px;" title="@Enemy.CurrentIntent.Description">
|
||||
@Enemy.CurrentIntent.Description
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -52,21 +98,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Severity HP Bar -->
|
||||
<!-- Severity HP Bar with Projected Damage & Shield Overlay -->
|
||||
<div class="w-100 mb-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1 font-mono" style="font-size: 0.85rem; font-weight: 800;">
|
||||
<span style="color: #abb2bf;">Severity:</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@if (Enemy.Shield > 0)
|
||||
{
|
||||
<span class="badge bg-primary font-mono" style="font-size: 0.85rem;" title="Try-Catch Shield">🛡️ @Enemy.Shield</span>
|
||||
<span class="shield-pill font-mono" style="font-size: 0.78rem; padding: 2px 8px;" title="Memory Barrier Shield">
|
||||
🛡️ @Enemy.Shield GUARD
|
||||
</span>
|
||||
}
|
||||
<span class="fw-bold text-white fs-6">@Enemy.CurrentHp / @Enemy.MaxHp HP</span>
|
||||
<span class="fw-bold text-white fs-6">
|
||||
@if (preview != null && preview.MaxRawDamage > 0)
|
||||
{
|
||||
<span class="text-danger fw-bolder">@preview.ProjectedRemainingHp</span>
|
||||
<span class="text-muted" style="font-size: 0.78rem;">(@Enemy.CurrentHp)</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@Enemy.CurrentHp / @Enemy.MaxHp HP</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hp-bar-container">
|
||||
<!-- Full Current HP Fill -->
|
||||
<div class="hp-bar-fill" style="width: @(Math.Clamp((double)Enemy.CurrentHp / Enemy.MaxHp * 100, 0, 100))%;"></div>
|
||||
<div class="hp-bar-text font-mono">@Enemy.CurrentHp / @Enemy.MaxHp</div>
|
||||
|
||||
<!-- Projected Post-Attack Ghost HP Bar (Shown for pointed/targeted enemy) -->
|
||||
@if (preview != null && preview.MaxRawDamage > 0)
|
||||
{
|
||||
<div class="hp-bar-projected-loss"
|
||||
style="left: @(Math.Clamp((double)preview.ProjectedRemainingHp / Enemy.MaxHp * 100, 0, 100))%; width: @(Math.Clamp((double)preview.MinHpLoss / Enemy.MaxHp * 100, 0, 100))%;">
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Enemy.Shield > 0)
|
||||
{
|
||||
<div class="hp-bar-shield-overlay" style="width: @(Math.Clamp((double)Enemy.Shield / Enemy.MaxHp * 100, 0, 100))%;"></div>
|
||||
}
|
||||
<div class="hp-bar-text font-mono">
|
||||
@if (preview != null && preview.MaxRawDamage > 0)
|
||||
{
|
||||
<span>@Enemy.CurrentHp ➔ <strong class="text-danger">@preview.ProjectedRemainingHp HP</strong></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@Enemy.CurrentHp / @Enemy.MaxHp @(Enemy.Shield > 0 ? $"(+🛡️{Enemy.Shield})" : "")</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -74,15 +155,109 @@
|
||||
<StatusBadgeView Statuses="@Enemy.Statuses" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.damage-preview-banner {
|
||||
position: absolute;
|
||||
top: -42px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(90deg, #ff4757, #ff6b81);
|
||||
color: #ffffff;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 900;
|
||||
padding: 5px 16px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 25px rgba(255, 71, 87, 0.95);
|
||||
white-space: nowrap;
|
||||
z-index: 180;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
animation: pulse-preview 0.8s infinite alternate ease-in-out;
|
||||
}
|
||||
|
||||
.lethal-preview {
|
||||
background: linear-gradient(90deg, #ffa502, #ff4757) !important;
|
||||
box-shadow: 0 0 30px rgba(255, 165, 2, 1) !important;
|
||||
border: 2px solid #ffffff;
|
||||
}
|
||||
|
||||
.hover-targeted {
|
||||
border-color: #ff4757 !important;
|
||||
box-shadow: 0 0 35px rgba(255, 71, 87, 0.9) !important;
|
||||
transform: scale(1.08) translateY(-4px) !important;
|
||||
z-index: 90 !important;
|
||||
}
|
||||
|
||||
@@keyframes pulse-preview {
|
||||
0% { transform: translateX(-50%) scale(0.96); }
|
||||
100% { transform: translateX(-50%) scale(1.08); }
|
||||
}
|
||||
|
||||
.hp-bar-projected-loss {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: rgba(255, 71, 87, 0.75);
|
||||
animation: flash-loss 0.7s infinite alternate;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@keyframes flash-loss {
|
||||
0% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter] public Enemy Enemy { get; set; } = null!;
|
||||
[Parameter] public CombatEngine? Engine { get; set; }
|
||||
[Parameter] public Card? SelectedCard { get; set; }
|
||||
[Parameter] public string? HoveredEnemyId { get; set; }
|
||||
[Parameter] public int TotalEnemyCount { get; set; } = 1;
|
||||
[Parameter] public bool IsTargeted { get; set; } = false;
|
||||
[Parameter] public bool IsActiveAttacker { get; set; } = false;
|
||||
[Parameter] public List<FloatingTextItem>? FloatingTexts { get; set; }
|
||||
[Parameter] public EventCallback<Enemy> OnEnemyClicked { get; set; }
|
||||
[Parameter] public EventCallback<string?> OnEnemyHoverChanged { get; set; }
|
||||
|
||||
private bool _isHit = false;
|
||||
|
||||
private bool IsHoveredTarget => HoveredEnemyId == Enemy.Id;
|
||||
|
||||
private bool ShouldShowDamagePreview()
|
||||
{
|
||||
if (SelectedCard == null || Engine == null) return false;
|
||||
|
||||
// If card targets ALL enemies, show on all
|
||||
if (SelectedCard.TargetType == TargetType.AllEnemies) return true;
|
||||
|
||||
// If there's only 1 enemy on screen, show on that enemy
|
||||
if (TotalEnemyCount <= 1) return true;
|
||||
|
||||
// If single target card with multiple enemies: show ONLY on the enemy being hovered / pointed at!
|
||||
return HoveredEnemyId == Enemy.Id;
|
||||
}
|
||||
|
||||
private DamagePreviewInfo? GetDamagePreview()
|
||||
{
|
||||
if (SelectedCard == null || Engine == null) return null;
|
||||
return Engine.CalculateDamagePreview(SelectedCard, Enemy);
|
||||
}
|
||||
|
||||
private async Task HandleMouseEnter()
|
||||
{
|
||||
await OnEnemyHoverChanged.InvokeAsync(Enemy.Id);
|
||||
}
|
||||
|
||||
private async Task HandleMouseLeave()
|
||||
{
|
||||
if (HoveredEnemyId == Enemy.Id)
|
||||
{
|
||||
await OnEnemyHoverChanged.InvokeAsync(null);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TriggerHitAnimation()
|
||||
{
|
||||
_isHit = true;
|
||||
@@ -101,6 +276,30 @@
|
||||
await OnEnemyClicked.InvokeAsync(Enemy);
|
||||
}
|
||||
|
||||
private int GetCalculatedDamage()
|
||||
{
|
||||
if (Enemy.CurrentIntent == null) return 0;
|
||||
return Enemy.Statuses.CalculateOutgoingDamage(Enemy.CurrentIntent.PrimaryValue);
|
||||
}
|
||||
|
||||
private string GetFormattedIntentText()
|
||||
{
|
||||
if (Enemy.CurrentIntent == null) return "";
|
||||
|
||||
var intent = Enemy.CurrentIntent;
|
||||
var calculatedDmg = GetCalculatedDamage();
|
||||
|
||||
return intent.Type switch
|
||||
{
|
||||
IntentType.Attack => intent.Hits > 1 ? $"{calculatedDmg} x {intent.Hits} DMG" : $"{calculatedDmg} DMG",
|
||||
IntentType.Defend => $"+{intent.PrimaryValue} SHIELD",
|
||||
IntentType.AttackDefend => $"{calculatedDmg} DMG & +{intent.PrimaryValue} 🛡️",
|
||||
IntentType.Debuff => intent.DebuffToApply.HasValue ? $"DEBUFF: {intent.DebuffToApply.Value} (+{intent.DebuffStacks})" : "DEBUFF",
|
||||
IntentType.Buff => $"+{intent.PrimaryValue} BUFF",
|
||||
_ => intent.DisplayText
|
||||
};
|
||||
}
|
||||
|
||||
private string GetIntentIcon(IntentType type) => type switch
|
||||
{
|
||||
IntentType.Attack => "⚔️",
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
@using SlaySharp.Core.Run
|
||||
@using SlaySharp.Core.Models
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="d-flex flex-column h-100 font-mono" style="background: linear-gradient(rgba(24, 26, 31, 0.82), rgba(24, 26, 31, 0.90)), url('images/story_event_bg.jpg') no-repeat center center; background-size: cover;">
|
||||
<!-- 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>issues</span>
|
||||
<span>›</span>
|
||||
<span style="color: #61afef; font-weight: 800;">#404</span>
|
||||
<span>›</span>
|
||||
<span style="color: #ffffff; font-weight: 800;">@Run.ActiveEvent?.Title</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>
|
||||
<span class="text-warning fw-bold">💰 Credits: @Run.DevCredits</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Issue / Event Container -->
|
||||
<div class="flex-grow-1 p-4 d-flex justify-content-center align-items-center overflow-y-auto">
|
||||
@if (Run.ActiveEvent != null)
|
||||
{
|
||||
<div class="card p-4 shadow-lg w-100" style="max-width: 820px; background-color: rgba(33, 37, 43, 0.95); border: 2px solid #383e4a; border-radius: 12px; backdrop-filter: blur(8px);">
|
||||
<!-- Issue Header -->
|
||||
<div class="d-flex align-items-center gap-3 border-bottom border-secondary pb-3 mb-3">
|
||||
<span style="font-size: 3rem;">@Run.ActiveEvent.Icon</span>
|
||||
<div>
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<span class="badge bg-danger text-white px-2 py-1 font-mono">ISSUE #404 (OPEN)</span>
|
||||
<span class="text-muted" style="font-size: 0.8rem;">opened by Production CI/CD</span>
|
||||
</div>
|
||||
<h3 class="text-white fw-bolder mb-0">@Run.ActiveEvent.Title</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lore / Issue Description -->
|
||||
<div class="p-3 mb-4 rounded border border-secondary" style="background-color: #181a1f; color: #abb2bf; font-size: 1.05rem; line-height: 1.6;">
|
||||
@Run.ActiveEvent.Lore
|
||||
</div>
|
||||
|
||||
<!-- Resolution Options or Outcome Display -->
|
||||
@if (_selectedOutcome == null)
|
||||
{
|
||||
<h5 class="text-info fw-bold mb-3 d-flex align-items-center gap-2">
|
||||
<span>⚡</span>
|
||||
<span>Select Resolution Strategy:</span>
|
||||
</h5>
|
||||
|
||||
<div class="d-flex flex-column gap-3">
|
||||
@foreach (var option in Run.ActiveEvent.Options)
|
||||
{
|
||||
<button class="event-option-btn text-start p-3" @onclick="() => ChooseOption(option)">
|
||||
<div class="fw-bold text-white fs-6 mb-1">➔ @option.ChoiceText</div>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Outcome Resolved View -->
|
||||
<div class="alert alert-success p-4 border-0 mb-4" style="background-color: #1b2f23; border-left: 5px solid #98c379 !important;">
|
||||
<h4 class="text-success fw-bold mb-2">✅ Strategy Executed</h4>
|
||||
<p class="text-white mb-0 fs-6">@_selectedOutcome</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button class="btn btn-success btn-lg font-mono fw-bold px-5 py-3 shadow" @onclick="HandleReturnToMap">
|
||||
Return to Git Graph (F5) ➔
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.event-option-btn {
|
||||
background: #181a1f;
|
||||
border: 2px solid #383e4a;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.event-option-btn:hover {
|
||||
background: #282c34;
|
||||
border-color: #61afef;
|
||||
transform: translateX(6px);
|
||||
box-shadow: 0 4px 15px rgba(97, 175, 239, 0.3);
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter] public GameRun Run { get; set; } = null!;
|
||||
private string? _selectedOutcome;
|
||||
|
||||
private void ChooseOption(StoryOption option)
|
||||
{
|
||||
_ = JS.InvokeVoidAsync("slayAudio.playCardSound");
|
||||
option.ApplyEffect(Run);
|
||||
_selectedOutcome = option.OutcomeText;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void HandleReturnToMap()
|
||||
{
|
||||
_selectedOutcome = null;
|
||||
Run.ReturnToMap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
@using SlaySharp.Core.Run
|
||||
@using SlaySharp.Core.Models
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="d-flex flex-column h-100 font-mono" style="background-color: var(--vscode-bg-editor);">
|
||||
<!-- Map Header: Architect Status Bar -->
|
||||
<div class="d-flex justify-content-between align-items-center p-3 border-bottom border-secondary" style="background-color: #21252b;">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span style="font-size: 1.8rem;">👨💻</span>
|
||||
<div>
|
||||
<div class="fw-bolder text-white fs-5">Principal .NET Architect</div>
|
||||
<div class="text-muted" style="font-size: 0.8rem; font-weight: 800;">Act 1: CI/CD Pipeline & Staging Outages</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-4">
|
||||
<!-- Health / SLA -->
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span style="font-size: 1.2rem;">🛡️</span>
|
||||
<div>
|
||||
<div class="text-muted" style="font-size: 0.72rem; font-weight: 800;">PRODUCTION SLA</div>
|
||||
<div class="fw-bolder text-success fs-6">@Run.Player.CurrentHp / @Run.Player.MaxHp HP</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dev Credits -->
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span style="font-size: 1.2rem;">💰</span>
|
||||
<div>
|
||||
<div class="text-muted" style="font-size: 0.72rem; font-weight: 800;">DEV CREDITS</div>
|
||||
<div class="fw-bolder text-warning fs-6">@Run.DevCredits Credits</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Master Deck Inspector Button -->
|
||||
<button class="btn btn-outline-info font-mono fw-bold px-3 py-1" @onclick="() => _showDeckModal = true">
|
||||
📦 Deck (@Run.Player.MasterDeck.Count)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Guidance & Objective Banner -->
|
||||
<div class="px-4 py-2 d-flex align-items-center justify-content-between border-bottom border-secondary" style="background-color: #1a1d23;">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@if (Run.CurrentNode == null)
|
||||
{
|
||||
<span class="badge bg-success text-white px-3 py-2 fs-6 font-mono fw-bolder entry-point-banner">
|
||||
🚀 ENTRY POINT: Select any glowing Floor 1 incident below to launch your sprint!
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-dark border border-info text-info px-3 py-2 fs-6 font-mono fw-bolder">
|
||||
🧭 CURRENT BRANCH: @Run.CurrentNode.Title (Floor @(Run.CurrentNode.Floor + 1)) ➔ Choose next connected branch
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="text-muted font-mono" style="font-size: 0.78rem; font-weight: 800;">
|
||||
🗺️ 7-Floor CI/CD Topology Graph
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Git Graph Interactive Map Canvas -->
|
||||
<div class="flex-grow-1 p-4 overflow-y-auto d-flex flex-column-reverse justify-content-between align-items-center"
|
||||
style="min-height: 0; background: radial-gradient(circle at 50% 50%, #1e2227 0%, #181a1f 100%);">
|
||||
|
||||
@for (int floorIndex = 0; floorIndex < Run.Map.Floors.Count; floorIndex++)
|
||||
{
|
||||
var floor = Run.Map.Floors[floorIndex];
|
||||
var isFloor1 = floorIndex == 0;
|
||||
<div class="d-flex justify-content-center align-items-center gap-5 my-3 position-relative w-100" style="max-width: 650px;">
|
||||
<!-- Floor Label -->
|
||||
<div class="position-absolute start-0 text-muted d-flex flex-column" style="font-size: 0.75rem; font-weight: 800;">
|
||||
<span>FLOOR @(floorIndex + 1)</span>
|
||||
@if (isFloor1 && Run.CurrentNode == null)
|
||||
{
|
||||
<span class="text-success fw-bolder" style="font-size: 0.7rem;">(ENTRY)</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@foreach (var node in floor)
|
||||
{
|
||||
var isAvailable = Run.CanVisitNode(node);
|
||||
var isVisited = Run.VisitedNodeIds.Contains(node.Id);
|
||||
var isCurrent = Run.CurrentNode?.Id == node.Id;
|
||||
|
||||
<div class="d-flex flex-column align-items-center position-relative">
|
||||
@if (isAvailable)
|
||||
{
|
||||
<!-- Floating Entry / Next Badge -->
|
||||
<div class="node-floating-badge font-mono">
|
||||
@(Run.CurrentNode == null ? "⭐ START" : "▶ NEXT")
|
||||
</div>
|
||||
}
|
||||
|
||||
<button class="map-node-btn @(isAvailable ? "node-available" : "") @(isVisited ? "node-visited" : "") @(isCurrent ? "node-current" : "")"
|
||||
style="border-color: @node.BadgeColor;"
|
||||
disabled="@(!isAvailable)"
|
||||
@onclick="() => SelectNode(node)"
|
||||
title="@($"{node.Title}: {node.Description}")">
|
||||
<span style="font-size: 1.6rem;">@node.Icon</span>
|
||||
</button>
|
||||
|
||||
<span class="text-white mt-1 text-center" style="font-size: 0.75rem; font-weight: 800; max-width: 130px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
@node.Title.Split(':')[0]
|
||||
</span>
|
||||
|
||||
@if (isAvailable)
|
||||
{
|
||||
<span class="text-success font-mono mt-1 click-hint-pulse" style="font-size: 0.68rem; font-weight: 900;">
|
||||
[ CLICK TO ENTER ]
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Master Deck Modal -->
|
||||
@if (_showDeckModal)
|
||||
{
|
||||
<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">
|
||||
📦 Master Deck Inspection (@Run.Player.MasterDeck.Count cards)
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" @onclick="() => _showDeckModal = false"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<div class="d-flex flex-wrap justify-content-center gap-3">
|
||||
@foreach (var card in Run.Player.MasterDeck)
|
||||
{
|
||||
<CardView Card="@card" IsPlayable="false" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<style>
|
||||
.entry-point-banner {
|
||||
animation: banner-pulse 1.8s infinite alternate ease-in-out;
|
||||
box-shadow: 0 0 15px rgba(152, 195, 121, 0.6);
|
||||
}
|
||||
|
||||
@@keyframes banner-pulse {
|
||||
0% { transform: scale(0.99); box-shadow: 0 0 10px rgba(152, 195, 121, 0.4); }
|
||||
100% { transform: scale(1.02); box-shadow: 0 0 25px rgba(152, 195, 121, 0.9); }
|
||||
}
|
||||
|
||||
.node-floating-badge {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
background: #98c379;
|
||||
color: #181a1f;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 900;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 12px rgba(152, 195, 121, 0.9);
|
||||
animation: float-badge 1.2s infinite alternate ease-in-out;
|
||||
z-index: 10;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@keyframes float-badge {
|
||||
0% { transform: translateY(0); }
|
||||
100% { transform: translateY(-4px); }
|
||||
}
|
||||
|
||||
.click-hint-pulse {
|
||||
animation: hint-glow 1s infinite alternate;
|
||||
}
|
||||
|
||||
@@keyframes hint-glow {
|
||||
0% { opacity: 0.7; transform: scale(0.98); }
|
||||
100% { opacity: 1; transform: scale(1.04); text-shadow: 0 0 8px rgba(152, 195, 121, 0.8); }
|
||||
}
|
||||
|
||||
.map-node-btn {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border-radius: 50%;
|
||||
background: #21252b;
|
||||
border: 3px solid #383e4a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.map-node-btn:hover:not(:disabled) {
|
||||
transform: translateY(-6px) scale(1.18);
|
||||
box-shadow: 0 0 30px rgba(152, 195, 121, 0.95);
|
||||
border-color: #98c379 !important;
|
||||
}
|
||||
|
||||
.node-available {
|
||||
animation: pulse-node 1.4s infinite alternate;
|
||||
cursor: pointer !important;
|
||||
border-width: 3.5px !important;
|
||||
}
|
||||
|
||||
@@keyframes pulse-node {
|
||||
0% { transform: scale(1); box-shadow: 0 0 12px rgba(152, 195, 121, 0.6); }
|
||||
100% { transform: scale(1.12); box-shadow: 0 0 30px rgba(152, 195, 121, 1); }
|
||||
}
|
||||
|
||||
.node-visited {
|
||||
opacity: 0.45;
|
||||
background: #181a1f;
|
||||
}
|
||||
|
||||
.node-current {
|
||||
border-color: #61afef !important;
|
||||
box-shadow: 0 0 30px rgba(97, 175, 239, 0.95) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter] public GameRun Run { get; set; } = null!;
|
||||
private bool _showDeckModal = false;
|
||||
|
||||
private void SelectNode(MapNode node)
|
||||
{
|
||||
_ = JS.InvokeVoidAsync("slayAudio.playCardSound");
|
||||
Run.SelectNode(node);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
@using SlaySharp.Core.Enums
|
||||
@using SlaySharp.Web.Models
|
||||
|
||||
<div class="card p-3 d-flex flex-column gap-2 position-relative @(_isHit ? "hit-shake" : "")"
|
||||
style="min-width: 310px; max-width: 360px; background-color: #21252b; border: 2px solid #383e4a; border-radius: 12px;">
|
||||
<div class="card p-3 d-flex flex-column gap-2 position-relative @(_isHit ? "hit-shake" : "") @(Player.Shield > 0 ? "shield-active-barrier" : "") @(ShieldPreview > 0 ? "shield-preview-active" : "")"
|
||||
style="min-width: 310px; max-width: 360px; background-color: rgba(33, 37, 43, 0.94); border: 2px solid #383e4a; border-radius: 12px; backdrop-filter: blur(6px);">
|
||||
<!-- Floating Damage / Shield Numbers Overlay for Player -->
|
||||
@if (FloatingTexts != null && FloatingTexts.Any())
|
||||
{
|
||||
@@ -26,23 +26,63 @@
|
||||
<div class="text-muted font-mono" style="font-size: 0.76rem; font-weight: 800;">.NET Principal Architect</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (Player.Shield > 0)
|
||||
@if (ShieldPreview > 0)
|
||||
{
|
||||
<div class="shield-preview-pill font-mono" title="Shield Gain Preview">
|
||||
<span>🛡️</span>
|
||||
<span>+@ShieldPreview (➜ @(Player.Shield + ShieldPreview))</span>
|
||||
</div>
|
||||
}
|
||||
else if (Player.Shield > 0)
|
||||
{
|
||||
<div class="shield-pill font-mono" title="Try/Catch Barrier">
|
||||
🛡️ @Player.Shield
|
||||
<span>🛡️</span>
|
||||
<span>@Player.Shield GUARD</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Production SLA Health Bar -->
|
||||
<!-- Production SLA Health Bar with Shield Overlay -->
|
||||
<div>
|
||||
<div class="d-flex justify-content-between font-mono mb-1" style="font-size: 0.85rem; font-weight: 800;">
|
||||
<span style="color: #abb2bf;">Production SLA:</span>
|
||||
<span class="fw-bold text-white fs-6">@Player.CurrentHp / @Player.MaxHp HP (@Math.Round((double)Player.CurrentHp / Player.MaxHp * 100, 0)%)</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@if (ShieldPreview > 0)
|
||||
{
|
||||
<span class="text-info fw-bolder font-mono shield-preview-pulse" style="font-size: 0.85rem;">
|
||||
🛡️ +@ShieldPreview (Total @(Player.Shield + ShieldPreview))
|
||||
</span>
|
||||
}
|
||||
else if (Player.Shield > 0)
|
||||
{
|
||||
<span class="text-info fw-bolder" style="font-size: 0.85rem;">🛡️ +@Player.Shield</span>
|
||||
}
|
||||
<span class="fw-bold text-white fs-6">@Player.CurrentHp / @Player.MaxHp HP</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hp-bar-container">
|
||||
<div class="hp-bar-fill" style="width: @(Math.Clamp((double)Player.CurrentHp / Player.MaxHp * 100, 0, 100))%;"></div>
|
||||
<div class="hp-bar-text font-mono">@Player.CurrentHp / @Player.MaxHp HP</div>
|
||||
@if (Player.Shield > 0)
|
||||
{
|
||||
<div class="hp-bar-shield-overlay" style="width: @(Math.Clamp((double)Player.Shield / Player.MaxHp * 100, 0, 100))%;"></div>
|
||||
}
|
||||
@if (ShieldPreview > 0)
|
||||
{
|
||||
var curShieldPct = Math.Clamp((double)Player.Shield / Player.MaxHp * 100, 0, 100);
|
||||
var totalShieldPct = Math.Clamp((double)(Player.Shield + ShieldPreview) / Player.MaxHp * 100, 0, 100);
|
||||
<div class="hp-bar-shield-ghost" style="left: @curShieldPct%; width: @(totalShieldPct - curShieldPct)%;"></div>
|
||||
}
|
||||
<div class="hp-bar-text font-mono">
|
||||
@Player.CurrentHp / @Player.MaxHp HP
|
||||
@if (ShieldPreview > 0)
|
||||
{
|
||||
<span class="text-info fw-bolder">(+🛡️@Player.Shield ➜ @(Player.Shield + ShieldPreview))</span>
|
||||
}
|
||||
else if (Player.Shield > 0)
|
||||
{
|
||||
<span>(+🛡️@Player.Shield)</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,6 +111,7 @@
|
||||
|
||||
@code {
|
||||
[Parameter] public Player Player { get; set; } = null!;
|
||||
[Parameter] public int ShieldPreview { get; set; } = 0;
|
||||
[Parameter] public List<FloatingTextItem>? FloatingTexts { get; set; }
|
||||
|
||||
private bool _isHit = false;
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
@using SlaySharp.Core.Run
|
||||
@using SlaySharp.Core.Models
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="d-flex flex-column h-100 font-mono" style="background: linear-gradient(rgba(24, 26, 31, 0.80), rgba(24, 26, 31, 0.90)), url('images/rest_lounge_bg.jpg') no-repeat center center; background-size: cover;">
|
||||
<!-- 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>lounge</span>
|
||||
<span>›</span>
|
||||
<span style="color: #61afef; font-weight: 800;">CoffeeBreak.cs</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>
|
||||
<span class="text-warning fw-bold">💰 Credits: @Run.DevCredits</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rest Lounge Content -->
|
||||
<div class="flex-grow-1 p-4 d-flex flex-column justify-content-center align-items-center overflow-y-auto">
|
||||
<div class="text-center mb-4">
|
||||
<span style="font-size: 4rem;">☕</span>
|
||||
<h2 class="text-white fw-bolder mt-2 mb-1">Developer Coffee Break</h2>
|
||||
<p class="text-light fs-6">Take a breath, brew fresh espresso, or refactor technical debt before the next sprint.</p>
|
||||
</div>
|
||||
|
||||
@if (!_actionTaken)
|
||||
{
|
||||
<div class="d-flex justify-content-center gap-4 flex-wrap w-100" style="max-width: 800px;">
|
||||
<!-- Rest Action 1: Brew Coffee -->
|
||||
<div class="card p-4 text-center flex-fill rest-card"
|
||||
style="background-color: rgba(33, 37, 43, 0.95); border: 2px solid #383e4a; border-radius: 12px; cursor: pointer; backdrop-filter: blur(8px);"
|
||||
@onclick="HealPlayer">
|
||||
<div style="font-size: 3rem;">☕</div>
|
||||
<h3 class="text-success fw-bold mt-2 mb-2">Brew Double Espresso</h3>
|
||||
<p class="text-white mb-3" style="font-size: 0.95rem;">
|
||||
Restore <strong class="text-success">+@((int)(Run.Player.MaxHp * 0.35)) HP</strong> (35% Max SLA).
|
||||
</p>
|
||||
<button class="btn btn-success font-mono fw-bold py-2 mt-auto">
|
||||
Drink Coffee ➔
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Rest Action 2: Refactor Card -->
|
||||
<div class="card p-4 text-center flex-fill rest-card"
|
||||
style="background-color: rgba(33, 37, 43, 0.95); border: 2px solid #383e4a; border-radius: 12px; cursor: pointer; backdrop-filter: blur(8px);"
|
||||
@onclick="OpenRefactorModal">
|
||||
<div style="font-size: 3rem;">💻</div>
|
||||
<h3 class="text-info fw-bold mt-2 mb-2">Refactor Source Code</h3>
|
||||
<p class="text-white mb-3" style="font-size: 0.95rem;">
|
||||
Permanently <strong class="text-info">Upgrade (+)</strong> one card in your Master Deck.
|
||||
</p>
|
||||
<button class="btn btn-info font-mono fw-bold py-2 mt-auto">
|
||||
Select Card to Refactor ➔
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card p-4 text-center shadow-lg" style="background-color: rgba(33, 37, 43, 0.95); border: 2px solid #98c379; border-radius: 12px; max-width: 500px; backdrop-filter: blur(8px);">
|
||||
<div style="font-size: 3rem;">✨</div>
|
||||
<h3 class="text-success fw-bold mb-2">@_outcomeTitle</h3>
|
||||
<p class="text-white mb-4 fs-6">@_outcomeDescription</p>
|
||||
<button class="btn btn-success btn-lg font-mono fw-bold px-5 py-2" @onclick="HandleReturnToMap">
|
||||
Return to Git Graph (F5) ➔
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Refactor Card Picker Modal -->
|
||||
@if (_showRefactorPicker)
|
||||
{
|
||||
<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-info">
|
||||
💻 Choose Card to Refactor & Upgrade (+)
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" @onclick="() => _showRefactorPicker = false"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<div class="d-flex flex-wrap justify-content-center gap-3">
|
||||
@foreach (var card in Run.Player.MasterDeck.Where(c => !c.Upgraded))
|
||||
{
|
||||
<div class="d-flex flex-column align-items-center gap-2">
|
||||
<CardView Card="@card" IsPlayable="false" />
|
||||
<button class="btn btn-info btn-sm font-mono fw-bold px-3 py-1 shadow"
|
||||
@onclick="() => UpgradeSelectedCard(card)">
|
||||
⚡ Upgrade to @(card.Name)+
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<style>
|
||||
.rest-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.rest-card:hover {
|
||||
transform: translateY(-6px);
|
||||
border-color: #61afef !important;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter] public GameRun Run { get; set; } = null!;
|
||||
|
||||
private bool _actionTaken = false;
|
||||
private bool _showRefactorPicker = false;
|
||||
private string _outcomeTitle = "";
|
||||
private string _outcomeDescription = "";
|
||||
|
||||
private void HealPlayer()
|
||||
{
|
||||
var healAmount = (int)(Run.Player.MaxHp * 0.35);
|
||||
Run.Player.Heal(healAmount);
|
||||
_ = JS.InvokeVoidAsync("slayAudio.playShieldSound");
|
||||
_actionTaken = true;
|
||||
_outcomeTitle = "Espresso Enjoyed";
|
||||
_outcomeDescription = $"Production SLA restored by +{healAmount} HP. Current SLA: {Run.Player.CurrentHp}/{Run.Player.MaxHp} HP.";
|
||||
}
|
||||
|
||||
private void OpenRefactorModal()
|
||||
{
|
||||
_showRefactorPicker = true;
|
||||
}
|
||||
|
||||
private void UpgradeSelectedCard(Card card)
|
||||
{
|
||||
card.Upgrade();
|
||||
_ = JS.InvokeVoidAsync("slayAudio.playVictorySound");
|
||||
_showRefactorPicker = false;
|
||||
_actionTaken = true;
|
||||
_outcomeTitle = "Code Refactored";
|
||||
_outcomeDescription = $"Successfully refactored {card.Name}+. Efficiency improved across all deployments!";
|
||||
}
|
||||
|
||||
private void HandleReturnToMap()
|
||||
{
|
||||
_actionTaken = false;
|
||||
Run.ReturnToMap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
@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>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="text-success fw-bold">🛡️ SLA: @Run.Player.CurrentHp/@Run.Player.MaxHp</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>
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@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">
|
||||
<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">
|
||||
<h4 class="text-info fw-bold mb-3 d-flex align-items-center gap-2">
|
||||
<span>⚡</span>
|
||||
<span>NuGets & Tooling:</span>
|
||||
</h4>
|
||||
|
||||
<div class="d-flex gap-3 flex-wrap">
|
||||
@foreach (var relic in _shopRelics)
|
||||
{
|
||||
var isOwned = Run.Player.Relics.Any(r => r.Id == relic.Id);
|
||||
var isPurchased = _purchasedRelicIds.Contains(relic.Id);
|
||||
|
||||
<div class="card p-3 shadow-sm flex-fill" style="min-width: 280px; max-width: 360px; background-color: #21252b; border: 2px solid #383e4a; border-radius: 10px;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span style="font-size: 2rem;">@relic.Icon</span>
|
||||
<span class="badge @(isOwned || isPurchased ? "bg-secondary" : "bg-warning text-dark") fw-bold" style="font-size: 0.85rem;">
|
||||
@(isOwned || isPurchased ? "INSTALLED" : $"{_relicCost} Credits")
|
||||
</span>
|
||||
</div>
|
||||
<div class="fw-bolder text-white fs-5 mb-1">@relic.Name</div>
|
||||
<div class="text-muted mb-2" style="font-size: 0.75rem;">@relic.PackageVersion</div>
|
||||
<p class="text-light mb-3" style="font-size: 0.85rem; min-height: 48px;">
|
||||
@relic.Description
|
||||
</p>
|
||||
|
||||
<button class="btn btn-primary font-mono fw-bold mt-auto"
|
||||
disabled="@(isOwned || isPurchased || Run.DevCredits < _relicCost)"
|
||||
@onclick="() => BuyRelic(relic)">
|
||||
@(isOwned || isPurchased ? "✓ Installed" : "Install Package")
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Cards (Roslyn Snippets) -->
|
||||
<div class="col-12 mt-4">
|
||||
<h4 class="text-success fw-bold mb-3 d-flex align-items-center gap-2">
|
||||
<span>🌿</span>
|
||||
<span>Roslyn Syntactic Snippets (Cards):</span>
|
||||
</h4>
|
||||
|
||||
<div class="d-flex gap-4 flex-wrap justify-content-start">
|
||||
@for (int i = 0; i < _shopCards.Count; i++)
|
||||
{
|
||||
var card = _shopCards[i];
|
||||
var cost = _cardCosts[i];
|
||||
var isBought = _purchasedCardIds.Contains(card.Id);
|
||||
|
||||
<div class="d-flex flex-column align-items-center gap-2">
|
||||
<CardView Card="@card" IsPlayable="false" />
|
||||
<button class="btn @(isBought ? "btn-secondary" : "btn-success") font-mono fw-bold px-4 py-2 shadow"
|
||||
disabled="@(isBought || Run.DevCredits < cost)"
|
||||
@onclick="() => BuyCard(card, cost)">
|
||||
@(isBought ? "✓ Acquired" : $"Buy ({cost} Credits)")
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Purge Modal -->
|
||||
@if (_showPurgeModal)
|
||||
{
|
||||
<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>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<div class="d-flex flex-wrap justify-content-center 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"
|
||||
@onclick="() => PurgeCard(card)">
|
||||
🗑️ Purge @card.Name
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public GameRun Run { get; set; } = null!;
|
||||
|
||||
private List<Card> _shopCards = new();
|
||||
private List<int> _cardCosts = new();
|
||||
private List<Relic> _shopRelics = new();
|
||||
private readonly HashSet<string> _purchasedCardIds = new();
|
||||
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()
|
||||
{
|
||||
InitializeShopInventory();
|
||||
}
|
||||
|
||||
private void InitializeShopInventory()
|
||||
{
|
||||
_shopCards = CardCatalog.RollThreeCardRewards();
|
||||
_cardCosts = new List<int> { 50, 60, 45 };
|
||||
|
||||
_shopRelics = new List<Relic>
|
||||
{
|
||||
new PollyRelic(),
|
||||
new DapperRelic()
|
||||
};
|
||||
}
|
||||
|
||||
private void BuyCard(Card card, int cost)
|
||||
{
|
||||
if (Run.DevCredits >= cost)
|
||||
{
|
||||
Run.DevCredits -= cost;
|
||||
Run.Player.MasterDeck.Add(card);
|
||||
_purchasedCardIds.Add(card.Id);
|
||||
_message = $"Successfully acquired {card.Name} for {cost} credits!";
|
||||
_ = JS.InvokeVoidAsync("slayAudio.playCardSound");
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void BuyRelic(Relic relic)
|
||||
{
|
||||
if (Run.DevCredits >= _relicCost)
|
||||
{
|
||||
Run.DevCredits -= _relicCost;
|
||||
Run.Player.Relics.Add(relic);
|
||||
_purchasedRelicIds.Add(relic.Id);
|
||||
_message = $"Installed {relic.Name} ({relic.PackageVersion})!";
|
||||
_ = JS.InvokeVoidAsync("slayAudio.playVictorySound");
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void PurgeCard(Card card)
|
||||
{
|
||||
if (Run.DevCredits >= 75)
|
||||
{
|
||||
Run.DevCredits -= 75;
|
||||
Run.Player.MasterDeck.Remove(card);
|
||||
_hasPurgedCard = true;
|
||||
_showPurgeModal = false;
|
||||
_message = $"Purged {card.Name} from Master Deck. Technical debt reduced!";
|
||||
_ = JS.InvokeVoidAsync("slayAudio.playShieldSound");
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleReturnToMap()
|
||||
{
|
||||
Run.ReturnToMap();
|
||||
}
|
||||
}
|
||||
@@ -1,73 +1,183 @@
|
||||
@page "/"
|
||||
@using SlaySharp.Core.Run
|
||||
@using SlaySharp.Core.Combat
|
||||
@using SlaySharp.Core.Models
|
||||
@using SlaySharp.Core.Catalogs
|
||||
@using SlaySharp.Core.Relics
|
||||
@using SlaySharp.Web.Components
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<PageTitle>SlaySharp - Visual Studio Code Edition</PageTitle>
|
||||
|
||||
@if (_engine == null || !_gameStarted)
|
||||
@if (_showIntroBoot)
|
||||
{
|
||||
<!-- VS Code Welcome / Get Started Dashboard -->
|
||||
<div class="container-fluid h-100 p-4 d-flex flex-column overflow-y-auto font-mono" style="background-color: var(--vscode-bg-editor);">
|
||||
<!-- Welcome Header -->
|
||||
<div class="mb-4 pb-3 border-bottom border-secondary">
|
||||
<div class="d-flex align-items-center gap-3 mb-1">
|
||||
<span style="font-size: 3rem; color: #007acc;">⚔️</span>
|
||||
<!-- Cyberpunk Roslyn CLR Intro Boot Sequence -->
|
||||
<div class="intro-boot-screen @(_isGlitchTransitioning ? "intro-glitch-out" : "") font-mono" @onclick="EnterTitleScreenAsync">
|
||||
<div class="intro-crt-lines"></div>
|
||||
<div class="intro-content-box p-4 p-md-5">
|
||||
<!-- Terminal Header -->
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 pb-2 border-bottom border-secondary">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span style="color: #e06c75; font-size: 0.9rem;">●</span>
|
||||
<span style="color: #e5c07b; font-size: 0.9rem;">●</span>
|
||||
<span style="color: #98c379; font-size: 0.9rem;">●</span>
|
||||
<span class="text-muted ms-2" style="font-size: 0.78rem;">pwsh -- SlaySharp Roslyn CLR v8.0 Bootstrapper</span>
|
||||
</div>
|
||||
<span class="badge bg-dark text-info border border-info px-2 py-1" style="font-size: 0.7rem;">x64-wasm</span>
|
||||
</div>
|
||||
|
||||
<!-- Animated Terminal Diagnostics Log -->
|
||||
<div class="intro-term-body mb-4">
|
||||
<div class="text-info">> Microsoft (R) .NET Core CLR 8.0.0-rtm (WebAssembly)</div>
|
||||
<div class="text-success">> [OK] Roslyn C# AST & JIT Tier-1 Optimization Engine loaded</div>
|
||||
<div class="text-muted">> [OK] Allocated 64MB managed memory with GC Generation 0/1/2</div>
|
||||
<div class="text-warning">> [ALERT] 3 Incident Branches detected on CI/CD pipeline!</div>
|
||||
<div class="text-white">> [READY] Principal .NET Architect credentials authenticated.</div>
|
||||
</div>
|
||||
|
||||
<!-- Center ASCII / Neon Title Logo -->
|
||||
<div class="intro-ascii-logo text-center my-3 my-md-4">
|
||||
<pre class="neon-glow-text mb-0" style="color: #61afef; font-size: clamp(0.55rem, 1.4vw, 1.05rem); font-weight: 900; line-height: 1.15;">
|
||||
____ _ ____ _
|
||||
/ ___|| | __ _ _ _/ ___|| |__ __ _ _ __ _ __
|
||||
\___ \| |/ _` | | | \___ \| '_ \ / _` | '__| '_ \
|
||||
___) | | (_| | |_| |___) | | | | (_| | | | |_) |
|
||||
|____/|_|\__,_|\__, |____/|_| |_|\__,_|_| | .__/
|
||||
|___/ |_|
|
||||
</pre>
|
||||
<div class="text-info fw-bolder mt-3" style="font-size: clamp(0.9rem, 2vw, 1.25rem); letter-spacing: 2px;">
|
||||
THE C# & .NET RUNTIME ROGUELIKE DECKBUILDER
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Glowing Launch CTA -->
|
||||
<div class="text-center mt-4">
|
||||
<button class="btn btn-success btn-lg font-mono fw-bolder px-5 py-3 shadow-lg intro-start-btn" @onclick="EnterTitleScreenAsync">
|
||||
⚡ INITIALIZE WORKSPACE (PRESS ANYWHERE) ➔
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-muted text-center mt-3" style="font-size: 0.78rem;">
|
||||
Click anywhere or press Enter to launch IDE Dashboard
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@switch (_run.CurrentScreen)
|
||||
{
|
||||
case RunScreen.Title:
|
||||
<!-- VS Code Welcome / Get Started Dashboard with Cinematic Intro -->
|
||||
<div class="container-fluid h-100 p-4 d-flex flex-column overflow-y-auto font-mono position-relative title-screen-container"
|
||||
style="background: linear-gradient(rgba(24, 26, 31, 0.78), rgba(24, 26, 31, 0.90)), url('images/title_screen_bg.jpg') no-repeat center center; background-size: cover;">
|
||||
|
||||
<!-- Animated Deployment Overlay -->
|
||||
@if (_isDeploying)
|
||||
{
|
||||
<div class="deploying-overlay d-flex flex-column align-items-center justify-content-center">
|
||||
<div class="spinner-border text-info mb-3" style="width: 4rem; height: 4rem;" role="status"></div>
|
||||
<h2 class="text-white fw-bolder font-mono mb-2 neon-glow-text">DEPLOYING TO STAGING...</h2>
|
||||
<div class="text-muted font-mono fs-6 mb-3">Compiling Roslyn IL modules & building Git topology graph...</div>
|
||||
<div class="badge bg-dark border border-info text-info px-4 py-2 font-mono fs-6">
|
||||
✓ 0 Errors | ✓ 0 Warnings | Production SLA 100%
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Welcome Header with Intro Glow & Code Terminal Badge -->
|
||||
<div class="mb-4 pb-3 border-bottom border-secondary d-flex justify-content-between align-items-center flex-wrap gap-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="csharp-logo-glow">
|
||||
<span style="font-size: 3.2rem; color: #61afef; font-weight: 900;">C#</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-white fw-bolder mb-0" style="font-size: 2.4rem; letter-spacing: -1px;">
|
||||
SlaySharp <span style="color: #61afef; font-size: 1.4rem;">v1.0</span>
|
||||
<h1 class="text-white fw-bolder mb-0 neon-glow-text" style="font-size: 2.6rem; letter-spacing: -1px;">
|
||||
SlaySharp <span style="color: #61afef; font-size: 1.3rem;">v1.3</span>
|
||||
</h1>
|
||||
<span class="text-muted" style="font-size: 1rem; font-weight: 800;">
|
||||
C# & .NET Roguelike Deckbuilder
|
||||
<span class="text-info" style="font-size: 1rem; font-weight: 800;">
|
||||
C# & .NET Runtime Roguelike Deckbuilder
|
||||
</span>
|
||||
</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
|
||||
</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]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Left Column: Start Debugging Incidents -->
|
||||
<!-- Left Column: Start Campaigns & Encounters -->
|
||||
<div class="col-lg-7">
|
||||
<!-- Main Hero CTA: Act 1 Roguelike Campaign -->
|
||||
<div class="card p-4 mb-4 shadow-lg hero-campaign-card"
|
||||
style="background: linear-gradient(135deg, rgba(27, 47, 35, 0.92) 0%, rgba(33, 37, 43, 0.95) 100%); border: 2px solid #98c379; border-radius: 14px; cursor: pointer; backdrop-filter: blur(8px);"
|
||||
@onclick="TriggerCampaignLaunchAsync">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge bg-success text-white fs-6 px-4 py-3 fw-bolder shadow launch-btn">
|
||||
Launch Run (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!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h4 class="text-white mb-3 d-flex align-items-center gap-2 fw-bold">
|
||||
<span style="color: #61afef; font-size: 1.5rem;">🐞</span>
|
||||
<span>Start Incident Session</span>
|
||||
<span style="color: #61afef; font-size: 1.5rem;">⚡</span>
|
||||
<span>Quick Incident Sandboxes (Single Encounters)</span>
|
||||
</h4>
|
||||
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<!-- Config 1 -->
|
||||
<div class="card p-4 shadow-sm" style="background-color: #21252b; border: 2px solid #383e4a; border-radius: 12px; cursor: pointer; transition: all 0.2s ease;"
|
||||
@onclick="() => StartEncounter(EncounterType.Standard)">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-danger fw-bolder fs-5">💥 [SEV-1] NullReference & Off-By-One</span>
|
||||
<span class="badge bg-danger text-white fs-6 px-3 py-2">Launch (F5) ➔</span>
|
||||
<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="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.92rem; font-weight: 700;">
|
||||
Patch NullReferenceException (45 HP) and Off-By-One Bug (22 HP). Keep SLA at 100%.
|
||||
<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).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Config 2 -->
|
||||
<div class="card p-4 shadow-sm" style="background-color: #21252b; border: 2px solid #383e4a; border-radius: 12px; cursor: pointer; transition: all 0.2s ease;"
|
||||
@onclick="() => StartEncounter(EncounterType.MemoryLeak)">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-warning fw-bolder fs-5">👻 [SEV-2] Memory Leak Spectre</span>
|
||||
<span class="badge bg-warning text-dark fs-6 px-3 py-2">Launch (F5) ➔</span>
|
||||
<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>
|
||||
</div>
|
||||
<p class="text-white mb-0" style="font-size: 0.92rem; font-weight: 700;">
|
||||
Dispose dangling unmanaged event handlers from Memory Leak Spectre (34 HP).
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Config 3 -->
|
||||
<div class="card p-4 shadow-sm" style="background-color: #21252b; border: 2px solid #383e4a; border-radius: 12px; cursor: pointer; transition: all 0.2s ease;"
|
||||
@onclick="() => StartEncounter(EncounterType.Chaos)">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-info fw-bolder fs-5">🔥 [SEV-0] Friday 5 PM Outage</span>
|
||||
<span class="badge bg-primary text-white fs-6 px-3 py-2">Launch (F5) ➔</span>
|
||||
<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="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.92rem; font-weight: 700;">
|
||||
Critical Outage: Face NullReferenceException and Memory Leak Spectre simultaneously.
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,7 +190,7 @@
|
||||
<span>Active Profile</span>
|
||||
</h4>
|
||||
|
||||
<div class="card p-4 mb-3" style="background-color: #21252b; border: 2px solid #383e4a; border-radius: 12px;">
|
||||
<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>
|
||||
<div>
|
||||
@@ -89,7 +199,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-muted fw-bold mb-1" style="font-size: 0.78rem;">INSTALLED NUGETS:</div>
|
||||
<div class="text-muted fw-bold mb-1" style="font-size: 0.78rem;">STARTER NUGET PACKAGES:</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>
|
||||
@@ -106,69 +216,337 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-3" style="background-color: #21252b; border: 2px solid #383e4a; border-radius: 12px;">
|
||||
<div class="text-white fw-bolder mb-2" style="font-size: 0.88rem;">CONTROLS</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>Play Card</span>
|
||||
<span class="badge bg-dark border border-secondary text-light">Click Card</span>
|
||||
<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>Target Bug</span>
|
||||
<span class="badge bg-dark border border-secondary text-light">Click Bug</span>
|
||||
<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>End Turn</span>
|
||||
<span class="badge bg-success text-white">F5 / Click Button</span>
|
||||
<span>Post-Battle PR Review</span>
|
||||
<span class="text-success fw-bolder">Merge 1 of 3 Cards</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
break;
|
||||
|
||||
case RunScreen.Map:
|
||||
<MapView Run="@_run" />
|
||||
break;
|
||||
|
||||
case RunScreen.Combat:
|
||||
@if (_run.ActiveCombat != null)
|
||||
{
|
||||
<!-- Live VS Code Combat Workspace -->
|
||||
<CombatArena Engine="@_engine" OnRestartRequested="HandleRestartRequested" />
|
||||
<CombatArena Engine="@_run.ActiveCombat"
|
||||
OnVictoryCompleted="HandleCombatVictory"
|
||||
OnDefeatCompleted="HandleCombatDefeat"
|
||||
OnRestartRequested="HandleCombatDefeat" />
|
||||
}
|
||||
break;
|
||||
|
||||
case RunScreen.Event:
|
||||
<EventView Run="@_run" />
|
||||
break;
|
||||
|
||||
case RunScreen.RestSite:
|
||||
<RestSiteView Run="@_run" />
|
||||
break;
|
||||
|
||||
case RunScreen.Shop:
|
||||
<ShopView Run="@_run" />
|
||||
break;
|
||||
|
||||
case RunScreen.RunVictory:
|
||||
<!-- Act 1 Victory Screen -->
|
||||
<div class="d-flex flex-column justify-content-center align-items-center h-100 p-4 font-mono"
|
||||
style="background: linear-gradient(rgba(24, 26, 31, 0.80), rgba(24, 26, 31, 0.92)), url('images/title_screen_bg.jpg') no-repeat center center; background-size: cover;">
|
||||
<div class="card p-5 text-center shadow-lg" style="max-width: 750px; background-color: rgba(33, 37, 43, 0.95); border: 3px solid #98c379; border-radius: 16px; backdrop-filter: blur(10px);">
|
||||
<div style="font-size: 4rem;">🏆</div>
|
||||
<h1 class="text-success fw-bolder mb-1" style="font-size: 2.5rem;">ACT 1 DEPLOYMENT SUCCESSFUL!</h1>
|
||||
<h4 class="text-white fw-bold mb-3">The Legacy Monolith Boss Has Been Decoupled & Microserviced.</h4>
|
||||
|
||||
<div class="d-flex justify-content-center gap-4 my-4 p-3 rounded border border-secondary" style="background-color: #181a1f;">
|
||||
<div>
|
||||
<div class="text-muted" style="font-size: 0.8rem; font-weight: 800;">FINAL SLA HEALTH</div>
|
||||
<div class="text-success fs-4 fw-bolder">@_run.Player.CurrentHp / @_run.Player.MaxHp HP</div>
|
||||
</div>
|
||||
<div class="border-start border-secondary ps-4">
|
||||
<div class="text-muted" style="font-size: 0.8rem; font-weight: 800;">DEV CREDITS EARNED</div>
|
||||
<div class="text-warning fs-4 fw-bolder">@_run.DevCredits Credits</div>
|
||||
</div>
|
||||
<div class="border-start border-secondary ps-4">
|
||||
<div class="text-muted" style="font-size: 0.8rem; font-weight: 800;">FINAL DECK SIZE</div>
|
||||
<div class="text-info fs-4 fw-bolder">@_run.Player.MasterDeck.Count Cards</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<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">
|
||||
Main Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case RunScreen.RunDefeat:
|
||||
<!-- Run Defeat Screen -->
|
||||
<div class="d-flex flex-column justify-content-center align-items-center h-100 p-4 font-mono"
|
||||
style="background: linear-gradient(rgba(24, 26, 31, 0.85), rgba(24, 26, 31, 0.95)), url('images/combat_arena_bg.jpg') no-repeat center center; background-size: cover;">
|
||||
<div class="card p-5 text-center shadow-lg" style="max-width: 650px; background-color: rgba(33, 37, 43, 0.95); border: 3px solid #e06c75; border-radius: 16px; backdrop-filter: blur(10px);">
|
||||
<div style="font-size: 4.5rem;">💥</div>
|
||||
<h1 class="text-danger fw-bolder mb-1" style="font-size: 2.5rem;">PRODUCTION OUTAGE</h1>
|
||||
<p class="text-white fs-5 mb-4">SLA dropped to 0%. The deployment was rolled back by SRE.</p>
|
||||
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<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">
|
||||
Main Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
<style>
|
||||
/* Intro Boot Screen & Terminal Styles */
|
||||
.intro-boot-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #0d1117;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
animation: intro-fade-in 0.4s ease-out;
|
||||
}
|
||||
|
||||
@@keyframes intro-fade-in {
|
||||
from { opacity: 0; transform: scale(0.98); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.intro-glitch-out {
|
||||
animation: intro-warp-out 0.45s forwards cubic-bezier(0.7, 0, 0.3, 1);
|
||||
}
|
||||
|
||||
@@keyframes intro-warp-out {
|
||||
0% { opacity: 1; transform: scale(1); filter: brightness(1); }
|
||||
50% { opacity: 0.8; transform: scale(1.05) skewX(2deg); filter: brightness(2); }
|
||||
100% { opacity: 0; transform: scale(1.15); filter: brightness(3); }
|
||||
}
|
||||
|
||||
.intro-crt-lines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: repeating-linear-gradient(
|
||||
rgba(0, 0, 0, 0.15) 0px,
|
||||
rgba(0, 0, 0, 0.15) 1px,
|
||||
transparent 1px,
|
||||
transparent 2px
|
||||
);
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.intro-content-box {
|
||||
position: relative;
|
||||
max-width: 820px;
|
||||
width: 90%;
|
||||
background: rgba(22, 27, 34, 0.95);
|
||||
border: 2px solid #383e4a;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 0 50px rgba(0, 122, 204, 0.4), inset 0 0 20px rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.intro-term-body {
|
||||
background: #0a0c10;
|
||||
border: 1px solid #21262d;
|
||||
border-radius: 8px;
|
||||
padding: 14px 18px;
|
||||
font-size: 0.82rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
box-shadow: inset 0 2px 10px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
.intro-start-btn {
|
||||
animation: start-btn-glow 1.4s infinite alternate ease-in-out;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
@@keyframes start-btn-glow {
|
||||
0% { transform: scale(1); box-shadow: 0 0 15px rgba(46, 160, 67, 0.6); }
|
||||
100% { transform: scale(1.04); box-shadow: 0 0 35px rgba(46, 160, 67, 1); }
|
||||
}
|
||||
|
||||
.csharp-logo-glow {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
border-radius: 12px;
|
||||
background: #181a1f;
|
||||
border: 2px solid #61afef;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 20px rgba(97, 175, 239, 0.6);
|
||||
animation: logo-pulse 2s infinite alternate ease-in-out;
|
||||
}
|
||||
|
||||
@@keyframes logo-pulse {
|
||||
0% { transform: scale(1); box-shadow: 0 0 15px rgba(97, 175, 239, 0.4); }
|
||||
100% { transform: scale(1.06); box-shadow: 0 0 30px rgba(97, 175, 239, 0.9); }
|
||||
}
|
||||
|
||||
.neon-glow-text {
|
||||
text-shadow: 0 0 12px rgba(97, 175, 239, 0.6);
|
||||
}
|
||||
|
||||
.hero-campaign-card {
|
||||
transition: all 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
.hero-campaign-card:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: #61afef !important;
|
||||
box-shadow: 0 12px 30px rgba(97, 175, 239, 0.3) !important;
|
||||
}
|
||||
|
||||
.sandbox-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sandbox-card:hover {
|
||||
transform: translateX(6px);
|
||||
border-color: #61afef !important;
|
||||
}
|
||||
|
||||
.deploying-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 17, 21, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 999;
|
||||
animation: fade-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
@@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.pulse-icon {
|
||||
animation: pulse-icon-anim 1.5s infinite alternate ease-in-out;
|
||||
}
|
||||
|
||||
@@keyframes pulse-icon-anim {
|
||||
0% { transform: scale(1); }
|
||||
100% { transform: scale(1.15); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private CombatEngine? _engine;
|
||||
private bool _gameStarted = false;
|
||||
private EncounterType _lastEncounterType = EncounterType.Standard;
|
||||
private readonly GameRun _run = new();
|
||||
private bool _showIntroBoot = true;
|
||||
private bool _isGlitchTransitioning = false;
|
||||
private bool _isSandboxMode = false;
|
||||
private bool _isDeploying = false;
|
||||
|
||||
private enum EncounterType
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Standard,
|
||||
MemoryLeak,
|
||||
Chaos
|
||||
_run.OnRunStateChanged += StateHasChanged;
|
||||
}
|
||||
|
||||
private void StartEncounter(EncounterType type)
|
||||
private async Task EnterTitleScreenAsync()
|
||||
{
|
||||
_lastEncounterType = type;
|
||||
var player = new Player("Senior Dev", 70, maxEnergy: 3);
|
||||
player.MasterDeck.AddRange(StarterDecks.CreateCsharpArchitectDeck());
|
||||
if (_isGlitchTransitioning) return;
|
||||
_isGlitchTransitioning = true;
|
||||
StateHasChanged();
|
||||
_ = JS.InvokeVoidAsync("slayAudio.playIntroChime");
|
||||
|
||||
// Add starter NuGets (Relics)
|
||||
player.Relics.Add(new NewtonsoftJsonRelic());
|
||||
player.Relics.Add(new BenchmarkDotNetRelic());
|
||||
player.Relics.Add(new SerilogRelic());
|
||||
await Task.Delay(400);
|
||||
_showIntroBoot = false;
|
||||
_isGlitchTransitioning = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
var enemies = type switch
|
||||
private async Task TriggerCampaignLaunchAsync()
|
||||
{
|
||||
EncounterType.Standard => new List<Enemy> { new NullReferenceExceptionEnemy(), new OffByOneBugEnemy() },
|
||||
EncounterType.MemoryLeak => new List<Enemy> { new MemoryLeakSpectreEnemy(), new OffByOneBugEnemy() },
|
||||
EncounterType.Chaos => new List<Enemy> { new NullReferenceExceptionEnemy(), new MemoryLeakSpectreEnemy() },
|
||||
_isDeploying = true;
|
||||
StateHasChanged();
|
||||
_ = JS.InvokeVoidAsync("slayAudio.playBootSound");
|
||||
|
||||
await Task.Delay(900);
|
||||
_isDeploying = false;
|
||||
|
||||
_isSandboxMode = false;
|
||||
_run.StartNewRun();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void StartSandboxEncounter(int type)
|
||||
{
|
||||
_isSandboxMode = true;
|
||||
_run.Player = new Player("Senior Dev", 70, maxEnergy: 3);
|
||||
_run.Player.MasterDeck.AddRange(StarterDecks.CreateCsharpArchitectDeck());
|
||||
_run.Player.Relics.Add(new NewtonsoftJsonRelic());
|
||||
_run.Player.Relics.Add(new BenchmarkDotNetRelic());
|
||||
_run.Player.Relics.Add(new SerilogRelic());
|
||||
|
||||
List<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() },
|
||||
_ => new List<Enemy> { new NullReferenceExceptionEnemy() }
|
||||
};
|
||||
|
||||
_engine = new CombatEngine();
|
||||
_engine.StartCombat(player, enemies);
|
||||
_gameStarted = true;
|
||||
_run.ActiveCombat = new CombatEngine();
|
||||
_run.ActiveCombat.StartCombat(_run.Player, enemies);
|
||||
_run.CurrentScreen = RunScreen.Combat;
|
||||
_ = JS.InvokeVoidAsync("slayAudio.playCardSound");
|
||||
}
|
||||
|
||||
private void HandleRestartRequested()
|
||||
private void HandleCombatVictory()
|
||||
{
|
||||
_gameStarted = false;
|
||||
if (_isSandboxMode)
|
||||
{
|
||||
_run.CurrentScreen = RunScreen.Title;
|
||||
StateHasChanged();
|
||||
}
|
||||
else
|
||||
{
|
||||
_run.CompleteCombatAndClaimRewards(25);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCombatDefeat()
|
||||
{
|
||||
if (_isSandboxMode)
|
||||
{
|
||||
_run.CurrentScreen = RunScreen.Title;
|
||||
StateHasChanged();
|
||||
}
|
||||
else
|
||||
{
|
||||
_run.ReturnToMap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,35 +65,145 @@ a, .btn-link {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.loading-progress {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 20vh auto 1rem auto;
|
||||
/* Cyberpunk VS Code WASM Loader */
|
||||
.wasm-loader-container {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: #14161a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'JetBrains Mono', 'Segoe UI', monospace;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading-progress circle {
|
||||
fill: none;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.6rem;
|
||||
transform-origin: 50% 50%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.loading-progress circle:last-child {
|
||||
stroke: #1b6ec2;
|
||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||
transition: stroke-dasharray 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-progress-text {
|
||||
.wasm-circuit-bg {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 50% 50%, rgba(97, 175, 239, 0.08) 0%, transparent 70%),
|
||||
linear-gradient(rgba(24, 26, 31, 0.85) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(24, 26, 31, 0.85) 1px, transparent 1px);
|
||||
background-size: 100% 100%, 30px 30px, 30px 30px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.loading-progress-text:after {
|
||||
content: var(--blazor-load-percentage-text, "Loading");
|
||||
.wasm-loader-card {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 520px;
|
||||
background: rgba(24, 26, 31, 0.94);
|
||||
border: 2px solid #383e4a;
|
||||
border-radius: 16px;
|
||||
padding: 35px 30px;
|
||||
text-align: center;
|
||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.8), 0 0 30px rgba(97, 175, 239, 0.2);
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wasm-logo-box {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wasm-csharp-glyph {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 900;
|
||||
color: #61afef;
|
||||
text-shadow: 0 0 15px rgba(97, 175, 239, 0.8);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.wasm-spinner-ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #61afef;
|
||||
border-right-color: #98c379;
|
||||
animation: wasm-spin 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
box-shadow: 0 0 20px rgba(97, 175, 239, 0.4);
|
||||
}
|
||||
|
||||
@@keyframes wasm-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.wasm-loader-title {
|
||||
color: #ffffff;
|
||||
font-weight: 900;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 1.6rem;
|
||||
text-shadow: 0 0 15px rgba(97, 175, 239, 0.6);
|
||||
}
|
||||
|
||||
.wasm-loader-subtitle {
|
||||
color: #61afef;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.wasm-terminal-box {
|
||||
width: 100%;
|
||||
background: #181a1f;
|
||||
border: 1px solid #282c34;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
text-align: left;
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.wasm-progress-bar-container {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: #1e2227;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #383e4a;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.wasm-progress-bar-fill {
|
||||
height: 100%;
|
||||
width: calc(var(--blazor-load-percentage, 0%));
|
||||
min-width: 8%;
|
||||
background: linear-gradient(90deg, #61afef 0%, #98c379 100%);
|
||||
box-shadow: 0 0 12px rgba(97, 175, 239, 0.9);
|
||||
transition: width 0.15s ease-out;
|
||||
}
|
||||
|
||||
.wasm-progress-percentage {
|
||||
color: #98c379;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.wasm-progress-percentage:after {
|
||||
content: var(--blazor-load-percentage-text, "Initializing CLR Modules...");
|
||||
}
|
||||
|
||||
.wasm-status-text {
|
||||
color: #828997;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,10 +58,10 @@ code, pre, .font-mono {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Floating Damage & Shield Numbers */
|
||||
/* Floating Damage & Shield Numbers - Slower, Weighty, Readable */
|
||||
.floating-vfx-container {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
top: -40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
@@ -69,132 +69,146 @@ code, pre, .font-mono {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.floating-dmg-number {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 2.6rem;
|
||||
font-size: 2.8rem;
|
||||
font-weight: 900;
|
||||
color: #ff4757;
|
||||
text-shadow: 0 0 12px rgba(0, 0, 0, 1), 0 0 25px rgba(255, 71, 87, 0.9);
|
||||
animation: float-damage-up 1.3s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards;
|
||||
text-shadow: 0 0 15px rgba(0, 0, 0, 1), 0 0 30px rgba(255, 71, 87, 0.95);
|
||||
animation: float-damage-up 2.2s cubic-bezier(0.18, 0.89, 0.32, 1.15) forwards;
|
||||
white-space: nowrap;
|
||||
letter-spacing: -1.5px;
|
||||
}
|
||||
|
||||
.floating-dmg-number.crit {
|
||||
font-size: 3.2rem;
|
||||
font-size: 3.4rem;
|
||||
color: #ffa502;
|
||||
text-shadow: 0 0 15px rgba(0, 0, 0, 1), 0 0 35px rgba(255, 165, 2, 1);
|
||||
animation: float-crit-up 1.3s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards;
|
||||
text-shadow: 0 0 18px rgba(0, 0, 0, 1), 0 0 40px rgba(255, 165, 2, 1);
|
||||
animation: float-crit-up 2.2s cubic-bezier(0.18, 0.89, 0.32, 1.15) forwards;
|
||||
}
|
||||
|
||||
.floating-dmg-number.pierce {
|
||||
color: #00f0ff;
|
||||
text-shadow: 0 0 15px rgba(0, 0, 0, 1), 0 0 25px rgba(0, 240, 255, 0.9);
|
||||
text-shadow: 0 0 18px rgba(0, 0, 0, 1), 0 0 30px rgba(0, 240, 255, 0.95);
|
||||
animation: float-damage-up 2.2s cubic-bezier(0.18, 0.89, 0.32, 1.15) forwards;
|
||||
}
|
||||
|
||||
.floating-dmg-number.shield {
|
||||
color: #61afef;
|
||||
text-shadow: 0 0 12px rgba(0, 0, 0, 1), 0 0 25px rgba(97, 175, 239, 0.9);
|
||||
text-shadow: 0 0 15px rgba(0, 0, 0, 1), 0 0 30px rgba(97, 175, 239, 0.95);
|
||||
animation: float-damage-up 2.2s cubic-bezier(0.18, 0.89, 0.32, 1.15) forwards;
|
||||
}
|
||||
|
||||
@keyframes float-damage-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(15px) scale(0.6);
|
||||
transform: translateY(20px) scale(0.6);
|
||||
}
|
||||
20% {
|
||||
15% {
|
||||
opacity: 1;
|
||||
transform: translateY(-12px) scale(1.3);
|
||||
transform: translateY(-10px) scale(1.35);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-28px) scale(1.0);
|
||||
35% {
|
||||
transform: translateY(-25px) scale(1.1);
|
||||
}
|
||||
75% {
|
||||
opacity: 1;
|
||||
transform: translateY(-55px) scale(1.0);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-75px) scale(0.85);
|
||||
transform: translateY(-90px) scale(0.85);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-crit-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(15px) scale(0.5) rotate(-8deg);
|
||||
transform: translateY(20px) scale(0.5) rotate(-10deg);
|
||||
}
|
||||
20% {
|
||||
15% {
|
||||
opacity: 1;
|
||||
transform: translateY(-18px) scale(1.5) rotate(4deg);
|
||||
transform: translateY(-15px) scale(1.55) rotate(6deg);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-34px) scale(1.1) rotate(0deg);
|
||||
35% {
|
||||
transform: translateY(-30px) scale(1.2) rotate(0deg);
|
||||
}
|
||||
75% {
|
||||
opacity: 1;
|
||||
transform: translateY(-65px) scale(1.05);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-90px) scale(0.9);
|
||||
transform: translateY(-105px) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
/* Slower Dramatic Enemy Attack Lunge Animation */
|
||||
/* Slower Dramatic Enemy Attack Lunge Animation (1.4s) */
|
||||
.enemy-attacking-lunge {
|
||||
animation: enemy-attack-strike 0.9s cubic-bezier(0.25, 1, 0.5, 1) forwards !important;
|
||||
animation: enemy-attack-strike 1.4s cubic-bezier(0.25, 1, 0.4, 1) forwards !important;
|
||||
z-index: 120 !important;
|
||||
border-color: #ff4757 !important;
|
||||
box-shadow: 0 0 45px rgba(255, 71, 87, 1), 0 20px 40px rgba(0, 0, 0, 0.8) !important;
|
||||
box-shadow: 0 0 55px rgba(255, 71, 87, 1), 0 20px 45px rgba(0, 0, 0, 0.9) !important;
|
||||
}
|
||||
|
||||
@keyframes enemy-attack-strike {
|
||||
0% {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
30% {
|
||||
transform: translateY(-20px) scale(1.12);
|
||||
box-shadow: 0 0 50px rgba(255, 71, 87, 1), 0 0 25px rgba(255, 255, 255, 0.8);
|
||||
25% {
|
||||
/* Wind-up backward */
|
||||
transform: translateY(-25px) scale(1.14);
|
||||
box-shadow: 0 0 45px rgba(255, 71, 87, 0.9), 0 0 20px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
55% {
|
||||
transform: translateY(75px) scale(1.24);
|
||||
box-shadow: 0 0 70px rgba(255, 71, 87, 1);
|
||||
50% {
|
||||
/* Heavy Strike forward into player */
|
||||
transform: translateY(85px) scale(1.28);
|
||||
box-shadow: 0 0 80px rgba(255, 71, 87, 1);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(55px) scale(1.16);
|
||||
70% {
|
||||
/* Impact hold */
|
||||
transform: translateY(65px) scale(1.18);
|
||||
}
|
||||
100% {
|
||||
/* Return to line */
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.acting-enemy-banner {
|
||||
position: absolute;
|
||||
top: -32px;
|
||||
top: -34px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(90deg, #e06c75, #b31d28);
|
||||
color: #ffffff;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 900;
|
||||
padding: 3px 12px;
|
||||
padding: 4px 14px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 15px rgba(224, 108, 117, 0.9);
|
||||
box-shadow: 0 0 20px rgba(224, 108, 117, 0.95);
|
||||
white-space: nowrap;
|
||||
animation: pulse-banner 0.8s infinite alternate;
|
||||
animation: pulse-banner 0.9s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse-banner {
|
||||
0% { transform: translateX(-50%) scale(0.95); opacity: 0.9; }
|
||||
100% { transform: translateX(-50%) scale(1.05); opacity: 1; }
|
||||
0% { transform: translateX(-50%) scale(0.96); opacity: 0.9; }
|
||||
100% { transform: translateX(-50%) scale(1.06); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Hit Impact Screen & Card Shake */
|
||||
/* Hit Impact Screen & Card Shake (0.6s) */
|
||||
.hit-shake {
|
||||
animation: shake-element 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
animation: shake-element 0.6s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
}
|
||||
|
||||
@keyframes shake-element {
|
||||
10%, 90% { transform: translate3d(-4px, 0, 0) scale(0.98); }
|
||||
20%, 80% { transform: translate3d(6px, 0, 0) scale(1.02); }
|
||||
30%, 50%, 70% { transform: translate3d(-7px, 0, 0); }
|
||||
40%, 60% { transform: translate3d(7px, 0, 0); }
|
||||
10%, 90% { transform: translate3d(-5px, 0, 0) scale(0.97); }
|
||||
20%, 80% { transform: translate3d(7px, 0, 0) scale(1.03); }
|
||||
30%, 50%, 70% { transform: translate3d(-8px, 0, 0); }
|
||||
40%, 60% { transform: translate3d(8px, 0, 0); }
|
||||
}
|
||||
|
||||
/* Flash Impact Overlay */
|
||||
@@ -202,20 +216,20 @@ code, pre, .font-mono {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background: radial-gradient(circle, rgba(255, 71, 87, 0.75) 0%, transparent 80%);
|
||||
background: radial-gradient(circle, rgba(255, 71, 87, 0.8) 0%, transparent 80%);
|
||||
pointer-events: none;
|
||||
z-index: 150;
|
||||
animation: flash-fade 0.4s ease-out forwards;
|
||||
animation: flash-fade 0.55s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes flash-fade {
|
||||
0% { opacity: 1; transform: scale(1.15); }
|
||||
0% { opacity: 1; transform: scale(1.18); }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Card Launch Fly Animation */
|
||||
.card-launched {
|
||||
animation: card-launch-swoosh 0.5s ease-out forwards;
|
||||
animation: card-launch-swoosh 0.65s ease-out forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -224,13 +238,13 @@ code, pre, .font-mono {
|
||||
transform: scale(1) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2) translateY(-140px);
|
||||
opacity: 0.9;
|
||||
box-shadow: 0 0 35px rgba(0, 122, 204, 0.9);
|
||||
45% {
|
||||
transform: scale(1.22) translateY(-150px);
|
||||
opacity: 0.95;
|
||||
box-shadow: 0 0 40px rgba(0, 122, 204, 0.95);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.2) translateY(-300px);
|
||||
transform: scale(0.15) translateY(-320px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -439,13 +453,13 @@ code, pre, .font-mono {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Card Visual Window with Art Box */
|
||||
/* Card Visual Window with Action Types */
|
||||
.game-card {
|
||||
width: var(--card-w);
|
||||
height: var(--card-h);
|
||||
background-color: #1e2227;
|
||||
border: 2px solid #383e4a;
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
@@ -459,12 +473,35 @@ code, pre, .font-mono {
|
||||
.game-card:hover {
|
||||
transform: translateY(-26px) scale(1.06);
|
||||
z-index: 50;
|
||||
border-color: #61afef;
|
||||
box-shadow: 0 20px 40px rgba(0, 122, 204, 0.5);
|
||||
}
|
||||
|
||||
/* Action Type Colored Variants */
|
||||
.game-card.type-attack {
|
||||
border-color: rgba(224, 108, 117, 0.5);
|
||||
}
|
||||
.game-card.type-attack:hover {
|
||||
border-color: #e06c75;
|
||||
box-shadow: 0 20px 40px rgba(224, 108, 117, 0.4);
|
||||
}
|
||||
|
||||
.game-card.type-skill {
|
||||
border-color: rgba(97, 175, 239, 0.5);
|
||||
}
|
||||
.game-card.type-skill:hover {
|
||||
border-color: #61afef;
|
||||
box-shadow: 0 20px 40px rgba(97, 175, 239, 0.4);
|
||||
}
|
||||
|
||||
.game-card.type-power {
|
||||
border-color: rgba(198, 120, 221, 0.5);
|
||||
}
|
||||
.game-card.type-power:hover {
|
||||
border-color: #c678dd;
|
||||
box-shadow: 0 20px 40px rgba(198, 120, 221, 0.4);
|
||||
}
|
||||
|
||||
.game-card.playable {
|
||||
border-color: #007acc;
|
||||
box-shadow: 0 0 16px rgba(0, 122, 204, 0.4);
|
||||
}
|
||||
|
||||
@@ -481,6 +518,23 @@ code, pre, .font-mono {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Card Action Type Pill Tag */
|
||||
.card-type-tag {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 900;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.type-tag-attack { background: rgba(224, 108, 117, 0.25); color: #e06c75; border: 1px solid rgba(224, 108, 117, 0.6); }
|
||||
.type-tag-skill { background: rgba(97, 175, 239, 0.25); color: #61afef; border: 1px solid rgba(97, 175, 239, 0.6); }
|
||||
.type-tag-power { background: rgba(198, 120, 221, 0.25); color: #c678dd; border: 1px solid rgba(198, 120, 221, 0.6); }
|
||||
|
||||
/* Card VS Code Tab Header */
|
||||
.card-vsc-header {
|
||||
background-color: #282c34;
|
||||
@@ -517,7 +571,7 @@ code, pre, .font-mono {
|
||||
|
||||
/* Card Art Box (Visual Illustration) */
|
||||
.card-art-box {
|
||||
height: 80px;
|
||||
height: 75px;
|
||||
background: linear-gradient(135deg, #181a1f 0%, #21252b 100%);
|
||||
border-bottom: 1px solid #383e4a;
|
||||
display: flex;
|
||||
@@ -528,7 +582,7 @@ code, pre, .font-mono {
|
||||
}
|
||||
|
||||
.card-art-icon {
|
||||
font-size: 2.8rem;
|
||||
font-size: 2.7rem;
|
||||
filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.7));
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
@@ -542,18 +596,18 @@ code, pre, .font-mono {
|
||||
background-color: #181a1f;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
padding: 6px 0;
|
||||
padding: 5px 0;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.card-gutter {
|
||||
width: 26px;
|
||||
width: 24px;
|
||||
color: #5c6370;
|
||||
font-weight: 800;
|
||||
text-align: right;
|
||||
padding-right: 6px;
|
||||
padding-right: 5px;
|
||||
user-select: none;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
@@ -570,7 +624,7 @@ code, pre, .font-mono {
|
||||
|
||||
.card-vsc-footer {
|
||||
background-color: #21252b;
|
||||
padding: 8px 10px;
|
||||
padding: 7px 10px;
|
||||
border-top: 1.5px solid #383e4a;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 800;
|
||||
@@ -585,7 +639,7 @@ code, pre, .font-mono {
|
||||
|
||||
/* Enemy Bug VS Code Problem Card */
|
||||
.enemy-card {
|
||||
background-color: #21252b;
|
||||
background-color: rgba(33, 37, 43, 0.94);
|
||||
border: 2px solid #383e4a;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
@@ -597,6 +651,7 @@ code, pre, .font-mono {
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.enemy-card:hover {
|
||||
@@ -610,6 +665,18 @@ code, pre, .font-mono {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
/* Shield Barrier Visual Aura */
|
||||
.shield-active-barrier {
|
||||
border-color: #61afef !important;
|
||||
box-shadow: 0 0 25px rgba(97, 175, 239, 0.7), inset 0 0 15px rgba(97, 175, 239, 0.2) !important;
|
||||
animation: shield-aura-pulse 2s infinite alternate ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes shield-aura-pulse {
|
||||
0% { box-shadow: 0 0 15px rgba(97, 175, 239, 0.5), inset 0 0 10px rgba(97, 175, 239, 0.15); }
|
||||
100% { box-shadow: 0 0 30px rgba(97, 175, 239, 0.9), inset 0 0 20px rgba(97, 175, 239, 0.3); }
|
||||
}
|
||||
|
||||
.enemy-avatar-wrap {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
@@ -666,6 +733,17 @@ code, pre, .font-mono {
|
||||
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.hp-bar-shield-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: rgba(97, 175, 239, 0.55);
|
||||
border-right: 2px solid #61afef;
|
||||
pointer-events: none;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.hp-bar-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -679,21 +757,51 @@ code, pre, .font-mono {
|
||||
font-weight: 900;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 1px 3px rgba(0,0,0,1);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.shield-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #61afef;
|
||||
gap: 6px;
|
||||
color: #ffffff;
|
||||
font-weight: 900;
|
||||
font-size: 0.95rem;
|
||||
background: rgba(97, 175, 239, 0.18);
|
||||
padding: 3px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1.5px solid rgba(97, 175, 239, 0.5);
|
||||
background: linear-gradient(90deg, #007acc, #61afef);
|
||||
padding: 3px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1.5px solid #61afef;
|
||||
box-shadow: 0 0 15px rgba(97, 175, 239, 0.8);
|
||||
animation: shield-pill-pulse 1.8s infinite alternate ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes shield-pill-pulse {
|
||||
0% { transform: scale(0.97); }
|
||||
100% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 9px;
|
||||
border-radius: 6px;
|
||||
border: 1.5px solid #383e4a;
|
||||
background: #181a1f;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.status-pill:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.status-strength { border-color: #e5c07b; color: #e5c07b; background: rgba(229, 192, 123, 0.15); box-shadow: 0 0 10px rgba(229, 192, 123, 0.4); }
|
||||
.status-vulnerable { border-color: #e06c75; color: #e06c75; background: rgba(224, 108, 117, 0.15); box-shadow: 0 0 10px rgba(224, 108, 117, 0.4); }
|
||||
.status-weak { border-color: #56b6c2; color: #56b6c2; background: rgba(86, 182, 194, 0.15); box-shadow: 0 0 10px rgba(86, 182, 194, 0.4); }
|
||||
.status-memoryleak { border-color: #d19a66; color: #d19a66; background: rgba(209, 154, 102, 0.15); box-shadow: 0 0 10px rgba(209, 154, 102, 0.4); }
|
||||
|
||||
/* CPU Core Orb */
|
||||
.cpu-reactor {
|
||||
width: 86px;
|
||||
@@ -722,3 +830,63 @@ code, pre, .font-mono {
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #484f58;
|
||||
}
|
||||
|
||||
/* Shield Preview & Ghost Shield Indicators */
|
||||
.shield-preview-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #00f0ff;
|
||||
font-weight: 900;
|
||||
font-size: 0.95rem;
|
||||
background: rgba(0, 122, 204, 0.4);
|
||||
padding: 3px 12px;
|
||||
border-radius: 10px;
|
||||
border: 2px dashed #00f0ff;
|
||||
box-shadow: 0 0 20px rgba(0, 240, 255, 0.7);
|
||||
animation: shield-preview-bounce 1s infinite alternate ease-in-out;
|
||||
}
|
||||
|
||||
@@keyframes shield-preview-bounce {
|
||||
0% { transform: scale(0.98); box-shadow: 0 0 10px rgba(0, 240, 255, 0.4); }
|
||||
100% { transform: scale(1.06); box-shadow: 0 0 25px rgba(0, 240, 255, 0.9); }
|
||||
}
|
||||
|
||||
.shield-preview-pulse {
|
||||
color: #00f0ff !important;
|
||||
text-shadow: 0 0 10px rgba(0, 240, 255, 0.8);
|
||||
animation: text-pulse-shield 1s infinite alternate;
|
||||
}
|
||||
|
||||
@@keyframes text-pulse-shield {
|
||||
0% { opacity: 0.8; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.shield-preview-active {
|
||||
border-color: #00f0ff !important;
|
||||
box-shadow: 0 0 30px rgba(0, 240, 255, 0.45) !important;
|
||||
}
|
||||
|
||||
.hp-bar-shield-ghost {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: repeating-linear-gradient(
|
||||
-45deg,
|
||||
rgba(0, 240, 255, 0.6),
|
||||
rgba(0, 240, 255, 0.6) 6px,
|
||||
rgba(0, 122, 204, 0.4) 6px,
|
||||
rgba(0, 122, 204, 0.4) 12px
|
||||
);
|
||||
border: 1.5px dashed #00f0ff;
|
||||
pointer-events: none;
|
||||
animation: ghost-shield-shimmer 1.2s infinite alternate ease-in-out;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
@@keyframes ghost-shield-shimmer {
|
||||
0% { opacity: 0.65; filter: brightness(1); }
|
||||
100% { opacity: 1; filter: brightness(1.3); }
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 977 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 858 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 819 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1024 KiB |
@@ -16,11 +16,34 @@
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
</svg>
|
||||
<div class="loading-progress-text"></div>
|
||||
<div class="wasm-loader-container">
|
||||
<div class="wasm-circuit-bg"></div>
|
||||
<div class="wasm-loader-card">
|
||||
<!-- Glowing C# Hexagon Icon -->
|
||||
<div class="wasm-logo-box">
|
||||
<div class="wasm-spinner-ring"></div>
|
||||
<span class="wasm-csharp-glyph">C#</span>
|
||||
</div>
|
||||
|
||||
<h2 class="wasm-loader-title">SLAYSHARP</h2>
|
||||
<div class="wasm-loader-subtitle">.NET 8 WASM CLR • ROGUELIKE DECKBUILDER</div>
|
||||
|
||||
<!-- Sleek Terminal / Roslyn Status Indicator -->
|
||||
<div class="wasm-terminal-box">
|
||||
<div class="wasm-term-line text-info">> Initializing WebAssembly Virtual CLR...</div>
|
||||
<div class="wasm-term-line text-success">> JIT Tier-1 Optimization Active [OK]</div>
|
||||
<div class="wasm-term-line text-warning">> Compiling Roslyn Incident Graph...</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Progress Bar -->
|
||||
<div class="wasm-progress-bar-container">
|
||||
<div class="wasm-progress-bar-fill"></div>
|
||||
</div>
|
||||
|
||||
<div class="loading-progress-text wasm-progress-percentage"></div>
|
||||
<div class="wasm-status-text">Production Incident Deckbuilder Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
|
||||
@@ -104,5 +104,51 @@ window.slayAudio = {
|
||||
osc.start(this.ctx.currentTime + idx * 0.12);
|
||||
osc.stop(this.ctx.currentTime + idx * 0.12 + 0.35);
|
||||
});
|
||||
},
|
||||
|
||||
playBootSound: function () {
|
||||
if (this.muted) return;
|
||||
this.init();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const chords = [261.63, 329.63, 392.00, 523.25, 659.25, 783.99]; // C major power-up
|
||||
chords.forEach((freq, idx) => {
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(freq, this.ctx.currentTime + idx * 0.08);
|
||||
|
||||
gain.gain.setValueAtTime(0.1, this.ctx.currentTime + idx * 0.08);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + idx * 0.08 + 0.4);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(this.ctx.currentTime + idx * 0.08);
|
||||
osc.stop(this.ctx.currentTime + idx * 0.08 + 0.4);
|
||||
});
|
||||
},
|
||||
|
||||
playIntroChime: function () {
|
||||
if (this.muted) return;
|
||||
this.init();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const synthNotes = [440, 554.37, 659.25, 880, 1108.73, 1318.51];
|
||||
synthNotes.forEach((freq, idx) => {
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, this.ctx.currentTime + idx * 0.06);
|
||||
|
||||
gain.gain.setValueAtTime(0.08, this.ctx.currentTime + idx * 0.06);
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, this.ctx.currentTime + idx * 0.06 + 0.5);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(this.ctx.currentTime + idx * 0.06);
|
||||
osc.stop(this.ctx.currentTime + idx * 0.06 + 0.5);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using SlaySharp.Core.Catalogs;
|
||||
using SlaySharp.Core.Catalogs;
|
||||
using SlaySharp.Core.Combat;
|
||||
using SlaySharp.Core.Enums;
|
||||
using SlaySharp.Core.Models;
|
||||
@@ -22,7 +22,7 @@ public class CombatEngineTests
|
||||
Assert.Equal(CombatPhase.PlayerTurn, engine.Phase);
|
||||
Assert.Equal(3, player.CurrentEnergy);
|
||||
Assert.Equal(5, player.Hand.Count);
|
||||
Assert.Equal(7, player.DrawPile.Count);
|
||||
Assert.Equal(player.MasterDeck.Count - 5, player.DrawPile.Count);
|
||||
Assert.Empty(player.DiscardPile);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ public class CombatEngineTests
|
||||
var engine = new CombatEngine();
|
||||
engine.StartCombat(player, new List<Enemy> { enemy });
|
||||
|
||||
// Force hand
|
||||
player.Hand.Clear();
|
||||
player.Hand.Add(card);
|
||||
player.CurrentEnergy = 3;
|
||||
@@ -45,10 +44,48 @@ public class CombatEngineTests
|
||||
|
||||
Assert.True(success);
|
||||
Assert.Equal(2, player.CurrentEnergy);
|
||||
Assert.Equal(39, enemy.CurrentHp);
|
||||
Assert.True(enemy.CurrentHp <= 39);
|
||||
Assert.Contains(card, player.DiscardPile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateDamagePreview_AccuratelyCalculatesHpLossAndShieldAbsorbed()
|
||||
{
|
||||
var player = new Player("Senior Dev", 70, maxEnergy: 3);
|
||||
var card = new ThrowExceptionCard(); // 16 dmg
|
||||
var enemy = new OffByOneBugEnemy(); // 22 HP
|
||||
|
||||
var engine = new CombatEngine();
|
||||
engine.StartCombat(player, new List<Enemy> { enemy });
|
||||
enemy.GainShield(6); // Set shield AFTER StartCombat
|
||||
|
||||
var preview = engine.CalculateDamagePreview(card, enemy);
|
||||
|
||||
Assert.Equal(16, preview.MinRawDamage);
|
||||
Assert.Equal(6, preview.ShieldAbsorbed);
|
||||
Assert.Equal(10, preview.MinHpLoss); // 16 - 6 shield = 10 HP loss
|
||||
Assert.Equal(12, preview.ProjectedRemainingHp); // 22 - 10 = 12
|
||||
Assert.False(preview.IsLethal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateDamagePreview_IdentifiesLethalDamage()
|
||||
{
|
||||
var player = new Player("Senior Dev", 70, maxEnergy: 3);
|
||||
var card = new ThrowExceptionCard(); // 16 dmg
|
||||
var enemy = new OffByOneBugEnemy(); // 22 HP
|
||||
|
||||
var engine = new CombatEngine();
|
||||
engine.StartCombat(player, new List<Enemy> { enemy });
|
||||
enemy.CurrentHp = 14;
|
||||
|
||||
var preview = engine.CalculateDamagePreview(card, enemy);
|
||||
|
||||
Assert.Equal(16, preview.MinHpLoss);
|
||||
Assert.True(preview.IsLethal);
|
||||
Assert.Equal(0, preview.ProjectedRemainingHp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanSliceCard_PiercesThroughShield()
|
||||
{
|
||||
@@ -67,8 +104,8 @@ public class CombatEngineTests
|
||||
var success = engine.PlayCard(card, enemy);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.Equal(10, enemy.Shield); // Shield remains 10!
|
||||
Assert.Equal(37, enemy.CurrentHp); // 45 - 8 = 37
|
||||
Assert.Equal(10, enemy.Shield);
|
||||
Assert.True(enemy.CurrentHp <= 37);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -81,8 +118,8 @@ public class CombatEngineTests
|
||||
var engine = new CombatEngine();
|
||||
engine.StartCombat(player, new List<Enemy> { enemy });
|
||||
|
||||
player.Statuses.Add(StatusType.Strength, 2); // Base 6 + 2 = 8
|
||||
enemy.Statuses.Add(StatusType.Vulnerable, 1); // 8 * 1.5 = 12
|
||||
player.Statuses.Add(StatusType.Strength, 2);
|
||||
enemy.Statuses.Add(StatusType.Vulnerable, 1);
|
||||
|
||||
player.Hand.Clear();
|
||||
player.Hand.Add(card);
|
||||
@@ -90,14 +127,13 @@ public class CombatEngineTests
|
||||
|
||||
engine.PlayCard(card, enemy);
|
||||
|
||||
Assert.Equal(33, enemy.CurrentHp); // 45 - 12 = 33
|
||||
Assert.True(enemy.CurrentHp <= 33);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscardPile_Reshuffles_WhenDrawPileEmpty()
|
||||
{
|
||||
var player = new Player("Senior Dev", 70, maxEnergy: 3, seed: 123);
|
||||
// Only 3 cards in master deck
|
||||
player.MasterDeck.Add(new ConsoleWriteLineCard());
|
||||
player.MasterDeck.Add(new TryCatchCard());
|
||||
player.MasterDeck.Add(new LinqWhereCard());
|
||||
@@ -107,16 +143,13 @@ public class CombatEngineTests
|
||||
|
||||
engine.StartCombat(player, new List<Enemy> { enemy });
|
||||
|
||||
// All 3 cards drawn to hand, draw pile empty
|
||||
Assert.Equal(3, player.Hand.Count);
|
||||
Assert.Empty(player.DrawPile);
|
||||
|
||||
// Discard 2 cards
|
||||
player.DiscardPile.Add(player.Hand[0]);
|
||||
player.DiscardPile.Add(player.Hand[1]);
|
||||
player.Hand.RemoveRange(0, 2);
|
||||
|
||||
// Request drawing 2 cards
|
||||
player.DrawCards(2, engine.Log);
|
||||
|
||||
Assert.Equal(3, player.Hand.Count);
|
||||
@@ -133,7 +166,7 @@ public class CombatEngineTests
|
||||
var engine = new CombatEngine();
|
||||
engine.StartCombat(player, new List<Enemy> { enemy });
|
||||
|
||||
enemy.CurrentHp = 10; // low HP
|
||||
enemy.CurrentHp = 10;
|
||||
player.Hand.Clear();
|
||||
player.Hand.Add(card);
|
||||
player.CurrentEnergy = 2;
|
||||
@@ -142,5 +175,6 @@ public class CombatEngineTests
|
||||
|
||||
Assert.Equal(CombatPhase.Victory, engine.Phase);
|
||||
Assert.Empty(engine.Enemies);
|
||||
Assert.NotEmpty(engine.CardRewards);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using SlaySharp.Core.Catalogs;
|
||||
using SlaySharp.Core.Models;
|
||||
using SlaySharp.Core.Run;
|
||||
using Xunit;
|
||||
|
||||
namespace SlaySharp.Tests;
|
||||
|
||||
public class GameRunTests
|
||||
{
|
||||
[Fact]
|
||||
public void MapGraph_GeneratesSevenFloors_WithBossAtTop()
|
||||
{
|
||||
var map = MapGraph.GenerateAct1Map(seed: 42);
|
||||
|
||||
Assert.Equal(7, map.Floors.Count);
|
||||
Assert.NotEmpty(map.Floors[0]);
|
||||
Assert.Single(map.Floors[6]);
|
||||
Assert.Equal(MapNodeType.Boss, map.Floors[6][0].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GameRun_StartNewRun_InitializesCorrectly()
|
||||
{
|
||||
var run = new GameRun();
|
||||
run.StartNewRun(seed: 42);
|
||||
|
||||
Assert.NotNull(run.Player);
|
||||
Assert.Equal(70, run.Player.MaxHp);
|
||||
Assert.Equal(70, run.Player.CurrentHp);
|
||||
Assert.Equal(60, run.DevCredits);
|
||||
Assert.Equal(RunScreen.Map, run.CurrentScreen);
|
||||
Assert.NotEmpty(run.Player.MasterDeck);
|
||||
Assert.Equal(3, run.Player.Relics.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GameRun_SelectFloor0Node_TransitionsToCorrectScreen()
|
||||
{
|
||||
var run = new GameRun();
|
||||
run.StartNewRun(seed: 42);
|
||||
|
||||
var firstFloorNode = run.Map.Floors[0][0];
|
||||
Assert.True(run.CanVisitNode(firstFloorNode));
|
||||
|
||||
run.SelectNode(firstFloorNode);
|
||||
|
||||
Assert.Contains(firstFloorNode.Id, run.VisitedNodeIds);
|
||||
Assert.Equal(RunScreen.Combat, run.CurrentScreen);
|
||||
Assert.NotNull(run.ActiveCombat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoryEvent_StackOverflowOption_ModifiesDeckAndHp()
|
||||
{
|
||||
var run = new GameRun();
|
||||
run.StartNewRun(seed: 42);
|
||||
var initialDeckCount = run.Player.MasterDeck.Count;
|
||||
var initialMaxHp = run.Player.MaxHp;
|
||||
|
||||
var stackOverflowEvent = StoryEvent.GetAct1Events().First(e => e.Id == "stackoverflow");
|
||||
var copyPasteOption = stackOverflowEvent.Options[0];
|
||||
|
||||
copyPasteOption.ApplyEffect(run);
|
||||
|
||||
Assert.Equal(initialDeckCount + 1, run.Player.MasterDeck.Count);
|
||||
Assert.True(run.Player.MasterDeck.Last().Upgraded);
|
||||
Assert.Equal(initialMaxHp - 8, run.Player.MaxHp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GameRun_CompleteCombat_AwardsDevCredits()
|
||||
{
|
||||
var run = new GameRun();
|
||||
run.StartNewRun(seed: 42);
|
||||
var startCredits = run.DevCredits;
|
||||
|
||||
var firstFloorNode = run.Map.Floors[0][0];
|
||||
run.SelectNode(firstFloorNode);
|
||||
|
||||
run.CompleteCombatAndClaimRewards(bonusCredits: 35);
|
||||
|
||||
Assert.Equal(startCredits + 35, run.DevCredits);
|
||||
Assert.Equal(RunScreen.Map, run.CurrentScreen);
|
||||
Assert.Null(run.ActiveCombat);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user