changed most TODOs to TODONEs

This commit is contained in:
2026-07-08 10:49:05 +02:00
parent ac09d67677
commit 50135e2e38
11 changed files with 666 additions and 48 deletions
+298 -27
View File
@@ -83,6 +83,26 @@ namespace KernelPanic.Combat
}
}
public sealed class FirstCardShieldEffect : ICardEffect
{
private readonly int _baseAmount;
private readonly int _firstCardBonus;
public FirstCardShieldEffect(int baseAmount, int firstCardBonus)
{
_baseAmount = baseAmount;
_firstCardBonus = firstCardBonus;
}
public void Execute(CombatContext context)
{
int amount = _baseAmount + (context.Card != null && context.Card.WasFirstCardThisTurn ? _firstCardBonus : 0);
amount = UpgradeMath.ScaleShield(UpgradeMath.ApplySourceFlatBonus(amount, context), context.Card);
context.Source.Shield += amount;
context.CombatManager.ReportEffectResult($"gained {amount} shield");
}
}
public sealed class ChanceDamageEffect : ICardEffect
{
private readonly int _damageAmount;
@@ -115,6 +135,26 @@ namespace KernelPanic.Combat
}
}
public sealed class QueuedGrowthDamageEffect : ICardEffect
{
private readonly int _baseAmount;
private readonly Language _language;
public QueuedGrowthDamageEffect(int baseAmount, Language language)
{
_baseAmount = baseAmount;
_language = language;
}
public void Execute(CombatContext context)
{
int queueGrowth = UnityEngine.Mathf.Max(0, (context.Card?.QueuePlayCount ?? 1) - 1);
int amount = _baseAmount + queueGrowth;
new DealDamageEffect(amount, amount, _language).Execute(context);
}
}
public sealed class ApplyStatusEffect : ICardEffect
{
private readonly StatusType _statusType;
@@ -209,6 +249,81 @@ namespace KernelPanic.Combat
}
}
public sealed class NextTurnCycleEffect : ICardEffect
{
private readonly int _amount;
public NextTurnCycleEffect(int amount)
{
_amount = amount;
}
public void Execute(CombatContext context)
{
int amount = UpgradeMath.ScaleAmount(_amount, context.Card);
context.CombatManager.AddNextTurnCycleBonus(amount);
context.CombatManager.ReportEffectResult($"next turn +{amount} cycle");
}
}
public sealed class QueueRepeatEffect : ICardEffect
{
private readonly int _count;
public QueueRepeatEffect(int count)
{
_count = count;
}
public void Execute(CombatContext context)
{
context.CombatManager.AddQueuedRepeatCharges(UpgradeMath.ScaleAmount(_count, context.Card));
}
}
public sealed class CopyLowestCostHandCardEffect : ICardEffect
{
public void Execute(CombatContext context)
{
if (context.HandController.Cards.Count >= context.HandController.RamCapacity)
{
context.CombatManager.ReportEffectResult("hand full: copied 0");
return;
}
CardInstance selected = null;
int selectedCost = int.MaxValue;
for (int i = 0; i < context.HandController.Cards.Count; i++)
{
CardInstance candidate = context.HandController.Cards[i];
if (candidate == null || candidate.Definition == null)
{
continue;
}
int cost = context.CombatManager.GetEffectiveCardCost(candidate);
if (cost < selectedCost)
{
selected = candidate;
selectedCost = cost;
}
}
if (selected == null)
{
context.CombatManager.ReportEffectResult("no card to copy");
return;
}
CardInstance copy = selected.CopyForCombat();
if (context.HandController.Add(copy))
{
string name = string.IsNullOrWhiteSpace(copy.Definition.DisplayName) ? copy.Definition.Id : copy.Definition.DisplayName;
context.CombatManager.ReportEffectResult($"copied {name}");
}
}
}
public sealed class UpgradeHandCardsEffect : ICardEffect
{
private readonly int _minCount;
@@ -247,6 +362,34 @@ namespace KernelPanic.Combat
}
}
public sealed class ShuffleTokenIntoDrawPileEffect : ICardEffect
{
private readonly string _tokenId;
public ShuffleTokenIntoDrawPileEffect(string tokenId)
{
_tokenId = tokenId;
}
public void Execute(CombatContext context)
{
if (context.CombatManager.TryCreateGeneratedCardById(_tokenId, out CardInstance token))
{
context.DeckController.AddToDrawPile(token, shuffle: true);
context.CombatManager.ReportEffectResult($"shuffled {GetCardName(token)} into draw pile");
}
else
{
context.CombatManager.ReportEffectResult($"token {_tokenId} unavailable");
}
}
private static string GetCardName(CardInstance card)
{
return string.IsNullOrWhiteSpace(card?.Definition?.DisplayName) ? card?.Definition?.Id ?? "--" : card.Definition.DisplayName;
}
}
public sealed class GenerateCardEffect : ICardEffect
{
private readonly Language _language;
@@ -293,18 +436,123 @@ namespace KernelPanic.Combat
}
}
public sealed class NoOpCardEffect : ICardEffect
public sealed class OverkillToShieldDamageEffect : ICardEffect
{
private readonly string _todo;
private readonly int _amount;
private readonly Language _language;
public NoOpCardEffect(string todo)
public OverkillToShieldDamageEffect(int amount, Language language)
{
_todo = todo;
_amount = amount;
_language = language;
}
public void Execute(CombatContext context)
{
context.CombatManager.LogEffectTodo(context.Card, _todo);
for (int i = 0; i < context.Targets.Count; i++)
{
CombatantState target = context.Targets[i];
if (target == null || target.IsDefeated)
{
continue;
}
int amount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_amount, context), context.Card);
int survivable = target.CurrentUptime + target.Shield;
context.DamagePipeline.DealDamage(new DamageRequest(context.Source, target, amount, _language, false, false));
int overkill = UnityEngine.Mathf.Max(0, amount - survivable);
if (overkill > 0)
{
context.Source.Shield += overkill;
context.CombatManager.ReportEffectResult($"overkill -> {overkill} shield");
}
}
}
}
public sealed class ConditionalShieldRepeatDamageEffect : ICardEffect
{
private readonly int _amount;
private readonly Language _language;
public ConditionalShieldRepeatDamageEffect(int amount, Language language)
{
_amount = amount;
_language = language;
}
public void Execute(CombatContext context)
{
int repeats = context.Source.Shield > 0 ? 2 : 1;
for (int repeat = 0; repeat < repeats; repeat++)
{
new DealDamageEffect(_amount, _amount, _language).Execute(context);
}
}
}
public sealed class UpdateManagerEffect : ICardEffect
{
private readonly int _amount;
public UpdateManagerEffect(int amount)
{
_amount = amount;
}
public void Execute(CombatContext context)
{
int amount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_amount, context), context.Card);
new DealDamageEffect(amount, amount, Language.Python, allEnemies: true).Execute(context);
context.CombatManager.ScheduleUpdateManagerRepeat(amount);
}
}
public sealed class TimeshiftSnapshotEffect : ICardEffect
{
public void Execute(CombatContext context)
{
context.CombatManager.ScheduleTimeshiftRestore(context.Source.CurrentUptime);
}
}
public sealed class RawhideEffect : ICardEffect
{
public void Execute(CombatContext context)
{
new DrawEffect(2).Execute(context);
context.CombatManager.GrantRawhideBonus(1);
context.CombatManager.ReportEffectResult("rawhide armed next card");
}
}
public sealed class IncomingAttackHalveEffect : ICardEffect
{
private readonly int _charges;
public IncomingAttackHalveEffect(int charges)
{
_charges = charges;
}
public void Execute(CombatContext context)
{
context.CombatManager.AddIncomingAttackHalfCharges(UpgradeMath.ScaleAmount(_charges, context.Card));
}
}
public sealed class NoOpCardEffect : ICardEffect
{
private readonly string _message;
public NoOpCardEffect(string message)
{
_message = message;
}
public void Execute(CombatContext context)
{
context.CombatManager.LogEffectTodo(context.Card, _message);
}
}
@@ -337,9 +585,13 @@ namespace KernelPanic.Combat
new CleanseEffect(harmfulOnly: true)
},
"lang_js_typeof" => One(new ChanceDamageEffect(8, 75, Language.JavaScript)),
"shop_py_list_append" => One(new DealDamageEffect(4, 4, Language.Python)),
"shop_py_async_def" => One(new DrawEffect(2)),
"shop_py_zip" => Todo("TODO: zip() needs queue-effect-multiply support."),
"shop_py_list_append" => One(new QueuedGrowthDamageEffect(4, Language.Python)),
"shop_py_async_def" => new ICardEffect[]
{
new DrawEffect(2),
new NextTurnCycleEffect(1)
},
"shop_py_zip" => One(new QueueRepeatEffect(2)),
"shop_js_math_random" => One(new DealDamageEffect(2, 20, Language.JavaScript)),
"shop_js_promise_all" => new ICardEffect[]
{
@@ -347,28 +599,37 @@ namespace KernelPanic.Combat
new DealDamageEffect(4, 10, Language.JavaScript),
new DealDamageEffect(4, 10, Language.JavaScript)
},
"shop_js_spread" => Todo("TODO: spread ... needs card-copy support."),
"ubuntu_snap_install" => Todo("TODO: snap install needs token/junk insertion system."),
"shop_js_spread" => One(new CopyLowestCostHandCardEffect()),
"ubuntu_snap_install" => new ICardEffect[]
{
new DealDamageEffect(6, 14, Language.JavaScript),
new ShuffleTokenIntoDrawPileEffect("ubuntu_pro_trial")
},
"ubuntu_do_release_upgrade" => One(new UpgradeHandCardsEffect(1, 3)),
"ubuntu_apt_install" => One(new GenerateCardEffect(Language.Python, Rarity.Common)),
"ubuntu_pro_trial" => Todo("TODO: ubuntu pro trial needs unplayable token handling."),
"mint_update_manager" => Todo("TODO: update manager needs delayed repeat system."),
"mint_cinnamon" => Todo("TODO: cinnamon needs shield effect support."),
"mint_nemo" => Todo("TODO: nemo needs conditional repeat based on shield."),
"mint_timeshift" => Todo("TODO: timeshift snapshot V3 needs delayed restore system before threshold/restore can be upgraded."),
"fedora_borrow_checker" => Todo("TODO: borrow checker needs shield and first-card tracking."),
"fedora_cargo_build" => Todo("TODO: cargo build --release needs overkill-to-shield handling."),
"fedora_dnf_update" => Todo("TODO: dnf update needs Java played count and cost mutation."),
"fedora_rawhide" => Todo("TODO: rawhide V3 needs permanent damage-growth doubling after growth effects exist."),
"fedora_selinux" => Todo("TODO: SELinux enforcing needs enemy attack mitigation statuses."),
_ => Todo("TODO: card effect is not implemented yet.")
"ubuntu_pro_trial" => One(new NoOpCardEffect("ubuntu pro trial is unplayable")),
"mint_update_manager" => One(new UpdateManagerEffect(5)),
"mint_cinnamon" => new ICardEffect[]
{
new ShieldEffect(3),
new DrawEffect(1)
},
"mint_nemo" => One(new ConditionalShieldRepeatDamageEffect(6, Language.JavaScript)),
"mint_timeshift" => One(new TimeshiftSnapshotEffect()),
"fedora_borrow_checker" => One(new FirstCardShieldEffect(8, 8)),
"fedora_cargo_build" => One(new OverkillToShieldDamageEffect(10, Language.Rust)),
"fedora_dnf_update" => One(new DealDamageEffect(9, 9, Language.Java)),
"fedora_rawhide" => One(new RawhideEffect()),
"fedora_selinux" => One(new IncomingAttackHalveEffect(2)),
_ => Todo("card effect is not authored")
};
}
public static bool RequiresSingleTarget(CardDefinition definition)
{
return definition?.Id is "lang_js_console_log" or "lang_js_fetch" or "lang_js_typeof" or "mint_fix_broken"
or "shop_py_list_append" or "shop_js_math_random" or "shop_js_promise_all";
or "shop_py_list_append" or "shop_js_math_random" or "shop_js_promise_all" or "ubuntu_snap_install"
or "mint_nemo" or "fedora_cargo_build" or "fedora_dnf_update";
}
public static bool TargetsAllEnemies(CardDefinition definition)
@@ -423,18 +684,28 @@ namespace KernelPanic.Combat
"lang_js_console_log" => $"deal {UpgradeMath.ScaleAmount(3, card)}-{UpgradeMath.ScaleAmount(9, card)}.{marker}",
"lang_js_fetch" => $"deal {UpgradeMath.ScaleAmount(5, card)}-{UpgradeMath.ScaleAmount(13, card)}.{marker}",
"lang_js_typeof" => $"75%: deal {UpgradeMath.ScaleAmount(8, card)}. 25%: deal 0.{marker}",
"shop_py_list_append" => $"queue: deal {UpgradeMath.ScaleAmount(4, card)}. TODO: grows when re-queued.{marker}",
"shop_py_async_def" => $"queue: draw {UpgradeMath.ScaleAmount(2, card)}. TODO: gain 1 cycle next turn.{marker}",
"shop_py_zip" => $"TODO: next 2 queued cards resolve twice.{marker}",
"shop_py_list_append" => $"queue: deal {UpgradeMath.ScaleAmount(4 + UnityEngine.Mathf.Max(0, card.QueuePlayCount - 1), card)}; +1 each time re-queued.{marker}",
"shop_py_async_def" => $"queue: draw {UpgradeMath.ScaleAmount(2, card)}. gain {UpgradeMath.ScaleAmount(1, card)} cycle next turn.{marker}",
"shop_py_zip" => $"next {UpgradeMath.ScaleAmount(2, card)} queued cards resolve twice.{marker}",
"shop_js_math_random" => $"deal {UpgradeMath.ScaleAmount(2, card)}-{UpgradeMath.ScaleAmount(20, card)}.{marker}",
"shop_js_promise_all" => $"deal {UpgradeMath.ScaleAmount(4, card)}-{UpgradeMath.ScaleAmount(10, card)} three times.{marker}",
"shop_js_spread" => $"TODO: copy the lowest-cost card in hand.{marker}",
"shop_js_spread" => $"copy the lowest-cost card in hand.{marker}",
"ubuntu_ask_ubuntu" => $"queue: draw {UpgradeMath.ScaleAmount(2, card)}.{marker}",
"ubuntu_unattended_upgrades" => $"queue: gain {UpgradeMath.ScaleShield(4, card)} shield now and next 2 turns.{marker}",
"ubuntu_snap_install" => $"deal {UpgradeMath.ScaleAmount(6, card)}-{UpgradeMath.ScaleAmount(14, card)}. shuffle an ubuntu pro trial into your draw pile.{marker}",
"ubuntu_do_release_upgrade" => $"upgrade {UpgradeMath.ScaleAmount(1, card)}-{UpgradeMath.ScaleAmount(3, card)} random hand cards.{marker}",
"ubuntu_apt_install" => $"queue: generate a random common Python card at 0c.{marker}",
"mint_fix_broken" => $"deal {UpgradeMath.ScaleAmount(8, card)}. cleanse harmful statuses.{marker}",
_ => string.IsNullOrWhiteSpace(card.Definition.Description) ? "TODO: effect not implemented." : $"{card.Definition.Description}{marker}"
"mint_update_manager" => $"queue: deal {UpgradeMath.ScaleAmount(5, card)} to all enemies. repeats at the end of your next turn.{marker}",
"mint_cinnamon" => $"queue: gain {UpgradeMath.ScaleShield(3, card)} shield. draw {UpgradeMath.ScaleAmount(1, card)}.{marker}",
"mint_nemo" => $"deal {UpgradeMath.ScaleAmount(6, card)}. if you have shield, deal it again.{marker}",
"mint_timeshift" => $"queue: record uptime now. at end of your next turn, restore to it if lower.{marker}",
"fedora_borrow_checker" => $"gain {UpgradeMath.ScaleShield(8, card)} shield. if first card this turn, gain {UpgradeMath.ScaleShield(8, card)} more.{marker}",
"fedora_cargo_build" => $"deal {UpgradeMath.ScaleAmount(10, card)}. overkill becomes shield.{marker}",
"fedora_dnf_update" => $"deal {UpgradeMath.ScaleAmount(9, card)}. costs 1 less per Java card played this combat.{marker}",
"fedora_rawhide" => $"draw {UpgradeMath.ScaleAmount(2, card)}. next card this turn gains bleeding edge.{marker}",
"fedora_selinux" => $"next {UpgradeMath.ScaleAmount(2, card)} enemy attacks deal half damage.{marker}",
_ => string.IsNullOrWhiteSpace(card.Definition.Description) ? "effect not authored." : $"{card.Definition.Description}{marker}"
};
}
+44
View File
@@ -23,6 +23,8 @@ namespace KernelPanic.Combat
public int MagnitudeBonus { get; private set; }
public int DrawRider { get; private set; }
public int UpgradeLevel { get; private set; }
public int QueuePlayCount { get; private set; }
public bool WasFirstCardThisTurn { get; private set; }
public bool IsBroken { get; set; }
public bool IsLocked { get; set; }
public bool CanUpgrade => UpgradeLevel == 0;
@@ -67,6 +69,17 @@ namespace KernelPanic.Combat
return true;
}
public bool ApplyCombatMagnitudeBonus(int amount)
{
if (amount <= 0)
{
return false;
}
MagnitudeBonus += amount;
return true;
}
public bool ApplyDrawRider(int amount)
{
if (amount <= 0)
@@ -79,6 +92,35 @@ namespace KernelPanic.Combat
return true;
}
public void MarkQueued()
{
QueuePlayCount++;
}
public void MarkPlayedThisTurn(bool wasFirst)
{
WasFirstCardThisTurn = wasFirst;
}
public CardInstance CopyForCombat()
{
CardInstance copy = new(Definition)
{
TemporaryCostDelta = TemporaryCostDelta,
IsBroken = IsBroken,
IsLocked = IsLocked
};
copy.PermanentCostDelta = PermanentCostDelta;
copy.MagnitudeBonus = MagnitudeBonus;
copy.DrawRider = DrawRider;
copy.UpgradeLevel = UpgradeLevel;
copy.QueuePlayCount = QueuePlayCount;
copy.WasFirstCardThisTurn = WasFirstCardThisTurn;
copy.SetTargetSnapshot(TargetSnapshot);
return copy;
}
public void SetTargetSnapshot(IEnumerable<CombatantState> targets)
{
_targetSnapshot.Clear();
@@ -98,6 +140,8 @@ namespace KernelPanic.Combat
public void ResetCombatState()
{
TemporaryCostDelta = 0;
QueuePlayCount = 0;
WasFirstCardThisTurn = false;
IsBroken = false;
IsLocked = false;
ClearTargetSnapshot();
+281 -4
View File
@@ -25,6 +25,7 @@ namespace KernelPanic.Combat
private readonly List<EnemyInstance> enemies = new();
private readonly HashSet<string> loggedEffectTodos = new();
private readonly List<CardDefinition> generatedCardPool = new();
private readonly List<DelayedCombatEffect> delayedEffects = new();
private RunManager runManager;
private RunConfig runConfig;
private HandController handController;
@@ -37,6 +38,11 @@ namespace KernelPanic.Combat
private bool ubuntuEmptyHandRefillUsed;
private int fedoraCardsDiscountedThisTurn;
private int fedoraCrashChance = 10;
private int cardsPlayedThisTurn;
private int javaCardsPlayedThisCombat;
private int rawhideBonusCharges;
private int queuedRepeatCharges;
private int nextTurnCycleBonus;
public TurnPhase CurrentPhase => currentPhase;
public RunConfig RunConfig => runConfig;
@@ -145,8 +151,14 @@ namespace KernelPanic.Combat
return false;
}
if (card.Definition.IsToken)
{
Log($"{GetCardName(card)} is unplayable");
return false;
}
bool fedoraBonus = CanApplyFedoraBonus();
int cost = GetCardCost(card);
int cost = GetEffectiveCardCost(card);
if (fedoraBonus)
{
cost = Mathf.Max(0, cost - 1);
@@ -173,6 +185,13 @@ namespace KernelPanic.Combat
playerState.Cycles -= cost;
ResolutionTrack track = card.Definition.ResolutionTrack;
card.MarkPlayedThisTurn(cardsPlayedThisTurn == 0);
cardsPlayedThisTurn++;
if (card.Definition.Language == Language.Java)
{
javaCardsPlayedThisCombat++;
}
GameEvents.RaiseCardPlayed(new CardPlayedEvent(card, track));
LogPlayIntent(card);
card.SetTargetSnapshot(CaptureTargets(card));
@@ -180,7 +199,13 @@ namespace KernelPanic.Combat
if (fedoraBonus)
{
fedoraCardsDiscountedThisTurn++;
if (RandomRoll.RollRange(1, 100, new RollContext(playerState)) <= fedoraCrashChance)
bool rawhideChargeUsed = rawhideBonusCharges > 0;
if (rawhideChargeUsed)
{
rawhideBonusCharges--;
}
if (!IsCrashImmune(card) && RandomRoll.RollRange(1, 100, new RollContext(playerState)) <= fedoraCrashChance)
{
fedoraCrashChance = 10;
if (IsDistro("fedora") && runConfig.DistroVersion >= 4)
@@ -198,11 +223,13 @@ namespace KernelPanic.Combat
fedoraCrashChance = Mathf.Min(90, fedoraCrashChance + 10);
playerState.DamageMultiplierPercent = runConfig.DistroVersion >= 2 ? 175 : 150;
ApplyFedoraGrowth(card, rawhideChargeUsed);
}
switch (track)
{
case ResolutionTrack.InterpreterQueue:
card.MarkQueued();
interpreterQueue.Enqueue(card);
break;
case ResolutionTrack.LazyStack:
@@ -235,6 +262,17 @@ namespace KernelPanic.Combat
return Mathf.Max(0, card.Definition.CycleCost + card.PermanentCostDelta + card.TemporaryCostDelta);
}
public int GetEffectiveCardCost(CardInstance card)
{
int cost = GetCardCost(card);
if (card?.Definition != null && string.Equals(card.Definition.Id, "fedora_dnf_update", StringComparison.OrdinalIgnoreCase))
{
cost -= javaCardsPlayedThisCombat;
}
return Mathf.Max(0, cost);
}
public bool RequiresTarget(CardInstance card)
{
return card?.Definition != null && CardEffectFactory.RequiresSingleTarget(card.Definition) && LivingEnemyStates().Count > 0;
@@ -258,6 +296,86 @@ namespace KernelPanic.Combat
}
}
public void AddNextTurnCycleBonus(int amount)
{
nextTurnCycleBonus += Mathf.Max(0, amount);
}
public void AddQueuedRepeatCharges(int amount)
{
queuedRepeatCharges += Mathf.Max(0, amount);
if (amount > 0)
{
ReportEffectResult($"next {amount} queued card(s) resolve twice");
}
}
public int ConsumeQueuedResolutionCount(CardInstance card)
{
if (queuedRepeatCharges <= 0 || card == null)
{
return 1;
}
queuedRepeatCharges--;
return 2;
}
public void GrantRawhideBonus(int charges)
{
rawhideBonusCharges += Mathf.Max(0, charges);
}
public void AddIncomingAttackHalfCharges(int charges)
{
if (playerState == null)
{
return;
}
playerState.IncomingAttackHalfCharges += Mathf.Max(0, charges);
ReportEffectResult($"next {charges} enemy attack(s) halved");
}
public void ScheduleUpdateManagerRepeat(int damageAmount)
{
delayedEffects.Add(DelayedCombatEffect.UpdateManagerRepeat(damageAmount, 2));
ReportEffectResult("update manager scheduled repeat");
}
public void ScheduleTimeshiftRestore(int uptime)
{
delayedEffects.Add(DelayedCombatEffect.TimeshiftRestore(uptime, 2));
ReportEffectResult($"timeshift snapshot recorded {uptime} uptime");
}
public bool TryCreateGeneratedCardById(string id, out CardInstance generatedCard)
{
generatedCard = null;
if (string.IsNullOrWhiteSpace(id))
{
return false;
}
IReadOnlyList<CardDefinition> source = generatedCardPool.Count > 0 ? generatedCardPool : runConfig?.StartingDeck;
if (source == null)
{
return false;
}
for (int i = 0; i < source.Count; i++)
{
CardDefinition definition = source[i];
if (definition != null && string.Equals(definition.Id, id, StringComparison.OrdinalIgnoreCase))
{
generatedCard = new CardInstance(definition);
return true;
}
}
return false;
}
public void SetGeneratedCardPool(IEnumerable<CardDefinition> cardPool)
{
generatedCardPool.Clear();
@@ -268,7 +386,7 @@ namespace KernelPanic.Combat
foreach (CardDefinition card in cardPool)
{
if (card != null && !card.IsToken && !card.IsRunOnly)
if (card != null)
{
generatedCardPool.Add(card);
}
@@ -394,6 +512,12 @@ namespace KernelPanic.Combat
ubuntuEmptyHandRefillUsed = false;
fedoraCrashChance = 10;
fedoraCardsDiscountedThisTurn = 0;
cardsPlayedThisTurn = 0;
javaCardsPlayedThisCombat = 0;
rawhideBonusCharges = 0;
queuedRepeatCharges = 0;
nextTurnCycleBonus = 0;
delayedEffects.Clear();
RandomRoll.Seed(runConfig.RunSeed);
playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles());
ApplyVersionState(playerState);
@@ -418,6 +542,7 @@ namespace KernelPanic.Combat
playerState.MaxCycles = runManager.EffectiveMaxCycles();
playerState.Cycles = 0;
playerState.Shield = 0;
playerState.IncomingAttackHalfCharges = 0;
playerState.IsDefeated = false;
playerState.MutableStatuses.Clear();
ApplyVersionState(playerState);
@@ -456,6 +581,13 @@ namespace KernelPanic.Combat
playerState.Cycles = playerState.MaxCycles;
fedoraCardsDiscountedThisTurn = 0;
cardsPlayedThisTurn = 0;
if (nextTurnCycleBonus > 0)
{
playerState.Cycles += nextTurnCycleBonus;
Log($"deferred cycle grant: +{nextTurnCycleBonus}");
nextTurnCycleBonus = 0;
}
statusEffects.Tick(playerState, StatusTickTiming.StartOfTurn, playerState, damagePipeline);
if (CheckLoss())
{
@@ -481,7 +613,16 @@ namespace KernelPanic.Combat
while (interpreterQueue.TryDequeue(out CardInstance card))
{
LogPlayIntent(card);
ExecuteCardEffects(card, ResolveQueuedTargets(card));
int resolutionCount = ConsumeQueuedResolutionCount(card);
for (int repeatIndex = 0; repeatIndex < resolutionCount; repeatIndex++)
{
ExecuteCardEffects(card, ResolveQueuedTargets(card));
if (CheckWinOrLoss())
{
return;
}
}
ResolveCardRiders(card);
deckController.Discard(card);
GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.InterpreterQueue));
@@ -541,6 +682,12 @@ namespace KernelPanic.Combat
return;
}
ResolveDelayedEndOfTurnEffects();
if (CheckWinOrLoss())
{
return;
}
RemoveDefeatedEnemies();
CheckWinOrLoss();
StateChanged?.Invoke();
@@ -638,6 +785,11 @@ namespace KernelPanic.Combat
}
private bool CanApplyFedoraBonus()
{
return CanApplyFedoraPassiveBonus() || rawhideBonusCharges > 0;
}
private bool CanApplyFedoraPassiveBonus()
{
if (!IsDistro("fedora") || playerState == null)
{
@@ -648,6 +800,27 @@ namespace KernelPanic.Combat
return fedoraCardsDiscountedThisTurn < limit;
}
private void ApplyFedoraGrowth(CardInstance card, bool rawhideChargeUsed)
{
if (!IsDistro("fedora") || runConfig.DistroVersion < 3 || card == null || !card.WasFirstCardThisTurn)
{
return;
}
int growth = rawhideChargeUsed ? 2 : 1;
if (card.ApplyCombatMagnitudeBonus(growth))
{
Log($"rawhide growth: {GetCardName(card)} +{growth} effect");
}
}
private static bool IsCrashImmune(CardInstance card)
{
string id = card?.Definition?.Id;
return string.Equals(id, "fedora_cargo_build", StringComparison.OrdinalIgnoreCase)
|| string.Equals(id, "fedora_selinux", StringComparison.OrdinalIgnoreCase);
}
private void ApplyVersionState(CombatantState state)
{
if (state == null)
@@ -856,6 +1029,7 @@ namespace KernelPanic.Combat
switch (intent.Kind)
{
case EnemyIntentKind.Attack:
value = ApplyIncomingAttackMitigation(value);
DamageResult attackResult = damagePipeline.DealDamage(new DamageRequest(enemy.State, playerState, value, intent.DamageType, intent.TrueDamage, false));
GameEvents.RaisePlayerDamaged(new PlayerDamagedEvent(enemy, attackResult.FinalAmount));
break;
@@ -879,6 +1053,79 @@ namespace KernelPanic.Combat
}
}
private int ApplyIncomingAttackMitigation(int value)
{
if (playerState == null || playerState.IncomingAttackHalfCharges <= 0)
{
return value;
}
playerState.IncomingAttackHalfCharges--;
int mitigated = Mathf.CeilToInt(Mathf.Max(0, value) * 0.5f);
Log($"SELinux enforcing halved attack {value}->{mitigated}");
return mitigated;
}
private void ResolveDelayedEndOfTurnEffects()
{
for (int i = delayedEffects.Count - 1; i >= 0; i--)
{
DelayedCombatEffect effect = delayedEffects[i];
effect.GarbageCollectionsRemaining--;
if (effect.GarbageCollectionsRemaining > 0)
{
delayedEffects[i] = effect;
continue;
}
delayedEffects.RemoveAt(i);
switch (effect.Kind)
{
case DelayedCombatEffectKind.UpdateManagerRepeat:
DealToAllEnemies(effect.Amount, Language.Python, "update manager repeat");
break;
case DelayedCombatEffectKind.TimeshiftRestore:
RestoreTimeshift(effect.Amount);
break;
}
}
}
private void DealToAllEnemies(int amount, Language language, string label)
{
List<CombatantState> targets = LivingEnemyStates();
for (int i = 0; i < targets.Count; i++)
{
damagePipeline.DealDamage(new DamageRequest(playerState, targets[i], amount, language, false, false));
}
Log($"{label}: dealt {amount} to all enemies");
RemoveDefeatedEnemies();
}
private void RestoreTimeshift(int snapshot)
{
if (playerState == null || playerState.IsDefeated || playerState.CurrentUptime >= snapshot)
{
return;
}
int target = snapshot;
if (IsDistro("mint") && runConfig.DistroVersion >= 3)
{
target = Mathf.Min(playerState.MaxUptime, snapshot + 2);
}
if (IsDistro("mint") && runConfig.DistroVersion >= 5)
{
target = playerState.MaxUptime;
}
int restored = target - playerState.CurrentUptime;
playerState.CurrentUptime = target;
Log($"timeshift restored {restored} uptime");
}
private bool CheckWinOrLoss()
{
return CheckLoss() || CheckWin();
@@ -944,5 +1191,35 @@ namespace KernelPanic.Combat
}
private bool IsCombatPaused => awaitingWaveContinue || runLost;
private enum DelayedCombatEffectKind
{
UpdateManagerRepeat,
TimeshiftRestore
}
private struct DelayedCombatEffect
{
private DelayedCombatEffect(DelayedCombatEffectKind kind, int amount, int garbageCollectionsRemaining)
{
Kind = kind;
Amount = amount;
GarbageCollectionsRemaining = garbageCollectionsRemaining;
}
public DelayedCombatEffectKind Kind { get; }
public int Amount { get; }
public int GarbageCollectionsRemaining { get; set; }
public static DelayedCombatEffect UpdateManagerRepeat(int amount, int garbageCollectionsRemaining)
{
return new DelayedCombatEffect(DelayedCombatEffectKind.UpdateManagerRepeat, amount, garbageCollectionsRemaining);
}
public static DelayedCombatEffect TimeshiftRestore(int uptime, int garbageCollectionsRemaining)
{
return new DelayedCombatEffect(DelayedCombatEffectKind.TimeshiftRestore, uptime, garbageCollectionsRemaining);
}
}
}
}
+1
View File
@@ -31,6 +31,7 @@ namespace KernelPanic.Combat
public bool AllowFlatDamageBuffs { get; set; }
public int FlatEffectBonus { get; set; }
public int DamageMultiplierPercent { get; set; } = 100;
public int IncomingAttackHalfCharges { get; set; }
public IReadOnlyList<StatusInstance> Statuses => _statuses;
public IReadOnlyDictionary<StatusType, int> ActiveStatuses => BuildStatusSnapshot();
+14
View File
@@ -100,6 +100,20 @@ namespace KernelPanic.Combat
}
}
public void AddToDrawPile(CardInstance card, bool shuffle)
{
if (card == null)
{
return;
}
_drawPile.Add(card);
if (shuffle)
{
ShuffleDrawPile();
}
}
public void Exhaust(CardInstance card)
{
if (card != null)
+4 -4
View File
@@ -41,7 +41,7 @@ namespace KernelPanic.Meta
{
1 => "first card each turn costs 1 less and deals +50%, with crash risk",
2 => "first-card damage bonus rises to +75%",
3 => "rawhide TODO: double permanent damage growth on non-crash first-card turn",
3 => "non-crashing first-card plays gain +1 effect this combat; rawhide doubles that growth",
4 => "crashes grant +1 Cycle that turn",
5 => "first two cards each turn get the discount, bonus, and independent crash rolls",
_ => "--"
@@ -50,12 +50,12 @@ namespace KernelPanic.Meta
{
1 => "random values roll max; no crits; ignores multiplicative damage buffs",
2 => "fixed effect values are treated as +2",
3 => "timeshift snapshot TODO: stronger restore when delayed restore exists",
3 => "timeshift snapshot restores 2 uptime beyond the snapshot, up to max uptime",
4 => "flat +damage can apply; multiplicative crit/mult remains ignored",
5 => "TODO: re-resolve reduced effect when enemy reduction/coercion exists",
5 => "timeshift snapshot restores to full uptime if lower",
_ => "--"
},
_ => safeVersion == 1 ? "base release" : $"TODO: V{safeVersion} release effect"
_ => safeVersion == 1 ? "base release" : $"V{safeVersion} release effect"
};
}
}
+16 -5
View File
@@ -120,6 +120,12 @@ namespace KernelPanic.UI
private IReadOnlyList<CardDefinition> BuildGeneratedCardPool()
{
List<CardDefinition> cards = new();
if (cardDatabase != null)
{
cards.AddRange(cardDatabase.AllCards);
return cards;
}
if (languageDeckDatabase == null)
{
return cards;
@@ -795,7 +801,7 @@ namespace KernelPanic.UI
button.AddToClassList("hand-card-selected");
}
if (CombatManager.GetCardCost(card) > combatManager.PlayerState.Cycles)
if (GetDisplayCardCost(card) > combatManager.PlayerState.Cycles)
{
button.AddToClassList("hand-card-unaffordable");
}
@@ -804,7 +810,7 @@ namespace KernelPanic.UI
return button;
}
private static VisualElement CardFaceView(CardInstance card)
private VisualElement CardFaceView(CardInstance card)
{
VisualElement face = new();
face.AddToClassList("hand-card");
@@ -812,7 +818,7 @@ namespace KernelPanic.UI
return face;
}
private static void PopulateCardFace(VisualElement target, CardInstance card)
private void PopulateCardFace(VisualElement target, CardInstance card)
{
string cardRarityClass = CardRarityClass(card.Definition.Rarity);
if (!string.IsNullOrEmpty(cardRarityClass))
@@ -822,7 +828,7 @@ namespace KernelPanic.UI
VisualElement top = new();
top.AddToClassList("card-top-row");
Label cost = new(CombatManager.GetCardCost(card).ToString());
Label cost = new(GetDisplayCardCost(card).ToString());
cost.AddToClassList("card-cost");
Label name = new($"{DisplayName(card.Definition)}{(card.IsUpgraded ? " +" : string.Empty)}");
name.AddToClassList("card-name");
@@ -1337,7 +1343,7 @@ namespace KernelPanic.UI
chip.AddToClassList(LanguageClass(card.Definition.Language));
Label name = new(DisplayName(card.Definition));
name.AddToClassList("card-chip-name");
Label meta = new($"{CombatManager.GetCardCost(card)}c / {TrackText(card.Definition.ResolutionTrack)}");
Label meta = new($"{GetDisplayCardCost(card)}c / {TrackText(card.Definition.ResolutionTrack)}");
meta.AddToClassList("card-chip-meta");
chip.Add(name);
chip.Add(meta);
@@ -1351,6 +1357,11 @@ namespace KernelPanic.UI
return label;
}
private int GetDisplayCardCost(CardInstance card)
{
return combatManager == null ? CombatManager.GetCardCost(card) : combatManager.GetEffectiveCardCost(card);
}
private static VisualElement CreatePanel(string title, string subtitle, float width)
{
VisualElement panel = new();