1179 lines
49 KiB
C#
1179 lines
49 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using KernelPanic.Core;
|
|
using KernelPanic.Data;
|
|
|
|
namespace KernelPanic.Combat
|
|
{
|
|
public sealed class DealDamageEffect : ICardEffect
|
|
{
|
|
private const int RustOverflowRollMax = 1000000;
|
|
private const string RustUnsigned64LimitText = "18446744073709551615";
|
|
|
|
private readonly int _minAmount;
|
|
private readonly int _maxAmount;
|
|
private readonly Language _language;
|
|
private readonly bool _trueDamage;
|
|
private readonly bool _canCrit;
|
|
private readonly bool _allEnemies;
|
|
|
|
public DealDamageEffect(int minAmount, int maxAmount, Language language, bool trueDamage = false, bool canCrit = true, bool allEnemies = false)
|
|
{
|
|
_minAmount = minAmount;
|
|
_maxAmount = maxAmount;
|
|
_language = language;
|
|
_trueDamage = trueDamage;
|
|
_canCrit = canCrit;
|
|
_allEnemies = allEnemies;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
IReadOnlyList<CombatantState> targets = _allEnemies
|
|
? context.Enemies.Where(enemy => enemy.State != null && !enemy.State.IsDefeated).Select(enemy => enemy.State).ToList()
|
|
: context.Targets;
|
|
|
|
if (StatusEffectController.Has(context.Source, StatusType.RaceCondition) && targets.Count > 1)
|
|
{
|
|
targets = ShuffleTargets(targets, context.Source);
|
|
}
|
|
|
|
bool dealtDamage = false;
|
|
for (int i = 0; i < targets.Count; i++)
|
|
{
|
|
CombatantState target = targets[i];
|
|
if (target == null || target.IsDefeated)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int minAmount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_minAmount, context), context.Card);
|
|
int maxAmount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_maxAmount, context), context.Card);
|
|
int amount = minAmount == maxAmount
|
|
? minAmount
|
|
: RandomRoll.RollRange(minAmount, maxAmount, new RollContext(context.Source));
|
|
bool rustUnsignedOverflow = IsRustDamage(context)
|
|
&& RandomRoll.RollRange(1, RustOverflowRollMax, new RollContext(context.Source)) == 1;
|
|
if (rustUnsignedOverflow)
|
|
{
|
|
amount = int.MaxValue;
|
|
context.CombatManager.ReportEffectResult($"rust unsigned overflow: {RustUnsigned64LimitText}");
|
|
}
|
|
|
|
bool rustOverkillToShield = IsRustDamage(context);
|
|
bool overkillSpills = IsSpillDamage(context);
|
|
int survivable = rustOverkillToShield || overkillSpills
|
|
? GetSurvivableDamage(target, _trueDamage)
|
|
: 0;
|
|
DamageResult result = context.DamagePipeline.DealDamage(new DamageRequest(context.Source, target, amount, _language, _trueDamage, _canCrit && !rustUnsignedOverflow));
|
|
dealtDamage |= result.IncomingAmount > 0;
|
|
if (rustOverkillToShield)
|
|
{
|
|
GrantRustOverkillShield(context, result, survivable);
|
|
}
|
|
|
|
if (overkillSpills)
|
|
{
|
|
dealtDamage |= SpillOverkillDamage(context, target, result, survivable);
|
|
}
|
|
}
|
|
|
|
SegfaultRecoil.TryApply(context, _language, dealtDamage);
|
|
}
|
|
|
|
private bool IsRustDamage(CombatContext context)
|
|
{
|
|
return _language == Language.Rust || context.Card?.Definition?.Language == Language.Rust;
|
|
}
|
|
|
|
private bool IsSpillDamage(CombatContext context)
|
|
{
|
|
Language cardLanguage = context.Card?.Definition == null ? _language : context.Card.Definition.Language;
|
|
return _language == Language.C
|
|
|| _language == Language.Java
|
|
|| cardLanguage == Language.C
|
|
|| cardLanguage == Language.Java;
|
|
}
|
|
|
|
private static int GetSurvivableDamage(CombatantState target, bool trueDamage)
|
|
{
|
|
return target == null ? 0 : trueDamage ? target.CurrentUptime : target.CurrentUptime + target.Shield;
|
|
}
|
|
|
|
private bool SpillOverkillDamage(CombatContext context, CombatantState firstTarget, DamageResult firstResult, int firstSurvivable)
|
|
{
|
|
int remaining = UnityEngine.Mathf.Max(0, firstResult.IncomingAmount - firstSurvivable);
|
|
if (remaining <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool dealtDamage = false;
|
|
for (int i = 0; i < context.Enemies.Count && remaining > 0; i++)
|
|
{
|
|
CombatantState spillTarget = context.Enemies[i].State;
|
|
if (spillTarget == null || spillTarget == firstTarget || spillTarget.IsDefeated)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int survivable = GetSurvivableDamage(spillTarget, _trueDamage);
|
|
DamageResult spillResult = context.DamagePipeline.DealDamage(new DamageRequest(context.Source, spillTarget, remaining, _language, _trueDamage, false, applySourceModifiers: false));
|
|
dealtDamage |= spillResult.IncomingAmount > 0;
|
|
remaining = UnityEngine.Mathf.Max(0, spillResult.IncomingAmount - survivable);
|
|
}
|
|
|
|
return dealtDamage;
|
|
}
|
|
|
|
private static void GrantRustOverkillShield(CombatContext context, DamageResult result, int survivable)
|
|
{
|
|
int overkill = UnityEngine.Mathf.Max(0, result.IncomingAmount - survivable);
|
|
if (overkill <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!context.CombatManager.CanPlayerReceiveCardShield(context.Card))
|
|
{
|
|
context.CombatManager.ReportEffectResult($"overkill shield blocked: {overkill}");
|
|
return;
|
|
}
|
|
|
|
context.CombatManager.GrantPlayerShield(overkill, "rust overkill");
|
|
}
|
|
|
|
private static IReadOnlyList<CombatantState> ShuffleTargets(IReadOnlyList<CombatantState> targets, CombatantState source)
|
|
{
|
|
List<CombatantState> shuffled = targets.ToList();
|
|
for (int i = shuffled.Count - 1; i > 0; i--)
|
|
{
|
|
int swapIndex = RandomRoll.RollRange(0, i, new RollContext(source));
|
|
(shuffled[i], shuffled[swapIndex]) = (shuffled[swapIndex], shuffled[i]);
|
|
}
|
|
|
|
return shuffled;
|
|
}
|
|
}
|
|
|
|
public sealed class ShieldEffect : ICardEffect
|
|
{
|
|
private readonly int _amount;
|
|
|
|
public ShieldEffect(int amount)
|
|
{
|
|
_amount = amount;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
int amount = UpgradeMath.ScaleShield(UpgradeMath.ApplySourceFlatBonus(_amount, context), context.Card);
|
|
if (!context.CombatManager.CanPlayerReceiveCardShield(context.Card))
|
|
{
|
|
context.CombatManager.ReportEffectResult($"shield blocked: {amount}");
|
|
return;
|
|
}
|
|
|
|
context.CombatManager.GrantPlayerShield(amount, "card");
|
|
}
|
|
}
|
|
|
|
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);
|
|
if (!context.CombatManager.CanPlayerReceiveCardShield(context.Card))
|
|
{
|
|
context.CombatManager.ReportEffectResult($"shield blocked: {amount}");
|
|
return;
|
|
}
|
|
|
|
context.CombatManager.GrantPlayerShield(amount, "card");
|
|
}
|
|
}
|
|
|
|
public sealed class ChanceDamageEffect : ICardEffect
|
|
{
|
|
private readonly int _damageAmount;
|
|
private readonly int _successPercent;
|
|
private readonly Language _language;
|
|
|
|
public ChanceDamageEffect(int damageAmount, int successPercent, Language language)
|
|
{
|
|
_damageAmount = damageAmount;
|
|
_successPercent = successPercent;
|
|
_language = language;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
for (int i = 0; i < context.Targets.Count; i++)
|
|
{
|
|
CombatantState target = context.Targets[i];
|
|
if (target == null || target.IsDefeated)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int roll = context.Source != null && context.Source.ForceMaxRolls
|
|
? 1
|
|
: RandomRoll.RollRange(1, 100, RollContext.None);
|
|
int damageAmount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_damageAmount, context), context.Card);
|
|
int amount = roll <= _successPercent ? damageAmount : 0;
|
|
DamageResult result = context.DamagePipeline.DealDamage(new DamageRequest(context.Source, target, amount, _language, false, true));
|
|
SegfaultRecoil.TryApply(context, _language, result.IncomingAmount > 0);
|
|
context.CombatManager.ReportEffectResult(amount == 0 ? "typeof -> NaN (0)" : $"typeof -> {amount}");
|
|
}
|
|
}
|
|
}
|
|
|
|
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 PreviousLanguageDamageEffect : ICardEffect
|
|
{
|
|
private readonly int _baseAmount;
|
|
private readonly int _matchingAmount;
|
|
private readonly Language _language;
|
|
|
|
public PreviousLanguageDamageEffect(int baseAmount, int matchingAmount, Language language)
|
|
{
|
|
_baseAmount = baseAmount;
|
|
_matchingAmount = matchingAmount;
|
|
_language = language;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
bool matches = context.Source != null
|
|
&& context.Source.HasPreviousPlayedCardLanguage
|
|
&& context.Source.PreviousPlayedCardLanguage == _language;
|
|
int amount = matches ? _matchingAmount : _baseAmount;
|
|
new DealDamageEffect(amount, amount, _language).Execute(context);
|
|
}
|
|
}
|
|
|
|
public sealed class BtwScaledDamageEffect : ICardEffect
|
|
{
|
|
private readonly int _baseAmount;
|
|
private readonly int _perBtw;
|
|
private readonly Language _language;
|
|
|
|
public BtwScaledDamageEffect(int baseAmount, int perBtw, Language language)
|
|
{
|
|
_baseAmount = baseAmount;
|
|
_perBtw = perBtw;
|
|
_language = language;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
int btw = UnityEngine.Mathf.Max(0, context.Source?.ArchBtwStacks ?? 0);
|
|
int perBtw = UnityEngine.Mathf.Max(_perBtw, context.Source?.ArchMakepkgBtwMultiplier ?? _perBtw);
|
|
int amount = _baseAmount + (btw * perBtw);
|
|
new DealDamageEffect(amount, amount, _language).Execute(context);
|
|
}
|
|
}
|
|
|
|
public sealed class RandomCommonLanguageCardEffect : ICardEffect
|
|
{
|
|
public void Execute(CombatContext context)
|
|
{
|
|
Language language = RandomRoll.RollRange(0, 1, new RollContext(context.Source)) == 0 ? Language.C : Language.Rust;
|
|
if (!context.CombatManager.TryCreateGeneratedCard(language, Rarity.Common, out CardInstance generated))
|
|
{
|
|
context.CombatManager.ReportEffectResult("TODO card-generation: no C/Rust common available");
|
|
return;
|
|
}
|
|
|
|
generated.TemporaryCostDelta -= CombatManager.GetCardCost(generated);
|
|
if (context.HandController.Add(generated))
|
|
{
|
|
context.CombatManager.ReportEffectResult($"AUR helper: generated {GetCardName(generated)} at 0c");
|
|
}
|
|
else
|
|
{
|
|
context.CombatManager.ReportEffectResult("AUR helper: hand full");
|
|
}
|
|
}
|
|
|
|
private static string GetCardName(CardInstance card)
|
|
{
|
|
return card?.Definition == null || string.IsNullOrWhiteSpace(card.Definition.DisplayName) ? card?.Definition?.Id ?? "--" : card.Definition.DisplayName;
|
|
}
|
|
}
|
|
|
|
public sealed class MinimalInstallEffect : ICardEffect
|
|
{
|
|
public void Execute(CombatContext context)
|
|
{
|
|
int exhausted = 0;
|
|
List<CardInstance> candidates = context.HandController.Cards.Where(card => card != null).Take(2).ToList();
|
|
for (int i = 0; i < candidates.Count; i++)
|
|
{
|
|
CardInstance card = candidates[i];
|
|
if (!context.HandController.Remove(card))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
context.DeckController.Exhaust(card);
|
|
context.CombatManager.HandleCardExhausted(card);
|
|
exhausted++;
|
|
}
|
|
|
|
if (exhausted <= 0)
|
|
{
|
|
context.CombatManager.ReportEffectResult("minimal install: no hand cards to exhaust");
|
|
return;
|
|
}
|
|
|
|
int shield = exhausted * 3;
|
|
if (context.CombatManager.CanPlayerReceiveCardShield(context.Card))
|
|
{
|
|
context.CombatManager.GrantPlayerShield(shield, "minimal install");
|
|
}
|
|
|
|
context.Source.Cycles += exhausted;
|
|
context.CombatManager.ReportEffectResult($"minimal install: exhausted {exhausted}, +{exhausted} Cycle");
|
|
}
|
|
}
|
|
|
|
public sealed class SizeofDamageEffect : ICardEffect
|
|
{
|
|
public void Execute(CombatContext context)
|
|
{
|
|
int cCards = context.HandController.Cards.Count(card => card?.Definition != null && card.Definition.Language == Language.C);
|
|
int amount = 5 * cCards;
|
|
new DealDamageEffect(amount, amount, Language.C).Execute(context);
|
|
}
|
|
}
|
|
|
|
public sealed class ConsultWikiEffect : ICardEffect
|
|
{
|
|
public void Execute(CombatContext context)
|
|
{
|
|
CardInstance broken = context.HandController.Cards.FirstOrDefault(card => card != null && card.IsBroken);
|
|
if (broken != null)
|
|
{
|
|
broken.IsBroken = false;
|
|
context.CombatManager.DrawCards(1, "consult the wiki");
|
|
context.CombatManager.ReportEffectResult("fixed a broken card");
|
|
return;
|
|
}
|
|
|
|
new DealDamageEffect(6, 6, Language.Rust).Execute(context);
|
|
}
|
|
}
|
|
|
|
|
|
public sealed class ApplyStatusEffect : ICardEffect
|
|
{
|
|
private readonly StatusType _statusType;
|
|
private readonly int _stacks;
|
|
private readonly int _duration;
|
|
private readonly bool _targetSelf;
|
|
private readonly bool _skipNextTick;
|
|
|
|
public ApplyStatusEffect(StatusType statusType, int stacks, int duration, bool targetSelf, bool skipNextTick = false)
|
|
{
|
|
_statusType = statusType;
|
|
_stacks = stacks;
|
|
_duration = duration;
|
|
_targetSelf = targetSelf;
|
|
_skipNextTick = skipNextTick;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
if (_targetSelf)
|
|
{
|
|
if (StatusEffectController.GetDescriptor(_statusType).IsBeneficial
|
|
&& !context.CombatManager.CanPlayerReceiveCardBuff(context.Card))
|
|
{
|
|
context.CombatManager.ReportEffectResult($"{_statusType} blocked");
|
|
return;
|
|
}
|
|
|
|
context.StatusEffects.Apply(context.Source, _statusType, UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_stacks, context), context.Card), _duration, context.Source, _skipNextTick);
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < context.Targets.Count; i++)
|
|
{
|
|
context.StatusEffects.Apply(context.Targets[i], _statusType, UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_stacks, context), context.Card), _duration, context.Source, _skipNextTick);
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class CleanseEffect : ICardEffect
|
|
{
|
|
private readonly bool _harmfulOnly;
|
|
|
|
public CleanseEffect(bool harmfulOnly)
|
|
{
|
|
_harmfulOnly = harmfulOnly;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
if (_harmfulOnly)
|
|
{
|
|
context.StatusEffects.CleanseHarmful(context.Source);
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class DrawEffect : ICardEffect
|
|
{
|
|
private readonly int _count;
|
|
|
|
public DrawEffect(int count)
|
|
{
|
|
_count = count;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
int room = context.HandController.RemainingRam;
|
|
if (room <= 0)
|
|
{
|
|
context.CombatManager.ReportEffectResult("hand full: drew 0");
|
|
return;
|
|
}
|
|
|
|
// RAM is a hard hand cap. Draw effects only pull cards that can fit; blocked cards stay in the draw pile.
|
|
int baseCount = _count;
|
|
if (context.Card?.Definition?.Id == "ubuntu_ask_ubuntu"
|
|
&& string.Equals(context.CombatManager.RunConfig?.Distro?.Id, "ubuntu", System.StringComparison.OrdinalIgnoreCase)
|
|
&& context.CombatManager.RunConfig.DistroVersion >= 3)
|
|
{
|
|
baseCount = 3;
|
|
}
|
|
|
|
int requested = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(baseCount, context), context.Card);
|
|
int drawCount = UnityEngine.Mathf.Min(requested, room);
|
|
IReadOnlyList<CardInstance> drawn = context.DeckController.Draw(drawCount);
|
|
int added = 0;
|
|
for (int i = 0; i < drawn.Count; i++)
|
|
{
|
|
if (context.HandController.Add(drawn[i]))
|
|
{
|
|
added++;
|
|
}
|
|
else
|
|
{
|
|
context.DeckController.AddToDrawPile(drawn[i], shuffle: false);
|
|
}
|
|
}
|
|
|
|
string capped = drawCount < requested ? " (hand full)" : string.Empty;
|
|
context.CombatManager.ReportEffectResult($"drew {added}{capped}");
|
|
}
|
|
}
|
|
|
|
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.RemainingRam <= 0)
|
|
{
|
|
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.CanAdd(copy))
|
|
{
|
|
context.CombatManager.ReportEffectResult("not enough RAM to copy");
|
|
return;
|
|
}
|
|
|
|
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;
|
|
private readonly int _maxCount;
|
|
|
|
public UpgradeHandCardsEffect(int minCount, int maxCount)
|
|
{
|
|
_minCount = minCount;
|
|
_maxCount = maxCount;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
int minCount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_minCount, context), context.Card);
|
|
int maxCount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_maxCount, context), context.Card);
|
|
int targetCount = minCount == maxCount
|
|
? minCount
|
|
: RandomRoll.RollRange(minCount, maxCount, new RollContext(context.Source));
|
|
|
|
List<CardInstance> candidates = context.HandController.Cards
|
|
.Where(card => card != null && card.CanUpgrade)
|
|
.ToList();
|
|
int upgraded = 0;
|
|
while (upgraded < targetCount && candidates.Count > 0)
|
|
{
|
|
int index = RandomRoll.RollRange(0, candidates.Count - 1, new RollContext(context.Source));
|
|
CardInstance card = candidates[index];
|
|
candidates.RemoveAt(index);
|
|
if (card.Upgrade())
|
|
{
|
|
upgraded++;
|
|
}
|
|
}
|
|
|
|
context.CombatManager.ReportEffectResult(upgraded == 0 ? "no cards upgraded" : $"upgraded {upgraded} card(s)");
|
|
}
|
|
}
|
|
|
|
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;
|
|
private readonly Rarity _rarity;
|
|
|
|
public GenerateCardEffect(Language language, Rarity rarity)
|
|
{
|
|
_language = language;
|
|
_rarity = rarity;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
if (context.HandController.RemainingRam <= 0)
|
|
{
|
|
context.CombatManager.ReportEffectResult("hand full: generated 0");
|
|
return;
|
|
}
|
|
|
|
if (!context.CombatManager.TryCreateGeneratedCard(_language, _rarity, out CardInstance generatedCard))
|
|
{
|
|
context.CombatManager.ReportEffectResult($"no {_rarity.ToString().ToLowerInvariant()} {_language} card pool");
|
|
return;
|
|
}
|
|
|
|
if (!context.HandController.CanAdd(generatedCard))
|
|
{
|
|
context.CombatManager.ReportEffectResult("not enough RAM: generated 0");
|
|
}
|
|
else if (context.HandController.Add(generatedCard))
|
|
{
|
|
context.CombatManager.ReportEffectResult($"generated {GetCardName(generatedCard)} at 0c");
|
|
}
|
|
else
|
|
{
|
|
context.CombatManager.ReportEffectResult("hand full: generated 0");
|
|
}
|
|
}
|
|
|
|
private static string GetCardName(CardInstance card)
|
|
{
|
|
if (card?.Definition == null)
|
|
{
|
|
return "--";
|
|
}
|
|
|
|
return string.IsNullOrWhiteSpace(card.Definition.DisplayName) ? card.Definition.Id : card.Definition.DisplayName;
|
|
}
|
|
}
|
|
|
|
public sealed class OverkillToShieldDamageEffect : ICardEffect
|
|
{
|
|
private readonly int _amount;
|
|
private readonly Language _language;
|
|
|
|
public OverkillToShieldDamageEffect(int amount, Language language)
|
|
{
|
|
_amount = amount;
|
|
_language = language;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
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;
|
|
DamageResult result = context.DamagePipeline.DealDamage(new DamageRequest(context.Source, target, amount, _language, false, true));
|
|
SegfaultRecoil.TryApply(context, _language, result.IncomingAmount > 0);
|
|
int overkill = UnityEngine.Mathf.Max(0, result.IncomingAmount - survivable);
|
|
if (overkill > 0)
|
|
{
|
|
if (!context.CombatManager.CanPlayerReceiveCardShield(context.Card))
|
|
{
|
|
context.CombatManager.ReportEffectResult($"overkill shield blocked: {overkill}");
|
|
continue;
|
|
}
|
|
|
|
context.CombatManager.GrantPlayerShield(overkill, "overkill");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 FirstToPackageEffect : ICardEffect
|
|
{
|
|
private readonly int _amount;
|
|
|
|
public FirstToPackageEffect(int amount)
|
|
{
|
|
_amount = amount;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
int amount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_amount, context), context.Card);
|
|
new DealDamageEffect(amount, amount, Language.Java).Execute(context);
|
|
context.CombatManager.AddJavaCostDiscountThisTurn(1);
|
|
}
|
|
}
|
|
|
|
public sealed class DnfAutoremoveEffect : ICardEffect
|
|
{
|
|
private readonly int _damageAmount;
|
|
private readonly int _cycleGain;
|
|
|
|
public DnfAutoremoveEffect(int damageAmount, int cycleGain)
|
|
{
|
|
_damageAmount = damageAmount;
|
|
_cycleGain = cycleGain;
|
|
}
|
|
|
|
public void Execute(CombatContext context)
|
|
{
|
|
CardInstance junk = FindJunkCard(context);
|
|
if (junk == null)
|
|
{
|
|
context.CombatManager.ReportEffectResult("dnf autoremove: no orphaned package");
|
|
return;
|
|
}
|
|
|
|
int damageAmount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_damageAmount, context), context.Card);
|
|
new DealDamageEffect(damageAmount, damageAmount, Language.Rust).Execute(context);
|
|
if (context.HandController.Remove(junk))
|
|
{
|
|
context.DeckController.Exhaust(junk);
|
|
context.CombatManager.HandleCardExhausted(junk);
|
|
int cycleGain = UpgradeMath.ScaleAmount(_cycleGain, context.Card);
|
|
context.Source.Cycles += cycleGain;
|
|
context.CombatManager.ReportEffectResult($"autoremove exhausted {GetCardName(junk)}: +{cycleGain} cycle");
|
|
}
|
|
}
|
|
|
|
private static CardInstance FindJunkCard(CombatContext context)
|
|
{
|
|
for (int i = 0; i < context.HandController.Cards.Count; i++)
|
|
{
|
|
CardInstance card = context.HandController.Cards[i];
|
|
if (IsJunk(card))
|
|
{
|
|
return card;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool IsJunk(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", System.StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| id.IndexOf("nop", System.StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| name.IndexOf("junk", System.StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| name.IndexOf("NOP", System.StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| name.IndexOf("broken", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
|
}
|
|
|
|
private static string GetCardName(CardInstance card)
|
|
{
|
|
return string.IsNullOrWhiteSpace(card?.Definition?.DisplayName) ? card?.Definition?.Id ?? "--" : card.Definition.DisplayName;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
internal static class SegfaultRecoil
|
|
{
|
|
private const int DefaultPercent = 20;
|
|
private const int DefaultUptimeDamage = 3;
|
|
|
|
public static void TryApply(CombatContext context, Language damageLanguage, bool dealtDamage)
|
|
{
|
|
if (!dealtDamage
|
|
|| context == null
|
|
|| context.SegfaultRecoilRolled
|
|
|| context.Source == null
|
|
|| context.Source.IsDefeated
|
|
|| context.Card?.Definition?.Language != Language.C
|
|
|| damageLanguage != Language.C)
|
|
{
|
|
return;
|
|
}
|
|
|
|
context.SegfaultRecoilRolled = true;
|
|
GetProfile(context.Card, out int percent, out int uptimeDamage, out bool drawOnSegfault);
|
|
if (RandomRoll.RollRange(1, 100, new RollContext(context.Source)) > percent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DamageResult result = context.DamagePipeline.DealDamage(new DamageRequest(null, context.Source, uptimeDamage, Language.C, true, false));
|
|
context.CombatManager.ReportEffectResult($"segfault: uptime -{result.FinalAmount}");
|
|
context.CombatManager.TryCorruptRandomHandCard("segfault");
|
|
if (drawOnSegfault)
|
|
{
|
|
context.CombatManager.DrawCards(2, "free() recoil");
|
|
}
|
|
}
|
|
|
|
private static void GetProfile(CardInstance card, out int percent, out int uptimeDamage, out bool drawOnSegfault)
|
|
{
|
|
percent = DefaultPercent;
|
|
uptimeDamage = DefaultUptimeDamage;
|
|
drawOnSegfault = false;
|
|
|
|
switch (card?.Definition?.Id)
|
|
{
|
|
case "shop_c_free":
|
|
drawOnSegfault = true;
|
|
break;
|
|
case "shop_c_kernel_exploit":
|
|
percent = 50;
|
|
uptimeDamage = 8;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class CardEffectFactory
|
|
{
|
|
public static IReadOnlyList<ICardEffect> CreateEffects(CardDefinition definition)
|
|
{
|
|
List<ICardEffect> serializedEffects = CreateSerializedEffects(definition);
|
|
if (serializedEffects.Count > 0)
|
|
{
|
|
return serializedEffects;
|
|
}
|
|
|
|
return definition?.Id switch
|
|
{
|
|
"lang_py_print" => One(new DealDamageEffect(3, 3, Language.Python)),
|
|
"lang_py_import_antigravity" => One(new DrawEffect(2)),
|
|
"lang_py_for_loop" => One(new DealDamageEffect(2, 2, Language.Python, allEnemies: true)),
|
|
"lang_js_console_log" => One(new DealDamageEffect(3, 9, Language.JavaScript)),
|
|
"lang_js_fetch" => One(new DealDamageEffect(5, 13, Language.JavaScript)),
|
|
"lang_rs_fn_main" => One(new DealDamageEffect(6, 6, Language.Rust)),
|
|
"lang_rs_unwrap" => new ICardEffect[]
|
|
{
|
|
new DealDamageEffect(9, 9, Language.Rust),
|
|
new DrawEffect(1)
|
|
},
|
|
"lang_rs_borrow" => One(new ShieldEffect(5)),
|
|
"lang_c_malloc" => One(new DealDamageEffect(12, 12, Language.C)),
|
|
"lang_c_printf" => One(new DealDamageEffect(7, 7, Language.C)),
|
|
"lang_c_pointer" => One(new PreviousLanguageDamageEffect(9, 15, Language.C)),
|
|
"lang_java_public_static_main" => One(new DealDamageEffect(7, 7, Language.Java)),
|
|
"lang_java_system_out_println" => One(new DealDamageEffect(5, 5, Language.Java)),
|
|
"lang_java_new_object" => One(new DealDamageEffect(3, 3, Language.Java)),
|
|
"ubuntu_ask_ubuntu" => One(new DrawEffect(2)),
|
|
"ubuntu_unattended_upgrades" => new ICardEffect[]
|
|
{
|
|
new ShieldEffect(4),
|
|
new ApplyStatusEffect(StatusType.UnattendedUpgrades, 4, 2, targetSelf: true, skipNextTick: true)
|
|
},
|
|
"mint_fix_broken" => new ICardEffect[]
|
|
{
|
|
new DealDamageEffect(8, 8, Language.JavaScript),
|
|
new CleanseEffect(harmfulOnly: true)
|
|
},
|
|
"lang_js_typeof" => One(new ChanceDamageEffect(8, 75, Language.JavaScript)),
|
|
"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[]
|
|
{
|
|
new DealDamageEffect(4, 10, Language.JavaScript),
|
|
new DealDamageEffect(4, 10, Language.JavaScript),
|
|
new DealDamageEffect(4, 10, Language.JavaScript)
|
|
},
|
|
"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" => 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_dnf_autoremove" => One(new DnfAutoremoveEffect(7, 1)),
|
|
"fedora_first_to_package" => One(new FirstToPackageEffect(7)),
|
|
"fedora_dnf_update" => One(new DealDamageEffect(9, 9, Language.Java)),
|
|
"fedora_rawhide" => One(new RawhideEffect()),
|
|
"fedora_selinux" => One(new IncomingAttackHalveEffect(2)),
|
|
"arch_pacman_syu" => One(new DealDamageEffect(14, 14, Language.C)),
|
|
"arch_consult_wiki" => One(new ConsultWikiEffect()),
|
|
"arch_makepkg" => One(new BtwScaledDamageEffect(7, 3, Language.C)),
|
|
"arch_aur_helper" => One(new RandomCommonLanguageCardEffect()),
|
|
"arch_minimal_install" => One(new MinimalInstallEffect()),
|
|
"shop_c_free" => One(new DealDamageEffect(18, 18, Language.C)),
|
|
"shop_c_sizeof" => One(new SizeofDamageEffect()),
|
|
"shop_c_kernel_exploit" => One(new DealDamageEffect(36, 36, Language.C, true)),
|
|
_ => 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 "lang_rs_fn_main" or "lang_rs_unwrap" or "lang_c_malloc" or "lang_c_printf" or "lang_c_pointer" or "lang_java_public_static_main"
|
|
or "lang_java_system_out_println" or "lang_java_new_object"
|
|
or "shop_py_list_append" or "shop_js_math_random" or "shop_js_promise_all" or "ubuntu_snap_install"
|
|
or "mint_nemo" or "fedora_dnf_autoremove" or "fedora_first_to_package" or "fedora_dnf_update"
|
|
or "arch_pacman_syu" or "arch_consult_wiki" or "arch_makepkg" or "shop_c_free" or "shop_c_sizeof" or "shop_c_kernel_exploit";
|
|
}
|
|
|
|
public static bool TargetsAllEnemies(CardDefinition definition)
|
|
{
|
|
return definition?.Id is "lang_py_for_loop";
|
|
}
|
|
|
|
private static List<ICardEffect> CreateSerializedEffects(CardDefinition definition)
|
|
{
|
|
List<ICardEffect> effects = new();
|
|
if (definition?.Effects == null)
|
|
{
|
|
return effects;
|
|
}
|
|
|
|
for (int i = 0; i < definition.Effects.Count; i++)
|
|
{
|
|
ICardEffect effect = definition.Effects[i] == null ? null : definition.Effects[i].CreateRuntimeEffect();
|
|
if (effect != null)
|
|
{
|
|
effects.Add(effect);
|
|
}
|
|
}
|
|
|
|
return effects;
|
|
}
|
|
|
|
private static IReadOnlyList<ICardEffect> One(ICardEffect effect)
|
|
{
|
|
return new[] { effect };
|
|
}
|
|
|
|
private static IReadOnlyList<ICardEffect> Todo(string message)
|
|
{
|
|
return One(new NoOpCardEffect(message));
|
|
}
|
|
|
|
public static string GetRulesText(CardInstance card)
|
|
{
|
|
if (card?.Definition == null)
|
|
{
|
|
return "--";
|
|
}
|
|
|
|
int level = card.UpgradeLevel;
|
|
string marker = level > 0 ? " [upgraded]" : string.Empty;
|
|
return card.Definition.Id switch
|
|
{
|
|
"lang_py_print" => $"queue: deal {UpgradeMath.ScaleAmount(3, card)}.{marker}",
|
|
"lang_py_import_antigravity" => $"queue: draw {UpgradeMath.ScaleAmount(2, card)}.{marker}",
|
|
"lang_py_for_loop" => $"queue: deal {UpgradeMath.ScaleAmount(2, card)} to all enemies.{marker}",
|
|
"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}",
|
|
"lang_rs_fn_main" => $"deal {UpgradeMath.ScaleAmount(6, card)}. overkill becomes shield.{marker}",
|
|
"lang_rs_unwrap" => $"deal {UpgradeMath.ScaleAmount(9, card)}. overkill becomes shield. draw {UpgradeMath.ScaleAmount(1, card)}.{marker}",
|
|
"lang_rs_borrow" => $"gain {UpgradeMath.ScaleShield(5, card)} shield.{marker}",
|
|
"lang_c_malloc" => $"deal {UpgradeMath.ScaleAmount(12, card)}. overkill spills. 20%: segfault, uptime -3 and corrupt a hand card.{marker}",
|
|
"lang_c_printf" => $"deal {UpgradeMath.ScaleAmount(7, card)}. overkill spills. 20%: segfault, uptime -3 and corrupt a hand card.{marker}",
|
|
"lang_c_pointer" => $"deal {UpgradeMath.ScaleAmount(9, card)}; {UpgradeMath.ScaleAmount(15, card)} if the previous card was C. overkill spills. 20%: segfault, uptime -3 and corrupt a hand card.{marker}",
|
|
"lang_java_public_static_main" => $"uses 2 RAM. deal {UpgradeMath.ScaleAmount(7, card)}. overkill spills. JIT: Java cards cost 1 less this combat each time you play a Java card; discount decays by 1 after each wave.{marker}",
|
|
"lang_java_system_out_println" => $"uses 2 RAM. deal {UpgradeMath.ScaleAmount(5, card)}. overkill spills. JIT applies; discount decays by 1 after each wave.{marker}",
|
|
"lang_java_new_object" => $"uses 2 RAM. deal {UpgradeMath.ScaleAmount(3, card)}. overkill spills. JIT applies; discount decays by 1 after each wave.{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" => $"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}",
|
|
"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_dnf_autoremove" => $"deal {UpgradeMath.ScaleAmount(7, card)}. exhaust a junk/NOP/broken card from hand; if you do, gain {UpgradeMath.ScaleAmount(1, card)} cycle. if not, do nothing.{marker}",
|
|
"fedora_first_to_package" => $"deal {UpgradeMath.ScaleAmount(7, card)}. Java cards cost 1 less this turn.{marker}",
|
|
"fedora_dnf_update" => $"deal {UpgradeMath.ScaleAmount(9, card)}. overkill spills. 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}",
|
|
"arch_pacman_syu" => $"deal {UpgradeMath.ScaleAmount(14, card)}. overkill spills. 20%: segfault, uptime -3 and corrupt a hand card.{marker}",
|
|
"arch_consult_wiki" => $"fix a broken card and draw {UpgradeMath.ScaleAmount(1, card)}; if none, deal {UpgradeMath.ScaleAmount(6, card)}.{marker}",
|
|
"arch_makepkg" => $"deal {UpgradeMath.ScaleAmount(7, card)} + btw x3; x4 at Arch v3+. overkill spills. 20%: segfault, uptime -3 and corrupt a hand card.{marker}",
|
|
"arch_aur_helper" => $"add a random C/Rust common card to hand; it costs 0 this turn.{marker}",
|
|
"arch_minimal_install" => $"exhaust up to 2 hand cards. per card: gain 3 Shield and 1 Cycle.{marker}",
|
|
"shop_c_free" => $"deal {UpgradeMath.ScaleAmount(18, card)}. overkill spills. 20%: segfault, uptime -3, corrupt a hand card, and draw 2.{marker}",
|
|
"shop_c_sizeof" => $"deal 5 x C cards in hand. overkill spills. 20%: segfault, uptime -3 and corrupt a hand card.{marker}",
|
|
"shop_c_kernel_exploit" => $"deal {UpgradeMath.ScaleAmount(36, card)} ignoring Shield. overkill spills. 50%: segfault, uptime -8 and corrupt a hand card.{marker}",
|
|
_ => string.IsNullOrWhiteSpace(card.Definition.Description) ? "effect not authored." : $"{card.Definition.Description}{marker}"
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
public static class UpgradeMath
|
|
{
|
|
public static int ScaleAmount(int baseAmount, CardInstance card)
|
|
{
|
|
return baseAmount + UnityEngine.Mathf.Max(0, card?.MagnitudeBonus ?? 0);
|
|
}
|
|
|
|
public static int ScaleShield(int baseAmount, CardInstance card)
|
|
{
|
|
return baseAmount + UnityEngine.Mathf.Max(0, card?.MagnitudeBonus ?? 0);
|
|
}
|
|
|
|
public static int ApplySourceFlatBonus(int amount, CombatContext context)
|
|
{
|
|
return amount + UnityEngine.Mathf.Max(0, context?.Source?.FlatEffectBonus ?? 0);
|
|
}
|
|
}
|
|
}
|