buff fedora, add elite enemies and add impact to attack

This commit is contained in:
2026-07-10 11:17:40 +02:00
parent e4db7af324
commit 37049a7c16
11 changed files with 832 additions and 114 deletions
@@ -11,39 +11,15 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="4222a6b8-add3-4f43-9c42-fd6bb5a76169" name="Changes" comment=""> <list default="true" id="4222a6b8-add3-4f43-9c42-fd6bb5a76169" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/Assets/Data/Cards/Arch/AurHelper.asset" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Data/Cards/Arch/AurHelper.asset.meta" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Data/Cards/Arch/ConsultWiki.asset" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Data/Cards/Arch/ConsultWiki.asset.meta" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Data/Cards/Arch/Makepkg.asset" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Data/Cards/Arch/Makepkg.asset.meta" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Data/Cards/Arch/MinimalInstall.asset" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Data/Cards/Arch/MinimalInstall.asset.meta" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Data/Cards/Arch/PacmanSyu.asset" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Data/Cards/Arch/PacmanSyu.asset.meta" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.linuxdistrosbuttheyarebreedable/.idea/indexLayout.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.linuxdistrosbuttheyarebreedable/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.linuxdistrosbuttheyarebreedable/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Data/Cards/Fedora/DnfUpdate.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Data/Cards/Fedora/DnfUpdate.asset" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Data/Cards/Languages/C/Malloc.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Data/Cards/Languages/C/Malloc.asset" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Data/Cards/Languages/C/Pointer.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Data/Cards/Languages/C/Pointer.asset" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Data/Cards/Languages/C/Printf.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Data/Cards/Languages/C/Printf.asset" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Data/Cards/Languages/Java/JavaNewObject.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Data/Cards/Languages/Java/JavaNewObject.asset" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Data/Cards/Languages/Java/JavaPublicStaticMain.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Data/Cards/Languages/Java/JavaPublicStaticMain.asset" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Data/Cards/Languages/Java/JavaSystemOutPrintln.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Data/Cards/Languages/Java/JavaSystemOutPrintln.asset" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Data/Cards/Shop/Free.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Data/Cards/Shop/Free.asset" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Data/Cards/Shop/KernelExploit.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Data/Cards/Shop/KernelExploit.asset" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Data/Cards/Shop/Sizeof.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Data/Cards/Shop/Sizeof.asset" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Resources/CombatScene.uss" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Resources/CombatScene.uss" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Assets/Resources/CombatScene.uss" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Resources/CombatScene.uss" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/CardEffects.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/CardEffects.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/CardEffects.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/CardEffects.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/CombatManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/CombatManager.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/CombatManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/CombatManager.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/CombatTuning.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/CombatTuning.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/CombatTuning.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/CombatTuning.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/CombatantState.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/CombatantState.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/DamagePipeline.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/DamagePipeline.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/DamagePipeline.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/DamagePipeline.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/EnemyArchetype.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/EnemyArchetype.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/EnemyArchetype.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/EnemyArchetype.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/EnemyInstance.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/EnemyInstance.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/EnemyInstance.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/EnemyInstance.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/StatusEffectController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/StatusEffectController.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Assets/Scripts/Combat/HandController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/HandController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Run/RepositoryOffer.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Run/RepositoryOffer.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Assets/Scripts/Core/GameEvents.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Core/GameEvents.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Run/RunManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Run/RunManager.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/CombatSceneController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/CombatSceneController.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/CombatSceneController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/CombatSceneController.cs" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
@@ -128,23 +104,23 @@
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" /> <component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
<component name="NextEditCompletionFeaturesState"> <component name="NextEditCompletionFeaturesState">
<decayedCancelled> <decayedCancelled>
<entry key="MS100" value="1.743262326028233" /> <entry key="MS100" value="1.7402462730204733" />
<entry key="MS500" value="5.976929062573193" /> <entry key="MS500" value="5.212822426631115" />
<entry key="S2" value="17.53689034207893" /> <entry key="S2" value="15.469623231379348" />
<entry key="S5" value="27.584806857875957" /> <entry key="S5" value="29.842466880777515" />
<entry key="S10" value="33.27162043754185" /> <entry key="S10" value="48.17178903184381" />
<entry key="S30" value="38.14780953249457" /> <entry key="S30" value="103.86178571051501" />
<entry key="S60" value="39.53571836868974" /> <entry key="S60" value="151.400209690654" />
<entry key="M2" value="40.262726944904955" /> <entry key="M2" value="201.01266486561804" />
<entry key="M5" value="43.32220297215448" /> <entry key="M5" value="261.5973708018234" />
<entry key="M10" value="66.17309975101882" /> <entry key="M10" value="299.5317141800844" />
<entry key="M15" value="106.65472090214925" /> <entry key="M15" value="327.6694025041181" />
<entry key="M30" value="269.18080676227913" /> <entry key="M30" value="438.2042699732131" />
<entry key="H1" value="561.7788280748766" /> <entry key="H1" value="698.7973356946319" />
<entry key="H2" value="954.5740596560953" /> <entry key="H2" value="1110.17006856563" />
<entry key="H4" value="1474.025592246917" /> <entry key="H4" value="1669.5601617756433" />
<entry key="D1" value="4120.268616608263" /> <entry key="D1" value="4400.825096157156" />
<entry key="W1" value="6567.8208311010485" /> <entry key="W1" value="6908.8758325877625" />
</decayedCancelled> </decayedCancelled>
<decayedSelected> <decayedSelected>
<entry key="MS100" value="0.0" /> <entry key="MS100" value="0.0" />
@@ -155,34 +131,34 @@
<entry key="S30" value="0.0" /> <entry key="S30" value="0.0" />
<entry key="S60" value="0.0" /> <entry key="S60" value="0.0" />
<entry key="M2" value="0.0" /> <entry key="M2" value="0.0" />
<entry key="M5" value="9.303102041982209E-153" /> <entry key="M5" value="3.867598516552587E-155" />
<entry key="M10" value="9.645258960745102E-77" /> <entry key="M10" value="6.219001942878838E-78" />
<entry key="M15" value="2.1031775057960495E-51" /> <entry key="M15" value="3.381793836897591E-52" />
<entry key="M30" value="4.586042805192191E-26" /> <entry key="M30" value="1.8389660182180397E-26" />
<entry key="H1" value="2.1436097902215403E-13" /> <entry key="H1" value="1.3574177751524003E-13" />
<entry key="H2" value="4.878952351612686E-7" /> <entry key="H2" value="3.8824911709080487E-7" />
<entry key="H4" value="9.54846149789426E-4" /> <entry key="H4" value="8.517759062521447E-4" />
<entry key="D1" value="0.9334224145260533" /> <entry key="D1" value="0.9158201973847542" />
<entry key="W1" value="3.244779666296953" /> <entry key="W1" value="3.235966878195177" />
</decayedSelected> </decayedSelected>
<decayedShown> <decayedShown>
<entry key="MS100" value="1.668533264021644" /> <entry key="MS100" value="1.684339997461739" />
<entry key="MS500" value="5.913573457970713" /> <entry key="MS500" value="5.1624842789484084" />
<entry key="S2" value="17.47919105497625" /> <entry key="S2" value="15.426146204785072" />
<entry key="S5" value="27.548161746345162" /> <entry key="S5" value="29.8122059339745" />
<entry key="S10" value="33.249516115897215" /> <entry key="S10" value="48.149217760882784" />
<entry key="S30" value="38.1393616328719" /> <entry key="S30" value="103.84706944591119" />
<entry key="S60" value="39.53134062098556" /> <entry key="S60" value="151.3898367310693" />
<entry key="M2" value="40.26049784489663" /> <entry key="M2" value="201.00547613774899" />
<entry key="M5" value="43.32125926171084" /> <entry key="M5" value="261.59163125526345" />
<entry key="M10" value="66.17242872752867" /> <entry key="M10" value="299.52705025178165" />
<entry key="M15" value="106.65399822613271" /> <entry key="M15" value="327.66555086022873" />
<entry key="M30" value="269.1797553890588" /> <entry key="M30" value="438.20154496052015" />
<entry key="H1" value="561.7775677248208" /> <entry key="H1" value="698.7952502773087" />
<entry key="H2" value="954.5729275577665" /> <entry key="H2" value="1110.1684860433274" />
<entry key="H4" value="1474.025680151079" /> <entry key="H4" value="1669.5598893072865" />
<entry key="D1" value="4121.2016186941155" /> <entry key="D1" value="4401.740444022695" />
<entry key="W1" value="6571.065502619166" /> <entry key="W1" value="6912.111683013967" />
</decayedShown> </decayedShown>
</component> </component>
<component name="ProjectColorInfo">{ <component name="ProjectColorInfo">{
@@ -276,7 +252,7 @@
<workItem from="1783519887449" duration="6425000" /> <workItem from="1783519887449" duration="6425000" />
<workItem from="1783578446731" duration="20118000" /> <workItem from="1783578446731" duration="20118000" />
<workItem from="1783615847433" duration="3791000" /> <workItem from="1783615847433" duration="3791000" />
<workItem from="1783665689762" duration="4791000" /> <workItem from="1783665689762" duration="8074000" />
</task> </task>
<task id="LOCAL-00001" summary="implement main screen"> <task id="LOCAL-00001" summary="implement main screen">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -510,7 +486,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1783620160977</updated> <updated>1783620160977</updated>
</task> </task>
<option name="localTasksCounter" value="30" /> <task id="LOCAL-00030" summary="implement more enemies, balance some programming languages and enemies to be less annoying">
<option name="closed" value="true" />
<created>1783671710808</created>
<option name="number" value="00030" />
<option name="presentableId" value="LOCAL-00030" />
<option name="project" value="LOCAL" />
<updated>1783671710808</updated>
</task>
<option name="localTasksCounter" value="31" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -526,7 +510,6 @@
<component name="UnityUnitTestConfiguration" currentTestLauncher="Both" /> <component name="UnityUnitTestConfiguration" currentTestLauncher="Both" />
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" /> <option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
<MESSAGE value="refactor card systems" />
<MESSAGE value="Add actual carts and shit&#10;&#10;Upgrade UI to Level 6&#10;&#10;Balls" /> <MESSAGE value="Add actual carts and shit&#10;&#10;Upgrade UI to Level 6&#10;&#10;Balls" />
<MESSAGE value="Implement more UX and cards for other distros" /> <MESSAGE value="Implement more UX and cards for other distros" />
<MESSAGE value="Fix some fucking bugs (FSFB)" /> <MESSAGE value="Fix some fucking bugs (FSFB)" />
@@ -551,7 +534,8 @@
<MESSAGE value="Make multiple UX improvements in distro -&gt; packages" /> <MESSAGE value="Make multiple UX improvements in distro -&gt; packages" />
<MESSAGE value="Implement Arch and some additional changes" /> <MESSAGE value="Implement Arch and some additional changes" />
<MESSAGE value="Fix C, more enemies and less scaling for enemies." /> <MESSAGE value="Fix C, more enemies and less scaling for enemies." />
<option name="LAST_COMMIT_MESSAGE" value="Fix C, more enemies and less scaling for enemies." /> <MESSAGE value="implement more enemies, balance some programming languages and enemies to be less annoying" />
<option name="LAST_COMMIT_MESSAGE" value="implement more enemies, balance some programming languages and enemies to be less annoying" />
</component> </component>
<component name="XDebuggerManager"> <component name="XDebuggerManager">
<breakpoint-manager> <breakpoint-manager>
+154
View File
@@ -59,6 +59,22 @@
transition-duration: 0ms; transition-duration: 0ms;
} }
.root-shake-major {
translate: 3px 0;
transition-property: translate;
transition-duration: 70ms;
}
.root-shake-massive {
translate: 6px 0;
transition-property: translate;
transition-duration: 90ms;
}
.root-shake-player {
background-color: #080202;
}
.scene-fade-curtain { .scene-fade-curtain {
position: absolute; position: absolute;
left: 0; left: 0;
@@ -502,6 +518,11 @@
background-color: var(--kp-panel-targeted); background-color: var(--kp-panel-targeted);
} }
.enemy-card-elite {
border-left-color: var(--kp-warning);
border-left-width: 4px;
}
.enemy-name { .enemy-name {
color: var(--kp-text); color: var(--kp-text);
-unity-font-style: bold; -unity-font-style: bold;
@@ -1120,6 +1141,12 @@
border-color: var(--kp-kill); border-color: var(--kp-kill);
} }
.damage-vignette-massive {
opacity: 0.94;
border-color: var(--kp-kill);
background-color: rgba(255, 51, 51, 0.08);
}
.float-number { .float-number {
position: absolute; position: absolute;
min-width: 60px; min-width: 60px;
@@ -1137,6 +1164,14 @@
color: var(--kp-damage); color: var(--kp-damage);
} }
.float-shield {
color: var(--kp-beneficial);
}
.float-true {
color: var(--kp-kill);
}
.float-heal { .float-heal {
color: var(--kp-beneficial); color: var(--kp-beneficial);
} }
@@ -1157,6 +1192,37 @@
font-size: 23px; font-size: 23px;
} }
.float-tier-minor {
font-size: 14px;
-unity-font-style: normal;
opacity: 0.86;
}
.float-tier-moderate {
font-size: 19px;
-unity-font-style: bold;
}
.float-tier-major {
font-size: 25px;
-unity-font-style: bold;
color: var(--kp-warning);
}
.float-tier-massive {
min-width: 92px;
height: 34px;
font-size: 32px;
-unity-font-style: bold;
color: var(--kp-kill);
background-color: rgba(255, 179, 71, 0.08);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-warning);
}
.float-static { .float-static {
transition-duration: 0ms; transition-duration: 0ms;
} }
@@ -1195,6 +1261,29 @@
border-color: var(--kp-beneficial); border-color: var(--kp-beneficial);
} }
.impact-tier-minor {
opacity: 0.72;
font-size: 9px;
}
.impact-tier-moderate {
font-size: 10px;
}
.impact-tier-major {
min-width: 58px;
font-size: 11px;
border-color: var(--kp-warning);
}
.impact-tier-massive {
min-width: 68px;
height: 22px;
font-size: 12px;
color: var(--kp-kill);
border-color: var(--kp-kill);
}
.feedback-card-ghost { .feedback-card-ghost {
opacity: 0.9; opacity: 0.9;
transition-property: left, top, opacity; transition-property: left, top, opacity;
@@ -1227,6 +1316,36 @@
transition-duration: 90ms; transition-duration: 90ms;
} }
.feedback-hit-minor {
background-color: var(--kp-panel-raised);
border-color: var(--kp-border);
scale: 0.98 0.98;
transition-property: scale, background-color, border-color;
transition-duration: 70ms;
}
.feedback-hit-major {
background-color: #251703;
border-color: var(--kp-warning);
translate: 6px 0;
scale: 1.055 0.94;
transition-property: scale, translate, background-color, border-color;
transition-duration: 140ms;
}
.feedback-hit-minor .meter-fill {
background-color: var(--kp-damage);
}
.feedback-hit .meter-fill,
.feedback-hit-major .meter-fill {
background-color: var(--kp-warning);
}
.feedback-crit .meter-fill {
background-color: var(--kp-kill);
}
.feedback-crit { .feedback-crit {
background-color: #2d0707; background-color: #2d0707;
border-color: var(--kp-kill); border-color: var(--kp-kill);
@@ -1245,6 +1364,20 @@
transition-duration: 80ms; transition-duration: 80ms;
} }
.feedback-clang-major {
background-color: #061820;
border-color: var(--kp-beneficial);
translate: 5px 0;
scale: 1.045 0.96;
transition-property: scale, translate, background-color, border-color;
transition-duration: 130ms;
}
.feedback-clang .meter-fill,
.feedback-clang-major .meter-fill {
background-color: var(--kp-beneficial);
}
.feedback-status { .feedback-status {
border-color: var(--kp-status-harmful); border-color: var(--kp-status-harmful);
} }
@@ -1361,9 +1494,15 @@
.reduced-motion .impact-marker, .reduced-motion .impact-marker,
.reduced-motion .feedback-card-ghost, .reduced-motion .feedback-card-ghost,
.reduced-motion .damage-vignette, .reduced-motion .damage-vignette,
.reduced-motion .root-shake-major,
.reduced-motion .root-shake-massive,
.reduced-motion .root-shake-player,
.reduced-motion .feedback-hit-minor,
.reduced-motion .feedback-hit, .reduced-motion .feedback-hit,
.reduced-motion .feedback-hit-major,
.reduced-motion .feedback-crit, .reduced-motion .feedback-crit,
.reduced-motion .feedback-clang, .reduced-motion .feedback-clang,
.reduced-motion .feedback-clang-major,
.reduced-motion .feedback-killed, .reduced-motion .feedback-killed,
.reduced-motion .enemy-anticipating, .reduced-motion .enemy-anticipating,
.reduced-motion .enemy-acting, .reduced-motion .enemy-acting,
@@ -1376,3 +1515,18 @@
.reduced-motion .death-ghost { .reduced-motion .death-ghost {
transition-duration: 0ms; transition-duration: 0ms;
} }
.reduced-motion .feedback-hit-minor,
.reduced-motion .feedback-hit,
.reduced-motion .feedback-hit-major,
.reduced-motion .feedback-crit,
.reduced-motion .feedback-clang,
.reduced-motion .feedback-clang-major,
.reduced-motion .feedback-killed,
.reduced-motion .enemy-anticipating,
.reduced-motion .enemy-acting,
.reduced-motion .root-shake-major,
.reduced-motion .root-shake-massive {
translate: 0 0;
scale: 1 1;
}
+17
View File
@@ -1062,6 +1062,23 @@ namespace KernelPanic.Combat
or "arch_pacman_syu" or "arch_consult_wiki" or "arch_makepkg" or "shop_c_free" or "shop_c_sizeof" or "shop_c_kernel_exploit"; 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 CanReceiveBleedingEdgeDamageBonus(CardDefinition definition)
{
if (definition == null || definition.ResolutionTrack != ResolutionTrack.Native)
{
return false;
}
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_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) public static bool TargetsAllEnemies(CardDefinition definition)
{ {
return definition?.Id is "lang_py_for_loop"; return definition?.Id is "lang_py_for_loop";
+241 -15
View File
@@ -39,7 +39,7 @@ namespace KernelPanic.Combat
private bool _skipNextAllocateDraw; private bool _skipNextAllocateDraw;
private bool _ubuntuEmptyHandRefillUsed; private bool _ubuntuEmptyHandRefillUsed;
private int _fedoraCardsDiscountedThisTurn; private int _fedoraCardsDiscountedThisTurn;
private int _fedoraCrashChance = 10; private int _fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
private int _cardsPlayedThisTurn; private int _cardsPlayedThisTurn;
private int _cardsPlayedThisWave; private int _cardsPlayedThisWave;
private int _cCardsPlayedThisTurn; private int _cCardsPlayedThisTurn;
@@ -54,6 +54,7 @@ namespace KernelPanic.Combat
private int _queuedRepeatCharges; private int _queuedRepeatCharges;
private int _nextTurnCycleBonus; private int _nextTurnCycleBonus;
private int _nextRacePairId = 1; private int _nextRacePairId = 1;
private int _activeKernelRamPenalty;
private Coroutine _phaseCoroutine; private Coroutine _phaseCoroutine;
public TurnPhase CurrentPhase => currentPhase; public TurnPhase CurrentPhase => currentPhase;
@@ -71,6 +72,7 @@ namespace KernelPanic.Combat
public bool AwaitingWaveContinue => _awaitingWaveContinue; public bool AwaitingWaveContinue => _awaitingWaveContinue;
public bool RunLost => _runLost; public bool RunLost => _runLost;
public int CurrentWaveNumber => _runManager == null ? 1 : _runManager.CurrentWaveNumber; public int CurrentWaveNumber => _runManager == null ? 1 : _runManager.CurrentWaveNumber;
public int ActiveKernelRamPenalty => _activeKernelRamPenalty;
public event Action StateChanged; public event Action StateChanged;
public event Action<string> CombatLog; public event Action<string> CombatLog;
@@ -194,11 +196,11 @@ namespace KernelPanic.Combat
if (card.IsLocked) if (card.IsLocked)
{ {
Log($"{GetCardName(card)} is locked"); Log($"{GetCardName(card)} is locked; pay {EnemyArchetypeCatalog.DrmUnlockCycleCost} Cycle to unlock");
return false; return false;
} }
bool fedoraBonus = CanApplyFedoraBonus(); bool fedoraBonus = CanApplyFedoraBonus(card);
int cost = GetEffectiveCardCost(card); int cost = GetEffectiveCardCost(card);
if (fedoraBonus) if (fedoraBonus)
{ {
@@ -248,11 +250,13 @@ namespace KernelPanic.Combat
} }
GameEvents.RaiseCardPlayed(new CardPlayedEvent(card, track)); GameEvents.RaiseCardPlayed(new CardPlayedEvent(card, track));
ApplyTelemetryCardPlayed();
LogPlayIntent(card); LogPlayIntent(card);
card.SetTargetSnapshot(CaptureTargets(card)); card.SetTargetSnapshot(CaptureTargets(card));
if (fedoraBonus) if (fedoraBonus)
{ {
bool firstFedoraBonusThisTurn = _fedoraCardsDiscountedThisTurn == 0;
_fedoraCardsDiscountedThisTurn++; _fedoraCardsDiscountedThisTurn++;
bool rawhideChargeUsed = _rawhideBonusCharges > 0; bool rawhideChargeUsed = _rawhideBonusCharges > 0;
if (rawhideChargeUsed) if (rawhideChargeUsed)
@@ -262,7 +266,7 @@ namespace KernelPanic.Combat
if (!IsCrashImmune(card) && RandomRoll.RollRange(1, 100, new RollContext(_playerState)) <= _fedoraCrashChance) if (!IsCrashImmune(card) && RandomRoll.RollRange(1, 100, new RollContext(_playerState)) <= _fedoraCrashChance)
{ {
_fedoraCrashChance = 10; _fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
if (IsDistro("fedora") && _runConfig.DistroVersion >= 4) if (IsDistro("fedora") && _runConfig.DistroVersion >= 4)
{ {
_playerState.Cycles += 1; _playerState.Cycles += 1;
@@ -276,10 +280,12 @@ namespace KernelPanic.Combat
return true; return true;
} }
_fedoraCrashChance = Mathf.Min(90, _fedoraCrashChance + 10); _fedoraCrashChance = Mathf.Min(
CombatTuning.FedoraBleedingEdgeMaxCrashChance,
_fedoraCrashChance + CombatTuning.FedoraBleedingEdgeCrashChanceStep);
card.MarkFedoraNonCrashBonus(); card.MarkFedoraNonCrashBonus();
_playerState.DamageMultiplierPercent = _runConfig.DistroVersion >= 2 ? 175 : 150; _playerState.DamageMultiplierPercent = _runConfig.DistroVersion >= 2 ? 175 : 150;
ApplyFedoraGrowth(card, rawhideChargeUsed); ApplyFedoraGrowth(card, rawhideChargeUsed, firstFedoraBonusThisTurn);
} }
if (card.Definition.Language == Language.C && _cCardsPlayedThisTurn == 0 && HasPackageEffect(PackageEffectKind.FirstCThisTurnDamageMultiplier, out PackageEffectData cPackageEffect)) if (card.Definition.Language == Language.C && _cCardsPlayedThisTurn == 0 && HasPackageEffect(PackageEffectKind.FirstCThisTurnDamageMultiplier, out PackageEffectData cPackageEffect))
@@ -372,6 +378,27 @@ namespace KernelPanic.Combat
return true; return true;
} }
public bool TryUnlockCard(CardInstance card)
{
if (currentPhase != TurnPhase.Execute || card == null || !card.IsLocked || _playerState == null)
{
return false;
}
int cost = EnemyArchetypeCatalog.DrmUnlockCycleCost;
if (_playerState.Cycles < cost)
{
Log($"not enough cycles to unlock {GetCardName(card)}");
return false;
}
_playerState.Cycles -= cost;
card.IsLocked = false;
Log($"unlocked {GetCardName(card)}");
StateChanged?.Invoke();
return true;
}
public void ReportEffectResult(string message) public void ReportEffectResult(string message)
{ {
if (!string.IsNullOrWhiteSpace(message)) if (!string.IsNullOrWhiteSpace(message))
@@ -626,7 +653,7 @@ namespace KernelPanic.Combat
_awaitingWaveContinue = false; _awaitingWaveContinue = false;
_loggedEffectTodos.Clear(); _loggedEffectTodos.Clear();
_ubuntuEmptyHandRefillUsed = false; _ubuntuEmptyHandRefillUsed = false;
_fedoraCrashChance = 10; _fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
_fedoraCardsDiscountedThisTurn = 0; _fedoraCardsDiscountedThisTurn = 0;
_cardsPlayedThisTurn = 0; _cardsPlayedThisTurn = 0;
_cardsPlayedThisWave = 0; _cardsPlayedThisWave = 0;
@@ -700,6 +727,7 @@ namespace KernelPanic.Combat
_interpreterQueue.Clear(); _interpreterQueue.Clear();
_enemies.Clear(); _enemies.Clear();
SpawnStructuralEnemies(); SpawnStructuralEnemies();
RecalculateKernelRamPressure();
PickEnemyIntents(); PickEnemyIntents();
StateChanged?.Invoke(); StateChanged?.Invoke();
yield return DrawOpeningHandSequenced(); yield return DrawOpeningHandSequenced();
@@ -715,6 +743,7 @@ namespace KernelPanic.Combat
} }
RevivePendingEnemies(); RevivePendingEnemies();
UnlockHandCards("license check expired");
_playerState.Cycles = _playerState.MaxCycles; _playerState.Cycles = _playerState.MaxCycles;
_fedoraCardsDiscountedThisTurn = 0; _fedoraCardsDiscountedThisTurn = 0;
_cardsPlayedThisTurn = 0; _cardsPlayedThisTurn = 0;
@@ -856,7 +885,7 @@ namespace KernelPanic.Combat
private IEnumerator DrawOpeningHandSequenced() private IEnumerator DrawOpeningHandSequenced()
{ {
int openingDraw = Mathf.Min(CombatTuning.OpeningHandSize, _playerState.Ram); int openingDraw = Mathf.Min(CombatTuning.OpeningHandSize, _handController == null ? _playerState.Ram : _handController.RamCapacity);
yield return DrawCardsToHandSequenced(openingDraw, "opening hand"); yield return DrawCardsToHandSequenced(openingDraw, "opening hand");
} }
@@ -954,9 +983,87 @@ namespace KernelPanic.Combat
StartCoroutine(DrawCardsToHandSequenced(_playerState.Ram, "ubuntu 24.04 refill")); StartCoroutine(DrawCardsToHandSequenced(_playerState.Ram, "ubuntu 24.04 refill"));
} }
private bool CanApplyFedoraBonus() private bool CanApplyFedoraBonus(CardInstance card)
{ {
return CanApplyFedoraPassiveBonus() || _rawhideBonusCharges > 0; return CardEffectFactory.CanReceiveBleedingEdgeDamageBonus(card?.Definition)
&& CardCanBenefitFromBleedingEdgeNow(card)
&& (CanApplyFedoraPassiveBonus() || _rawhideBonusCharges > 0);
}
private bool CardCanBenefitFromBleedingEdgeNow(CardInstance card)
{
string id = card?.Definition?.Id;
if (string.IsNullOrWhiteSpace(id))
{
return false;
}
if (id == "arch_consult_wiki")
{
return _handController == null || !HasBrokenHandCard();
}
if (id == "fedora_dnf_autoremove")
{
return HasJunkHandCard(card);
}
return true;
}
private bool HasBrokenHandCard()
{
if (_handController == null)
{
return false;
}
for (int i = 0; i < _handController.Cards.Count; i++)
{
if (_handController.Cards[i]?.IsBroken == true)
{
return true;
}
}
return false;
}
private bool HasJunkHandCard(CardInstance playedCard)
{
if (_handController == null)
{
return false;
}
for (int i = 0; i < _handController.Cards.Count; i++)
{
CardInstance card = _handController.Cards[i];
if (card != playedCard && IsJunkForDnfAutoremove(card))
{
return true;
}
}
return false;
}
private static bool IsJunkForDnfAutoremove(CardInstance card)
{
if (card == null)
{
return false;
}
string id = card.Definition?.Id ?? string.Empty;
string name = card.Definition?.DisplayName ?? string.Empty;
return card.Definition != null && card.Definition.IsToken
|| card.IsBroken
|| id.IndexOf("junk", StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("nop", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("junk", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("NOP", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("broken", StringComparison.OrdinalIgnoreCase) >= 0;
} }
private void DecayJavaWarmupForNextWave() private void DecayJavaWarmupForNextWave()
@@ -981,9 +1088,9 @@ namespace KernelPanic.Combat
return _fedoraCardsDiscountedThisTurn < limit; return _fedoraCardsDiscountedThisTurn < limit;
} }
private void ApplyFedoraGrowth(CardInstance card, bool rawhideChargeUsed) private void ApplyFedoraGrowth(CardInstance card, bool rawhideChargeUsed, bool firstFedoraBonusThisTurn)
{ {
if (!IsDistro("fedora") || _runConfig.DistroVersion < 3 || card == null || !card.WasFirstCardThisTurn) if (!IsDistro("fedora") || _runConfig.DistroVersion < 3 || card == null || !firstFedoraBonusThisTurn)
{ {
return; return;
} }
@@ -1739,6 +1846,7 @@ namespace KernelPanic.Combat
_runManager?.AddBits(CombatTuning.BitsPerKill); _runManager?.AddBits(CombatTuning.BitsPerKill);
HandleEnemyDeath(enemy); HandleEnemyDeath(enemy);
_enemies.RemoveAt(i); _enemies.RemoveAt(i);
RecalculateKernelRamPressure();
} }
} }
@@ -1803,6 +1911,87 @@ namespace KernelPanic.Combat
return candidates[index]; return candidates[index];
} }
private void UnlockHandCards(string reason)
{
if (_handController == null)
{
return;
}
int unlocked = 0;
for (int i = 0; i < _handController.Cards.Count; i++)
{
CardInstance card = _handController.Cards[i];
if (card != null && card.IsLocked)
{
card.IsLocked = false;
unlocked++;
}
}
if (unlocked > 0)
{
Log($"{reason}: unlocked {unlocked} card(s)");
}
}
private void RecalculateKernelRamPressure()
{
if (_handController == null || _playerState == null || _runManager == null)
{
return;
}
int penalty = 0;
for (int i = 0; i < _enemies.Count; i++)
{
EnemyInstance enemy = _enemies[i];
if (enemy.HasBehavior(EnemyBehaviorFlags.RamPressure) && !enemy.State.IsDefeated)
{
penalty += EnemyArchetypeCatalog.KernelPanicRamPenalty;
}
}
_activeKernelRamPenalty = penalty;
int baseRam = _runManager.EffectiveRam();
int effectiveRam = Mathf.Max(1, baseRam - penalty);
_playerState.Ram = effectiveRam;
_handController.SetRamCapacity(effectiveRam);
}
private void ApplyTelemetryCardPlayed()
{
int collectors = 0;
for (int i = 0; i < _enemies.Count; i++)
{
EnemyInstance enemy = _enemies[i];
if (enemy.HasBehavior(EnemyBehaviorFlags.TelemetryCollector) && !enemy.State.IsDefeated)
{
enemy.AddTelemetryStack();
collectors++;
}
}
if (collectors > 0)
{
Log($"telemetry collector profiled card: +{EnemyArchetypeCatalog.TelemetryDamageGrowthPerCard} damage");
StateChanged?.Invoke();
}
}
private void LockRandomHandCard(EnemyInstance source)
{
CardInstance card = RandomHandCard(cardInstance => cardInstance != null && !cardInstance.IsBroken && !cardInstance.IsLocked);
if (card == null)
{
Log("license check: no unlockable hand card");
return;
}
card.IsLocked = true;
Log($"{source.Name} locked {GetCardName(card)}");
}
private void BreakRacePair(EnemyInstance defeated) private void BreakRacePair(EnemyInstance defeated)
{ {
EnemyInstance survivor = RacePartner(defeated); EnemyInstance survivor = RacePartner(defeated);
@@ -1829,7 +2018,8 @@ namespace KernelPanic.Combat
int count = Mathf.Clamp(CombatTuning.BaseEnemiesPerWave + countGrowth, CombatTuning.BaseEnemiesPerWave, CombatTuning.MaxEnemiesPerWave); int count = Mathf.Clamp(CombatTuning.BaseEnemiesPerWave + countGrowth, CombatTuning.BaseEnemiesPerWave, CombatTuning.MaxEnemiesPerWave);
int waveUptimeBonus = Mathf.Max(0, wave - 1) * CombatTuning.EnemyUptimeGrowthPerWave; int waveUptimeBonus = Mathf.Max(0, wave - 1) * CombatTuning.EnemyUptimeGrowthPerWave;
bool rootkitSpawned = false; bool rootkitSpawned = false;
for (int i = 0; i < count; i++) bool eliteSpawned = TrySpawnEliteEncounter(wave, waveUptimeBonus);
for (int i = eliteSpawned ? 1 : 0; i < count; i++)
{ {
string archetypeId = CreatePlaceholderArchetypeId(wave, i); string archetypeId = CreatePlaceholderArchetypeId(wave, i);
if (archetypeId == "rootkit") if (archetypeId == "rootkit")
@@ -1860,6 +2050,33 @@ namespace KernelPanic.Combat
} }
} }
private bool TrySpawnEliteEncounter(int wave, int uptimeBonus)
{
if (wave < 5)
{
return false;
}
bool guaranteedIntro = wave == 5;
bool rolledElite = RandomRoll.RollRange(1, 100, RollContext.None) <= 35;
if (!guaranteedIntro && !rolledElite)
{
return false;
}
SpawnEnemy(CreateEliteArchetypeId(), uptimeBonus);
Log("elite process escorted by fodder");
// TODO: Replace this single-elite chance with authored elite encounter assets.
return true;
}
private static string CreateEliteArchetypeId()
{
string[] pool = { "daemon", "kernel_panic", "telemetry_collector", "drm_guardian" };
int index = RandomRoll.RollRange(0, pool.Length - 1, RollContext.None);
return pool[index];
}
private void SpawnRacePair(int uptimeBonus) private void SpawnRacePair(int uptimeBonus)
{ {
int pairId = _nextRacePairId++; int pairId = _nextRacePairId++;
@@ -2002,15 +2219,24 @@ namespace KernelPanic.Combat
Log("TODO: buff intent needs enemy buff/status support."); Log("TODO: buff intent needs enemy buff/status support.");
break; break;
case EnemyIntentKind.Special: case EnemyIntentKind.Special:
ExecuteSpecialEnemyIntent(enemy); ExecuteSpecialEnemyIntent(enemy, value);
break; break;
} }
enemy.AdvanceCountdownAfterAction(); enemy.AdvanceCountdownAfterAction();
} }
private void ExecuteSpecialEnemyIntent(EnemyInstance enemy) private void ExecuteSpecialEnemyIntent(EnemyInstance enemy, int value)
{ {
if (enemy.HasBehavior(EnemyBehaviorFlags.CardLocker))
{
LockRandomHandCard(enemy);
value = ApplyIncomingAttackMitigation(value);
DamageResult result = _damagePipeline.DealDamage(new DamageRequest(enemy.State, _playerState, value, enemy.CurrentIntent.DamageType, enemy.CurrentIntent.TrueDamage, false));
GameEvents.RaisePlayerDamaged(new PlayerDamagedEvent(enemy, result.FinalAmount));
return;
}
if (enemy.HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath)) if (enemy.HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath))
{ {
enemy.AdvanceSegfaultCountdown(); enemy.AdvanceSegfaultCountdown();
+34
View File
@@ -41,6 +41,12 @@ namespace KernelPanic.Combat
public const int StatUpgradeHealPercent = 30; public const int StatUpgradeHealPercent = 30;
public const int StatUpgradeRam = 1; public const int StatUpgradeRam = 1;
public const int UpgradeMagnitudeBonus = 2; public const int UpgradeMagnitudeBonus = 2;
public const int FedoraBleedingEdgeBaseCrashChance = 5;
public const int FedoraBleedingEdgeCrashChanceStep = 5;
public const int FedoraBleedingEdgeMaxCrashChance = 90;
public const int HitMagnitudeModeratePercent = 10;
public const int HitMagnitudeMajorPercent = 25;
public const int HitMagnitudeMassivePercent = 45;
public static int ScaleStatUpgradeMaxUptime(int maxUptime) public static int ScaleStatUpgradeMaxUptime(int maxUptime)
{ {
@@ -61,4 +67,32 @@ namespace KernelPanic.Combat
public const float EnemyTelegraphDelaySeconds = 0.35f; public const float EnemyTelegraphDelaySeconds = 0.35f;
public const float EnemyActionDelaySeconds = 0.45f; public const float EnemyActionDelaySeconds = 0.45f;
} }
public enum HitMagnitudeTier
{
Minor,
Moderate,
Major,
Massive
}
public static class HitMagnitude
{
public static HitMagnitudeTier Classify(int damageAmount, int targetMaxUptime, bool targetDefeated)
{
int maxUptime = UnityEngine.Mathf.Max(1, targetMaxUptime);
int amount = UnityEngine.Mathf.Max(0, damageAmount);
int percent = UnityEngine.Mathf.FloorToInt((amount * 100f) / maxUptime);
HitMagnitudeTier tier = percent >= CombatTuning.HitMagnitudeMassivePercent
? HitMagnitudeTier.Massive
: percent >= CombatTuning.HitMagnitudeMajorPercent
? HitMagnitudeTier.Major
: percent >= CombatTuning.HitMagnitudeModeratePercent
? HitMagnitudeTier.Moderate
: HitMagnitudeTier.Minor;
return targetDefeated && tier < HitMagnitudeTier.Major ? HitMagnitudeTier.Major : tier;
}
}
} }
+1 -1
View File
@@ -81,7 +81,7 @@ namespace KernelPanic.Combat
int finalAmount = ApplyShieldAndUptime(amount, request, out int absorbedAmount, out int shieldDamage, out int uptimeDamage); int finalAmount = ApplyShieldAndUptime(amount, request, out int absorbedAmount, out int shieldDamage, out int uptimeDamage);
ApplyArchRollingReleaseSave(request); ApplyArchRollingReleaseSave(request);
GameEvents.RaiseDamageDealt(new DamageDealtEvent(request.Source, request.Target, finalAmount, request.Language, amount, absorbedAmount, wasCritical, shieldDamage, uptimeDamage)); GameEvents.RaiseDamageDealt(new DamageDealtEvent(request.Source, request.Target, finalAmount, request.Language, amount, absorbedAmount, wasCritical, shieldDamage, uptimeDamage, request.TrueDamage));
bool defeated = false; bool defeated = false;
if (request.Target.CurrentUptime <= 0 && !request.Target.IsDefeated) if (request.Target.CurrentUptime <= 0 && !request.Target.IsDefeated)
+58 -3
View File
@@ -19,7 +19,16 @@ namespace KernelPanic.Combat
SegfaultOnDeath = 1 << 7, SegfaultOnDeath = 1 << 7,
RacePair = 1 << 8, RacePair = 1 << 8,
RootkitMasked = 1 << 9, RootkitMasked = 1 << 9,
Elite = 1 << 10 Elite = 1 << 10,
RamPressure = 1 << 11,
TelemetryCollector = 1 << 12,
CardLocker = 1 << 13
}
public enum EnemyTier
{
Fodder,
Elite
} }
public sealed class EnemyArchetypeDescriptor public sealed class EnemyArchetypeDescriptor
@@ -30,7 +39,8 @@ namespace KernelPanic.Combat
int baseUptimeMin, int baseUptimeMin,
int baseUptimeMax, int baseUptimeMax,
EnemyBehaviorFlags behaviorFlags, EnemyBehaviorFlags behaviorFlags,
IReadOnlyList<EnemyIntent> intentPool) IReadOnlyList<EnemyIntent> intentPool,
EnemyTier tier = EnemyTier.Fodder)
{ {
Id = string.IsNullOrWhiteSpace(id) ? "unknown_process" : id; Id = string.IsNullOrWhiteSpace(id) ? "unknown_process" : id;
DisplayName = string.IsNullOrWhiteSpace(displayName) ? Id : displayName; DisplayName = string.IsNullOrWhiteSpace(displayName) ? Id : displayName;
@@ -38,6 +48,7 @@ namespace KernelPanic.Combat
BaseUptimeMax = Mathf.Max(BaseUptimeMin, baseUptimeMax); BaseUptimeMax = Mathf.Max(BaseUptimeMin, baseUptimeMax);
BehaviorFlags = behaviorFlags; BehaviorFlags = behaviorFlags;
IntentPool = intentPool ?? Array.Empty<EnemyIntent>(); IntentPool = intentPool ?? Array.Empty<EnemyIntent>();
Tier = tier;
} }
public string Id { get; } public string Id { get; }
@@ -46,6 +57,7 @@ namespace KernelPanic.Combat
public int BaseUptimeMax { get; } public int BaseUptimeMax { get; }
public EnemyBehaviorFlags BehaviorFlags { get; } public EnemyBehaviorFlags BehaviorFlags { get; }
public IReadOnlyList<EnemyIntent> IntentPool { get; } public IReadOnlyList<EnemyIntent> IntentPool { get; }
public EnemyTier Tier { get; }
} }
/// <summary> /// <summary>
@@ -61,6 +73,9 @@ namespace KernelPanic.Combat
public const int ForkBombTotalCap = 5; public const int ForkBombTotalCap = 5;
public const int RootkitMaskedDamagePercent = 10; public const int RootkitMaskedDamagePercent = 10;
public const int RacePairDamageBonus = 2; public const int RacePairDamageBonus = 2;
public const int KernelPanicRamPenalty = 2;
public const int TelemetryDamageGrowthPerCard = 1;
public const int DrmUnlockCycleCost = 1;
public const int CronInitialCountdown = 2; public const int CronInitialCountdown = 2;
public const int SegfaultInitialCountdown = 2; public const int SegfaultInitialCountdown = 2;
@@ -106,7 +121,8 @@ namespace KernelPanic.Combat
new[] new[]
{ {
new EnemyIntent(EnemyIntentKind.Attack, 6, 8, Language.C, "daemon hit", "!") new EnemyIntent(EnemyIntentKind.Attack, 6, 8, Language.C, "daemon hit", "!")
}); },
EnemyTier.Elite);
private static readonly EnemyArchetypeDescriptor Defender = new( private static readonly EnemyArchetypeDescriptor Defender = new(
"firewalld", "firewalld",
@@ -176,6 +192,42 @@ namespace KernelPanic.Combat
new EnemyIntent(EnemyIntentKind.Attack, 1, 2, Language.C, "masked hit", "?") new EnemyIntent(EnemyIntentKind.Attack, 1, 2, Language.C, "masked hit", "?")
}); });
private static readonly EnemyArchetypeDescriptor KernelPanic = new(
"kernel_panic",
"Kernel Panic",
25,
30,
EnemyBehaviorFlags.Elite | EnemyBehaviorFlags.RamPressure,
new[]
{
new EnemyIntent(EnemyIntentKind.Attack, 5, 7, Language.C, "panic hit", "!")
},
EnemyTier.Elite);
private static readonly EnemyArchetypeDescriptor TelemetryCollector = new(
"telemetry_collector",
"Telemetry Collector",
20,
25,
EnemyBehaviorFlags.Elite | EnemyBehaviorFlags.TelemetryCollector,
new[]
{
new EnemyIntent(EnemyIntentKind.Attack, 2, 3, Language.C, "profile hit", "!")
},
EnemyTier.Elite);
private static readonly EnemyArchetypeDescriptor DrmGuardian = new(
"drm_guardian",
"DRM Guardian",
25,
30,
EnemyBehaviorFlags.Elite | EnemyBehaviorFlags.CardLocker,
new[]
{
new EnemyIntent(EnemyIntentKind.Special, 4, 6, Language.C, "license check", "$")
},
EnemyTier.Elite);
public static EnemyArchetypeDescriptor Get(string id) public static EnemyArchetypeDescriptor Get(string id)
{ {
return id switch return id switch
@@ -189,6 +241,9 @@ namespace KernelPanic.Combat
"segfault" => Segfault, "segfault" => Segfault,
"race_condition" => RaceCondition, "race_condition" => RaceCondition,
"rootkit" => Rootkit, "rootkit" => Rootkit,
"kernel_panic" => KernelPanic,
"telemetry_collector" => TelemetryCollector,
"drm_guardian" => DrmGuardian,
_ => ZombieProcess _ => ZombieProcess
}; };
} }
+26 -2
View File
@@ -46,14 +46,18 @@ namespace KernelPanic.Combat
public bool LethalHitThisTurn { get; private set; } public bool LethalHitThisTurn { get; private set; }
public int PairId { get; private set; } = -1; public int PairId { get; private set; } = -1;
public int RaceEnrageStacks { get; private set; } public int RaceEnrageStacks { get; private set; }
public int TelemetryStacks { get; private set; }
public bool SpawnedFromDeath { get; private set; } public bool SpawnedFromDeath { get; private set; }
public bool HasPendingMarker => PendingRevive || HasRevived; public bool HasPendingMarker => PendingRevive || HasRevived;
public bool HasSpecialSignal => HasSplitSignal || HasSegfaultSignal || HasRaceSignal || HasRootkitSignal || HasEliteSignal || SpawnedFromDeath; public bool HasSpecialSignal => HasSplitSignal || HasSegfaultSignal || HasRaceSignal || HasRootkitSignal || HasEliteSignal || HasRamPressureSignal || HasTelemetrySignal || HasCardLockerSignal || SpawnedFromDeath;
public bool HasSplitSignal => HasBehavior(EnemyBehaviorFlags.Split); public bool HasSplitSignal => HasBehavior(EnemyBehaviorFlags.Split);
public bool HasSegfaultSignal => HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath); public bool HasSegfaultSignal => HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath);
public bool HasRaceSignal => HasBehavior(EnemyBehaviorFlags.RacePair); public bool HasRaceSignal => HasBehavior(EnemyBehaviorFlags.RacePair);
public bool HasRootkitSignal => HasBehavior(EnemyBehaviorFlags.RootkitMasked); public bool HasRootkitSignal => HasBehavior(EnemyBehaviorFlags.RootkitMasked);
public bool HasEliteSignal => HasBehavior(EnemyBehaviorFlags.Elite); public bool HasEliteSignal => Archetype.Tier == EnemyTier.Elite || HasBehavior(EnemyBehaviorFlags.Elite);
public bool HasRamPressureSignal => HasBehavior(EnemyBehaviorFlags.RamPressure);
public bool HasTelemetrySignal => HasBehavior(EnemyBehaviorFlags.TelemetryCollector);
public bool HasCardLockerSignal => HasBehavior(EnemyBehaviorFlags.CardLocker);
public EnemyIntent DisplayIntent => BuildDisplayIntent(); public EnemyIntent DisplayIntent => BuildDisplayIntent();
public void PickNextIntent() public void PickNextIntent()
@@ -93,6 +97,14 @@ namespace KernelPanic.Combat
int maxValue = UnityEngine.Mathf.Min(CurrentIntent.MaxValue + growth, EnemyArchetypeCatalog.MemoryLeakAttackCapMax); int maxValue = UnityEngine.Mathf.Min(CurrentIntent.MaxValue + growth, EnemyArchetypeCatalog.MemoryLeakAttackCapMax);
CurrentIntent = CurrentIntent.WithValues(minValue, maxValue, "growing leak"); CurrentIntent = CurrentIntent.WithValues(minValue, maxValue, "growing leak");
} }
if (HasBehavior(EnemyBehaviorFlags.TelemetryCollector) && CurrentIntent.Kind == EnemyIntentKind.Attack && TelemetryStacks > 0)
{
CurrentIntent = CurrentIntent.WithValues(
CurrentIntent.MinValue + TelemetryStacks,
CurrentIntent.MaxValue + TelemetryStacks,
"profiled hit");
}
} }
public bool HasBehavior(EnemyBehaviorFlags behavior) public bool HasBehavior(EnemyBehaviorFlags behavior)
@@ -128,6 +140,18 @@ namespace KernelPanic.Combat
RaceEnrageStacks++; RaceEnrageStacks++;
} }
public void AddTelemetryStack()
{
TelemetryStacks += EnemyArchetypeCatalog.TelemetryDamageGrowthPerCard;
if (CurrentIntent.Kind == EnemyIntentKind.Attack)
{
CurrentIntent = CurrentIntent.WithValues(
CurrentIntent.MinValue + EnemyArchetypeCatalog.TelemetryDamageGrowthPerCard,
CurrentIntent.MaxValue + EnemyArchetypeCatalog.TelemetryDamageGrowthPerCard,
"profiled hit");
}
}
public void ResetTurnLethalMarker() public void ResetTurnLethalMarker()
{ {
LethalHitThisTurn = false; LethalHitThisTurn = false;
+5
View File
@@ -54,6 +54,11 @@ namespace KernelPanic.Combat
return card != null && _cards.Remove(card); return card != null && _cards.Remove(card);
} }
public void SetRamCapacity(int ramCapacity)
{
RamCapacity = System.Math.Max(1, ramCapacity);
}
public void Clear() public void Clear()
{ {
_cards.Clear(); _cards.Clear();
+8 -1
View File
@@ -162,11 +162,16 @@ namespace KernelPanic.Core
} }
public DamageDealtEvent(CombatantState source, CombatantState target, int amount, Language language, int incomingAmount, int absorbedAmount, bool wasCritical) public DamageDealtEvent(CombatantState source, CombatantState target, int amount, Language language, int incomingAmount, int absorbedAmount, bool wasCritical)
: this(source, target, amount, language, incomingAmount, absorbedAmount, wasCritical, 0, amount) : this(source, target, amount, language, incomingAmount, absorbedAmount, wasCritical, 0, amount, false)
{ {
} }
public DamageDealtEvent(CombatantState source, CombatantState target, int amount, Language language, int incomingAmount, int absorbedAmount, bool wasCritical, int shieldDamage, int uptimeDamage) public DamageDealtEvent(CombatantState source, CombatantState target, int amount, Language language, int incomingAmount, int absorbedAmount, bool wasCritical, int shieldDamage, int uptimeDamage)
: this(source, target, amount, language, incomingAmount, absorbedAmount, wasCritical, shieldDamage, uptimeDamage, false)
{
}
public DamageDealtEvent(CombatantState source, CombatantState target, int amount, Language language, int incomingAmount, int absorbedAmount, bool wasCritical, int shieldDamage, int uptimeDamage, bool trueDamage)
{ {
Source = source; Source = source;
Target = target; Target = target;
@@ -177,6 +182,7 @@ namespace KernelPanic.Core
WasCritical = wasCritical; WasCritical = wasCritical;
ShieldDamage = shieldDamage; ShieldDamage = shieldDamage;
UptimeDamage = uptimeDamage; UptimeDamage = uptimeDamage;
TrueDamage = trueDamage;
} }
public CombatantState Source { get; } public CombatantState Source { get; }
@@ -188,6 +194,7 @@ namespace KernelPanic.Core
public bool WasCritical { get; } public bool WasCritical { get; }
public int ShieldDamage { get; } public int ShieldDamage { get; }
public int UptimeDamage { get; } public int UptimeDamage { get; }
public bool TrueDamage { get; }
public bool WasFullyBlocked => IncomingAmount > 0 && UptimeDamage <= 0; public bool WasFullyBlocked => IncomingAmount > 0 && UptimeDamage <= 0;
public bool WasMitigated => ShieldDamage > 0 || AbsorbedAmount > 0 || WasFullyBlocked; public bool WasMitigated => ShieldDamage > 0 || AbsorbedAmount > 0 || WasFullyBlocked;
} }
+231 -19
View File
@@ -23,6 +23,7 @@ namespace KernelPanic.UI
private const string StyleResourcePath = "CombatScene"; private const string StyleResourcePath = "CombatScene";
private const string SharedScrollbarStyleResourcePath = "TerminalScrollbars"; private const string SharedScrollbarStyleResourcePath = "TerminalScrollbars";
private const string SharedRarityStyleResourcePath = "RarityPresentation"; private const string SharedRarityStyleResourcePath = "RarityPresentation";
private const int MaxFeedbackElements = 42;
private const string FilledCycle = "●"; private const string FilledCycle = "●";
private const string EmptyCycle = "○"; private const string EmptyCycle = "○";
@@ -66,6 +67,9 @@ namespace KernelPanic.UI
private int _queueCascadeIndex; private int _queueCascadeIndex;
private int _feedbackCascadeIndex; private int _feedbackCascadeIndex;
private int _feedbackBeatVersion; private int _feedbackBeatVersion;
private int _rootShakeVersion;
private int _hitStopVersion;
private int _lastHitStopFrame = -1;
private void Awake() private void Awake()
{ {
@@ -143,6 +147,7 @@ namespace KernelPanic.UI
private void OnDisable() private void OnDisable()
{ {
Time.timeScale = 1f;
SettleRunRewardsIfNeeded(); SettleRunRewardsIfNeeded();
_combatManager.StateChanged -= Refresh; _combatManager.StateChanged -= Refresh;
_combatManager.CombatLog -= HandleCombatLog; _combatManager.CombatLog -= HandleCombatLog;
@@ -432,6 +437,14 @@ namespace KernelPanic.UI
Label ram = new($"ram hand cap {_combatManager.HandController.UsedRam}/{_combatManager.HandController.RamCapacity}"); Label ram = new($"ram hand cap {_combatManager.HandController.UsedRam}/{_combatManager.HandController.RamCapacity}");
ram.AddToClassList("ram-note"); ram.AddToClassList("ram-note");
_playerPanel.Add(ram); _playerPanel.Add(ram);
if (_combatManager.ActiveKernelRamPenalty > 0)
{
Label ramPressure = new($"! kernel panic: RAM -{_combatManager.ActiveKernelRamPenalty}");
ramPressure.AddToClassList("status-label");
ramPressure.AddToClassList("intent-attack");
_playerPanel.Add(ramPressure);
}
_playerPanel.Add(StatusBlock(state, true)); _playerPanel.Add(StatusBlock(state, true));
} }
@@ -465,6 +478,7 @@ namespace KernelPanic.UI
card.text = string.Empty; card.text = string.Empty;
card.AddToClassList("enemy-card"); card.AddToClassList("enemy-card");
card.EnableInClassList("enemy-card-compact", compactEnemies); card.EnableInClassList("enemy-card-compact", compactEnemies);
card.EnableInClassList("enemy-card-elite", enemy.HasEliteSignal);
bool highlighted = _combatManager.PendingTargetCard != null || _combatManager.SelectedEnemyIndex == index; bool highlighted = _combatManager.PendingTargetCard != null || _combatManager.SelectedEnemyIndex == index;
if (highlighted) if (highlighted)
{ {
@@ -823,7 +837,16 @@ namespace KernelPanic.UI
private VisualElement CardFace(CardInstance card) private VisualElement CardFace(CardInstance card)
{ {
Button button = new(() => PlayCardWithFeedback(card)); Button button = new(() =>
{
if (card.IsLocked)
{
UnlockCardWithFeedback(card);
return;
}
PlayCardWithFeedback(card);
});
button.text = string.Empty; button.text = string.Empty;
button.AddToClassList("hand-card"); button.AddToClassList("hand-card");
if (_combatManager.PendingTargetCard == card) if (_combatManager.PendingTargetCard == card)
@@ -839,7 +862,7 @@ namespace KernelPanic.UI
if (card.IsBroken || card.IsLocked) if (card.IsBroken || card.IsLocked)
{ {
button.AddToClassList("hand-card-unaffordable"); button.AddToClassList("hand-card-unaffordable");
button.SetEnabled(false); button.SetEnabled(!card.IsBroken);
} }
PopulateCardFace(button, card); PopulateCardFace(button, card);
@@ -874,7 +897,7 @@ namespace KernelPanic.UI
if (card.IsBroken || card.IsLocked) if (card.IsBroken || card.IsLocked)
{ {
Label state = new(card.IsBroken ? "segv corrupt" : "locked"); Label state = new(card.IsBroken ? "segv corrupt" : $"locked: pay {EnemyArchetypeCatalog.DrmUnlockCycleCost}c");
state.AddToClassList("status-label"); state.AddToClassList("status-label");
target.Add(state); target.Add(state);
} }
@@ -926,6 +949,21 @@ namespace KernelPanic.UI
row.Add(Tag("! elite", "intent-attack")); row.Add(Tag("! elite", "intent-attack"));
} }
if (enemy.HasRamPressureSignal)
{
row.Add(Tag($"# RAM -{EnemyArchetypeCatalog.KernelPanicRamPenalty}", "intent-attack"));
}
if (enemy.HasTelemetrySignal)
{
row.Add(Tag($"+ profile {enemy.TelemetryStacks}", "intent-special"));
}
if (enemy.HasCardLockerSignal)
{
row.Add(Tag($"$ lock {EnemyArchetypeCatalog.DrmUnlockCycleCost}c", "intent-special"));
}
if (enemy.HasSplitSignal) if (enemy.HasSplitSignal)
{ {
string text = CountLivingArchetype(enemies, enemy.ArchetypeId) >= EnemyArchetypeCatalog.ForkBombTotalCap string text = CountLivingArchetype(enemies, enemy.ArchetypeId) >= EnemyArchetypeCatalog.ForkBombTotalCap
@@ -1155,6 +1193,29 @@ namespace KernelPanic.UI
return false; return false;
} }
private bool UnlockCardWithFeedback(CardInstance card)
{
int cyclesBefore = _combatManager.PlayerState == null ? 0 : _combatManager.PlayerState.Cycles;
bool unlocked = _combatManager.TryUnlockCard(card);
if (unlocked)
{
int spent = Mathf.Max(0, cyclesBefore - (_combatManager.PlayerState == null ? cyclesBefore : _combatManager.PlayerState.Cycles));
if (spent > 0)
{
PlayElementBeat(_turnResourceGrid, "cycles-spent-beat", 220);
}
return true;
}
if (_handCardElements.TryGetValue(card, out VisualElement face))
{
PlayElementBeat(face, "feedback-denied", 220);
}
return false;
}
private void HandleCardPlayed(CardPlayedEvent payload) private void HandleCardPlayed(CardPlayedEvent payload)
{ {
if (payload.Card == null) if (payload.Card == null)
@@ -1216,24 +1277,30 @@ namespace KernelPanic.UI
{ {
bool isPlayer = payload.Target == _combatManager.PlayerState; bool isPlayer = payload.Target == _combatManager.PlayerState;
VisualElement target = CombatantElement(payload.Target); VisualElement target = CombatantElement(payload.Target);
int magnitudeAmount = payload.UptimeDamage > 0 ? payload.UptimeDamage : payload.ShieldDamage > 0 ? payload.ShieldDamage : payload.Amount;
bool defeated = payload.Target != null && payload.Target.CurrentUptime <= 0;
HitMagnitudeTier tier = HitMagnitude.Classify(magnitudeAmount, payload.Target?.MaxUptime ?? 1, defeated);
bool shieldOnly = payload.WasFullyBlocked && payload.ShieldDamage > 0;
ScheduleCombatBeat(beatIndex => ScheduleCombatBeat(beatIndex =>
{ {
string text = payload.WasFullyBlocked ? "clang" : payload.UptimeDamage <= 0 ? "blocked" : $"-{Mathf.RoundToInt(payload.UptimeDamage)}"; string bypass = payload.TrueDamage ? "! " : string.Empty;
string tone = payload.WasFullyBlocked || payload.WasMitigated ? "float-muted" : "float-damage"; string text = shieldOnly
string beatClass = payload.WasFullyBlocked || payload.WasMitigated ? "feedback-clang" : payload.WasCritical ? "feedback-crit" : "feedback-hit"; ? $"{bypass}block {payload.ShieldDamage}"
int beatDuration = payload.WasCritical ? 360 : payload.WasMitigated ? 260 : 240; : payload.WasFullyBlocked
? $"{bypass}blocked"
if (payload.WasCritical || payload.UptimeDamage >= 8) : $"{bypass}-{Mathf.RoundToInt(payload.UptimeDamage)}";
{ string tone = BuildDamageFloatClasses(tier, payload, shieldOnly);
tone += " float-large"; string beatClass = BuildDamageBeatClass(tier, payload, shieldOnly);
} int beatDuration = HitBeatDuration(tier, payload.WasMitigated);
TriggerHitStop(tier);
TriggerRootShake(tier, isPlayer);
SpawnFloatingText(target, text, tone, beatIndex); SpawnFloatingText(target, text, tone, beatIndex);
SpawnImpactMarker(target, payload.WasCritical, payload.WasMitigated, beatIndex); SpawnImpactMarker(target, payload.WasCritical, payload.WasMitigated, beatIndex, tier, shieldOnly);
PlayElementBeat(target, beatClass, beatDuration); PlayElementBeat(target, beatClass, beatDuration);
if (isPlayer && payload.UptimeDamage > 0) if (isPlayer && payload.UptimeDamage > 0)
{ {
FlashDamageVignette(payload.WasCritical); FlashDamageVignette(tier);
} }
}); });
} }
@@ -1308,6 +1375,80 @@ namespace KernelPanic.UI
_previousShield[state] = state.Shield; _previousShield[state] = state.Shield;
} }
private static string BuildDamageFloatClasses(HitMagnitudeTier tier, DamageDealtEvent payload, bool shieldOnly)
{
string classes = shieldOnly || payload.WasFullyBlocked ? "float-shield" : "float-damage";
classes += " " + TierFloatClass(tier);
if (payload.TrueDamage)
{
classes += " float-true";
}
return classes;
}
private static string BuildDamageBeatClass(HitMagnitudeTier tier, DamageDealtEvent payload, bool shieldOnly)
{
if (shieldOnly || payload.WasFullyBlocked)
{
return tier >= HitMagnitudeTier.Major ? "feedback-clang-major" : "feedback-clang";
}
if (payload.WasCritical || tier == HitMagnitudeTier.Massive)
{
return "feedback-crit";
}
return tier switch
{
HitMagnitudeTier.Minor => "feedback-hit-minor",
HitMagnitudeTier.Moderate => "feedback-hit",
HitMagnitudeTier.Major => "feedback-hit-major",
_ => "feedback-crit"
};
}
private static string TierFloatClass(HitMagnitudeTier tier)
{
return tier switch
{
HitMagnitudeTier.Minor => "float-tier-minor",
HitMagnitudeTier.Moderate => "float-tier-moderate",
HitMagnitudeTier.Major => "float-tier-major",
HitMagnitudeTier.Massive => "float-tier-massive",
_ => "float-tier-moderate"
};
}
private static string TierImpactClass(HitMagnitudeTier tier)
{
return tier switch
{
HitMagnitudeTier.Minor => "impact-tier-minor",
HitMagnitudeTier.Moderate => "impact-tier-moderate",
HitMagnitudeTier.Major => "impact-tier-major",
HitMagnitudeTier.Massive => "impact-tier-massive",
_ => "impact-tier-moderate"
};
}
private static int HitBeatDuration(HitMagnitudeTier tier, bool mitigated)
{
if (mitigated && tier < HitMagnitudeTier.Major)
{
return 220;
}
return tier switch
{
HitMagnitudeTier.Minor => 120,
HitMagnitudeTier.Moderate => 220,
HitMagnitudeTier.Major => 340,
HitMagnitudeTier.Massive => 460,
_ => 220
};
}
private VisualElement CombatantElement(CombatantState state) private VisualElement CombatantElement(CombatantState state)
{ {
if (state != null && _combatantElements.TryGetValue(state, out VisualElement element)) if (state != null && _combatantElements.TryGetValue(state, out VisualElement element))
@@ -1375,6 +1516,7 @@ namespace KernelPanic.UI
return; return;
} }
PruneFeedbackLayer();
Rect anchorRect = anchor.worldBound; Rect anchorRect = anchor.worldBound;
Rect rootRect = _root.worldBound; Rect rootRect = _root.worldBound;
Label label = new(text); Label label = new(text);
@@ -1412,18 +1554,20 @@ namespace KernelPanic.UI
ScheduleFeedback(() => label.RemoveFromHierarchy(), 520); ScheduleFeedback(() => label.RemoveFromHierarchy(), 520);
} }
private void SpawnImpactMarker(VisualElement anchor, bool critical, bool mitigated, int cascadeOffset) private void SpawnImpactMarker(VisualElement anchor, bool critical, bool mitigated, int cascadeOffset, HitMagnitudeTier tier = HitMagnitudeTier.Moderate, bool shieldOnly = false)
{ {
if (_feedbackLayer == null || anchor == null) if (_feedbackLayer == null || anchor == null)
{ {
return; return;
} }
PruneFeedbackLayer();
Rect anchorRect = anchor.worldBound; Rect anchorRect = anchor.worldBound;
Rect rootRect = _root.worldBound; Rect rootRect = _root.worldBound;
Label marker = new(critical ? "crit" : mitigated ? "clang" : "hit"); Label marker = new(shieldOnly ? "shield" : critical ? "crit" : mitigated ? "clang" : "hit");
marker.AddToClassList("impact-marker"); marker.AddToClassList("impact-marker");
marker.AddToClassList(critical ? "impact-marker-crit" : mitigated ? "impact-marker-clang" : "impact-marker-hit"); marker.AddToClassList(shieldOnly || mitigated ? "impact-marker-clang" : critical ? "impact-marker-crit" : "impact-marker-hit");
marker.AddToClassList(TierImpactClass(tier));
float lane = (cascadeOffset % 4) * 18f; float lane = (cascadeOffset % 4) * 18f;
marker.style.left = anchorRect.xMax - rootRect.x - 54f; marker.style.left = anchorRect.xMax - rootRect.x - 54f;
@@ -1444,6 +1588,65 @@ namespace KernelPanic.UI
ScheduleFeedback(() => marker.RemoveFromHierarchy(), 420); ScheduleFeedback(() => marker.RemoveFromHierarchy(), 420);
} }
private void TriggerHitStop(HitMagnitudeTier tier)
{
if (UIPreferences.ReducedMotion || tier < HitMagnitudeTier.Major || Time.frameCount == _lastHitStopFrame)
{
return;
}
_lastHitStopFrame = Time.frameCount;
int version = ++_hitStopVersion;
float duration = tier == HitMagnitudeTier.Massive ? 0.16f : 0.09f;
Time.timeScale = 0.04f;
ScheduleFeedback(() =>
{
if (_hitStopVersion == version)
{
Time.timeScale = 1f;
}
}, Mathf.RoundToInt(duration * 1000f));
}
private void TriggerRootShake(HitMagnitudeTier tier, bool playerHit)
{
if (UIPreferences.ReducedMotion || tier < HitMagnitudeTier.Major || _root == null)
{
return;
}
int version = ++_rootShakeVersion;
_root.RemoveFromClassList("root-shake-major");
_root.RemoveFromClassList("root-shake-massive");
_root.RemoveFromClassList("root-shake-player");
_root.AddToClassList(tier == HitMagnitudeTier.Massive ? "root-shake-massive" : "root-shake-major");
_root.EnableInClassList("root-shake-player", playerHit);
ScheduleFeedback(() =>
{
if (_rootShakeVersion != version)
{
return;
}
_root.RemoveFromClassList("root-shake-major");
_root.RemoveFromClassList("root-shake-massive");
_root.RemoveFromClassList("root-shake-player");
}, tier == HitMagnitudeTier.Massive ? 220 : 140);
}
private void PruneFeedbackLayer()
{
if (_feedbackLayer == null)
{
return;
}
while (_feedbackLayer.childCount >= MaxFeedbackElements)
{
_feedbackLayer.ElementAt(0).RemoveFromHierarchy();
}
}
private void FlyCardGhost(CardInstance card, VisualElement source, VisualElement destination, string className) private void FlyCardGhost(CardInstance card, VisualElement source, VisualElement destination, string className)
{ {
if (_feedbackLayer == null || card == null) if (_feedbackLayer == null || card == null)
@@ -1514,21 +1717,29 @@ namespace KernelPanic.UI
ScheduleFeedback(() => ghost.RemoveFromHierarchy(), 360); ScheduleFeedback(() => ghost.RemoveFromHierarchy(), 360);
} }
private void FlashDamageVignette(bool critical) private void FlashDamageVignette(HitMagnitudeTier tier)
{ {
if (_damageVignette == null) if (_damageVignette == null)
{ {
return; return;
} }
PlayElementBeat(_damageVignette, critical ? "damage-vignette-crit" : "damage-vignette-on", UIPreferences.ReducedMotion ? 120 : critical ? 320 : 240); string className = tier >= HitMagnitudeTier.Massive
? "damage-vignette-massive"
: tier >= HitMagnitudeTier.Major
? "damage-vignette-crit"
: "damage-vignette-on";
PlayElementBeat(_damageVignette, className, UIPreferences.ReducedMotion ? 120 : tier >= HitMagnitudeTier.Massive ? 360 : tier >= HitMagnitudeTier.Major ? 300 : 180);
} }
private static void RemoveFeedbackBeatClasses(VisualElement element) private static void RemoveFeedbackBeatClasses(VisualElement element)
{ {
element.RemoveFromClassList("feedback-hit-minor");
element.RemoveFromClassList("feedback-hit"); element.RemoveFromClassList("feedback-hit");
element.RemoveFromClassList("feedback-hit-major");
element.RemoveFromClassList("feedback-crit"); element.RemoveFromClassList("feedback-crit");
element.RemoveFromClassList("feedback-clang"); element.RemoveFromClassList("feedback-clang");
element.RemoveFromClassList("feedback-clang-major");
element.RemoveFromClassList("feedback-block"); element.RemoveFromClassList("feedback-block");
element.RemoveFromClassList("feedback-status"); element.RemoveFromClassList("feedback-status");
element.RemoveFromClassList("feedback-boost"); element.RemoveFromClassList("feedback-boost");
@@ -1543,6 +1754,7 @@ namespace KernelPanic.UI
element.RemoveFromClassList("hand-card-drawn"); element.RemoveFromClassList("hand-card-drawn");
element.RemoveFromClassList("damage-vignette-on"); element.RemoveFromClassList("damage-vignette-on");
element.RemoveFromClassList("damage-vignette-crit"); element.RemoveFromClassList("damage-vignette-crit");
element.RemoveFromClassList("damage-vignette-massive");
} }
private void ScheduleCombatBeat(Action action, int holdMs = 0) private void ScheduleCombatBeat(Action action, int holdMs = 0)