Implement cutscene for gambling and make it dopamine-inducing

This commit is contained in:
2026-07-08 10:29:49 +02:00
parent 598cd0aa27
commit ac09d67677
13 changed files with 1761 additions and 71 deletions
@@ -11,9 +11,19 @@
</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 beforePath="$PROJECT_DIR$/Assets/Resources/CombatScene.uss" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Resources/CombatScene.uss" afterDir="false" /> <change afterPath="$PROJECT_DIR$/Assets/Resources/GamblingScene.uss" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Resources/RarityPresentation.uss" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Scripts/Meta/GachaPullContext.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Scripts/UI/GamblingSceneController.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Assets/Scripts/UI/RarityPresentation.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/Scenes/GamblingScene.unity" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scenes/GamblingScene.unity" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Core/SceneLoader.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Core/SceneLoader.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Meta/GachaTuning.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Meta/GachaTuning.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" />
<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/Scripts/UI/MainMenuController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/MainMenuController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ProjectSettings/EditorBuildSettings.asset" beforeDir="false" afterPath="$PROJECT_DIR$/ProjectSettings/EditorBuildSettings.asset" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -21,7 +31,7 @@
<option name="LAST_RESOLUTION" value="IGNORE" /> <option name="LAST_RESOLUTION" value="IGNORE" />
</component> </component>
<component name="EmbeddingIndexingInfo"> <component name="EmbeddingIndexingInfo">
<option name="cachedIndexableFilesCount" value="44" /> <option name="cachedIndexableFilesCount" value="46" />
<option name="fileBasedEmbeddingIndicesEnabled" value="true" /> <option name="fileBasedEmbeddingIndicesEnabled" value="true" />
</component> </component>
<component name="Git.Settings"> <component name="Git.Settings">
@@ -70,23 +80,23 @@
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" /> <component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
<component name="NextEditCompletionFeaturesState"> <component name="NextEditCompletionFeaturesState">
<decayedCancelled> <decayedCancelled>
<entry key="MS100" value="1.026460791011334" /> <entry key="MS100" value="1.0623037946223934" />
<entry key="MS500" value="1.4860095350060054" /> <entry key="MS500" value="3.3946231944932093" />
<entry key="S2" value="2.6444467846038227" /> <entry key="S2" value="13.152660945937393" />
<entry key="S5" value="4.842254075216646" /> <entry key="S5" value="24.1598334590001" />
<entry key="S10" value="6.468439791164836" /> <entry key="S10" value="33.8798077205763" />
<entry key="S30" value="8.038810318782259" /> <entry key="S30" value="58.19510567053269" />
<entry key="S60" value="9.594633537295994" /> <entry key="S60" value="77.3856499085495" />
<entry key="M2" value="20.243382686476437" /> <entry key="M2" value="102.00601157029814" />
<entry key="M5" value="63.58627499290541" /> <entry key="M5" value="137.65784976271723" />
<entry key="M10" value="123.19334985734534" /> <entry key="M10" value="165.66654267587106" />
<entry key="M15" value="169.67982802867456" /> <entry key="M15" value="186.93733876684658" />
<entry key="M30" value="263.8069798782453" /> <entry key="M30" value="255.34135629387984" />
<entry key="H1" value="405.314020833015" /> <entry key="H1" value="366.1678729821731" />
<entry key="H2" value="674.2556828746846" /> <entry key="H2" value="476.8702050334635" />
<entry key="H4" value="1106.1957697237865" /> <entry key="H4" value="590.3523177793572" />
<entry key="D1" value="2326.0084884842904" /> <entry key="D1" value="1946.7042196302243" />
<entry key="W1" value="2780.8343863258788" /> <entry key="W1" value="3215.7039196395745" />
</decayedCancelled> </decayedCancelled>
<decayedSelected> <decayedSelected>
<entry key="MS100" value="0.0" /> <entry key="MS100" value="0.0" />
@@ -94,37 +104,37 @@
<entry key="S2" value="0.0" /> <entry key="S2" value="0.0" />
<entry key="S5" value="0.0" /> <entry key="S5" value="0.0" />
<entry key="S10" value="0.0" /> <entry key="S10" value="0.0" />
<entry key="S30" value="2.6792133394898083E-170" /> <entry key="S30" value="0.0" />
<entry key="S60" value="1.636830272047101E-85" /> <entry key="S60" value="0.0" />
<entry key="M2" value="4.045775910807539E-43" /> <entry key="M2" value="5.2702880915532256E-223" />
<entry key="M5" value="1.1035721441393251E-17" /> <entry key="M5" value="1.2266869422157816E-89" />
<entry key="M10" value="3.322005635364477E-9" /> <entry key="M10" value="3.502409088350206E-45" />
<entry key="M15" value="2.2263848764463433E-6" /> <entry key="M15" value="2.3062758293224876E-30" />
<entry key="M30" value="0.0014921080092110655" /> <entry key="M30" value="1.5186432482681809E-15" />
<entry key="H1" value="0.03866578312125709" /> <entry key="H1" value="3.900807883852808E-8" />
<entry key="H2" value="0.20721305616718577" /> <entry key="H2" value="2.0812823039862135E-4" />
<entry key="H4" value="0.6222697235055645" /> <entry key="H4" value="0.01972130313642213" />
<entry key="D1" value="2.7484368986677024" /> <entry key="D1" value="1.5461273409431215" />
<entry key="W1" value="3.786049516239156" /> <entry key="W1" value="3.487343990420277" />
</decayedSelected> </decayedSelected>
<decayedShown> <decayedShown>
<entry key="MS100" value="1.0049797369168623" /> <entry key="MS100" value="1.0197451953704615" />
<entry key="MS500" value="1.478503669631289" /> <entry key="MS500" value="3.3707583435859494" />
<entry key="S2" value="2.6407224963924176" /> <entry key="S2" value="13.129985145747765" />
<entry key="S5" value="4.839408734689747" /> <entry key="S5" value="24.143018672679645" />
<entry key="S10" value="6.466516334385955" /> <entry key="S10" value="33.867989076642985" />
<entry key="S30" value="8.038007761765067" /> <entry key="S30" value="58.188374682435416" />
<entry key="S60" value="9.594143833655684" /> <entry key="S60" value="77.38119558741009" />
<entry key="M2" value="20.2428233697882" /> <entry key="M2" value="102.00309255211835" />
<entry key="M5" value="63.585521817921595" /> <entry key="M5" value="137.6562714734684" />
<entry key="M10" value="123.19258340133266" /> <entry key="M10" value="165.6655746148282" />
<entry key="M15" value="169.67910745780532" /> <entry key="M15" value="186.93658735123063" />
<entry key="M30" value="263.80787472691503" /> <entry key="M30" value="255.34078224152634" />
<entry key="H1" value="405.352065646138" /> <entry key="H1" value="366.16741523840614" />
<entry key="H2" value="674.4620834608667" /> <entry key="H2" value="476.8700971872463" />
<entry key="H4" value="1106.8171811444822" /> <entry key="H4" value="590.3718230883779" />
<entry key="D1" value="2328.7565510850786" /> <entry key="D1" value="1948.2500994732964" />
<entry key="W1" value="2784.6203691164433" /> <entry key="W1" value="3219.19119674037" />
</decayedShown> </decayedShown>
</component> </component>
<component name="ProjectColorInfo">{ <component name="ProjectColorInfo">{
@@ -213,7 +223,7 @@
<workItem from="1783330590375" duration="18451000" /> <workItem from="1783330590375" duration="18451000" />
<workItem from="1783371975450" duration="26000" /> <workItem from="1783371975450" duration="26000" />
<workItem from="1783372397891" duration="989000" /> <workItem from="1783372397891" duration="989000" />
<workItem from="1783410502976" duration="13134000" /> <workItem from="1783410502976" duration="19169000" />
</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" />
@@ -359,7 +369,23 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1783427430046</updated> <updated>1783427430046</updated>
</task> </task>
<option name="localTasksCounter" value="19" /> <task id="LOCAL-00019" summary="Fix balls">
<option name="closed" value="true" />
<created>1783492556269</created>
<option name="number" value="00019" />
<option name="presentableId" value="LOCAL-00019" />
<option name="project" value="LOCAL" />
<updated>1783492556270</updated>
</task>
<task id="LOCAL-00020" summary="update some strings">
<option name="closed" value="true" />
<created>1783493370994</created>
<option name="number" value="00020" />
<option name="presentableId" value="LOCAL-00020" />
<option name="project" value="LOCAL" />
<updated>1783493370994</updated>
</task>
<option name="localTasksCounter" value="21" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -392,7 +418,9 @@
<MESSAGE value="implement scaling rewards per wave completed in combat mode" /> <MESSAGE value="implement scaling rewards per wave completed in combat mode" />
<MESSAGE value="Implement animations, pipeline for duplicates and merging." /> <MESSAGE value="Implement animations, pipeline for duplicates and merging." />
<MESSAGE value="make some small changes to merging" /> <MESSAGE value="make some small changes to merging" />
<option name="LAST_COMMIT_MESSAGE" value="make some small changes to merging" /> <MESSAGE value="Fix balls" />
<MESSAGE value="update some strings" />
<option name="LAST_COMMIT_MESSAGE" value="update some strings" />
</component> </component>
<component name="XDebuggerManager"> <component name="XDebuggerManager">
<breakpoint-manager> <breakpoint-manager>
+390
View File
@@ -0,0 +1,390 @@
:root {
--kp-bg: #030605;
--kp-bg-deep: #020403;
--kp-panel: #07110d;
--kp-panel-raised: #0b1812;
--kp-primary: #5cff91;
--kp-primary-dim: #2f7a4d;
--kp-muted: #7f9b8a;
--kp-border: #14592f;
--kp-hover: #102018;
--kp-amber: #ffbf3f;
}
.gambling-root {
flex-grow: 1;
background-color: var(--kp-bg);
color: var(--kp-primary);
font-size: 16px;
}
.hidden {
display: none;
}
.gambling-shell {
flex-grow: 1;
padding: 18px 28px;
background-color: var(--kp-bg);
}
.gambling-frame {
flex-grow: 1;
position: relative;
padding: 14px;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border);
background-color: var(--kp-bg-deep);
}
.gambling-header {
flex-direction: row;
align-items: flex-start;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom-width: 1px;
border-bottom-color: var(--kp-border);
}
.gambling-title-block {
flex-grow: 1;
min-width: 0;
}
.gambling-command {
color: var(--kp-primary);
font-size: 20px;
-unity-font-style: bold;
}
.gambling-cost {
color: var(--kp-muted);
margin-top: 4px;
white-space: normal;
}
.gambling-skip {
color: var(--kp-muted);
min-width: 210px;
-unity-text-align: upper-right;
}
.gambling-flourish {
min-height: 20px;
color: var(--kp-muted);
margin-bottom: 8px;
transition-property: color;
transition-duration: 160ms;
}
.gambling-flourish-complete {
color: var(--kp-primary);
}
.gambling-results {
height: 330px;
min-height: 330px;
max-height: 330px;
flex-direction: row;
flex-wrap: wrap;
align-content: flex-start;
}
.pull-card {
width: 19%;
height: 152px;
min-height: 152px;
margin-right: 1%;
margin-bottom: 10px;
padding: 9px;
background-color: var(--kp-panel);
border-left-width: 3px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border);
opacity: 1;
translate: 0 0;
scale: 1 1;
transition-property: opacity, translate, scale, background-color, border-color;
transition-duration: 180ms;
}
.pull-card-hidden {
opacity: 0;
translate: 0 10px;
}
.pull-card-held {
color: var(--kp-muted);
background-color: var(--kp-bg);
border-color: var(--kp-primary-dim);
scale: 1 1;
}
.pull-card-held-rare {
border-color: var(--kp-rarity-4);
background-color: var(--kp-rarity-4-bg);
border-left-width: 5px;
scale: 1 1;
}
.pull-card-held-legendary {
border-color: var(--kp-rarity-5);
background-color: var(--kp-rarity-5-bg);
border-left-width: 6px;
scale: 1 1;
}
.pull-card-flash {
background-color: var(--kp-rarity-5-bg);
scale: 1 1;
}
.pull-card-revealed {
opacity: 1;
}
.pull-card-rare-pop {
scale: 1 1;
border-color: var(--kp-rarity-4);
border-left-width: 6px;
background-color: #0a2522;
}
.pull-card-legendary-pop {
scale: 1 1;
border-color: var(--kp-rarity-5);
border-left-width: 7px;
background-color: #2c1f07;
}
.pull-card-glow-4 {
border-color: var(--kp-rarity-4);
}
.pull-card-glow-5 {
border-color: var(--kp-rarity-5);
background-color: #1e1505;
}
.pull-card-dimmed {
opacity: 0.34;
scale: 1 1;
}
.pull-card-hidden.pull-card-dimmed {
opacity: 0;
}
.pull-card.rarity-3 {
background-color: var(--kp-rarity-3-bg);
opacity: 0.82;
}
.pull-card.rarity-4 {
background-color: var(--kp-rarity-4-bg);
}
.pull-card.rarity-5 {
background-color: var(--kp-rarity-5-bg);
border-left-width: 5px;
scale: 1 1;
}
.pull-card.rarity-5.pull-card-glow-5 {
border-left-width: 5px;
border-color: var(--kp-rarity-5);
background-color: #211705;
}
.pull-card.rarity-5.pull-card-glow-on {
background-color: #2a1c05;
border-color: #ffd76f;
scale: 1 1;
}
.pull-card-top {
flex-direction: row;
align-items: center;
height: 24px;
margin-bottom: 8px;
}
.pull-index {
width: 28px;
min-width: 28px;
color: var(--kp-muted);
font-size: 12px;
}
.pull-rarity {
flex-grow: 1;
min-width: 0;
-unity-font-style: bold;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pull-type-icon {
width: 22px;
min-width: 22px;
height: 22px;
color: var(--kp-muted);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border);
-unity-text-align: middle-center;
}
.pull-name {
flex-grow: 1;
min-width: 0;
color: var(--kp-primary);
-unity-font-style: bold;
font-size: 16px;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 8px;
}
.pull-status {
align-self: flex-start;
min-height: 22px;
padding-left: 6px;
padding-right: 6px;
padding-top: 2px;
padding-bottom: 2px;
background-color: var(--kp-bg);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border);
color: var(--kp-muted);
-unity-text-align: middle-center;
}
.pull-status-new {
color: var(--kp-primary);
border-color: var(--kp-primary);
}
.pull-status-pop {
background-color: var(--kp-primary);
color: var(--kp-bg);
scale: 1.08 1.08;
transition-property: scale, background-color, color;
transition-duration: 120ms;
}
.effect-layer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.spark {
position: absolute;
width: 8px;
height: 8px;
border-radius: 0;
opacity: 0;
scale: 1 1;
transition-property: left, top, opacity, scale;
transition-duration: 620ms;
}
.spark-active {
opacity: 0;
}
.gold-shimmer {
position: absolute;
left: 0;
top: 0;
width: 0;
height: 0;
opacity: 0;
background-color: #ffbf3f08;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: #ffbf3f33;
scale: 1 1;
transition-property: opacity, scale;
transition-duration: 180ms;
}
.gold-shimmer-on {
opacity: 0.85;
scale: 1 1;
}
.summary-panel {
margin-top: 6px;
padding-top: 8px;
border-top-width: 1px;
border-top-color: var(--kp-border);
}
.summary-totals {
color: var(--kp-muted);
margin-top: 0;
white-space: normal;
transition-property: color, scale;
transition-duration: 120ms;
}
.summary-tick {
color: var(--kp-primary);
scale: 1.01 1.01;
}
.continue-button {
height: 34px;
margin-top: 8px;
background-color: var(--kp-panel);
color: var(--kp-primary);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-primary);
opacity: 1;
transition-property: opacity, background-color, color;
transition-duration: 180ms;
}
.continue-button-pending {
opacity: 0;
}
.continue-button:hover {
background-color: var(--kp-primary);
color: var(--kp-bg);
}
.reduced-motion .pull-card,
.reduced-motion .continue-button,
.reduced-motion .spark,
.reduced-motion .gold-shimmer,
.reduced-motion .summary-totals {
transition-duration: 0ms;
}
.reduced-motion .pull-card-glow-4,
.reduced-motion .pull-card-glow-5 {
scale: 1 1;
}
+24
View File
@@ -0,0 +1,24 @@
:root {
--kp-rarity-3: #7f9b8a;
--kp-rarity-3-bg: #07100c;
--kp-rarity-4: #5cfff2;
--kp-rarity-4-bg: #08201e;
--kp-rarity-5: #ffbf3f;
--kp-rarity-5-bg: #211705;
}
.rarity-3 {
color: var(--kp-rarity-3);
border-color: var(--kp-rarity-3);
}
.rarity-4 {
color: var(--kp-rarity-4);
border-color: var(--kp-rarity-4);
}
.rarity-5 {
color: var(--kp-rarity-5);
border-color: var(--kp-rarity-5);
-unity-font-style: bold;
}
+31 -1
View File
@@ -130,6 +130,8 @@ GameObject:
- component: {fileID: 2124216809} - component: {fileID: 2124216809}
- component: {fileID: 2124216808} - component: {fileID: 2124216808}
- component: {fileID: 2124216807} - component: {fileID: 2124216807}
- component: {fileID: 2124216811}
- component: {fileID: 2124216810}
m_Layer: 0 m_Layer: 0
m_Name: Main Camera m_Name: Main Camera
m_TagString: MainCamera m_TagString: MainCamera
@@ -155,7 +157,7 @@ Camera:
m_Enabled: 1 m_Enabled: 1
serializedVersion: 2 serializedVersion: 2
m_ClearFlags: 1 m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} m_BackGroundColor: {r: 0.011764706, g: 0.023529412, b: 0.019607844, a: 1}
m_projectionMatrixMode: 1 m_projectionMatrixMode: 1
m_GateFitMode: 2 m_GateFitMode: 2
m_FOVAxisMode: 0 m_FOVAxisMode: 0
@@ -211,6 +213,34 @@ Transform:
m_Children: [] m_Children: []
m_Father: {fileID: 0} m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &2124216810
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2124216806}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d5b4ca12d26e4e54962ab9f6658e72d2, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2124216811
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2124216806}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier:
m_PanelSettings: {fileID: 11400000, guid: 7d68bd7978394c8295debbd403a39d80, type: 2}
m_ParentUI: {fileID: 0}
sourceAsset: {fileID: 0}
m_SortingOrder: 0
--- !u!1660057539 &9223372036854775807 --- !u!1660057539 &9223372036854775807
SceneRoots: SceneRoots:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
+6
View File
@@ -11,6 +11,7 @@ namespace KernelPanic.Core
{ {
public const string MainMenuSceneName = "MainMenuScene"; public const string MainMenuSceneName = "MainMenuScene";
public const string GameSceneName = "GameScene"; public const string GameSceneName = "GameScene";
public const string GamblingSceneName = "GamblingScene";
private const string CurtainClassName = "scene-fade-curtain"; private const string CurtainClassName = "scene-fade-curtain";
private const string CurtainVisibleClassName = "scene-fade-visible"; private const string CurtainVisibleClassName = "scene-fade-visible";
@@ -26,6 +27,11 @@ namespace KernelPanic.Core
FadeThenLoad(fadeRoot, GameSceneName); FadeThenLoad(fadeRoot, GameSceneName);
} }
public static void LoadGambling(VisualElement fadeRoot)
{
FadeThenLoad(fadeRoot, GamblingSceneName);
}
private static void FadeThenLoad(VisualElement fadeRoot, string sceneName) private static void FadeThenLoad(VisualElement fadeRoot, string sceneName)
{ {
if (fadeRoot == null) if (fadeRoot == null)
+189
View File
@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using KernelPanic.Data;
using KernelPanic.Core;
namespace KernelPanic.Meta
{
/// <summary>
/// Transient scene-transition handoff for gacha pull cutscenes.
/// </summary>
public static class GachaPullContext
{
private static PendingGachaPull pendingPull;
private static CompletedGachaPull completedPull;
public static bool HasPendingPull => pendingPull != null;
public static bool HasCompletedPull => completedPull != null;
public static void SetPending(string bannerId, int pullCount, int entropyTokenCount, DistroDatabase distroDatabase, string focusUnitId)
{
pendingPull = new PendingGachaPull(bannerId, pullCount, entropyTokenCount, distroDatabase, focusUnitId);
completedPull = null;
}
public static bool TryConsumePending(out PendingGachaPull pull)
{
pull = pendingPull;
pendingPull = null;
return pull != null;
}
public static void SetCompleted(CompletedGachaPull pull)
{
completedPull = pull;
}
public static bool TryConsumeCompleted(out CompletedGachaPull pull)
{
pull = completedPull;
completedPull = null;
return pull != null;
}
}
public sealed class PendingGachaPull
{
public PendingGachaPull(string bannerId, int pullCount, int entropyTokenCount, DistroDatabase distroDatabase, string focusUnitId)
{
BannerId = string.IsNullOrWhiteSpace(bannerId) ? GachaService.BeginnerBannerId : bannerId;
PullCount = Math.Max(1, pullCount);
EntropyTokenCount = Math.Max(0, entropyTokenCount);
DistroDatabase = distroDatabase;
FocusUnitId = focusUnitId;
}
public string BannerId { get; }
public int PullCount { get; }
public int EntropyTokenCount { get; }
public DistroDatabase DistroDatabase { get; }
public string FocusUnitId { get; }
}
public sealed class CompletedGachaPull
{
public CompletedGachaPull(string bannerId, string headerText, IReadOnlyList<CompletedGachaReward> rewards)
{
BannerId = string.IsNullOrWhiteSpace(bannerId) ? GachaService.BeginnerBannerId : bannerId;
HeaderText = headerText;
Rewards = rewards ?? Array.Empty<CompletedGachaReward>();
}
public string BannerId { get; }
public string HeaderText { get; }
public IReadOnlyList<CompletedGachaReward> Rewards { get; }
public IReadOnlyList<string> RewardLines
{
get
{
List<string> lines = new();
for (int i = 0; i < Rewards.Count; i++)
{
CompletedGachaReward reward = Rewards[i];
string status = string.IsNullOrWhiteSpace(reward.StatusText) ? string.Empty : $" {reward.StatusText}";
string languages = string.IsNullOrWhiteSpace(reward.LanguageUnlockText) ? string.Empty : $" {reward.LanguageUnlockText}";
lines.Add($"{reward.Index:00}: {reward.Stars}★ {reward.DisplayName} [{reward.TypeText}]{status}{languages}");
}
return lines;
}
}
public IReadOnlyList<int> RewardStars
{
get
{
List<int> stars = new();
for (int i = 0; i < Rewards.Count; i++)
{
stars.Add(Rewards[i].Stars);
}
return stars;
}
}
}
public sealed class CompletedGachaReward
{
public CompletedGachaReward(int index, int stars, string displayName, GachaRewardType rewardType, PullOutcomeKind outcomeKind, int mergesAwarded, IReadOnlyList<Language> languagesUnlocked, bool guaranteed, bool pityTriggered)
{
Index = Math.Max(1, index);
Stars = Math.Max(3, stars);
RewardType = rewardType;
DisplayName = NormalizeDisplayName(displayName, rewardType);
OutcomeKind = outcomeKind;
MergesAwarded = Math.Max(0, mergesAwarded);
LanguagesUnlocked = languagesUnlocked ?? Array.Empty<Language>();
Guaranteed = guaranteed;
PityTriggered = pityTriggered;
}
public int Index { get; }
public int Stars { get; }
public string DisplayName { get; }
public GachaRewardType RewardType { get; }
public PullOutcomeKind OutcomeKind { get; }
public int MergesAwarded { get; }
public IReadOnlyList<Language> LanguagesUnlocked { get; }
public bool Guaranteed { get; }
public bool PityTriggered { get; }
public bool IsCharacter => RewardType == GachaRewardType.Distro || RewardType == GachaRewardType.FutureStandardFiveStarDistro;
public string TypeText => IsCharacter ? "character" : "equipment";
public string StatusText
{
get
{
return OutcomeKind switch
{
PullOutcomeKind.Granted => "NEW",
PullOutcomeKind.Dupe => $"→ {MergesAwarded} merges",
PullOutcomeKind.DupeOverflow => $"→ {MergesAwarded} merges",
_ => Guaranteed ? "guaranteed" : PityTriggered ? "pity" : string.Empty
};
}
}
public string LanguageUnlockText
{
get
{
if (LanguagesUnlocked.Count == 0)
{
return string.Empty;
}
List<string> names = new();
for (int i = 0; i < LanguagesUnlocked.Count; i++)
{
names.Add(LanguagesUnlocked[i].ToString());
}
return $"unlocks {string.Join(", ", names)}";
}
}
private static string NormalizeDisplayName(string displayName, GachaRewardType rewardType)
{
if (string.IsNullOrWhiteSpace(displayName))
{
return "unknown result";
}
if (rewardType != GachaRewardType.Equipment)
{
return displayName;
}
string trimmed = displayName.Trim();
return trimmed.StartsWith("3-star ", StringComparison.OrdinalIgnoreCase) ||
trimmed.StartsWith("4-star ", StringComparison.OrdinalIgnoreCase) ||
trimmed.StartsWith("5-star ", StringComparison.OrdinalIgnoreCase)
? trimmed.Substring(7)
: trimmed;
}
}
}
+2 -2
View File
@@ -6,8 +6,8 @@ namespace KernelPanic.Meta
public static class GachaTuning public static class GachaTuning
{ {
public const int MaxVersion = 5; public const int MaxVersion = 5;
public const int MergesPerDupe = 20; public const int MergesPerDupe = 50;
public const int MergesOnCharacterBonus = 10; public const int MergesOnCharacterBonus = 25;
public const float MergesMaxVersionOverflowMultiplier = 1.5f; public const float MergesMaxVersionOverflowMultiplier = 1.5f;
public static int DupeConsolationBandwidth => 0; public static int DupeConsolationBandwidth => 0;
public static readonly int[] VersionCosts = { 50, 120, 250, 500 }; public static readonly int[] VersionCosts = { 50, 120, 250, 500 };
@@ -21,6 +21,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 FilledCycle = "●"; private const string FilledCycle = "●";
private const string EmptyCycle = "○"; private const string EmptyCycle = "○";
@@ -218,6 +219,12 @@ namespace KernelPanic.UI
private void LoadStyles() private void LoadStyles()
{ {
StyleSheet rarityStyleSheet = Resources.Load<StyleSheet>(SharedRarityStyleResourcePath);
if (rarityStyleSheet != null)
{
root.styleSheets.Add(rarityStyleSheet);
}
StyleSheet styleSheet = Resources.Load<StyleSheet>(StyleResourcePath); StyleSheet styleSheet = Resources.Load<StyleSheet>(StyleResourcePath);
if (styleSheet != null) if (styleSheet != null)
{ {
+49 -2
View File
@@ -34,6 +34,7 @@ namespace KernelPanic.UI
private Func<IReadOnlyList<DistroDefinition>, PullResolutionResult> _resolvePulledDistros; private Func<IReadOnlyList<DistroDefinition>, PullResolutionResult> _resolvePulledDistros;
private Action _onChanged; private Action _onChanged;
private Action _requestRootCreditExchange; private Action _requestRootCreditExchange;
private Action<string, int, int> _requestPullCutscene;
private int _selectedBannerIndex; private int _selectedBannerIndex;
private int _pendingPullCount; private int _pendingPullCount;
private int _pendingMissingTokens; private int _pendingMissingTokens;
@@ -44,7 +45,7 @@ namespace KernelPanic.UI
private readonly List<int> _resultRewardStars = new(); private readonly List<int> _resultRewardStars = new();
private bool _resultRevealed; private bool _resultRevealed;
public void Bind(VisualElement root, DistroDatabase database, FontAsset artFont, GachaService service, PlayerCollection collection, EntropyWallet entropyWallet, Func<IReadOnlyList<DistroDefinition>, PullResolutionResult> pullResolver, 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, Action<string, int, int> pullCutsceneCallback)
{ {
_distroDatabase = database; _distroDatabase = database;
_monospaceFont = artFont; _monospaceFont = artFont;
@@ -54,6 +55,7 @@ namespace KernelPanic.UI
_resolvePulledDistros = pullResolver; _resolvePulledDistros = pullResolver;
_onChanged = changedCallback; _onChanged = changedCallback;
_requestRootCreditExchange = rootCreditExchangeCallback; _requestRootCreditExchange = rootCreditExchangeCallback;
_requestPullCutscene = pullCutsceneCallback;
_bannerList = root.Q<VisualElement>("GachaBannerList"); _bannerList = root.Q<VisualElement>("GachaBannerList");
_bannerDetail = root.Q<VisualElement>("GachaBannerDetail"); _bannerDetail = root.Q<VisualElement>("GachaBannerDetail");
} }
@@ -232,6 +234,12 @@ namespace KernelPanic.UI
private void PullSelectedBanner(int pullCount, int entropyTokenCount) private void PullSelectedBanner(int pullCount, int entropyTokenCount)
{ {
string bannerId = _bannerIds[_selectedBannerIndex]; string bannerId = _bannerIds[_selectedBannerIndex];
if (_requestPullCutscene != null)
{
_requestPullCutscene.Invoke(bannerId, pullCount, entropyTokenCount);
return;
}
if (bannerId != GachaService.BeginnerBannerId) if (bannerId != GachaService.BeginnerBannerId)
{ {
_resultBannerId = bannerId; _resultBannerId = bannerId;
@@ -285,7 +293,7 @@ namespace KernelPanic.UI
string suffix = reward.Guaranteed ? " guaranteed" : reward.PityTriggered ? " pity" : string.Empty; string suffix = reward.Guaranteed ? " guaranteed" : reward.PityTriggered ? " pity" : string.Empty;
string outcomeText = FormatPullOutcome(outcome); string outcomeText = FormatPullOutcome(outcome);
_resultRewardLines.Add($"{i + 1:00}: {reward.StarRating}-star {reward.DisplayName}{suffix}{outcomeText}"); _resultRewardLines.Add($"{i + 1:00}: {reward.StarRating}★ {FormatRewardDisplayName(reward)}{suffix}{outcomeText}");
_resultRewardStars.Add(reward.StarRating); _resultRewardStars.Add(reward.StarRating);
} }
@@ -296,6 +304,30 @@ namespace KernelPanic.UI
Refresh(); Refresh();
} }
public void SetCompletedPullResult(CompletedGachaPull result)
{
if (result == null)
{
return;
}
_resultBannerId = result.BannerId;
_resultText = result.HeaderText;
_resultRewardLines.Clear();
_resultRewardStars.Clear();
for (int i = 0; i < result.RewardLines.Count; i++)
{
_resultRewardLines.Add(result.RewardLines[i]);
}
for (int i = 0; i < result.RewardStars.Count; i++)
{
_resultRewardStars.Add(result.RewardStars[i]);
}
_resultRevealed = true;
}
private VisualElement BuildPullResultReveal() private VisualElement BuildPullResultReveal()
{ {
VisualElement container = new(); VisualElement container = new();
@@ -385,6 +417,21 @@ namespace KernelPanic.UI
}; };
} }
private static string FormatRewardDisplayName(GachaReward reward)
{
if (reward.RewardType != GachaRewardType.Equipment || string.IsNullOrWhiteSpace(reward.DisplayName))
{
return reward.DisplayName;
}
string trimmed = reward.DisplayName.Trim();
return trimmed.StartsWith("3-star ", StringComparison.OrdinalIgnoreCase) ||
trimmed.StartsWith("4-star ", StringComparison.OrdinalIgnoreCase) ||
trimmed.StartsWith("5-star ", StringComparison.OrdinalIgnoreCase)
? trimmed.Substring(7)
: trimmed;
}
private void RequestPullSelectedBanner(int pullCount) private void RequestPullSelectedBanner(int pullCount)
{ {
string bannerId = _bannerIds[_selectedBannerIndex]; string bannerId = _bannerIds[_selectedBannerIndex];
@@ -0,0 +1,848 @@
using System;
using System.Collections;
using System.Collections.Generic;
using KernelPanic.Core;
using KernelPanic.Data;
using KernelPanic.Meta;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;
namespace KernelPanic.UI
{
/// <summary>
/// UI Toolkit gacha pull cutscene rendered by GamblingScene.
/// </summary>
[RequireComponent(typeof(UIDocument))]
public sealed class GamblingSceneController : MonoBehaviour
{
private const string GamblingStyleResourcePath = "GamblingScene";
private const string RarityStyleResourcePath = "RarityPresentation";
private const string SharedScrollbarStyleResourcePath = "TerminalScrollbars";
private const int FlourishMs = 420;
private const int CommonRevealMs = 150;
private const int RareRevealMs = 240;
private const int LegendaryRevealMs = 420;
private const int RareAnticipationMs = 520;
private const int LegendaryAnticipationMs = 820;
private const int RareSparkCount = 6;
private const int LegendarySparkCount = 18;
private const int SparkLifetimeMs = 520;
private const int PopClassMs = 280;
private const int LegendaryShimmerMs = 480;
private const int SummaryTickSteps = 8;
private const int SummaryTickMs = 220;
private readonly List<CompletedGachaReward> rewards = new();
private readonly List<PullCardView> cardViews = new();
private UIDocument document;
private VisualElement root;
private Label commandLabel;
private Label costLabel;
private Label flourishLabel;
private Label skipHint;
private VisualElement resultGrid;
private VisualElement effectLayer;
private VisualElement goldShimmer;
private VisualElement summaryPanel;
private Label summaryTotalsLabel;
private Button continueButton;
private string completedHeader;
private bool skipRequested;
private bool revealComplete;
private bool failed;
private void Awake()
{
document = GetComponent<UIDocument>();
root = document.rootVisualElement;
root.Clear();
root.focusable = true;
root.AddToClassList("gambling-root");
root.EnableInClassList("reduced-motion", UIPreferences.ReducedMotion);
LoadStyles();
BuildLayout();
root.RegisterCallback<KeyDownEvent>(HandleKeyDown);
root.RegisterCallback<PointerDownEvent>(HandlePointerDown);
}
private void OnDestroy()
{
if (root == null)
{
return;
}
root.UnregisterCallback<KeyDownEvent>(HandleKeyDown);
root.UnregisterCallback<PointerDownEvent>(HandlePointerDown);
}
private void Start()
{
root.Focus();
StartCoroutine(RunCutscene());
}
private void LoadStyles()
{
AddStyle(RarityStyleResourcePath);
AddStyle(GamblingStyleResourcePath);
AddStyle(SharedScrollbarStyleResourcePath);
}
private void AddStyle(string resourcePath)
{
StyleSheet styleSheet = Resources.Load<StyleSheet>(resourcePath);
if (styleSheet != null)
{
root.styleSheets.Add(styleSheet);
}
}
private void BuildLayout()
{
VisualElement shell = new();
shell.AddToClassList("gambling-shell");
root.Add(shell);
VisualElement frame = new();
frame.AddToClassList("gambling-frame");
shell.Add(frame);
VisualElement header = new();
header.AddToClassList("gambling-header");
frame.Add(header);
VisualElement titleBlock = new();
titleBlock.AddToClassList("gambling-title-block");
header.Add(titleBlock);
commandLabel = new("git pull");
commandLabel.AddToClassList("gambling-command");
titleBlock.Add(commandLabel);
costLabel = new();
costLabel.AddToClassList("gambling-cost");
titleBlock.Add(costLabel);
skipHint = new("[tap/key] reveal all");
skipHint.AddToClassList("gambling-skip");
header.Add(skipHint);
flourishLabel = new();
flourishLabel.AddToClassList("gambling-flourish");
frame.Add(flourishLabel);
resultGrid = new VisualElement();
resultGrid.AddToClassList("gambling-results");
frame.Add(resultGrid);
goldShimmer = new VisualElement();
goldShimmer.AddToClassList("gold-shimmer");
goldShimmer.pickingMode = PickingMode.Ignore;
frame.Add(goldShimmer);
effectLayer = new VisualElement();
effectLayer.AddToClassList("effect-layer");
effectLayer.pickingMode = PickingMode.Ignore;
frame.Add(effectLayer);
summaryPanel = new VisualElement();
summaryPanel.AddToClassList("summary-panel");
summaryPanel.AddToClassList("hidden");
frame.Add(summaryPanel);
continueButton = new Button(ContinueToMenu) { text = "> continue" };
continueButton.AddToClassList("continue-button");
continueButton.AddToClassList("continue-button-pending");
continueButton.SetEnabled(false);
frame.Add(continueButton);
}
private IEnumerator RunCutscene()
{
if (!GachaPullContext.TryConsumePending(out PendingGachaPull pending))
{
failed = true;
commandLabel.text = "git pull failed";
costLabel.text = "no pending pull";
ShowContinue();
yield break;
}
commandLabel.text = pending.PullCount == 10 ? "git pull --ten" : "git pull --single";
int pullCost = pending.PullCount == 10 ? GachaService.BeginnerTenPullCost : GachaService.BeginnerSinglePullCost;
costLabel.text = pending.EntropyTokenCount > 0
? $"cost: {pullCost} Commits / entropy fallback: {pending.EntropyTokenCount * GachaService.EntropyPerPullToken}"
: $"cost: {pullCost} Commits";
flourishLabel.text = "$ fetch-pack | verify | unpack";
CompletedGachaPull completed = ResolvePull(pending);
GachaPullContext.SetCompleted(completed);
completedHeader = completed.HeaderText;
rewards.Clear();
rewards.AddRange(completed.Rewards);
if (rewards.Count == 0)
{
failed = true;
costLabel.text = completed.HeaderText;
ShowContinue();
yield break;
}
BuildRows();
if (UIPreferences.ReducedMotion)
{
RevealAll();
yield break;
}
yield return WaitOrSkip(FlourishMs);
flourishLabel.text = "$ unpacking sealed objects";
for (int i = 0; i < rewards.Count; i++)
{
if (skipRequested)
{
RevealAll();
yield break;
}
CompletedGachaReward reward = rewards[i];
PullCardView card = cardViews[i];
int holdMs = GetAnticipationMs(reward.Stars);
if (holdMs > 0)
{
ApplyGridFocus(i, reward.Stars);
card.ShowHold(reward.Stars >= 5 ? "........ signed tag" : "... promoted object", reward.Stars);
yield return WaitOrSkip(holdMs);
if (skipRequested)
{
RevealAll();
yield break;
}
card.HideHold();
}
card.Reveal(allowFlash: reward.Stars >= 4, persistentMotion: true);
PlayRevealJuice(card, reward);
yield return WaitOrSkip(GetRevealMs(reward.Stars));
ClearGridFocus();
}
CompleteReveal();
}
private CompletedGachaPull ResolvePull(PendingGachaPull pending)
{
if (pending.DistroDatabase == null)
{
return new CompletedGachaPull(pending.BannerId, "git pull failed: distro database unavailable", Array.Empty<CompletedGachaReward>());
}
SaveService saveService = new();
SaveData saveData = saveService.Load();
saveData.EnsureLists();
EntropyWallet wallet = new();
wallet.SetBalance(saveData.entropyBalance);
PlayerCollection collection = BuildCollection(saveData, pending.DistroDatabase);
GachaService gacha = new();
RestoreBannerPool(gacha, saveData, pending.DistroDatabase);
gacha.LoadProgress(saveData);
GachaPullResult result = pending.BannerId == GachaService.BeginnerBannerId
? gacha.PerformBeginnerPull(pending.PullCount, wallet, pending.EntropyTokenCount)
: GachaPullResult.Failed(pending.BannerId, "remote not implemented yet");
if (!result.Success)
{
return new CompletedGachaPull(pending.BannerId, $"git pull failed: {result.FailureReason}", Array.Empty<CompletedGachaReward>());
}
List<string> header = new()
{
$"spent {result.CurrencySpent} {GachaService.FormatCurrencyName(result.CurrencyType)}"
};
if (result.EntropySpent > 0)
{
header.Add($"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 = PullResolver.Resolve(distroRewards, new PullResolutionContext(saveData, collection, pending.DistroDatabase, pending.FocusUnitId));
List<CompletedGachaReward> completedRewards = new();
int distroOutcomeIndex = 0;
for (int i = 0; i < result.Rewards.Count; i++)
{
GachaReward reward = result.Rewards[i];
PullResolutionOutcome? outcome = null;
if (reward.Distro != null && resolution != null && distroOutcomeIndex < resolution.Outcomes.Count)
{
outcome = resolution.Outcomes[distroOutcomeIndex];
distroOutcomeIndex++;
}
PullOutcomeKind outcomeKind = outcome?.Kind ?? PullOutcomeKind.Invalid;
int merges = outcome?.MergesAwarded ?? 0;
IReadOnlyList<Language> languages = outcome?.LanguagesNewlyUnlocked ?? Array.Empty<Language>();
completedRewards.Add(new CompletedGachaReward(i + 1, reward.StarRating, reward.DisplayName, reward.RewardType, outcomeKind, merges, languages, reward.Guaranteed, reward.PityTriggered));
}
WriteState(saveService, saveData, wallet, collection, gacha);
return new CompletedGachaPull(result.BannerId, string.Join(" / ", header), completedRewards);
}
private void BuildRows()
{
resultGrid.Clear();
cardViews.Clear();
for (int i = 0; i < rewards.Count; i++)
{
PullCardView card = new(rewards[i]);
cardViews.Add(card);
resultGrid.Add(card.Root);
}
}
private void RevealAll()
{
skipRequested = true;
ClearGridFocus();
goldShimmer.RemoveFromClassList("gold-shimmer-on");
for (int i = 0; i < cardViews.Count; i++)
{
cardViews[i].HideHold();
cardViews[i].Reveal(allowFlash: false, persistentMotion: !UIPreferences.ReducedMotion);
}
CompleteReveal();
}
private void CompleteReveal()
{
if (revealComplete)
{
return;
}
revealComplete = true;
skipHint.text = "complete";
flourishLabel.text = completedHeader;
flourishLabel.AddToClassList("gambling-flourish-complete");
BuildSummary(animate: !skipRequested && !UIPreferences.ReducedMotion);
ShowContinue();
}
private void BuildSummary(bool animate)
{
summaryPanel.Clear();
summaryPanel.RemoveFromClassList("hidden");
summaryTotalsLabel = new Label { name = "PullSummaryTotals" };
summaryTotalsLabel.AddToClassList("summary-totals");
summaryPanel.Add(summaryTotalsLabel);
SummaryTotals totals = CalculateTotals();
if (!animate)
{
summaryTotalsLabel.text = FormatTotals(totals.Units, totals.Merges, totals.LanguagesText);
return;
}
summaryTotalsLabel.AddToClassList("summary-tick");
for (int step = 0; step <= SummaryTickSteps; step++)
{
int capturedStep = step;
int delay = Mathf.RoundToInt((SummaryTickMs / (float)SummaryTickSteps) * capturedStep);
summaryTotalsLabel.schedule.Execute(() =>
{
float t = capturedStep / (float)SummaryTickSteps;
int units = Mathf.RoundToInt(Mathf.Lerp(0, totals.Units, t));
int merges = Mathf.RoundToInt(Mathf.Lerp(0, totals.Merges, t));
summaryTotalsLabel.text = FormatTotals(units, merges, capturedStep == SummaryTickSteps ? totals.LanguagesText : "...");
if (capturedStep == SummaryTickSteps)
{
summaryTotalsLabel.RemoveFromClassList("summary-tick");
}
}).StartingIn(delay);
}
}
private SummaryTotals CalculateTotals()
{
int units = 0;
int merges = 0;
HashSet<Language> languages = new();
for (int i = 0; i < rewards.Count; i++)
{
if (rewards[i].OutcomeKind == PullOutcomeKind.Granted && rewards[i].IsCharacter)
{
units++;
}
merges += rewards[i].MergesAwarded;
for (int languageIndex = 0; languageIndex < rewards[i].LanguagesUnlocked.Count; languageIndex++)
{
languages.Add(rewards[i].LanguagesUnlocked[languageIndex]);
}
}
return new SummaryTotals(units, merges, FormatLanguages(languages));
}
private static string FormatTotals(int units, int merges, string languagesText)
{
string unitText = units == 0 ? "none" : units.ToString();
string mergeText = merges == 0 ? "none" : merges.ToString();
return $"units gained: {unitText} merges: {mergeText} languages unlocked: {languagesText}";
}
private static string FormatLanguages(HashSet<Language> languages)
{
if (languages.Count == 0)
{
return "none";
}
List<string> names = new();
foreach (Language language in languages)
{
names.Add(language.ToString());
}
names.Sort(StringComparer.OrdinalIgnoreCase);
return string.Join(", ", names);
}
private static PlayerCollection BuildCollection(SaveData saveData, DistroDatabase distroDatabase)
{
PlayerCollection collection = new();
for (int i = 0; i < saveData.ownedUnits.Count; i++)
{
OwnedUnitSaveEntry entry = saveData.ownedUnits[i];
DistroDefinition unit = entry == null ? null : distroDatabase.FindById(entry.id);
if (unit != null)
{
collection.AddSilently(unit, entry.version);
}
}
return collection;
}
private static void RestoreBannerPool(GachaService gacha, SaveData saveData, DistroDatabase distroDatabase)
{
for (int i = 0; i < saveData.bannerPoolIds.Count; i++)
{
DistroDefinition unit = distroDatabase.FindById(saveData.bannerPoolIds[i]);
if (unit != null)
{
gacha.AddToBannerPool(unit);
}
}
if (saveData.starterChosen && gacha.BannerPool.Count == 0)
{
IReadOnlyList<DistroDefinition> distros = distroDatabase.AllDistros;
for (int i = 0; i < distros.Count; i++)
{
gacha.AddToBannerPool(distros[i]);
}
}
}
private static void WriteState(SaveService saveService, SaveData saveData, EntropyWallet wallet, PlayerCollection collection, GachaService gacha)
{
Dictionary<string, int> mergeBalances = new(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < saveData.ownedUnits.Count; i++)
{
OwnedUnitSaveEntry entry = saveData.ownedUnits[i];
if (entry != null && !string.IsNullOrWhiteSpace(entry.id))
{
mergeBalances[entry.id] = Math.Max(0, entry.merges);
}
}
saveData.entropyBalance = wallet.Balance;
saveData.ownedUnits.Clear();
saveData.ownedUnitIds.Clear();
saveData.bannerPoolIds.Clear();
gacha.WriteProgress(saveData);
for (int i = 0; i < collection.OwnedUnits.Count; i++)
{
DistroDefinition unit = collection.OwnedUnits[i];
if (unit != null && !string.IsNullOrWhiteSpace(unit.Id))
{
saveData.ownedUnits.Add(new OwnedUnitSaveEntry
{
id = unit.Id,
version = Mathf.Clamp(collection.GetVersion(unit.Id), 1, GachaTuning.MaxVersion),
merges = mergeBalances.TryGetValue(unit.Id, out int merges) ? merges : 0
});
}
}
for (int i = 0; i < gacha.BannerPool.Count; i++)
{
DistroDefinition unit = gacha.BannerPool[i];
if (unit != null && !string.IsNullOrWhiteSpace(unit.Id))
{
saveData.bannerPoolIds.Add(unit.Id);
}
}
saveService.Save(saveData);
}
private IEnumerator WaitOrSkip(int milliseconds)
{
float elapsed = 0f;
float duration = Mathf.Max(0f, milliseconds / 1000f);
while (!skipRequested && elapsed < duration)
{
elapsed += Time.deltaTime;
yield return null;
}
}
private static int GetAnticipationMs(int stars)
{
return stars >= 5 ? LegendaryAnticipationMs : stars >= 4 ? RareAnticipationMs : 0;
}
private static int GetRevealMs(int stars)
{
return stars >= 5 ? LegendaryRevealMs : stars >= 4 ? RareRevealMs : CommonRevealMs;
}
private void ApplyGridFocus(int focusedIndex, int stars)
{
if (UIPreferences.ReducedMotion || stars < 5)
{
return;
}
for (int i = 0; i < cardViews.Count; i++)
{
cardViews[i].SetDimmed(i != focusedIndex);
}
}
private void ClearGridFocus()
{
for (int i = 0; i < cardViews.Count; i++)
{
cardViews[i].SetDimmed(false);
}
}
private void PlayRevealJuice(PullCardView card, CompletedGachaReward reward)
{
if (UIPreferences.ReducedMotion)
{
return;
}
if (reward.Stars >= 5)
{
card.PlayTemporaryClass("pull-card-legendary-pop", PopClassMs);
PlayGoldShimmer(card.Root);
EmitSparks(card.Root, RarityPresentation.ForStars(reward.Stars).Color, LegendarySparkCount, 76f);
}
else if (reward.Stars >= 4)
{
card.PlayTemporaryClass("pull-card-rare-pop", PopClassMs);
EmitSparks(card.Root, RarityPresentation.ForStars(reward.Stars).Color, RareSparkCount, 44f);
}
if (reward.OutcomeKind == PullOutcomeKind.Granted)
{
card.PulseStatus(PopClassMs);
}
}
private void PlayGoldShimmer(VisualElement source)
{
if (source == null || goldShimmer?.parent == null)
{
return;
}
Rect sourceBounds = source.worldBound;
Rect parentBounds = goldShimmer.parent.worldBound;
float left = Mathf.Max(0f, sourceBounds.x - parentBounds.x);
float top = Mathf.Max(0f, sourceBounds.y - parentBounds.y);
goldShimmer.style.left = left;
goldShimmer.style.top = top;
goldShimmer.style.width = sourceBounds.width;
goldShimmer.style.height = sourceBounds.height;
goldShimmer.AddToClassList("gold-shimmer-on");
goldShimmer.schedule.Execute(() => goldShimmer.RemoveFromClassList("gold-shimmer-on")).StartingIn(LegendaryShimmerMs);
}
private void EmitSparks(VisualElement source, Color color, int count, float radius)
{
if (effectLayer == null || source == null)
{
return;
}
Rect sourceBounds = source.worldBound;
Rect layerBounds = effectLayer.worldBound;
float centerX = sourceBounds.center.x - layerBounds.x;
float centerY = sourceBounds.center.y - layerBounds.y;
float halfWidth = sourceBounds.width * 0.5f;
float halfHeight = sourceBounds.height * 0.5f;
for (int i = 0; i < count; i++)
{
VisualElement spark = CreateSpark();
float angle = (Mathf.PI * 2f * i / count) + UnityEngine.Random.Range(-0.18f, 0.18f);
float dirX = Mathf.Cos(angle);
float dirY = Mathf.Sin(angle);
float edgeDistanceX = Mathf.Approximately(dirX, 0f) ? float.MaxValue : halfWidth / Mathf.Abs(dirX);
float edgeDistanceY = Mathf.Approximately(dirY, 0f) ? float.MaxValue : halfHeight / Mathf.Abs(dirY);
float edgeDistance = Mathf.Min(edgeDistanceX, edgeDistanceY);
float startX = centerX + (dirX * (edgeDistance + 10f)) - 4f;
float startY = centerY + (dirY * (edgeDistance + 10f)) - 4f;
float distance = UnityEngine.Random.Range(radius * 0.55f, radius);
float targetX = startX + (dirX * distance);
float targetY = startY + (dirY * distance);
spark.style.backgroundColor = color;
spark.style.left = startX;
spark.style.top = startY;
spark.style.opacity = 1f;
spark.style.scale = new Scale(new Vector2(1.35f, 1.35f));
spark.RemoveFromClassList("spark-active");
spark.schedule.Execute(() =>
{
spark.AddToClassList("spark-active");
spark.style.left = targetX;
spark.style.top = targetY;
spark.style.opacity = 0f;
spark.style.scale = new Scale(new Vector2(0.25f, 0.25f));
}).StartingIn(35);
spark.schedule.Execute(() =>
{
spark.RemoveFromHierarchy();
}).StartingIn(SparkLifetimeMs);
}
}
private VisualElement CreateSpark()
{
VisualElement spark = new();
spark.pickingMode = PickingMode.Ignore;
spark.AddToClassList("spark");
effectLayer.Add(spark);
return spark;
}
private void HandleKeyDown(KeyDownEvent evt)
{
if (revealComplete)
{
ContinueToMenu();
evt.StopPropagation();
return;
}
skipRequested = true;
evt.StopPropagation();
}
private void HandlePointerDown(PointerDownEvent evt)
{
if (revealComplete)
{
return;
}
skipRequested = true;
evt.StopPropagation();
}
private void ShowContinue()
{
continueButton.SetEnabled(true);
continueButton.RemoveFromClassList("continue-button-pending");
if (failed)
{
skipHint.text = "failed";
}
}
private void ContinueToMenu()
{
SceneManager.LoadSceneAsync(SceneLoader.MainMenuSceneName);
}
private readonly struct SummaryTotals
{
public SummaryTotals(int units, int merges, string languagesText)
{
Units = units;
Merges = merges;
LanguagesText = languagesText;
}
public int Units { get; }
public int Merges { get; }
public string LanguagesText { get; }
}
private sealed class PullCardView
{
private readonly CompletedGachaReward reward;
private readonly Label rarityLabel;
private readonly Label nameLabel;
private readonly Label statusLabel;
private readonly Label typeIconLabel;
private IVisualElementScheduledItem glowPulse;
private bool glowOn;
public PullCardView(CompletedGachaReward reward)
{
this.reward = reward;
RarityStyle rarity = RarityPresentation.ForStars(reward.Stars);
Root = new VisualElement();
Root.AddToClassList("pull-card");
Root.AddToClassList("pull-card-hidden");
Root.AddToClassList(rarity.ClassName);
VisualElement top = new();
top.AddToClassList("pull-card-top");
Root.Add(top);
Label indexLabel = new($"{reward.Index:00}");
indexLabel.AddToClassList("pull-index");
top.Add(indexLabel);
rarityLabel = new(rarity.Stars);
rarityLabel.AddToClassList("pull-rarity");
rarityLabel.AddToClassList(rarity.ClassName);
top.Add(rarityLabel);
typeIconLabel = new(reward.IsCharacter ? "@" : "#");
typeIconLabel.tooltip = reward.TypeText;
typeIconLabel.AddToClassList("pull-type-icon");
top.Add(typeIconLabel);
nameLabel = new(reward.DisplayName);
nameLabel.AddToClassList("pull-name");
Root.Add(nameLabel);
statusLabel = new(reward.StatusText);
statusLabel.AddToClassList("pull-status");
statusLabel.EnableInClassList("pull-status-new", reward.OutcomeKind == PullOutcomeKind.Granted);
statusLabel.visible = !string.IsNullOrWhiteSpace(reward.StatusText);
if (statusLabel.visible)
{
Root.Add(statusLabel);
}
}
public VisualElement Root { get; }
public void ShowHold(string holdText, int stars)
{
Root.RemoveFromClassList("pull-card-hidden");
Root.AddToClassList("pull-card-held");
Root.EnableInClassList("pull-card-held-rare", stars == 4);
Root.EnableInClassList("pull-card-held-legendary", stars >= 5);
rarityLabel.text = "...";
nameLabel.text = holdText;
statusLabel.text = "";
statusLabel.visible = false;
typeIconLabel.visible = false;
}
public void HideHold()
{
Root.RemoveFromClassList("pull-card-held");
Root.RemoveFromClassList("pull-card-held-rare");
Root.RemoveFromClassList("pull-card-held-legendary");
}
public void Reveal(bool allowFlash, bool persistentMotion)
{
RarityStyle rarity = RarityPresentation.ForStars(reward.Stars);
Root.RemoveFromClassList("pull-card-hidden");
Root.RemoveFromClassList("pull-card-held");
Root.RemoveFromClassList("pull-card-held-rare");
Root.RemoveFromClassList("pull-card-held-legendary");
Root.AddToClassList("pull-card-revealed");
Root.EnableInClassList("pull-card-glow-4", persistentMotion && reward.Stars == 4);
Root.EnableInClassList("pull-card-glow-5", persistentMotion && reward.Stars >= 5);
SetPersistentGlow(persistentMotion && reward.Stars >= 5);
rarityLabel.text = reward.Stars >= 5 ? $"{rarity.Badge} {rarity.Stars}" : rarity.Stars;
nameLabel.text = reward.DisplayName;
typeIconLabel.text = reward.IsCharacter ? "@" : "#";
typeIconLabel.visible = true;
statusLabel.text = reward.StatusText;
statusLabel.visible = !string.IsNullOrWhiteSpace(reward.StatusText);
if (allowFlash)
{
Root.AddToClassList("pull-card-flash");
Root.schedule.Execute(() => Root.RemoveFromClassList("pull-card-flash")).StartingIn(PopClassMs);
}
}
public void SetDimmed(bool dimmed)
{
Root.EnableInClassList("pull-card-dimmed", dimmed);
}
public void PlayTemporaryClass(string className, int durationMs)
{
Root.AddToClassList(className);
Root.schedule.Execute(() => Root.RemoveFromClassList(className)).StartingIn(durationMs);
}
public void PulseStatus(int durationMs)
{
if (!statusLabel.visible)
{
return;
}
statusLabel.AddToClassList("pull-status-pop");
statusLabel.schedule.Execute(() => statusLabel.RemoveFromClassList("pull-status-pop")).StartingIn(durationMs);
}
private void SetPersistentGlow(bool enabled)
{
if (!enabled)
{
glowPulse?.Pause();
Root.RemoveFromClassList("pull-card-glow-on");
return;
}
if (glowPulse != null)
{
glowPulse.Resume();
return;
}
glowPulse = Root.schedule.Execute(() =>
{
glowOn = !glowOn;
Root.EnableInClassList("pull-card-glow-on", glowOn);
}).Every(900);
}
}
}
}
+94 -15
View File
@@ -72,6 +72,7 @@ namespace KernelPanic.UI
private const string ModalOpenClassName = "modal-open"; private const string ModalOpenClassName = "modal-open";
private const int ModalTransitionMs = 160; private const int ModalTransitionMs = 160;
private const string SharedScrollbarStyleResourcePath = "TerminalScrollbars"; private const string SharedScrollbarStyleResourcePath = "TerminalScrollbars";
private const string SharedRarityStyleResourcePath = "RarityPresentation";
private const float BootIntroSeconds = 1.5f; private const float BootIntroSeconds = 1.5f;
private static bool _bootIntroPlayed; private static bool _bootIntroPlayed;
@@ -98,6 +99,7 @@ namespace KernelPanic.UI
private readonly List<VisualElement> _packageRows = new(); private readonly List<VisualElement> _packageRows = new();
private readonly List<VisualElement> _loadoutRows = new(); private readonly List<VisualElement> _loadoutRows = new();
private readonly List<Language> _selectedRunLanguages = new(); private readonly List<Language> _selectedRunLanguages = new();
private readonly List<MenuScreen> _screenHistory = new();
private UIDocument _document; private UIDocument _document;
private VisualElement _root; private VisualElement _root;
private VisualElement _shellRoot; private VisualElement _shellRoot;
@@ -168,6 +170,7 @@ namespace KernelPanic.UI
private Action _rootCreditExchangePlusHundredClicked; private Action _rootCreditExchangePlusHundredClicked;
private IVisualElementScheduledItem _blinkSchedule; private IVisualElementScheduledItem _blinkSchedule;
private IVisualElementScheduledItem _bootIntroSchedule; private IVisualElementScheduledItem _bootIntroSchedule;
private MenuScreen _activeScreen = MenuScreen.Main;
public void Initialize(EntropyWallet initializedWallet) public void Initialize(EntropyWallet initializedWallet)
{ {
@@ -178,11 +181,7 @@ namespace KernelPanic.UI
private void Awake() private void Awake()
{ {
_document = GetComponent<UIDocument>(); _document = GetComponent<UIDocument>();
_saveService = new SaveService(); ComposeMetaServices();
_gachaService = new GachaService();
_playerCollection = new PlayerCollection(); // TODO: Replace with persistent player-collection service composition.
_cardLoadout = new CardLoadout(_playerCollection.OwnedUnits);
Initialize(new EntropyWallet()); // TODO: Replace with persistent wallet service composition.
BindElements(); BindElements();
LoadSharedStyles(); LoadSharedStyles();
BindCommandEntries(); BindCommandEntries();
@@ -192,13 +191,22 @@ namespace KernelPanic.UI
FontAsset resolvedFont = ResolveMonospaceFont(); FontAsset resolvedFont = ResolveMonospaceFont();
featuredUnitPanel.Bind(_root, resolvedFont); featuredUnitPanel.Bind(_root, resolvedFont);
collectionScreen.Bind(_root, resolvedFont, languageDeckDatabase, cardDatabase, _playerCollection, GetMergesBalance, UpgradeCollectionUnit); collectionScreen.Bind(_root, resolvedFont, languageDeckDatabase, cardDatabase, _playerCollection, GetMergesBalance, UpgradeCollectionUnit);
gachaScreen.Bind(_root, distroDatabase, resolvedFont, _gachaService, _playerCollection, _wallet, ResolvePulledDistros, HandleMetaStateChanged, OpenRootCreditExchange); gachaScreen.Bind(_root, distroDatabase, resolvedFont, _gachaService, _playerCollection, _wallet, ResolvePulledDistros, HandleMetaStateChanged, OpenRootCreditExchange, StartGachaPullCutscene);
LoadMetaState(); LoadMetaState();
_playerCollection.Changed += HandleMetaStateChanged; _playerCollection.Changed += HandleMetaStateChanged;
_gachaService.Changed += HandleMetaStateChanged; _gachaService.Changed += HandleMetaStateChanged;
ApplyOptionalFont(resolvedFont); ApplyOptionalFont(resolvedFont);
} }
private void ComposeMetaServices()
{
_saveService = new SaveService();
_wallet = new EntropyWallet();
_gachaService = new GachaService();
_playerCollection = new PlayerCollection();
_cardLoadout = new CardLoadout(_playerCollection.OwnedUnits);
}
private void OnEnable() private void OnEnable()
{ {
RegisterCallbacks(); RegisterCallbacks();
@@ -212,7 +220,16 @@ namespace KernelPanic.UI
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits); collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
gachaScreen.Refresh(); gachaScreen.Refresh();
SelectCommand(0); SelectCommand(0);
ShowMainMenu(); if (GachaPullContext.TryConsumeCompleted(out CompletedGachaPull completedPull))
{
gachaScreen.SetCompletedPullResult(completedPull);
ShowGacha();
}
else
{
ShowMainMenu();
}
StartAmbientSchedules(); StartAmbientSchedules();
PlayBootIntroIfNeeded(); PlayBootIntroIfNeeded();
} }
@@ -277,6 +294,12 @@ namespace KernelPanic.UI
private void LoadSharedStyles() private void LoadSharedStyles()
{ {
StyleSheet rarityStyleSheet = Resources.Load<StyleSheet>(SharedRarityStyleResourcePath);
if (rarityStyleSheet != null)
{
_root.styleSheets.Add(rarityStyleSheet);
}
StyleSheet scrollbarStyleSheet = Resources.Load<StyleSheet>(SharedScrollbarStyleResourcePath); StyleSheet scrollbarStyleSheet = Resources.Load<StyleSheet>(SharedScrollbarStyleResourcePath);
if (scrollbarStyleSheet != null) if (scrollbarStyleSheet != null)
{ {
@@ -765,6 +788,13 @@ namespace KernelPanic.UI
return PullResolver.Resolve(pulledDistros, context); return PullResolver.Resolve(pulledDistros, context);
} }
private void StartGachaPullCutscene(string bannerId, int pullCount, int entropyTokenCount)
{
SaveCurrentState();
GachaPullContext.SetPending(bannerId, pullCount, entropyTokenCount, distroDatabase, _playerCollection.FeaturedUnit?.Id);
SceneLoader.LoadGambling(_root);
}
private int GetMergesBalance(DistroDefinition unit) private int GetMergesBalance(DistroDefinition unit)
{ {
_saveData ??= SaveData.CreateDefault(); _saveData ??= SaveData.CreateDefault();
@@ -992,7 +1022,7 @@ namespace KernelPanic.UI
if (evt.keyCode == KeyCode.Escape && IsSubScreenVisible()) if (evt.keyCode == KeyCode.Escape && IsSubScreenVisible())
{ {
ShowMainMenu(); NavigateBack();
evt.StopPropagation(); evt.StopPropagation();
return; return;
} }
@@ -1669,7 +1699,7 @@ namespace KernelPanic.UI
private void HandleStartRunClicked() private void HandleStartRunClicked()
{ {
RefreshRunSetup(); RefreshRunSetup();
ShowPanel(_runSetupPanel); ShowScreen(MenuScreen.RunSetup);
} }
private void HandleQuitClicked() private void HandleQuitClicked()
@@ -1686,14 +1716,14 @@ namespace KernelPanic.UI
_cardLoadout.ClearAll(); _cardLoadout.ClearAll();
_selectedRunLanguages.Clear(); _selectedRunLanguages.Clear();
_runSetupNotice = null; _runSetupNotice = null;
ShowPanel(_mainMenuPanel); ShowScreen(MenuScreen.Main, false);
_root.Focus(); _root.Focus();
} }
private void ShowCollection() private void ShowCollection()
{ {
ShowCollectionUnits(); ShowCollectionUnits();
ShowPanel(_collectionPanel); ShowScreen(MenuScreen.Collection);
} }
private void HandleCollectionBack() private void HandleCollectionBack()
@@ -1745,16 +1775,56 @@ namespace KernelPanic.UI
private void ShowGacha() private void ShowGacha()
{ {
gachaScreen.Open(); gachaScreen.Open();
ShowPanel(_gachaPanel); ShowScreen(MenuScreen.Gacha);
} }
private void ShowSettings() private void ShowSettings()
{ {
ShowPanel(_settingsPanel); ShowScreen(MenuScreen.Settings);
} }
// TODO: Replace this direct toggle with a screen-stack/router when menu flows need history or transitions. private void NavigateBack()
private void ShowPanel(VisualElement activePanel) {
if (_screenHistory.Count == 0)
{
ShowMainMenu();
return;
}
MenuScreen previous = _screenHistory[_screenHistory.Count - 1];
_screenHistory.RemoveAt(_screenHistory.Count - 1);
ShowScreen(previous, false);
}
private void ShowScreen(MenuScreen screen, bool addToHistory = true)
{
if (screen == MenuScreen.Main)
{
_screenHistory.Clear();
}
else if (addToHistory && _activeScreen != screen)
{
_screenHistory.Add(_activeScreen);
}
_activeScreen = screen;
VisualElement activePanel = PanelForScreen(screen);
ApplyScreenVisibility(activePanel);
}
private VisualElement PanelForScreen(MenuScreen screen)
{
return screen switch
{
MenuScreen.Collection => _collectionPanel,
MenuScreen.RunSetup => _runSetupPanel,
MenuScreen.Gacha => _gachaPanel,
MenuScreen.Settings => _settingsPanel,
_ => _mainMenuPanel
};
}
private void ApplyScreenVisibility(VisualElement activePanel)
{ {
_mainMenuPanel.EnableInClassList(HiddenClassName, _mainMenuPanel != activePanel); _mainMenuPanel.EnableInClassList(HiddenClassName, _mainMenuPanel != activePanel);
_collectionPanel.EnableInClassList(HiddenClassName, _collectionPanel != activePanel); _collectionPanel.EnableInClassList(HiddenClassName, _collectionPanel != activePanel);
@@ -1777,6 +1847,15 @@ namespace KernelPanic.UI
RefreshCurrencyReadouts(); RefreshCurrencyReadouts();
} }
private enum MenuScreen
{
Main,
Collection,
RunSetup,
Gacha,
Settings
}
private sealed class CommandMenuEntry private sealed class CommandMenuEntry
{ {
private readonly Label _cursor; private readonly Label _cursor;
+39
View File
@@ -0,0 +1,39 @@
using UnityEngine;
namespace KernelPanic.UI
{
/// <summary>
/// Shared rarity presentation tokens for pull, shop, collection, and drop UIs.
/// USS classes are defined in Resources/RarityPresentation.uss.
/// </summary>
public static class RarityPresentation
{
public static RarityStyle ForStars(int stars)
{
return stars switch
{
>= 5 => new RarityStyle(5, "5★", "★★★★★", "rarity-5", new Color(1f, 0.75f, 0.25f)),
4 => new RarityStyle(4, "4★", "★★★★", "rarity-4", new Color(0.35f, 1f, 0.95f)),
_ => new RarityStyle(3, "3★", "★★★", "rarity-3", new Color(0.5f, 0.62f, 0.54f))
};
}
}
public readonly struct RarityStyle
{
public RarityStyle(int tier, string badge, string stars, string className, Color color)
{
Tier = tier;
Badge = badge;
Stars = stars;
ClassName = className;
Color = color;
}
public int Tier { get; }
public string Badge { get; }
public string Stars { get; }
public string ClassName { get; }
public Color Color { get; }
}
}
@@ -11,5 +11,8 @@ EditorBuildSettings:
- enabled: 1 - enabled: 1
path: Assets/Scenes/GameScene.unity path: Assets/Scenes/GameScene.unity
guid: e4259fa62d854e27af3cc2a10fb210c8 guid: e4259fa62d854e27af3cc2a10fb210c8
- enabled: 1
path: Assets/Scenes/GamblingScene.unity
guid: 761d33a16bc244544a2f119e1bd042bd
m_configObjects: m_configObjects:
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3}