buff fedora, add elite enemies and add impact to attack

This commit is contained in:
2026-07-10 11:17:40 +02:00
parent e4db7af324
commit 37049a7c16
11 changed files with 832 additions and 114 deletions
+241 -15
View File
@@ -39,7 +39,7 @@ namespace KernelPanic.Combat
private bool _skipNextAllocateDraw;
private bool _ubuntuEmptyHandRefillUsed;
private int _fedoraCardsDiscountedThisTurn;
private int _fedoraCrashChance = 10;
private int _fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
private int _cardsPlayedThisTurn;
private int _cardsPlayedThisWave;
private int _cCardsPlayedThisTurn;
@@ -54,6 +54,7 @@ namespace KernelPanic.Combat
private int _queuedRepeatCharges;
private int _nextTurnCycleBonus;
private int _nextRacePairId = 1;
private int _activeKernelRamPenalty;
private Coroutine _phaseCoroutine;
public TurnPhase CurrentPhase => currentPhase;
@@ -71,6 +72,7 @@ namespace KernelPanic.Combat
public bool AwaitingWaveContinue => _awaitingWaveContinue;
public bool RunLost => _runLost;
public int CurrentWaveNumber => _runManager == null ? 1 : _runManager.CurrentWaveNumber;
public int ActiveKernelRamPenalty => _activeKernelRamPenalty;
public event Action StateChanged;
public event Action<string> CombatLog;
@@ -194,11 +196,11 @@ namespace KernelPanic.Combat
if (card.IsLocked)
{
Log($"{GetCardName(card)} is locked");
Log($"{GetCardName(card)} is locked; pay {EnemyArchetypeCatalog.DrmUnlockCycleCost} Cycle to unlock");
return false;
}
bool fedoraBonus = CanApplyFedoraBonus();
bool fedoraBonus = CanApplyFedoraBonus(card);
int cost = GetEffectiveCardCost(card);
if (fedoraBonus)
{
@@ -248,11 +250,13 @@ namespace KernelPanic.Combat
}
GameEvents.RaiseCardPlayed(new CardPlayedEvent(card, track));
ApplyTelemetryCardPlayed();
LogPlayIntent(card);
card.SetTargetSnapshot(CaptureTargets(card));
if (fedoraBonus)
{
bool firstFedoraBonusThisTurn = _fedoraCardsDiscountedThisTurn == 0;
_fedoraCardsDiscountedThisTurn++;
bool rawhideChargeUsed = _rawhideBonusCharges > 0;
if (rawhideChargeUsed)
@@ -262,7 +266,7 @@ namespace KernelPanic.Combat
if (!IsCrashImmune(card) && RandomRoll.RollRange(1, 100, new RollContext(_playerState)) <= _fedoraCrashChance)
{
_fedoraCrashChance = 10;
_fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
if (IsDistro("fedora") && _runConfig.DistroVersion >= 4)
{
_playerState.Cycles += 1;
@@ -276,10 +280,12 @@ namespace KernelPanic.Combat
return true;
}
_fedoraCrashChance = Mathf.Min(90, _fedoraCrashChance + 10);
_fedoraCrashChance = Mathf.Min(
CombatTuning.FedoraBleedingEdgeMaxCrashChance,
_fedoraCrashChance + CombatTuning.FedoraBleedingEdgeCrashChanceStep);
card.MarkFedoraNonCrashBonus();
_playerState.DamageMultiplierPercent = _runConfig.DistroVersion >= 2 ? 175 : 150;
ApplyFedoraGrowth(card, rawhideChargeUsed);
ApplyFedoraGrowth(card, rawhideChargeUsed, firstFedoraBonusThisTurn);
}
if (card.Definition.Language == Language.C && _cCardsPlayedThisTurn == 0 && HasPackageEffect(PackageEffectKind.FirstCThisTurnDamageMultiplier, out PackageEffectData cPackageEffect))
@@ -372,6 +378,27 @@ namespace KernelPanic.Combat
return true;
}
public bool TryUnlockCard(CardInstance card)
{
if (currentPhase != TurnPhase.Execute || card == null || !card.IsLocked || _playerState == null)
{
return false;
}
int cost = EnemyArchetypeCatalog.DrmUnlockCycleCost;
if (_playerState.Cycles < cost)
{
Log($"not enough cycles to unlock {GetCardName(card)}");
return false;
}
_playerState.Cycles -= cost;
card.IsLocked = false;
Log($"unlocked {GetCardName(card)}");
StateChanged?.Invoke();
return true;
}
public void ReportEffectResult(string message)
{
if (!string.IsNullOrWhiteSpace(message))
@@ -626,7 +653,7 @@ namespace KernelPanic.Combat
_awaitingWaveContinue = false;
_loggedEffectTodos.Clear();
_ubuntuEmptyHandRefillUsed = false;
_fedoraCrashChance = 10;
_fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
_fedoraCardsDiscountedThisTurn = 0;
_cardsPlayedThisTurn = 0;
_cardsPlayedThisWave = 0;
@@ -700,6 +727,7 @@ namespace KernelPanic.Combat
_interpreterQueue.Clear();
_enemies.Clear();
SpawnStructuralEnemies();
RecalculateKernelRamPressure();
PickEnemyIntents();
StateChanged?.Invoke();
yield return DrawOpeningHandSequenced();
@@ -715,6 +743,7 @@ namespace KernelPanic.Combat
}
RevivePendingEnemies();
UnlockHandCards("license check expired");
_playerState.Cycles = _playerState.MaxCycles;
_fedoraCardsDiscountedThisTurn = 0;
_cardsPlayedThisTurn = 0;
@@ -856,7 +885,7 @@ namespace KernelPanic.Combat
private IEnumerator DrawOpeningHandSequenced()
{
int openingDraw = Mathf.Min(CombatTuning.OpeningHandSize, _playerState.Ram);
int openingDraw = Mathf.Min(CombatTuning.OpeningHandSize, _handController == null ? _playerState.Ram : _handController.RamCapacity);
yield return DrawCardsToHandSequenced(openingDraw, "opening hand");
}
@@ -954,9 +983,87 @@ namespace KernelPanic.Combat
StartCoroutine(DrawCardsToHandSequenced(_playerState.Ram, "ubuntu 24.04 refill"));
}
private bool CanApplyFedoraBonus()
private bool CanApplyFedoraBonus(CardInstance card)
{
return CanApplyFedoraPassiveBonus() || _rawhideBonusCharges > 0;
return CardEffectFactory.CanReceiveBleedingEdgeDamageBonus(card?.Definition)
&& CardCanBenefitFromBleedingEdgeNow(card)
&& (CanApplyFedoraPassiveBonus() || _rawhideBonusCharges > 0);
}
private bool CardCanBenefitFromBleedingEdgeNow(CardInstance card)
{
string id = card?.Definition?.Id;
if (string.IsNullOrWhiteSpace(id))
{
return false;
}
if (id == "arch_consult_wiki")
{
return _handController == null || !HasBrokenHandCard();
}
if (id == "fedora_dnf_autoremove")
{
return HasJunkHandCard(card);
}
return true;
}
private bool HasBrokenHandCard()
{
if (_handController == null)
{
return false;
}
for (int i = 0; i < _handController.Cards.Count; i++)
{
if (_handController.Cards[i]?.IsBroken == true)
{
return true;
}
}
return false;
}
private bool HasJunkHandCard(CardInstance playedCard)
{
if (_handController == null)
{
return false;
}
for (int i = 0; i < _handController.Cards.Count; i++)
{
CardInstance card = _handController.Cards[i];
if (card != playedCard && IsJunkForDnfAutoremove(card))
{
return true;
}
}
return false;
}
private static bool IsJunkForDnfAutoremove(CardInstance card)
{
if (card == null)
{
return false;
}
string id = card.Definition?.Id ?? string.Empty;
string name = card.Definition?.DisplayName ?? string.Empty;
return card.Definition != null && card.Definition.IsToken
|| card.IsBroken
|| id.IndexOf("junk", StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("nop", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("junk", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("NOP", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("broken", StringComparison.OrdinalIgnoreCase) >= 0;
}
private void DecayJavaWarmupForNextWave()
@@ -981,9 +1088,9 @@ namespace KernelPanic.Combat
return _fedoraCardsDiscountedThisTurn < limit;
}
private void ApplyFedoraGrowth(CardInstance card, bool rawhideChargeUsed)
private void ApplyFedoraGrowth(CardInstance card, bool rawhideChargeUsed, bool firstFedoraBonusThisTurn)
{
if (!IsDistro("fedora") || _runConfig.DistroVersion < 3 || card == null || !card.WasFirstCardThisTurn)
if (!IsDistro("fedora") || _runConfig.DistroVersion < 3 || card == null || !firstFedoraBonusThisTurn)
{
return;
}
@@ -1739,6 +1846,7 @@ namespace KernelPanic.Combat
_runManager?.AddBits(CombatTuning.BitsPerKill);
HandleEnemyDeath(enemy);
_enemies.RemoveAt(i);
RecalculateKernelRamPressure();
}
}
@@ -1803,6 +1911,87 @@ namespace KernelPanic.Combat
return candidates[index];
}
private void UnlockHandCards(string reason)
{
if (_handController == null)
{
return;
}
int unlocked = 0;
for (int i = 0; i < _handController.Cards.Count; i++)
{
CardInstance card = _handController.Cards[i];
if (card != null && card.IsLocked)
{
card.IsLocked = false;
unlocked++;
}
}
if (unlocked > 0)
{
Log($"{reason}: unlocked {unlocked} card(s)");
}
}
private void RecalculateKernelRamPressure()
{
if (_handController == null || _playerState == null || _runManager == null)
{
return;
}
int penalty = 0;
for (int i = 0; i < _enemies.Count; i++)
{
EnemyInstance enemy = _enemies[i];
if (enemy.HasBehavior(EnemyBehaviorFlags.RamPressure) && !enemy.State.IsDefeated)
{
penalty += EnemyArchetypeCatalog.KernelPanicRamPenalty;
}
}
_activeKernelRamPenalty = penalty;
int baseRam = _runManager.EffectiveRam();
int effectiveRam = Mathf.Max(1, baseRam - penalty);
_playerState.Ram = effectiveRam;
_handController.SetRamCapacity(effectiveRam);
}
private void ApplyTelemetryCardPlayed()
{
int collectors = 0;
for (int i = 0; i < _enemies.Count; i++)
{
EnemyInstance enemy = _enemies[i];
if (enemy.HasBehavior(EnemyBehaviorFlags.TelemetryCollector) && !enemy.State.IsDefeated)
{
enemy.AddTelemetryStack();
collectors++;
}
}
if (collectors > 0)
{
Log($"telemetry collector profiled card: +{EnemyArchetypeCatalog.TelemetryDamageGrowthPerCard} damage");
StateChanged?.Invoke();
}
}
private void LockRandomHandCard(EnemyInstance source)
{
CardInstance card = RandomHandCard(cardInstance => cardInstance != null && !cardInstance.IsBroken && !cardInstance.IsLocked);
if (card == null)
{
Log("license check: no unlockable hand card");
return;
}
card.IsLocked = true;
Log($"{source.Name} locked {GetCardName(card)}");
}
private void BreakRacePair(EnemyInstance defeated)
{
EnemyInstance survivor = RacePartner(defeated);
@@ -1829,7 +2018,8 @@ namespace KernelPanic.Combat
int count = Mathf.Clamp(CombatTuning.BaseEnemiesPerWave + countGrowth, CombatTuning.BaseEnemiesPerWave, CombatTuning.MaxEnemiesPerWave);
int waveUptimeBonus = Mathf.Max(0, wave - 1) * CombatTuning.EnemyUptimeGrowthPerWave;
bool rootkitSpawned = false;
for (int i = 0; i < count; i++)
bool eliteSpawned = TrySpawnEliteEncounter(wave, waveUptimeBonus);
for (int i = eliteSpawned ? 1 : 0; i < count; i++)
{
string archetypeId = CreatePlaceholderArchetypeId(wave, i);
if (archetypeId == "rootkit")
@@ -1860,6 +2050,33 @@ namespace KernelPanic.Combat
}
}
private bool TrySpawnEliteEncounter(int wave, int uptimeBonus)
{
if (wave < 5)
{
return false;
}
bool guaranteedIntro = wave == 5;
bool rolledElite = RandomRoll.RollRange(1, 100, RollContext.None) <= 35;
if (!guaranteedIntro && !rolledElite)
{
return false;
}
SpawnEnemy(CreateEliteArchetypeId(), uptimeBonus);
Log("elite process escorted by fodder");
// TODO: Replace this single-elite chance with authored elite encounter assets.
return true;
}
private static string CreateEliteArchetypeId()
{
string[] pool = { "daemon", "kernel_panic", "telemetry_collector", "drm_guardian" };
int index = RandomRoll.RollRange(0, pool.Length - 1, RollContext.None);
return pool[index];
}
private void SpawnRacePair(int uptimeBonus)
{
int pairId = _nextRacePairId++;
@@ -2002,15 +2219,24 @@ namespace KernelPanic.Combat
Log("TODO: buff intent needs enemy buff/status support.");
break;
case EnemyIntentKind.Special:
ExecuteSpecialEnemyIntent(enemy);
ExecuteSpecialEnemyIntent(enemy, value);
break;
}
enemy.AdvanceCountdownAfterAction();
}
private void ExecuteSpecialEnemyIntent(EnemyInstance enemy)
private void ExecuteSpecialEnemyIntent(EnemyInstance enemy, int value)
{
if (enemy.HasBehavior(EnemyBehaviorFlags.CardLocker))
{
LockRandomHandCard(enemy);
value = ApplyIncomingAttackMitigation(value);
DamageResult result = _damagePipeline.DealDamage(new DamageRequest(enemy.State, _playerState, value, enemy.CurrentIntent.DamageType, enemy.CurrentIntent.TrueDamage, false));
GameEvents.RaisePlayerDamaged(new PlayerDamagedEvent(enemy, result.FinalAmount));
return;
}
if (enemy.HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath))
{
enemy.AdvanceSegfaultCountdown();