Implement animations, pipeline for duplicates and merging.

This commit is contained in:
2026-07-07 14:10:31 +02:00
parent 30db1e1108
commit d504e0b916
22 changed files with 1947 additions and 117 deletions
@@ -11,13 +11,29 @@
</component>
<component name="ChangeListManager">
<list default="true" id="4222a6b8-add3-4f43-9c42-fd6bb5a76169" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/Assets/Resources/TerminalScrollbars.uss" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Scripts/Meta/DistroVersionCatalog.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Scripts/Meta/GachaTuning.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Scripts/Meta/PullResolver.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Scripts/Meta/VersionUpgrader.cs" afterDir="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/Scripts/Combat/CombatTuning.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/CombatTuning.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Meta/GachaService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Meta/GachaService.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/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/CombatManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/CombatManager.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/DeckController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Combat/DeckController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Meta/PlayerCollection.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Meta/PlayerCollection.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Meta/SaveService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Meta/SaveService.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Run/RunConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Run/RunConfig.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Run/RunContext.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Run/RunContext.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/CollectionScreenController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/CollectionScreenController.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/GachaScreenController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/GachaScreenController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/MainMenuController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/MainMenuController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/CollectionScreen.uxml" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/CollectionScreen.uxml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/MainMenu.uss" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/MainMenu.uss" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/MainMenu.uxml" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/MainMenu.uxml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -52,9 +68,13 @@
<setting file="file://$PROJECT_DIR$/Assets/Data/Cards/Fedora/CargoBuildRelease.asset" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/Data/Cards/Languages/Python/PyPrint.asset" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/Data/Cards/Ubuntu/AskUbuntu.asset" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/Data/Distros/DistroDatabase.asset" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/Data/Distros/Fedora.asset" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/Data/Distros/Mint.asset" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/Data/Distros/Ubuntu.asset" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/Data/Passives/AptUpdate.asset" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/Data/Passives/BleedingEdge.asset" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/Data/Passives/ItJustWorks.asset" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/Scripts/Data/LanguageDeckDatabase.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/Scripts/UI.meta" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/UI/MainMenuPanelSettings.asset" root0="FORCE_HIGHLIGHTING" />
@@ -70,23 +90,23 @@
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
<component name="NextEditCompletionFeaturesState">
<decayedCancelled>
<entry key="MS100" value="1.6593257874423712" />
<entry key="MS500" value="4.690994084349008" />
<entry key="S2" value="14.465357308970995" />
<entry key="S5" value="30.992381303472097" />
<entry key="S10" value="48.5846958827385" />
<entry key="S30" value="71.76833898022443" />
<entry key="S60" value="80.15161397083796" />
<entry key="M2" value="84.87531446244894" />
<entry key="M5" value="87.90107930582121" />
<entry key="M10" value="89.36799351143095" />
<entry key="M15" value="93.16275126446527" />
<entry key="M30" value="133.51634266577747" />
<entry key="H1" value="278.73000832894314" />
<entry key="H2" value="556.4819305444627" />
<entry key="H4" value="958.972222388022" />
<entry key="D1" value="2009.2210985288775" />
<entry key="W1" value="2381.8392808733556" />
<entry key="MS100" value="1.42846046364743" />
<entry key="MS500" value="3.1473272504526837" />
<entry key="S2" value="4.406032316795342" />
<entry key="S5" value="7.013900503844766" />
<entry key="S10" value="16.21402635518147" />
<entry key="S30" value="38.90401123450886" />
<entry key="S60" value="52.53282462788902" />
<entry key="M2" value="76.41615227872627" />
<entry key="M5" value="125.46422729844767" />
<entry key="M10" value="158.76574582237612" />
<entry key="M15" value="176.22359481016986" />
<entry key="M30" value="217.72508392110834" />
<entry key="H1" value="328.4135925429288" />
<entry key="H2" value="583.9663964386095" />
<entry key="H4" value="1000.6154353482859" />
<entry key="D1" value="2163.2750640427894" />
<entry key="W1" value="2591.3632929927458" />
</decayedCancelled>
<decayedSelected>
<entry key="MS100" value="0.0" />
@@ -94,37 +114,37 @@
<entry key="S2" value="0.0" />
<entry key="S5" value="0.0" />
<entry key="S10" value="0.0" />
<entry key="S30" value="3.081765536046895E-110" />
<entry key="S60" value="1.7554958091795295E-55" />
<entry key="M2" value="4.189863731888548E-28" />
<entry key="M5" value="1.1191285094720122E-11" />
<entry key="M10" value="3.3453378147386345E-6" />
<entry key="M15" value="2.2367974199991652E-4" />
<entry key="M30" value="0.014955931470204738" />
<entry key="H1" value="0.12241465504527704" />
<entry key="H2" value="0.36869769063784513" />
<entry key="H4" value="0.8300520405421339" />
<entry key="D1" value="2.883634496098765" />
<entry key="W1" value="3.8121106294007556" />
<entry key="S30" value="4.679335776173224E-152" />
<entry key="S60" value="2.163177241044572E-76" />
<entry key="M2" value="1.4707743678227887E-38" />
<entry key="M5" value="7.362383250718034E-16" />
<entry key="M10" value="2.713371196633097E-8" />
<entry key="M15" value="9.029689298894987E-6" />
<entry key="M30" value="0.0030049451106002496" />
<entry key="H1" value="0.05487126235349662" />
<entry key="H2" value="0.24684605257568554" />
<entry key="H4" value="0.6791773688385119" />
<entry key="D1" value="2.788815988774459" />
<entry key="W1" value="3.7939461251078934" />
</decayedSelected>
<decayedShown>
<entry key="MS100" value="1.6056852423710548" />
<entry key="MS500" value="4.655592836166669" />
<entry key="S2" value="14.436328275205241" />
<entry key="S5" value="30.967614820111674" />
<entry key="S10" value="48.565252560398456" />
<entry key="S30" value="71.75871155917086" />
<entry key="S60" value="80.14622622688532" />
<entry key="M2" value="84.872458348968" />
<entry key="M5" value="87.89989521451953" />
<entry key="M10" value="89.36739411138522" />
<entry key="M15" value="93.16255011580246" />
<entry key="M30" value="133.530877068796" />
<entry key="H1" value="278.8516097409327" />
<entry key="H2" value="556.8495610944027" />
<entry key="H4" value="959.8012838755719" />
<entry key="D1" value="2012.1043621012425" />
<entry key="W1" value="2385.651327343784" />
<entry key="MS100" value="1.36580045961104" />
<entry key="MS500" value="3.120980742316745" />
<entry key="S2" value="4.3968676938883835" />
<entry key="S5" value="7.008354689811898" />
<entry key="S10" value="16.20802261033003" />
<entry key="S30" value="38.89936714660714" />
<entry key="S60" value="52.52966542050411" />
<entry key="M2" value="76.41373653537866" />
<entry key="M5" value="125.46254176247474" />
<entry key="M10" value="158.76465401645888" />
<entry key="M15" value="176.22278439428334" />
<entry key="M30" value="217.72754364072628" />
<entry key="H1" value="328.467843705019" />
<entry key="H2" value="584.2123911751798" />
<entry key="H4" value="1001.293731005342" />
<entry key="D1" value="2166.063508996286" />
<entry key="W1" value="2595.1571734947097" />
</decayedShown>
</component>
<component name="ProjectColorInfo">{
@@ -213,7 +233,7 @@
<workItem from="1783330590375" duration="18451000" />
<workItem from="1783371975450" duration="26000" />
<workItem from="1783372397891" duration="989000" />
<workItem from="1783410502976" duration="5821000" />
<workItem from="1783410502976" duration="10247000" />
</task>
<task id="LOCAL-00001" summary="implement main screen">
<option name="closed" value="true" />
@@ -335,7 +355,15 @@
<option name="project" value="LOCAL" />
<updated>1783417593908</updated>
</task>
<option name="localTasksCounter" value="16" />
<task id="LOCAL-00016" summary="implement scaling rewards per wave completed in combat mode">
<option name="closed" value="true" />
<created>1783421764662</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1783421764662</updated>
</task>
<option name="localTasksCounter" value="17" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -365,7 +393,8 @@
<MESSAGE value="finish implementing main" />
<MESSAGE value="fix some stuff" />
<MESSAGE value="Implement too many changes to list" />
<option name="LAST_COMMIT_MESSAGE" value="Implement too many changes to list" />
<MESSAGE value="implement scaling rewards per wave completed in combat mode" />
<option name="LAST_COMMIT_MESSAGE" value="implement scaling rewards per wave completed in combat mode" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
+203
View File
@@ -32,6 +32,10 @@
--kp-lang-go: #6de6ff;
--kp-lang-ruby: #ff5f7d;
--kp-lang-php: #9aa2ff;
--kp-primary: var(--kp-text);
--kp-scroll-track: var(--kp-panel);
--kp-scroll-thumb: var(--kp-text-dim);
--kp-scroll-thumb-hover: var(--kp-border-strong);
}
.combat-root {
@@ -186,6 +190,8 @@
.meter-fill {
height: 100%;
background-color: var(--kp-text);
transition-property: width, background-color;
transition-duration: 220ms;
}
.meter-fill-warning {
@@ -951,3 +957,200 @@
opacity: 0.72;
border-color: var(--kp-text-dim);
}
.feedback-layer {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.damage-vignette {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
opacity: 0;
border-left-width: 10px;
border-right-width: 10px;
border-top-width: 10px;
border-bottom-width: 10px;
border-color: var(--kp-danger);
transition-property: opacity;
transition-duration: 120ms;
}
.damage-vignette-on {
opacity: 0.62;
}
.float-number {
position: absolute;
min-width: 60px;
height: 24px;
-unity-text-align: middle-center;
-unity-font-style: bold;
font-size: 18px;
opacity: 1;
color: var(--kp-warning);
transition-property: top, opacity;
transition-duration: 420ms;
}
.float-damage {
color: var(--kp-warning);
}
.float-heal {
color: var(--kp-beneficial);
}
.float-status {
color: var(--kp-danger);
}
.float-muted {
color: var(--kp-text-dim);
}
.float-kill {
color: var(--kp-danger);
}
.float-large {
font-size: 23px;
}
.float-static {
transition-duration: 0ms;
}
.feedback-card-ghost {
opacity: 0.9;
transition-property: left, top, opacity;
transition-duration: 260ms;
}
.feedback-card-route {
border-color: var(--kp-accent);
}
.feedback-card-strike {
border-color: var(--kp-warning);
}
.feedback-card-discard {
border-color: var(--kp-text-dim);
}
.feedback-card-instant {
opacity: 0.72;
transition-duration: 0ms;
}
.feedback-hit {
background-color: var(--kp-action-hover);
border-color: var(--kp-warning);
scale: 1.025 0.98;
transition-property: scale, background-color, border-color;
transition-duration: 90ms;
}
.feedback-block {
border-color: var(--kp-beneficial);
transition-property: border-color;
transition-duration: 90ms;
}
.feedback-status {
border-color: var(--kp-danger);
}
.feedback-boost {
border-color: var(--kp-beneficial);
}
.feedback-killed {
opacity: 0.5;
scale: 0.95 0.95;
border-color: var(--kp-danger);
transition-property: opacity, scale, border-color;
transition-duration: 180ms;
}
.death-ghost {
background-color: var(--kp-panel-raised);
border-left-width: 2px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-danger);
opacity: 0.72;
transition-property: opacity, scale;
transition-duration: 300ms;
}
.enemy-acting {
border-color: var(--kp-accent);
scale: 1.035 1.035;
transition-property: scale, border-color;
transition-duration: 120ms;
}
.phase-pulse {
background-color: var(--kp-warning);
transition-property: background-color;
transition-duration: 120ms;
}
.cycles-spent-beat {
opacity: 0.62;
transition-property: opacity;
transition-duration: 120ms;
}
.feedback-denied {
border-color: var(--kp-danger);
translate: 4px 0;
transition-property: translate, border-color;
transition-duration: 70ms;
}
.hand-card-drawn {
border-color: var(--kp-beneficial);
translate: 0 -8px;
transition-property: translate, border-color;
transition-duration: 160ms;
}
.queue-chip-resolve {
border-color: var(--kp-warning);
scale: 1.06 1.06;
transition-property: scale, border-color;
transition-duration: 110ms;
}
.stack-chip-resolve {
border-color: var(--kp-lang-haskell);
scale: 1.08 1.08;
transition-property: scale, border-color;
transition-duration: 130ms;
}
.reduced-motion .float-number,
.reduced-motion .feedback-card-ghost,
.reduced-motion .damage-vignette,
.reduced-motion .feedback-hit,
.reduced-motion .feedback-killed,
.reduced-motion .enemy-acting,
.reduced-motion .phase-pulse,
.reduced-motion .cycles-spent-beat,
.reduced-motion .feedback-denied,
.reduced-motion .hand-card-drawn,
.reduced-motion .queue-chip-resolve,
.reduced-motion .stack-chip-resolve,
.reduced-motion .death-ghost {
transition-duration: 0ms;
}
+94
View File
@@ -0,0 +1,94 @@
:root {
--kp-scroll-track: var(--kp-panel);
--kp-scroll-thumb: var(--kp-border);
--kp-scroll-thumb-hover: var(--kp-primary);
}
ScrollView .unity-scroll-view__content-viewport {
padding-right: 2px;
padding-bottom: 2px;
}
ScrollView .unity-scroll-view__vertical-scroller {
width: 6px;
min-width: 6px;
background-color: transparent;
border-left-width: 0;
border-right-width: 0;
border-top-width: 0;
border-bottom-width: 0;
border-radius: 0;
}
ScrollView .unity-scroll-view__horizontal-scroller {
height: 6px;
min-height: 6px;
background-color: transparent;
border-left-width: 0;
border-right-width: 0;
border-top-width: 0;
border-bottom-width: 0;
border-radius: 0;
}
ScrollView .unity-scroller,
ScrollView .unity-scroller--vertical,
ScrollView .unity-scroller--horizontal,
ScrollView .unity-scroller__slider,
ScrollView .unity-base-slider {
background-color: transparent;
border-left-width: 0;
border-right-width: 0;
border-top-width: 0;
border-bottom-width: 0;
border-radius: 0;
}
ScrollView .unity-scroller__low-button,
ScrollView .unity-scroller__high-button {
display: none;
width: 0;
height: 0;
min-width: 0;
min-height: 0;
background-color: transparent;
border-left-width: 0;
border-right-width: 0;
border-top-width: 0;
border-bottom-width: 0;
}
ScrollView .unity-base-slider__tracker {
background-color: var(--kp-scroll-track);
opacity: 0.28;
border-left-width: 0;
border-right-width: 0;
border-top-width: 0;
border-bottom-width: 0;
border-radius: 0;
}
ScrollView .unity-base-slider__dragger,
ScrollView .unity-base-slider__dragger-border,
ScrollView #unity-dragger {
min-width: 6px;
min-height: 6px;
background-color: var(--kp-scroll-thumb);
border-left-width: 0;
border-right-width: 0;
border-top-width: 0;
border-bottom-width: 0;
border-radius: 0;
}
ScrollView .unity-base-slider__dragger:hover,
ScrollView .unity-base-slider__dragger-border:hover,
ScrollView #unity-dragger:hover {
background-color: var(--kp-scroll-thumb-hover);
}
ScrollView .unity-base-slider__dragger:active,
ScrollView .unity-base-slider__dragger-border:active,
ScrollView #unity-dragger:active {
background-color: var(--kp-scroll-thumb-hover);
}
+24 -11
View File
@@ -43,8 +43,8 @@ namespace KernelPanic.Combat
continue;
}
int minAmount = UpgradeMath.ScaleAmount(_minAmount, context.Card);
int maxAmount = UpgradeMath.ScaleAmount(_maxAmount, context.Card);
int minAmount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_minAmount, context), context.Card);
int maxAmount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_maxAmount, context), context.Card);
int amount = minAmount == maxAmount
? minAmount
: RandomRoll.RollRange(minAmount, maxAmount, new RollContext(context.Source));
@@ -77,7 +77,7 @@ namespace KernelPanic.Combat
public void Execute(CombatContext context)
{
int amount = UpgradeMath.ScaleShield(_amount, context.Card);
int amount = UpgradeMath.ScaleShield(UpgradeMath.ApplySourceFlatBonus(_amount, context), context.Card);
context.Source.Shield += amount;
context.CombatManager.ReportEffectResult($"gained {amount} shield");
}
@@ -107,7 +107,7 @@ namespace KernelPanic.Combat
}
int roll = RandomRoll.RollRange(1, 100, new RollContext(context.Source));
int damageAmount = UpgradeMath.ScaleAmount(_damageAmount, context.Card);
int damageAmount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_damageAmount, context), context.Card);
int amount = roll <= _successPercent ? damageAmount : 0;
context.DamagePipeline.DealDamage(new DamageRequest(context.Source, target, amount, _language, false, false));
context.CombatManager.ReportEffectResult(amount == 0 ? "typeof -> NaN (0)" : $"typeof -> {amount}");
@@ -136,13 +136,13 @@ namespace KernelPanic.Combat
{
if (_targetSelf)
{
context.StatusEffects.Apply(context.Source, _statusType, UpgradeMath.ScaleAmount(_stacks, context.Card), _duration, context.Source, _skipNextTick);
context.StatusEffects.Apply(context.Source, _statusType, UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_stacks, context), context.Card), _duration, context.Source, _skipNextTick);
return;
}
for (int i = 0; i < context.Targets.Count; i++)
{
context.StatusEffects.Apply(context.Targets[i], _statusType, UpgradeMath.ScaleAmount(_stacks, context.Card), _duration, context.Source, _skipNextTick);
context.StatusEffects.Apply(context.Targets[i], _statusType, UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_stacks, context), context.Card), _duration, context.Source, _skipNextTick);
}
}
}
@@ -184,7 +184,15 @@ namespace KernelPanic.Combat
}
// RAM is a hard hand cap. Draw effects only pull cards that can fit; blocked cards stay in the draw pile.
int requested = UpgradeMath.ScaleAmount(_count, context.Card);
int baseCount = _count;
if (context.Card?.Definition?.Id == "ubuntu_ask_ubuntu"
&& string.Equals(context.CombatManager.RunConfig?.Distro?.Id, "ubuntu", System.StringComparison.OrdinalIgnoreCase)
&& context.CombatManager.RunConfig.DistroVersion >= 3)
{
baseCount = 3;
}
int requested = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(baseCount, context), context.Card);
int drawCount = UnityEngine.Mathf.Min(requested, room);
IReadOnlyList<CardInstance> drawn = context.DeckController.Draw(drawCount);
int added = 0;
@@ -214,8 +222,8 @@ namespace KernelPanic.Combat
public void Execute(CombatContext context)
{
int minCount = UpgradeMath.ScaleAmount(_minCount, context.Card);
int maxCount = UpgradeMath.ScaleAmount(_maxCount, context.Card);
int minCount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_minCount, context), context.Card);
int maxCount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_maxCount, context), context.Card);
int targetCount = minCount == maxCount
? minCount
: RandomRoll.RollRange(minCount, maxCount, new RollContext(context.Source));
@@ -347,11 +355,11 @@ namespace KernelPanic.Combat
"mint_update_manager" => Todo("TODO: update manager needs delayed repeat system."),
"mint_cinnamon" => Todo("TODO: cinnamon needs shield effect support."),
"mint_nemo" => Todo("TODO: nemo needs conditional repeat based on shield."),
"mint_timeshift" => Todo("TODO: timeshift snapshot needs delayed restore system."),
"mint_timeshift" => Todo("TODO: timeshift snapshot V3 needs delayed restore system before threshold/restore can be upgraded."),
"fedora_borrow_checker" => Todo("TODO: borrow checker needs shield and first-card tracking."),
"fedora_cargo_build" => Todo("TODO: cargo build --release needs overkill-to-shield handling."),
"fedora_dnf_update" => Todo("TODO: dnf update needs Java played count and cost mutation."),
"fedora_rawhide" => Todo("TODO: rawhide needs next-card cost mutation and crash-risk hook."),
"fedora_rawhide" => Todo("TODO: rawhide V3 needs permanent damage-growth doubling after growth effects exist."),
"fedora_selinux" => Todo("TODO: SELinux enforcing needs enemy attack mitigation statuses."),
_ => Todo("TODO: card effect is not implemented yet.")
};
@@ -429,6 +437,7 @@ namespace KernelPanic.Combat
_ => string.IsNullOrWhiteSpace(card.Definition.Description) ? "TODO: effect not implemented." : $"{card.Definition.Description}{marker}"
};
}
}
public static class UpgradeMath
@@ -443,5 +452,9 @@ namespace KernelPanic.Combat
return baseAmount + UnityEngine.Mathf.Max(0, card?.MagnitudeBonus ?? 0);
}
public static int ApplySourceFlatBonus(int amount, CombatContext context)
{
return amount + UnityEngine.Mathf.Max(0, context?.Source?.FlatEffectBonus ?? 0);
}
}
}
+110
View File
@@ -1,6 +1,7 @@
using KernelPanic.Core;
using KernelPanic.Data;
using KernelPanic.Run;
using KernelPanic.Meta;
using UnityEngine;
using System;
using System.Collections.Generic;
@@ -33,6 +34,9 @@ namespace KernelPanic.Combat
private bool awaitingWaveContinue;
private bool runLost;
private bool skipNextAllocateDraw;
private bool ubuntuEmptyHandRefillUsed;
private int fedoraCardsDiscountedThisTurn;
private int fedoraCrashChance = 10;
public TurnPhase CurrentPhase => currentPhase;
public RunConfig RunConfig => runConfig;
@@ -141,7 +145,13 @@ namespace KernelPanic.Combat
return false;
}
bool fedoraBonus = CanApplyFedoraBonus();
int cost = GetCardCost(card);
if (fedoraBonus)
{
cost = Mathf.Max(0, cost - 1);
}
if (playerState.Cycles < cost)
{
Log($"not enough cycles for {GetCardName(card)}");
@@ -167,6 +177,29 @@ namespace KernelPanic.Combat
LogPlayIntent(card);
card.SetTargetSnapshot(CaptureTargets(card));
if (fedoraBonus)
{
fedoraCardsDiscountedThisTurn++;
if (RandomRoll.RollRange(1, 100, new RollContext(playerState)) <= fedoraCrashChance)
{
fedoraCrashChance = 10;
if (IsDistro("fedora") && runConfig.DistroVersion >= 4)
{
playerState.Cycles += 1;
}
deckController.Discard(card);
pendingTargetCard = null;
selectedEnemyIndex = -1;
Log($"{GetCardName(card)} crashed under bleeding edge");
StateChanged?.Invoke();
return true;
}
fedoraCrashChance = Mathf.Min(90, fedoraCrashChance + 10);
playerState.DamageMultiplierPercent = runConfig.DistroVersion >= 2 ? 175 : 150;
}
switch (track)
{
case ResolutionTrack.InterpreterQueue:
@@ -184,7 +217,10 @@ namespace KernelPanic.Combat
break;
}
playerState.DamageMultiplierPercent = 100;
pendingTargetCard = null;
TryApplyUbuntuEmptyHandRefill();
StateChanged?.Invoke();
return true;
}
@@ -355,8 +391,12 @@ namespace KernelPanic.Combat
runLost = false;
awaitingWaveContinue = false;
loggedEffectTodos.Clear();
ubuntuEmptyHandRefillUsed = false;
fedoraCrashChance = 10;
fedoraCardsDiscountedThisTurn = 0;
RandomRoll.Seed(runConfig.RunSeed);
playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles());
ApplyVersionState(playerState);
StartWave(preservePlayerUptime: false);
Log($"booted {runConfig.Distro.DisplayName} with {runManager.RunDeck.Count} cards");
StateChanged?.Invoke();
@@ -368,6 +408,7 @@ namespace KernelPanic.Combat
if (!preservePlayerUptime || playerState == null)
{
playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles());
ApplyVersionState(playerState);
}
else
{
@@ -379,6 +420,7 @@ namespace KernelPanic.Combat
playerState.Shield = 0;
playerState.IsDefeated = false;
playerState.MutableStatuses.Clear();
ApplyVersionState(playerState);
}
handController = new HandController(playerState.Ram);
@@ -413,12 +455,15 @@ namespace KernelPanic.Combat
}
playerState.Cycles = playerState.MaxCycles;
fedoraCardsDiscountedThisTurn = 0;
statusEffects.Tick(playerState, StatusTickTiming.StartOfTurn, playerState, damagePipeline);
if (CheckLoss())
{
return;
}
TryApplyUbuntuAptUpdate();
if (skipNextAllocateDraw)
{
skipNextAllocateDraw = false;
@@ -557,6 +602,71 @@ namespace KernelPanic.Combat
}
}
private void TryApplyUbuntuAptUpdate()
{
if (!IsDistro("ubuntu") || handController == null || playerState == null || handController.Cards.Count >= handController.RamCapacity)
{
return;
}
int lookCount = runConfig.DistroVersion >= 2 ? 3 : 2;
if (!deckController.TryDrawCheapestFromTop(lookCount, out CardInstance card))
{
return;
}
if (runConfig.DistroVersion >= 4)
{
card.TemporaryCostDelta -= 1;
}
if (handController.Add(card))
{
Log($"apt update: staged {GetCardName(card)} from top {lookCount}");
}
}
private void TryApplyUbuntuEmptyHandRefill()
{
if (!IsDistro("ubuntu") || runConfig.DistroVersion < 5 || ubuntuEmptyHandRefillUsed || handController == null || handController.Cards.Count > 0)
{
return;
}
ubuntuEmptyHandRefillUsed = true;
DrawCardsToHand(playerState.Ram, "ubuntu 24.04 refill");
}
private bool CanApplyFedoraBonus()
{
if (!IsDistro("fedora") || playerState == null)
{
return false;
}
int limit = runConfig.DistroVersion >= 5 ? 2 : 1;
return fedoraCardsDiscountedThisTurn < limit;
}
private void ApplyVersionState(CombatantState state)
{
if (state == null)
{
return;
}
state.forceMaxRolls = IsDistro("mint");
state.IgnoreDamageMultipliers = IsDistro("mint");
state.AllowFlatDamageBuffs = IsDistro("mint") && runConfig.DistroVersion >= 4;
state.FlatEffectBonus = IsDistro("mint") && runConfig.DistroVersion >= 2 ? 2 : 0;
state.DamageMultiplierPercent = 100;
}
private bool IsDistro(string id)
{
return string.Equals(runConfig?.Distro?.Id, id, StringComparison.OrdinalIgnoreCase);
}
private CombatContext BuildContext(CardInstance card, IReadOnlyList<CombatantState> targets)
{
return new CombatContext(card, playerState, targets, this, damagePipeline, statusEffects, deckController, handController, enemies);
+4
View File
@@ -27,6 +27,10 @@ namespace KernelPanic.Combat
public int MaxCycles { get; set; }
public bool IsDefeated { get; set; }
public bool forceMaxRolls { get; set; }
public bool IgnoreDamageMultipliers { get; set; }
public bool AllowFlatDamageBuffs { get; set; }
public int FlatEffectBonus { get; set; }
public int DamageMultiplierPercent { get; set; } = 100;
public IReadOnlyList<StatusInstance> Statuses => _statuses;
public IReadOnlyDictionary<StatusType, int> ActiveStatuses => BuildStatusSnapshot();
+15 -1
View File
@@ -74,18 +74,32 @@ namespace KernelPanic.Combat
private static int ApplyMultipliers(int amount, DamageRequest request)
{
// TODO: Apply buffs/debuffs and Mint's no-multiplier rule here.
if (request.Source != null && request.Source.IgnoreDamageMultipliers)
{
// Mint V1+ ignores multiplicative damage modifiers. Mint V4 still allows flat
// bonuses before this step; no flat buff status exists yet, so the flag is a hook.
return amount;
}
int multiplier = request.Source == null ? 100 : Mathf.Max(0, request.Source.DamageMultiplierPercent);
return Mathf.RoundToInt(amount * (multiplier / 100f));
}
private static int ApplyCrit(int amount, DamageRequest request)
{
if (request.Source != null && request.Source.forceMaxRolls)
{
return amount;
}
// TODO: Add crit calculation and crit modifiers.
return amount;
}
private static 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;
}
+37
View File
@@ -55,6 +55,43 @@ namespace KernelPanic.Combat
return drawn;
}
public bool TryDrawCheapestFromTop(int lookCount, out CardInstance card)
{
card = null;
if (lookCount <= 0)
{
return false;
}
if (_drawPile.Count == 0)
{
ReshuffleDiscardIntoDraw();
}
if (_drawPile.Count == 0)
{
return false;
}
int inspectCount = System.Math.Min(lookCount, _drawPile.Count);
int selectedIndex = _drawPile.Count - 1;
int selectedCost = CombatManager.GetCardCost(_drawPile[selectedIndex]);
for (int offset = 1; offset < inspectCount; offset++)
{
int index = _drawPile.Count - 1 - offset;
int cost = CombatManager.GetCardCost(_drawPile[index]);
if (cost < selectedCost)
{
selectedCost = cost;
selectedIndex = index;
}
}
card = _drawPile[selectedIndex];
_drawPile.RemoveAt(selectedIndex);
return card != null;
}
public void Discard(CardInstance card)
{
if (card != null)
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
namespace KernelPanic.Meta
{
public static class DistroVersionCatalog
{
private static readonly Dictionary<string, string[]> ReleaseLabels = new(StringComparer.OrdinalIgnoreCase)
{
["ubuntu"] = new[] { "20.04", "21.04", "22.04", "23.04", "24.04" },
["fedora"] = new[] { "38", "39", "40", "41", "42" },
["mint"] = new[] { "21", "21.2", "21.3", "22", "22.1" }
};
public static string GetReleaseLabel(string unitId, int version)
{
int safeVersion = Math.Max(1, Math.Min(GachaTuning.MaxVersion, version));
if (!string.IsNullOrWhiteSpace(unitId) && ReleaseLabels.TryGetValue(unitId, out string[] labels) && safeVersion <= labels.Length)
{
return labels[safeVersion - 1];
}
return $"V{safeVersion}";
}
public static string GetEffectSummary(string unitId, int version)
{
int safeVersion = Math.Max(1, Math.Min(GachaTuning.MaxVersion, version));
return (unitId ?? string.Empty).ToLowerInvariant() switch
{
"ubuntu" => safeVersion switch
{
1 => "start of turn: apt update chooses the cheaper of top 2 draw cards",
2 => "apt update looks at top 3 instead of top 2",
3 => "ask ubuntu draws 3 instead of 2",
4 => "apt update card costs 1 less this turn",
5 => "once per combat, empty hand refills up to RAM",
_ => "--"
},
"fedora" => safeVersion switch
{
1 => "first card each turn costs 1 less and deals +50%, with crash risk",
2 => "first-card damage bonus rises to +75%",
3 => "rawhide TODO: double permanent damage growth on non-crash first-card turn",
4 => "crashes grant +1 Cycle that turn",
5 => "first two cards each turn get the discount, bonus, and independent crash rolls",
_ => "--"
},
"mint" => safeVersion switch
{
1 => "random values roll max; no crits; ignores multiplicative damage buffs",
2 => "fixed effect values are treated as +2",
3 => "timeshift snapshot TODO: stronger restore when delayed restore exists",
4 => "flat +damage can apply; multiplicative crit/mult remains ignored",
5 => "TODO: re-resolve reduced effect when enemy reduction/coercion exists",
_ => "--"
},
_ => safeVersion == 1 ? "base release" : $"TODO: V{safeVersion} release effect"
};
}
}
}
+26
View File
@@ -0,0 +1,26 @@
namespace KernelPanic.Meta
{
/// <summary>
/// Tunable gacha progression amounts. Kept in code until designers need asset-driven economy tuning.
/// </summary>
public static class GachaTuning
{
public const int MaxVersion = 5;
public const int MergesPerDupe = 20;
public const int MergesOnCharacterBonus = 10;
public const float MergesMaxVersionOverflowMultiplier = 1.5f;
public static int DupeConsolationBandwidth => 0;
public static readonly int[] VersionCosts = { 50, 120, 250, 500 };
public static int GetVersionUpgradeCost(int targetVersion)
{
if (targetVersion <= 1 || targetVersion > MaxVersion)
{
return 0;
}
int index = targetVersion - 2;
return index >= 0 && index < VersionCosts.Length ? VersionCosts[index] : VersionCosts[VersionCosts.Length - 1];
}
}
}
+71
View File
@@ -10,6 +10,7 @@ namespace KernelPanic.Meta
public sealed class PlayerCollection
{
private readonly List<DistroDefinition> _ownedUnits = new();
private readonly Dictionary<string, int> _versions = new(StringComparer.OrdinalIgnoreCase);
private int _featuredIndex;
// TODO: Populate owned units from SaveService during bootstrap.
@@ -19,17 +20,47 @@ namespace KernelPanic.Meta
public event Action Changed;
public void Add(DistroDefinition unit)
{
Add(unit, 1);
}
public void Add(DistroDefinition unit, int version)
{
Add(unit, version, true);
}
public void AddSilently(DistroDefinition unit, int version)
{
Add(unit, version, false);
}
private void Add(DistroDefinition unit, int version, bool notify)
{
if (unit == null)
{
return;
}
if (IsOwned(unit.Id))
{
SetVersion(unit.Id, version, notify);
return;
}
// Persistence is wired by the owner so this domain class stays IO-free.
_ownedUnits.Add(unit);
_versions[unit.Id] = Math.Max(1, version);
_featuredIndex = Math.Clamp(_featuredIndex, 0, _ownedUnits.Count - 1);
if (notify)
{
Changed?.Invoke();
}
}
public bool IsOwned(string unitId)
{
return GetOwnedCount(unitId) > 0;
}
public int GetOwnedCount(string unitId)
{
@@ -51,6 +82,46 @@ namespace KernelPanic.Meta
return count;
}
public int GetVersion(string unitId)
{
if (string.IsNullOrWhiteSpace(unitId))
{
return 0;
}
return _versions.TryGetValue(unitId, out int version) ? Math.Max(1, version) : IsOwned(unitId) ? 1 : 0;
}
public void SetVersion(string unitId, int version)
{
SetVersion(unitId, version, true);
}
public void SetVersionSilently(string unitId, int version)
{
SetVersion(unitId, version, false);
}
private void SetVersion(string unitId, int version, bool notify)
{
if (string.IsNullOrWhiteSpace(unitId) || !IsOwned(unitId))
{
return;
}
int safeVersion = Math.Max(1, version);
if (_versions.TryGetValue(unitId, out int oldVersion) && oldVersion == safeVersion)
{
return;
}
_versions[unitId] = safeVersion;
if (notify)
{
Changed?.Invoke();
}
}
public void SelectNextFeatured()
{
if (_ownedUnits.Count < 2)
+225
View File
@@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using KernelPanic.Core;
using KernelPanic.Data;
using UnityEngine;
namespace KernelPanic.Meta
{
public static class PullResolver
{
public static event Action<PullResolutionResult> PullResolved;
public static PullResolutionResult Resolve(IEnumerable<DistroDefinition> pullResults, PullResolutionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.SaveData ??= SaveData.CreateDefault();
context.SaveData.EnsureLists();
List<PullResolutionOutcome> outcomes = new();
if (pullResults != null)
{
foreach (DistroDefinition unit in pullResults)
{
outcomes.Add(ResolveOne(unit, context));
}
}
PullResolutionResult result = new(outcomes);
PullResolved?.Invoke(result);
return result;
}
public static PullResolutionResult Resolve(IEnumerable<string> pullResultIds, PullResolutionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
List<DistroDefinition> units = new();
if (pullResultIds != null)
{
foreach (string id in pullResultIds)
{
units.Add(context.DistroDatabase == null ? null : context.DistroDatabase.FindById(id));
}
}
return Resolve(units, context);
}
public static PullResolutionResult ResolveAndSave(IEnumerable<DistroDefinition> pullResults, PullResolutionContext context, SaveService saveService)
{
PullResolutionResult result = Resolve(pullResults, context);
saveService?.Save(context.SaveData);
return result;
}
public static PullResolutionResult ResolveAndSave(IEnumerable<string> pullResultIds, PullResolutionContext context, SaveService saveService)
{
PullResolutionResult result = Resolve(pullResultIds, context);
saveService?.Save(context.SaveData);
return result;
}
private static PullResolutionOutcome ResolveOne(DistroDefinition unit, PullResolutionContext context)
{
if (unit == null || string.IsNullOrWhiteSpace(unit.Id))
{
return new PullResolutionOutcome(null, PullOutcomeKind.Invalid, 0, Array.Empty<Language>());
}
OwnedUnitSaveEntry owned = context.SaveData.FindOwnedUnit(unit.Id);
bool ownedInCollection = context.Collection != null && context.Collection.IsOwned(unit.Id);
if (owned == null && !ownedInCollection)
{
IReadOnlyList<Language> before = SnapshotUnlockedLanguages(context.Collection);
context.SaveData.AddOwnedUnit(unit.Id, 1);
context.Collection?.AddSilently(unit, 1);
IReadOnlyList<Language> unlocked = NewlyUnlockedLanguages(before, context.Collection, unit);
return new PullResolutionOutcome(unit.Id, PullOutcomeKind.Granted, 0, unlocked);
}
owned ??= context.SaveData.AddOwnedUnit(unit.Id, context.Collection?.GetVersion(unit.Id) ?? 1);
int version = Mathf.Clamp(owned.version, 1, GachaTuning.MaxVersion);
owned.version = version;
bool overflow = version >= GachaTuning.MaxVersion;
int merges = CalculateDupeMerges(overflow, unit.Id, context.FocusUnitId);
context.SaveData.merges = Math.Max(0, context.SaveData.merges) + merges;
if (GachaTuning.DupeConsolationBandwidth > 0)
{
context.SaveData.standardPullCurrency += GachaTuning.DupeConsolationBandwidth;
}
context.Collection?.AddSilently(unit, version);
return new PullResolutionOutcome(unit.Id, overflow ? PullOutcomeKind.DupeOverflow : PullOutcomeKind.Dupe, merges, Array.Empty<Language>());
}
private static int CalculateDupeMerges(bool overflow, string unitId, string focusUnitId)
{
float baseAmount = GachaTuning.MergesPerDupe;
if (overflow)
{
baseAmount *= GachaTuning.MergesMaxVersionOverflowMultiplier;
}
int merges = Mathf.RoundToInt(baseAmount);
if (!string.IsNullOrWhiteSpace(focusUnitId) && string.Equals(unitId, focusUnitId, StringComparison.OrdinalIgnoreCase))
{
merges += GachaTuning.MergesOnCharacterBonus;
}
return Math.Max(1, merges);
}
private static IReadOnlyList<Language> SnapshotUnlockedLanguages(PlayerCollection collection)
{
List<Language> unlocked = new();
if (collection == null)
{
return unlocked;
}
IReadOnlyList<LanguageCatalogEntry> languages = LanguageCatalog.All;
for (int i = 0; i < languages.Count; i++)
{
if (LanguageUnlock.IsUnlocked(languages[i].Language, collection))
{
unlocked.Add(languages[i].Language);
}
}
return unlocked;
}
private static IReadOnlyList<Language> NewlyUnlockedLanguages(IReadOnlyList<Language> before, PlayerCollection collection, DistroDefinition unit)
{
List<Language> unlocked = new();
if (collection == null || unit == null)
{
return unlocked;
}
AddNewlyUnlockedLanguage(unit.PrimaryLanguage, before, collection, unlocked);
AddNewlyUnlockedLanguage(unit.SecondaryLanguage, before, collection, unlocked);
return unlocked;
}
private static void AddNewlyUnlockedLanguage(Language language, IReadOnlyList<Language> before, PlayerCollection collection, List<Language> unlocked)
{
if (!ContainsLanguage(unlocked, language) && !ContainsLanguage(before, language) && LanguageUnlock.IsUnlocked(language, collection))
{
unlocked.Add(language);
}
}
private static bool ContainsLanguage(IReadOnlyList<Language> languages, Language language)
{
for (int i = 0; i < languages.Count; i++)
{
if (languages[i] == language)
{
return true;
}
}
return false;
}
}
public sealed class PullResolutionContext
{
public PullResolutionContext(SaveData saveData, PlayerCollection collection, DistroDatabase distroDatabase = null, string focusUnitId = null)
{
SaveData = saveData;
Collection = collection;
DistroDatabase = distroDatabase;
FocusUnitId = focusUnitId;
}
public SaveData SaveData { get; set; }
public PlayerCollection Collection { get; }
public DistroDatabase DistroDatabase { get; }
public string FocusUnitId { get; }
}
public sealed class PullResolutionResult
{
public PullResolutionResult(IReadOnlyList<PullResolutionOutcome> outcomes)
{
Outcomes = outcomes ?? Array.Empty<PullResolutionOutcome>();
}
public IReadOnlyList<PullResolutionOutcome> Outcomes { get; }
}
public readonly struct PullResolutionOutcome
{
public PullResolutionOutcome(string unitId, PullOutcomeKind kind, int mergesAwarded, IReadOnlyList<Language> languagesNewlyUnlocked)
{
UnitId = unitId;
Kind = kind;
MergesAwarded = mergesAwarded;
LanguagesNewlyUnlocked = languagesNewlyUnlocked ?? Array.Empty<Language>();
}
public string UnitId { get; }
public PullOutcomeKind Kind { get; }
public int MergesAwarded { get; }
public IReadOnlyList<Language> LanguagesNewlyUnlocked { get; }
}
public enum PullOutcomeKind
{
Invalid,
Granted,
Dupe,
DupeOverflow
}
}
+110
View File
@@ -62,6 +62,8 @@ namespace KernelPanic.Meta
public int rootCredits = GachaService.TestRootCreditsBalance;
public int standardPullCurrency;
public int limitedPullCurrency;
public int merges;
public List<OwnedUnitSaveEntry> ownedUnits = new();
public List<string> ownedUnitIds = new();
public List<string> bannerPoolIds = new();
public GachaBannerState beginnerBannerState = new(GachaService.BeginnerBannerId);
@@ -74,6 +76,7 @@ namespace KernelPanic.Meta
public void EnsureLists()
{
ownedUnits ??= new List<OwnedUnitSaveEntry>();
ownedUnitIds ??= new List<string>();
bannerPoolIds ??= new List<string>();
beginnerBannerState ??= new GachaBannerState(GachaService.BeginnerBannerId);
@@ -81,7 +84,114 @@ namespace KernelPanic.Meta
beginnerBannerState.EnsureLists();
lastRunLoadout ??= new LastRunLoadoutSaveEntry();
lastRunLoadout.EnsureLists();
merges = Math.Max(0, merges);
MigrateLegacyOwnedUnitIds();
NormalizeOwnedUnits();
}
public OwnedUnitSaveEntry FindOwnedUnit(string id)
{
if (string.IsNullOrWhiteSpace(id))
{
return null;
}
EnsureLists();
for (int i = 0; i < ownedUnits.Count; i++)
{
OwnedUnitSaveEntry entry = ownedUnits[i];
if (entry != null && string.Equals(entry.id, id, StringComparison.OrdinalIgnoreCase))
{
return entry;
}
}
return null;
}
public bool IsUnitOwned(string id)
{
return FindOwnedUnit(id) != null;
}
public OwnedUnitSaveEntry AddOwnedUnit(string id, int version)
{
if (string.IsNullOrWhiteSpace(id))
{
return null;
}
EnsureLists();
OwnedUnitSaveEntry existing = FindOwnedUnit(id);
if (existing != null)
{
existing.version = Math.Max(1, Math.Min(GachaTuning.MaxVersion, existing.version));
return existing;
}
OwnedUnitSaveEntry entry = new()
{
id = id,
version = Math.Max(1, Math.Min(GachaTuning.MaxVersion, version))
};
ownedUnits.Add(entry);
return entry;
}
private void MigrateLegacyOwnedUnitIds()
{
for (int i = 0; i < ownedUnitIds.Count; i++)
{
string id = ownedUnitIds[i];
if (string.IsNullOrWhiteSpace(id) || HasOwnedUnitEntry(id))
{
continue;
}
ownedUnits.Add(new OwnedUnitSaveEntry
{
id = id,
version = 1
});
}
}
private void NormalizeOwnedUnits()
{
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
for (int i = ownedUnits.Count - 1; i >= 0; i--)
{
OwnedUnitSaveEntry entry = ownedUnits[i];
if (entry == null || string.IsNullOrWhiteSpace(entry.id) || !seen.Add(entry.id))
{
ownedUnits.RemoveAt(i);
continue;
}
entry.version = Math.Max(1, Math.Min(GachaTuning.MaxVersion, entry.version));
}
}
private bool HasOwnedUnitEntry(string id)
{
for (int i = 0; i < ownedUnits.Count; i++)
{
OwnedUnitSaveEntry entry = ownedUnits[i];
if (entry != null && string.Equals(entry.id, id, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}
[Serializable]
public sealed class OwnedUnitSaveEntry
{
public string id;
public int version = 1;
}
[Serializable]
+105
View File
@@ -0,0 +1,105 @@
using System;
using UnityEngine;
namespace KernelPanic.Meta
{
public static class VersionUpgrader
{
public static event Action<VersionUpgradeResult> UnitUpgraded;
public static bool TryUpgrade(string unitId, SaveData state, PlayerCollection collection, out VersionUpgradeResult result)
{
result = TryUpgrade(unitId, state, collection);
return result.Success;
}
public static VersionUpgradeResult TryUpgradeAndSave(string unitId, SaveData state, PlayerCollection collection, SaveService saveService)
{
VersionUpgradeResult result = TryUpgrade(unitId, state, collection);
if (result.Success)
{
saveService?.Save(state);
}
return result;
}
public static VersionUpgradeResult TryUpgrade(string unitId, SaveData state, PlayerCollection collection)
{
state ??= SaveData.CreateDefault();
state.EnsureLists();
OwnedUnitSaveEntry entry = state.FindOwnedUnit(unitId);
if (entry == null && collection != null && collection.IsOwned(unitId))
{
entry = state.AddOwnedUnit(unitId, collection.GetVersion(unitId));
}
if (entry == null)
{
return VersionUpgradeResult.Failed(unitId, VersionUpgradeFailureReason.NotOwned);
}
int currentVersion = Mathf.Clamp(entry.version, 1, GachaTuning.MaxVersion);
if (currentVersion >= GachaTuning.MaxVersion)
{
return VersionUpgradeResult.Failed(unitId, VersionUpgradeFailureReason.MaxVersion, currentVersion, currentVersion, 0);
}
int targetVersion = currentVersion + 1;
int cost = GachaTuning.GetVersionUpgradeCost(targetVersion);
if (state.merges < cost)
{
return VersionUpgradeResult.Failed(unitId, VersionUpgradeFailureReason.InsufficientMerges, currentVersion, targetVersion, cost);
}
state.merges -= cost;
entry.version = targetVersion;
collection?.SetVersionSilently(unitId, targetVersion);
VersionUpgradeResult result = VersionUpgradeResult.Upgraded(unitId, currentVersion, targetVersion, cost, state.merges);
UnitUpgraded?.Invoke(result);
return result;
}
}
public readonly struct VersionUpgradeResult
{
private VersionUpgradeResult(string unitId, bool success, VersionUpgradeFailureReason failureReason, int fromVersion, int toVersion, int cost, int mergesRemaining)
{
UnitId = unitId;
Success = success;
FailureReason = failureReason;
FromVersion = fromVersion;
ToVersion = toVersion;
Cost = cost;
MergesRemaining = mergesRemaining;
}
public string UnitId { get; }
public bool Success { get; }
public VersionUpgradeFailureReason FailureReason { get; }
public int FromVersion { get; }
public int ToVersion { get; }
public int Cost { get; }
public int MergesRemaining { get; }
public static VersionUpgradeResult Upgraded(string unitId, int fromVersion, int toVersion, int cost, int mergesRemaining)
{
return new VersionUpgradeResult(unitId, true, VersionUpgradeFailureReason.None, fromVersion, toVersion, cost, mergesRemaining);
}
public static VersionUpgradeResult Failed(string unitId, VersionUpgradeFailureReason reason, int fromVersion = 0, int toVersion = 0, int cost = 0)
{
return new VersionUpgradeResult(unitId, false, reason, fromVersion, toVersion, cost, 0);
}
}
public enum VersionUpgradeFailureReason
{
None,
NotOwned,
MaxVersion,
InsufficientMerges
}
}
+8 -1
View File
@@ -10,17 +10,23 @@ namespace KernelPanic.Run
public sealed class RunConfig
{
public RunConfig(DistroDefinition distro, Language primaryLanguage, Language secondaryLanguage, IReadOnlyList<CardDefinition> startingDeck)
: this(distro, primaryLanguage, secondaryLanguage, startingDeck, 0)
: this(distro, primaryLanguage, secondaryLanguage, startingDeck, 0, 1)
{
}
public RunConfig(DistroDefinition distro, Language primaryLanguage, Language secondaryLanguage, IReadOnlyList<CardDefinition> startingDeck, int runSeed)
: this(distro, primaryLanguage, secondaryLanguage, startingDeck, runSeed, 1)
{
}
public RunConfig(DistroDefinition distro, Language primaryLanguage, Language secondaryLanguage, IReadOnlyList<CardDefinition> startingDeck, int runSeed, int distroVersion)
{
Distro = distro;
PrimaryLanguage = primaryLanguage;
SecondaryLanguage = secondaryLanguage;
StartingDeck = startingDeck;
RunSeed = runSeed;
DistroVersion = UnityEngine.Mathf.Clamp(distroVersion, 1, KernelPanic.Meta.GachaTuning.MaxVersion);
}
public DistroDefinition Distro { get; }
@@ -28,5 +34,6 @@ namespace KernelPanic.Run
public Language SecondaryLanguage { get; }
public IReadOnlyList<CardDefinition> StartingDeck { get; }
public int RunSeed { get; }
public int DistroVersion { get; }
}
}
+10 -3
View File
@@ -18,7 +18,12 @@ namespace KernelPanic.Run
public static void Set(DistroDefinition distro, IReadOnlyList<CardDefinition> equippedCards, Language primaryLanguage, Language secondaryLanguage)
{
pendingRun = new PendingRun(distro, equippedCards, primaryLanguage, secondaryLanguage, Environment.TickCount);
Set(distro, equippedCards, primaryLanguage, secondaryLanguage, 1);
}
public static void Set(DistroDefinition distro, IReadOnlyList<CardDefinition> equippedCards, Language primaryLanguage, Language secondaryLanguage, int distroVersion)
{
pendingRun = new PendingRun(distro, equippedCards, primaryLanguage, secondaryLanguage, Environment.TickCount, distroVersion);
}
public static bool TryCreateRunConfig(LanguageDeckDatabase languageDeckDatabase, out RunConfig config)
@@ -41,13 +46,15 @@ namespace KernelPanic.Run
private readonly Language primaryLanguage;
private readonly Language secondaryLanguage;
private readonly int runSeed;
private readonly int distroVersion;
public PendingRun(DistroDefinition distro, IReadOnlyList<CardDefinition> cards, Language primaryLanguage, Language secondaryLanguage, int runSeed)
public PendingRun(DistroDefinition distro, IReadOnlyList<CardDefinition> cards, Language primaryLanguage, Language secondaryLanguage, int runSeed, int distroVersion)
{
this.distro = distro;
this.primaryLanguage = primaryLanguage;
this.secondaryLanguage = secondaryLanguage;
this.runSeed = runSeed;
this.distroVersion = distroVersion;
if (cards == null)
{
@@ -70,7 +77,7 @@ namespace KernelPanic.Run
List<CardDefinition> startingDeck = new(equippedCards);
AddLanguageDeck(languageDeckDatabase, primaryLanguage, startingDeck);
AddLanguageDeck(languageDeckDatabase, secondaryLanguage, startingDeck);
return new RunConfig(distro, primaryLanguage, secondaryLanguage, startingDeck, runSeed);
return new RunConfig(distro, primaryLanguage, secondaryLanguage, startingDeck, runSeed, distroVersion);
}
private static void AddLanguageDeck(LanguageDeckDatabase languageDeckDatabase, Language language, List<CardDefinition> target)
+115 -4
View File
@@ -18,11 +18,14 @@ namespace KernelPanic.UI
private readonly List<VisualElement> _rows = new();
private VisualElement _list;
private ScrollView _detailScroll;
private VisualElement _detail;
private FontAsset _monospaceFont;
private LanguageDeckDatabase _languageDeckDatabase;
private CardDatabase _cardDatabase;
private PlayerCollection _playerCollection;
private Func<int> _getMergesBalance;
private Func<DistroDefinition, VersionUpgradeResult> _tryUpgradeUnit;
private IReadOnlyList<DistroDefinition> _units = Array.Empty<DistroDefinition>();
private IReadOnlyList<LanguageCatalogEntry> _languages = LanguageCatalog.All;
private IReadOnlyList<CardEntry> _cards = Array.Empty<CardEntry>();
@@ -35,14 +38,17 @@ namespace KernelPanic.UI
public event Action ViewChanged;
public void Bind(VisualElement root, FontAsset artFont, LanguageDeckDatabase deckDatabase, CardDatabase cardsDatabase, PlayerCollection collection)
public void Bind(VisualElement root, FontAsset artFont, LanguageDeckDatabase deckDatabase, CardDatabase cardsDatabase, PlayerCollection collection, Func<int> getMergesBalance = null, Func<DistroDefinition, VersionUpgradeResult> tryUpgradeUnit = null)
{
_monospaceFont = artFont;
_languageDeckDatabase = deckDatabase;
_cardDatabase = cardsDatabase;
_playerCollection = collection;
_getMergesBalance = getMergesBalance;
_tryUpgradeUnit = tryUpgradeUnit;
_list = root.Q<VisualElement>("CollectionList");
_detail = root.Q<VisualElement>("CollectionDetail");
_detailScroll = root.Q<ScrollView>("CollectionDetail");
_detail = _detailScroll?.contentContainer;
}
public bool IsCardSubview => _mode == CollectionMode.CardSubview;
@@ -282,7 +288,79 @@ namespace KernelPanic.UI
readout.Add(details);
_detail.Add(readout);
_detail.Add(BuildSubviewCommand($"> cat ~/units/{unit.Id}/cards", "exclusive packages", HasListableCards(unit), () => OpenUnitCards(unit), "no packages installed"));
AddVersionUpgradePanel(_detail, unit);
_detail.Add(BuildSubviewCommand($"cat ~/units/{unit.Id}/cards", "exclusive packages", HasListableCards(unit), () => OpenUnitCards(unit), "no packages installed"));
}
private void AddVersionUpgradePanel(VisualElement target, DistroDefinition unit)
{
int version = _playerCollection == null ? 1 : Mathf.Clamp(_playerCollection.GetVersion(unit.Id), 1, GachaTuning.MaxVersion);
string release = DistroVersionCatalog.GetReleaseLabel(unit.Id, version);
int merges = _getMergesBalance?.Invoke() ?? 0;
VisualElement panel = new();
panel.AddToClassList("version-upgrade-panel");
VisualElement summary = new();
summary.AddToClassList("version-upgrade-summary");
Label current = new($"{DistroPresentation.DisplayName(unit)} {release} v{version}/{GachaTuning.MaxVersion}");
current.AddToClassList("version-upgrade-current");
summary.Add(current);
Label wallet = new(version < GachaTuning.MaxVersion ? $"merges {merges} / {GachaTuning.GetVersionUpgradeCost(version + 1)}" : $"merges {merges}");
wallet.AddToClassList("version-upgrade-wallet");
summary.Add(wallet);
panel.Add(summary);
if (version < GachaTuning.MaxVersion)
{
int targetVersion = version + 1;
int cost = GachaTuning.GetVersionUpgradeCost(targetVersion);
Label next = new($"next {DistroVersionCatalog.GetReleaseLabel(unit.Id, targetVersion)} - {DistroVersionCatalog.GetEffectSummary(unit.Id, targetVersion)}");
next.AddToClassList("version-upgrade-next");
panel.Add(next);
bool affordable = merges >= cost;
wallet.EnableInClassList(affordable ? "version-upgrade-wallet-affordable" : "version-upgrade-wallet-warning", true);
panel.Add(BuildCompactUpgradeCommand($"> apt full-upgrade ({cost} merges)", affordable ? "ready" : "insufficient merges", affordable && _tryUpgradeUnit != null, () => UpgradeUnit(unit)));
}
else
{
Label next = new("latest release installed");
next.AddToClassList("version-upgrade-next");
panel.Add(next);
panel.Add(BuildCompactUpgradeCommand("> apt full-upgrade", "latest release", false, null));
}
VisualElement path = new();
path.AddToClassList("version-path");
for (int i = 1; i <= GachaTuning.MaxVersion; i++)
{
string className = i <= version ? "version-path-owned" : i == version + 1 ? "version-path-next" : "version-path-locked";
Label step = new($"v{i} {DistroVersionCatalog.GetReleaseLabel(unit.Id, i)}");
step.AddToClassList("version-path-step");
step.AddToClassList(className);
path.Add(step);
}
panel.Add(path);
target.Add(panel);
}
private void UpgradeUnit(DistroDefinition unit)
{
if (unit == null || _tryUpgradeUnit == null)
{
return;
}
VersionUpgradeResult result = _tryUpgradeUnit(unit);
RenderUnitDetail(unit);
if (!result.Success)
{
_detail.Add(BuildDetailLine("upgrade", FormatUpgradeFailure(result.FailureReason)));
}
}
private void RenderLanguageDetail(LanguageCatalogEntry language)
@@ -310,7 +388,7 @@ namespace KernelPanic.UI
return;
}
_detail.Add(BuildSubviewCommand($"> cat ~/lang/{GetLanguageId(language.Language)}/deck", "starter deck", true, () => OpenLanguageDeck(language), null));
_detail.Add(BuildSubviewCommand($"cat ~/lang/{GetLanguageId(language.Language)}/deck", "starter deck", true, () => OpenLanguageDeck(language), null));
}
private void OpenUnitCards(DistroDefinition unit)
@@ -463,6 +541,28 @@ namespace KernelPanic.UI
return row;
}
private static VisualElement BuildCompactUpgradeCommand(string command, string state, bool enabled, Action action)
{
VisualElement row = new();
row.AddToClassList("version-upgrade-command");
row.EnableInClassList("disabled-row", !enabled);
Label commandLabel = new(command);
commandLabel.AddToClassList("version-upgrade-command-text");
Label stateLabel = new(state);
stateLabel.AddToClassList("version-upgrade-command-state");
row.Add(commandLabel);
row.Add(stateLabel);
if (enabled)
{
row.RegisterCallback<ClickEvent>(_ => action?.Invoke());
}
return row;
}
private static void AddPassiveDetails(VisualElement target, DistroDefinition unit)
{
if (unit.Passive == null)
@@ -600,6 +700,17 @@ namespace KernelPanic.UI
return card == null ? "--" : $"{card.Language} / {card.CycleCost}c";
}
private static string FormatUpgradeFailure(VersionUpgradeFailureReason reason)
{
return reason switch
{
VersionUpgradeFailureReason.NotOwned => "not owned",
VersionUpgradeFailureReason.MaxVersion => "latest release",
VersionUpgradeFailureReason.InsufficientMerges => "insufficient merges",
_ => "upgrade failed"
};
}
private static string GetLanguageId(Language language)
{
return language switch
+417 -4
View File
@@ -20,6 +20,7 @@ namespace KernelPanic.UI
public sealed class CombatSceneController : MonoBehaviour
{
private const string StyleResourcePath = "CombatScene";
private const string SharedScrollbarStyleResourcePath = "TerminalScrollbars";
private const string FilledCycle = "●";
private const string EmptyCycle = "○";
@@ -48,8 +49,18 @@ namespace KernelPanic.UI
private VisualElement handRow;
private VisualElement turnResourceGrid;
private Label logLabel;
private VisualElement feedbackLayer;
private VisualElement damageVignette;
private VisualElement overlay;
private Color distroAccent;
private readonly Dictionary<CombatantState, VisualElement> combatantElements = new();
private readonly Dictionary<CardInstance, VisualElement> handCardElements = new();
private readonly Dictionary<CardInstance, VisualElement> queueChipElements = new();
private readonly Dictionary<CardInstance, VisualElement> stackChipElements = new();
private readonly Dictionary<CombatantState, int> previousUptime = new();
private readonly Dictionary<CombatantState, int> previousShield = new();
private readonly HashSet<CardInstance> knownHandCards = new();
private int queueCascadeIndex;
private void Awake()
{
@@ -64,9 +75,18 @@ namespace KernelPanic.UI
private void OnEnable()
{
root?.EnableInClassList("reduced-motion", UIPreferences.ReducedMotion);
combatManager.StateChanged += Refresh;
combatManager.CombatLog += HandleCombatLog;
runManager.RepositoryChanged += Refresh;
GameEvents.CardPlayed += HandleCardPlayed;
GameEvents.CardResolved += HandleCardResolved;
GameEvents.PhaseChanged += HandlePhaseChanged;
GameEvents.DamageDealt += HandleDamageDealt;
GameEvents.CombatantDefeated += HandleCombatantDefeated;
GameEvents.EnemyWouldAct += HandleEnemyWouldAct;
GameEvents.PlayerDamaged += HandlePlayerDamaged;
GameEvents.StatusApplied += HandleStatusApplied;
}
private void Start()
@@ -101,6 +121,14 @@ namespace KernelPanic.UI
combatManager.StateChanged -= Refresh;
combatManager.CombatLog -= HandleCombatLog;
runManager.RepositoryChanged -= Refresh;
GameEvents.CardPlayed -= HandleCardPlayed;
GameEvents.CardResolved -= HandleCardResolved;
GameEvents.PhaseChanged -= HandlePhaseChanged;
GameEvents.DamageDealt -= HandleDamageDealt;
GameEvents.CombatantDefeated -= HandleCombatantDefeated;
GameEvents.EnemyWouldAct -= HandleEnemyWouldAct;
GameEvents.PlayerDamaged -= HandlePlayerDamaged;
GameEvents.StatusApplied -= HandleStatusApplied;
}
private bool TryBuildRunConfig(out RunConfig config)
@@ -140,7 +168,7 @@ namespace KernelPanic.UI
AddLanguageDeck(Language.Python, startingDeck);
AddLanguageDeck(Language.JavaScript, startingDeck);
config = new RunConfig(distro, Language.Python, Language.JavaScript, startingDeck, Environment.TickCount);
config = new RunConfig(distro, Language.Python, Language.JavaScript, startingDeck, Environment.TickCount, 1);
return true;
}
@@ -182,6 +210,12 @@ namespace KernelPanic.UI
{
root.styleSheets.Add(styleSheet);
}
StyleSheet scrollbarStyleSheet = Resources.Load<StyleSheet>(SharedScrollbarStyleResourcePath);
if (scrollbarStyleSheet != null)
{
root.styleSheets.Add(scrollbarStyleSheet);
}
}
private void BuildLayout()
@@ -239,6 +273,16 @@ namespace KernelPanic.UI
logLabel.AddToClassList("log-line");
commandPanel.Add(logLabel);
feedbackLayer = new();
feedbackLayer.AddToClassList("feedback-layer");
feedbackLayer.pickingMode = PickingMode.Ignore;
root.Add(feedbackLayer);
damageVignette = new();
damageVignette.AddToClassList("damage-vignette");
damageVignette.pickingMode = PickingMode.Ignore;
root.Add(damageVignette);
overlay = new();
overlay.AddToClassList("overlay");
overlay.style.display = DisplayStyle.None;
@@ -336,10 +380,13 @@ namespace KernelPanic.UI
private void RefreshPlayer()
{
playerPanel.Clear();
combatantElements.Remove(combatManager.PlayerState);
combatantElements[combatManager.PlayerState] = playerPanel;
playerPanel.Add(PanelHeader(DisplayName(runManager.CurrentConfig.Distro), "htop"));
ApplyAccent(playerPanel);
CombatantState state = combatManager.PlayerState;
DetectBeneficialResourceFeedback(state, playerPanel);
playerPanel.Add(MeterBlock("uptime", state.CurrentUptime, state.MaxUptime, MeterTone.Uptime));
playerPanel.Add(MeterBlock("shield", state.Shield, Mathf.Max(1, state.Shield), state.Shield > 0 ? MeterTone.Beneficial : MeterTone.Muted));
playerPanel.Add(CycleBlock(state.Cycles, state.MaxCycles));
@@ -352,6 +399,8 @@ namespace KernelPanic.UI
private void RefreshTracks()
{
queueChipElements.Clear();
stackChipElements.Clear();
FillCardStrip(interpreterStrip, combatManager.InterpreterQueue.Cards, "queue empty");
FillCardStrip(lazyStackPile, combatManager.LazyStack.Cards, "stack empty");
tokenArea.Clear();
@@ -361,6 +410,12 @@ namespace KernelPanic.UI
private void RefreshEnemies()
{
enemyRow.Clear();
combatantElements.Clear();
if (combatManager.PlayerState != null)
{
combatantElements[combatManager.PlayerState] = playerPanel;
}
for (int i = 0; i < combatManager.Enemies.Count; i++)
{
int index = i;
@@ -377,6 +432,8 @@ namespace KernelPanic.UI
Label name = new(enemy.Name);
name.AddToClassList("enemy-name");
card.Add(name);
combatantElements[enemy.State] = card;
DetectBeneficialResourceFeedback(enemy.State, card);
card.Add(MeterBlock("uptime", enemy.CurrentUptime, enemy.MaxUptime, MeterTone.Uptime));
if (enemy.State.Shield > 0)
{
@@ -394,11 +451,27 @@ namespace KernelPanic.UI
private void RefreshHand()
{
handRow.Clear();
handCardElements.Clear();
HashSet<CardInstance> currentHandCards = new();
IReadOnlyList<CardInstance> hand = combatManager.HandController.Cards;
for (int i = 0; i < hand.Count; i++)
{
CardInstance card = hand[i];
handRow.Add(CardFace(card));
currentHandCards.Add(card);
VisualElement face = CardFace(card);
handCardElements[card] = face;
if (!knownHandCards.Contains(card))
{
PlayElementBeat(face, "hand-card-drawn", 260);
}
handRow.Add(face);
}
knownHandCards.Clear();
foreach (CardInstance card in currentHandCards)
{
knownHandCards.Add(card);
}
}
@@ -694,7 +767,7 @@ namespace KernelPanic.UI
private VisualElement CardFace(CardInstance card)
{
Button button = new(() => combatManager.PlayCard(card));
Button button = new(() => PlayCardWithFeedback(card));
button.text = string.Empty;
button.AddToClassList("hand-card");
if (combatManager.PendingTargetCard == card)
@@ -885,8 +958,348 @@ namespace KernelPanic.UI
for (int i = 0; i < cards.Count; i++)
{
target.Add(CardChip(cards[i]));
VisualElement chip = CardChip(cards[i]);
if (target == interpreterStrip)
{
queueChipElements[cards[i]] = chip;
}
else if (target == lazyStackPile)
{
stackChipElements[cards[i]] = chip;
}
target.Add(chip);
}
}
private bool PlayCardWithFeedback(CardInstance card)
{
int cyclesBefore = combatManager.PlayerState == null ? 0 : combatManager.PlayerState.Cycles;
bool played = combatManager.PlayCard(card);
if (played)
{
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 (combatManager.PendingTargetCard != card && handCardElements.TryGetValue(card, out VisualElement face))
{
PlayElementBeat(face, "feedback-denied", 220);
}
return false;
}
private void HandleCardPlayed(CardPlayedEvent payload)
{
if (payload.Card == null)
{
return;
}
VisualElement source = handCardElements.TryGetValue(payload.Card, out VisualElement cardElement) ? cardElement : null;
VisualElement destination = payload.Track switch
{
ResolutionTrack.InterpreterQueue => interpreterStrip,
ResolutionTrack.LazyStack => lazyStackPile,
_ => FirstTargetElement(payload.Card)
};
FlyCardGhost(payload.Card, source, destination, payload.Track == ResolutionTrack.Native ? "feedback-card-strike" : "feedback-card-route");
}
private void HandleCardResolved(CardResolvedEvent payload)
{
if (payload.Card == null)
{
return;
}
if (payload.Track == ResolutionTrack.InterpreterQueue)
{
int delay = UIPreferences.ReducedMotion ? 0 : Mathf.Min(queueCascadeIndex * 120, 600);
queueCascadeIndex++;
ScheduleFeedback(() =>
{
VisualElement chip = queueChipElements.TryGetValue(payload.Card, out VisualElement queuedChip) ? queuedChip : interpreterStrip;
PlayElementBeat(chip, "queue-chip-resolve", 260);
FlyCardGhost(payload.Card, chip, turnResourceGrid, "feedback-card-discard");
}, delay);
return;
}
if (payload.Track == ResolutionTrack.LazyStack)
{
VisualElement chip = stackChipElements.TryGetValue(payload.Card, out VisualElement stackChip) ? stackChip : lazyStackPile;
PlayElementBeat(chip, "stack-chip-resolve", 320);
FlyCardGhost(payload.Card, chip, turnResourceGrid, "feedback-card-discard");
}
}
private void HandlePhaseChanged(PhaseChangedEvent payload)
{
if (payload.NextPhase == TurnPhase.Interpret)
{
queueCascadeIndex = 0;
}
ScheduleFeedback(() => PlayElementBeat(phaseLabel, "phase-pulse", 260), 0);
}
private void HandleDamageDealt(DamageDealtEvent payload)
{
ScheduleFeedback(() =>
{
bool isPlayer = payload.Target == combatManager.PlayerState;
VisualElement target = CombatantElement(payload.Target);
string text = payload.Amount <= 0 ? "blocked" : $"-{Mathf.RoundToInt(payload.Amount)}";
string tone = payload.Amount <= 0 ? "float-muted" : "float-damage";
if (payload.Amount >= 8)
{
tone += " float-large";
}
SpawnFloatingText(target, text, tone);
PlayElementBeat(target, payload.Amount <= 0 ? "feedback-block" : "feedback-hit", 240);
if (isPlayer && payload.Amount > 0)
{
FlashDamageVignette();
}
}, 0);
}
private void HandleCombatantDefeated(CombatantDefeatedEvent payload)
{
ScheduleFeedback(() =>
{
VisualElement target = CombatantElement(payload.Combatant);
SpawnFloatingText(target, "killed", "float-kill float-large");
SpawnDeathGhost(target);
PlayElementBeat(target, "feedback-killed", 260);
}, 0);
}
private void HandleEnemyWouldAct(EnemyWouldActEvent payload)
{
ScheduleFeedback(() => PlayElementBeat(CombatantElement(payload.Enemy?.State), "enemy-acting", 260), 0);
}
private void HandlePlayerDamaged(PlayerDamagedEvent payload)
{
if (payload.Amount > 0)
{
ScheduleFeedback(FlashDamageVignette, 0);
}
}
private void HandleStatusApplied(StatusAppliedEvent payload)
{
ScheduleFeedback(() =>
{
StatusDescriptor descriptor = StatusEffectController.GetDescriptor(payload.StatusType);
SpawnFloatingText(CombatantElement(payload.Target), $"{descriptor.IconKey} x{payload.Stacks}", descriptor.IsBeneficial ? "float-heal" : "float-status");
PlayElementBeat(CombatantElement(payload.Target), descriptor.IsBeneficial ? "feedback-boost" : "feedback-status", 220);
}, 0);
}
private void DetectBeneficialResourceFeedback(CombatantState state, VisualElement element)
{
if (state == null || element == null)
{
return;
}
if (previousUptime.TryGetValue(state, out int oldUptime) && state.CurrentUptime > oldUptime)
{
SpawnFloatingText(element, $"+{state.CurrentUptime - oldUptime}", "float-heal");
PlayElementBeat(element, "feedback-boost", 220);
}
if (previousShield.TryGetValue(state, out int oldShield) && state.Shield > oldShield)
{
SpawnFloatingText(element, $"+{state.Shield - oldShield} shield", "float-heal");
PlayElementBeat(element, "feedback-block", 220);
}
previousUptime[state] = state.CurrentUptime;
previousShield[state] = state.Shield;
}
private VisualElement CombatantElement(CombatantState state)
{
if (state != null && combatantElements.TryGetValue(state, out VisualElement element))
{
return element;
}
return state == combatManager.PlayerState ? playerPanel : enemyRow;
}
private VisualElement FirstTargetElement(CardInstance card)
{
if (card?.TargetSnapshot == null)
{
return enemyRow;
}
for (int i = 0; i < card.TargetSnapshot.Count; i++)
{
VisualElement element = CombatantElement(card.TargetSnapshot[i]);
if (element != null)
{
return element;
}
}
return enemyRow;
}
private void PlayElementBeat(VisualElement element, string className, int durationMs)
{
if (element == null || string.IsNullOrWhiteSpace(className))
{
return;
}
element.RemoveFromClassList(className);
element.AddToClassList(className);
ScheduleFeedback(() => element.RemoveFromClassList(className), UIPreferences.ReducedMotion ? 80 : durationMs);
}
private void SpawnFloatingText(VisualElement anchor, string text, string className)
{
if (feedbackLayer == null || anchor == null)
{
return;
}
Rect anchorRect = anchor.worldBound;
Rect rootRect = root.worldBound;
Label label = new(text);
label.AddToClassList("float-number");
if (!string.IsNullOrWhiteSpace(className))
{
string[] classes = className.Split(' ');
for (int i = 0; i < classes.Length; i++)
{
if (!string.IsNullOrWhiteSpace(classes[i]))
{
label.AddToClassList(classes[i]);
}
}
}
label.style.left = anchorRect.center.x - rootRect.x - 30f;
label.style.top = anchorRect.center.y - rootRect.y - 16f;
feedbackLayer.Add(label);
if (UIPreferences.ReducedMotion)
{
label.AddToClassList("float-static");
ScheduleFeedback(() => label.RemoveFromHierarchy(), 420);
return;
}
ScheduleFeedback(() =>
{
label.style.top = anchorRect.center.y - rootRect.y - 42f;
label.style.opacity = 0f;
}, 20);
ScheduleFeedback(() => label.RemoveFromHierarchy(), 520);
}
private void FlyCardGhost(CardInstance card, VisualElement source, VisualElement destination, string className)
{
if (feedbackLayer == null || card == null)
{
return;
}
Rect rootRect = root.worldBound;
Rect sourceRect = source == null ? new Rect(rootRect.center.x - 70f, rootRect.center.y - 50f, 140f, 110f) : source.worldBound;
Rect destinationRect = destination == null ? sourceRect : destination.worldBound;
VisualElement ghost = CardFaceView(card);
ghost.AddToClassList("feedback-card-ghost");
ghost.AddToClassList(className);
ghost.style.position = Position.Absolute;
ghost.style.left = sourceRect.x - rootRect.x;
ghost.style.top = sourceRect.y - rootRect.y;
ghost.style.width = Mathf.Max(110f, sourceRect.width);
ghost.style.height = Mathf.Max(94f, sourceRect.height);
feedbackLayer.Add(ghost);
if (UIPreferences.ReducedMotion)
{
ghost.AddToClassList("feedback-card-instant");
ScheduleFeedback(() => ghost.RemoveFromHierarchy(), 140);
return;
}
ScheduleFeedback(() =>
{
ghost.style.left = destinationRect.center.x - rootRect.x - (sourceRect.width * 0.5f);
ghost.style.top = destinationRect.center.y - rootRect.y - (sourceRect.height * 0.5f);
ghost.style.opacity = 0f;
}, 20);
ScheduleFeedback(() => ghost.RemoveFromHierarchy(), 360);
}
private void SpawnDeathGhost(VisualElement source)
{
if (feedbackLayer == null || source == null)
{
return;
}
Rect rootRect = root.worldBound;
Rect sourceRect = source.worldBound;
VisualElement ghost = new();
ghost.AddToClassList("death-ghost");
ghost.style.position = Position.Absolute;
ghost.style.left = sourceRect.x - rootRect.x;
ghost.style.top = sourceRect.y - rootRect.y;
ghost.style.width = sourceRect.width;
ghost.style.height = sourceRect.height;
feedbackLayer.Add(ghost);
if (UIPreferences.ReducedMotion)
{
ScheduleFeedback(() => ghost.RemoveFromHierarchy(), 180);
return;
}
ScheduleFeedback(() =>
{
ghost.style.opacity = 0f;
ghost.style.scale = new Scale(new Vector3(0.92f, 0.92f, 1f));
}, 20);
ScheduleFeedback(() => ghost.RemoveFromHierarchy(), 360);
}
private void FlashDamageVignette()
{
if (damageVignette == null)
{
return;
}
PlayElementBeat(damageVignette, "damage-vignette-on", UIPreferences.ReducedMotion ? 120 : 240);
}
private void ScheduleFeedback(Action action, int delayMs)
{
if (action == null || root == null)
{
return;
}
root.schedule.Execute(action).StartingIn(Mathf.Max(0, delayMs));
}
private void HandleCombatLog(string message)
+37 -6
View File
@@ -31,6 +31,7 @@ namespace KernelPanic.UI
private GachaService _gachaService;
private PlayerCollection _playerCollection;
private EntropyWallet _wallet;
private Func<IReadOnlyList<DistroDefinition>, PullResolutionResult> _resolvePulledDistros;
private Action _onChanged;
private Action _requestRootCreditExchange;
private int _selectedBannerIndex;
@@ -40,13 +41,14 @@ namespace KernelPanic.UI
private string _resultBannerId;
private string _resultText;
public void Bind(VisualElement root, DistroDatabase database, FontAsset artFont, GachaService service, PlayerCollection collection, EntropyWallet entropyWallet, Action changedCallback, Action rootCreditExchangeCallback)
public void Bind(VisualElement root, DistroDatabase database, FontAsset artFont, GachaService service, PlayerCollection collection, EntropyWallet entropyWallet, Func<IReadOnlyList<DistroDefinition>, PullResolutionResult> pullResolver, Action changedCallback, Action rootCreditExchangeCallback)
{
_distroDatabase = database;
_monospaceFont = artFont;
_gachaService = service;
_playerCollection = collection;
_wallet = entropyWallet;
_resolvePulledDistros = pullResolver;
_onChanged = changedCallback;
_requestRootCreditExchange = rootCreditExchangeCallback;
_bannerList = root.Q<VisualElement>("GachaBannerList");
@@ -252,18 +254,30 @@ namespace KernelPanic.UI
lines.Add($"entropy fallback: spent {result.EntropySpent} entropy");
}
List<DistroDefinition> distroRewards = new();
for (int i = 0; i < result.Rewards.Count; i++)
{
if (result.Rewards[i].Distro != null)
{
distroRewards.Add(result.Rewards[i].Distro);
}
}
PullResolutionResult resolution = _resolvePulledDistros?.Invoke(distroRewards);
int distroOutcomeIndex = 0;
for (int i = 0; i < result.Rewards.Count; i++)
{
GachaReward reward = result.Rewards[i];
bool duplicate = reward.Distro != null && _playerCollection.GetOwnedCount(reward.Distro.Id) > 0;
if (reward.Distro != null)
PullResolutionOutcome? outcome = null;
if (reward.Distro != null && resolution != null && distroOutcomeIndex < resolution.Outcomes.Count)
{
_playerCollection.Add(reward.Distro);
outcome = resolution.Outcomes[distroOutcomeIndex];
distroOutcomeIndex++;
}
string suffix = reward.Guaranteed ? " guaranteed" : reward.PityTriggered ? " pity" : string.Empty;
string duplicateText = duplicate ? " duplicate" : string.Empty;
lines.Add($"{i + 1:00}: {reward.StarRating}-star {reward.DisplayName}{suffix}{duplicateText}");
string outcomeText = FormatPullOutcome(outcome);
lines.Add($"{i + 1:00}: {reward.StarRating}-star {reward.DisplayName}{suffix}{outcomeText}");
}
_resultBannerId = bannerId;
@@ -272,6 +286,23 @@ namespace KernelPanic.UI
Refresh();
}
private static string FormatPullOutcome(PullResolutionOutcome? outcome)
{
if (!outcome.HasValue)
{
return string.Empty;
}
PullResolutionOutcome value = outcome.Value;
return value.Kind switch
{
PullOutcomeKind.Granted => " granted",
PullOutcomeKind.Dupe => $" duplicate +{value.MergesAwarded} merges",
PullOutcomeKind.DupeOverflow => $" max-version duplicate +{value.MergesAwarded} merges",
_ => string.Empty
};
}
private void RequestPullSelectedBanner(int pullCount)
{
string bannerId = _bannerIds[_selectedBannerIndex];
+58 -9
View File
@@ -23,6 +23,7 @@ namespace KernelPanic.UI
private const string HiddenClassName = "hidden";
private const string SelectedClassName = "selected";
private const string CursorOnClassName = "cursor-on";
private const string SharedScrollbarStyleResourcePath = "TerminalScrollbars";
private const float BootIntroSeconds = 1.5f;
private static bool _bootIntroPlayed;
@@ -133,13 +134,14 @@ namespace KernelPanic.UI
_cardLoadout = new CardLoadout(_playerCollection.OwnedUnits);
Initialize(new EntropyWallet()); // TODO: Replace with persistent wallet service composition.
BindElements();
LoadSharedStyles();
BindCommandEntries();
RegisterCommandEntryCallbacks();
featuredUnitPanel.Bind(_root, monospaceFont);
collectionScreen.Bind(_root, monospaceFont, languageDeckDatabase, cardDatabase, _playerCollection);
collectionScreen.Bind(_root, monospaceFont, languageDeckDatabase, cardDatabase, _playerCollection, GetMergesBalance, UpgradeCollectionUnit);
BindScreenFrames();
starterSelection.Bind(_root, distroDatabase, HandleStarterConfirmed);
gachaScreen.Bind(_root, distroDatabase, monospaceFont, _gachaService, _playerCollection, _wallet, HandleMetaStateChanged, OpenRootCreditExchange);
gachaScreen.Bind(_root, distroDatabase, monospaceFont, _gachaService, _playerCollection, _wallet, ResolvePulledDistros, HandleMetaStateChanged, OpenRootCreditExchange);
LoadMetaState();
_playerCollection.Changed += HandleMetaStateChanged;
_gachaService.Changed += HandleMetaStateChanged;
@@ -221,6 +223,15 @@ namespace KernelPanic.UI
_collectionLanguagesButton = _root.Q<Button>("CollectionLanguagesButton");
}
private void LoadSharedStyles()
{
StyleSheet scrollbarStyleSheet = Resources.Load<StyleSheet>(SharedScrollbarStyleResourcePath);
if (scrollbarStyleSheet != null)
{
_root.styleSheets.Add(scrollbarStyleSheet);
}
}
private void BindScreenFrames()
{
_runSetupFrame.Bind(_runSetupPanel, "$ ./start_run --configure", "[esc] back [arrows] navigate [enter] select [b] boot run", ShowMainMenu);
@@ -554,12 +565,13 @@ namespace KernelPanic.UI
_saveData.EnsureLists();
_wallet.SetBalance(_saveData.entropyBalance);
for (int i = 0; i < _saveData.ownedUnitIds.Count; i++)
for (int i = 0; i < _saveData.ownedUnits.Count; i++)
{
DistroDefinition unit = ResolveSavedDistro(_saveData.ownedUnitIds[i]);
OwnedUnitSaveEntry entry = _saveData.ownedUnits[i];
DistroDefinition unit = ResolveSavedDistro(entry?.id);
if (unit != null)
{
_playerCollection.Add(unit);
_playerCollection.Add(unit, entry.version);
}
}
@@ -624,7 +636,7 @@ namespace KernelPanic.UI
private void HandleStarterConfirmed(DistroDefinition picked, IReadOnlyList<DistroDefinition> remaining)
{
_playerCollection.Add(picked);
_playerCollection.Add(picked, 1);
AddAllFourStarDistrosToBeginnerBannerPool();
_gachaService.SetBeginnerGuaranteedDistros(remaining);
_saveData.starterChosen = true;
@@ -639,6 +651,7 @@ namespace KernelPanic.UI
_saveData ??= SaveData.CreateDefault();
_saveData.EnsureLists();
_saveData.entropyBalance = _wallet == null ? 0 : _wallet.Balance;
_saveData.ownedUnits.Clear();
_saveData.ownedUnitIds.Clear();
_saveData.bannerPoolIds.Clear();
_gachaService.WriteProgress(_saveData);
@@ -648,7 +661,11 @@ namespace KernelPanic.UI
DistroDefinition unit = _playerCollection.OwnedUnits[i];
if (unit != null && !string.IsNullOrWhiteSpace(unit.Id))
{
_saveData.ownedUnitIds.Add(unit.Id);
_saveData.ownedUnits.Add(new OwnedUnitSaveEntry
{
id = unit.Id,
version = Mathf.Clamp(_playerCollection.GetVersion(unit.Id), 1, GachaTuning.MaxVersion)
});
}
}
@@ -664,6 +681,38 @@ namespace KernelPanic.UI
_saveService.Save(_saveData);
}
private PullResolutionResult ResolvePulledDistros(IReadOnlyList<DistroDefinition> pulledDistros)
{
PullResolutionContext context = new(_saveData, _playerCollection, distroDatabase, _playerCollection.FeaturedUnit?.Id);
return PullResolver.Resolve(pulledDistros, context);
}
private int GetMergesBalance()
{
_saveData ??= SaveData.CreateDefault();
_saveData.EnsureLists();
return _saveData.merges;
}
private VersionUpgradeResult UpgradeCollectionUnit(DistroDefinition unit)
{
if (unit == null)
{
return VersionUpgradeResult.Failed(null, VersionUpgradeFailureReason.NotOwned);
}
VersionUpgradeResult result = VersionUpgrader.TryUpgrade(unit.Id, _saveData, _playerCollection);
if (result.Success)
{
SaveCurrentState();
featuredUnitPanel.Refresh(_playerCollection.OwnedUnits, _playerCollection.FeaturedUnit);
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
RefreshCurrencyReadouts();
}
return result;
}
private void SaveLastRunLoadout(DistroDefinition unit, IReadOnlyList<string> cardIds)
{
_saveData ??= SaveData.CreateDefault();
@@ -708,7 +757,7 @@ namespace KernelPanic.UI
_rootCreditsToEntropyButton?.SetEnabled(true);
bool showPullTokens = IsGachaVisible();
_pullTokensLabel.text = showPullTokens
? $"bandwidth={_gachaService.PullTokens} compute-credits={_gachaService.LimitedPullTokens}"
? $"bandwidth={_gachaService.PullTokens} compute-credits={_gachaService.LimitedPullTokens} merges={_saveData.merges}"
: string.Empty;
_pullTokensLabel.EnableInClassList(HiddenClassName, !showPullTokens);
}
@@ -1452,7 +1501,7 @@ namespace KernelPanic.UI
}
SaveLastRunLoadout(unit, equippedCardIds);
RunContext.Set(unit, BuildEquippedCardDefinitions(unit, equippedCardIds), _selectedRunLanguages[0], _selectedRunLanguages[1]);
RunContext.Set(unit, BuildEquippedCardDefinitions(unit, equippedCardIds), _selectedRunLanguages[0], _selectedRunLanguages[1], _playerCollection.GetVersion(unit.Id));
SceneLoader.LoadGame();
}
+1 -1
View File
@@ -11,7 +11,7 @@
</ui:VisualElement>
<ui:VisualElement name="CollectionContent" class="collection-shell">
<ui:ScrollView name="CollectionList" class="collection-list" />
<ui:VisualElement name="CollectionDetail" class="collection-detail" />
<ui:ScrollView name="CollectionDetail" class="collection-detail" />
</ui:VisualElement>
</ui:VisualElement>
<ui:VisualElement class="screen-frame-footer">
+132 -23
View File
@@ -8,6 +8,9 @@
--kp-border: #1d3b2a;
--kp-hover: #102018;
--kp-amber: #ffbf5c;
--kp-scroll-track: var(--kp-panel);
--kp-scroll-thumb: var(--kp-muted);
--kp-scroll-thumb-hover: var(--kp-primary-dim);
}
.terminal-root {
@@ -774,8 +777,8 @@ Button {
.collection-detail {
flex-grow: 1;
min-width: 0;
overflow: hidden;
padding: 16px;
min-height: 0;
padding: 0;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
@@ -787,14 +790,141 @@ Button {
background-color: var(--kp-panel);
}
.collection-detail .unity-scroll-view__content-container {
min-width: 0;
padding: 16px;
}
.collection-detail-readout {
flex-direction: row;
flex-shrink: 0;
margin-top: 12px;
margin-bottom: 12px;
padding-top: 10px;
padding-bottom: 10px;
border-top-width: 1px;
border-bottom-width: 1px;
border-top-color: var(--kp-border);
border-bottom-color: var(--kp-border);
overflow: hidden;
}
.version-upgrade-panel {
flex-shrink: 0;
margin-top: 4px;
margin-bottom: 10px;
padding: 10px 10px;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: var(--kp-border);
border-right-color: var(--kp-border);
border-top-color: var(--kp-border);
border-bottom-color: var(--kp-border);
background-color: var(--kp-bg-solid);
}
.version-upgrade-summary {
flex-direction: row;
align-items: center;
margin-bottom: 4px;
}
.version-upgrade-current {
flex-grow: 1;
min-width: 0;
color: var(--kp-primary);
-unity-font-style: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.version-upgrade-wallet {
color: var(--kp-muted);
min-width: 110px;
-unity-text-align: middle-right;
}
.version-upgrade-wallet-affordable {
color: var(--kp-primary);
}
.version-upgrade-wallet-warning {
color: var(--kp-amber);
}
.version-upgrade-next {
color: var(--kp-muted);
font-size: 13px;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 5px;
}
.version-upgrade-command {
min-height: 26px;
flex-direction: row;
align-items: center;
padding-left: 8px;
padding-right: 8px;
border-left-width: 2px;
border-left-color: var(--kp-primary);
background-color: var(--kp-hover);
}
.version-upgrade-command:hover {
background-color: var(--kp-panel);
}
.version-upgrade-command-text {
flex-grow: 1;
min-width: 0;
color: var(--kp-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.version-upgrade-command-state {
color: var(--kp-muted);
min-width: 120px;
-unity-text-align: middle-right;
}
.version-path {
flex-direction: row;
flex-wrap: wrap;
margin-top: 8px;
}
.version-path-step {
color: var(--kp-muted);
font-size: 12px;
margin-right: 14px;
margin-bottom: 5px;
}
.version-path-owned {
color: var(--kp-primary);
}
.version-path-next {
color: var(--kp-amber);
-unity-font-style: bold;
}
.version-path-locked {
color: var(--kp-muted);
opacity: 0.65;
}
.collection-detail-values {
flex-grow: 1;
min-width: 0;
padding-left: 12px;
}
.collection-detail .kv-value {
@@ -1039,27 +1169,6 @@ Button {
margin-top: 18px;
}
ScrollView .unity-scroll-view__vertical-scroller,
ScrollView .unity-scroll-view__horizontal-scroller {
width: 6px;
background-color: var(--kp-panel);
}
ScrollView .unity-scroller__low-button,
ScrollView .unity-scroller__high-button {
display: none;
}
ScrollView .unity-base-slider__tracker {
background-color: var(--kp-panel);
border-radius: 0;
}
ScrollView .unity-base-slider__dragger {
background-color: var(--kp-muted);
border-radius: 0;
}
.modal-overlay {
position: absolute;
left: 0;