Implement too many changes to list

This commit is contained in:
2026-07-07 11:46:31 +02:00
parent 8f66e83b65
commit 28486bff8c
50 changed files with 6344 additions and 1131 deletions
+6
View File
@@ -35,3 +35,9 @@ MonoBehaviour:
- {fileID: 11400000, guid: a7d40000000000000000000000000004, type: 2} - {fileID: 11400000, guid: a7d40000000000000000000000000004, type: 2}
- {fileID: 11400000, guid: a7d40000000000000000000000000005, type: 2} - {fileID: 11400000, guid: a7d40000000000000000000000000005, type: 2}
- {fileID: 11400000, guid: a7d40000000000000000000000000006, type: 2} - {fileID: 11400000, guid: a7d40000000000000000000000000006, type: 2}
- {fileID: 11400000, guid: a7d50000000000000000000000000001, type: 2}
- {fileID: 11400000, guid: a7d50000000000000000000000000002, type: 2}
- {fileID: 11400000, guid: a7d50000000000000000000000000003, type: 2}
- {fileID: 11400000, guid: a7d50000000000000000000000000004, type: 2}
- {fileID: 11400000, guid: a7d50000000000000000000000000005, type: 2}
- {fileID: 11400000, guid: a7d50000000000000000000000000006, type: 2}
+30
View File
@@ -0,0 +1,30 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5178747fac4dbfc448ff15d8a687e03c, type: 3}
m_Name: AsyncDef
m_EditorClassIdentifier:
id: shop_py_async_def
displayName: async def
description: 'queue: draw 2. TODO: gain 1 Cycle next turn.'
flavorText: returns eventually. emotionally, at least.
language: 3
rarity: 1
cycleCost: 1
resolutionTrack: 1
distroExclusive: 0
isToken: 0
isRunOnly: 1
designNotes: Run-only Repository card. Draw 2 works now; next-turn cycle grant waits for deferred-cycle-grant.
requiresSystem:
- deferred-cycle-grant
upgradeTarget: {fileID: 0}
effects: []
+30
View File
@@ -0,0 +1,30 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5178747fac4dbfc448ff15d8a687e03c, type: 3}
m_Name: ListAppend
m_EditorClassIdentifier:
id: shop_py_list_append
displayName: list.append()
description: 'queue: deal 4. TODO: grows when re-queued.'
flavorText: mutable state, but make it cute.
language: 3
rarity: 1
cycleCost: 1
resolutionTrack: 1
distroExclusive: 0
isToken: 0
isRunOnly: 1
designNotes: Run-only Repository card. Base deal 4 works now; permanent per-combat requeue growth waits for requeue-tracking.
requiresSystem:
- requeue-tracking
upgradeTarget: {fileID: 0}
effects: []
+30
View File
@@ -0,0 +1,30 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5178747fac4dbfc448ff15d8a687e03c, type: 3}
m_Name: MathRandom
m_EditorClassIdentifier:
id: shop_js_math_random
displayName: Math.random()
description: deal 2-20.
flavorText: cryptographically unsuitable, emotionally perfect.
language: 4
rarity: 1
cycleCost: 1
resolutionTrack: 0
distroExclusive: 0
isToken: 0
isRunOnly: 1
designNotes: Run-only Repository card. JavaScript damage range roll 2-20.
requiresSystem:
- damage-range-roll
upgradeTarget: {fileID: 0}
effects: []
+30
View File
@@ -0,0 +1,30 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5178747fac4dbfc448ff15d8a687e03c, type: 3}
m_Name: PromiseAll
m_EditorClassIdentifier:
id: shop_js_promise_all
displayName: Promise.all()
description: deal 4-10 three times.
flavorText: all or nothing, except the stack traces.
language: 4
rarity: 1
cycleCost: 2
resolutionTrack: 0
distroExclusive: 0
isToken: 0
isRunOnly: 1
designNotes: Run-only Repository card. Three independent JavaScript damage rolls.
requiresSystem:
- damage-range-roll
upgradeTarget: {fileID: 0}
effects: []
+30
View File
@@ -0,0 +1,30 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5178747fac4dbfc448ff15d8a687e03c, type: 3}
m_Name: Spread
m_EditorClassIdentifier:
id: shop_js_spread
displayName: spread ...
description: 'TODO: copy the lowest-cost card in hand.'
flavorText: cloning by punctuation, naturally.
language: 4
rarity: 1
cycleCost: 1
resolutionTrack: 0
distroExclusive: 0
isToken: 0
isRunOnly: 1
designNotes: Run-only Repository card. Requires card-copy.
requiresSystem:
- card-copy
upgradeTarget: {fileID: 0}
effects: []
+30
View File
@@ -0,0 +1,30 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5178747fac4dbfc448ff15d8a687e03c, type: 3}
m_Name: Zip
m_EditorClassIdentifier:
id: shop_py_zip
displayName: zip()
description: 'TODO: next 2 queued cards resolve twice.'
flavorText: pairs nicely until lengths disagree.
language: 3
rarity: 1
cycleCost: 1
resolutionTrack: 1
distroExclusive: 0
isToken: 0
isRunOnly: 1
designNotes: Run-only Repository card. Requires queue-effect-multiply.
requiresSystem:
- queue-effect-multiply
upgradeTarget: {fileID: 0}
effects: []
+953
View File
@@ -0,0 +1,953 @@
:root {
--kp-bg: #030605;
--kp-bg-deep: #020403;
--kp-panel: #07110d;
--kp-panel-raised: #0b1812;
--kp-panel-track: #050b08;
--kp-panel-targeted: #10160d;
--kp-panel-hover: #0d2016;
--kp-action-hover: #111409;
--kp-text: #63ff91;
--kp-text-dim: #7a9783;
--kp-border: #14592f;
--kp-border-subtle: #0c2d1b;
--kp-border-meter: #0d3a22;
--kp-border-empty: #0a2416;
--kp-border-strong: #27d866;
--kp-warning: #ffb347;
--kp-beneficial: #57d8ff;
--kp-accent: #ff7a1a;
--kp-danger: #ff5f5f;
--kp-overlay: rgba(0, 0, 0, 0.88);
--kp-lang-c: #8ea4a9;
--kp-lang-cpp: #6da6ff;
--kp-lang-rust: #d78b4a;
--kp-lang-python: #f2ce61;
--kp-lang-javascript: #f7df1e;
--kp-lang-typescript: #5aa7ff;
--kp-lang-haskell: #b48cff;
--kp-lang-assembly: #ff6b6b;
--kp-lang-java: #ff8c42;
--kp-lang-go: #6de6ff;
--kp-lang-ruby: #ff5f7d;
--kp-lang-php: #9aa2ff;
}
.combat-root {
background-color: var(--kp-bg);
color: var(--kp-text);
font-size: 13px;
}
.combat-status-bar {
height: 34px;
padding-left: 10px;
padding-right: 10px;
flex-direction: row;
align-items: center;
background-color: var(--kp-bg-deep);
border-bottom-width: 1px;
border-bottom-color: var(--kp-border);
}
.status-cluster {
flex-direction: row;
align-items: center;
margin-right: 14px;
}
.status-label {
color: var(--kp-text-dim);
font-size: 11px;
margin-right: 5px;
}
.status-value {
color: var(--kp-text);
-unity-font-style: bold;
}
.phase-pill {
color: var(--kp-bg);
background-color: var(--kp-accent);
padding-left: 8px;
padding-right: 8px;
padding-top: 2px;
padding-bottom: 2px;
-unity-font-style: bold;
}
.status-spacer {
flex-grow: 1;
}
.seed-value {
color: var(--kp-text-dim);
font-size: 11px;
}
.main-layout {
flex-grow: 1;
min-height: 0;
flex-direction: row;
padding: 10px;
}
.bottom-layout {
min-height: 178px;
flex-direction: row;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 10px;
}
.panel {
min-height: 0;
padding: 10px;
background-color: var(--kp-panel);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border);
}
.panel-header {
flex-direction: row;
align-items: center;
margin-bottom: 10px;
border-bottom-width: 1px;
border-bottom-color: var(--kp-border-subtle);
padding-bottom: 5px;
}
.panel-title {
color: var(--kp-text);
-unity-font-style: bold;
font-size: 15px;
flex-grow: 1;
}
.panel-subtitle {
color: var(--kp-text-dim);
font-size: 11px;
}
.field-panel {
flex-grow: 1;
margin-left: 10px;
margin-right: 10px;
}
.hand-panel {
flex-grow: 1;
}
.turn-panel {
margin-left: 10px;
}
.meter-block {
margin-bottom: 10px;
}
.meter-head {
flex-direction: row;
align-items: center;
margin-bottom: 4px;
}
.metric-label {
color: var(--kp-text-dim);
font-size: 11px;
flex-grow: 1;
}
.metric-value {
color: var(--kp-text);
-unity-font-style: bold;
}
.metric-value-muted {
color: var(--kp-text-dim);
}
.meter-track {
height: 13px;
background-color: var(--kp-bg-deep);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border-meter);
}
.meter-fill {
height: 100%;
background-color: var(--kp-text);
}
.meter-fill-warning {
background-color: var(--kp-warning);
}
.meter-fill-danger {
background-color: var(--kp-danger);
}
.meter-fill-beneficial {
background-color: var(--kp-beneficial);
}
.cycle-row {
margin-top: 2px;
margin-bottom: 10px;
}
.cycle-pips {
color: var(--kp-warning);
font-size: 17px;
-unity-font-style: bold;
}
.ram-note {
color: var(--kp-text-dim);
font-size: 11px;
margin-bottom: 8px;
}
.status-section-title {
color: var(--kp-text-dim);
font-size: 11px;
margin-top: 8px;
margin-bottom: 5px;
}
.status-row {
flex-direction: row;
flex-wrap: wrap;
}
.status-pip {
flex-direction: row;
align-items: center;
margin-right: 5px;
margin-bottom: 5px;
padding-left: 5px;
padding-right: 5px;
padding-top: 2px;
padding-bottom: 2px;
background-color: var(--kp-bg-deep);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border);
}
.status-pip-harmful {
color: var(--kp-warning);
border-color: var(--kp-warning);
}
.status-pip-beneficial {
color: var(--kp-beneficial);
border-color: var(--kp-beneficial);
}
.status-pip-empty {
color: var(--kp-text-dim);
}
.status-tooltip {
color: var(--kp-text-dim);
font-size: 11px;
white-space: normal;
margin-bottom: 4px;
}
.track-zone {
flex-grow: 1;
min-height: 0;
margin-bottom: 8px;
padding: 8px;
background-color: var(--kp-panel-track);
border-left-width: 2px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border);
}
.track-zone-queue {
border-left-color: var(--kp-lang-python);
}
.track-zone-stack {
border-left-color: var(--kp-lang-haskell);
}
.track-zone-token {
border-left-color: var(--kp-lang-go);
}
.track-header {
flex-direction: row;
align-items: center;
margin-bottom: 6px;
}
.track-title {
color: var(--kp-text);
-unity-font-style: bold;
flex-grow: 1;
}
.track-subtitle {
color: var(--kp-text-dim);
font-size: 11px;
}
.track-contents {
flex-grow: 1;
min-height: 36px;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
}
.empty-state {
color: var(--kp-text-dim);
font-size: 12px;
background-color: var(--kp-bg);
padding-left: 8px;
padding-right: 8px;
padding-top: 5px;
padding-bottom: 5px;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border-empty);
}
.card-chip {
margin-right: 6px;
margin-bottom: 6px;
padding-left: 7px;
padding-right: 7px;
padding-top: 5px;
padding-bottom: 5px;
background-color: var(--kp-bg-deep);
border-left-width: 2px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border);
}
.card-chip-name {
color: var(--kp-text);
-unity-font-style: bold;
font-size: 12px;
}
.card-chip-meta {
color: var(--kp-text-dim);
font-size: 10px;
}
.enemy-row {
flex-direction: row;
flex-wrap: wrap;
flex-grow: 1;
align-content: flex-start;
}
.enemy-card {
width: 132px;
min-height: 154px;
margin-right: 8px;
margin-bottom: 8px;
padding: 8px;
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-border);
color: var(--kp-text);
}
.enemy-card:hover {
border-color: var(--kp-border-strong);
}
.enemy-card-targeted {
border-color: var(--kp-accent);
background-color: var(--kp-panel-targeted);
}
.enemy-name {
color: var(--kp-text);
-unity-font-style: bold;
font-size: 14px;
margin-bottom: 7px;
white-space: normal;
}
.enemy-shield {
color: var(--kp-beneficial);
font-size: 11px;
margin-top: 4px;
margin-bottom: 4px;
}
.intent-panel {
margin-top: 8px;
padding: 6px;
background-color: var(--kp-bg-deep);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border-meter);
}
.intent-label {
color: var(--kp-text-dim);
font-size: 10px;
}
.intent-value {
-unity-font-style: bold;
font-size: 18px;
}
.intent-attack {
color: var(--kp-warning);
border-color: var(--kp-warning);
}
.intent-defend {
color: var(--kp-beneficial);
border-color: var(--kp-beneficial);
}
.intent-special {
color: var(--kp-accent);
border-color: var(--kp-accent);
}
.hand-row {
flex-direction: row;
flex-grow: 1;
min-height: 0;
}
.hand-card {
width: 158px;
min-height: 146px;
margin-right: 8px;
padding: 8px;
background-color: var(--kp-panel-raised);
color: var(--kp-text);
border-left-width: 2px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border);
}
.hand-card:hover {
border-color: var(--kp-border-strong);
background-color: var(--kp-panel-hover);
}
.hand-card-selected {
border-color: var(--kp-accent);
background-color: var(--kp-action-hover);
}
.hand-card-unaffordable {
opacity: 0.48;
}
.hand-card-rare {
border-right-color: var(--kp-beneficial);
}
.hand-card-epic {
border-right-color: var(--kp-lang-haskell);
}
.hand-card-legendary {
border-right-color: var(--kp-warning);
}
.card-top-row {
flex-direction: row;
align-items: center;
margin-bottom: 6px;
}
.card-cost {
width: 28px;
height: 28px;
margin-right: 7px;
background-color: var(--kp-bg-deep);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-warning);
color: var(--kp-warning);
-unity-text-align: middle-center;
-unity-font-style: bold;
font-size: 16px;
}
.card-name {
flex-grow: 1;
color: var(--kp-text);
-unity-font-style: bold;
font-size: 14px;
white-space: normal;
}
.tag-row {
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 7px;
}
.tag {
font-size: 10px;
padding-left: 5px;
padding-right: 5px;
padding-top: 2px;
padding-bottom: 2px;
margin-right: 5px;
margin-bottom: 4px;
background-color: var(--kp-bg-deep);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border);
}
.track-tag {
color: var(--kp-text-dim);
}
.language-c {
color: var(--kp-lang-c);
border-color: var(--kp-lang-c);
}
.language-cpp {
color: var(--kp-lang-cpp);
border-color: var(--kp-lang-cpp);
}
.language-rust {
color: var(--kp-lang-rust);
border-color: var(--kp-lang-rust);
}
.language-python {
color: var(--kp-lang-python);
border-color: var(--kp-lang-python);
}
.language-javascript {
color: var(--kp-lang-javascript);
border-color: var(--kp-lang-javascript);
}
.language-typescript {
color: var(--kp-lang-typescript);
border-color: var(--kp-lang-typescript);
}
.language-haskell {
color: var(--kp-lang-haskell);
border-color: var(--kp-lang-haskell);
}
.language-assembly {
color: var(--kp-lang-assembly);
border-color: var(--kp-lang-assembly);
}
.language-java {
color: var(--kp-lang-java);
border-color: var(--kp-lang-java);
}
.language-go {
color: var(--kp-lang-go);
border-color: var(--kp-lang-go);
}
.language-ruby {
color: var(--kp-lang-ruby);
border-color: var(--kp-lang-ruby);
}
.language-php {
color: var(--kp-lang-php);
border-color: var(--kp-lang-php);
}
.card-effect {
color: var(--kp-text-dim);
white-space: normal;
font-size: 11px;
flex-grow: 1;
}
.turn-resource-grid {
margin-bottom: 10px;
}
.turn-stat-row {
flex-direction: row;
margin-bottom: 4px;
}
.turn-stat-label {
color: var(--kp-text-dim);
width: 72px;
font-size: 11px;
}
.turn-stat-value {
color: var(--kp-text);
-unity-font-style: bold;
}
.primary-action {
height: 44px;
background-color: var(--kp-bg-deep);
color: var(--kp-text);
border-left-width: 2px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-accent);
-unity-font-style: bold;
}
.primary-action:hover {
background-color: var(--kp-action-hover);
}
.log-line {
color: var(--kp-text-dim);
white-space: normal;
margin-top: 8px;
font-size: 11px;
}
.overlay {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: var(--kp-overlay);
justify-content: center;
align-items: center;
}
.overlay-title {
color: var(--kp-warning);
-unity-font-style: bold;
font-size: 28px;
margin-bottom: 10px;
}
.overlay-line {
color: var(--kp-text);
margin-bottom: 6px;
}
.repository {
width: 900px;
height: 86%;
padding: 14px;
background-color: var(--kp-panel);
border-left-width: 2px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-accent);
}
.repository-header {
flex-direction: row;
align-items: center;
margin-bottom: 8px;
}
.repository-header-copy {
flex-grow: 1;
}
.repository-wallet {
min-width: 132px;
padding: 8px;
background-color: var(--kp-bg-deep);
border-left-width: 2px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-accent);
}
.repository-wallet-value {
color: var(--kp-warning);
-unity-font-style: bold;
font-size: 22px;
}
.repository-body {
flex-grow: 1;
min-height: 0;
margin-top: 8px;
margin-bottom: 10px;
}
.repository-offers {
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 10px;
}
.repository-row {
margin-bottom: 8px;
padding: 8px;
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-border);
}
.repository-tile {
width: 270px;
min-height: 244px;
margin-right: 10px;
margin-bottom: 10px;
padding: 8px;
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-border);
}
.repository-tile-card {
border-left-color: var(--kp-accent);
}
.repository-tile-upgrade {
border-left-color: var(--kp-beneficial);
}
.repository-tile-stat {
border-left-color: var(--kp-warning);
min-height: 190px;
}
.repository-tile-top {
flex-direction: row;
align-items: center;
margin-bottom: 7px;
}
.repository-kind {
color: var(--kp-text-dim);
font-size: 11px;
flex-grow: 1;
}
.repository-price {
min-width: 46px;
padding-left: 7px;
padding-right: 7px;
padding-top: 3px;
padding-bottom: 3px;
color: var(--kp-bg);
background-color: var(--kp-warning);
-unity-font-style: bold;
-unity-text-align: middle-center;
}
.repository-price-warning {
color: var(--kp-warning);
background-color: var(--kp-bg-deep);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-warning);
}
.repository-name {
color: var(--kp-text);
-unity-font-style: bold;
margin-bottom: 4px;
}
.repository-description {
color: var(--kp-text-dim);
white-space: normal;
margin-bottom: 6px;
}
.repository-action {
height: 28px;
margin-top: 7px;
background-color: var(--kp-bg-deep);
color: var(--kp-text);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-accent);
-unity-font-style: bold;
}
.repository-card-content {
flex-grow: 1;
}
.repository-card-preview {
width: 100%;
min-height: 154px;
margin-right: 0;
}
.repository-run-only-tag {
color: var(--kp-accent);
border-color: var(--kp-accent);
}
.repository-upgrade-content {
flex-grow: 1;
}
.repository-upgrade-card {
width: 100%;
min-height: 126px;
margin-right: 0;
opacity: 0.72;
}
.repository-delta {
flex-direction: row;
align-items: center;
margin-top: 7px;
padding: 7px;
background-color: var(--kp-bg-deep);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-beneficial);
}
.repository-delta-icon {
color: var(--kp-beneficial);
-unity-font-style: bold;
font-size: 18px;
margin-right: 7px;
}
.repository-delta-text {
color: var(--kp-text);
white-space: normal;
flex-grow: 1;
}
.repository-stat-content {
flex-grow: 1;
justify-content: center;
}
.repository-stat-icon {
color: var(--kp-warning);
-unity-font-style: bold;
font-size: 30px;
margin-bottom: 10px;
}
.repository-footer {
flex-direction: row;
min-height: 44px;
flex-shrink: 0;
}
.repository-footer-action {
width: 210px;
margin-right: 8px;
}
.repository-leave-action {
flex-grow: 1;
}
.repository-leave-block {
flex-grow: 1;
}
.repository-banked-hint {
color: var(--kp-text-dim);
font-size: 11px;
margin-top: 4px;
-unity-text-align: middle-center;
}
.repository-remove {
margin-bottom: 2px;
padding: 8px;
background-color: var(--kp-bg-deep);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border-empty);
}
.repository-remove-list {
flex-direction: row;
flex-wrap: wrap;
}
.repo-remove-button {
height: 24px;
margin-right: 6px;
margin-bottom: 6px;
background-color: var(--kp-bg-deep);
color: var(--kp-text-dim);
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border);
}
.repo-unaffordable {
opacity: 0.45;
}
.repo-sold {
opacity: 0.72;
border-color: var(--kp-text-dim);
}
+64 -33
View File
@@ -90,7 +90,6 @@ GameObject:
- component: {fileID: 100004} - component: {fileID: 100004}
- component: {fileID: 100003} - component: {fileID: 100003}
- component: {fileID: 100001} - component: {fileID: 100001}
- component: {fileID: 100002}
m_Layer: 0 m_Layer: 0
m_Name: Main Camera m_Name: Main Camera
m_TagString: MainCamera m_TagString: MainCamera
@@ -106,38 +105,6 @@ AudioListener:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100000} m_GameObject: {fileID: 100000}
m_Enabled: 1 m_Enabled: 1
--- !u!114 &100002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100000}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_RenderShadows: 1
m_RequiresDepthTextureOption: 2
m_RequiresOpaqueTextureOption: 2
m_CameraType: 0
m_Cameras: []
m_RendererIndex: -1
m_VolumeLayerMask:
serializedVersion: 2
m_Bits: 1
m_VolumeTrigger: {fileID: 0}
m_RenderPostProcessing: 0
m_Antialiasing: 0
m_AntialiasingQuality: 2
m_StopNaN: 0
m_Dithering: 0
m_ClearDepth: 1
m_AllowXRRendering: 1
m_RequiresDepthTexture: 0
m_RequiresColorTexture: 0
m_Version: 2
--- !u!20 &100003 --- !u!20 &100003
Camera: Camera:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -206,6 +173,10 @@ GameObject:
serializedVersion: 6 serializedVersion: 6
m_Component: m_Component:
- component: {fileID: 200001} - component: {fileID: 200001}
- component: {fileID: 200002}
- component: {fileID: 200003}
- component: {fileID: 200004}
- component: {fileID: 200005}
m_Layer: 0 m_Layer: 0
m_Name: GameRoot m_Name: GameRoot
m_TagString: Untagged m_TagString: Untagged
@@ -229,3 +200,63 @@ Transform:
m_Father: {fileID: 0} m_Father: {fileID: 0}
m_RootOrder: 1 m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &200002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200000}
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!114 &200003
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200000}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9f87432412de1ee46bf7d4c9c530d4c6, type: 3}
m_Name:
m_EditorClassIdentifier:
currentPhase: 0
--- !u!114 &200004
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200000}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 234cc6c5ca577b64ea44020f5514cebe, type: 3}
m_Name:
m_EditorClassIdentifier:
combatManager: {fileID: 200003}
currentWaveNumber: 0
isRunActive: 0
isPlayerDead: 0
--- !u!114 &200005
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 200000}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b1100000000000000000000000000003, type: 3}
m_Name:
m_EditorClassIdentifier:
distroDatabase: {fileID: 11400000, guid: 4d88ca5446fa4d80b2fbd4b12697cb7c, type: 2}
cardDatabase: {fileID: 11400000, guid: 1632132e32bfc31e8a293140f5908aa3, type: 2}
languageDeckDatabase: {fileID: 11400000, guid: a7d20000000000000000000000000001, type: 2}
-33
View File
@@ -134,7 +134,6 @@ GameObject:
- component: {fileID: 519420032} - component: {fileID: 519420032}
- component: {fileID: 519420031} - component: {fileID: 519420031}
- component: {fileID: 519420029} - component: {fileID: 519420029}
- component: {fileID: 519420030}
m_Layer: 0 m_Layer: 0
m_Name: Main Camera m_Name: Main Camera
m_TagString: MainCamera m_TagString: MainCamera
@@ -150,38 +149,6 @@ AudioListener:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 519420028} m_GameObject: {fileID: 519420028}
m_Enabled: 1 m_Enabled: 1
--- !u!114 &519420030
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 519420028}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_RenderShadows: 1
m_RequiresDepthTextureOption: 2
m_RequiresOpaqueTextureOption: 2
m_CameraType: 0
m_Cameras: []
m_RendererIndex: -1
m_VolumeLayerMask:
serializedVersion: 2
m_Bits: 1
m_VolumeTrigger: {fileID: 0}
m_RenderPostProcessing: 0
m_Antialiasing: 0
m_AntialiasingQuality: 2
m_StopNaN: 0
m_Dithering: 0
m_ClearDepth: 1
m_AllowXRRendering: 1
m_RequiresDepthTexture: 0
m_RequiresColorTexture: 0
m_Version: 2
--- !u!20 &519420031 --- !u!20 &519420031
Camera: Camera:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
+447
View File
@@ -0,0 +1,447 @@
using System.Collections.Generic;
using System.Linq;
using KernelPanic.Core;
using KernelPanic.Data;
namespace KernelPanic.Combat
{
public sealed class DealDamageEffect : ICardEffect
{
private readonly int _minAmount;
private readonly int _maxAmount;
private readonly Language _language;
private readonly bool _trueDamage;
private readonly bool _canCrit;
private readonly bool _allEnemies;
public DealDamageEffect(int minAmount, int maxAmount, Language language, bool trueDamage = false, bool canCrit = false, bool allEnemies = false)
{
_minAmount = minAmount;
_maxAmount = maxAmount;
_language = language;
_trueDamage = trueDamage;
_canCrit = canCrit;
_allEnemies = allEnemies;
}
public void Execute(CombatContext context)
{
IReadOnlyList<CombatantState> targets = _allEnemies
? context.Enemies.Where(enemy => enemy.State != null && !enemy.State.IsDefeated).Select(enemy => enemy.State).ToList()
: context.Targets;
if (StatusEffectController.Has(context.Source, StatusType.RaceCondition) && targets.Count > 1)
{
targets = ShuffleTargets(targets, context.Source);
}
for (int i = 0; i < targets.Count; i++)
{
CombatantState target = targets[i];
if (target == null || target.IsDefeated)
{
continue;
}
int minAmount = UpgradeMath.ScaleAmount(_minAmount, context.Card);
int maxAmount = UpgradeMath.ScaleAmount(_maxAmount, context.Card);
int amount = minAmount == maxAmount
? minAmount
: RandomRoll.RollRange(minAmount, maxAmount, new RollContext(context.Source));
context.DamagePipeline.DealDamage(new DamageRequest(context.Source, target, amount, _language, _trueDamage, _canCrit));
}
}
private static IReadOnlyList<CombatantState> ShuffleTargets(IReadOnlyList<CombatantState> targets, CombatantState source)
{
List<CombatantState> shuffled = targets.ToList();
for (int i = shuffled.Count - 1; i > 0; i--)
{
int swapIndex = RandomRoll.RollRange(0, i, new RollContext(source));
(shuffled[i], shuffled[swapIndex]) = (shuffled[swapIndex], shuffled[i]);
}
return shuffled;
}
}
public sealed class ShieldEffect : ICardEffect
{
private readonly int _amount;
public ShieldEffect(int amount)
{
_amount = amount;
}
public void Execute(CombatContext context)
{
int amount = UpgradeMath.ScaleShield(_amount, context.Card);
context.Source.Shield += amount;
context.CombatManager.ReportEffectResult($"gained {amount} shield");
}
}
public sealed class ChanceDamageEffect : ICardEffect
{
private readonly int _damageAmount;
private readonly int _successPercent;
private readonly Language _language;
public ChanceDamageEffect(int damageAmount, int successPercent, Language language)
{
_damageAmount = damageAmount;
_successPercent = successPercent;
_language = language;
}
public void Execute(CombatContext context)
{
for (int i = 0; i < context.Targets.Count; i++)
{
CombatantState target = context.Targets[i];
if (target == null || target.IsDefeated)
{
continue;
}
int roll = RandomRoll.RollRange(1, 100, new RollContext(context.Source));
int damageAmount = UpgradeMath.ScaleAmount(_damageAmount, 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}");
}
}
}
public sealed class ApplyStatusEffect : ICardEffect
{
private readonly StatusType _statusType;
private readonly int _stacks;
private readonly int _duration;
private readonly bool _targetSelf;
private readonly bool _skipNextTick;
public ApplyStatusEffect(StatusType statusType, int stacks, int duration, bool targetSelf, bool skipNextTick = false)
{
_statusType = statusType;
_stacks = stacks;
_duration = duration;
_targetSelf = targetSelf;
_skipNextTick = skipNextTick;
}
public void Execute(CombatContext context)
{
if (_targetSelf)
{
context.StatusEffects.Apply(context.Source, _statusType, UpgradeMath.ScaleAmount(_stacks, 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);
}
}
}
public sealed class CleanseEffect : ICardEffect
{
private readonly bool _harmfulOnly;
public CleanseEffect(bool harmfulOnly)
{
_harmfulOnly = harmfulOnly;
}
public void Execute(CombatContext context)
{
if (_harmfulOnly)
{
context.StatusEffects.CleanseHarmful(context.Source);
}
}
}
public sealed class DrawEffect : ICardEffect
{
private readonly int _count;
public DrawEffect(int count)
{
_count = count;
}
public void Execute(CombatContext context)
{
int room = context.HandController.RamCapacity - context.HandController.Cards.Count;
if (room <= 0)
{
context.CombatManager.ReportEffectResult("hand full: drew 0");
return;
}
// RAM is a hard hand cap. Draw effects only pull cards that can fit; blocked cards stay in the draw pile.
int requested = UpgradeMath.ScaleAmount(_count, context.Card);
int drawCount = UnityEngine.Mathf.Min(requested, room);
IReadOnlyList<CardInstance> drawn = context.DeckController.Draw(drawCount);
int added = 0;
for (int i = 0; i < drawn.Count; i++)
{
if (context.HandController.Add(drawn[i]))
{
added++;
}
}
string capped = drawCount < requested ? " (hand full)" : string.Empty;
context.CombatManager.ReportEffectResult($"drew {added}{capped}");
}
}
public sealed class UpgradeHandCardsEffect : ICardEffect
{
private readonly int _minCount;
private readonly int _maxCount;
public UpgradeHandCardsEffect(int minCount, int maxCount)
{
_minCount = minCount;
_maxCount = maxCount;
}
public void Execute(CombatContext context)
{
int minCount = UpgradeMath.ScaleAmount(_minCount, context.Card);
int maxCount = UpgradeMath.ScaleAmount(_maxCount, context.Card);
int targetCount = minCount == maxCount
? minCount
: RandomRoll.RollRange(minCount, maxCount, new RollContext(context.Source));
List<CardInstance> candidates = context.HandController.Cards
.Where(card => card != null && card.CanUpgrade)
.ToList();
int upgraded = 0;
while (upgraded < targetCount && candidates.Count > 0)
{
int index = RandomRoll.RollRange(0, candidates.Count - 1, new RollContext(context.Source));
CardInstance card = candidates[index];
candidates.RemoveAt(index);
if (card.Upgrade())
{
upgraded++;
}
}
context.CombatManager.ReportEffectResult(upgraded == 0 ? "no cards upgraded" : $"upgraded {upgraded} card(s)");
}
}
public sealed class GenerateCardEffect : ICardEffect
{
private readonly Language _language;
private readonly Rarity _rarity;
public GenerateCardEffect(Language language, Rarity rarity)
{
_language = language;
_rarity = rarity;
}
public void Execute(CombatContext context)
{
if (context.HandController.Cards.Count >= context.HandController.RamCapacity)
{
context.CombatManager.ReportEffectResult("hand full: generated 0");
return;
}
if (!context.CombatManager.TryCreateGeneratedCard(_language, _rarity, out CardInstance generatedCard))
{
context.CombatManager.ReportEffectResult($"no {_rarity.ToString().ToLowerInvariant()} {_language} card pool");
return;
}
if (context.HandController.Add(generatedCard))
{
context.CombatManager.ReportEffectResult($"generated {GetCardName(generatedCard)} at 0c");
}
else
{
context.CombatManager.ReportEffectResult("hand full: generated 0");
}
}
private static string GetCardName(CardInstance card)
{
if (card?.Definition == null)
{
return "--";
}
return string.IsNullOrWhiteSpace(card.Definition.DisplayName) ? card.Definition.Id : card.Definition.DisplayName;
}
}
public sealed class NoOpCardEffect : ICardEffect
{
private readonly string _todo;
public NoOpCardEffect(string todo)
{
_todo = todo;
}
public void Execute(CombatContext context)
{
context.CombatManager.LogEffectTodo(context.Card, _todo);
}
}
public static class CardEffectFactory
{
public static IReadOnlyList<ICardEffect> CreateEffects(CardDefinition definition)
{
List<ICardEffect> serializedEffects = CreateSerializedEffects(definition);
if (serializedEffects.Count > 0)
{
return serializedEffects;
}
return definition?.Id switch
{
"lang_py_print" => One(new DealDamageEffect(3, 3, Language.Python)),
"lang_py_import_antigravity" => One(new DrawEffect(2)),
"lang_py_for_loop" => One(new DealDamageEffect(2, 2, Language.Python, allEnemies: true)),
"lang_js_console_log" => One(new DealDamageEffect(3, 9, Language.JavaScript)),
"lang_js_fetch" => One(new DealDamageEffect(5, 13, Language.JavaScript)),
"ubuntu_ask_ubuntu" => One(new DrawEffect(2)),
"ubuntu_unattended_upgrades" => new ICardEffect[]
{
new ShieldEffect(4),
new ApplyStatusEffect(StatusType.UnattendedUpgrades, 4, 2, targetSelf: true, skipNextTick: true)
},
"mint_fix_broken" => new ICardEffect[]
{
new DealDamageEffect(8, 8, Language.JavaScript),
new CleanseEffect(harmfulOnly: true)
},
"lang_js_typeof" => One(new ChanceDamageEffect(8, 75, Language.JavaScript)),
"shop_py_list_append" => One(new DealDamageEffect(4, 4, Language.Python)),
"shop_py_async_def" => One(new DrawEffect(2)),
"shop_py_zip" => Todo("TODO: zip() needs queue-effect-multiply support."),
"shop_js_math_random" => One(new DealDamageEffect(2, 20, Language.JavaScript)),
"shop_js_promise_all" => new ICardEffect[]
{
new DealDamageEffect(4, 10, Language.JavaScript),
new DealDamageEffect(4, 10, Language.JavaScript),
new DealDamageEffect(4, 10, Language.JavaScript)
},
"shop_js_spread" => Todo("TODO: spread ... needs card-copy support."),
"ubuntu_snap_install" => Todo("TODO: snap install needs token/junk insertion system."),
"ubuntu_do_release_upgrade" => One(new UpgradeHandCardsEffect(1, 3)),
"ubuntu_apt_install" => One(new GenerateCardEffect(Language.Python, Rarity.Common)),
"ubuntu_pro_trial" => Todo("TODO: ubuntu pro trial needs unplayable token handling."),
"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."),
"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_selinux" => Todo("TODO: SELinux enforcing needs enemy attack mitigation statuses."),
_ => Todo("TODO: card effect is not implemented yet.")
};
}
public static bool RequiresSingleTarget(CardDefinition definition)
{
return definition?.Id is "lang_js_console_log" or "lang_js_fetch" or "lang_js_typeof" or "mint_fix_broken"
or "shop_py_list_append" or "shop_js_math_random" or "shop_js_promise_all";
}
public static bool TargetsAllEnemies(CardDefinition definition)
{
return definition?.Id is "lang_py_for_loop";
}
private static List<ICardEffect> CreateSerializedEffects(CardDefinition definition)
{
List<ICardEffect> effects = new();
if (definition?.Effects == null)
{
return effects;
}
for (int i = 0; i < definition.Effects.Count; i++)
{
ICardEffect effect = definition.Effects[i] == null ? null : definition.Effects[i].CreateRuntimeEffect();
if (effect != null)
{
effects.Add(effect);
}
}
return effects;
}
private static IReadOnlyList<ICardEffect> One(ICardEffect effect)
{
return new[] { effect };
}
private static IReadOnlyList<ICardEffect> Todo(string message)
{
return One(new NoOpCardEffect(message));
}
public static string GetRulesText(CardInstance card)
{
if (card?.Definition == null)
{
return "--";
}
int level = card.UpgradeLevel;
string marker = level > 0 ? " [upgraded]" : string.Empty;
return card.Definition.Id switch
{
"lang_py_print" => $"queue: deal {UpgradeMath.ScaleAmount(3, card)}.{marker}",
"lang_py_import_antigravity" => $"queue: draw {UpgradeMath.ScaleAmount(2, card)}.{marker}",
"lang_py_for_loop" => $"queue: deal {UpgradeMath.ScaleAmount(2, card)} to all enemies.{marker}",
"lang_js_console_log" => $"deal {UpgradeMath.ScaleAmount(3, card)}-{UpgradeMath.ScaleAmount(9, card)}.{marker}",
"lang_js_fetch" => $"deal {UpgradeMath.ScaleAmount(5, card)}-{UpgradeMath.ScaleAmount(13, card)}.{marker}",
"lang_js_typeof" => $"75%: deal {UpgradeMath.ScaleAmount(8, card)}. 25%: deal 0.{marker}",
"shop_py_list_append" => $"queue: deal {UpgradeMath.ScaleAmount(4, card)}. TODO: grows when re-queued.{marker}",
"shop_py_async_def" => $"queue: draw {UpgradeMath.ScaleAmount(2, card)}. TODO: gain 1 cycle next turn.{marker}",
"shop_py_zip" => $"TODO: next 2 queued cards resolve twice.{marker}",
"shop_js_math_random" => $"deal {UpgradeMath.ScaleAmount(2, card)}-{UpgradeMath.ScaleAmount(20, card)}.{marker}",
"shop_js_promise_all" => $"deal {UpgradeMath.ScaleAmount(4, card)}-{UpgradeMath.ScaleAmount(10, card)} three times.{marker}",
"shop_js_spread" => $"TODO: copy the lowest-cost card in hand.{marker}",
"ubuntu_ask_ubuntu" => $"queue: draw {UpgradeMath.ScaleAmount(2, card)}.{marker}",
"ubuntu_unattended_upgrades" => $"queue: gain {UpgradeMath.ScaleShield(4, card)} shield now and next 2 turns.{marker}",
"ubuntu_do_release_upgrade" => $"upgrade {UpgradeMath.ScaleAmount(1, card)}-{UpgradeMath.ScaleAmount(3, card)} random hand cards.{marker}",
"ubuntu_apt_install" => $"queue: generate a random common Python card at 0c.{marker}",
"mint_fix_broken" => $"deal {UpgradeMath.ScaleAmount(8, card)}. cleanse harmful statuses.{marker}",
_ => string.IsNullOrWhiteSpace(card.Definition.Description) ? "TODO: effect not implemented." : $"{card.Definition.Description}{marker}"
};
}
}
public static class UpgradeMath
{
public static int ScaleAmount(int baseAmount, CardInstance card)
{
return baseAmount + UnityEngine.Mathf.Max(0, card?.MagnitudeBonus ?? 0);
}
public static int ScaleShield(int baseAmount, CardInstance card)
{
return baseAmount + UnityEngine.Mathf.Max(0, card?.MagnitudeBonus ?? 0);
}
}
}
+86
View File
@@ -1,4 +1,6 @@
using KernelPanic.Data; using KernelPanic.Data;
using System.Collections.Generic;
using System.Linq;
namespace KernelPanic.Combat namespace KernelPanic.Combat
{ {
@@ -7,14 +9,98 @@ namespace KernelPanic.Combat
/// </summary> /// </summary>
public sealed class CardInstance public sealed class CardInstance
{ {
private readonly List<CombatantState> _targetSnapshot = new();
public CardInstance(CardDefinition definition) public CardInstance(CardDefinition definition)
{ {
Definition = definition; Definition = definition;
} }
public CardDefinition Definition { get; } public CardDefinition Definition { get; }
public IReadOnlyList<CombatantState> TargetSnapshot => _targetSnapshot;
public int TemporaryCostDelta { get; set; } public int TemporaryCostDelta { get; set; }
public int PermanentCostDelta { get; private set; }
public int MagnitudeBonus { get; private set; }
public int DrawRider { get; private set; }
public int UpgradeLevel { get; private set; }
public bool IsBroken { get; set; } public bool IsBroken { get; set; }
public bool IsLocked { get; set; } public bool IsLocked { get; set; }
public bool CanUpgrade => UpgradeLevel == 0;
public bool CanReceiveRepositoryUpgrade => UpgradeLevel < 3;
public bool IsUpgraded => UpgradeLevel > 0;
public bool Upgrade()
{
if (!CanUpgrade)
{
return false;
}
UpgradeLevel = 1;
ApplyCostUpgrade(-1);
ApplyMagnitudeUpgrade(1);
return true;
}
public bool ApplyCostUpgrade(int delta)
{
if (Definition == null || Definition.CycleCost + PermanentCostDelta + TemporaryCostDelta <= 0)
{
return false;
}
PermanentCostDelta += delta;
UpgradeLevel++;
return true;
}
public bool ApplyMagnitudeUpgrade(int amount)
{
if (amount <= 0)
{
return false;
}
MagnitudeBonus += amount;
UpgradeLevel++;
return true;
}
public bool ApplyDrawRider(int amount)
{
if (amount <= 0)
{
return false;
}
DrawRider += amount;
UpgradeLevel++;
return true;
}
public void SetTargetSnapshot(IEnumerable<CombatantState> targets)
{
_targetSnapshot.Clear();
if (targets == null)
{
return;
}
_targetSnapshot.AddRange(targets.Where(target => target != null && !target.IsDefeated));
}
public void ClearTargetSnapshot()
{
_targetSnapshot.Clear();
}
public void ResetCombatState()
{
TemporaryCostDelta = 0;
IsBroken = false;
IsLocked = false;
ClearTargetSnapshot();
}
} }
} }
+22 -1
View File
@@ -7,15 +7,36 @@ namespace KernelPanic.Combat
/// </summary> /// </summary>
public sealed class CombatContext public sealed class CombatContext
{ {
public CombatContext(CombatantState source, IReadOnlyList<CombatantState> targets, CombatManager combatManager) public CombatContext(
CardInstance card,
CombatantState source,
IReadOnlyList<CombatantState> targets,
CombatManager combatManager,
DamagePipeline damagePipeline,
StatusEffectController statusEffects,
DeckController deckController,
HandController handController,
IReadOnlyList<EnemyInstance> enemies)
{ {
Card = card;
Source = source; Source = source;
Targets = targets; Targets = targets;
CombatManager = combatManager; CombatManager = combatManager;
DamagePipeline = damagePipeline;
StatusEffects = statusEffects;
DeckController = deckController;
HandController = handController;
Enemies = enemies;
} }
public CardInstance Card { get; }
public CombatantState Source { get; } public CombatantState Source { get; }
public IReadOnlyList<CombatantState> Targets { get; } public IReadOnlyList<CombatantState> Targets { get; }
public CombatManager CombatManager { get; } public CombatManager CombatManager { get; }
public DamagePipeline DamagePipeline { get; }
public StatusEffectController StatusEffects { get; }
public DeckController DeckController { get; }
public HandController HandController { get; }
public IReadOnlyList<EnemyInstance> Enemies { get; }
} }
} }
+782
View File
@@ -1,5 +1,10 @@
using KernelPanic.Core; using KernelPanic.Core;
using KernelPanic.Data;
using KernelPanic.Run;
using UnityEngine; using UnityEngine;
using System;
using System.Collections.Generic;
using System.Linq;
namespace KernelPanic.Combat namespace KernelPanic.Combat
{ {
@@ -10,26 +15,277 @@ namespace KernelPanic.Combat
{ {
[SerializeField] private TurnPhase currentPhase = TurnPhase.Boot; [SerializeField] private TurnPhase currentPhase = TurnPhase.Boot;
private readonly DeckController deckController = new();
private readonly InterpreterQueue interpreterQueue = new();
private readonly LazyStack lazyStack = new();
private readonly NativeTrack nativeTrack = new();
private readonly StatusEffectController statusEffects = new();
private readonly DamagePipeline damagePipeline = new();
private readonly List<EnemyInstance> enemies = new();
private readonly HashSet<string> loggedEffectTodos = new();
private readonly List<CardDefinition> generatedCardPool = new();
private RunManager runManager;
private RunConfig runConfig;
private HandController handController;
private CombatantState playerState;
private int selectedEnemyIndex = -1;
private CardInstance pendingTargetCard;
private bool awaitingWaveContinue;
private bool runLost;
private bool skipNextAllocateDraw;
public TurnPhase CurrentPhase => currentPhase; public TurnPhase CurrentPhase => currentPhase;
public RunConfig RunConfig => runConfig;
public DeckController DeckController => deckController;
public HandController HandController => handController;
public CombatantState PlayerState => playerState;
public InterpreterQueue InterpreterQueue => interpreterQueue;
public LazyStack LazyStack => lazyStack;
public NativeTrack NativeTrack => nativeTrack;
public DamagePipeline DamagePipeline => damagePipeline;
public IReadOnlyList<EnemyInstance> Enemies => enemies;
public int SelectedEnemyIndex => selectedEnemyIndex;
public CardInstance PendingTargetCard => pendingTargetCard;
public bool AwaitingWaveContinue => awaitingWaveContinue;
public bool RunLost => runLost;
public int CurrentWaveNumber => runManager == null ? 1 : runManager.CurrentWaveNumber;
public event Action StateChanged;
public event Action<string> CombatLog;
private void Awake()
{
runManager = GetComponent<RunManager>();
}
public void StartCombat() public void StartCombat()
{ {
if (runConfig == null)
{
Debug.LogWarning("CombatManager.StartCombat called without a RunConfig.");
return;
}
SetPhase(TurnPhase.Boot); SetPhase(TurnPhase.Boot);
} }
public void StartCombat(RunConfig config)
{
runConfig = config;
StartCombat();
}
public void AdvancePhase() public void AdvancePhase()
{ {
if (IsCombatPaused)
{
return;
}
TurnPhase nextPhase = GetNextPhase(currentPhase); TurnPhase nextPhase = GetNextPhase(currentPhase);
SetPhase(nextPhase); SetPhase(nextPhase);
} }
public void EndPlayerTurn() public void EndPlayerTurn()
{ {
if (currentPhase == TurnPhase.Execute && !IsCombatPaused)
{
pendingTargetCard = null;
selectedEnemyIndex = -1;
SetPhase(TurnPhase.Interpret); SetPhase(TurnPhase.Interpret);
} }
}
public void ContinueToNextWave()
{
if (!awaitingWaveContinue || runLost)
{
return;
}
awaitingWaveContinue = false;
StartWave(preservePlayerUptime: true);
SetPhase(TurnPhase.Allocate);
}
public void SelectEnemy(int index)
{
if (IsCombatPaused)
{
return;
}
if (index < 0 || index >= enemies.Count)
{
selectedEnemyIndex = -1;
StateChanged?.Invoke();
return;
}
selectedEnemyIndex = index;
if (pendingTargetCard != null)
{
CardInstance card = pendingTargetCard;
pendingTargetCard = null;
PlayCard(card);
return;
}
StateChanged?.Invoke();
}
public bool PlayCard(CardInstance card)
{
if (currentPhase != TurnPhase.Execute || IsCombatPaused || card == null || handController == null || playerState == null)
{
return false;
}
int cost = GetCardCost(card);
if (playerState.Cycles < cost)
{
Log($"not enough cycles for {GetCardName(card)}");
return false;
}
if (RequiresTarget(card) && selectedEnemyIndex < 0)
{
pendingTargetCard = card;
Log($"select target for {GetCardName(card)}");
StateChanged?.Invoke();
return false;
}
if (!handController.Remove(card))
{
return false;
}
playerState.Cycles -= cost;
ResolutionTrack track = card.Definition.ResolutionTrack;
GameEvents.RaiseCardPlayed(new CardPlayedEvent(card, track));
LogPlayIntent(card);
card.SetTargetSnapshot(CaptureTargets(card));
switch (track)
{
case ResolutionTrack.InterpreterQueue:
interpreterQueue.Enqueue(card);
break;
case ResolutionTrack.LazyStack:
lazyStack.Enqueue(card);
break;
default:
ExecuteCardEffects(card, ResolveTargets(card));
ResolveCardRiders(card);
deckController.Discard(card);
GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.Native));
selectedEnemyIndex = -1;
break;
}
pendingTargetCard = null;
StateChanged?.Invoke();
return true;
}
public static int GetCardCost(CardInstance card)
{
if (card?.Definition == null)
{
return 0;
}
return Mathf.Max(0, card.Definition.CycleCost + card.PermanentCostDelta + card.TemporaryCostDelta);
}
public bool RequiresTarget(CardInstance card)
{
return card?.Definition != null && CardEffectFactory.RequiresSingleTarget(card.Definition) && LivingEnemyStates().Count > 0;
}
public void LogEffectTodo(CardInstance card, string message)
{
string cardId = card?.Definition == null ? string.Empty : card.Definition.Id;
string key = $"{cardId}:{message}";
if (loggedEffectTodos.Add(key))
{
Log(message);
}
}
public void ReportEffectResult(string message)
{
if (!string.IsNullOrWhiteSpace(message))
{
Log(message);
}
}
public void SetGeneratedCardPool(IEnumerable<CardDefinition> cardPool)
{
generatedCardPool.Clear();
if (cardPool == null)
{
return;
}
foreach (CardDefinition card in cardPool)
{
if (card != null && !card.IsToken && !card.IsRunOnly)
{
generatedCardPool.Add(card);
}
}
}
public bool TryCreateGeneratedCard(Language language, Rarity rarity, out CardInstance generatedCard)
{
generatedCard = null;
List<CardDefinition> pool = new();
HashSet<string> seenIds = new();
IReadOnlyList<CardDefinition> source = generatedCardPool.Count > 0 ? generatedCardPool : runConfig?.StartingDeck;
if (source == null)
{
return false;
}
for (int i = 0; i < source.Count; i++)
{
CardDefinition definition = source[i];
if (definition == null || definition.IsToken || definition.IsRunOnly || definition.Language != language || definition.Rarity != rarity)
{
continue;
}
if (seenIds.Add(definition.Id))
{
pool.Add(definition);
}
}
if (pool.Count == 0)
{
return false;
}
int index = RandomRoll.RollRange(0, pool.Count - 1, new RollContext(playerState));
CardDefinition selected = pool[index];
generatedCard = new CardInstance(selected)
{
TemporaryCostDelta = -selected.CycleCost
};
return true;
}
private void SetPhase(TurnPhase nextPhase) private void SetPhase(TurnPhase nextPhase)
{ {
if (IsCombatPaused && nextPhase != TurnPhase.Boot)
{
return;
}
TurnPhase previousPhase = currentPhase; TurnPhase previousPhase = currentPhase;
currentPhase = nextPhase; currentPhase = nextPhase;
GameEvents.RaisePhaseChanged(new PhaseChangedEvent(previousPhase, nextPhase)); GameEvents.RaisePhaseChanged(new PhaseChangedEvent(previousPhase, nextPhase));
@@ -51,6 +307,532 @@ namespace KernelPanic.Combat
private void EnterPhase(TurnPhase phase) private void EnterPhase(TurnPhase phase)
{ {
switch (phase)
{
case TurnPhase.Boot:
BootCombat();
if (!IsCombatPaused)
{
AdvancePhase();
}
break;
case TurnPhase.Allocate:
AllocateTurn();
if (!IsCombatPaused)
{
AdvancePhase();
}
break;
case TurnPhase.Execute:
StateChanged?.Invoke();
break;
case TurnPhase.Interpret:
InterpretQueue();
if (!IsCombatPaused)
{
AdvancePhase();
}
break;
case TurnPhase.EnemyProcess:
ProcessEnemies();
if (!IsCombatPaused)
{
AdvancePhase();
}
break;
case TurnPhase.GarbageCollection:
GarbageCollect();
if (!IsCombatPaused)
{
AdvancePhase();
}
break;
}
}
private void BootCombat()
{
runLost = false;
awaitingWaveContinue = false;
loggedEffectTodos.Clear();
RandomRoll.Seed(runConfig.RunSeed);
playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles());
StartWave(preservePlayerUptime: false);
Log($"booted {runConfig.Distro.DisplayName} with {runManager.RunDeck.Count} cards");
StateChanged?.Invoke();
}
private void StartWave(bool preservePlayerUptime)
{
int carriedUptime = playerState == null ? 0 : playerState.CurrentUptime;
if (!preservePlayerUptime || playerState == null)
{
playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles());
}
else
{
playerState.MaxUptime = runManager.EffectiveMaxUptime();
playerState.CurrentUptime = Mathf.Clamp(carriedUptime, 1, playerState.MaxUptime);
playerState.Ram = runManager.EffectiveRam();
playerState.MaxCycles = runManager.EffectiveMaxCycles();
playerState.Cycles = 0;
playerState.Shield = 0;
playerState.IsDefeated = false;
playerState.MutableStatuses.Clear();
}
handController = new HandController(playerState.Ram);
selectedEnemyIndex = -1;
pendingTargetCard = null;
List<CardInstance> startingCards = new();
for (int i = 0; i < runManager.RunDeck.Count; i++)
{
CardInstance card = runManager.RunDeck[i];
if (card != null)
{
card.ResetCombatState();
startingCards.Add(card);
}
}
deckController.Initialize(startingCards);
interpreterQueue.Clear();
enemies.Clear();
SpawnStructuralEnemies();
PickEnemyIntents();
DrawOpeningHand();
skipNextAllocateDraw = true;
}
private void AllocateTurn()
{
if (CheckLoss())
{
return;
}
playerState.Cycles = playerState.MaxCycles;
statusEffects.Tick(playerState, StatusTickTiming.StartOfTurn, playerState, damagePipeline);
if (CheckLoss())
{
return;
}
if (skipNextAllocateDraw)
{
skipNextAllocateDraw = false;
}
else
{
DrawForTurn();
}
StateChanged?.Invoke();
}
private void InterpretQueue()
{
while (interpreterQueue.TryDequeue(out CardInstance card))
{
LogPlayIntent(card);
ExecuteCardEffects(card, ResolveQueuedTargets(card));
ResolveCardRiders(card);
deckController.Discard(card);
GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.InterpreterQueue));
if (CheckWinOrLoss())
{
return;
}
}
CheckWinOrLoss();
StateChanged?.Invoke();
}
private void ProcessEnemies()
{
for (int i = 0; i < enemies.Count; i++)
{
EnemyInstance enemy = enemies[i];
if (enemy.State.IsDefeated)
{
continue;
}
statusEffects.Tick(enemy.State, StatusTickTiming.StartOfTurn, playerState, damagePipeline);
RemoveDefeatedEnemies();
if (CheckWinOrLoss())
{
return;
}
if (enemy.State.IsDefeated)
{
continue;
}
Log($"{enemy.Name} executes {enemy.CurrentIntent.DisplayText}");
GameEvents.RaiseEnemyWouldAct(new EnemyWouldActEvent(enemy));
ExecuteEnemyIntent(enemy);
GameEvents.RaiseEnemyActed(new EnemyActedEvent(enemy, enemy.CurrentIntent));
if (CheckLoss())
{
return;
}
}
PickEnemyIntents();
StateChanged?.Invoke();
}
private void GarbageCollect()
{
selectedEnemyIndex = -1;
pendingTargetCard = null;
statusEffects.Tick(playerState, StatusTickTiming.EndOfTurn, playerState, damagePipeline);
if (CheckLoss())
{
return;
}
RemoveDefeatedEnemies();
CheckWinOrLoss();
StateChanged?.Invoke();
}
private void DrawOpeningHand()
{
int openingDraw = Mathf.Min(CombatTuning.OpeningHandSize, playerState.Ram);
DrawCardsToHand(openingDraw, "opening hand");
}
private void DrawForTurn()
{
int requested = CombatTuning.DrawPerTurn;
if (CombatTuning.MinimumHandFloor > 0 && handController.Cards.Count < CombatTuning.MinimumHandFloor && deckController.AvailableToDrawCount > 0)
{
requested = Mathf.Max(requested, CombatTuning.MinimumHandFloor - handController.Cards.Count);
}
DrawCardsToHand(requested, "turn draw");
}
private void DrawCardsToHand(int requestedCount, string label)
{
if (handController == null || playerState == null || requestedCount <= 0)
{
return;
}
int room = playerState.Ram - handController.Cards.Count;
if (room <= 0)
{
Log($"{label}: hand full");
return;
}
int drawCount = Mathf.Min(requestedCount, room);
IReadOnlyList<CardInstance> drawn = deckController.Draw(drawCount);
int added = 0;
for (int i = 0; i < drawn.Count; i++)
{
if (handController.Add(drawn[i]))
{
added++;
}
}
if (added == 0 && deckController.AvailableToDrawCount == 0)
{
Log($"{label}: no cards to draw");
}
else if (added < requestedCount)
{
Log($"{label}: drew {added} (hand full)");
}
else
{
Log($"{label}: drew {added}");
}
}
private CombatContext BuildContext(CardInstance card, IReadOnlyList<CombatantState> targets)
{
return new CombatContext(card, playerState, targets, this, damagePipeline, statusEffects, deckController, handController, enemies);
}
private void ExecuteCardEffects(CardInstance card, IReadOnlyList<CombatantState> targets)
{
IReadOnlyList<ICardEffect> effects = CardEffectFactory.CreateEffects(card?.Definition);
CombatContext context = BuildContext(card, targets);
for (int i = 0; i < effects.Count; i++)
{
effects[i].Execute(context);
RemoveDefeatedEnemies();
if (CheckWinOrLoss())
{
return;
} }
} }
} }
private void ResolveCardRiders(CardInstance card)
{
if (card == null || card.DrawRider <= 0 || handController == null || playerState == null)
{
return;
}
DrawCardsToHand(card.DrawRider, "upgrade rider");
}
private IReadOnlyList<CombatantState> CaptureTargets(CardInstance card)
{
if (card?.Definition == null)
{
return Array.Empty<CombatantState>();
}
if (CardEffectFactory.TargetsAllEnemies(card.Definition))
{
return LivingEnemyStates();
}
if (selectedEnemyIndex >= 0 && selectedEnemyIndex < enemies.Count && !enemies[selectedEnemyIndex].State.IsDefeated)
{
return new[] { enemies[selectedEnemyIndex].State };
}
if (RequiresEnemyTarget(card))
{
CombatantState firstLiving = FirstLivingEnemyState();
return firstLiving == null ? Array.Empty<CombatantState>() : new[] { firstLiving };
}
return Array.Empty<CombatantState>();
}
private IReadOnlyList<CombatantState> ResolveTargets(CardInstance card)
{
List<CombatantState> targets = card.TargetSnapshot.Where(target => target != null && !target.IsDefeated).ToList();
if (targets.Count == 0 && RequiresEnemyTarget(card))
{
CombatantState firstLiving = FirstLivingEnemyState();
if (firstLiving != null)
{
targets.Add(firstLiving);
}
}
return targets;
}
private IReadOnlyList<CombatantState> ResolveQueuedTargets(CardInstance card)
{
List<CombatantState> targets = ResolveTargets(card).ToList();
if (targets.Count == 0 && CardEffectFactory.TargetsAllEnemies(card?.Definition))
{
targets.AddRange(LivingEnemyStates());
}
// Queued cards snapshot targets when played. If those targets die before Interpret, retarget to living enemies.
return targets;
}
private bool RequiresEnemyTarget(CardInstance card)
{
if (card?.Definition == null)
{
return false;
}
return CardEffectFactory.RequiresSingleTarget(card.Definition)
|| CardEffectFactory.TargetsAllEnemies(card.Definition)
|| card.Definition.Id == "lang_py_print";
}
private List<CombatantState> LivingEnemyStates()
{
return enemies
.Where(enemy => enemy.State != null && !enemy.State.IsDefeated)
.Select(enemy => enemy.State)
.ToList();
}
private CombatantState FirstLivingEnemyState()
{
for (int i = 0; i < enemies.Count; i++)
{
if (enemies[i].State != null && !enemies[i].State.IsDefeated)
{
return enemies[i].State;
}
}
return null;
}
private void RemoveDefeatedEnemies()
{
for (int i = enemies.Count - 1; i >= 0; i--)
{
if (enemies[i].State.IsDefeated)
{
Log($"{enemies[i].Name} exited");
runManager?.AddBits(CombatTuning.BitsPerKill);
enemies.RemoveAt(i);
}
}
if (selectedEnemyIndex >= enemies.Count)
{
selectedEnemyIndex = -1;
}
}
private void SpawnStructuralEnemies()
{
int wave = CurrentWaveNumber;
int count = Mathf.Clamp(CombatTuning.BaseEnemiesPerWave + ((wave - 1) * CombatTuning.AdditionalEnemiesPerWave), CombatTuning.BaseEnemiesPerWave, CombatTuning.MaxEnemiesPerWave);
int waveUptimeBonus = Mathf.Max(0, wave - 1) * CombatTuning.EnemyUptimeGrowthPerWave;
for (int i = 0; i < count; i++)
{
// TODO: Replace this low-HP additive wave stub with the real encounter/difficulty system.
int baseUptime = RandomRoll.RollRange(CombatTuning.BaseEnemyUptimeMin, CombatTuning.BaseEnemyUptimeMax, RollContext.None);
int uptime = baseUptime + waveUptimeBonus;
enemies.Add(new EnemyInstance("zombie_process", uptime, CreateZombieProcessIntentPool(wave, i)));
}
}
private static IReadOnlyList<EnemyIntent> CreateZombieProcessIntentPool(int wave, int slotIndex)
{
// TODO: Replace this code-shaped pool with EnemyDefinition-provided intent data.
int attack = GetZombieProcessAttack(wave, slotIndex);
return new[]
{
new EnemyIntent(EnemyIntentKind.Attack, attack, attack, Language.C, "attack", ">"),
new EnemyIntent(EnemyIntentKind.Defend, 3 + wave, 3 + wave, Language.C, "defend", "#"),
new EnemyIntent(EnemyIntentKind.StatusAttack, CombatTuning.EnemyStatusAttackDamage, CombatTuning.EnemyStatusAttackDamage, Language.C, "leak", "!", false, StatusType.MemoryLeak, 1, CombatTuning.EnemyStatusDuration)
};
}
private static int GetZombieProcessAttack(int wave, int slotIndex)
{
int safeWave = Mathf.Max(1, wave);
int waveGrowth = (safeWave - 1) / Mathf.Max(1, CombatTuning.EnemyAttackGrowthEveryWaves);
int slotVariance = CombatTuning.EnemySlotAttackVariance <= 0 ? 0 : slotIndex % (CombatTuning.EnemySlotAttackVariance + 1);
return CombatTuning.BaseEnemyAttack + waveGrowth + slotVariance;
}
private void PickEnemyIntents()
{
for (int i = 0; i < enemies.Count; i++)
{
if (!enemies[i].State.IsDefeated)
{
enemies[i].PickNextIntent();
}
}
}
private void ExecuteEnemyIntent(EnemyInstance enemy)
{
EnemyIntent intent = enemy.CurrentIntent;
int value = intent.MinValue == intent.MaxValue
? intent.MinValue
: RandomRoll.RollRange(intent.MinValue, intent.MaxValue, new RollContext(enemy.State));
switch (intent.Kind)
{
case EnemyIntentKind.Attack:
DamageResult attackResult = damagePipeline.DealDamage(new DamageRequest(enemy.State, playerState, value, intent.DamageType, intent.TrueDamage, false));
GameEvents.RaisePlayerDamaged(new PlayerDamagedEvent(enemy, attackResult.FinalAmount));
break;
case EnemyIntentKind.Defend:
enemy.State.Shield += value;
break;
case EnemyIntentKind.StatusAttack:
DamageResult statusResult = damagePipeline.DealDamage(new DamageRequest(enemy.State, playerState, value, intent.DamageType, intent.TrueDamage, false));
GameEvents.RaisePlayerDamaged(new PlayerDamagedEvent(enemy, statusResult.FinalAmount));
if (intent.StatusStacks > 0)
{
statusEffects.Apply(playerState, intent.StatusType, intent.StatusStacks, intent.StatusDuration, enemy.State, skipNextTick: true);
}
break;
case EnemyIntentKind.Buff:
Log("TODO: buff intent needs enemy buff/status support.");
break;
case EnemyIntentKind.Special:
Log("TODO: special intent needs enemy-specific behavior hooks.");
break;
}
}
private bool CheckWinOrLoss()
{
return CheckLoss() || CheckWin();
}
private bool CheckLoss()
{
if (runLost || playerState == null || !playerState.IsDefeated)
{
return runLost;
}
runLost = true;
awaitingWaveContinue = false;
selectedEnemyIndex = -1;
pendingTargetCard = null;
GameEvents.RaiseEncounterLost(new EncounterLostEvent(CurrentWaveNumber));
GameEvents.RaiseRunEnded(new RunEndedEvent(playerDied: true, entropyEarned: 0));
Log("kernel panic: player uptime reached 0");
StateChanged?.Invoke();
return true;
}
private bool CheckWin()
{
if (awaitingWaveContinue || runLost || enemies.Count > 0)
{
return awaitingWaveContinue;
}
awaitingWaveContinue = true;
selectedEnemyIndex = -1;
pendingTargetCard = null;
int clearedWave = CurrentWaveNumber;
GameEvents.RaiseEncounterWon(new EncounterWonEvent(clearedWave));
GameEvents.RaiseWaveCleared(new WaveClearedEvent(clearedWave));
Log("wave cleared -> repository available");
StateChanged?.Invoke();
return true;
}
private void LogPlayIntent(CardInstance card)
{
string notes = card?.Definition == null || string.IsNullOrWhiteSpace(card.Definition.DesignNotes)
? "no-op scaffold"
: card.Definition.DesignNotes;
Log($"{GetCardName(card)} would resolve: {notes}");
}
private void Log(string message)
{
CombatLog?.Invoke(message);
}
private static string GetCardName(CardInstance card)
{
if (card?.Definition == null)
{
return "--";
}
return string.IsNullOrWhiteSpace(card.Definition.DisplayName) ? card.Definition.Id : card.Definition.DisplayName;
}
private bool IsCombatPaused => awaitingWaveContinue || runLost;
}
}
+41
View File
@@ -0,0 +1,41 @@
namespace KernelPanic.Combat
{
/// <summary>
/// Central combat economy/scaffold tuning. Kept in code while the combat scaffold is still volatile;
/// promote to a ScriptableObject once designers need per-run or per-encounter tuning without recompiles.
/// </summary>
public static class CombatTuning
{
public const int OpeningHandSize = 5;
public const int DrawPerTurn = 1;
public const int MinimumHandFloor = 0;
public const int BaseEnemyUptimeMin = 5;
public const int BaseEnemyUptimeMax = 10;
public const int EnemyUptimeGrowthPerWave = 2;
public const int BaseEnemyAttack = 3;
public const int EnemyAttackGrowthEveryWaves = 2;
public const int EnemySlotAttackVariance = 1;
public const int EnemyStatusAttackDamage = 1;
public const int EnemyStatusDuration = 3;
public const int BaseEnemiesPerWave = 2;
public const int AdditionalEnemiesPerWave = 1;
public const int MaxEnemiesPerWave = 5;
public const int BitsPerKill = 1;
public const int ShopSize = 5;
public const int RerollBaseCost = 1;
public const int RerollCostStep = 1;
public const int RemoveCardCost = 1;
public const int NewCardOfferCost = 2;
public const int UpgradeOfferCost = 2;
public const int StatUpgradeCost = 3;
public const int StatUpgradeMaxCycles = 1;
public const int StatUpgradeMaxUptime = 4;
public const int StatUpgradeHeal = 5;
public const int StatUpgradeRam = 1;
public const int UpgradeMagnitudeBonus = 2;
}
}
+31 -2
View File
@@ -8,7 +8,7 @@ namespace KernelPanic.Combat
/// </summary> /// </summary>
public sealed class CombatantState public sealed class CombatantState
{ {
private readonly Dictionary<StatusType, int> activeStatuses = new(); private readonly List<StatusInstance> _statuses = new();
public CombatantState(int maxUptime, int ram, int cycles) public CombatantState(int maxUptime, int ram, int cycles)
{ {
@@ -16,6 +16,7 @@ namespace KernelPanic.Combat
CurrentUptime = maxUptime; CurrentUptime = maxUptime;
Ram = ram; Ram = ram;
Cycles = cycles; Cycles = cycles;
MaxCycles = cycles;
} }
public int CurrentUptime { get; set; } public int CurrentUptime { get; set; }
@@ -23,6 +24,34 @@ namespace KernelPanic.Combat
public int Shield { get; set; } public int Shield { get; set; }
public int Ram { get; set; } public int Ram { get; set; }
public int Cycles { get; set; } public int Cycles { get; set; }
public IReadOnlyDictionary<StatusType, int> ActiveStatuses => activeStatuses; public int MaxCycles { get; set; }
public bool IsDefeated { get; set; }
public bool forceMaxRolls { get; set; }
public IReadOnlyList<StatusInstance> Statuses => _statuses;
public IReadOnlyDictionary<StatusType, int> ActiveStatuses => BuildStatusSnapshot();
internal List<StatusInstance> MutableStatuses => _statuses;
public bool HasStatus(StatusType type)
{
return _statuses.Exists(status => status.Type == type);
}
public int StacksOf(StatusType type)
{
StatusInstance status = _statuses.Find(instance => instance.Type == type);
return status == null ? 0 : status.Stacks;
}
private IReadOnlyDictionary<StatusType, int> BuildStatusSnapshot()
{
Dictionary<StatusType, int> snapshot = new();
for (int i = 0; i < _statuses.Count; i++)
{
snapshot[_statuses[i].Type] = _statuses[i].Stacks;
}
return snapshot;
}
} }
} }
+111
View File
@@ -0,0 +1,111 @@
using KernelPanic.Core;
using UnityEngine;
namespace KernelPanic.Combat
{
public readonly struct DamageRequest
{
public DamageRequest(
CombatantState source,
CombatantState target,
int amount,
Language language,
bool trueDamage,
bool canCrit)
{
Source = source;
Target = target;
Amount = amount;
Language = language;
TrueDamage = trueDamage;
CanCrit = canCrit;
}
public CombatantState Source { get; }
public CombatantState Target { get; }
public int Amount { get; }
public Language Language { get; }
public bool TrueDamage { get; }
public bool CanCrit { get; }
}
public readonly struct DamageResult
{
public DamageResult(int finalAmount, bool targetDefeated)
{
FinalAmount = finalAmount;
TargetDefeated = targetDefeated;
}
public int FinalAmount { get; }
public bool TargetDefeated { get; }
}
/// <summary>
/// Single damage chokepoint for shield, uptime, damage events, and defeat decisions.
/// </summary>
public sealed class DamagePipeline
{
public DamageResult DealDamage(DamageRequest request)
{
if (request.Target == null || request.Target.IsDefeated)
{
return new DamageResult(0, false);
}
int amount = Mathf.Max(0, request.Amount);
amount = ApplyMultipliers(amount, request);
amount = ApplyCrit(amount, request);
amount = ApplyResistancesAndWeaknesses(amount, request);
int finalAmount = ApplyShieldAndUptime(amount, request);
GameEvents.RaiseDamageDealt(new DamageDealtEvent(request.Source, request.Target, finalAmount, request.Language));
bool defeated = false;
if (request.Target.CurrentUptime <= 0 && !request.Target.IsDefeated)
{
request.Target.IsDefeated = true;
defeated = true;
GameEvents.RaiseCombatantDefeated(new CombatantDefeatedEvent(request.Target));
}
return new DamageResult(finalAmount, defeated);
}
private static int ApplyMultipliers(int amount, DamageRequest request)
{
// TODO: Apply buffs/debuffs and Mint's no-multiplier rule here.
return amount;
}
private static int ApplyCrit(int amount, DamageRequest request)
{
// TODO: Add crit calculation and crit modifiers.
return amount;
}
private static int ApplyResistancesAndWeaknesses(int amount, DamageRequest request)
{
// TODO: Apply enemy language resistances and weaknesses here.
return amount;
}
private static int ApplyShieldAndUptime(int amount, DamageRequest request)
{
CombatantState target = request.Target;
int remaining = amount;
int shieldDamage = 0;
if (!request.TrueDamage && target.Shield > 0)
{
shieldDamage = Mathf.Min(target.Shield, remaining);
target.Shield -= shieldDamage;
remaining -= shieldDamage;
}
int uptimeDamage = Mathf.Min(target.CurrentUptime, remaining);
target.CurrentUptime = Mathf.Max(0, target.CurrentUptime - uptimeDamage);
return shieldDamage + uptimeDamage;
}
}
}
+59 -13
View File
@@ -1,5 +1,5 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace KernelPanic.Combat namespace KernelPanic.Combat
{ {
@@ -8,42 +8,88 @@ namespace KernelPanic.Combat
/// </summary> /// </summary>
public sealed class DeckController public sealed class DeckController
{ {
private readonly List<CardInstance> drawPile = new(); private readonly List<CardInstance> _drawPile = new();
private readonly List<CardInstance> discardPile = new(); private readonly List<CardInstance> _discardPile = new();
private readonly List<CardInstance> exhaustPile = new(); private readonly List<CardInstance> _exhaustPile = new();
public IReadOnlyList<CardInstance> DrawPile => drawPile; public IReadOnlyList<CardInstance> DrawPile => _drawPile;
public IReadOnlyList<CardInstance> DiscardPile => discardPile; public IReadOnlyList<CardInstance> DiscardPile => _discardPile;
public IReadOnlyList<CardInstance> ExhaustPile => exhaustPile; public IReadOnlyList<CardInstance> ExhaustPile => _exhaustPile;
public int AvailableToDrawCount => _drawPile.Count + _discardPile.Count;
public void Initialize(IEnumerable<CardInstance> cards) public void Initialize(IEnumerable<CardInstance> cards)
{ {
throw new NotImplementedException(); _drawPile.Clear();
_discardPile.Clear();
_exhaustPile.Clear();
if (cards != null)
{
_drawPile.AddRange(cards.Where(card => card != null));
}
ShuffleDrawPile();
} }
public IReadOnlyList<CardInstance> Draw(int count) public IReadOnlyList<CardInstance> Draw(int count)
{ {
throw new NotImplementedException(); List<CardInstance> drawn = new();
for (int i = 0; i < count; i++)
{
if (_drawPile.Count == 0)
{
ReshuffleDiscardIntoDraw();
}
if (_drawPile.Count == 0)
{
break;
}
int topIndex = _drawPile.Count - 1;
CardInstance card = _drawPile[topIndex];
_drawPile.RemoveAt(topIndex);
drawn.Add(card);
}
return drawn;
} }
public void Discard(CardInstance card) public void Discard(CardInstance card)
{ {
throw new NotImplementedException(); if (card != null)
{
_discardPile.Add(card);
}
} }
public void Exhaust(CardInstance card) public void Exhaust(CardInstance card)
{ {
throw new NotImplementedException(); if (card != null)
{
_exhaustPile.Add(card);
}
} }
public void ShuffleDrawPile() public void ShuffleDrawPile()
{ {
throw new NotImplementedException(); for (int i = _drawPile.Count - 1; i > 0; i--)
{
int swapIndex = RandomRoll.RollRange(0, i, RollContext.None);
(_drawPile[i], _drawPile[swapIndex]) = (_drawPile[swapIndex], _drawPile[i]);
}
} }
public void ReshuffleDiscardIntoDraw() public void ReshuffleDiscardIntoDraw()
{ {
throw new NotImplementedException(); if (_discardPile.Count == 0)
{
return;
}
_drawPile.AddRange(_discardPile);
_discardPile.Clear();
ShuffleDrawPile();
} }
} }
} }
+41
View File
@@ -0,0 +1,41 @@
using System.Collections.Generic;
namespace KernelPanic.Combat
{
/// <summary>
/// Runtime-only enemy placeholder used until encounter and EnemyDefinition assets are wired.
/// </summary>
public sealed class EnemyInstance
{
private readonly List<EnemyIntent> _intentPool = new();
public EnemyInstance(string name, int maxUptime, IEnumerable<EnemyIntent> intentPool)
{
Name = string.IsNullOrWhiteSpace(name) ? "unknown_process" : name;
State = new CombatantState(maxUptime, 0, 0);
if (intentPool != null)
{
_intentPool.AddRange(intentPool);
}
}
public string Name { get; }
public int CurrentUptime => State.CurrentUptime;
public int MaxUptime => State.MaxUptime;
public CombatantState State { get; }
public IReadOnlyList<EnemyIntent> IntentPool => _intentPool;
public EnemyIntent CurrentIntent { get; private set; }
public void PickNextIntent()
{
if (_intentPool.Count == 0)
{
CurrentIntent = default;
return;
}
int index = RandomRoll.RollRange(0, _intentPool.Count - 1, new RollContext(State));
CurrentIntent = _intentPool[index];
}
}
}
+57
View File
@@ -0,0 +1,57 @@
using KernelPanic.Core;
namespace KernelPanic.Combat
{
public enum EnemyIntentKind
{
Attack,
Defend,
Buff,
StatusAttack,
Special
}
/// <summary>
/// Runtime enemy intent shape. Future EnemyDefinition assets should provide pools of this data.
/// </summary>
public readonly struct EnemyIntent
{
public EnemyIntent(
EnemyIntentKind kind,
int minValue,
int maxValue,
Language damageType,
string displayLabel,
string iconKey,
bool trueDamage = false,
StatusType statusType = StatusType.MemoryLeak,
int statusStacks = 0,
int statusDuration = -1)
{
Kind = kind;
MinValue = minValue;
MaxValue = maxValue;
DamageType = damageType;
DisplayLabel = string.IsNullOrWhiteSpace(displayLabel) ? kind.ToString() : displayLabel;
IconKey = string.IsNullOrWhiteSpace(iconKey) ? "?" : iconKey;
TrueDamage = trueDamage;
StatusType = statusType;
StatusStacks = statusStacks;
StatusDuration = statusDuration;
}
public EnemyIntentKind Kind { get; }
public int MinValue { get; }
public int MaxValue { get; }
public Language DamageType { get; }
public string DisplayLabel { get; }
public string IconKey { get; }
public bool TrueDamage { get; }
public StatusType StatusType { get; }
public int StatusStacks { get; }
public int StatusDuration { get; }
public string ValueText => MinValue == MaxValue ? MinValue.ToString() : $"{MinValue}-{MaxValue}";
public string DisplayText => $"{IconKey} {DisplayLabel} {ValueText}";
}
}
+13 -8
View File
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace KernelPanic.Combat namespace KernelPanic.Combat
@@ -8,7 +7,7 @@ namespace KernelPanic.Combat
/// </summary> /// </summary>
public sealed class HandController public sealed class HandController
{ {
private readonly List<CardInstance> cards = new(); private readonly List<CardInstance> _cards = new();
public HandController(int ramCapacity) public HandController(int ramCapacity)
{ {
@@ -16,26 +15,32 @@ namespace KernelPanic.Combat
} }
public int RamCapacity { get; private set; } public int RamCapacity { get; private set; }
public IReadOnlyList<CardInstance> Cards => cards; public IReadOnlyList<CardInstance> Cards => _cards;
public bool CanAdd(CardInstance card) public bool CanAdd(CardInstance card)
{ {
throw new NotImplementedException(); return card != null && _cards.Count < RamCapacity;
} }
public void Add(CardInstance card) public bool Add(CardInstance card)
{ {
throw new NotImplementedException(); if (!CanAdd(card))
{
return false;
}
_cards.Add(card);
return true;
} }
public bool Remove(CardInstance card) public bool Remove(CardInstance card)
{ {
throw new NotImplementedException(); return card != null && _cards.Remove(card);
} }
public void Clear() public void Clear()
{ {
throw new NotImplementedException(); _cards.Clear();
} }
} }
} }
+33 -6
View File
@@ -1,5 +1,5 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using KernelPanic.Core;
namespace KernelPanic.Combat namespace KernelPanic.Combat
{ {
@@ -8,23 +8,50 @@ namespace KernelPanic.Combat
/// </summary> /// </summary>
public sealed class InterpreterQueue : IResolutionTrack public sealed class InterpreterQueue : IResolutionTrack
{ {
private readonly Queue<CardInstance> queuedCards = new(); private readonly Queue<CardInstance> _queuedCards = new();
private readonly List<CardInstance> _queuedView = new();
public int Count => queuedCards.Count; public int Count => _queuedCards.Count;
public IReadOnlyList<CardInstance> Cards => _queuedView;
public void Enqueue(CardInstance card) public void Enqueue(CardInstance card)
{ {
throw new NotImplementedException(); if (card == null)
{
return;
}
_queuedCards.Enqueue(card);
_queuedView.Add(card);
} }
public void Resolve(CombatContext context) public void Resolve(CombatContext context)
{ {
throw new NotImplementedException(); while (_queuedCards.Count > 0)
{
CardInstance card = _queuedCards.Dequeue();
_queuedView.Remove(card);
GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.InterpreterQueue));
}
}
public bool TryDequeue(out CardInstance card)
{
if (_queuedCards.Count == 0)
{
card = null;
return false;
}
card = _queuedCards.Dequeue();
_queuedView.Remove(card);
return true;
} }
public void Clear() public void Clear()
{ {
throw new NotImplementedException(); _queuedCards.Clear();
_queuedView.Clear();
} }
} }
} }
+30 -6
View File
@@ -1,5 +1,5 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using KernelPanic.Core;
namespace KernelPanic.Combat namespace KernelPanic.Combat
{ {
@@ -8,23 +8,47 @@ namespace KernelPanic.Combat
/// </summary> /// </summary>
public sealed class LazyStack : IResolutionTrack public sealed class LazyStack : IResolutionTrack
{ {
private readonly Stack<CardInstance> stackedCards = new(); private readonly Stack<CardInstance> _stackedCards = new();
private readonly List<CardInstance> _stackedView = new();
public int Count => stackedCards.Count; public int Count => _stackedCards.Count;
public IReadOnlyList<CardInstance> Cards => _stackedView;
public void Enqueue(CardInstance card) public void Enqueue(CardInstance card)
{ {
throw new NotImplementedException(); if (card == null)
{
return;
}
_stackedCards.Push(card);
_stackedView.Insert(0, card);
} }
public void Resolve(CombatContext context) public void Resolve(CombatContext context)
{ {
throw new NotImplementedException(); ForceEvaluate(context);
} }
public void ForceEvaluate(CombatContext context) public void ForceEvaluate(CombatContext context)
{ {
throw new NotImplementedException(); while (TryPop(out CardInstance card))
{
GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.LazyStack));
}
}
public bool TryPop(out CardInstance card)
{
if (_stackedCards.Count == 0)
{
card = null;
return false;
}
card = _stackedCards.Pop();
_stackedView.Remove(card);
return true;
} }
} }
} }
+6 -3
View File
@@ -1,4 +1,4 @@
using System; using KernelPanic.Core;
namespace KernelPanic.Combat namespace KernelPanic.Combat
{ {
@@ -9,12 +9,15 @@ namespace KernelPanic.Combat
{ {
public void Enqueue(CardInstance card) public void Enqueue(CardInstance card)
{ {
throw new NotImplementedException(); if (card != null)
{
GameEvents.RaiseCardPlayed(new CardPlayedEvent(card, ResolutionTrack.Native));
}
} }
public void Resolve(CombatContext context) public void Resolve(CombatContext context)
{ {
throw new NotImplementedException(); // Native cards resolve immediately in CombatManager.PlayCard; this track has no queue.
} }
} }
} }
+47
View File
@@ -0,0 +1,47 @@
using System;
namespace KernelPanic.Combat
{
/// <summary>
/// Carries source data for combat rolls so later passives can bias the result.
/// </summary>
public readonly struct RollContext
{
public RollContext(CombatantState source)
{
Source = source;
}
public static RollContext None => new(null);
public CombatantState Source { get; }
}
/// <summary>
/// Single combat randomness chokepoint. Seeded from RunConfig.RunSeed during boot for reproducible runs.
/// </summary>
public static class RandomRoll
{
private static Random _random = new(0);
public static void Seed(int seed)
{
_random = new Random(seed);
}
public static int RollRange(int min, int max, RollContext context)
{
if (min > max)
{
(min, max) = (max, min);
}
// Mint's passive will flip this bool; no passive sets it in this scaffold.
if (context.Source != null && context.Source.forceMaxRolls)
{
return max;
}
return _random.Next(min, max + 1);
}
}
}
+191 -6
View File
@@ -1,26 +1,211 @@
using System; using System;
using System.Collections.Generic;
using KernelPanic.Core; using KernelPanic.Core;
using UnityEngine;
namespace KernelPanic.Combat namespace KernelPanic.Combat
{ {
/// <summary> /// <summary>
/// Applies, ticks, and expires status effects on a combatant. /// Applies, ticks, cleanses, and expires status effects on a combatant.
/// </summary> /// </summary>
public sealed class StatusEffectController public sealed class StatusEffectController
{ {
public void Apply(CombatantState target, StatusType statusType, int stacks) private static readonly Dictionary<StatusType, StatusDescriptor> Descriptors = new()
{ {
throw new NotImplementedException(); [StatusType.MemoryLeak] = new(StatusType.MemoryLeak, "Memory Leak", "leak", false, StatusStackingRule.Intensity, StatusTickTiming.StartOfTurn, "Takes damage equal to stacks at start of turn."),
[StatusType.Segfault] = new(StatusType.Segfault, "Segfault", "segv", false, StatusStackingRule.Unique, StatusTickTiming.StartOfTurn, "Loses one cycle this turn. TODO: lock a random card slot."),
[StatusType.RaceCondition] = new(StatusType.RaceCondition, "Race Condition", "race", false, StatusStackingRule.Refresh, StatusTickTiming.None, "Randomizes multi-target resolution order where supported."),
[StatusType.Deprecated] = new(StatusType.Deprecated, "Deprecated", "dep", false, StatusStackingRule.Duration, StatusTickTiming.StartOfTurn, "A temporary positive value decays. Currently reduces shield by 1."),
[StatusType.DependencyError] = new(StatusType.DependencyError, "Dependency Error", "deps", false, StatusStackingRule.Unique, StatusTickTiming.None, "Registered for off-support card cost rules. TODO: connect draw-time cost path."),
[StatusType.Deadlock] = new(StatusType.Deadlock, "Deadlock", "lock", false, StatusStackingRule.Duration, StatusTickTiming.None, "Registered goroutine freeze. TODO: connect goroutine token ticking."),
[StatusType.UnattendedUpgrades] = new(StatusType.UnattendedUpgrades, "Unattended Upgrades", "apt", true, StatusStackingRule.Duration, StatusTickTiming.EndOfTurn, "Grants shield at end of turn, then expires."),
};
public static StatusDescriptor GetDescriptor(StatusType statusType)
{
return Descriptors[statusType];
} }
public void Tick(CombatantState target) public static bool Has(CombatantState target, StatusType statusType)
{ {
throw new NotImplementedException(); return target != null && target.HasStatus(statusType);
}
public static int StacksOf(CombatantState target, StatusType statusType)
{
return target == null ? 0 : target.StacksOf(statusType);
}
public void Apply(CombatantState target, StatusType statusType, int stacks, int duration, CombatantState source = null, bool skipNextTick = false)
{
if (target == null || target.IsDefeated)
{
return;
}
StatusDescriptor descriptor = GetDescriptor(statusType);
StatusInstance existing = target.MutableStatuses.Find(status => status.Type == statusType);
int safeStacks = Mathf.Max(1, stacks);
int safeDuration = duration == -1 ? -1 : Mathf.Max(1, duration);
if (existing == null)
{
target.MutableStatuses.Add(new StatusInstance(statusType, safeStacks, safeDuration, source, skipNextTick));
}
else
{
existing.Source = source ?? existing.Source;
Merge(existing, descriptor.StackingRule, safeStacks, safeDuration, skipNextTick);
}
GameEvents.RaiseStatusApplied(new StatusAppliedEvent(source, target, statusType, safeStacks, safeDuration));
}
public void Tick(CombatantState target, StatusTickTiming timing, CombatantState source, DamagePipeline damagePipeline)
{
if (target == null || target.IsDefeated)
{
return;
}
List<StatusInstance> snapshot = new(target.MutableStatuses);
for (int i = 0; i < snapshot.Count; i++)
{
StatusInstance status = snapshot[i];
if (!target.MutableStatuses.Contains(status))
{
continue;
}
StatusDescriptor descriptor = GetDescriptor(status.Type);
if (descriptor.TickTiming != timing)
{
continue;
}
if (status.SkipNextTick)
{
status.SkipNextTick = false;
continue;
}
ResolveTick(status, target, status.Source ?? source, damagePipeline);
DecayDuration(target, status);
}
}
public int Cleanse(CombatantState target, Predicate<StatusInstance> predicate)
{
if (target == null || predicate == null)
{
return 0;
}
int removed = 0;
for (int i = target.MutableStatuses.Count - 1; i >= 0; i--)
{
StatusInstance status = target.MutableStatuses[i];
if (predicate(status))
{
target.MutableStatuses.RemoveAt(i);
removed++;
GameEvents.RaiseStatusCleansed(new StatusCleansedEvent(target, status.Type));
}
}
return removed;
}
public int CleanseHarmful(CombatantState target)
{
return Cleanse(target, status => !GetDescriptor(status.Type).IsBeneficial);
} }
public void Expire(CombatantState target, StatusType statusType) public void Expire(CombatantState target, StatusType statusType)
{ {
throw new NotImplementedException(); if (target == null)
{
return;
}
for (int i = target.MutableStatuses.Count - 1; i >= 0; i--)
{
if (target.MutableStatuses[i].Type == statusType)
{
target.MutableStatuses.RemoveAt(i);
GameEvents.RaiseStatusExpired(new StatusExpiredEvent(target, statusType));
}
}
}
private static void Merge(StatusInstance existing, StatusStackingRule stackingRule, int stacks, int duration, bool skipNextTick)
{
switch (stackingRule)
{
case StatusStackingRule.Intensity:
existing.Stacks += stacks;
existing.Duration = MergeDuration(existing.Duration, duration);
break;
case StatusStackingRule.Duration:
existing.Stacks = Mathf.Max(existing.Stacks, stacks);
existing.Duration = existing.Duration == -1 || duration == -1 ? -1 : existing.Duration + duration;
break;
case StatusStackingRule.Refresh:
existing.Stacks = Mathf.Max(existing.Stacks, stacks);
existing.Duration = duration;
break;
case StatusStackingRule.Unique:
break;
}
existing.SkipNextTick |= skipNextTick;
}
private static int MergeDuration(int current, int incoming)
{
if (current == -1 || incoming == -1)
{
return -1;
}
return Mathf.Max(current, incoming);
}
private static void ResolveTick(StatusInstance status, CombatantState target, CombatantState source, DamagePipeline damagePipeline)
{
switch (status.Type)
{
case StatusType.MemoryLeak:
damagePipeline.DealDamage(new DamageRequest(source, target, status.Stacks, Language.C, false, false));
break;
case StatusType.Segfault:
target.Cycles = Mathf.Max(0, target.Cycles - 1);
break;
case StatusType.Deprecated:
target.Shield = Mathf.Max(0, target.Shield - 1);
break;
case StatusType.UnattendedUpgrades:
target.Shield += status.Stacks;
break;
case StatusType.DependencyError:
case StatusType.Deadlock:
case StatusType.RaceCondition:
break;
}
}
private void DecayDuration(CombatantState target, StatusInstance status)
{
if (status.Duration == -1)
{
return;
}
status.Duration--;
if (status.Duration <= 0)
{
Expire(target, status.Type);
}
} }
} }
} }
+66
View File
@@ -0,0 +1,66 @@
using KernelPanic.Core;
namespace KernelPanic.Combat
{
public enum StatusStackingRule
{
Intensity,
Duration,
Refresh,
Unique
}
public enum StatusTickTiming
{
None,
StartOfTurn,
EndOfTurn
}
public sealed class StatusInstance
{
public StatusInstance(StatusType type, int stacks, int duration, CombatantState source = null, bool skipNextTick = false)
{
Type = type;
Stacks = stacks;
Duration = duration;
Source = source;
SkipNextTick = skipNextTick;
}
public StatusType Type { get; }
public int Stacks { get; set; }
public int Duration { get; set; }
public CombatantState Source { get; set; }
public bool SkipNextTick { get; set; }
}
public readonly struct StatusDescriptor
{
public StatusDescriptor(
StatusType type,
string displayName,
string iconKey,
bool isBeneficial,
StatusStackingRule stackingRule,
StatusTickTiming tickTiming,
string tooltip)
{
Type = type;
DisplayName = displayName;
IconKey = iconKey;
IsBeneficial = isBeneficial;
StackingRule = stackingRule;
TickTiming = tickTiming;
Tooltip = tooltip;
}
public StatusType Type { get; }
public string DisplayName { get; }
public string IconKey { get; }
public bool IsBeneficial { get; }
public StatusStackingRule StackingRule { get; }
public StatusTickTiming TickTiming { get; }
public string Tooltip { get; }
}
}
+182
View File
@@ -12,7 +12,17 @@ namespace KernelPanic.Core
public static event Action<CardResolvedEvent> CardResolved; public static event Action<CardResolvedEvent> CardResolved;
public static event Action<PhaseChangedEvent> PhaseChanged; public static event Action<PhaseChangedEvent> PhaseChanged;
public static event Action<DamageDealtEvent> DamageDealt; public static event Action<DamageDealtEvent> DamageDealt;
public static event Action<CombatantDefeatedEvent> CombatantDefeated;
public static event Action<EnemyWouldActEvent> EnemyWouldAct;
public static event Action<EnemyActedEvent> EnemyActed;
public static event Action<PlayerDamagedEvent> PlayerDamaged;
public static event Action<EncounterWonEvent> EncounterWon;
public static event Action<EncounterLostEvent> EncounterLost;
public static event Action<WaveClearedEvent> WaveCleared; public static event Action<WaveClearedEvent> WaveCleared;
public static event Action<WaveAdvancedEvent> WaveAdvanced;
public static event Action<StatusAppliedEvent> StatusApplied;
public static event Action<StatusExpiredEvent> StatusExpired;
public static event Action<StatusCleansedEvent> StatusCleansed;
public static event Action<RunEndedEvent> RunEnded; public static event Action<RunEndedEvent> RunEnded;
public static void RaiseCardPlayed(CardPlayedEvent payload) public static void RaiseCardPlayed(CardPlayedEvent payload)
@@ -35,11 +45,61 @@ namespace KernelPanic.Core
DamageDealt?.Invoke(payload); DamageDealt?.Invoke(payload);
} }
public static void RaiseCombatantDefeated(CombatantDefeatedEvent payload)
{
CombatantDefeated?.Invoke(payload);
}
public static void RaiseEnemyWouldAct(EnemyWouldActEvent payload)
{
EnemyWouldAct?.Invoke(payload);
}
public static void RaiseEnemyActed(EnemyActedEvent payload)
{
EnemyActed?.Invoke(payload);
}
public static void RaisePlayerDamaged(PlayerDamagedEvent payload)
{
PlayerDamaged?.Invoke(payload);
}
public static void RaiseEncounterWon(EncounterWonEvent payload)
{
EncounterWon?.Invoke(payload);
}
public static void RaiseEncounterLost(EncounterLostEvent payload)
{
EncounterLost?.Invoke(payload);
}
public static void RaiseWaveCleared(WaveClearedEvent payload) public static void RaiseWaveCleared(WaveClearedEvent payload)
{ {
WaveCleared?.Invoke(payload); WaveCleared?.Invoke(payload);
} }
public static void RaiseWaveAdvanced(WaveAdvancedEvent payload)
{
WaveAdvanced?.Invoke(payload);
}
public static void RaiseStatusApplied(StatusAppliedEvent payload)
{
StatusApplied?.Invoke(payload);
}
public static void RaiseStatusExpired(StatusExpiredEvent payload)
{
StatusExpired?.Invoke(payload);
}
public static void RaiseStatusCleansed(StatusCleansedEvent payload)
{
StatusCleansed?.Invoke(payload);
}
public static void RaiseRunEnded(RunEndedEvent payload) public static void RaiseRunEnded(RunEndedEvent payload)
{ {
RunEnded?.Invoke(payload); RunEnded?.Invoke(payload);
@@ -110,6 +170,76 @@ namespace KernelPanic.Core
public Language Language { get; } public Language Language { get; }
} }
/// <summary>
/// Describes a combatant reaching zero uptime through the damage pipeline.
/// </summary>
public readonly struct CombatantDefeatedEvent
{
public CombatantDefeatedEvent(CombatantState combatant)
{
Combatant = combatant;
}
public CombatantState Combatant { get; }
}
/// <summary>
/// Describes an enemy placeholder reaching its structural action point.
/// </summary>
public readonly struct EnemyWouldActEvent
{
public EnemyWouldActEvent(EnemyInstance enemy)
{
Enemy = enemy;
}
public EnemyInstance Enemy { get; }
}
public readonly struct EnemyActedEvent
{
public EnemyActedEvent(EnemyInstance enemy, EnemyIntent intent)
{
Enemy = enemy;
Intent = intent;
}
public EnemyInstance Enemy { get; }
public EnemyIntent Intent { get; }
}
public readonly struct PlayerDamagedEvent
{
public PlayerDamagedEvent(EnemyInstance enemy, int amount)
{
Enemy = enemy;
Amount = amount;
}
public EnemyInstance Enemy { get; }
public int Amount { get; }
}
public readonly struct EncounterWonEvent
{
public EncounterWonEvent(int waveNumber)
{
WaveNumber = waveNumber;
}
public int WaveNumber { get; }
}
public readonly struct EncounterLostEvent
{
public EncounterLostEvent(int waveNumber)
{
WaveNumber = waveNumber;
}
public int WaveNumber { get; }
}
/// <summary> /// <summary>
/// Describes a completed enemy wave. /// Describes a completed enemy wave.
/// </summary> /// </summary>
@@ -123,6 +253,58 @@ namespace KernelPanic.Core
public int WaveNumber { get; } public int WaveNumber { get; }
} }
public readonly struct WaveAdvancedEvent
{
public WaveAdvancedEvent(int waveNumber)
{
WaveNumber = waveNumber;
}
public int WaveNumber { get; }
}
public readonly struct StatusAppliedEvent
{
public StatusAppliedEvent(CombatantState source, CombatantState target, StatusType statusType, int stacks, int duration)
{
Source = source;
Target = target;
StatusType = statusType;
Stacks = stacks;
Duration = duration;
}
public CombatantState Source { get; }
public CombatantState Target { get; }
public StatusType StatusType { get; }
public int Stacks { get; }
public int Duration { get; }
}
public readonly struct StatusExpiredEvent
{
public StatusExpiredEvent(CombatantState target, StatusType statusType)
{
Target = target;
StatusType = statusType;
}
public CombatantState Target { get; }
public StatusType StatusType { get; }
}
public readonly struct StatusCleansedEvent
{
public StatusCleansedEvent(CombatantState target, StatusType statusType)
{
Target = target;
StatusType = statusType;
}
public CombatantState Target { get; }
public StatusType StatusType { get; }
}
/// <summary> /// <summary>
/// Describes the end of a run and its earned meta currency. /// Describes the end of a run and its earned meta currency.
/// </summary> /// </summary>
+1 -1
View File
@@ -16,6 +16,6 @@ namespace KernelPanic.Core
Java, Java,
Go, Go,
Ruby, Ruby,
PHP Php
} }
} }
+2 -1
View File
@@ -10,6 +10,7 @@ namespace KernelPanic.Core
RaceCondition, RaceCondition,
Deprecated, Deprecated,
DependencyError, DependencyError,
Deadlock Deadlock,
UnattendedUpgrades
} }
} }
+2
View File
@@ -19,6 +19,7 @@ namespace KernelPanic.Data
[SerializeField] private ResolutionTrack resolutionTrack; [SerializeField] private ResolutionTrack resolutionTrack;
[SerializeField] private bool distroExclusive; [SerializeField] private bool distroExclusive;
[SerializeField] private bool isToken; [SerializeField] private bool isToken;
[SerializeField] private bool isRunOnly;
[SerializeField, TextArea] private string designNotes; [SerializeField, TextArea] private string designNotes;
[SerializeField] private List<string> requiresSystem = new(); [SerializeField] private List<string> requiresSystem = new();
[SerializeField] private CardDefinition upgradeTarget; [SerializeField] private CardDefinition upgradeTarget;
@@ -34,6 +35,7 @@ namespace KernelPanic.Data
public ResolutionTrack ResolutionTrack => resolutionTrack; public ResolutionTrack ResolutionTrack => resolutionTrack;
public bool DistroExclusive => distroExclusive; public bool DistroExclusive => distroExclusive;
public bool IsToken => isToken; public bool IsToken => isToken;
public bool IsRunOnly => isRunOnly;
public string DesignNotes => designNotes; public string DesignNotes => designNotes;
public IReadOnlyList<string> RequiresSystem => requiresSystem; public IReadOnlyList<string> RequiresSystem => requiresSystem;
public CardDefinition UpgradeTarget => upgradeTarget; public CardDefinition UpgradeTarget => upgradeTarget;
+1 -1
View File
@@ -22,7 +22,7 @@ namespace KernelPanic.Data
new(Language.Java, "Java", "warming vm", ResolutionTrack.Native, "TODO: Java starts expensive and gets cheaper as the combat warms up. Signature mechanic copy pending.", null, Distro("debian", "Debian"), Distro("fedora", "Fedora")), new(Language.Java, "Java", "warming vm", ResolutionTrack.Native, "TODO: Java starts expensive and gets cheaper as the combat warms up. Signature mechanic copy pending.", null, Distro("debian", "Debian"), Distro("fedora", "Fedora")),
new(Language.Go, "Go", "small services", ResolutionTrack.Native, "TODO: Go uses direct native actions with simple scaling and concurrency hooks. Signature mechanic copy pending.", null, Distro("cachyos", "CachyOS"), Distro("nixos", "NixOS")), new(Language.Go, "Go", "small services", ResolutionTrack.Native, "TODO: Go uses direct native actions with simple scaling and concurrency hooks. Signature mechanic copy pending.", null, Distro("cachyos", "CachyOS"), Distro("nixos", "NixOS")),
new(Language.Ruby, "Ruby", "sharp scripts", ResolutionTrack.InterpreterQueue, "TODO: Ruby queues expressive effects with combo-friendly timing. Signature mechanic copy pending.", null, Distro("kali", "Kali"), Distro("endeavouros", "EndeavourOS")), new(Language.Ruby, "Ruby", "sharp scripts", ResolutionTrack.InterpreterQueue, "TODO: Ruby queues expressive effects with combo-friendly timing. Signature mechanic copy pending.", null, Distro("kali", "Kali"), Distro("endeavouros", "EndeavourOS")),
new(Language.PHP, "PHP", "legacy glue", ResolutionTrack.InterpreterQueue, "TODO: PHP queues practical effects that improve when the board is messy. Signature mechanic copy pending.", null, Distro("debian", "Debian"), Distro("cachyos", "CachyOS")), new(Language.Php, "PHP", "legacy glue", ResolutionTrack.InterpreterQueue, "TODO: PHP queues practical effects that improve when the board is messy. Signature mechanic copy pending.", null, Distro("debian", "Debian"), Distro("cachyos", "CachyOS")),
}; };
public static IReadOnlyList<LanguageCatalogEntry> All => Entries; public static IReadOnlyList<LanguageCatalogEntry> All => Entries;
+16 -16
View File
@@ -21,24 +21,24 @@ namespace KernelPanic.Meta
{ {
public const int MaxEquippedCards = 4; public const int MaxEquippedCards = 4;
private readonly IReadOnlyList<DistroDefinition> ownedUnits; private readonly IReadOnlyList<DistroDefinition> _ownedUnits;
private readonly Dictionary<string, List<string>> equippedByDistro = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, List<string>> _equippedByDistro = new(StringComparer.OrdinalIgnoreCase);
public CardLoadout(IReadOnlyList<DistroDefinition> ownedUnits) public CardLoadout(IReadOnlyList<DistroDefinition> ownedUnits)
{ {
this.ownedUnits = ownedUnits ?? Array.Empty<DistroDefinition>(); this._ownedUnits = ownedUnits ?? Array.Empty<DistroDefinition>();
} }
public IReadOnlyList<string> GetEquippedCardIds(string distroId) public IReadOnlyList<string> GetEquippedCardIds(string distroId)
{ {
return equippedByDistro.TryGetValue(distroId, out List<string> equipped) return _equippedByDistro.TryGetValue(distroId, out List<string> equipped)
? equipped ? equipped
: Array.Empty<string>(); : Array.Empty<string>();
} }
public bool HasLoadout(string distroId) public bool HasLoadout(string distroId)
{ {
return !string.IsNullOrWhiteSpace(distroId) && equippedByDistro.ContainsKey(distroId); return !string.IsNullOrWhiteSpace(distroId) && _equippedByDistro.ContainsKey(distroId);
} }
public void EnsureLoadout(DistroDefinition distro) public void EnsureLoadout(DistroDefinition distro)
@@ -48,12 +48,12 @@ namespace KernelPanic.Meta
return; return;
} }
equippedByDistro[distro.Id] = new List<string>(); _equippedByDistro[distro.Id] = new List<string>();
} }
public void ClearAll() public void ClearAll()
{ {
equippedByDistro.Clear(); _equippedByDistro.Clear();
} }
public void ClearLoadout(DistroDefinition distro) public void ClearLoadout(DistroDefinition distro)
@@ -63,7 +63,7 @@ namespace KernelPanic.Meta
return; return;
} }
equippedByDistro[distro.Id] = new List<string>(); _equippedByDistro[distro.Id] = new List<string>();
} }
public bool TryLoad(string distroId, IEnumerable<string> cardIds, out bool skippedInvalid) public bool TryLoad(string distroId, IEnumerable<string> cardIds, out bool skippedInvalid)
@@ -80,7 +80,7 @@ namespace KernelPanic.Meta
foreach (string cardId in cardIds ?? Array.Empty<string>()) foreach (string cardId in cardIds ?? Array.Empty<string>())
{ {
CardDefinition card = FindExclusiveCard(distro, cardId); CardDefinition card = FindExclusiveCard(distro, cardId);
if (card == null || card.IsToken || equipped.Contains(card.Id)) if (card == null || card.IsToken || card.IsRunOnly || equipped.Contains(card.Id))
{ {
skippedInvalid = true; skippedInvalid = true;
continue; continue;
@@ -96,7 +96,7 @@ namespace KernelPanic.Meta
} }
} }
equippedByDistro[distro.Id] = equipped; _equippedByDistro[distro.Id] = equipped;
return true; return true;
} }
@@ -116,14 +116,14 @@ namespace KernelPanic.Meta
return false; return false;
} }
if (card.IsToken) if (card.IsToken || card.IsRunOnly)
{ {
reason = CardLoadoutFailureReason.Token; reason = CardLoadoutFailureReason.Token;
return false; return false;
} }
EnsureLoadout(distro); EnsureLoadout(distro);
List<string> equipped = equippedByDistro[distro.Id]; List<string> equipped = _equippedByDistro[distro.Id];
if (equipped.Contains(card.Id)) if (equipped.Contains(card.Id))
{ {
reason = CardLoadoutFailureReason.Duplicate; reason = CardLoadoutFailureReason.Duplicate;
@@ -151,7 +151,7 @@ namespace KernelPanic.Meta
} }
EnsureLoadout(distro); EnsureLoadout(distro);
List<string> equipped = equippedByDistro[distro.Id]; List<string> equipped = _equippedByDistro[distro.Id];
if (!equipped.Remove(cardId)) if (!equipped.Remove(cardId))
{ {
reason = CardLoadoutFailureReason.NotEquipped; reason = CardLoadoutFailureReason.NotEquipped;
@@ -169,9 +169,9 @@ namespace KernelPanic.Meta
return null; return null;
} }
for (int i = 0; i < ownedUnits.Count; i++) for (int i = 0; i < _ownedUnits.Count; i++)
{ {
DistroDefinition distro = ownedUnits[i]; DistroDefinition distro = _ownedUnits[i];
if (distro != null && string.Equals(distro.Id, distroId, StringComparison.OrdinalIgnoreCase)) if (distro != null && string.Equals(distro.Id, distroId, StringComparison.OrdinalIgnoreCase))
{ {
return distro; return distro;
@@ -191,7 +191,7 @@ namespace KernelPanic.Meta
for (int i = 0; i < distro.ExclusiveCards.Count; i++) for (int i = 0; i < distro.ExclusiveCards.Count; i++)
{ {
CardDefinition card = distro.ExclusiveCards[i]; CardDefinition card = distro.ExclusiveCards[i];
if (card != null && string.Equals(card.Id, cardId, StringComparison.OrdinalIgnoreCase)) if (card != null && !card.IsRunOnly && string.Equals(card.Id, cardId, StringComparison.OrdinalIgnoreCase))
{ {
return card; return card;
} }
+55 -54
View File
@@ -19,6 +19,7 @@ namespace KernelPanic.Meta
public const int FiveStarSoftPityStart = 66; public const int FiveStarSoftPityStart = 66;
public const int FiveStarHardPity = 80; public const int FiveStarHardPity = 80;
public const int EntropyPerPullToken = 100; public const int EntropyPerPullToken = 100;
public const int TestRootCreditsBalance = 10000;
private const double FourStarBaseChance = 0.12d; private const double FourStarBaseChance = 0.12d;
private const double FiveStarBaseChance = 0.008d; private const double FiveStarBaseChance = 0.008d;
@@ -26,10 +27,10 @@ namespace KernelPanic.Meta
private const double DistroChanceOnFeaturedTier = 0.5d; private const double DistroChanceOnFeaturedTier = 0.5d;
private const double LimitedStandardFiveStarChance = 1d / 3d; private const double LimitedStandardFiveStarChance = 1d / 3d;
private readonly List<DistroDefinition> bannerPool = new(); private readonly List<DistroDefinition> _bannerPool = new();
private readonly Dictionary<GachaCurrencyType, int> currencyBalances = new(); private readonly Dictionary<GachaCurrencyType, int> _currencyBalances = new();
private readonly Random random; private readonly Random _random;
private GachaBannerState beginnerState = new(BeginnerBannerId); private GachaBannerState _beginnerState = new(BeginnerBannerId);
public GachaService() public GachaService()
: this(new Random()) : this(new Random())
@@ -38,17 +39,17 @@ namespace KernelPanic.Meta
public GachaService(Random random) public GachaService(Random random)
{ {
this.random = random ?? new Random(); this._random = random ?? new Random();
currencyBalances[GachaCurrencyType.StandardPull] = 0; _currencyBalances[GachaCurrencyType.StandardPull] = 0;
currencyBalances[GachaCurrencyType.LimitedPull] = 0; _currencyBalances[GachaCurrencyType.LimitedPull] = 0;
} }
public int PullTokens => GetCurrencyBalance(GachaCurrencyType.StandardPull); public int PullTokens => GetCurrencyBalance(GachaCurrencyType.StandardPull);
public int LimitedPullTokens => GetCurrencyBalance(GachaCurrencyType.LimitedPull); public int LimitedPullTokens => GetCurrencyBalance(GachaCurrencyType.LimitedPull);
public int RootCredits { get; private set; } public int RootCredits { get; private set; }
public IReadOnlyList<DistroDefinition> BannerPool => bannerPool; public IReadOnlyList<DistroDefinition> BannerPool => _bannerPool;
public GachaBannerState BeginnerState => beginnerState; public GachaBannerState BeginnerState => _beginnerState;
public bool IsBeginnerBannerAvailable => !beginnerState.exhausted && beginnerState.totalPulls < BeginnerMaxPulls && bannerPool.Count > 0; public bool IsBeginnerBannerAvailable => !_beginnerState.exhausted && _beginnerState.totalPulls < BeginnerMaxPulls && _bannerPool.Count > 0;
public event Action BannerPoolChanged; public event Action BannerPoolChanged;
public event Action Changed; public event Action Changed;
@@ -61,18 +62,18 @@ namespace KernelPanic.Meta
} }
data.EnsureLists(); data.EnsureLists();
RootCredits = Math.Max(0, data.rootCredits); RootCredits = Math.Max(TestRootCreditsBalance, data.rootCredits);
SetCurrencyBalanceSilently(GachaCurrencyType.StandardPull, data.standardPullCurrency); SetCurrencyBalanceSilently(GachaCurrencyType.StandardPull, data.standardPullCurrency);
SetCurrencyBalanceSilently(GachaCurrencyType.LimitedPull, data.limitedPullCurrency); SetCurrencyBalanceSilently(GachaCurrencyType.LimitedPull, data.limitedPullCurrency);
beginnerState = data.beginnerBannerState ?? new GachaBannerState(BeginnerBannerId); _beginnerState = data.beginnerBannerState ?? new GachaBannerState(BeginnerBannerId);
beginnerState.bannerId = BeginnerBannerId; _beginnerState.bannerId = BeginnerBannerId;
beginnerState.EnsureLists(); _beginnerState.EnsureLists();
if (beginnerState.totalPulls >= BeginnerMaxPulls) if (_beginnerState.totalPulls >= BeginnerMaxPulls)
{ {
beginnerState.exhausted = true; _beginnerState.exhausted = true;
} }
beginnerState.fiveStarPityCounter = Math.Max(0, beginnerState.fiveStarPityCounter); _beginnerState.fiveStarPityCounter = Math.Max(0, _beginnerState.fiveStarPityCounter);
Changed?.Invoke(); Changed?.Invoke();
} }
@@ -88,12 +89,12 @@ namespace KernelPanic.Meta
data.rootCredits = RootCredits; data.rootCredits = RootCredits;
data.standardPullCurrency = GetCurrencyBalance(GachaCurrencyType.StandardPull); data.standardPullCurrency = GetCurrencyBalance(GachaCurrencyType.StandardPull);
data.limitedPullCurrency = GetCurrencyBalance(GachaCurrencyType.LimitedPull); data.limitedPullCurrency = GetCurrencyBalance(GachaCurrencyType.LimitedPull);
data.beginnerBannerState = CloneState(beginnerState); data.beginnerBannerState = CloneState(_beginnerState);
} }
public int GetCurrencyBalance(GachaCurrencyType currencyType) public int GetCurrencyBalance(GachaCurrencyType currencyType)
{ {
return currencyBalances.TryGetValue(currencyType, out int balance) ? balance : 0; return _currencyBalances.TryGetValue(currencyType, out int balance) ? balance : 0;
} }
public void SetCurrencyBalance(GachaCurrencyType currencyType, int amount) public void SetCurrencyBalance(GachaCurrencyType currencyType, int amount)
@@ -152,19 +153,19 @@ namespace KernelPanic.Meta
public void AddToBannerPool(DistroDefinition distro) public void AddToBannerPool(DistroDefinition distro)
{ {
if (distro == null || bannerPool.Contains(distro)) if (distro == null || _bannerPool.Contains(distro))
{ {
return; return;
} }
bannerPool.Add(distro); _bannerPool.Add(distro);
BannerPoolChanged?.Invoke(); BannerPoolChanged?.Invoke();
Changed?.Invoke(); Changed?.Invoke();
} }
public void SetBeginnerGuaranteedDistros(IReadOnlyList<DistroDefinition> distros) public void SetBeginnerGuaranteedDistros(IReadOnlyList<DistroDefinition> distros)
{ {
beginnerState.guaranteedDistroIds.Clear(); _beginnerState.guaranteedDistroIds.Clear();
if (distros != null) if (distros != null)
{ {
for (int i = 0; i < distros.Count; i++) for (int i = 0; i < distros.Count; i++)
@@ -172,7 +173,7 @@ namespace KernelPanic.Meta
DistroDefinition distro = distros[i]; DistroDefinition distro = distros[i];
if (distro != null && !string.IsNullOrWhiteSpace(distro.Id)) if (distro != null && !string.IsNullOrWhiteSpace(distro.Id))
{ {
beginnerState.guaranteedDistroIds.Add(distro.Id); _beginnerState.guaranteedDistroIds.Add(distro.Id);
} }
} }
} }
@@ -199,7 +200,7 @@ namespace KernelPanic.Meta
return false; return false;
} }
int remainingPulls = BeginnerMaxPulls - beginnerState.totalPulls; int remainingPulls = BeginnerMaxPulls - _beginnerState.totalPulls;
if (pullCount > remainingPulls) if (pullCount > remainingPulls)
{ {
failureReason = $"only {remainingPulls} beginner pull(s) remain"; failureReason = $"only {remainingPulls} beginner pull(s) remain";
@@ -255,13 +256,13 @@ namespace KernelPanic.Meta
List<GachaReward> rewards = new(); List<GachaReward> rewards = new();
for (int i = 0; i < pullCount; i++) for (int i = 0; i < pullCount; i++)
{ {
beginnerState.totalPulls++; _beginnerState.totalPulls++;
rewards.Add(RollBeginnerReward(beginnerState.totalPulls)); rewards.Add(RollBeginnerReward(_beginnerState.totalPulls));
} }
if (beginnerState.totalPulls >= BeginnerMaxPulls) if (_beginnerState.totalPulls >= BeginnerMaxPulls)
{ {
beginnerState.exhausted = true; _beginnerState.exhausted = true;
} }
Changed?.Invoke(); Changed?.Invoke();
@@ -272,8 +273,8 @@ namespace KernelPanic.Meta
{ {
return currencyType switch return currencyType switch
{ {
GachaCurrencyType.LimitedPull => "feature-pull-token", GachaCurrencyType.LimitedPull => "Compute Credits",
_ => "stable-pull-token" _ => "Commits"
}; };
} }
@@ -308,39 +309,39 @@ namespace KernelPanic.Meta
{ {
if (pullNumber == 20) if (pullNumber == 20)
{ {
AdvanceFiveStarPityAfterNonFiveStar(beginnerState); AdvanceFiveStarPityAfterNonFiveStar(_beginnerState);
return RewardGuaranteedBeginnerDistro(0, "20-pull starter guarantee"); return RewardGuaranteedBeginnerDistro(0, "20-pull starter guarantee");
} }
if (pullNumber == 40) if (pullNumber == 40)
{ {
AdvanceFiveStarPityAfterNonFiveStar(beginnerState); AdvanceFiveStarPityAfterNonFiveStar(_beginnerState);
return RewardGuaranteedBeginnerDistro(1, "40-pull starter guarantee"); return RewardGuaranteedBeginnerDistro(1, "40-pull starter guarantee");
} }
if (pullNumber == 50) if (pullNumber == 50)
{ {
beginnerState.pityCounter = 0; _beginnerState.pityCounter = 0;
return GachaReward.FutureStandardFiveStar("future standard 5-star distro", "50-pull beginner capstone"); return GachaReward.FutureStandardFiveStar("future standard 5-star distro", "50-pull beginner capstone");
} }
if (TryRollFiveStar(beginnerState, out bool fiveStarPityTriggered)) if (TryRollFiveStar(_beginnerState, out bool fiveStarPityTriggered))
{ {
return RewardRandomFiveStar(fiveStarPityTriggered); return RewardRandomFiveStar(fiveStarPityTriggered);
} }
bool pityTriggered = beginnerState.pityCounter >= FourStarHardPity - 1; bool pityTriggered = _beginnerState.pityCounter >= FourStarHardPity - 1;
bool hitFourStarOrCharacter = pityTriggered || random.NextDouble() < FourStarBaseChance; bool hitFourStarOrCharacter = pityTriggered || _random.NextDouble() < FourStarBaseChance;
if (!hitFourStarOrCharacter) if (!hitFourStarOrCharacter)
{ {
beginnerState.pityCounter++; _beginnerState.pityCounter++;
return GachaReward.Equipment(3, "3-star equipment", false, false); return GachaReward.Equipment(3, "3-star equipment", false, false);
} }
beginnerState.pityCounter = 0; _beginnerState.pityCounter = 0;
if (bannerPool.Count > 0 && random.NextDouble() < DistroChanceOnFeaturedTier) if (_bannerPool.Count > 0 && _random.NextDouble() < DistroChanceOnFeaturedTier)
{ {
DistroDefinition distro = bannerPool[random.Next(bannerPool.Count)]; DistroDefinition distro = _bannerPool[_random.Next(_bannerPool.Count)];
return GachaReward.DistroReward(distro, 4, pityTriggered, false); return GachaReward.DistroReward(distro, 4, pityTriggered, false);
} }
@@ -349,12 +350,12 @@ namespace KernelPanic.Meta
private bool TryRollFiveStar(GachaBannerState state, out bool pityTriggered) private bool TryRollFiveStar(GachaBannerState state, out bool pityTriggered)
{ {
state ??= beginnerState; state ??= _beginnerState;
int pityPullNumber = Math.Max(0, state.fiveStarPityCounter) + 1; int pityPullNumber = Math.Max(0, state.fiveStarPityCounter) + 1;
pityTriggered = pityPullNumber >= FiveStarSoftPityStart; pityTriggered = pityPullNumber >= FiveStarSoftPityStart;
double chance = GetFiveStarChance(state); double chance = GetFiveStarChance(state);
bool hit = chance >= 1d || random.NextDouble() < chance; bool hit = chance >= 1d || _random.NextDouble() < chance;
if (!hit) if (!hit)
{ {
state.fiveStarPityCounter++; state.fiveStarPityCounter++;
@@ -376,18 +377,18 @@ namespace KernelPanic.Meta
private GachaReward RewardRandomFiveStar(bool pityTriggered) private GachaReward RewardRandomFiveStar(bool pityTriggered)
{ {
if (bannerPool.Count == 0 || random.NextDouble() >= DistroChanceOnFeaturedTier) if (_bannerPool.Count == 0 || _random.NextDouble() >= DistroChanceOnFeaturedTier)
{ {
return GachaReward.Equipment(5, "5-star equipment", pityTriggered, false); return GachaReward.Equipment(5, "5-star equipment", pityTriggered, false);
} }
DistroDefinition distro = bannerPool.Count == 0 ? null : bannerPool[random.Next(bannerPool.Count)]; DistroDefinition distro = _bannerPool.Count == 0 ? null : _bannerPool[_random.Next(_bannerPool.Count)];
return GachaReward.DistroReward(distro, 5, pityTriggered, false); return GachaReward.DistroReward(distro, 5, pityTriggered, false);
} }
public bool ResolveFeaturedTierIsDistro() public bool ResolveFeaturedTierIsDistro()
{ {
return random.NextDouble() < DistroChanceOnFeaturedTier; return _random.NextDouble() < DistroChanceOnFeaturedTier;
} }
public bool ResolveLimitedFiveStarUsesStandardPool(GachaBannerState limitedState) public bool ResolveLimitedFiveStarUsesStandardPool(GachaBannerState limitedState)
@@ -403,21 +404,21 @@ namespace KernelPanic.Meta
return false; return false;
} }
bool useStandardPool = random.NextDouble() < LimitedStandardFiveStarChance; bool useStandardPool = _random.NextDouble() < LimitedStandardFiveStarChance;
limitedState.featuredFiveStarGuaranteed = useStandardPool; limitedState.featuredFiveStarGuaranteed = useStandardPool;
return useStandardPool; return useStandardPool;
} }
private GachaReward RewardGuaranteedBeginnerDistro(int guaranteeIndex, string reason) private GachaReward RewardGuaranteedBeginnerDistro(int guaranteeIndex, string reason)
{ {
beginnerState.pityCounter = 0; _beginnerState.pityCounter = 0;
DistroDefinition distro = null; DistroDefinition distro = null;
if (guaranteeIndex >= 0 && guaranteeIndex < beginnerState.guaranteedDistroIds.Count) if (guaranteeIndex >= 0 && guaranteeIndex < _beginnerState.guaranteedDistroIds.Count)
{ {
distro = FindBannerPoolDistro(beginnerState.guaranteedDistroIds[guaranteeIndex]); distro = FindBannerPoolDistro(_beginnerState.guaranteedDistroIds[guaranteeIndex]);
} }
distro ??= bannerPool.Count == 0 ? null : bannerPool[Math.Min(guaranteeIndex, bannerPool.Count - 1)]; distro ??= _bannerPool.Count == 0 ? null : _bannerPool[Math.Min(guaranteeIndex, _bannerPool.Count - 1)];
return distro == null return distro == null
? GachaReward.Equipment(4, "4-star equipment", true, true) ? GachaReward.Equipment(4, "4-star equipment", true, true)
: GachaReward.DistroReward(distro, 4, true, true, reason); : GachaReward.DistroReward(distro, 4, true, true, reason);
@@ -430,9 +431,9 @@ namespace KernelPanic.Meta
return null; return null;
} }
for (int i = 0; i < bannerPool.Count; i++) for (int i = 0; i < _bannerPool.Count; i++)
{ {
DistroDefinition distro = bannerPool[i]; DistroDefinition distro = _bannerPool[i];
if (distro != null && string.Equals(distro.Id, id, StringComparison.OrdinalIgnoreCase)) if (distro != null && string.Equals(distro.Id, id, StringComparison.OrdinalIgnoreCase))
{ {
return distro; return distro;
@@ -444,7 +445,7 @@ namespace KernelPanic.Meta
private void SetCurrencyBalanceSilently(GachaCurrencyType currencyType, int amount) private void SetCurrencyBalanceSilently(GachaCurrencyType currencyType, int amount)
{ {
currencyBalances[currencyType] = Math.Max(0, amount); _currencyBalances[currencyType] = Math.Max(0, amount);
} }
private bool CanPullBeginnerWithEntropy(int pullCount, EntropyWallet wallet, int entropyTokenCount, out string failureReason) private bool CanPullBeginnerWithEntropy(int pullCount, EntropyWallet wallet, int entropyTokenCount, out string failureReason)
@@ -461,7 +462,7 @@ namespace KernelPanic.Meta
return false; return false;
} }
int remainingPulls = BeginnerMaxPulls - beginnerState.totalPulls; int remainingPulls = BeginnerMaxPulls - _beginnerState.totalPulls;
if (pullCount > remainingPulls) if (pullCount > remainingPulls)
{ {
failureReason = $"only {remainingPulls} beginner pull(s) remain"; failureReason = $"only {remainingPulls} beginner pull(s) remain";
+10 -10
View File
@@ -9,12 +9,12 @@ namespace KernelPanic.Meta
/// </summary> /// </summary>
public sealed class PlayerCollection public sealed class PlayerCollection
{ {
private readonly List<DistroDefinition> ownedUnits = new(); private readonly List<DistroDefinition> _ownedUnits = new();
private int featuredIndex; private int _featuredIndex;
// TODO: Populate owned units from SaveService during bootstrap. // TODO: Populate owned units from SaveService during bootstrap.
public IReadOnlyList<DistroDefinition> OwnedUnits => ownedUnits; public IReadOnlyList<DistroDefinition> OwnedUnits => _ownedUnits;
public DistroDefinition FeaturedUnit => ownedUnits.Count == 0 ? null : ownedUnits[Math.Clamp(featuredIndex, 0, ownedUnits.Count - 1)]; public DistroDefinition FeaturedUnit => _ownedUnits.Count == 0 ? null : _ownedUnits[Math.Clamp(_featuredIndex, 0, _ownedUnits.Count - 1)];
public event Action Changed; public event Action Changed;
@@ -26,8 +26,8 @@ namespace KernelPanic.Meta
} }
// Persistence is wired by the owner so this domain class stays IO-free. // Persistence is wired by the owner so this domain class stays IO-free.
ownedUnits.Add(unit); _ownedUnits.Add(unit);
featuredIndex = Math.Clamp(featuredIndex, 0, ownedUnits.Count - 1); _featuredIndex = Math.Clamp(_featuredIndex, 0, _ownedUnits.Count - 1);
Changed?.Invoke(); Changed?.Invoke();
} }
@@ -39,9 +39,9 @@ namespace KernelPanic.Meta
} }
int count = 0; int count = 0;
for (int i = 0; i < ownedUnits.Count; i++) for (int i = 0; i < _ownedUnits.Count; i++)
{ {
DistroDefinition unit = ownedUnits[i]; DistroDefinition unit = _ownedUnits[i];
if (unit != null && string.Equals(unit.Id, unitId, StringComparison.OrdinalIgnoreCase)) if (unit != null && string.Equals(unit.Id, unitId, StringComparison.OrdinalIgnoreCase))
{ {
count++; count++;
@@ -53,12 +53,12 @@ namespace KernelPanic.Meta
public void SelectNextFeatured() public void SelectNextFeatured()
{ {
if (ownedUnits.Count < 2) if (_ownedUnits.Count < 2)
{ {
return; return;
} }
featuredIndex = (featuredIndex + 1) % ownedUnits.Count; _featuredIndex = (_featuredIndex + 1) % _ownedUnits.Count;
} }
} }
} }
+1 -1
View File
@@ -59,7 +59,7 @@ namespace KernelPanic.Meta
{ {
public bool starterChosen; public bool starterChosen;
public int entropyBalance; public int entropyBalance;
public int rootCredits; public int rootCredits = GachaService.TestRootCreditsBalance;
public int standardPullCurrency; public int standardPullCurrency;
public int limitedPullCurrency; public int limitedPullCurrency;
public List<string> ownedUnitIds = new(); public List<string> ownedUnitIds = new();
+157
View File
@@ -0,0 +1,157 @@
using KernelPanic.Combat;
using KernelPanic.Data;
namespace KernelPanic.Run
{
public enum RepositoryOfferKind
{
NewCard,
CardUpgrade,
StatUpgrade
}
public enum CardUpgradeKind
{
CostDown,
MagnitudeUp,
DrawRider
}
public enum RunStatUpgradeKind
{
MaxCycles,
MaxUptime,
Heal,
Ram
}
public sealed class RepositoryOffer
{
private RepositoryOffer(
RepositoryOfferKind kind,
string commandName,
string displayName,
string description,
int price)
{
Kind = kind;
CommandName = commandName;
DisplayName = displayName;
Description = description;
Price = price;
}
public RepositoryOfferKind Kind { get; }
public string CommandName { get; }
public string DisplayName { get; }
public string Description { get; }
public int Price { get; }
public CardDefinition CardDefinition { get; private set; }
public CardInstance TargetCard { get; private set; }
public CardUpgradeKind CardUpgradeKind { get; private set; }
public RunStatUpgradeKind StatUpgradeKind { get; private set; }
public bool Sold { get; private set; }
public static RepositoryOffer NewCard(CardDefinition card, int price)
{
string name = DisplayNameFor(card);
RepositoryOffer offer = new(RepositoryOfferKind.NewCard, SafeCommandName(card?.Id, name), name, RulesTextFor(card), price)
{
CardDefinition = card
};
return offer;
}
public static RepositoryOffer CardUpgrade(CardInstance target, CardUpgradeKind upgradeKind, int price)
{
string cardName = DisplayNameFor(target?.Definition);
string description = UpgradeDescription(target, upgradeKind);
RepositoryOffer offer = new(RepositoryOfferKind.CardUpgrade, SafeCommandName(target?.Definition?.Id, cardName), $"upgrade {cardName}", description, price)
{
TargetCard = target,
CardUpgradeKind = upgradeKind
};
return offer;
}
public static RepositoryOffer StatUpgrade(RunStatUpgradeKind upgradeKind, int price)
{
return new RepositoryOffer(RepositoryOfferKind.StatUpgrade, upgradeKind.ToString().ToLowerInvariant(), StatName(upgradeKind), StatDescription(upgradeKind), price)
{
StatUpgradeKind = upgradeKind
};
}
public void MarkSold()
{
Sold = true;
}
private static string RulesTextFor(CardDefinition card)
{
if (card == null)
{
return "--";
}
CardInstance preview = new(card);
return CardEffectFactory.GetRulesText(preview);
}
private static string UpgradeDescription(CardInstance target, CardUpgradeKind upgradeKind)
{
if (target?.Definition == null)
{
return "--";
}
return upgradeKind switch
{
CardUpgradeKind.CostDown => $"cost {CombatManager.GetCardCost(target)} -> {UnityEngine.Mathf.Max(0, CombatManager.GetCardCost(target) - 1)}",
CardUpgradeKind.MagnitudeUp => $"effect magnitude +{CombatTuning.UpgradeMagnitudeBonus}",
CardUpgradeKind.DrawRider => "add rider: draw 1 after resolving",
_ => "--"
};
}
private static string StatName(RunStatUpgradeKind upgradeKind)
{
return upgradeKind switch
{
RunStatUpgradeKind.MaxCycles => "+1 max cycles",
RunStatUpgradeKind.MaxUptime => "+max uptime",
RunStatUpgradeKind.Heal => "heal uptime",
RunStatUpgradeKind.Ram => "+1 RAM",
_ => upgradeKind.ToString()
};
}
private static string StatDescription(RunStatUpgradeKind upgradeKind)
{
return upgradeKind switch
{
RunStatUpgradeKind.MaxCycles => $"+{CombatTuning.StatUpgradeMaxCycles} max cycles this run",
RunStatUpgradeKind.MaxUptime => $"+{CombatTuning.StatUpgradeMaxUptime} max uptime this run",
RunStatUpgradeKind.Heal => $"heal {CombatTuning.StatUpgradeHeal} uptime now",
RunStatUpgradeKind.Ram => $"+{CombatTuning.StatUpgradeRam} RAM this run",
_ => "--"
};
}
private static string DisplayNameFor(CardDefinition card)
{
if (card == null)
{
return "--";
}
return string.IsNullOrWhiteSpace(card.DisplayName) ? card.Id : card.DisplayName;
}
private static string SafeCommandName(string id, string fallback)
{
string value = string.IsNullOrWhiteSpace(id) ? fallback : id;
return string.IsNullOrWhiteSpace(value) ? "package" : value.Replace(' ', '-');
}
}
}
+2 -2
View File
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using KernelPanic.Data; using KernelPanic.Data;
@@ -11,7 +10,8 @@ namespace KernelPanic.Run
{ {
public IReadOnlyList<RewardChoice> GeneratePostWaveChoices(RunConfig runConfig, int waveNumber) public IReadOnlyList<RewardChoice> GeneratePostWaveChoices(RunConfig runConfig, int waveNumber)
{ {
throw new NotImplementedException(); // TODO: Generate real post-wave rewards once card reward pools and upgrade rules exist.
return new[] { new RewardChoice(RewardChoiceType.Skip, null) };
} }
} }
+7
View File
@@ -10,16 +10,23 @@ namespace KernelPanic.Run
public sealed class RunConfig public sealed class RunConfig
{ {
public RunConfig(DistroDefinition distro, Language primaryLanguage, Language secondaryLanguage, IReadOnlyList<CardDefinition> startingDeck) public RunConfig(DistroDefinition distro, Language primaryLanguage, Language secondaryLanguage, IReadOnlyList<CardDefinition> startingDeck)
: this(distro, primaryLanguage, secondaryLanguage, startingDeck, 0)
{
}
public RunConfig(DistroDefinition distro, Language primaryLanguage, Language secondaryLanguage, IReadOnlyList<CardDefinition> startingDeck, int runSeed)
{ {
Distro = distro; Distro = distro;
PrimaryLanguage = primaryLanguage; PrimaryLanguage = primaryLanguage;
SecondaryLanguage = secondaryLanguage; SecondaryLanguage = secondaryLanguage;
StartingDeck = startingDeck; StartingDeck = startingDeck;
RunSeed = runSeed;
} }
public DistroDefinition Distro { get; } public DistroDefinition Distro { get; }
public Language PrimaryLanguage { get; } public Language PrimaryLanguage { get; }
public Language SecondaryLanguage { get; } public Language SecondaryLanguage { get; }
public IReadOnlyList<CardDefinition> StartingDeck { get; } public IReadOnlyList<CardDefinition> StartingDeck { get; }
public int RunSeed { get; }
} }
} }
+98
View File
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using KernelPanic.Core;
using KernelPanic.Data;
namespace KernelPanic.Run
{
/// <summary>
/// Scene-transition handoff for a just-configured run. This is intentionally a plain static
/// context instead of a ScriptableObject asset because the payload is transient runtime state,
/// not reusable authored content.
/// </summary>
public static class RunContext
{
private static PendingRun pendingRun;
public static bool HasPendingRun => pendingRun != null;
public static void Set(DistroDefinition distro, IReadOnlyList<CardDefinition> equippedCards, Language primaryLanguage, Language secondaryLanguage)
{
pendingRun = new PendingRun(distro, equippedCards, primaryLanguage, secondaryLanguage, Environment.TickCount);
}
public static bool TryCreateRunConfig(LanguageDeckDatabase languageDeckDatabase, out RunConfig config)
{
if (pendingRun == null || pendingRun.Distro == null)
{
config = null;
return false;
}
config = pendingRun.CreateRunConfig(languageDeckDatabase);
pendingRun = null;
return true;
}
private sealed class PendingRun
{
private readonly DistroDefinition distro;
private readonly List<CardDefinition> equippedCards = new();
private readonly Language primaryLanguage;
private readonly Language secondaryLanguage;
private readonly int runSeed;
public PendingRun(DistroDefinition distro, IReadOnlyList<CardDefinition> cards, Language primaryLanguage, Language secondaryLanguage, int runSeed)
{
this.distro = distro;
this.primaryLanguage = primaryLanguage;
this.secondaryLanguage = secondaryLanguage;
this.runSeed = runSeed;
if (cards == null)
{
return;
}
for (int i = 0; i < cards.Count; i++)
{
if (cards[i] != null && !cards[i].IsRunOnly)
{
equippedCards.Add(cards[i]);
}
}
}
public DistroDefinition Distro => distro;
public RunConfig CreateRunConfig(LanguageDeckDatabase languageDeckDatabase)
{
List<CardDefinition> startingDeck = new(equippedCards);
AddLanguageDeck(languageDeckDatabase, primaryLanguage, startingDeck);
AddLanguageDeck(languageDeckDatabase, secondaryLanguage, startingDeck);
return new RunConfig(distro, primaryLanguage, secondaryLanguage, startingDeck, runSeed);
}
private static void AddLanguageDeck(LanguageDeckDatabase languageDeckDatabase, Language language, List<CardDefinition> target)
{
LanguageDeckDefinition deck = languageDeckDatabase == null ? null : languageDeckDatabase.FindByLanguage(language);
if (deck == null)
{
return;
}
for (int entryIndex = 0; entryIndex < deck.Entries.Count; entryIndex++)
{
LanguageDeckDefinition.LanguageDeckEntry entry = deck.Entries[entryIndex];
for (int copy = 0; copy < entry.Count; copy++)
{
if (entry.Card != null && !entry.Card.IsRunOnly)
{
target.Add(entry.Card);
}
}
}
}
}
}
}
+377 -1
View File
@@ -1,5 +1,9 @@
using KernelPanic.Combat; using KernelPanic.Combat;
using KernelPanic.Core; using KernelPanic.Core;
using KernelPanic.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine; using UnityEngine;
namespace KernelPanic.Run namespace KernelPanic.Run
@@ -14,9 +18,30 @@ namespace KernelPanic.Run
[SerializeField] private bool isRunActive; [SerializeField] private bool isRunActive;
[SerializeField] private bool isPlayerDead; [SerializeField] private bool isPlayerDead;
private readonly List<CardInstance> runDeck = new();
private readonly List<RepositoryOffer> repositoryOffers = new();
private readonly HashSet<string> soldOfferKeys = new(StringComparer.OrdinalIgnoreCase);
private int bits;
private int rerollsThisVisit;
private int maxUptimeBonus;
private int maxCyclesBonus;
private int ramBonus;
private bool repositoryVisitActive;
public int CurrentWaveNumber => currentWaveNumber; public int CurrentWaveNumber => currentWaveNumber;
public bool IsRunActive => isRunActive; public bool IsRunActive => isRunActive;
public bool IsPlayerDead => isPlayerDead; public bool IsPlayerDead => isPlayerDead;
public RunConfig CurrentConfig { get; private set; }
public IReadOnlyList<CardInstance> RunDeck => runDeck;
public IReadOnlyList<RepositoryOffer> RepositoryOffers => repositoryOffers;
public bool RepositoryVisitActive => repositoryVisitActive;
public int Bits => bits;
public int RerollCost => CombatTuning.RerollBaseCost + (rerollsThisVisit * CombatTuning.RerollCostStep);
public int MaxUptimeBonus => maxUptimeBonus;
public int MaxCyclesBonus => maxCyclesBonus;
public int RamBonus => ramBonus;
public event Action RepositoryChanged;
private void OnEnable() private void OnEnable()
{ {
@@ -32,25 +57,376 @@ namespace KernelPanic.Run
public void StartRun(RunConfig config) public void StartRun(RunConfig config)
{ {
CurrentConfig = config;
isRunActive = true; isRunActive = true;
isPlayerDead = false; isPlayerDead = false;
currentWaveNumber = 1; currentWaveNumber = 1;
bits = 0;
rerollsThisVisit = 0;
maxUptimeBonus = 0;
maxCyclesBonus = 0;
ramBonus = 0;
InitializeRunDeck(config);
StartCombat(); StartCombat();
} }
public void StartCombat() public void StartCombat()
{ {
combatManager?.StartCombat(); combatManager?.StartCombat(CurrentConfig);
}
public void AddBits(int amount)
{
if (!isRunActive || amount <= 0)
{
return;
}
bits += amount;
RepositoryChanged?.Invoke();
}
public void GenerateRepositoryOffers(CardDatabase cardDatabase, LanguageDeckDatabase languageDeckDatabase)
{
repositoryOffers.Clear();
rerollsThisVisit = 0;
soldOfferKeys.Clear();
FillRepositoryOffers(cardDatabase, languageDeckDatabase);
repositoryVisitActive = true;
RepositoryChanged?.Invoke();
}
public bool RerollRepositoryOffers(CardDatabase cardDatabase, LanguageDeckDatabase languageDeckDatabase)
{
int cost = RerollCost;
if (bits < cost)
{
return false;
}
bits -= cost;
rerollsThisVisit++;
repositoryOffers.Clear();
FillRepositoryOffers(cardDatabase, languageDeckDatabase);
repositoryVisitActive = true;
RepositoryChanged?.Invoke();
return true;
}
public bool BuyOffer(RepositoryOffer offer, CombatantState playerState)
{
if (offer == null || offer.Sold || bits < offer.Price)
{
return false;
}
bool applied = offer.Kind switch
{
RepositoryOfferKind.NewCard => AddCardToRunDeck(offer.CardDefinition),
RepositoryOfferKind.CardUpgrade => ApplyCardUpgrade(offer.TargetCard, offer.CardUpgradeKind),
RepositoryOfferKind.StatUpgrade => ApplyStatUpgrade(offer.StatUpgradeKind, playerState),
_ => false
};
if (!applied)
{
return false;
}
bits -= offer.Price;
offer.MarkSold();
soldOfferKeys.Add(OfferKey(offer));
RepositoryChanged?.Invoke();
return true;
}
public bool RemoveCard(CardInstance card)
{
if (card == null || bits < CombatTuning.RemoveCardCost || runDeck.Count <= 1)
{
return false;
}
if (!runDeck.Remove(card))
{
return false;
}
bits -= CombatTuning.RemoveCardCost;
RepositoryChanged?.Invoke();
return true;
}
public int EffectiveMaxUptime()
{
return Mathf.Max(1, (CurrentConfig?.Distro?.BaseUptime ?? 1) + maxUptimeBonus);
}
public int EffectiveRam()
{
return Mathf.Max(1, (CurrentConfig?.Distro?.BaseRam ?? 1) + ramBonus);
}
public int EffectiveMaxCycles()
{
return Mathf.Max(1, (CurrentConfig?.Distro?.BaseCyclesPerTurn ?? 1) + maxCyclesBonus);
} }
private void HandleWaveCleared(WaveClearedEvent payload) private void HandleWaveCleared(WaveClearedEvent payload)
{ {
currentWaveNumber = Mathf.Max(currentWaveNumber + 1, payload.WaveNumber + 1);
repositoryOffers.Clear();
rerollsThisVisit = 0;
soldOfferKeys.Clear();
repositoryVisitActive = false;
GameEvents.RaiseWaveAdvanced(new WaveAdvancedEvent(currentWaveNumber));
} }
private void HandleRunEnded(RunEndedEvent payload) private void HandleRunEnded(RunEndedEvent payload)
{ {
isRunActive = false; isRunActive = false;
isPlayerDead = payload.PlayerDied; isPlayerDead = payload.PlayerDied;
bits = 0;
repositoryOffers.Clear();
repositoryVisitActive = false;
runDeck.Clear();
maxUptimeBonus = 0;
maxCyclesBonus = 0;
ramBonus = 0;
RepositoryChanged?.Invoke();
}
private void InitializeRunDeck(RunConfig config)
{
runDeck.Clear();
if (config?.StartingDeck == null)
{
return;
}
for (int i = 0; i < config.StartingDeck.Count; i++)
{
CardDefinition definition = config.StartingDeck[i];
if (definition != null && !definition.IsRunOnly)
{
runDeck.Add(new CardInstance(definition));
}
}
}
private void FillRepositoryOffers(CardDatabase cardDatabase, LanguageDeckDatabase languageDeckDatabase)
{
int guard = 0;
while (repositoryOffers.Count < CombatTuning.ShopSize && guard < 80)
{
guard++;
RepositoryOffer offer = CreateRandomOffer(cardDatabase, languageDeckDatabase);
if (offer == null || soldOfferKeys.Contains(OfferKey(offer)) || repositoryOffers.Any(existing => IsDuplicateOffer(existing, offer)))
{
continue;
}
repositoryOffers.Add(offer);
}
}
private RepositoryOffer CreateRandomOffer(CardDatabase cardDatabase, LanguageDeckDatabase languageDeckDatabase)
{
int roll = UnityEngine.Random.Range(0, 100);
if (roll < 45)
{
return CreateCardOffer(cardDatabase, languageDeckDatabase);
}
if (roll < 75)
{
return CreateUpgradeOffer();
}
return CreateStatOffer();
}
private RepositoryOffer CreateCardOffer(CardDatabase cardDatabase, LanguageDeckDatabase languageDeckDatabase)
{
List<CardDefinition> pool = BuildCardOfferPool(cardDatabase, languageDeckDatabase);
if (pool.Count == 0)
{
return null;
}
CardDefinition selected = pool[UnityEngine.Random.Range(0, pool.Count)];
return RepositoryOffer.NewCard(selected, selected.IsRunOnly ? CombatTuning.NewCardOfferCost + 1 : CombatTuning.NewCardOfferCost);
}
private RepositoryOffer CreateUpgradeOffer()
{
List<CardInstance> candidates = runDeck.Where(card => card != null && card.CanReceiveRepositoryUpgrade).ToList();
if (candidates.Count == 0)
{
return CreateStatOffer();
}
CardInstance target = candidates[UnityEngine.Random.Range(0, candidates.Count)];
CardUpgradeKind kind = (CardUpgradeKind)UnityEngine.Random.Range(0, 3);
if (kind == CardUpgradeKind.CostDown && CombatManager.GetCardCost(target) <= 0)
{
kind = CardUpgradeKind.MagnitudeUp;
}
return RepositoryOffer.CardUpgrade(target, kind, CombatTuning.UpgradeOfferCost);
}
private static RepositoryOffer CreateStatOffer()
{
RunStatUpgradeKind kind = (RunStatUpgradeKind)UnityEngine.Random.Range(0, 4);
return RepositoryOffer.StatUpgrade(kind, CombatTuning.StatUpgradeCost);
}
private List<CardDefinition> BuildCardOfferPool(CardDatabase cardDatabase, LanguageDeckDatabase languageDeckDatabase)
{
List<CardDefinition> pool = new();
AddLanguageCards(languageDeckDatabase, CurrentConfig?.PrimaryLanguage ?? Language.Python, pool, 3);
AddLanguageCards(languageDeckDatabase, CurrentConfig?.SecondaryLanguage ?? Language.JavaScript, pool, 3);
if (cardDatabase != null)
{
for (int i = 0; i < cardDatabase.AllCards.Count; i++)
{
CardDefinition card = cardDatabase.AllCards[i];
if (card == null || card.IsToken)
{
continue;
}
if (card.IsRunOnly || card.Language == CurrentConfig.PrimaryLanguage || card.Language == CurrentConfig.SecondaryLanguage)
{
pool.Add(card);
}
}
}
return pool;
}
private static void AddLanguageCards(LanguageDeckDatabase languageDeckDatabase, Language language, List<CardDefinition> pool, int weight)
{
LanguageDeckDefinition deck = languageDeckDatabase == null ? null : languageDeckDatabase.FindByLanguage(language);
if (deck == null)
{
return;
}
for (int i = 0; i < deck.Entries.Count; i++)
{
CardDefinition card = deck.Entries[i].Card;
if (card == null || card.IsToken || card.IsRunOnly)
{
continue;
}
for (int copy = 0; copy < Mathf.Max(1, weight); copy++)
{
pool.Add(card);
}
}
}
private static bool IsDuplicateOffer(RepositoryOffer left, RepositoryOffer right)
{
if (left.Kind != right.Kind)
{
return false;
}
return left.Kind switch
{
RepositoryOfferKind.NewCard => left.CardDefinition == right.CardDefinition,
RepositoryOfferKind.CardUpgrade => left.TargetCard == right.TargetCard && left.CardUpgradeKind == right.CardUpgradeKind,
RepositoryOfferKind.StatUpgrade => left.StatUpgradeKind == right.StatUpgradeKind,
_ => false
};
}
private static string OfferKey(RepositoryOffer offer)
{
if (offer == null)
{
return string.Empty;
}
return offer.Kind switch
{
RepositoryOfferKind.NewCard => $"card:{offer.CardDefinition?.Id}",
RepositoryOfferKind.CardUpgrade => $"upgrade:{offer.TargetCard?.Definition?.Id}:{offer.TargetCard?.GetHashCode()}:{offer.CardUpgradeKind}",
RepositoryOfferKind.StatUpgrade => $"stat:{offer.StatUpgradeKind}",
_ => offer.DisplayName
};
}
private bool AddCardToRunDeck(CardDefinition card)
{
if (card == null)
{
return false;
}
runDeck.Add(new CardInstance(card));
return true;
}
private static bool ApplyCardUpgrade(CardInstance card, CardUpgradeKind kind)
{
if (card == null)
{
return false;
}
return kind switch
{
CardUpgradeKind.CostDown => card.ApplyCostUpgrade(-1),
CardUpgradeKind.MagnitudeUp => card.ApplyMagnitudeUpgrade(CombatTuning.UpgradeMagnitudeBonus),
CardUpgradeKind.DrawRider => card.ApplyDrawRider(1),
_ => false
};
}
private bool ApplyStatUpgrade(RunStatUpgradeKind kind, CombatantState playerState)
{
switch (kind)
{
case RunStatUpgradeKind.MaxCycles:
maxCyclesBonus += CombatTuning.StatUpgradeMaxCycles;
if (playerState != null)
{
playerState.MaxCycles += CombatTuning.StatUpgradeMaxCycles;
playerState.Cycles += CombatTuning.StatUpgradeMaxCycles;
}
return true;
case RunStatUpgradeKind.MaxUptime:
maxUptimeBonus += CombatTuning.StatUpgradeMaxUptime;
if (playerState != null)
{
playerState.MaxUptime += CombatTuning.StatUpgradeMaxUptime;
}
return true;
case RunStatUpgradeKind.Heal:
if (playerState == null)
{
return false;
}
playerState.CurrentUptime = Mathf.Min(playerState.MaxUptime, playerState.CurrentUptime + CombatTuning.StatUpgradeHeal);
return true;
case RunStatUpgradeKind.Ram:
ramBonus += CombatTuning.StatUpgradeRam;
if (playerState != null)
{
playerState.Ram += CombatTuning.StatUpgradeRam;
}
return true;
default:
return false;
}
} }
} }
} }
+31 -31
View File
@@ -18,40 +18,40 @@ namespace KernelPanic.UI
private const float MonospaceTolerance = 0.02f; private const float MonospaceTolerance = 0.02f;
private static readonly Dictionary<FontAsset, float> AdvanceRatioByFont = new(); private static readonly Dictionary<FontAsset, float> AdvanceRatioByFont = new();
private static readonly HashSet<FontAsset> WarnedFonts = new(); private static readonly HashSet<FontAsset> WarnedFonts = new();
private static bool hasDefaultAdvanceRatio; private static bool _hasDefaultAdvanceRatio;
private static float defaultAdvanceRatio; private static float _defaultAdvanceRatio;
private readonly Label label; private readonly Label _label;
private readonly FontAsset font; private readonly FontAsset _font;
private string art; private string _art;
public AsciiArtFitter(Label label, FontAsset font) public AsciiArtFitter(Label label, FontAsset font)
{ {
this.label = label; this._label = label;
this.font = font; this._font = font;
label.RegisterCallback<GeometryChangedEvent>(_ => Fit()); label.RegisterCallback<GeometryChangedEvent>(_ => Fit());
} }
public void SetArt(string value) public void SetArt(string value)
{ {
art = value; _art = value;
Fit(); Fit();
} }
private void Fit() private void Fit()
{ {
if (string.IsNullOrWhiteSpace(art)) if (string.IsNullOrWhiteSpace(_art))
{ {
return; return;
} }
Rect region = label.layout; Rect region = _label.layout;
if (!IsUsable(region.width) || !IsUsable(region.height)) if (!IsUsable(region.width) || !IsUsable(region.height))
{ {
return; return;
} }
GetArtDimensions(art, out int rows, out int columns); GetArtDimensions(_art, out int rows, out int columns);
if (rows <= 0 || columns <= 0) if (rows <= 0 || columns <= 0)
{ {
return; return;
@@ -64,49 +64,49 @@ namespace KernelPanic.UI
float blockWidth = columns * fontSize * advanceRatio; float blockWidth = columns * fontSize * advanceRatio;
float blockHeight = rows * fontSize * LineHeightRatio; float blockHeight = rows * fontSize * LineHeightRatio;
label.style.fontSize = fontSize; _label.style.fontSize = fontSize;
label.style.paddingLeft = Mathf.Max(0f, (region.width - blockWidth) * 0.5f); _label.style.paddingLeft = Mathf.Max(0f, (region.width - blockWidth) * 0.5f);
label.style.paddingTop = Mathf.Max(0f, (region.height - blockHeight) * 0.5f); _label.style.paddingTop = Mathf.Max(0f, (region.height - blockHeight) * 0.5f);
label.style.paddingRight = 0f; _label.style.paddingRight = 0f;
label.style.paddingBottom = 0f; _label.style.paddingBottom = 0f;
} }
private float GetAdvanceRatio() private float GetAdvanceRatio()
{ {
if (font == null && hasDefaultAdvanceRatio) if (_font == null && _hasDefaultAdvanceRatio)
{ {
return defaultAdvanceRatio; return _defaultAdvanceRatio;
} }
if (font != null && AdvanceRatioByFont.TryGetValue(font, out float cached)) if (_font != null && AdvanceRatioByFont.TryGetValue(_font, out float cached))
{ {
return cached; return cached;
} }
float previousFontSize = label.resolvedStyle.fontSize; float previousFontSize = _label.resolvedStyle.fontSize;
label.style.fontSize = ReferenceFontSize; _label.style.fontSize = ReferenceFontSize;
float mWidth = label.MeasureTextSize("MMMMMMMMMM", 0f, VisualElement.MeasureMode.Undefined, 0f, VisualElement.MeasureMode.Undefined).x; float mWidth = _label.MeasureTextSize("MMMMMMMMMM", 0f, VisualElement.MeasureMode.Undefined, 0f, VisualElement.MeasureMode.Undefined).x;
float iWidth = label.MeasureTextSize("iiiiiiiiii", 0f, VisualElement.MeasureMode.Undefined, 0f, VisualElement.MeasureMode.Undefined).x; float iWidth = _label.MeasureTextSize("iiiiiiiiii", 0f, VisualElement.MeasureMode.Undefined, 0f, VisualElement.MeasureMode.Undefined).x;
label.style.fontSize = previousFontSize; _label.style.fontSize = previousFontSize;
float advanceRatio = mWidth > 0f ? mWidth / (10f * ReferenceFontSize) : 0.6f; float advanceRatio = mWidth > 0f ? mWidth / (10f * ReferenceFontSize) : 0.6f;
if (font == null) if (_font == null)
{ {
defaultAdvanceRatio = advanceRatio; _defaultAdvanceRatio = advanceRatio;
hasDefaultAdvanceRatio = true; _hasDefaultAdvanceRatio = true;
} }
else else
{ {
AdvanceRatioByFont[font] = advanceRatio; AdvanceRatioByFont[_font] = advanceRatio;
} }
if (font != null && mWidth > 0f && iWidth > 0f) if (_font != null && mWidth > 0f && iWidth > 0f)
{ {
float difference = Mathf.Abs(mWidth - iWidth) / Mathf.Max(mWidth, iWidth); float difference = Mathf.Abs(mWidth - iWidth) / Mathf.Max(mWidth, iWidth);
if (difference > MonospaceTolerance && WarnedFonts.Add(font)) if (difference > MonospaceTolerance && WarnedFonts.Add(_font))
{ {
Debug.LogWarning($"ASCII art font '{font.name}' is not monospace; art may misalign."); Debug.LogWarning($"ASCII art font '{_font.name}' is not monospace; art may misalign.");
} }
} }
+36 -36
View File
@@ -14,31 +14,31 @@ namespace KernelPanic.UI
private const int ExtraLines = 2; private const int ExtraLines = 2;
private const int TickMilliseconds = 450; private const int TickMilliseconds = 450;
private readonly VisualElement layer; private readonly VisualElement _layer;
private readonly IReadOnlyList<string> sourceLines; private readonly IReadOnlyList<string> _sourceLines;
private readonly List<Label> labels = new(); private readonly List<Label> _labels = new();
private IVisualElementScheduledItem tickSchedule; private IVisualElementScheduledItem _tickSchedule;
private float lastRegionHeight; private float _lastRegionHeight;
private int cursor; private int _cursor;
private bool reducedMotion; private bool _reducedMotion;
private bool started; private bool _started;
public BackgroundLogRingBuffer(VisualElement layer, IReadOnlyList<string> sourceLines) public BackgroundLogRingBuffer(VisualElement layer, IReadOnlyList<string> sourceLines)
{ {
this.layer = layer; this._layer = layer;
this.sourceLines = sourceLines ?? Array.Empty<string>(); this._sourceLines = sourceLines ?? Array.Empty<string>();
this.layer.RegisterCallback<GeometryChangedEvent>(HandleGeometryChanged); this._layer.RegisterCallback<GeometryChangedEvent>(HandleGeometryChanged);
} }
public void Start(bool useReducedMotion) public void Start(bool useReducedMotion)
{ {
reducedMotion = useReducedMotion; _reducedMotion = useReducedMotion;
started = true; _started = true;
cursor = 0; _cursor = 0;
if (lastRegionHeight > 0f) if (_lastRegionHeight > 0f)
{ {
RebuildLabelsForHeight(lastRegionHeight); RebuildLabelsForHeight(_lastRegionHeight);
Render(); Render();
} }
@@ -47,9 +47,9 @@ namespace KernelPanic.UI
public void Stop() public void Stop()
{ {
started = false; _started = false;
tickSchedule?.Pause(); _tickSchedule?.Pause();
tickSchedule = null; _tickSchedule = null;
} }
private void HandleGeometryChanged(GeometryChangedEvent evt) private void HandleGeometryChanged(GeometryChangedEvent evt)
@@ -59,8 +59,8 @@ namespace KernelPanic.UI
return; return;
} }
lastRegionHeight = evt.newRect.height; _lastRegionHeight = evt.newRect.height;
if (!started) if (!_started)
{ {
return; return;
} }
@@ -72,52 +72,52 @@ namespace KernelPanic.UI
private void RebuildLabelsForHeight(float regionHeight) private void RebuildLabelsForHeight(float regionHeight)
{ {
int targetCount = Mathf.Max(ExtraLines, Mathf.CeilToInt(regionHeight / LineHeight) + ExtraLines); int targetCount = Mathf.Max(ExtraLines, Mathf.CeilToInt(regionHeight / LineHeight) + ExtraLines);
if (labels.Count == targetCount) if (_labels.Count == targetCount)
{ {
return; return;
} }
layer.Clear(); _layer.Clear();
labels.Clear(); _labels.Clear();
for (int i = 0; i < targetCount; i++) for (int i = 0; i < targetCount; i++)
{ {
Label label = new() { pickingMode = PickingMode.Ignore }; Label label = new() { pickingMode = PickingMode.Ignore };
label.AddToClassList("background-log-line"); label.AddToClassList("background-log-line");
layer.Add(label); _layer.Add(label);
labels.Add(label); _labels.Add(label);
} }
} }
private void RestartTick() private void RestartTick()
{ {
tickSchedule?.Pause(); _tickSchedule?.Pause();
tickSchedule = null; _tickSchedule = null;
if (reducedMotion || sourceLines.Count == 0) if (_reducedMotion || _sourceLines.Count == 0)
{ {
return; return;
} }
tickSchedule = layer.schedule.Execute(Advance).Every(TickMilliseconds); _tickSchedule = _layer.schedule.Execute(Advance).Every(TickMilliseconds);
} }
private void Advance() private void Advance()
{ {
if (sourceLines.Count == 0) if (_sourceLines.Count == 0)
{ {
return; return;
} }
cursor = (cursor + 1) % sourceLines.Count; _cursor = (_cursor + 1) % _sourceLines.Count;
Render(); Render();
} }
private void Render() private void Render()
{ {
if (sourceLines.Count == 0) if (_sourceLines.Count == 0)
{ {
foreach (Label label in labels) foreach (Label label in _labels)
{ {
label.text = string.Empty; label.text = string.Empty;
} }
@@ -125,9 +125,9 @@ namespace KernelPanic.UI
return; return;
} }
for (int i = 0; i < labels.Count; i++) for (int i = 0; i < _labels.Count; i++)
{ {
labels[i].text = sourceLines[(cursor + i) % sourceLines.Count]; _labels[i].text = _sourceLines[(_cursor + i) % _sourceLines.Count];
} }
} }
} }
+121 -121
View File
@@ -15,50 +15,50 @@ namespace KernelPanic.UI
[Serializable] [Serializable]
public sealed class CollectionScreenController public sealed class CollectionScreenController
{ {
private readonly List<VisualElement> rows = new(); private readonly List<VisualElement> _rows = new();
private VisualElement list; private VisualElement _list;
private VisualElement detail; private VisualElement _detail;
private FontAsset monospaceFont; private FontAsset _monospaceFont;
private LanguageDeckDatabase languageDeckDatabase; private LanguageDeckDatabase _languageDeckDatabase;
private CardDatabase cardDatabase; private CardDatabase _cardDatabase;
private PlayerCollection playerCollection; private PlayerCollection _playerCollection;
private IReadOnlyList<DistroDefinition> units = Array.Empty<DistroDefinition>(); private IReadOnlyList<DistroDefinition> _units = Array.Empty<DistroDefinition>();
private IReadOnlyList<LanguageCatalogEntry> languages = LanguageCatalog.All; private IReadOnlyList<LanguageCatalogEntry> _languages = LanguageCatalog.All;
private IReadOnlyList<CardEntry> cards = Array.Empty<CardEntry>(); private IReadOnlyList<CardEntry> _cards = Array.Empty<CardEntry>();
private CollectionMode mode; private CollectionMode _mode;
private CollectionMode returnMode; private CollectionMode _returnMode;
private int selectedUnitIndex; private int _selectedUnitIndex;
private int selectedLanguageIndex; private int _selectedLanguageIndex;
private int selectedCardIndex; private int _selectedCardIndex;
private string emptyCardMessage = "no cards installed"; private string _emptyCardMessage = "no cards installed";
public event Action ViewChanged; 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)
{ {
monospaceFont = artFont; _monospaceFont = artFont;
languageDeckDatabase = deckDatabase; _languageDeckDatabase = deckDatabase;
cardDatabase = cardsDatabase; _cardDatabase = cardsDatabase;
playerCollection = collection; _playerCollection = collection;
list = root.Q<VisualElement>("CollectionList"); _list = root.Q<VisualElement>("CollectionList");
detail = root.Q<VisualElement>("CollectionDetail"); _detail = root.Q<VisualElement>("CollectionDetail");
} }
public bool IsCardSubview => mode == CollectionMode.CardSubview; public bool IsCardSubview => _mode == CollectionMode.CardSubview;
public string CurrentTitle { get; private set; } = "$ ls ~/collection"; public string CurrentTitle { get; private set; } = "$ ls ~/collection";
public string CurrentHint { get; private set; } = "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select"; public string CurrentHint { get; private set; } = "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select";
public void RefreshUnits(IReadOnlyList<DistroDefinition> ownedUnits) public void RefreshUnits(IReadOnlyList<DistroDefinition> ownedUnits)
{ {
units = ownedUnits ?? Array.Empty<DistroDefinition>(); _units = ownedUnits ?? Array.Empty<DistroDefinition>();
ShowUnits(); ShowUnits();
} }
public void ShowUnits() public void ShowUnits()
{ {
mode = CollectionMode.Units; _mode = CollectionMode.Units;
CurrentTitle = "$ ls ~/collection"; CurrentTitle = "$ ls ~/collection";
CurrentHint = "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select"; CurrentHint = "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select";
RenderUnits(); RenderUnits();
@@ -67,7 +67,7 @@ namespace KernelPanic.UI
public void ShowLanguages() public void ShowLanguages()
{ {
mode = CollectionMode.Languages; _mode = CollectionMode.Languages;
CurrentTitle = "$ ls ~/collection/languages"; CurrentTitle = "$ ls ~/collection/languages";
CurrentHint = "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select"; CurrentHint = "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select";
RenderLanguages(); RenderLanguages();
@@ -81,7 +81,7 @@ namespace KernelPanic.UI
return false; return false;
} }
if (returnMode == CollectionMode.Languages) if (_returnMode == CollectionMode.Languages)
{ {
ShowLanguages(); ShowLanguages();
} }
@@ -95,33 +95,33 @@ namespace KernelPanic.UI
public void SelectRelative(int delta) public void SelectRelative(int delta)
{ {
if (mode == CollectionMode.Units) if (_mode == CollectionMode.Units)
{ {
SelectUnit(selectedUnitIndex + delta); SelectUnit(_selectedUnitIndex + delta);
return; return;
} }
if (mode == CollectionMode.Languages) if (_mode == CollectionMode.Languages)
{ {
SelectLanguage(selectedLanguageIndex + delta); SelectLanguage(_selectedLanguageIndex + delta);
return; return;
} }
SelectCard(selectedCardIndex + delta); SelectCard(_selectedCardIndex + delta);
} }
public void ActivateSelected() public void ActivateSelected()
{ {
if (mode == CollectionMode.Units && units.Count > 0) if (_mode == CollectionMode.Units && _units.Count > 0)
{ {
OpenUnitCards(units[selectedUnitIndex]); OpenUnitCards(_units[_selectedUnitIndex]);
return; return;
} }
if (mode == CollectionMode.Languages && languages.Count > 0) if (_mode == CollectionMode.Languages && _languages.Count > 0)
{ {
LanguageCatalogEntry language = languages[selectedLanguageIndex]; LanguageCatalogEntry language = _languages[_selectedLanguageIndex];
if (LanguageUnlock.IsUnlocked(language.Language, playerCollection)) if (LanguageUnlock.IsUnlocked(language.Language, _playerCollection))
{ {
OpenLanguageDeck(language); OpenLanguageDeck(language);
} }
@@ -130,27 +130,27 @@ namespace KernelPanic.UI
private void RenderUnits() private void RenderUnits()
{ {
if (list == null || detail == null) if (_list == null || _detail == null)
{ {
return; return;
} }
rows.Clear(); _rows.Clear();
list.Clear(); _list.Clear();
if (units.Count == 0) if (_units.Count == 0)
{ {
detail.Clear(); _detail.Clear();
detail.Add(new Label("no units installed") { name = "CollectionEmptyTitle" }); _detail.Add(new Label("no units installed") { name = "CollectionEmptyTitle" });
detail.Add(new Label("run: curl gacha.sh | sh") { name = "CollectionEmptyHint" }); _detail.Add(new Label("run: curl gacha.sh | sh") { name = "CollectionEmptyHint" });
return; return;
} }
selectedUnitIndex = Mathf.Clamp(selectedUnitIndex, 0, units.Count - 1); _selectedUnitIndex = Mathf.Clamp(_selectedUnitIndex, 0, _units.Count - 1);
for (int i = 0; i < units.Count; i++) for (int i = 0; i < _units.Count; i++)
{ {
int index = i; int index = i;
DistroDefinition unit = units[i]; DistroDefinition unit = _units[i];
VisualElement row = new(); VisualElement row = new();
row.AddToClassList("collection-row"); row.AddToClassList("collection-row");
row.RegisterCallback<PointerEnterEvent>(_ => SelectUnit(index)); row.RegisterCallback<PointerEnterEvent>(_ => SelectUnit(index));
@@ -165,29 +165,29 @@ namespace KernelPanic.UI
row.Add(name); row.Add(name);
row.Add(unitLanguages); row.Add(unitLanguages);
list.Add(row); _list.Add(row);
rows.Add(row); _rows.Add(row);
} }
SelectUnit(selectedUnitIndex); SelectUnit(_selectedUnitIndex);
} }
private void RenderLanguages() private void RenderLanguages()
{ {
if (list == null || detail == null) if (_list == null || _detail == null)
{ {
return; return;
} }
rows.Clear(); _rows.Clear();
list.Clear(); _list.Clear();
selectedLanguageIndex = Mathf.Clamp(selectedLanguageIndex, 0, languages.Count - 1); _selectedLanguageIndex = Mathf.Clamp(_selectedLanguageIndex, 0, _languages.Count - 1);
for (int i = 0; i < languages.Count; i++) for (int i = 0; i < _languages.Count; i++)
{ {
int index = i; int index = i;
LanguageCatalogEntry language = languages[i]; LanguageCatalogEntry language = _languages[i];
bool unlocked = LanguageUnlock.IsUnlocked(language.Language, playerCollection); bool unlocked = LanguageUnlock.IsUnlocked(language.Language, _playerCollection);
VisualElement row = new(); VisualElement row = new();
row.AddToClassList("collection-row"); row.AddToClassList("collection-row");
row.EnableInClassList("locked", !unlocked); row.EnableInClassList("locked", !unlocked);
@@ -202,72 +202,72 @@ namespace KernelPanic.UI
row.Add(name); row.Add(name);
row.Add(tag); row.Add(tag);
list.Add(row); _list.Add(row);
rows.Add(row); _rows.Add(row);
} }
SelectLanguage(selectedLanguageIndex); SelectLanguage(_selectedLanguageIndex);
} }
private void SelectUnit(int index) private void SelectUnit(int index)
{ {
if (units.Count == 0) if (_units.Count == 0)
{ {
return; return;
} }
selectedUnitIndex = Mathf.Clamp(index, 0, units.Count - 1); _selectedUnitIndex = Mathf.Clamp(index, 0, _units.Count - 1);
ApplySelection(selectedUnitIndex); ApplySelection(_selectedUnitIndex);
RenderUnitDetail(units[selectedUnitIndex]); RenderUnitDetail(_units[_selectedUnitIndex]);
} }
private void SelectLanguage(int index) private void SelectLanguage(int index)
{ {
selectedLanguageIndex = Mathf.Clamp(index, 0, languages.Count - 1); _selectedLanguageIndex = Mathf.Clamp(index, 0, _languages.Count - 1);
ApplySelection(selectedLanguageIndex); ApplySelection(_selectedLanguageIndex);
RenderLanguageDetail(languages[selectedLanguageIndex]); RenderLanguageDetail(_languages[_selectedLanguageIndex]);
} }
private void SelectCard(int index) private void SelectCard(int index)
{ {
if (cards.Count == 0) if (_cards.Count == 0)
{ {
return; return;
} }
selectedCardIndex = Mathf.Clamp(index, 0, cards.Count - 1); _selectedCardIndex = Mathf.Clamp(index, 0, _cards.Count - 1);
ApplySelection(selectedCardIndex); ApplySelection(_selectedCardIndex);
RenderCardDetail(cards[selectedCardIndex]); RenderCardDetail(_cards[_selectedCardIndex]);
} }
private void ApplySelection(int selectedIndex) private void ApplySelection(int selectedIndex)
{ {
for (int i = 0; i < rows.Count; i++) for (int i = 0; i < _rows.Count; i++)
{ {
rows[i].EnableInClassList("selected", i == selectedIndex); _rows[i].EnableInClassList("selected", i == selectedIndex);
} }
} }
private void RenderUnitDetail(DistroDefinition unit) private void RenderUnitDetail(DistroDefinition unit)
{ {
detail.Clear(); _detail.Clear();
Label name = new(DistroPresentation.DisplayName(unit)); Label name = new(DistroPresentation.DisplayName(unit));
name.AddToClassList("collection-detail-name"); name.AddToClassList("collection-detail-name");
name.style.color = new StyleColor(unit.AccentColor); name.style.color = new StyleColor(unit.AccentColor);
detail.Add(name); _detail.Add(name);
Label description = new(string.IsNullOrWhiteSpace(unit.Description) ? "--" : unit.Description); Label description = new(string.IsNullOrWhiteSpace(unit.Description) ? "--" : unit.Description);
description.AddToClassList("collection-detail-description"); description.AddToClassList("collection-detail-description");
detail.Add(description); _detail.Add(description);
AddPassiveDetails(detail, unit); AddPassiveDetails(_detail, unit);
VisualElement readout = new(); VisualElement readout = new();
readout.AddToClassList("collection-detail-readout"); readout.AddToClassList("collection-detail-readout");
Label artLabel = new(); Label artLabel = new();
DistroArtPresenter.ConfigureArtLabel(artLabel, monospaceFont); DistroArtPresenter.ConfigureArtLabel(artLabel, _monospaceFont);
AsciiArtFitter artFitter = new(artLabel, monospaceFont); AsciiArtFitter artFitter = new(artLabel, _monospaceFont);
VisualElement artPlaceholder = DistroArtPresenter.CreatePlaceholder(); VisualElement artPlaceholder = DistroArtPresenter.CreatePlaceholder();
artFitter.SetArt(DistroArtPresenter.Render(artLabel, artPlaceholder, unit)); artFitter.SetArt(DistroArtPresenter.Render(artLabel, artPlaceholder, unit));
readout.Add(artPlaceholder); readout.Add(artPlaceholder);
@@ -281,23 +281,23 @@ namespace KernelPanic.UI
details.Add(BuildDetailLine("cycles", unit.BaseCyclesPerTurn.ToString())); details.Add(BuildDetailLine("cycles", unit.BaseCyclesPerTurn.ToString()));
readout.Add(details); readout.Add(details);
detail.Add(readout); _detail.Add(readout);
detail.Add(BuildSubviewCommand($"> cat ~/units/{unit.Id}/cards", "exclusive packages", HasListableCards(unit), () => OpenUnitCards(unit), "no packages installed")); _detail.Add(BuildSubviewCommand($"> cat ~/units/{unit.Id}/cards", "exclusive packages", HasListableCards(unit), () => OpenUnitCards(unit), "no packages installed"));
} }
private void RenderLanguageDetail(LanguageCatalogEntry language) private void RenderLanguageDetail(LanguageCatalogEntry language)
{ {
detail.Clear(); _detail.Clear();
bool unlocked = LanguageUnlock.IsUnlocked(language.Language, playerCollection); bool unlocked = LanguageUnlock.IsUnlocked(language.Language, _playerCollection);
Label name = new(language.DisplayName); Label name = new(language.DisplayName);
name.AddToClassList("collection-detail-name"); name.AddToClassList("collection-detail-name");
detail.Add(name); _detail.Add(name);
detail.Add(BuildDetailLine("track", language.ResolutionTrack.ToString())); _detail.Add(BuildDetailLine("track", language.ResolutionTrack.ToString()));
Label how = new(language.HowItWorks); Label how = new(language.HowItWorks);
how.AddToClassList("collection-detail-description"); how.AddToClassList("collection-detail-description");
detail.Add(how); _detail.Add(how);
if (!unlocked) if (!unlocked)
{ {
@@ -306,11 +306,11 @@ namespace KernelPanic.UI
: language.UnlockHint; : language.UnlockHint;
Label hint = new(hintText); Label hint = new(hintText);
hint.AddToClassList("package-notice"); hint.AddToClassList("package-notice");
detail.Add(hint); _detail.Add(hint);
return; 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) private void OpenUnitCards(DistroDefinition unit)
@@ -320,9 +320,9 @@ namespace KernelPanic.UI
return; return;
} }
returnMode = CollectionMode.Units; _returnMode = CollectionMode.Units;
cards = BuildUnitCardEntries(unit); _cards = BuildUnitCardEntries(unit);
emptyCardMessage = "no packages installed"; _emptyCardMessage = "no packages installed";
CurrentTitle = $"$ cat ~/units/{unit.Id}/cards"; CurrentTitle = $"$ cat ~/units/{unit.Id}/cards";
CurrentHint = "[esc] back [arrows] navigate"; CurrentHint = "[esc] back [arrows] navigate";
RenderCardSubview(); RenderCardSubview();
@@ -331,9 +331,9 @@ namespace KernelPanic.UI
private void OpenLanguageDeck(LanguageCatalogEntry language) private void OpenLanguageDeck(LanguageCatalogEntry language)
{ {
returnMode = CollectionMode.Languages; _returnMode = CollectionMode.Languages;
cards = BuildStarterDeckEntries(language); _cards = BuildStarterDeckEntries(language);
emptyCardMessage = "starter deck not yet available"; _emptyCardMessage = "starter deck not yet available";
CurrentTitle = $"$ cat ~/lang/{GetLanguageId(language.Language)}/deck"; CurrentTitle = $"$ cat ~/lang/{GetLanguageId(language.Language)}/deck";
CurrentHint = "[esc] back [arrows] navigate"; CurrentHint = "[esc] back [arrows] navigate";
RenderCardSubview(); RenderCardSubview();
@@ -342,31 +342,31 @@ namespace KernelPanic.UI
private void RenderCardSubview() private void RenderCardSubview()
{ {
mode = CollectionMode.CardSubview; _mode = CollectionMode.CardSubview;
rows.Clear(); _rows.Clear();
list.Clear(); _list.Clear();
detail.Clear(); _detail.Clear();
selectedCardIndex = Mathf.Clamp(selectedCardIndex, 0, Math.Max(0, cards.Count - 1)); _selectedCardIndex = Mathf.Clamp(_selectedCardIndex, 0, Math.Max(0, _cards.Count - 1));
if (cards.Count == 0) if (_cards.Count == 0)
{ {
detail.Add(new Label(emptyCardMessage) { name = "CollectionEmptyTitle" }); _detail.Add(new Label(_emptyCardMessage) { name = "CollectionEmptyTitle" });
detail.Add(new Label("check back after deck data is installed") { name = "CollectionEmptyHint" }); _detail.Add(new Label("check back after deck data is installed") { name = "CollectionEmptyHint" });
return; return;
} }
for (int i = 0; i < cards.Count; i++) for (int i = 0; i < _cards.Count; i++)
{ {
int index = i; int index = i;
CardEntry entry = cards[i]; CardEntry entry = _cards[i];
VisualElement row = BuildCardRow(entry); VisualElement row = BuildCardRow(entry);
row.RegisterCallback<PointerEnterEvent>(_ => SelectCard(index)); row.RegisterCallback<PointerEnterEvent>(_ => SelectCard(index));
row.RegisterCallback<ClickEvent>(_ => SelectCard(index)); row.RegisterCallback<ClickEvent>(_ => SelectCard(index));
list.Add(row); _list.Add(row);
rows.Add(row); _rows.Add(row);
} }
SelectCard(selectedCardIndex); SelectCard(_selectedCardIndex);
} }
private VisualElement BuildCardRow(CardEntry entry) private VisualElement BuildCardRow(CardEntry entry)
@@ -398,28 +398,28 @@ namespace KernelPanic.UI
private void RenderCardDetail(CardEntry entry) private void RenderCardDetail(CardEntry entry)
{ {
detail.Clear(); _detail.Clear();
CardDefinition card = entry.Card; CardDefinition card = entry.Card;
Label name = new(GetCardDisplayName(card)); Label name = new(GetCardDisplayName(card));
name.AddToClassList("collection-detail-name"); name.AddToClassList("collection-detail-name");
detail.Add(name); _detail.Add(name);
Label description = new(card == null || string.IsNullOrWhiteSpace(card.Description) ? "--" : card.Description); Label description = new(card == null || string.IsNullOrWhiteSpace(card.Description) ? "--" : card.Description);
description.AddToClassList("collection-detail-description"); description.AddToClassList("collection-detail-description");
detail.Add(description); _detail.Add(description);
if (card != null && !string.IsNullOrWhiteSpace(card.FlavorText)) if (card != null && !string.IsNullOrWhiteSpace(card.FlavorText))
{ {
Label flavor = new(card.FlavorText); Label flavor = new(card.FlavorText);
flavor.AddToClassList("flavor-text"); flavor.AddToClassList("flavor-text");
detail.Add(flavor); _detail.Add(flavor);
} }
detail.Add(BuildDetailLine("lang", card == null ? "--" : card.Language.ToString())); _detail.Add(BuildDetailLine("lang", card == null ? "--" : card.Language.ToString()));
detail.Add(BuildDetailLine("rarity", card == null ? "--" : card.Rarity.ToString())); _detail.Add(BuildDetailLine("rarity", card == null ? "--" : card.Rarity.ToString()));
detail.Add(BuildDetailLine("cost", card == null ? "--" : card.CycleCost.ToString())); _detail.Add(BuildDetailLine("cost", card == null ? "--" : card.CycleCost.ToString()));
detail.Add(BuildDetailLine("track", card == null ? "--" : card.ResolutionTrack.ToString())); _detail.Add(BuildDetailLine("track", card == null ? "--" : card.ResolutionTrack.ToString()));
} }
private static VisualElement BuildDetailLine(string key, string value) private static VisualElement BuildDetailLine(string key, string value)
@@ -497,7 +497,7 @@ namespace KernelPanic.UI
for (int cardIndex = 0; cardIndex < unit.ExclusiveCards.Count; cardIndex++) for (int cardIndex = 0; cardIndex < unit.ExclusiveCards.Count; cardIndex++)
{ {
CardDefinition card = unit.ExclusiveCards[cardIndex]; CardDefinition card = unit.ExclusiveCards[cardIndex];
if (card == null || card.IsToken) if (card == null || card.IsToken || card.IsRunOnly)
{ {
continue; continue;
} }
@@ -510,7 +510,7 @@ namespace KernelPanic.UI
private IReadOnlyList<CardEntry> BuildStarterDeckEntries(LanguageCatalogEntry language) private IReadOnlyList<CardEntry> BuildStarterDeckEntries(LanguageCatalogEntry language)
{ {
LanguageDeckDefinition deck = languageDeckDatabase == null ? null : languageDeckDatabase.FindByLanguage(language.Language); LanguageDeckDefinition deck = _languageDeckDatabase == null ? null : _languageDeckDatabase.FindByLanguage(language.Language);
if (deck == null) if (deck == null)
{ {
return BuildRegisteredStarterDeckEntries(language.Language); return BuildRegisteredStarterDeckEntries(language.Language);
@@ -520,7 +520,7 @@ namespace KernelPanic.UI
for (int i = 0; i < deck.Entries.Count; i++) for (int i = 0; i < deck.Entries.Count; i++)
{ {
LanguageDeckDefinition.LanguageDeckEntry deckEntry = deck.Entries[i]; LanguageDeckDefinition.LanguageDeckEntry deckEntry = deck.Entries[i];
if (deckEntry.Card == null || deckEntry.Count <= 0) if (deckEntry.Card == null || deckEntry.Card.IsRunOnly || deckEntry.Count <= 0)
{ {
continue; continue;
} }
@@ -533,7 +533,7 @@ namespace KernelPanic.UI
private IReadOnlyList<CardEntry> BuildRegisteredStarterDeckEntries(Language language) private IReadOnlyList<CardEntry> BuildRegisteredStarterDeckEntries(Language language)
{ {
if (cardDatabase == null) if (_cardDatabase == null)
{ {
return Array.Empty<CardEntry>(); return Array.Empty<CardEntry>();
} }
@@ -557,8 +557,8 @@ namespace KernelPanic.UI
List<CardEntry> entries = new(); List<CardEntry> entries = new();
for (int i = 0; i < cardRefs.Length; i++) for (int i = 0; i < cardRefs.Length; i++)
{ {
CardDefinition card = cardDatabase.FindById(cardRefs[i].Id); CardDefinition card = _cardDatabase.FindById(cardRefs[i].Id);
if (card == null) if (card == null || card.IsRunOnly)
{ {
continue; continue;
} }
File diff suppressed because it is too large Load Diff
+61 -61
View File
@@ -15,43 +15,43 @@ namespace KernelPanic.UI
{ {
private const string EmptyAccent = "5cff91"; private const string EmptyAccent = "5cff91";
private VisualElement panel; private VisualElement _panel;
private Label titleLabel; private Label _titleLabel;
private Label asciiLabel; private Label _asciiLabel;
private VisualElement asciiPlaceholder; private VisualElement _asciiPlaceholder;
private Label unitNameLabel; private Label _unitNameLabel;
private Label languagesLabel; private Label _languagesLabel;
private Label passiveLabel; private Label _passiveLabel;
private Label bestWaveLabel; private Label _bestWaveLabel;
private Label emptyTitleLabel; private Label _emptyTitleLabel;
private Label emptyHintLabel; private Label _emptyHintLabel;
private VisualElement populatedState; private VisualElement _populatedState;
private VisualElement emptyState; private VisualElement _emptyState;
private FontAsset monospaceFont; private FontAsset _monospaceFont;
private AsciiArtFitter asciiFitter; private AsciiArtFitter _asciiFitter;
private IReadOnlyList<DistroDefinition> units = Array.Empty<DistroDefinition>(); private IReadOnlyList<DistroDefinition> _units = Array.Empty<DistroDefinition>();
private int selectedIndex; private int _selectedIndex;
public void Bind(VisualElement root, FontAsset artFont) public void Bind(VisualElement root, FontAsset artFont)
{ {
monospaceFont = artFont; _monospaceFont = artFont;
panel = root.Q<VisualElement>("FeaturedUnitPanel"); _panel = root.Q<VisualElement>("FeaturedUnitPanel");
titleLabel = root.Q<Label>("FeaturedUnitTitle"); _titleLabel = root.Q<Label>("FeaturedUnitTitle");
asciiLabel = root.Q<Label>("FeaturedUnitAscii"); _asciiLabel = root.Q<Label>("FeaturedUnitAscii");
unitNameLabel = root.Q<Label>("FeaturedUnitName"); _unitNameLabel = root.Q<Label>("FeaturedUnitName");
languagesLabel = root.Q<Label>("FeaturedUnitLanguages"); _languagesLabel = root.Q<Label>("FeaturedUnitLanguages");
passiveLabel = root.Q<Label>("FeaturedUnitPassive"); _passiveLabel = root.Q<Label>("FeaturedUnitPassive");
bestWaveLabel = root.Q<Label>("FeaturedUnitBestWave"); _bestWaveLabel = root.Q<Label>("FeaturedUnitBestWave");
emptyTitleLabel = root.Q<Label>("FeaturedUnitEmptyTitle"); _emptyTitleLabel = root.Q<Label>("FeaturedUnitEmptyTitle");
emptyHintLabel = root.Q<Label>("FeaturedUnitEmptyHint"); _emptyHintLabel = root.Q<Label>("FeaturedUnitEmptyHint");
populatedState = root.Q<VisualElement>("FeaturedUnitPopulatedState"); _populatedState = root.Q<VisualElement>("FeaturedUnitPopulatedState");
emptyState = root.Q<VisualElement>("FeaturedUnitEmptyState"); _emptyState = root.Q<VisualElement>("FeaturedUnitEmptyState");
DistroArtPresenter.ConfigureArtLabel(asciiLabel, monospaceFont); DistroArtPresenter.ConfigureArtLabel(_asciiLabel, _monospaceFont);
asciiFitter = new AsciiArtFitter(asciiLabel, monospaceFont); _asciiFitter = new AsciiArtFitter(_asciiLabel, _monospaceFont);
asciiPlaceholder = DistroArtPresenter.CreatePlaceholder(); _asciiPlaceholder = DistroArtPresenter.CreatePlaceholder();
asciiPlaceholder.AddToClassList("hidden"); _asciiPlaceholder.AddToClassList("hidden");
populatedState.Insert(0, asciiPlaceholder); _populatedState.Insert(0, _asciiPlaceholder);
} }
public void Refresh(IReadOnlyList<DistroDefinition> ownedUnits) public void Refresh(IReadOnlyList<DistroDefinition> ownedUnits)
@@ -61,7 +61,7 @@ namespace KernelPanic.UI
public void Refresh(IReadOnlyList<DistroDefinition> ownedUnits, DistroDefinition featuredUnit) public void Refresh(IReadOnlyList<DistroDefinition> ownedUnits, DistroDefinition featuredUnit)
{ {
units = ownedUnits ?? Array.Empty<DistroDefinition>(); _units = ownedUnits ?? Array.Empty<DistroDefinition>();
Refresh(featuredUnit); Refresh(featuredUnit);
} }
@@ -72,25 +72,25 @@ namespace KernelPanic.UI
private void Refresh(DistroDefinition featuredUnit) private void Refresh(DistroDefinition featuredUnit)
{ {
if (units == null || units.Count == 0) if (_units == null || _units.Count == 0)
{ {
ShowEmptyState(); ShowEmptyState();
return; return;
} }
selectedIndex = ResolveSelectedIndex(featuredUnit); _selectedIndex = ResolveSelectedIndex(featuredUnit);
ShowUnit(units[selectedIndex]); ShowUnit(_units[_selectedIndex]);
} }
public void SelectNext() public void SelectNext()
{ {
int unitCount = units?.Count ?? 0; int unitCount = _units?.Count ?? 0;
if (unitCount < 2) if (unitCount < 2)
{ {
return; return;
} }
selectedIndex = (selectedIndex + 1) % unitCount; _selectedIndex = (_selectedIndex + 1) % unitCount;
Refresh(); Refresh();
} }
@@ -98,40 +98,40 @@ namespace KernelPanic.UI
{ {
if (featuredUnit != null) if (featuredUnit != null)
{ {
for (int i = 0; i < units.Count; i++) for (int i = 0; i < _units.Count; i++)
{ {
if (units[i] == featuredUnit) if (_units[i] == featuredUnit)
{ {
return i; return i;
} }
} }
} }
return Mathf.Clamp(selectedIndex, 0, units.Count - 1); return Mathf.Clamp(_selectedIndex, 0, _units.Count - 1);
} }
private void ShowEmptyState() private void ShowEmptyState()
{ {
populatedState.AddToClassList("hidden"); _populatedState.AddToClassList("hidden");
emptyState.RemoveFromClassList("hidden"); _emptyState.RemoveFromClassList("hidden");
titleLabel.text = "[ neofetch ]"; _titleLabel.text = "[ neofetch ]";
emptyTitleLabel.text = "no units installed"; _emptyTitleLabel.text = "no units installed";
emptyHintLabel.text = "run: curl gacha.sh | sh"; _emptyHintLabel.text = "run: curl gacha.sh | sh";
SetAccent(EmptyAccent); SetAccent(EmptyAccent);
} }
private void ShowUnit(DistroDefinition unit) private void ShowUnit(DistroDefinition unit)
{ {
populatedState.RemoveFromClassList("hidden"); _populatedState.RemoveFromClassList("hidden");
emptyState.AddToClassList("hidden"); _emptyState.AddToClassList("hidden");
titleLabel.text = "[ neofetch ]"; _titleLabel.text = "[ neofetch ]";
asciiFitter.SetArt(DistroArtPresenter.Render(asciiLabel, asciiPlaceholder, unit)); _asciiFitter.SetArt(DistroArtPresenter.Render(_asciiLabel, _asciiPlaceholder, unit));
unitNameLabel.text = DistroPresentation.DisplayName(unit); _unitNameLabel.text = DistroPresentation.DisplayName(unit);
languagesLabel.text = DistroPresentation.FormatLanguages(unit); _languagesLabel.text = DistroPresentation.FormatLanguages(unit);
passiveLabel.text = unit.Passive == null || string.IsNullOrWhiteSpace(unit.Passive.Name) ? "--" : unit.Passive.Name; _passiveLabel.text = unit.Passive == null || string.IsNullOrWhiteSpace(unit.Passive.Name) ? "--" : unit.Passive.Name;
bestWaveLabel.text = "--"; // TODO: Bind best-wave stats from SaveService when stats exist. _bestWaveLabel.text = "--"; // TODO: Bind best-wave stats from SaveService when stats exist.
SetAccent(ColorUtility.ToHtmlStringRGB(unit.AccentColor)); SetAccent(ColorUtility.ToHtmlStringRGB(unit.AccentColor));
} }
@@ -139,12 +139,12 @@ namespace KernelPanic.UI
{ {
string color = $"#{htmlColor}"; string color = $"#{htmlColor}";
Color parsedColor = ColorUtility.TryParseHtmlString(color, out Color parsed) ? parsed : Color.green; Color parsedColor = ColorUtility.TryParseHtmlString(color, out Color parsed) ? parsed : Color.green;
panel.style.borderTopColor = new StyleColor(parsedColor); _panel.style.borderTopColor = new StyleColor(parsedColor);
panel.style.borderRightColor = new StyleColor(parsedColor); _panel.style.borderRightColor = new StyleColor(parsedColor);
panel.style.borderBottomColor = new StyleColor(parsedColor); _panel.style.borderBottomColor = new StyleColor(parsedColor);
panel.style.borderLeftColor = new StyleColor(parsedColor); _panel.style.borderLeftColor = new StyleColor(parsedColor);
titleLabel.style.color = new StyleColor(parsedColor); _titleLabel.style.color = new StyleColor(parsedColor);
unitNameLabel.style.color = new StyleColor(parsedColor); _unitNameLabel.style.color = new StyleColor(parsedColor);
} }
} }
} }
+109 -109
View File
@@ -16,49 +16,49 @@ namespace KernelPanic.UI
{ {
private const string SelectedClassName = "selected"; private const string SelectedClassName = "selected";
private readonly List<VisualElement> bannerRows = new(); private readonly List<VisualElement> _bannerRows = new();
private readonly string[] bannerIds = private readonly string[] _bannerIds =
{ {
GachaService.BeginnerBannerId, GachaService.BeginnerBannerId,
GachaService.StandardBannerId, GachaService.StandardBannerId,
GachaService.LimitedBannerId GachaService.LimitedBannerId
}; };
private VisualElement bannerList; private VisualElement _bannerList;
private VisualElement bannerDetail; private VisualElement _bannerDetail;
private DistroDatabase distroDatabase; private DistroDatabase _distroDatabase;
private FontAsset monospaceFont; private FontAsset _monospaceFont;
private GachaService gachaService; private GachaService _gachaService;
private PlayerCollection playerCollection; private PlayerCollection _playerCollection;
private EntropyWallet wallet; private EntropyWallet _wallet;
private Action onChanged; private Action _onChanged;
private Action requestRootCreditExchange; private Action _requestRootCreditExchange;
private int selectedBannerIndex; private int _selectedBannerIndex;
private int pendingPullCount; private int _pendingPullCount;
private int pendingMissingTokens; private int _pendingMissingTokens;
private bool hasOpened; private bool _hasOpened;
private string resultBannerId; private string _resultBannerId;
private string resultText; 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, Action changedCallback, Action rootCreditExchangeCallback)
{ {
distroDatabase = database; _distroDatabase = database;
monospaceFont = artFont; _monospaceFont = artFont;
gachaService = service; _gachaService = service;
playerCollection = collection; _playerCollection = collection;
wallet = entropyWallet; _wallet = entropyWallet;
onChanged = changedCallback; _onChanged = changedCallback;
requestRootCreditExchange = rootCreditExchangeCallback; _requestRootCreditExchange = rootCreditExchangeCallback;
bannerList = root.Q<VisualElement>("GachaBannerList"); _bannerList = root.Q<VisualElement>("GachaBannerList");
bannerDetail = root.Q<VisualElement>("GachaBannerDetail"); _bannerDetail = root.Q<VisualElement>("GachaBannerDetail");
} }
public void Open() public void Open()
{ {
if (!hasOpened) if (!_hasOpened)
{ {
selectedBannerIndex = 0; _selectedBannerIndex = 0;
hasOpened = true; _hasOpened = true;
} }
Refresh(); Refresh();
@@ -66,24 +66,24 @@ namespace KernelPanic.UI
public void Refresh() public void Refresh()
{ {
if (bannerList == null || bannerDetail == null || gachaService == null) if (_bannerList == null || _bannerDetail == null || _gachaService == null)
{ {
return; return;
} }
bannerRows.Clear(); _bannerRows.Clear();
bannerList.Clear(); _bannerList.Clear();
for (int i = 0; i < bannerIds.Length; i++) for (int i = 0; i < _bannerIds.Length; i++)
{ {
int index = i; int index = i;
VisualElement row = BuildBannerRow(bannerIds[i]); VisualElement row = BuildBannerRow(_bannerIds[i]);
row.RegisterCallback<ClickEvent>(_ => SelectBanner(index)); row.RegisterCallback<ClickEvent>(_ => SelectBanner(index));
bannerList.Add(row); _bannerList.Add(row);
bannerRows.Add(row); _bannerRows.Add(row);
} }
selectedBannerIndex = Mathf.Clamp(selectedBannerIndex, 0, bannerRows.Count - 1); _selectedBannerIndex = Mathf.Clamp(_selectedBannerIndex, 0, _bannerRows.Count - 1);
ApplySelectedBanner(); ApplySelectedBanner();
} }
@@ -111,22 +111,22 @@ namespace KernelPanic.UI
private void SelectBanner(int index) private void SelectBanner(int index)
{ {
if (bannerRows.Count == 0) if (_bannerRows.Count == 0)
{ {
return; return;
} }
selectedBannerIndex = (index + bannerRows.Count) % bannerRows.Count; _selectedBannerIndex = (index + _bannerRows.Count) % _bannerRows.Count;
ApplySelectedBanner(); ApplySelectedBanner();
} }
private void ApplySelectedBanner() private void ApplySelectedBanner()
{ {
for (int i = 0; i < bannerRows.Count; i++) for (int i = 0; i < _bannerRows.Count; i++)
{ {
bool selected = i == selectedBannerIndex; bool selected = i == _selectedBannerIndex;
bannerRows[i].EnableInClassList(SelectedClassName, selected); _bannerRows[i].EnableInClassList(SelectedClassName, selected);
bannerRows[i].Q<Label>(className: "command-cursor").visible = selected; _bannerRows[i].Q<Label>(className: "command-cursor").visible = selected;
} }
RenderSelectedBanner(); RenderSelectedBanner();
@@ -134,8 +134,8 @@ namespace KernelPanic.UI
private void RenderSelectedBanner() private void RenderSelectedBanner()
{ {
bannerDetail.Clear(); _bannerDetail.Clear();
string bannerId = bannerIds[selectedBannerIndex]; string bannerId = _bannerIds[_selectedBannerIndex];
if (bannerId == GachaService.BeginnerBannerId) if (bannerId == GachaService.BeginnerBannerId)
{ {
RenderBeginnerBanner(); RenderBeginnerBanner();
@@ -147,22 +147,22 @@ namespace KernelPanic.UI
private void RenderBeginnerBanner() private void RenderBeginnerBanner()
{ {
GachaBannerState state = gachaService.BeginnerState; GachaBannerState state = _gachaService.BeginnerState;
bannerDetail.Add(BuildBannerHero("beginner install media")); _bannerDetail.Add(BuildBannerHero("beginner install media"));
Label title = new("beginner install media"); Label title = new("beginner install media");
title.AddToClassList("gacha-detail-title"); title.AddToClassList("gacha-detail-title");
bannerDetail.Add(title); _bannerDetail.Add(title);
bannerDetail.Add(BuildInfoLine("entropy", wallet == null ? "--" : wallet.Balance.ToString())); _bannerDetail.Add(BuildInfoLine("entropy", _wallet == null ? "--" : _wallet.Balance.ToString()));
bannerDetail.Add(BuildInfoLine("pulls", $"{state.totalPulls}/{GachaService.BeginnerMaxPulls}")); _bannerDetail.Add(BuildInfoLine("pulls", $"{state.totalPulls}/{GachaService.BeginnerMaxPulls}"));
bannerDetail.Add(BuildInfoLine("4-star pity", $"{state.pityCounter}/{GachaService.FourStarHardPity}")); _bannerDetail.Add(BuildInfoLine("4-star pity", $"{state.pityCounter}/{GachaService.FourStarHardPity}"));
bannerDetail.Add(BuildInfoLine("ten pull", $"{GachaService.BeginnerTenPullCost} stable-pull-tokens for 10 pulls")); _bannerDetail.Add(BuildInfoLine("ten pull", $"{GachaService.BeginnerTenPullCost} Commits for 10 pulls"));
bannerDetail.Add(BuildInfoLine("guarantees", FormatBeginnerGuarantees())); _bannerDetail.Add(BuildInfoLine("guarantees", FormatBeginnerGuarantees()));
Label rules = new("Base result is a 3-star equipment cache. A 4-star equipment cache or starter distro can appear early, with one 4-star+ result guaranteed every 10 pulls."); Label rules = new("Base result is a 3-star equipment cache. A 4-star equipment cache or starter distro can appear early, with one 4-star+ result guaranteed every 10 pulls.");
rules.AddToClassList("gacha-rules"); rules.AddToClassList("gacha-rules");
bannerDetail.Add(rules); _bannerDetail.Add(rules);
VisualElement actions = new(); VisualElement actions = new();
actions.AddToClassList("gacha-actions"); actions.AddToClassList("gacha-actions");
@@ -185,62 +185,62 @@ namespace KernelPanic.UI
tenButton.SetEnabled(CanAttemptBeginnerPull(10)); tenButton.SetEnabled(CanAttemptBeginnerPull(10));
actions.Add(tenButton); actions.Add(tenButton);
bannerDetail.Add(actions); _bannerDetail.Add(actions);
if (pendingPullCount > 0) if (_pendingPullCount > 0)
{ {
bannerDetail.Add(BuildEntropyPrompt()); _bannerDetail.Add(BuildEntropyPrompt());
} }
if (resultBannerId == GachaService.BeginnerBannerId && !string.IsNullOrWhiteSpace(resultText)) if (_resultBannerId == GachaService.BeginnerBannerId && !string.IsNullOrWhiteSpace(_resultText))
{ {
Label result = new(resultText); Label result = new(_resultText);
result.AddToClassList("gacha-result"); result.AddToClassList("gacha-result");
bannerDetail.Add(result); _bannerDetail.Add(result);
} }
} }
private void RenderFutureBanner(string bannerId) private void RenderFutureBanner(string bannerId)
{ {
bannerDetail.Add(BuildBannerHero(GetBannerTitle(bannerId))); _bannerDetail.Add(BuildBannerHero(GetBannerTitle(bannerId)));
Label title = new(GetBannerTitle(bannerId)); Label title = new(GetBannerTitle(bannerId));
title.AddToClassList("gacha-detail-title"); title.AddToClassList("gacha-detail-title");
bannerDetail.Add(title); _bannerDetail.Add(title);
string currency = bannerId == GachaService.StandardBannerId string currency = bannerId == GachaService.StandardBannerId
? GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull) ? GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)
: GachaService.FormatCurrencyName(GachaCurrencyType.LimitedPull); : GachaService.FormatCurrencyName(GachaCurrencyType.LimitedPull);
bannerDetail.Add(BuildInfoLine("pull currency", currency)); _bannerDetail.Add(BuildInfoLine("pull currency", currency));
bannerDetail.Add(BuildInfoLine("status", "not implemented yet")); _bannerDetail.Add(BuildInfoLine("status", "not implemented yet"));
bannerDetail.Add(BuildInfoLine("pity", "own counter, 10-pull 4-star+ guarantee")); _bannerDetail.Add(BuildInfoLine("pity", "own counter, 10-pull 4-star+ guarantee"));
bannerDetail.Add(new Label("Use the beginner implementation as the template: define the banner pool, persist a separate GachaBannerState, then route single/ten pulls through GachaService.") { name = "FutureBannerHint" }); _bannerDetail.Add(new Label("Use the beginner implementation as the template: define the banner pool, persist a separate GachaBannerState, then route single/ten pulls through GachaService.") { name = "FutureBannerHint" });
bannerDetail.ElementAt(bannerDetail.childCount - 1).AddToClassList("gacha-rules"); _bannerDetail.ElementAt(_bannerDetail.childCount - 1).AddToClassList("gacha-rules");
if (resultBannerId == bannerId && !string.IsNullOrWhiteSpace(resultText)) if (_resultBannerId == bannerId && !string.IsNullOrWhiteSpace(_resultText))
{ {
Label result = new(resultText); Label result = new(_resultText);
result.AddToClassList("gacha-result"); result.AddToClassList("gacha-result");
bannerDetail.Add(result); _bannerDetail.Add(result);
} }
} }
private void PullSelectedBanner(int pullCount, int entropyTokenCount) private void PullSelectedBanner(int pullCount, int entropyTokenCount)
{ {
string bannerId = bannerIds[selectedBannerIndex]; string bannerId = _bannerIds[_selectedBannerIndex];
if (bannerId != GachaService.BeginnerBannerId) if (bannerId != GachaService.BeginnerBannerId)
{ {
resultBannerId = bannerId; _resultBannerId = bannerId;
resultText = $"git pull origin {bannerId}: remote not implemented yet"; _resultText = $"git pull origin {bannerId}: remote not implemented yet";
RenderSelectedBanner(); RenderSelectedBanner();
return; return;
} }
GachaPullResult result = gachaService.PerformBeginnerPull(pullCount, wallet, entropyTokenCount); GachaPullResult result = _gachaService.PerformBeginnerPull(pullCount, _wallet, entropyTokenCount);
if (!result.Success) if (!result.Success)
{ {
resultBannerId = bannerId; _resultBannerId = bannerId;
resultText = $"git pull failed: {result.FailureReason}"; _resultText = $"git pull failed: {result.FailureReason}";
RenderSelectedBanner(); RenderSelectedBanner();
return; return;
} }
@@ -255,10 +255,10 @@ namespace KernelPanic.UI
for (int i = 0; i < result.Rewards.Count; i++) for (int i = 0; i < result.Rewards.Count; i++)
{ {
GachaReward reward = result.Rewards[i]; GachaReward reward = result.Rewards[i];
bool duplicate = reward.Distro != null && playerCollection.GetOwnedCount(reward.Distro.Id) > 0; bool duplicate = reward.Distro != null && _playerCollection.GetOwnedCount(reward.Distro.Id) > 0;
if (reward.Distro != null) if (reward.Distro != null)
{ {
playerCollection.Add(reward.Distro); _playerCollection.Add(reward.Distro);
} }
string suffix = reward.Guaranteed ? " guaranteed" : reward.PityTriggered ? " pity" : string.Empty; string suffix = reward.Guaranteed ? " guaranteed" : reward.PityTriggered ? " pity" : string.Empty;
@@ -266,23 +266,23 @@ namespace KernelPanic.UI
lines.Add($"{i + 1:00}: {reward.StarRating}-star {reward.DisplayName}{suffix}{duplicateText}"); lines.Add($"{i + 1:00}: {reward.StarRating}-star {reward.DisplayName}{suffix}{duplicateText}");
} }
resultBannerId = bannerId; _resultBannerId = bannerId;
resultText = string.Join("\n", lines); _resultText = string.Join("\n", lines);
onChanged?.Invoke(); _onChanged?.Invoke();
Refresh(); Refresh();
} }
private void RequestPullSelectedBanner(int pullCount) private void RequestPullSelectedBanner(int pullCount)
{ {
string bannerId = bannerIds[selectedBannerIndex]; string bannerId = _bannerIds[_selectedBannerIndex];
if (bannerId != GachaService.BeginnerBannerId) if (bannerId != GachaService.BeginnerBannerId)
{ {
PullSelectedBanner(pullCount, 0); PullSelectedBanner(pullCount, 0);
return; return;
} }
int cost = gachaService.GetBeginnerPullCost(pullCount); int cost = _gachaService.GetBeginnerPullCost(pullCount);
int missingTokens = gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, cost); int missingTokens = _gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, cost);
if (missingTokens <= 0) if (missingTokens <= 0)
{ {
ClearEntropyPrompt(); ClearEntropyPrompt();
@@ -290,24 +290,24 @@ namespace KernelPanic.UI
return; return;
} }
if (!gachaService.CanCoverMissingPullTokensWithEntropy(wallet, missingTokens)) if (!_gachaService.CanCoverMissingPullTokensWithEntropy(_wallet, missingTokens))
{ {
if (gachaService.RootCredits > 0) if (_gachaService.RootCredits > 0)
{ {
requestRootCreditExchange?.Invoke(); _requestRootCreditExchange?.Invoke();
return; return;
} }
resultBannerId = bannerId; _resultBannerId = bannerId;
resultText = $"git pull failed: need {missingTokens} {GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)} or {missingTokens * GachaService.EntropyPerPullToken} entropy"; _resultText = $"git pull failed: need {missingTokens} {GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)} or {missingTokens * GachaService.EntropyPerPullToken} entropy";
RenderSelectedBanner(); RenderSelectedBanner();
return; return;
} }
pendingPullCount = pullCount; _pendingPullCount = pullCount;
pendingMissingTokens = missingTokens; _pendingMissingTokens = missingTokens;
resultBannerId = bannerId; _resultBannerId = bannerId;
resultText = null; _resultText = null;
RenderSelectedBanner(); RenderSelectedBanner();
} }
@@ -316,8 +316,8 @@ namespace KernelPanic.UI
VisualElement prompt = new(); VisualElement prompt = new();
prompt.AddToClassList("gacha-entropy-prompt"); prompt.AddToClassList("gacha-entropy-prompt");
int entropyCost = pendingMissingTokens * GachaService.EntropyPerPullToken; int entropyCost = _pendingMissingTokens * GachaService.EntropyPerPullToken;
Label copy = new($"missing {pendingMissingTokens} stable-pull-token(s). spend {entropyCost} entropy to complete git pull?"); Label copy = new($"missing {_pendingMissingTokens} Commits. spend {entropyCost} entropy to complete git pull?");
copy.AddToClassList("gacha-result"); copy.AddToClassList("gacha-result");
prompt.Add(copy); prompt.Add(copy);
@@ -338,8 +338,8 @@ namespace KernelPanic.UI
Button confirm = new(() => Button confirm = new(() =>
{ {
int pullCount = pendingPullCount; int pullCount = _pendingPullCount;
int missingTokens = pendingMissingTokens; int missingTokens = _pendingMissingTokens;
ClearEntropyPrompt(); ClearEntropyPrompt();
PullSelectedBanner(pullCount, missingTokens); PullSelectedBanner(pullCount, missingTokens);
}) })
@@ -356,26 +356,26 @@ namespace KernelPanic.UI
private bool CanAttemptBeginnerPull(int pullCount) private bool CanAttemptBeginnerPull(int pullCount)
{ {
if (gachaService.CanPullBeginner(pullCount, out _)) if (_gachaService.CanPullBeginner(pullCount, out _))
{ {
return true; return true;
} }
if (!gachaService.IsBeginnerBannerAvailable || pullCount > GachaService.BeginnerMaxPulls - gachaService.BeginnerState.totalPulls) if (!_gachaService.IsBeginnerBannerAvailable || pullCount > GachaService.BeginnerMaxPulls - _gachaService.BeginnerState.totalPulls)
{ {
return false; return false;
} }
int missingTokens = gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, gachaService.GetBeginnerPullCost(pullCount)); int missingTokens = _gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, _gachaService.GetBeginnerPullCost(pullCount));
return missingTokens > 0 && return missingTokens > 0 &&
(gachaService.CanCoverMissingPullTokensWithEntropy(wallet, missingTokens) || (_gachaService.CanCoverMissingPullTokensWithEntropy(_wallet, missingTokens) ||
gachaService.RootCredits > 0); _gachaService.RootCredits > 0);
} }
private void ClearEntropyPrompt() private void ClearEntropyPrompt()
{ {
pendingPullCount = 0; _pendingPullCount = 0;
pendingMissingTokens = 0; _pendingMissingTokens = 0;
} }
private VisualElement BuildBannerHero(string bannerTitle) private VisualElement BuildBannerHero(string bannerTitle)
@@ -386,7 +386,7 @@ namespace KernelPanic.UI
VisualElement artRow = new(); VisualElement artRow = new();
artRow.AddToClassList("gacha-hero-art-row"); artRow.AddToClassList("gacha-hero-art-row");
IReadOnlyList<DistroDefinition> distros = distroDatabase == null ? Array.Empty<DistroDefinition>() : distroDatabase.AllDistros; IReadOnlyList<DistroDefinition> distros = _distroDatabase == null ? Array.Empty<DistroDefinition>() : _distroDatabase.AllDistros;
int artCount = Mathf.Min(3, distros.Count); int artCount = Mathf.Min(3, distros.Count);
for (int i = 0; i < artCount; i++) for (int i = 0; i < artCount; i++)
{ {
@@ -417,11 +417,11 @@ namespace KernelPanic.UI
artShell.AddToClassList("gacha-art-shell"); artShell.AddToClassList("gacha-art-shell");
Label artLabel = new(); Label artLabel = new();
DistroArtPresenter.ConfigureArtLabel(artLabel, monospaceFont); DistroArtPresenter.ConfigureArtLabel(artLabel, _monospaceFont);
artLabel.AddToClassList("gacha-ascii-art"); artLabel.AddToClassList("gacha-ascii-art");
VisualElement placeholder = DistroArtPresenter.CreatePlaceholder(); VisualElement placeholder = DistroArtPresenter.CreatePlaceholder();
placeholder.AddToClassList("gacha-ascii-placeholder"); placeholder.AddToClassList("gacha-ascii-placeholder");
AsciiArtFitter artFitter = new(artLabel, monospaceFont); AsciiArtFitter artFitter = new(artLabel, _monospaceFont);
artFitter.SetArt(DistroArtPresenter.Render(artLabel, placeholder, distro)); artFitter.SetArt(DistroArtPresenter.Render(artLabel, placeholder, distro));
if (distro != null) if (distro != null)
{ {
@@ -445,7 +445,7 @@ namespace KernelPanic.UI
private string FormatBeginnerGuarantees() private string FormatBeginnerGuarantees()
{ {
IReadOnlyList<string> ids = gachaService.BeginnerState.guaranteedDistroIds; IReadOnlyList<string> ids = _gachaService.BeginnerState.guaranteedDistroIds;
string first = ids.Count > 0 ? ids[0] : "--"; string first = ids.Count > 0 ? ids[0] : "--";
string second = ids.Count > 1 ? ids[1] : "--"; string second = ids.Count > 1 ? ids[1] : "--";
return $"20={first}, 40={second}, 50=future standard 5-star"; return $"20={first}, 40={second}, 50=future standard 5-star";
@@ -466,7 +466,7 @@ namespace KernelPanic.UI
{ {
return bannerId switch return bannerId switch
{ {
GachaService.BeginnerBannerId => gachaService.IsBeginnerBannerAvailable ? $"{gachaService.BeginnerState.totalPulls}/{GachaService.BeginnerMaxPulls}" : "closed", GachaService.BeginnerBannerId => _gachaService.IsBeginnerBannerAvailable ? $"{_gachaService.BeginnerState.totalPulls}/{GachaService.BeginnerMaxPulls}" : "closed",
GachaService.StandardBannerId => "soon", GachaService.StandardBannerId => "soon",
GachaService.LimitedBannerId => "soon", GachaService.LimitedBannerId => "soon",
_ => "--" _ => "--"
File diff suppressed because it is too large Load Diff
+13 -13
View File
@@ -8,39 +8,39 @@ namespace KernelPanic.UI
/// </summary> /// </summary>
public sealed class ScreenFrameController public sealed class ScreenFrameController
{ {
private Label titleLabel; private Label _titleLabel;
private Label hintLabel; private Label _hintLabel;
public void Bind(VisualElement panel, string title, string hint, Action onBack) public void Bind(VisualElement panel, string title, string hint, Action onBack)
{ {
titleLabel = panel.Q<Label>(className: "screen-frame-title"); _titleLabel = panel.Q<Label>(className: "screen-frame-title");
hintLabel = panel.Q<Label>(className: "screen-frame-hint"); _hintLabel = panel.Q<Label>(className: "screen-frame-hint");
if (titleLabel != null) if (_titleLabel != null)
{ {
titleLabel.text = title; _titleLabel.text = title;
} }
if (hintLabel != null) if (_hintLabel != null)
{ {
hintLabel.text = hint; _hintLabel.text = hint;
hintLabel.RegisterCallback<ClickEvent>(_ => onBack?.Invoke()); _hintLabel.RegisterCallback<ClickEvent>(_ => onBack?.Invoke());
} }
} }
public void SetHint(string hint) public void SetHint(string hint)
{ {
if (hintLabel != null) if (_hintLabel != null)
{ {
hintLabel.text = hint; _hintLabel.text = hint;
} }
} }
public void SetTitle(string title) public void SetTitle(string title)
{ {
if (titleLabel != null) if (_titleLabel != null)
{ {
titleLabel.text = title; _titleLabel.text = title;
} }
} }
} }
+105 -105
View File
@@ -16,52 +16,52 @@ namespace KernelPanic.UI
{ {
private const int StarterCount = 3; private const int StarterCount = 3;
private readonly List<VisualElement> cards = new(); private readonly List<VisualElement> _cards = new();
private readonly List<Label> names = new(); private readonly List<Label> _names = new();
private readonly List<Label> languages = new(); private readonly List<Label> _languages = new();
private readonly List<Label> descriptions = new(); private readonly List<Label> _descriptions = new();
private VisualElement root; private VisualElement _root;
private VisualElement modal; private VisualElement _modal;
private VisualElement options; private VisualElement _options;
private Label confirmLabel; private Label _confirmLabel;
private Label errorLabel; private Label _errorLabel;
private DistroDatabase database; private DistroDatabase _database;
private Action<DistroDefinition, IReadOnlyList<DistroDefinition>> onConfirmed; private Action<DistroDefinition, IReadOnlyList<DistroDefinition>> _onConfirmed;
private IReadOnlyList<DistroDefinition> activeStarters = Array.Empty<DistroDefinition>(); private IReadOnlyList<DistroDefinition> _activeStarters = Array.Empty<DistroDefinition>();
private int selectedIndex; private int _selectedIndex;
private bool active; private bool _active;
private bool showingError; private bool _showingError;
private bool confirming; private bool _confirming;
private IVisualElementScheduledItem closeSchedule; private IVisualElementScheduledItem _closeSchedule;
public bool IsActive => active; public bool IsActive => _active;
public void Bind(VisualElement documentRoot, DistroDatabase distroDatabase, Action<DistroDefinition, IReadOnlyList<DistroDefinition>> onStarterConfirmed) public void Bind(VisualElement documentRoot, DistroDatabase distroDatabase, Action<DistroDefinition, IReadOnlyList<DistroDefinition>> onStarterConfirmed)
{ {
root = documentRoot; _root = documentRoot;
database = distroDatabase; _database = distroDatabase;
onConfirmed = onStarterConfirmed; _onConfirmed = onStarterConfirmed;
modal = root.Q<VisualElement>("StarterModal"); _modal = _root.Q<VisualElement>("StarterModal");
options = root.Q<VisualElement>("StarterOptions"); _options = _root.Q<VisualElement>("StarterOptions");
confirmLabel = root.Q<Label>("StarterConfirmLabel"); _confirmLabel = _root.Q<Label>("StarterConfirmLabel");
errorLabel = root.Q<Label>("StarterErrorLabel"); _errorLabel = _root.Q<Label>("StarterErrorLabel");
for (int i = 0; i < StarterCount; i++) for (int i = 0; i < StarterCount; i++)
{ {
cards.Add(root.Q<VisualElement>($"StarterOption{i}")); _cards.Add(_root.Q<VisualElement>($"StarterOption{i}"));
names.Add(root.Q<Label>($"StarterName{i}")); _names.Add(_root.Q<Label>($"StarterName{i}"));
languages.Add(root.Q<Label>($"StarterLanguages{i}")); _languages.Add(_root.Q<Label>($"StarterLanguages{i}"));
descriptions.Add(root.Q<Label>($"StarterDescription{i}")); _descriptions.Add(_root.Q<Label>($"StarterDescription{i}"));
} }
for (int i = 0; i < cards.Count; i++) for (int i = 0; i < _cards.Count; i++)
{ {
int index = i; int index = i;
cards[i].RegisterCallback<PointerEnterEvent>(_ => Select(index)); _cards[i].RegisterCallback<PointerEnterEvent>(_ => Select(index));
cards[i].RegisterCallback<ClickEvent>(_ => _cards[i].RegisterCallback<ClickEvent>(_ =>
{ {
Select(index); Select(index);
ConfirmIntent(); ConfirmIntent();
@@ -83,31 +83,31 @@ namespace KernelPanic.UI
return; return;
} }
activeStarters = starters; _activeStarters = starters;
active = true; _active = true;
showingError = false; _showingError = false;
confirming = false; _confirming = false;
errorLabel.AddToClassList("hidden"); _errorLabel.AddToClassList("hidden");
options.RemoveFromClassList("hidden"); _options.RemoveFromClassList("hidden");
confirmLabel.AddToClassList("hidden"); _confirmLabel.AddToClassList("hidden");
modal.RemoveFromClassList("hidden"); _modal.RemoveFromClassList("hidden");
for (int i = 0; i < cards.Count; i++) for (int i = 0; i < _cards.Count; i++)
{ {
DistroDefinition unit = starters[i]; DistroDefinition unit = starters[i];
names[i].text = DistroPresentation.DisplayName(unit); _names[i].text = DistroPresentation.DisplayName(unit);
names[i].style.color = new StyleColor(unit.AccentColor); _names[i].style.color = new StyleColor(unit.AccentColor);
languages[i].text = DistroPresentation.FormatLanguages(unit); _languages[i].text = DistroPresentation.FormatLanguages(unit);
descriptions[i].text = unit.Description; _descriptions[i].text = unit.Description;
} }
Select(0); Select(0);
root.Focus(); _root.Focus();
} }
public void HandleKeyDown(KeyDownEvent evt) public void HandleKeyDown(KeyDownEvent evt)
{ {
if (showingError) if (_showingError)
{ {
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter || if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter ||
evt.keyCode == KeyCode.Escape || evt.keyCode == KeyCode.Space) evt.keyCode == KeyCode.Escape || evt.keyCode == KeyCode.Space)
@@ -120,13 +120,13 @@ namespace KernelPanic.UI
if (evt.keyCode == KeyCode.LeftArrow) if (evt.keyCode == KeyCode.LeftArrow)
{ {
Select(selectedIndex - 1); Select(_selectedIndex - 1);
return; return;
} }
if (evt.keyCode == KeyCode.RightArrow) if (evt.keyCode == KeyCode.RightArrow)
{ {
Select(selectedIndex + 1); Select(_selectedIndex + 1);
return; return;
} }
@@ -139,7 +139,7 @@ namespace KernelPanic.UI
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter) if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{ {
if (confirming) if (_confirming)
{ {
Confirm(); Confirm();
return; return;
@@ -149,7 +149,7 @@ namespace KernelPanic.UI
return; return;
} }
if (evt.keyCode == KeyCode.Y && confirming) if (evt.keyCode == KeyCode.Y && _confirming)
{ {
Confirm(); Confirm();
return; return;
@@ -157,32 +157,32 @@ namespace KernelPanic.UI
if (evt.keyCode == KeyCode.Escape || evt.keyCode == KeyCode.N) if (evt.keyCode == KeyCode.Escape || evt.keyCode == KeyCode.N)
{ {
confirming = false; _confirming = false;
confirmLabel.AddToClassList("hidden"); _confirmLabel.AddToClassList("hidden");
} }
} }
public void PauseSchedules() public void PauseSchedules()
{ {
closeSchedule?.Pause(); _closeSchedule?.Pause();
} }
private bool TryGetStarters(out IReadOnlyList<DistroDefinition> starters, out string error) private bool TryGetStarters(out IReadOnlyList<DistroDefinition> starters, out string error)
{ {
if (database == null) if (_database == null)
{ {
starters = Array.Empty<DistroDefinition>(); starters = Array.Empty<DistroDefinition>();
error = "no DistroDatabase is assigned"; error = "no DistroDatabase is assigned";
return false; return false;
} }
if (!database.TryValidate(out error)) if (!_database.TryValidate(out error))
{ {
starters = Array.Empty<DistroDefinition>(); starters = Array.Empty<DistroDefinition>();
return false; return false;
} }
IReadOnlyList<DistroDefinition> allDistros = database.AllDistros; IReadOnlyList<DistroDefinition> allDistros = _database.AllDistros;
if (allDistros.Count < StarterCount) if (allDistros.Count < StarterCount)
{ {
starters = Array.Empty<DistroDefinition>(); starters = Array.Empty<DistroDefinition>();
@@ -197,103 +197,103 @@ namespace KernelPanic.UI
private void ShowConfigError(string error) private void ShowConfigError(string error)
{ {
active = true; _active = true;
showingError = true; _showingError = true;
confirming = false; _confirming = false;
options.AddToClassList("hidden"); _options.AddToClassList("hidden");
confirmLabel.AddToClassList("hidden"); _confirmLabel.AddToClassList("hidden");
errorLabel.text = $"starter selection unavailable: {error}. press enter to continue."; _errorLabel.text = $"starter selection unavailable: {error}. press enter to continue.";
errorLabel.RemoveFromClassList("hidden"); _errorLabel.RemoveFromClassList("hidden");
modal.RemoveFromClassList("hidden"); _modal.RemoveFromClassList("hidden");
root.Focus(); _root.Focus();
} }
private void Select(int index) private void Select(int index)
{ {
if (activeStarters.Count == 0) if (_activeStarters.Count == 0)
{ {
return; return;
} }
int slotCount = Mathf.Min(activeStarters.Count, cards.Count); int slotCount = Mathf.Min(_activeStarters.Count, _cards.Count);
selectedIndex = (index + slotCount) % slotCount; _selectedIndex = (index + slotCount) % slotCount;
confirming = false; _confirming = false;
confirmLabel.AddToClassList("hidden"); _confirmLabel.AddToClassList("hidden");
for (int i = 0; i < cards.Count; i++) for (int i = 0; i < _cards.Count; i++)
{ {
bool selected = i == selectedIndex; bool selected = i == _selectedIndex;
cards[i].EnableInClassList("selected", selected); _cards[i].EnableInClassList("selected", selected);
if (i < activeStarters.Count && selected) if (i < _activeStarters.Count && selected)
{ {
StyleColor accent = new(activeStarters[i].AccentColor); StyleColor accent = new(_activeStarters[i].AccentColor);
cards[i].style.borderTopColor = accent; _cards[i].style.borderTopColor = accent;
cards[i].style.borderRightColor = accent; _cards[i].style.borderRightColor = accent;
cards[i].style.borderBottomColor = accent; _cards[i].style.borderBottomColor = accent;
cards[i].style.borderLeftColor = accent; _cards[i].style.borderLeftColor = accent;
} }
else else
{ {
cards[i].style.borderTopColor = StyleKeyword.Null; _cards[i].style.borderTopColor = StyleKeyword.Null;
cards[i].style.borderRightColor = StyleKeyword.Null; _cards[i].style.borderRightColor = StyleKeyword.Null;
cards[i].style.borderBottomColor = StyleKeyword.Null; _cards[i].style.borderBottomColor = StyleKeyword.Null;
cards[i].style.borderLeftColor = StyleKeyword.Null; _cards[i].style.borderLeftColor = StyleKeyword.Null;
} }
} }
} }
private void ConfirmIntent() private void ConfirmIntent()
{ {
if (selectedIndex >= activeStarters.Count) if (_selectedIndex >= _activeStarters.Count)
{ {
return; return;
} }
confirming = true; _confirming = true;
confirmLabel.text = $"install {DistroPresentation.DisplayName(activeStarters[selectedIndex])}? this cannot be undone [Y/n]"; _confirmLabel.text = $"install {DistroPresentation.DisplayName(_activeStarters[_selectedIndex])}? this cannot be undone [Y/n]";
confirmLabel.RemoveFromClassList("hidden"); _confirmLabel.RemoveFromClassList("hidden");
} }
private void Confirm() private void Confirm()
{ {
if (selectedIndex >= activeStarters.Count) if (_selectedIndex >= _activeStarters.Count)
{ {
return; return;
} }
DistroDefinition picked = activeStarters[selectedIndex]; DistroDefinition picked = _activeStarters[_selectedIndex];
List<DistroDefinition> remaining = new(); List<DistroDefinition> remaining = new();
for (int i = 0; i < activeStarters.Count; i++) for (int i = 0; i < _activeStarters.Count; i++)
{ {
if (i != selectedIndex) if (i != _selectedIndex)
{ {
remaining.Add(activeStarters[i]); remaining.Add(_activeStarters[i]);
} }
} }
onConfirmed?.Invoke(picked, remaining); _onConfirmed?.Invoke(picked, remaining);
confirmLabel.text = $"installing {picked.Id} (1/1)... done"; _confirmLabel.text = $"installing {picked.Id} (1/1)... done";
confirmLabel.RemoveFromClassList("hidden"); _confirmLabel.RemoveFromClassList("hidden");
if (UIPreferences.ReducedMotion) if (UIPreferences.ReducedMotion)
{ {
Close(); Close();
return; return;
} }
closeSchedule?.Pause(); _closeSchedule?.Pause();
closeSchedule = root.schedule.Execute(Close).StartingIn(600); _closeSchedule = _root.schedule.Execute(Close).StartingIn(600);
} }
private void Close() private void Close()
{ {
closeSchedule?.Pause(); _closeSchedule?.Pause();
closeSchedule = null; _closeSchedule = null;
active = false; _active = false;
confirming = false; _confirming = false;
showingError = false; _showingError = false;
modal.AddToClassList("hidden"); _modal.AddToClassList("hidden");
root.Focus(); _root.Focus();
} }
} }
} }