diff --git a/.idea/.idea.linuxdistrosbuttheyarebreedable/.idea/workspace.xml b/.idea/.idea.linuxdistrosbuttheyarebreedable/.idea/workspace.xml
index 315ec41..4126be3 100644
--- a/.idea/.idea.linuxdistrosbuttheyarebreedable/.idea/workspace.xml
+++ b/.idea/.idea.linuxdistrosbuttheyarebreedable/.idea/workspace.xml
@@ -11,30 +11,40 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
+
+
-
-
@@ -66,15 +76,22 @@
}
}
+
+
+
+
+
+
+
@@ -111,23 +128,23 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -138,34 +155,34 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
@@ -258,7 +275,8 @@
-
+
+
@@ -484,7 +502,15 @@
1783614775509
-
+
+
+ 1783620160977
+
+
+
+ 1783620160977
+
+
@@ -500,7 +526,6 @@
-
@@ -525,7 +550,8 @@
-
+
+
diff --git a/Assets/Data/Cards/Arch/AurHelper.asset b/Assets/Data/Cards/Arch/AurHelper.asset
new file mode 100644
index 0000000..87576a4
--- /dev/null
+++ b/Assets/Data/Cards/Arch/AurHelper.asset
@@ -0,0 +1,28 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5178747fac4dbfc448ff15d8a687e03c, type: 3}
+ m_Name: AurHelper
+ m_EditorClassIdentifier:
+ id: arch_aur_helper
+ displayName: AUR helper
+ description: add a random common C or Rust card to hand. it costs 0 this turn.
+ flavorText: definitely read the PKGBUILD. definitely.
+ language: 2
+ rarity: 1
+ cycleCost: 1
+ resolutionTrack: 0
+ distroExclusive: 1
+ isToken: 0
+ isRunOnly: 0
+ designNotes: Arch exclusive. Uses existing generated-card pool; reports TODO if C/Rust commons are unavailable.
+ requiresSystem: []
+ upgradeTarget: {fileID: 0}
+ effects: []
diff --git a/Assets/Data/Cards/Arch/AurHelper.asset.meta b/Assets/Data/Cards/Arch/AurHelper.asset.meta
new file mode 100644
index 0000000..58c5d30
--- /dev/null
+++ b/Assets/Data/Cards/Arch/AurHelper.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: aa110000000000000000000000000004
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Cards/Arch/ConsultWiki.asset b/Assets/Data/Cards/Arch/ConsultWiki.asset
new file mode 100644
index 0000000..b1a30b6
--- /dev/null
+++ b/Assets/Data/Cards/Arch/ConsultWiki.asset
@@ -0,0 +1,30 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5178747fac4dbfc448ff15d8a687e03c, type: 3}
+ m_Name: ConsultWiki
+ m_EditorClassIdentifier:
+ id: arch_consult_wiki
+ displayName: Consult the Wiki
+ description: fix a broken card and draw 1. if none are broken, deal 6.
+ flavorText: the answer was in a footnote from 2013.
+ language: 2
+ rarity: 0
+ cycleCost: 0
+ resolutionTrack: 0
+ distroExclusive: 1
+ isToken: 0
+ isRunOnly: 0
+ designNotes: Arch exclusive. Uses existing IsBroken flag if present; card-break creation is TODO, draw/damage fallback works now.
+ requiresSystem:
+ - card-break
+ upgradeTarget: {fileID: 0}
+ effects: []
diff --git a/Assets/Data/Cards/Arch/ConsultWiki.asset.meta b/Assets/Data/Cards/Arch/ConsultWiki.asset.meta
new file mode 100644
index 0000000..4fd8ab4
--- /dev/null
+++ b/Assets/Data/Cards/Arch/ConsultWiki.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: aa110000000000000000000000000002
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Cards/Arch/Makepkg.asset b/Assets/Data/Cards/Arch/Makepkg.asset
new file mode 100644
index 0000000..46d869e
--- /dev/null
+++ b/Assets/Data/Cards/Arch/Makepkg.asset
@@ -0,0 +1,28 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5178747fac4dbfc448ff15d8a687e03c, type: 3}
+ m_Name: Makepkg
+ m_EditorClassIdentifier:
+ id: arch_makepkg
+ displayName: makepkg
+ description: deal 7 + btw stacks x3; x4 at Arch v3+. overkill spills. 20% segfault: uptime -3 and corrupt a random card in hand.
+ flavorText: it built on my machine, which is the only machine.
+ language: 0
+ rarity: 1
+ cycleCost: 2
+ resolutionTrack: 0
+ distroExclusive: 1
+ isToken: 0
+ isRunOnly: 0
+ designNotes: Arch exclusive. Reads CombatantState.ArchBtwStacks, then damage still routes through DamagePipeline and receives normal btw flat bonus too.
+ requiresSystem: []
+ upgradeTarget: {fileID: 0}
+ effects: []
diff --git a/Assets/Data/Cards/Arch/Makepkg.asset.meta b/Assets/Data/Cards/Arch/Makepkg.asset.meta
new file mode 100644
index 0000000..2123770
--- /dev/null
+++ b/Assets/Data/Cards/Arch/Makepkg.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: aa110000000000000000000000000003
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Cards/Arch/MinimalInstall.asset b/Assets/Data/Cards/Arch/MinimalInstall.asset
new file mode 100644
index 0000000..f3cbd0c
--- /dev/null
+++ b/Assets/Data/Cards/Arch/MinimalInstall.asset
@@ -0,0 +1,28 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5178747fac4dbfc448ff15d8a687e03c, type: 3}
+ m_Name: MinimalInstall
+ m_EditorClassIdentifier:
+ id: arch_minimal_install
+ displayName: minimal install
+ description: exhaust up to 2 cards from hand. per card exhausted, gain 3 Shield and 1 Cycle.
+ flavorText: no desktop, no browser, no regrets.
+ language: 2
+ rarity: 1
+ cycleCost: 1
+ resolutionTrack: 0
+ distroExclusive: 1
+ isToken: 0
+ isRunOnly: 0
+ designNotes: Arch exclusive. Exhausts first available hand cards using existing exhaust pile, shield, and cycle systems.
+ requiresSystem: []
+ upgradeTarget: {fileID: 0}
+ effects: []
diff --git a/Assets/Data/Cards/Arch/MinimalInstall.asset.meta b/Assets/Data/Cards/Arch/MinimalInstall.asset.meta
new file mode 100644
index 0000000..8658748
--- /dev/null
+++ b/Assets/Data/Cards/Arch/MinimalInstall.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: aa110000000000000000000000000005
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Cards/Arch/PacmanSyu.asset b/Assets/Data/Cards/Arch/PacmanSyu.asset
new file mode 100644
index 0000000..636e722
--- /dev/null
+++ b/Assets/Data/Cards/Arch/PacmanSyu.asset
@@ -0,0 +1,29 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5178747fac4dbfc448ff15d8a687e03c, type: 3}
+ m_Name: PacmanSyu
+ m_EditorClassIdentifier:
+ id: arch_pacman_syu
+ displayName: pacman -Syu
+ description: deal 14. overkill spills. 20% segfault: uptime -3 and corrupt a random card in hand.
+ flavorText: full system upgrade. what could possibly ABI wrong?
+ language: 0
+ rarity: 1
+ cycleCost: 1
+ resolutionTrack: 0
+ distroExclusive: 1
+ isToken: 0
+ isRunOnly: 0
+ designNotes: Arch exclusive. Damage and segfault card corruption are live through DamagePipeline and the broken-card hand state.
+ requiresSystem: []
+ upgradeTarget: {fileID: 0}
+ effects: []
diff --git a/Assets/Data/Cards/Arch/PacmanSyu.asset.meta b/Assets/Data/Cards/Arch/PacmanSyu.asset.meta
new file mode 100644
index 0000000..09072c1
--- /dev/null
+++ b/Assets/Data/Cards/Arch/PacmanSyu.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: aa110000000000000000000000000001
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Cards/Fedora/DnfUpdate.asset b/Assets/Data/Cards/Fedora/DnfUpdate.asset
index 2486c05..40c058a 100644
--- a/Assets/Data/Cards/Fedora/DnfUpdate.asset
+++ b/Assets/Data/Cards/Fedora/DnfUpdate.asset
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
id: fedora_dnf_update
displayName: dnf update
- description: deal 9. costs 1 less per java card played this combat (min 0).
+ description: deal 9. overkill spills. costs 1 less per java card played this combat (min 0).
flavorText: today everything gets newer.
language: 8
rarity: 0
diff --git a/Assets/Data/Cards/Languages/C/Malloc.asset b/Assets/Data/Cards/Languages/C/Malloc.asset
index 383fbf5..e3d67c7 100644
--- a/Assets/Data/Cards/Languages/C/Malloc.asset
+++ b/Assets/Data/Cards/Languages/C/Malloc.asset
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
id: lang_c_malloc
displayName: malloc()
- description: deal 8. 20% segfault: uptime -3.
+ description: deal 12. overkill spills. 20% segfault: uptime -3 and corrupt a random card in hand.
flavorText: memory acquired. ownership emotionally unresolved.
language: 0
rarity: 0
diff --git a/Assets/Data/Cards/Languages/C/Pointer.asset b/Assets/Data/Cards/Languages/C/Pointer.asset
index 3d68b5c..45d1627 100644
--- a/Assets/Data/Cards/Languages/C/Pointer.asset
+++ b/Assets/Data/Cards/Languages/C/Pointer.asset
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
id: lang_c_pointer
displayName: pointer
- description: deal 6. if the previous card was C, deal 10 instead. 20% segfault: uptime -3.
+ description: deal 9. if the previous card was C, deal 15 instead. overkill spills. 20% segfault: uptime -3 and corrupt a random card in hand.
flavorText: points at the bug with suspicious confidence.
language: 0
rarity: 0
diff --git a/Assets/Data/Cards/Languages/C/Printf.asset b/Assets/Data/Cards/Languages/C/Printf.asset
index 1503090..7418020 100644
--- a/Assets/Data/Cards/Languages/C/Printf.asset
+++ b/Assets/Data/Cards/Languages/C/Printf.asset
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
id: lang_c_printf
displayName: printf()
- description: deal 4. 20% segfault: uptime -3.
+ description: deal 7. overkill spills. 20% segfault: uptime -3 and corrupt a random card in hand.
flavorText: debugging by yelling into the void.
language: 0
rarity: 0
diff --git a/Assets/Data/Cards/Languages/Java/JavaNewObject.asset b/Assets/Data/Cards/Languages/Java/JavaNewObject.asset
index ec9bb53..b31d11e 100644
--- a/Assets/Data/Cards/Languages/Java/JavaNewObject.asset
+++ b/Assets/Data/Cards/Languages/Java/JavaNewObject.asset
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
id: lang_java_new_object
displayName: new Object()
- description: uses 2 RAM. deal 3. JIT applies.
+ description: uses 2 RAM. deal 3. overkill spills. JIT applies.
flavorText: allocation as a love language.
language: 8
rarity: 0
diff --git a/Assets/Data/Cards/Languages/Java/JavaPublicStaticMain.asset b/Assets/Data/Cards/Languages/Java/JavaPublicStaticMain.asset
index 8b1b583..e72d007 100644
--- a/Assets/Data/Cards/Languages/Java/JavaPublicStaticMain.asset
+++ b/Assets/Data/Cards/Languages/Java/JavaPublicStaticMain.asset
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
id: lang_java_public_static_main
displayName: public static void main
- description: uses 2 RAM. deal 7. JIT: Java cards cost 1 less this combat each time you play a Java card; the discount decays by 1 after each wave.
+ description: uses 2 RAM. deal 7. overkill spills. JIT: Java cards cost 1 less this combat each time you play a Java card; the discount decays by 1 after each wave.
flavorText: verbose, but warmed up.
language: 8
rarity: 0
diff --git a/Assets/Data/Cards/Languages/Java/JavaSystemOutPrintln.asset b/Assets/Data/Cards/Languages/Java/JavaSystemOutPrintln.asset
index 757ab81..9a6c93a 100644
--- a/Assets/Data/Cards/Languages/Java/JavaSystemOutPrintln.asset
+++ b/Assets/Data/Cards/Languages/Java/JavaSystemOutPrintln.asset
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
id: lang_java_system_out_println
displayName: System.out.println
- description: uses 2 RAM. deal 5. JIT applies.
+ description: uses 2 RAM. deal 5. overkill spills. JIT applies.
flavorText: standard output, standard damage.
language: 8
rarity: 0
diff --git a/Assets/Data/Cards/Shop/Free.asset b/Assets/Data/Cards/Shop/Free.asset
index 0e92e60..2c066a6 100644
--- a/Assets/Data/Cards/Shop/Free.asset
+++ b/Assets/Data/Cards/Shop/Free.asset
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
id: shop_c_free
displayName: free()
- description: deal 12. 20% segfault: uptime -3 and draw 2.
+ description: deal 18. overkill spills. 20% segfault: uptime -3, corrupt a random card in hand, and draw 2.
flavorText: double-free your schedule, not your heap.
language: 0
rarity: 0
diff --git a/Assets/Data/Cards/Shop/KernelExploit.asset b/Assets/Data/Cards/Shop/KernelExploit.asset
index 04ef461..417346e 100644
--- a/Assets/Data/Cards/Shop/KernelExploit.asset
+++ b/Assets/Data/Cards/Shop/KernelExploit.asset
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
id: shop_c_kernel_exploit
displayName: kernel_exploit
- description: deal 25 ignoring Shield. 50% segfault: uptime -8.
+ description: deal 36 ignoring Shield. overkill spills. 50% segfault: uptime -8 and corrupt a random card in hand.
flavorText: privilege escalated. heartbeat also escalated.
language: 0
rarity: 1
diff --git a/Assets/Data/Cards/Shop/Sizeof.asset b/Assets/Data/Cards/Shop/Sizeof.asset
index e24862f..b1d2461 100644
--- a/Assets/Data/Cards/Shop/Sizeof.asset
+++ b/Assets/Data/Cards/Shop/Sizeof.asset
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
id: shop_c_sizeof
displayName: sizeof()
- description: deal 3 x C cards in hand. 20% segfault: uptime -3.
+ description: deal 5 x C cards in hand. overkill spills. 20% segfault: uptime -3 and corrupt a random card in hand.
flavorText: compile-time confidence, runtime consequences.
language: 0
rarity: 1
diff --git a/Assets/Resources/CombatScene.uss b/Assets/Resources/CombatScene.uss
index 1f25108..41223df 100644
--- a/Assets/Resources/CombatScene.uss
+++ b/Assets/Resources/CombatScene.uss
@@ -400,10 +400,23 @@
font-size: 10px;
}
+.enemy-scroll {
+ flex-grow: 1;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.enemy-scroll .unity-scroll-view__content-container {
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-content: flex-start;
+}
+
.enemy-row {
flex-direction: row;
flex-wrap: wrap;
flex-grow: 1;
+ min-height: 0;
align-content: flex-start;
}
@@ -422,6 +435,64 @@
color: var(--kp-text);
}
+.enemy-row-compact {
+ align-content: flex-start;
+}
+
+.enemy-card-compact {
+ width: 98px;
+ min-height: 126px;
+ margin-right: 6px;
+ margin-bottom: 6px;
+ padding: 6px;
+}
+
+.enemy-card-compact .enemy-name {
+ font-size: 12px;
+ margin-bottom: 5px;
+}
+
+.enemy-card-compact .meter-block {
+ margin-bottom: 6px;
+}
+
+.enemy-card-compact .meter-head {
+ margin-bottom: 3px;
+}
+
+.enemy-card-compact .metric-label,
+.enemy-card-compact .status-label,
+.enemy-card-compact .enemy-shield {
+ font-size: 10px;
+}
+
+.enemy-card-compact .metric-value,
+.enemy-card-compact .metric-value-muted {
+ font-size: 11px;
+}
+
+.enemy-card-compact .meter-track {
+ height: 10px;
+}
+
+.enemy-card-compact .status-pip,
+.enemy-card-compact .tag {
+ font-size: 9px;
+ padding-left: 4px;
+ padding-right: 4px;
+ margin-right: 3px;
+ margin-bottom: 3px;
+}
+
+.enemy-card-compact .intent-panel {
+ margin-top: 5px;
+ padding: 5px;
+}
+
+.enemy-card-compact .intent-value {
+ font-size: 15px;
+}
+
.enemy-card:hover {
border-color: var(--kp-border-strong);
}
diff --git a/Assets/Scripts/Combat/CardEffects.cs b/Assets/Scripts/Combat/CardEffects.cs
index c6f982e..974259e 100644
--- a/Assets/Scripts/Combat/CardEffects.cs
+++ b/Assets/Scripts/Combat/CardEffects.cs
@@ -7,12 +7,15 @@ 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;
+ private readonly bool _allEnemies;
public DealDamageEffect(int minAmount, int maxAmount, Language language, bool trueDamage = false, bool canCrit = true, bool allEnemies = false)
{
@@ -49,22 +52,80 @@ namespace KernelPanic.Combat
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 = _language == Language.Rust || context.Card?.Definition?.Language == Language.Rust;
- int survivable = rustOverkillToShield
- ? _trueDamage ? target.CurrentUptime : target.CurrentUptime + target.Shield
+ 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));
+ 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);
@@ -165,7 +226,9 @@ namespace KernelPanic.Combat
continue;
}
- int roll = RandomRoll.RollRange(1, 100, new RollContext(context.Source));
+ 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));
@@ -308,7 +371,7 @@ namespace KernelPanic.Combat
public void Execute(CombatContext context)
{
int cCards = context.HandController.Cards.Count(card => card?.Definition != null && card.Definition.Language == Language.C);
- int amount = 3 * cCards;
+ int amount = 5 * cCards;
new DealDamageEffect(amount, amount, Language.C).Execute(context);
}
}
@@ -873,6 +936,7 @@ namespace KernelPanic.Combat
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");
@@ -922,9 +986,9 @@ namespace KernelPanic.Combat
new DrawEffect(1)
},
"lang_rs_borrow" => One(new ShieldEffect(5)),
- "lang_c_malloc" => One(new DealDamageEffect(8, 8, Language.C)),
- "lang_c_printf" => One(new DealDamageEffect(4, 4, Language.C)),
- "lang_c_pointer" => One(new PreviousLanguageDamageEffect(6, 10, Language.C)),
+ "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)),
@@ -976,14 +1040,14 @@ namespace KernelPanic.Combat
"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(8, 8, Language.C)),
+ "arch_pacman_syu" => One(new DealDamageEffect(14, 14, Language.C)),
"arch_consult_wiki" => One(new ConsultWikiEffect()),
- "arch_makepkg" => One(new BtwScaledDamageEffect(4, 2, Language.C)),
+ "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(12, 12, Language.C)),
+ "shop_c_free" => One(new DealDamageEffect(18, 18, Language.C)),
"shop_c_sizeof" => One(new SizeofDamageEffect()),
- "shop_c_kernel_exploit" => One(new DealDamageEffect(25, 25, Language.C, true)),
+ "shop_c_kernel_exploit" => One(new DealDamageEffect(36, 36, Language.C, true)),
_ => Todo("card effect is not authored")
};
}
@@ -1053,12 +1117,12 @@ namespace KernelPanic.Combat
"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(8, card)}. 20%: segfault, uptime -3.{marker}",
- "lang_c_printf" => $"deal {UpgradeMath.ScaleAmount(4, card)}. 20%: segfault, uptime -3.{marker}",
- "lang_c_pointer" => $"deal {UpgradeMath.ScaleAmount(6, card)}; {UpgradeMath.ScaleAmount(10, card)} if the previous card was C. 20%: segfault, uptime -3.{marker}",
- "lang_java_public_static_main" => $"uses 2 RAM. deal {UpgradeMath.ScaleAmount(7, card)}. 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)}. JIT applies; discount decays by 1 after each wave.{marker}",
- "lang_java_new_object" => $"uses 2 RAM. deal {UpgradeMath.ScaleAmount(3, card)}. JIT applies; discount decays by 1 after each wave.{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}",
@@ -1077,17 +1141,17 @@ namespace KernelPanic.Combat
"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)}. costs 1 less per Java card played this combat.{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(8, card)}. 20%: segfault, uptime -3. TODO: 20% break a random hand card.{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(4, card)} + btw x2; x3 at Arch v3+. 20%: segfault, uptime -3.{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(12, card)}. 20%: segfault, uptime -3 and draw 2.{marker}",
- "shop_c_sizeof" => $"deal 3 x C cards in hand. 20%: segfault, uptime -3.{marker}",
- "shop_c_kernel_exploit" => $"deal {UpgradeMath.ScaleAmount(25, card)} ignoring Shield. 50%: segfault, uptime -8.{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}"
};
}
diff --git a/Assets/Scripts/Combat/CombatManager.cs b/Assets/Scripts/Combat/CombatManager.cs
index 25bf43c..ede66d5 100644
--- a/Assets/Scripts/Combat/CombatManager.cs
+++ b/Assets/Scripts/Combat/CombatManager.cs
@@ -53,6 +53,7 @@ namespace KernelPanic.Combat
private int _rawhideBonusCharges;
private int _queuedRepeatCharges;
private int _nextTurnCycleBonus;
+ private int _nextRacePairId = 1;
private Coroutine _phaseCoroutine;
public TurnPhase CurrentPhase => currentPhase;
@@ -77,6 +78,7 @@ namespace KernelPanic.Combat
private void Awake()
{
_runManager = GetComponent();
+ _damagePipeline.ResistanceResolver = ResolveDamageResistance;
}
private void OnEnable()
@@ -184,6 +186,18 @@ namespace KernelPanic.Combat
return false;
}
+ if (card.IsBroken)
+ {
+ Log($"{GetCardName(card)} is corrupted");
+ return false;
+ }
+
+ if (card.IsLocked)
+ {
+ Log($"{GetCardName(card)} is locked");
+ return false;
+ }
+
bool fedoraBonus = CanApplyFedoraBonus();
int cost = GetEffectiveCardCost(card);
if (fedoraBonus)
@@ -343,6 +357,21 @@ namespace KernelPanic.Combat
}
}
+ public bool TryCorruptRandomHandCard(string sourceLabel)
+ {
+ CardInstance card = RandomHandCard(cardInstance => cardInstance != null && !cardInstance.IsBroken);
+ if (card == null)
+ {
+ Log($"{sourceLabel}: no card available to corrupt");
+ return false;
+ }
+
+ card.IsBroken = true;
+ Log($"{sourceLabel}: corrupted {GetCardName(card)}");
+ StateChanged?.Invoke();
+ return true;
+ }
+
public void ReportEffectResult(string message)
{
if (!string.IsNullOrWhiteSpace(message))
@@ -651,6 +680,7 @@ namespace KernelPanic.Combat
_pendingTargetCard = null;
_cardsPlayedThisWave = 0;
_turnNumberThisWave = 0;
+ _nextRacePairId = 1;
_packageShieldBonusTriggeredThisTurn = false;
_packageTimeshiftTriggeredThisWave = false;
ResetArchWaveState();
@@ -753,10 +783,10 @@ namespace KernelPanic.Combat
private IEnumerator ProcessEnemiesSequenced()
{
- int enemyCountAtPhaseStart = _enemies.Count;
- for (int i = 0; i < enemyCountAtPhaseStart && i < _enemies.Count; i++)
+ List actors = BuildEnemyActionOrder();
+ for (int i = 0; i < actors.Count; i++)
{
- EnemyInstance enemy = _enemies[i];
+ EnemyInstance enemy = actors[i];
if (enemy.State.IsDefeated)
{
continue;
@@ -779,11 +809,17 @@ namespace KernelPanic.Combat
StateChanged?.Invoke();
yield return Wait(CombatTuning.EnemyTelegraphDelaySeconds);
- ExecuteEnemyIntent(enemy, i);
- enemy.MarkTurnSurvived();
+ ExecuteEnemyIntent(enemy, _enemies.IndexOf(enemy));
+ ResolvePassiveEnemyAfterAction(enemy);
+ if (!enemy.State.IsDefeated)
+ {
+ enemy.MarkTurnSurvived();
+ }
+
GameEvents.RaiseEnemyActed(new EnemyActedEvent(enemy, enemy.CurrentIntent));
+ RemoveDefeatedEnemies();
StateChanged?.Invoke();
- if (CheckLoss())
+ if (CheckWinOrLoss())
{
yield break;
}
@@ -1519,7 +1555,7 @@ namespace KernelPanic.Combat
state.DamageMultiplierPercent = 100;
state.CurrentCardDamageMultiplierPercent = 100;
state.ArchBtwDamagePerStack = IsDistro("arch") && _runConfig.DistroVersion >= 2 ? 2 : 1;
- state.ArchMakepkgBtwMultiplier = IsDistro("arch") && _runConfig.DistroVersion >= 3 ? 3 : 2;
+ state.ArchMakepkgBtwMultiplier = IsDistro("arch") && _runConfig.DistroVersion >= 3 ? 4 : 3;
state.ArchRollingReleaseShieldOnSave = IsDistro("arch") && _runConfig.DistroVersion >= 4 ? 15 : 0;
state.ArchRollingReleaseCyclesOnSave = IsDistro("arch") && _runConfig.DistroVersion >= 4 ? 2 : 0;
}
@@ -1693,13 +1729,15 @@ namespace KernelPanic.Combat
{
if (_enemies[i].State.IsDefeated)
{
- if (_enemies[i].PendingRevive)
+ EnemyInstance enemy = _enemies[i];
+ if (enemy.PendingRevive)
{
continue;
}
- Log($"{_enemies[i].Name} exited");
+ Log($"{enemy.Name} exited");
_runManager?.AddBits(CombatTuning.BitsPerKill);
+ HandleEnemyDeath(enemy);
_enemies.RemoveAt(i);
}
}
@@ -1710,23 +1748,135 @@ namespace KernelPanic.Combat
}
}
+ private void HandleEnemyDeath(EnemyInstance enemy)
+ {
+ if (enemy == null)
+ {
+ return;
+ }
+
+ if (enemy.HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath))
+ {
+ ApplySegfaultDeathPayload(enemy);
+ }
+
+ if (enemy.HasBehavior(EnemyBehaviorFlags.RacePair))
+ {
+ BreakRacePair(enemy);
+ }
+
+ if (enemy.HasBehavior(EnemyBehaviorFlags.LeavesOrphan))
+ {
+ SpawnOrphanFromDeath(enemy);
+ }
+ }
+
+ private void ApplySegfaultDeathPayload(EnemyInstance enemy)
+ {
+ _statusEffects.Apply(_playerState, StatusType.Segfault, 1, -1, enemy.State, skipNextTick: true);
+ TryCorruptRandomHandCard("segfault");
+ }
+
+ private CardInstance RandomHandCard(Predicate predicate)
+ {
+ if (_handController == null || _handController.Cards.Count == 0)
+ {
+ return null;
+ }
+
+ List candidates = new();
+ for (int i = 0; i < _handController.Cards.Count; i++)
+ {
+ CardInstance candidate = _handController.Cards[i];
+ if (predicate == null || predicate(candidate))
+ {
+ candidates.Add(candidate);
+ }
+ }
+
+ if (candidates.Count == 0)
+ {
+ return null;
+ }
+
+ int index = RandomRoll.RollRange(0, candidates.Count - 1, new RollContext(_playerState));
+ return candidates[index];
+ }
+
+ private void BreakRacePair(EnemyInstance defeated)
+ {
+ EnemyInstance survivor = RacePartner(defeated);
+ if (survivor == null)
+ {
+ return;
+ }
+
+ survivor.EnrageRaceSurvivor();
+ Log($"{survivor.Name} race link broke; survivor enraged");
+ }
+
+ private void SpawnOrphanFromDeath(EnemyInstance source)
+ {
+ EnemyInstance orphan = SpawnEnemy("orphan_process", 0);
+ orphan.MarkSpawnedFromDeath();
+ Log($"{source.Name} left an orphan process");
+ }
+
private void SpawnStructuralEnemies()
{
int wave = CurrentWaveNumber;
int countGrowth = ((wave - 1) / 2) * CombatTuning.AdditionalEnemiesPerWave;
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++)
{
- SpawnEnemy(CreatePlaceholderArchetypeId(wave, i), waveUptimeBonus);
+ string archetypeId = CreatePlaceholderArchetypeId(wave, i);
+ if (archetypeId == "rootkit")
+ {
+ if (rootkitSpawned)
+ {
+ archetypeId = CreateRootkitFallbackArchetypeId(wave, i);
+ }
+ else
+ {
+ rootkitSpawned = true;
+ }
+ }
+
+ if (archetypeId == "race_condition" && i + 1 < count)
+ {
+ SpawnRacePair(waveUptimeBonus);
+ i++;
+ continue;
+ }
+
+ if (archetypeId == "race_condition")
+ {
+ archetypeId = "segfault";
+ }
+
+ SpawnEnemy(archetypeId, waveUptimeBonus);
}
}
- private void SpawnEnemy(string archetypeId, int uptimeBonus)
+ private void SpawnRacePair(int uptimeBonus)
+ {
+ int pairId = _nextRacePairId++;
+ EnemyInstance first = SpawnEnemy("race_condition", uptimeBonus);
+ EnemyInstance second = SpawnEnemy("race_condition", uptimeBonus);
+ first.SetPairId(pairId);
+ second.SetPairId(pairId);
+ Log("race condition pair spawned");
+ }
+
+ private EnemyInstance SpawnEnemy(string archetypeId, int uptimeBonus)
{
EnemyArchetypeDescriptor archetype = EnemyArchetypeCatalog.Get(archetypeId);
int baseUptime = RandomRoll.RollRange(archetype.BaseUptimeMin, archetype.BaseUptimeMax, RollContext.None);
- _enemies.Add(new EnemyInstance(archetype, baseUptime + Mathf.Max(0, uptimeBonus)));
+ EnemyInstance enemy = new(archetype, baseUptime + Mathf.Max(0, uptimeBonus));
+ _enemies.Add(enemy);
+ return enemy;
}
private static string CreatePlaceholderArchetypeId(int wave, int slotIndex)
@@ -1736,17 +1886,28 @@ namespace KernelPanic.Combat
{
<= 1 => new[] { "zombie_process", "memory_leak" },
2 => new[] { "zombie_process", "memory_leak", "fork_bomb", "firewalld" },
- 3 => new[] { "zombie_process", "memory_leak", "fork_bomb", "firewalld", "daemon" },
- _ => new[] { "zombie_process", "memory_leak", "fork_bomb", "firewalld", "daemon", "cron_job" }
+ 3 => new[] { "zombie_process", "memory_leak", "fork_bomb", "firewalld", "segfault" },
+ 4 => new[] { "zombie_process", "memory_leak", "fork_bomb", "firewalld", "cron_job", "segfault", "race_condition" },
+ _ => new[] { "zombie_process", "memory_leak", "fork_bomb", "firewalld", "daemon", "cron_job", "segfault", "race_condition", "rootkit" }
};
if (slotIndex == 0)
{
+ if (wave >= 5 && wave % 2 == 1)
+ {
+ return "rootkit";
+ }
+
return wave % 2 == 0 ? "firewalld" : "zombie_process";
}
if (slotIndex == 1 && wave >= 2)
{
+ if (wave >= 4 && wave % 4 == 0)
+ {
+ return "race_condition";
+ }
+
return wave % 3 == 0 ? "fork_bomb" : "memory_leak";
}
@@ -1754,6 +1915,13 @@ namespace KernelPanic.Combat
return pool[index];
}
+ private static string CreateRootkitFallbackArchetypeId(int wave, int slotIndex)
+ {
+ return slotIndex % 2 == 0
+ ? "firewalld"
+ : wave % 3 == 0 ? "fork_bomb" : "memory_leak";
+ }
+
private void PickEnemyIntents()
{
for (int i = 0; i < _enemies.Count; i++)
@@ -1765,6 +1933,37 @@ namespace KernelPanic.Combat
}
}
+ private List BuildEnemyActionOrder()
+ {
+ List actors = _enemies
+ .Where(enemy => enemy.State != null && !enemy.State.IsDefeated)
+ .ToList();
+
+ for (int i = 0; i < actors.Count - 1; i++)
+ {
+ EnemyInstance enemy = actors[i];
+ if (!IsRacePairLinked(enemy))
+ {
+ continue;
+ }
+
+ EnemyInstance partner = RacePartner(enemy);
+ int partnerIndex = partner == null ? -1 : actors.IndexOf(partner);
+ if (partnerIndex <= i)
+ {
+ continue;
+ }
+
+ if (RandomRoll.RollRange(0, 1, new RollContext(enemy.State)) == 1)
+ {
+ actors[i] = partner;
+ actors[partnerIndex] = enemy;
+ }
+ }
+
+ return actors;
+ }
+
private void ExecuteEnemyIntent(EnemyInstance enemy, int enemyIndex)
{
EnemyIntent intent = enemy.CurrentIntent;
@@ -1775,6 +1974,7 @@ namespace KernelPanic.Combat
switch (intent.Kind)
{
case EnemyIntentKind.Attack:
+ value = ApplyEnemyAttackBonuses(enemy, value);
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));
@@ -1790,6 +1990,7 @@ namespace KernelPanic.Combat
}
break;
case EnemyIntentKind.StatusAttack:
+ value = ApplyEnemyAttackBonuses(enemy, value);
DamageResult statusResult = _damagePipeline.DealDamage(new DamageRequest(enemy.State, _playerState, value, intent.DamageType, intent.TrueDamage, false));
GameEvents.RaisePlayerDamaged(new PlayerDamagedEvent(enemy, statusResult.FinalAmount));
if (intent.StatusStacks > 0)
@@ -1810,15 +2011,50 @@ namespace KernelPanic.Combat
private void ExecuteSpecialEnemyIntent(EnemyInstance enemy)
{
- if (enemy.HasBehavior(EnemyBehaviorFlags.Split))
+ if (enemy.HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath))
{
- TrySplitForkBomb(enemy);
+ enemy.AdvanceSegfaultCountdown();
+ if (enemy.CountdownRemaining <= 0)
+ {
+ _damagePipeline.DealDamage(new DamageRequest(enemy.State, enemy.State, enemy.CurrentUptime, Language.C, true, false));
+ Log($"{enemy.Name} dereferenced null");
+ }
+
return;
}
Log("TODO: special intent needs enemy-specific behavior hooks.");
}
+ private void ResolvePassiveEnemyAfterAction(EnemyInstance enemy)
+ {
+ if (enemy == null || enemy.State.IsDefeated)
+ {
+ return;
+ }
+
+ if (enemy.HasBehavior(EnemyBehaviorFlags.Split))
+ {
+ TrySplitForkBomb(enemy);
+ }
+ }
+
+ private int ApplyEnemyAttackBonuses(EnemyInstance enemy, int value)
+ {
+ int modified = value;
+ if (IsRacePairLinked(enemy))
+ {
+ modified += EnemyArchetypeCatalog.RacePairDamageBonus;
+ }
+
+ if (enemy != null && enemy.RaceEnrageStacks > 0)
+ {
+ modified += enemy.RaceEnrageStacks;
+ }
+
+ return modified;
+ }
+
private void TrySplitForkBomb(EnemyInstance source)
{
int totalForkBombs = _enemies.Count(enemy => enemy.ArchetypeId == source.ArchetypeId && !enemy.State.IsDefeated);
@@ -1836,6 +2072,34 @@ namespace KernelPanic.Combat
Log("fork bomb split");
}
+ private bool IsRacePairLinked(EnemyInstance enemy)
+ {
+ return enemy != null
+ && enemy.HasBehavior(EnemyBehaviorFlags.RacePair)
+ && RacePartner(enemy) != null;
+ }
+
+ private EnemyInstance RacePartner(EnemyInstance enemy)
+ {
+ if (enemy == null || enemy.PairId < 0)
+ {
+ return null;
+ }
+
+ for (int i = 0; i < _enemies.Count; i++)
+ {
+ EnemyInstance candidate = _enemies[i];
+ if (candidate != enemy
+ && candidate.PairId == enemy.PairId
+ && !candidate.State.IsDefeated)
+ {
+ return candidate;
+ }
+ }
+
+ return null;
+ }
+
private void DefendSelfAndAdjacent(int enemyIndex, int amount)
{
int applied = 0;
@@ -1866,6 +2130,38 @@ namespace KernelPanic.Combat
return mitigated;
}
+ private int ResolveDamageResistance(DamageRequest request, int amount)
+ {
+ EnemyInstance target = FindEnemy(request.Target);
+ if (target == null
+ || !target.HasBehavior(EnemyBehaviorFlags.RootkitMasked)
+ || !HasOtherLivingEnemy(target))
+ {
+ return amount;
+ }
+
+ if (amount <= 0)
+ {
+ return 0;
+ }
+
+ return Mathf.Max(1, Mathf.CeilToInt(amount * (EnemyArchetypeCatalog.RootkitMaskedDamagePercent / 100f)));
+ }
+
+ private bool HasOtherLivingEnemy(EnemyInstance target)
+ {
+ for (int i = 0; i < _enemies.Count; i++)
+ {
+ EnemyInstance enemy = _enemies[i];
+ if (enemy != target && enemy.State != null && !enemy.State.IsDefeated)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private void ResolveDelayedEndOfTurnEffects()
{
for (int i = _delayedEffects.Count - 1; i >= 0; i--)
diff --git a/Assets/Scripts/Combat/CombatTuning.cs b/Assets/Scripts/Combat/CombatTuning.cs
index c5eb8c7..f923e77 100644
--- a/Assets/Scripts/Combat/CombatTuning.cs
+++ b/Assets/Scripts/Combat/CombatTuning.cs
@@ -37,11 +37,21 @@ namespace KernelPanic.Combat
public const int EntropyStartWave = 5;
public const int EntropyPerWave = 3;
public const int StatUpgradeMaxCycles = 1;
- public const int StatUpgradeMaxUptime = 4;
- public const int StatUpgradeHeal = 5;
+ public const int StatUpgradeMaxUptimePercent = 20;
+ public const int StatUpgradeHealPercent = 30;
public const int StatUpgradeRam = 1;
public const int UpgradeMagnitudeBonus = 2;
+ public static int ScaleStatUpgradeMaxUptime(int maxUptime)
+ {
+ return System.Math.Max(1, ((System.Math.Max(1, maxUptime) * StatUpgradeMaxUptimePercent) + 99) / 100);
+ }
+
+ public static int ScaleStatUpgradeHeal(int maxUptime)
+ {
+ return System.Math.Max(1, ((System.Math.Max(1, maxUptime) * StatUpgradeHealPercent) + 99) / 100);
+ }
+
// Combat pacing: gaps between phase transitions and per-item resolution steps so
// players can follow drawing/queue/enemy resolution instead of seeing it all at once.
// Skipped entirely when UIPreferences.ReducedMotion is set.
diff --git a/Assets/Scripts/Combat/CombatantState.cs b/Assets/Scripts/Combat/CombatantState.cs
index 2e48d3b..414cf3a 100644
--- a/Assets/Scripts/Combat/CombatantState.cs
+++ b/Assets/Scripts/Combat/CombatantState.cs
@@ -36,7 +36,7 @@ namespace KernelPanic.Combat
public int IncomingAttackHalfCharges { get; set; }
public int ArchBtwStacks { get; set; }
public int ArchBtwDamagePerStack { get; set; } = 1;
- public int ArchMakepkgBtwMultiplier { get; set; } = 2;
+ public int ArchMakepkgBtwMultiplier { get; set; } = 3;
public int ArchRollingReleaseSavesRemaining { get; set; }
public int ArchRollingReleaseShieldOnSave { get; set; }
public int ArchRollingReleaseCyclesOnSave { get; set; }
diff --git a/Assets/Scripts/Combat/DamagePipeline.cs b/Assets/Scripts/Combat/DamagePipeline.cs
index 307a93b..7726892 100644
--- a/Assets/Scripts/Combat/DamagePipeline.cs
+++ b/Assets/Scripts/Combat/DamagePipeline.cs
@@ -11,7 +11,8 @@ namespace KernelPanic.Combat
int amount,
Language language,
bool trueDamage,
- bool canCrit)
+ bool canCrit,
+ bool applySourceModifiers = true)
{
Source = source;
Target = target;
@@ -19,6 +20,7 @@ namespace KernelPanic.Combat
Language = language;
TrueDamage = trueDamage;
CanCrit = canCrit;
+ ApplySourceModifiers = applySourceModifiers;
}
public CombatantState Source { get; }
@@ -27,6 +29,7 @@ namespace KernelPanic.Combat
public Language Language { get; }
public bool TrueDamage { get; }
public bool CanCrit { get; }
+ public bool ApplySourceModifiers { get; }
}
public readonly struct DamageResult
@@ -56,6 +59,8 @@ namespace KernelPanic.Combat
///
public sealed class DamagePipeline
{
+ public System.Func ResistanceResolver { get; set; }
+
public DamageResult DealDamage(DamageRequest request)
{
if (request.Target == null || request.Target.IsDefeated)
@@ -64,9 +69,14 @@ namespace KernelPanic.Combat
}
int amount = Mathf.Max(0, request.Amount);
- amount = ApplyFlatAdditions(amount, request);
- amount = ApplyMultipliers(amount, request);
- amount = ApplyCrit(amount, request, out bool wasCritical);
+ bool wasCritical = false;
+ if (request.ApplySourceModifiers)
+ {
+ amount = ApplyFlatAdditions(amount, request);
+ amount = ApplyMultipliers(amount, request);
+ amount = ApplyCrit(amount, request, out wasCritical);
+ }
+
amount = ApplyResistancesAndWeaknesses(amount, request);
int finalAmount = ApplyShieldAndUptime(amount, request, out int absorbedAmount, out int shieldDamage, out int uptimeDamage);
@@ -86,6 +96,11 @@ namespace KernelPanic.Combat
private static int ApplyMultipliers(int amount, DamageRequest request)
{
+ if (amount == int.MaxValue)
+ {
+ return amount;
+ }
+
if (request.Source != null && request.Source.IgnoreDamageMultipliers)
{
// Mint V1+ ignores multiplicative damage modifiers. Mint V4 still allows flat
@@ -100,6 +115,11 @@ namespace KernelPanic.Combat
private static int ApplyFlatAdditions(int amount, DamageRequest request)
{
+ if (amount == int.MaxValue)
+ {
+ return amount;
+ }
+
if (request.Source != null && request.Language == Language.JavaScript)
{
amount += Mathf.Max(0, request.Source.JavaScriptFlatDamageBonus);
@@ -116,7 +136,7 @@ namespace KernelPanic.Combat
private static int ApplyCrit(int amount, DamageRequest request, out bool wasCritical)
{
wasCritical = false;
- if (!request.CanCrit || amount <= 0 || request.Source != null && request.Source.ForceMaxRolls)
+ if (!request.CanCrit || amount <= 0 || amount == int.MaxValue || request.Source != null && request.Source.ForceMaxRolls)
{
return amount;
}
@@ -144,12 +164,12 @@ namespace KernelPanic.Combat
target.ArchRollingReleaseRecoveredThisHit = true;
}
- private static int ApplyResistancesAndWeaknesses(int amount, DamageRequest request)
+ private int ApplyResistancesAndWeaknesses(int amount, DamageRequest request)
{
// TODO: Enemy resistances/reductions are not authored yet. Mint V5 should re-resolve
// a once-per-combat reduced effect here once those reduction sources exist.
// TODO: Apply enemy language resistances and weaknesses here.
- return amount;
+ return ResistanceResolver == null ? amount : ResistanceResolver(request, amount);
}
private static int ApplyShieldAndUptime(int amount, DamageRequest request, out int absorbedAmount, out int shieldDamage, out int uptimeDamage)
diff --git a/Assets/Scripts/Combat/EnemyArchetype.cs b/Assets/Scripts/Combat/EnemyArchetype.cs
index e099803..0898d88 100644
--- a/Assets/Scripts/Combat/EnemyArchetype.cs
+++ b/Assets/Scripts/Combat/EnemyArchetype.cs
@@ -14,7 +14,12 @@ namespace KernelPanic.Combat
Grow = 1 << 2,
ObfuscateIntent = 1 << 3,
DefendAllies = 1 << 4,
- Countdown = 1 << 5
+ Countdown = 1 << 5,
+ LeavesOrphan = 1 << 6,
+ SegfaultOnDeath = 1 << 7,
+ RacePair = 1 << 8,
+ RootkitMasked = 1 << 9,
+ Elite = 1 << 10
}
public sealed class EnemyArchetypeDescriptor
@@ -51,18 +56,23 @@ namespace KernelPanic.Combat
{
public const int ZombieReapThresholdPercent = 30;
public const int MemoryLeakAttackGrowthPerTurn = 1;
+ public const int MemoryLeakAttackCapMin = 4;
+ public const int MemoryLeakAttackCapMax = 5;
public const int ForkBombTotalCap = 5;
+ public const int RootkitMaskedDamagePercent = 10;
+ public const int RacePairDamageBonus = 2;
public const int CronInitialCountdown = 2;
+ public const int SegfaultInitialCountdown = 2;
private static readonly EnemyArchetypeDescriptor ZombieProcess = new(
"zombie_process",
"Zombie Process",
6,
10,
- EnemyBehaviorFlags.Revive,
+ EnemyBehaviorFlags.Revive | EnemyBehaviorFlags.LeavesOrphan,
new[]
{
- new EnemyIntent(EnemyIntentKind.Attack, 3, 4, Language.C, "attack", "!")
+ new EnemyIntent(EnemyIntentKind.Attack, 2, 3, Language.C, "attack", "!")
});
private static readonly EnemyArchetypeDescriptor MemoryLeak = new(
@@ -73,7 +83,7 @@ namespace KernelPanic.Combat
EnemyBehaviorFlags.Grow,
new[]
{
- new EnemyIntent(EnemyIntentKind.StatusAttack, 1, 1, Language.C, "leak", "!", false, StatusType.MemoryLeak, 1, CombatTuning.EnemyStatusDuration)
+ new EnemyIntent(EnemyIntentKind.StatusAttack, 1, 2, Language.C, "leak", "!", false, StatusType.MemoryLeak, 1, CombatTuning.EnemyStatusDuration)
});
private static readonly EnemyArchetypeDescriptor ForkBomb = new(
@@ -81,10 +91,10 @@ namespace KernelPanic.Combat
"Fork Bomb",
3,
5,
- EnemyBehaviorFlags.Split,
+ EnemyBehaviorFlags.Split | EnemyBehaviorFlags.LeavesOrphan,
new[]
{
- new EnemyIntent(EnemyIntentKind.Special, 0, 0, Language.C, "fork", ":", false, StatusType.MemoryLeak, 0, -1, "split")
+ new EnemyIntent(EnemyIntentKind.Attack, 1, 2, Language.C, "fork hit", "!")
});
private static readonly EnemyArchetypeDescriptor Daemon = new(
@@ -92,7 +102,7 @@ namespace KernelPanic.Combat
"Daemon",
10,
14,
- EnemyBehaviorFlags.ObfuscateIntent,
+ EnemyBehaviorFlags.ObfuscateIntent | EnemyBehaviorFlags.Elite,
new[]
{
new EnemyIntent(EnemyIntentKind.Attack, 6, 8, Language.C, "daemon hit", "!")
@@ -108,7 +118,7 @@ namespace KernelPanic.Combat
{
new EnemyIntent(EnemyIntentKind.Defend, 3, 5, Language.C, "shield adjacent", "#"),
new EnemyIntent(EnemyIntentKind.Defend, 3, 5, Language.C, "shield adjacent", "#"),
- new EnemyIntent(EnemyIntentKind.Attack, 1, 2, Language.C, "poke", "!")
+ new EnemyIntent(EnemyIntentKind.Attack, 0, 1, Language.C, "poke", "!")
});
private static readonly EnemyArchetypeDescriptor CronJob = new(
@@ -122,6 +132,50 @@ namespace KernelPanic.Combat
new EnemyIntent(EnemyIntentKind.Attack, 10, 12, Language.C, "cron fire", "@")
});
+ private static readonly EnemyArchetypeDescriptor OrphanProcess = new(
+ "orphan_process",
+ "Orphan Process",
+ 2,
+ 4,
+ EnemyBehaviorFlags.None,
+ new[]
+ {
+ new EnemyIntent(EnemyIntentKind.Attack, 1, 2, Language.C, "orphan chip", ".")
+ });
+
+ private static readonly EnemyArchetypeDescriptor Segfault = new(
+ "segfault",
+ "Segfault",
+ 2,
+ 4,
+ EnemyBehaviorFlags.SegfaultOnDeath,
+ new[]
+ {
+ new EnemyIntent(EnemyIntentKind.Attack, 2, 3, Language.C, "unstable hit", "!")
+ });
+
+ private static readonly EnemyArchetypeDescriptor RaceCondition = new(
+ "race_condition",
+ "Race Condition",
+ 5,
+ 7,
+ EnemyBehaviorFlags.RacePair,
+ new[]
+ {
+ new EnemyIntent(EnemyIntentKind.Attack, 2, 3, Language.C, "racy strike", "~")
+ });
+
+ private static readonly EnemyArchetypeDescriptor Rootkit = new(
+ "rootkit",
+ "Rootkit",
+ 14,
+ 18,
+ EnemyBehaviorFlags.RootkitMasked,
+ new[]
+ {
+ new EnemyIntent(EnemyIntentKind.Attack, 1, 2, Language.C, "masked hit", "?")
+ });
+
public static EnemyArchetypeDescriptor Get(string id)
{
return id switch
@@ -131,6 +185,10 @@ namespace KernelPanic.Combat
"daemon" => Daemon,
"firewalld" => Defender,
"cron_job" => CronJob,
+ "orphan_process" => OrphanProcess,
+ "segfault" => Segfault,
+ "race_condition" => RaceCondition,
+ "rootkit" => Rootkit,
_ => ZombieProcess
};
}
diff --git a/Assets/Scripts/Combat/EnemyInstance.cs b/Assets/Scripts/Combat/EnemyInstance.cs
index b5bba6e..48e185b 100644
--- a/Assets/Scripts/Combat/EnemyInstance.cs
+++ b/Assets/Scripts/Combat/EnemyInstance.cs
@@ -24,7 +24,9 @@ namespace KernelPanic.Combat
_intentPool.AddRange(Archetype.IntentPool);
}
- CountdownRemaining = HasBehavior(EnemyBehaviorFlags.Countdown) ? EnemyArchetypeCatalog.CronInitialCountdown : 0;
+ CountdownRemaining = HasBehavior(EnemyBehaviorFlags.Countdown)
+ ? EnemyArchetypeCatalog.CronInitialCountdown
+ : HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath) ? EnemyArchetypeCatalog.SegfaultInitialCountdown : 0;
}
public string Name { get; }
@@ -42,7 +44,16 @@ namespace KernelPanic.Combat
public bool PendingRevive { get; private set; }
public bool Reaped { get; private set; }
public bool LethalHitThisTurn { get; private set; }
+ public int PairId { get; private set; } = -1;
+ public int RaceEnrageStacks { get; private set; }
+ public bool SpawnedFromDeath { get; private set; }
public bool HasPendingMarker => PendingRevive || HasRevived;
+ public bool HasSpecialSignal => HasSplitSignal || HasSegfaultSignal || HasRaceSignal || HasRootkitSignal || HasEliteSignal || SpawnedFromDeath;
+ public bool HasSplitSignal => HasBehavior(EnemyBehaviorFlags.Split);
+ public bool HasSegfaultSignal => HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath);
+ public bool HasRaceSignal => HasBehavior(EnemyBehaviorFlags.RacePair);
+ public bool HasRootkitSignal => HasBehavior(EnemyBehaviorFlags.RootkitMasked);
+ public bool HasEliteSignal => HasBehavior(EnemyBehaviorFlags.Elite);
public EnemyIntent DisplayIntent => BuildDisplayIntent();
public void PickNextIntent()
@@ -61,6 +72,12 @@ namespace KernelPanic.Combat
return;
}
+ if (HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath) && CountdownRemaining > 0)
+ {
+ CurrentIntent = new EnemyIntent(EnemyIntentKind.Special, CountdownRemaining, CountdownRemaining, KernelPanic.Core.Language.C, "segfault in", "!");
+ return;
+ }
+
if (_intentPool.Count == 0)
{
CurrentIntent = default;
@@ -72,7 +89,9 @@ namespace KernelPanic.Combat
if (HasBehavior(EnemyBehaviorFlags.Grow))
{
int growth = TurnsAlive * EnemyArchetypeCatalog.MemoryLeakAttackGrowthPerTurn;
- CurrentIntent = CurrentIntent.WithValues(CurrentIntent.MinValue + growth, CurrentIntent.MaxValue + growth, "growing leak");
+ int minValue = UnityEngine.Mathf.Min(CurrentIntent.MinValue + growth, EnemyArchetypeCatalog.MemoryLeakAttackCapMin);
+ int maxValue = UnityEngine.Mathf.Min(CurrentIntent.MaxValue + growth, EnemyArchetypeCatalog.MemoryLeakAttackCapMax);
+ CurrentIntent = CurrentIntent.WithValues(minValue, maxValue, "growing leak");
}
}
@@ -94,6 +113,21 @@ namespace KernelPanic.Combat
TurnsAlive++;
}
+ public void SetPairId(int pairId)
+ {
+ PairId = pairId;
+ }
+
+ public void MarkSpawnedFromDeath()
+ {
+ SpawnedFromDeath = true;
+ }
+
+ public void EnrageRaceSurvivor()
+ {
+ RaceEnrageStacks++;
+ }
+
public void ResetTurnLethalMarker()
{
LethalHitThisTurn = false;
@@ -142,6 +176,14 @@ namespace KernelPanic.Combat
: UnityEngine.Mathf.Max(0, CountdownRemaining - 1);
}
+ public void AdvanceSegfaultCountdown()
+ {
+ if (HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath))
+ {
+ CountdownRemaining = UnityEngine.Mathf.Max(0, CountdownRemaining - 1);
+ }
+ }
+
private EnemyIntent BuildDisplayIntent()
{
if (PendingRevive)
diff --git a/Assets/Scripts/Combat/StatusEffectController.cs b/Assets/Scripts/Combat/StatusEffectController.cs
index 1887358..e5e876c 100644
--- a/Assets/Scripts/Combat/StatusEffectController.cs
+++ b/Assets/Scripts/Combat/StatusEffectController.cs
@@ -13,7 +13,7 @@ namespace KernelPanic.Combat
private static readonly Dictionary Descriptors = new()
{
[StatusType.MemoryLeak] = new(StatusType.MemoryLeak, "Memory Leak", "leak", false, StatusStackingRule.Intensity, StatusTickTiming.StartOfTurn, "Takes damage equal to stacks at start of turn."),
- [StatusType.Segfault] = new(StatusType.Segfault, "Segfault", "segv", false, StatusStackingRule.Unique, StatusTickTiming.StartOfTurn, "Loses one cycle this turn. TODO: lock a random card slot."),
+ [StatusType.Segfault] = new(StatusType.Segfault, "Segfault", "segv", false, StatusStackingRule.Unique, StatusTickTiming.StartOfTurn, "Loses one cycle this turn. Segfault events corrupt a random hand card until fixed."),
[StatusType.RaceCondition] = new(StatusType.RaceCondition, "Race Condition", "race", false, StatusStackingRule.Refresh, StatusTickTiming.None, "Randomizes multi-target resolution order where supported."),
[StatusType.Deprecated] = new(StatusType.Deprecated, "Deprecated", "dep", false, StatusStackingRule.Duration, StatusTickTiming.StartOfTurn, "A temporary positive value decays. Currently reduces shield by 1."),
[StatusType.DependencyError] = new(StatusType.DependencyError, "Dependency Error", "deps", false, StatusStackingRule.Unique, StatusTickTiming.None, "Registered for off-support card cost rules. TODO: connect draw-time cost path."),
diff --git a/Assets/Scripts/Run/RepositoryOffer.cs b/Assets/Scripts/Run/RepositoryOffer.cs
index 2973222..f4fc3de 100644
--- a/Assets/Scripts/Run/RepositoryOffer.cs
+++ b/Assets/Scripts/Run/RepositoryOffer.cs
@@ -131,8 +131,8 @@ namespace KernelPanic.Run
return upgradeKind switch
{
RunStatUpgradeKind.MaxCycles => $"+{CombatTuning.StatUpgradeMaxCycles} max cycles this run",
- RunStatUpgradeKind.MaxUptime => $"+{CombatTuning.StatUpgradeMaxUptime} max uptime this run",
- RunStatUpgradeKind.Heal => $"heal {CombatTuning.StatUpgradeHeal} uptime now",
+ RunStatUpgradeKind.MaxUptime => $"+{CombatTuning.StatUpgradeMaxUptimePercent}% max uptime this run",
+ RunStatUpgradeKind.Heal => $"heal {CombatTuning.StatUpgradeHealPercent}% max uptime now",
RunStatUpgradeKind.Ram => $"+{CombatTuning.StatUpgradeRam} RAM this run",
_ => "--"
};
diff --git a/Assets/Scripts/Run/RunManager.cs b/Assets/Scripts/Run/RunManager.cs
index b33593f..12b8eac 100644
--- a/Assets/Scripts/Run/RunManager.cs
+++ b/Assets/Scripts/Run/RunManager.cs
@@ -496,10 +496,11 @@ namespace KernelPanic.Run
}
return true;
case RunStatUpgradeKind.MaxUptime:
- _maxUptimeBonus += CombatTuning.StatUpgradeMaxUptime;
+ int uptimeBonus = CombatTuning.ScaleStatUpgradeMaxUptime(playerState == null ? EffectiveMaxUptime() : playerState.MaxUptime);
+ _maxUptimeBonus += uptimeBonus;
if (playerState != null)
{
- playerState.MaxUptime += CombatTuning.StatUpgradeMaxUptime;
+ playerState.MaxUptime += uptimeBonus;
}
return true;
case RunStatUpgradeKind.Heal:
@@ -508,7 +509,8 @@ namespace KernelPanic.Run
return false;
}
- playerState.CurrentUptime = Mathf.Min(playerState.MaxUptime, playerState.CurrentUptime + CombatTuning.StatUpgradeHeal);
+ int healAmount = CombatTuning.ScaleStatUpgradeHeal(playerState.MaxUptime);
+ playerState.CurrentUptime = Mathf.Min(playerState.MaxUptime, playerState.CurrentUptime + healAmount);
return true;
case RunStatUpgradeKind.Ram:
_ramBonus += CombatTuning.StatUpgradeRam;
diff --git a/Assets/Scripts/UI/CombatSceneController.cs b/Assets/Scripts/UI/CombatSceneController.cs
index 4a9eec5..607461e 100644
--- a/Assets/Scripts/UI/CombatSceneController.cs
+++ b/Assets/Scripts/UI/CombatSceneController.cs
@@ -12,7 +12,8 @@ namespace KernelPanic.UI
{
///
/// Runtime-built GameScene combat UI. Intent icons use a compact terminal vocabulary:
- /// ! attack/status attack, # defend, + buff, * special, ? hidden, ~ reviving, : split, @ countdown.
+ /// ! attack/status attack/segfault, # defend, + buff, * special, ? hidden/rootkit,
+ /// ~ reviving/race link, : split, @ countdown, . orphan.
///
[RequireComponent(typeof(UIDocument))]
[RequireComponent(typeof(RunManager))]
@@ -276,9 +277,12 @@ namespace KernelPanic.UI
VisualElement enemiesPanel = CreatePanel("enemy processes", "intent telemetry", 438);
main.Add(enemiesPanel);
+ ScrollView enemyScroll = new(ScrollViewMode.Vertical);
+ enemyScroll.AddToClassList("enemy-scroll");
+ enemiesPanel.Add(enemyScroll);
_enemyRow = new();
_enemyRow.AddToClassList("enemy-row");
- enemiesPanel.Add(_enemyRow);
+ enemyScroll.Add(_enemyRow);
VisualElement bottom = new();
bottom.AddToClassList("bottom-layout");
@@ -445,6 +449,9 @@ namespace KernelPanic.UI
{
_enemyRow.Clear();
_combatantElements.Clear();
+ int enemyCount = _combatManager.Enemies.Count;
+ bool compactEnemies = enemyCount >= 5;
+ _enemyRow.EnableInClassList("enemy-row-compact", compactEnemies);
if (_combatManager.PlayerState != null)
{
_combatantElements[_combatManager.PlayerState] = _playerPanel;
@@ -457,6 +464,7 @@ namespace KernelPanic.UI
Button card = new(() => _combatManager.SelectEnemy(index));
card.text = string.Empty;
card.AddToClassList("enemy-card");
+ card.EnableInClassList("enemy-card-compact", compactEnemies);
bool highlighted = _combatManager.PendingTargetCard != null || _combatManager.SelectedEnemyIndex == index;
if (highlighted)
{
@@ -473,6 +481,11 @@ namespace KernelPanic.UI
card.Add(marker);
}
+ if (enemy.HasSpecialSignal)
+ {
+ card.Add(EnemySignalPanel(enemy, _combatManager.Enemies));
+ }
+
_combatantElements[enemy.State] = card;
DetectBeneficialResourceFeedback(enemy.State, card);
ApplyStatusStateClasses(card, enemy.State);
@@ -823,6 +836,12 @@ namespace KernelPanic.UI
button.AddToClassList("hand-card-unaffordable");
}
+ if (card.IsBroken || card.IsLocked)
+ {
+ button.AddToClassList("hand-card-unaffordable");
+ button.SetEnabled(false);
+ }
+
PopulateCardFace(button, card);
return button;
}
@@ -853,6 +872,13 @@ namespace KernelPanic.UI
top.Add(name);
target.Add(top);
+ if (card.IsBroken || card.IsLocked)
+ {
+ Label state = new(card.IsBroken ? "segv corrupt" : "locked");
+ state.AddToClassList("status-label");
+ target.Add(state);
+ }
+
VisualElement tags = new();
tags.AddToClassList("tag-row");
tags.Add(Tag(card.Definition.Language.ToString(), LanguageClass(card.Definition.Language)));
@@ -885,6 +911,97 @@ namespace KernelPanic.UI
return panel;
}
+ private static VisualElement EnemySignalPanel(EnemyInstance enemy, IReadOnlyList enemies)
+ {
+ VisualElement row = new();
+ row.AddToClassList("status-row");
+
+ if (enemy.SpawnedFromDeath)
+ {
+ row.Add(Tag(". orphaned", "track-tag"));
+ }
+
+ if (enemy.HasEliteSignal)
+ {
+ row.Add(Tag("! elite", "intent-attack"));
+ }
+
+ if (enemy.HasSplitSignal)
+ {
+ string text = CountLivingArchetype(enemies, enemy.ArchetypeId) >= EnemyArchetypeCatalog.ForkBombTotalCap
+ ? ": split capped"
+ : ": split next";
+ row.Add(Tag(text, "track-tag"));
+ }
+
+ if (enemy.HasSegfaultSignal)
+ {
+ row.Add(Tag($"! corrupt in {enemy.CountdownRemaining}", "track-tag"));
+ }
+
+ if (enemy.HasRaceSignal)
+ {
+ string text = HasLivingPair(enemies, enemy) ? "~ linked random" : "~ unlinked";
+ row.Add(Tag(text, "track-tag"));
+ }
+
+ if (enemy.HasRootkitSignal)
+ {
+ string text = HasOtherLivingEnemy(enemies, enemy) ? "? masked reduced" : "? exposed";
+ row.Add(Tag(text, "track-tag"));
+ }
+
+ return row;
+ }
+
+ private static int CountLivingArchetype(IReadOnlyList enemies, string archetypeId)
+ {
+ int count = 0;
+ for (int i = 0; i < enemies.Count; i++)
+ {
+ EnemyInstance enemy = enemies[i];
+ if (enemy.ArchetypeId == archetypeId && !enemy.State.IsDefeated)
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ private static bool HasLivingPair(IReadOnlyList enemies, EnemyInstance target)
+ {
+ if (target == null || target.PairId < 0)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < enemies.Count; i++)
+ {
+ EnemyInstance enemy = enemies[i];
+ if (enemy != target && enemy.PairId == target.PairId && !enemy.State.IsDefeated)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool HasOtherLivingEnemy(IReadOnlyList enemies, EnemyInstance target)
+ {
+ for (int i = 0; i < enemies.Count; i++)
+ {
+ EnemyInstance enemy = enemies[i];
+ if (enemy != target && !enemy.State.IsDefeated)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private static VisualElement MeterBlock(string name, int current, int max, MeterTone tone)
{
float ratio = max <= 0 ? 0f : Mathf.Clamp01((float)current / max);