Fix various issues related to styling and animations

This commit is contained in:
2026-07-10 21:28:10 +02:00
parent c973c2d51a
commit 2ba43fb2d3
10 changed files with 2126 additions and 205 deletions
+511 -36
View File
@@ -286,21 +286,33 @@
flex-wrap: wrap;
}
.status-detail-list {
flex-direction: column;
}
.status-detail {
margin-bottom: 7px;
}
.status-pip {
flex-direction: row;
align-items: center;
align-self: flex-start;
min-height: 18px;
margin-right: 5px;
margin-bottom: 5px;
padding-left: 5px;
padding-right: 5px;
padding-top: 2px;
padding-bottom: 2px;
padding-top: 1px;
padding-bottom: 1px;
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);
font-size: 11px;
-unity-text-align: middle-center;
}
.status-pip-harmful {
@@ -321,6 +333,7 @@
color: var(--kp-text-dim);
font-size: 11px;
white-space: normal;
min-height: 14px;
margin-bottom: 4px;
}
@@ -523,6 +536,25 @@
border-left-width: 4px;
}
.enemy-card-defeated-hold {
opacity: 0.34;
background-color: #120303;
border-color: var(--kp-kill);
}
.enemy-card-defeated-hold .enemy-name {
color: var(--kp-danger);
}
.enemy-exited-label {
color: var(--kp-text-dim);
font-size: 11px;
margin-top: 8px;
padding-top: 6px;
border-top-width: 1px;
border-color: var(--kp-border-empty);
}
.enemy-name {
color: var(--kp-text);
-unity-font-style: bold;
@@ -800,6 +832,44 @@
background-color: var(--kp-action-hover);
}
.secondary-action {
height: 34px;
margin-top: 8px;
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-strong);
-unity-font-style: bold;
}
.secondary-action:hover {
color: var(--kp-text);
border-color: var(--kp-warning);
}
.surrender-action {
margin-top: 10px;
}
.surrender-action-confirming {
color: var(--kp-warning);
border-color: var(--kp-warning);
}
.surrender-confirm {
color: var(--kp-warning);
white-space: normal;
font-size: 11px;
margin-top: 6px;
}
.hidden {
display: none;
}
.log-line {
color: var(--kp-text-dim);
white-space: normal;
@@ -813,9 +883,13 @@
right: 0;
top: 0;
bottom: 0;
background-color: var(--kp-overlay);
justify-content: center;
align-items: center;
background-color: var(--kp-bg);
justify-content: flex-start;
align-items: stretch;
padding-left: 12px;
padding-right: 12px;
padding-top: 12px;
padding-bottom: 12px;
}
.overlay-title {
@@ -830,10 +904,196 @@
margin-bottom: 6px;
}
.death-overlay {
flex-grow: 1;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
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-subtle);
}
.death-crash-beat {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
opacity: 0.18;
background-color: #160805;
border-left-width: 6px;
border-right-width: 6px;
border-top-width: 2px;
border-bottom-width: 2px;
border-color: var(--kp-danger);
translate: 0 -8px;
transition-property: opacity, translate;
transition-duration: 220ms;
}
.death-crash-beat-settled {
opacity: 0;
translate: 0 0;
}
.death-content {
width: 72%;
max-width: 760px;
min-width: 540px;
padding-left: 26px;
padding-right: 26px;
padding-top: 24px;
padding-bottom: 24px;
border-left-width: 3px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-accent);
background-color: rgba(3, 6, 5, 0.92);
}
.death-content .overlay-title {
font-size: 46px;
color: var(--kp-warning);
margin-bottom: 12px;
-unity-text-align: middle-center;
}
.death-cause {
color: var(--kp-warning);
font-size: 16px;
-unity-font-style: bold;
-unity-text-align: middle-center;
margin-bottom: 24px;
padding-bottom: 18px;
border-bottom-width: 1px;
border-color: var(--kp-border);
}
.death-section {
padding-top: 14px;
padding-bottom: 14px;
border-bottom-width: 1px;
border-color: var(--kp-border-subtle);
}
.death-summary {
align-items: center;
margin-bottom: 8px;
}
.death-stat-row {
flex-direction: row;
min-width: 330px;
margin-bottom: 5px;
}
.death-stat-label {
width: 150px;
color: var(--kp-text-dim);
font-size: 12px;
}
.death-stat-value {
color: var(--kp-text);
font-size: 13px;
-unity-font-style: bold;
}
.death-rewards {
flex-direction: row;
justify-content: center;
margin-top: 8px;
padding-top: 18px;
padding-bottom: 20px;
border-color: var(--kp-border);
}
.death-reward-item {
flex-direction: row;
align-items: center;
min-width: 220px;
margin-left: 10px;
margin-right: 10px;
padding-left: 14px;
padding-right: 14px;
padding-top: 10px;
padding-bottom: 10px;
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-warning);
}
.death-reward-bandwidth {
border-color: var(--kp-beneficial);
}
.death-reward-icon {
width: 30px;
color: var(--kp-warning);
-unity-font-style: bold;
font-size: 20px;
}
.death-reward-bandwidth .death-reward-icon {
color: var(--kp-beneficial);
}
.death-reward-value {
color: var(--kp-warning);
-unity-font-style: bold;
font-size: 34px;
margin-right: 8px;
}
.death-reward-bandwidth .death-reward-value {
color: var(--kp-beneficial);
}
.death-reward-name {
color: var(--kp-text);
font-size: 13px;
-unity-font-style: bold;
}
.death-reward-subdued {
opacity: 0.62;
border-color: var(--kp-border);
}
.death-reward-subdued .death-reward-value {
color: var(--kp-text-dim);
}
.death-reward-subdued .death-reward-icon,
.death-reward-subdued .death-reward-name {
color: var(--kp-text-dim);
}
.death-return-action {
margin-top: 30px;
height: 48px;
font-size: 14px;
}
.reduced-motion .death-crash-beat {
display: none;
transition-duration: 0ms;
}
.repository {
width: 900px;
height: 86%;
padding: 14px;
flex-grow: 1;
width: 100%;
height: 100%;
padding: 16px;
background-color: var(--kp-panel);
border-left-width: 2px;
border-right-width: 1px;
@@ -845,16 +1105,31 @@
.repository-header {
flex-direction: row;
align-items: center;
margin-bottom: 8px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom-width: 1px;
border-color: var(--kp-border);
}
.repository-header-copy {
flex-grow: 1;
}
.repository-title {
color: var(--kp-warning);
-unity-font-style: bold;
font-size: 30px;
margin-bottom: 2px;
}
.repository-subtitle {
color: var(--kp-text);
font-size: 13px;
}
.repository-wallet {
min-width: 132px;
padding: 8px;
min-width: 150px;
padding: 10px;
background-color: var(--kp-bg-deep);
border-left-width: 2px;
border-right-width: 1px;
@@ -866,20 +1141,21 @@
.repository-wallet-value {
color: var(--kp-warning);
-unity-font-style: bold;
font-size: 22px;
font-size: 24px;
margin-top: 4px;
}
.repository-body {
flex-grow: 1;
min-height: 0;
margin-top: 8px;
margin-bottom: 10px;
margin-bottom: 12px;
padding-right: 4px;
}
.repository-offers {
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 10px;
margin-bottom: 12px;
}
.repository-row {
@@ -894,11 +1170,11 @@
}
.repository-tile {
width: 270px;
min-height: 244px;
margin-right: 10px;
margin-bottom: 10px;
padding: 8px;
width: 280px;
height: 332px;
margin-right: 12px;
margin-bottom: 12px;
padding: 10px;
background-color: var(--kp-panel-raised);
border-left-width: 2px;
border-right-width: 1px;
@@ -917,23 +1193,25 @@
.repository-tile-stat {
border-left-color: var(--kp-warning);
min-height: 190px;
}
.repository-tile-top {
flex-direction: row;
align-items: center;
margin-bottom: 7px;
margin-bottom: 8px;
min-height: 24px;
}
.repository-kind {
color: var(--kp-text-dim);
font-size: 11px;
flex-grow: 1;
-unity-font-style: bold;
}
.repository-price {
min-width: 46px;
min-width: 48px;
height: 24px;
padding-left: 7px;
padding-right: 7px;
padding-top: 3px;
@@ -958,6 +1236,7 @@
color: var(--kp-text);
-unity-font-style: bold;
margin-bottom: 4px;
white-space: normal;
}
.repository-description {
@@ -967,8 +1246,8 @@
}
.repository-action {
height: 28px;
margin-top: 7px;
height: 34px;
margin-top: 8px;
background-color: var(--kp-bg-deep);
color: var(--kp-text);
border-left-width: 1px;
@@ -977,6 +1256,12 @@
border-bottom-width: 1px;
border-color: var(--kp-accent);
-unity-font-style: bold;
font-size: 12px;
}
.repository-reroll-action {
border-color: var(--kp-border-strong);
color: var(--kp-text-dim);
}
.repository-card-content {
@@ -985,8 +1270,9 @@
.repository-card-preview {
width: 100%;
min-height: 154px;
height: 176px;
margin-right: 0;
margin-bottom: 6px;
}
.repository-run-only-tag {
@@ -1000,7 +1286,7 @@
.repository-upgrade-card {
width: 100%;
min-height: 126px;
height: 176px;
margin-right: 0;
opacity: 0.72;
}
@@ -1016,6 +1302,8 @@
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-beneficial);
min-height: 44px;
flex-shrink: 0;
}
.repository-delta-icon {
@@ -1045,21 +1333,27 @@
.repository-footer {
flex-direction: row;
min-height: 44px;
min-height: 56px;
flex-shrink: 0;
padding-top: 10px;
border-top-width: 1px;
border-color: var(--kp-border);
}
.repository-footer-action {
width: 210px;
margin-right: 8px;
width: 190px;
height: 38px;
margin-right: 16px;
}
.repository-leave-action {
flex-grow: 1;
height: 38px;
}
.repository-leave-block {
flex-grow: 1;
margin-left: 2px;
}
.repository-banked-hint {
@@ -1071,7 +1365,7 @@
.repository-remove {
margin-bottom: 2px;
padding: 8px;
padding: 10px;
background-color: var(--kp-bg-deep);
border-left-width: 1px;
border-right-width: 1px;
@@ -1086,7 +1380,7 @@
}
.repo-remove-button {
height: 24px;
height: 28px;
margin-right: 6px;
margin-bottom: 6px;
background-color: var(--kp-bg-deep);
@@ -1099,11 +1393,11 @@
}
.repo-unaffordable {
opacity: 0.45;
opacity: 0.5;
}
.repo-sold {
opacity: 0.72;
opacity: 0.66;
border-color: var(--kp-text-dim);
}
@@ -1284,6 +1578,24 @@
border-color: var(--kp-kill);
}
.overflow-travel {
min-width: 42px;
height: 20px;
padding-left: 5px;
padding-right: 5px;
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);
-unity-font-style: bold;
-unity-text-align: middle-center;
transition-property: left, top, opacity;
transition-duration: 180ms;
}
.feedback-card-ghost {
opacity: 0.9;
transition-property: left, top, opacity;
@@ -1395,6 +1707,15 @@
transition-duration: 220ms;
}
.feedback-spawned {
opacity: 1;
scale: 1.06 1.06;
background-color: #061820;
border-color: var(--kp-accent);
transition-property: opacity, scale, background-color, border-color;
transition-duration: 220ms;
}
.death-ghost {
background-color: #240606;
border-left-width: 2px;
@@ -1490,8 +1811,148 @@
transition-duration: 130ms;
}
.peek-card {
position: absolute;
opacity: 0;
transition-property: opacity, scale, border-color;
transition-duration: 160ms;
}
.peek-card-marked {
border-color: var(--kp-beneficial);
scale: 1.05 1.05;
}
.peek-card-marked.peek-card-tie {
border-color: var(--kp-warning);
}
.fedora-badge {
border-color: var(--kp-accent);
color: var(--kp-accent);
}
.fedora-risk-meter {
margin-top: 4px;
}
.fedora-risk-snap {
opacity: 0.4;
scale: 1.15 1;
transition-property: opacity, scale;
transition-duration: 130ms;
}
.feedback-fedora-windup {
position: absolute;
opacity: 0.94;
border-color: var(--kp-accent);
background-color: var(--kp-action-hover);
scale: 1.04 1.04;
transition-property: opacity, scale, border-color, background-color;
transition-duration: 200ms;
}
.feedback-fedora-crash {
border-color: var(--kp-danger);
background-color: #2d0707;
}
.fedora-crash-stamp {
position: absolute;
right: 4px;
top: 4px;
color: var(--kp-danger);
-unity-font-style: bold;
font-size: 12px;
padding-left: 4px;
padding-right: 4px;
padding-top: 1px;
padding-bottom: 1px;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-danger);
background-color: rgba(45, 7, 7, 0.85);
}
.float-crash {
color: var(--kp-danger);
}
.feedback-fedora-payoff {
background-color: #3a1400;
border-color: var(--kp-accent);
translate: 10px 0;
scale: 1.1 0.88;
transition-property: scale, translate, background-color, border-color;
transition-duration: 190ms;
}
.feedback-fedora-payoff .meter-fill {
background-color: var(--kp-accent);
}
.float-fedora-payoff {
color: var(--kp-accent);
font-size: 27px;
}
.arch-btw-counter {
margin-top: 4px;
padding-left: 6px;
padding-right: 6px;
padding-top: 2px;
padding-bottom: 2px;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-color: var(--kp-border);
align-self: flex-start;
}
.arch-btw-value {
-unity-font-style: bold;
font-size: 13px;
transition-property: scale, opacity, color, border-color;
transition-duration: 140ms;
}
.arch-btw-pop {
scale: 1.25 1.25;
}
.arch-btw-reset {
color: var(--kp-text-dim);
opacity: 0.5;
scale: 0.9 0.9;
}
.arch-btw-persist {
border-left-width: 2px;
border-right-width: 2px;
border-top-width: 2px;
border-bottom-width: 2px;
border-color: var(--kp-beneficial);
scale: 1.08 1.08;
}
.float-btw-bonus {
color: var(--kp-beneficial);
font-size: 12px;
}
.damage-vignette-rolling-release {
opacity: 0.9;
border-color: var(--kp-beneficial);
background-color: rgba(87, 216, 255, 0.1);
}
.reduced-motion .float-number,
.reduced-motion .impact-marker,
.reduced-motion .overflow-travel,
.reduced-motion .feedback-card-ghost,
.reduced-motion .damage-vignette,
.reduced-motion .root-shake-major,
@@ -1512,7 +1973,14 @@
.reduced-motion .hand-card-drawn,
.reduced-motion .queue-chip-resolve,
.reduced-motion .stack-chip-resolve,
.reduced-motion .death-ghost {
.reduced-motion .death-ghost,
.reduced-motion .peek-card,
.reduced-motion .feedback-fedora-windup,
.reduced-motion .feedback-fedora-crash,
.reduced-motion .feedback-fedora-payoff,
.reduced-motion .feedback-spawned,
.reduced-motion .fedora-risk-snap,
.reduced-motion .arch-btw-value {
transition-duration: 0ms;
}
@@ -1526,7 +1994,14 @@
.reduced-motion .enemy-anticipating,
.reduced-motion .enemy-acting,
.reduced-motion .root-shake-major,
.reduced-motion .root-shake-massive {
.reduced-motion .root-shake-massive,
.reduced-motion .peek-card,
.reduced-motion .peek-card-marked,
.reduced-motion .feedback-fedora-windup,
.reduced-motion .feedback-fedora-payoff,
.reduced-motion .feedback-spawned,
.reduced-motion .fedora-risk-snap,
.reduced-motion .arch-btw-value {
translate: 0 0;
scale: 1 1;
}
+12
View File
@@ -72,6 +72,11 @@ namespace KernelPanic.Combat
GrantRustOverkillShield(context, result, survivable);
}
if (result.TargetDefeated)
{
context.CombatManager.QueueDefeatedCombatantForRemoval(target);
}
if (overkillSpills)
{
dealtDamage |= SpillOverkillDamage(context, target, result, survivable);
@@ -118,8 +123,15 @@ namespace KernelPanic.Combat
}
int survivable = GetSurvivableDamage(spillTarget, _trueDamage);
GameEvents.RaiseOverflowDamageTravel(new OverflowDamageTravelEvent(context.Source, firstTarget, spillTarget, remaining, _language));
DamageResult spillResult = context.DamagePipeline.DealDamage(new DamageRequest(context.Source, spillTarget, remaining, _language, _trueDamage, false, applySourceModifiers: false));
dealtDamage |= spillResult.IncomingAmount > 0;
if (spillResult.TargetDefeated)
{
context.CombatManager.QueueDefeatedCombatantForRemoval(spillTarget);
}
firstTarget = spillTarget;
remaining = UnityEngine.Mathf.Max(0, spillResult.IncomingAmount - survivable);
}
+272 -24
View File
@@ -28,6 +28,10 @@ namespace KernelPanic.Combat
private readonly HashSet<string> _loggedEffectTodos = new();
private readonly List<CardDefinition> _generatedCardPool = new();
private readonly List<DelayedCombatEffect> _delayedEffects = new();
private readonly HashSet<EnemyInstance> _enemiesPendingExit = new();
private readonly Queue<EnemyInstance> _enemyExitQueue = new();
private readonly Dictionary<EnemyInstance, IReadOnlyList<DeathSpawnRequest>> _pendingDeathSpawns = new();
private readonly HashSet<EnemyInstance> _completedDeathVisuals = new();
private RunManager _runManager;
private RunConfig _runConfig;
private HandController _handController;
@@ -39,7 +43,7 @@ namespace KernelPanic.Combat
private bool _skipNextAllocateDraw;
private bool _ubuntuEmptyHandRefillUsed;
private int _fedoraCardsDiscountedThisTurn;
private int _fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
private float _fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
private int _cardsPlayedThisTurn;
private int _cardsPlayedThisWave;
private int _cCardsPlayedThisTurn;
@@ -55,6 +59,9 @@ namespace KernelPanic.Combat
private int _nextTurnCycleBonus;
private int _nextRacePairId = 1;
private int _activeKernelRamPenalty;
private int _deathSpawnSequencesActive;
private bool _enemyExitQueueDraining;
private EnemyInstance _enemyExitAwaitingVisual;
private Coroutine _phaseCoroutine;
public TurnPhase CurrentPhase => currentPhase;
@@ -66,6 +73,7 @@ namespace KernelPanic.Combat
public LazyStack LazyStack => _lazyStack;
public NativeTrack NativeTrack => _nativeTrack;
public DamagePipeline DamagePipeline => _damagePipeline;
public float FedoraCrashChance => _fedoraCrashChance;
public IReadOnlyList<EnemyInstance> Enemies => _enemies;
public int SelectedEnemyIndex => _selectedEnemyIndex;
public CardInstance PendingTargetCard => _pendingTargetCard;
@@ -73,6 +81,12 @@ namespace KernelPanic.Combat
public bool RunLost => _runLost;
public int CurrentWaveNumber => _runManager == null ? 1 : _runManager.CurrentWaveNumber;
public int ActiveKernelRamPenalty => _activeKernelRamPenalty;
public bool CanSurrenderRun => currentPhase == TurnPhase.Execute
&& !_awaitingWaveContinue
&& !_runLost
&& !VisualResolutionPending
&& _playerState != null
&& !_playerState.IsDefeated;
public event Action StateChanged;
public event Action<string> CombatLog;
@@ -86,11 +100,13 @@ namespace KernelPanic.Combat
private void OnEnable()
{
GameEvents.DamageDealt += HandlePackageDamageDealt;
GameEvents.CombatantDeathVisualCompleted += HandleCombatantDeathVisualCompleted;
}
private void OnDisable()
{
GameEvents.DamageDealt -= HandlePackageDamageDealt;
GameEvents.CombatantDeathVisualCompleted -= HandleCombatantDeathVisualCompleted;
}
public void StartCombat()
@@ -131,6 +147,31 @@ namespace KernelPanic.Combat
}
}
public void SurrenderRun()
{
if (!CanSurrenderRun)
{
return;
}
_pendingTargetCard = null;
_selectedEnemyIndex = -1;
_damagePipeline.DefeatCombatant(_playerState, null, Language.C);
CheckLoss();
StateChanged?.Invoke();
}
public void QueueDefeatedCombatantForRemoval(CombatantState combatant)
{
EnemyInstance enemy = EnemyForState(combatant);
if (enemy == null)
{
return;
}
QueueDefeatedEnemyForRemoval(enemy);
}
public void ContinueToNextWave()
{
if (!_awaitingWaveContinue || _runLost)
@@ -156,7 +197,7 @@ namespace KernelPanic.Combat
return;
}
if (index < 0 || index >= _enemies.Count)
if (index < 0 || index >= _enemies.Count || _enemies[index].State.IsDefeated)
{
_selectedEnemyIndex = -1;
StateChanged?.Invoke();
@@ -264,7 +305,8 @@ namespace KernelPanic.Combat
_rawhideBonusCharges--;
}
if (!IsCrashImmune(card) && RandomRoll.RollRange(1, 100, new RollContext(_playerState)) <= _fedoraCrashChance)
float fedoraCrashChanceBeforeRoll = _fedoraCrashChance;
if (!IsCrashImmune(card) && RandomRoll.RollRange(1, 1000, new RollContext(_playerState)) <= Mathf.RoundToInt(_fedoraCrashChance * 10f))
{
_fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
if (IsDistro("fedora") && _runConfig.DistroVersion >= 4)
@@ -276,6 +318,7 @@ namespace KernelPanic.Combat
_pendingTargetCard = null;
_selectedEnemyIndex = -1;
Log($"{GetCardName(card)} crashed under bleeding edge");
GameEvents.RaiseFedoraBleedingEdgeTriggered(new FedoraBleedingEdgeEvent(card, true, firstFedoraBonusThisTurn, 0, fedoraCrashChanceBeforeRoll, _fedoraCrashChance));
StateChanged?.Invoke();
return true;
}
@@ -286,6 +329,7 @@ namespace KernelPanic.Combat
card.MarkFedoraNonCrashBonus();
_playerState.DamageMultiplierPercent = _runConfig.DistroVersion >= 2 ? 175 : 150;
ApplyFedoraGrowth(card, rawhideChargeUsed, firstFedoraBonusThisTurn);
GameEvents.RaiseFedoraBleedingEdgeTriggered(new FedoraBleedingEdgeEvent(card, false, firstFedoraBonusThisTurn, _playerState.DamageMultiplierPercent, fedoraCrashChanceBeforeRoll, _fedoraCrashChance));
}
if (card.Definition.Language == Language.C && _cCardsPlayedThisTurn == 0 && HasPackageEffect(PackageEffectKind.FirstCThisTurnDamageMultiplier, out PackageEffectData cPackageEffect))
@@ -669,6 +713,13 @@ namespace KernelPanic.Combat
_queuedRepeatCharges = 0;
_nextTurnCycleBonus = 0;
_delayedEffects.Clear();
_enemiesPendingExit.Clear();
_enemyExitQueue.Clear();
_pendingDeathSpawns.Clear();
_completedDeathVisuals.Clear();
_deathSpawnSequencesActive = 0;
_enemyExitQueueDraining = false;
_enemyExitAwaitingVisual = null;
RandomRoll.Seed(_runConfig.RunSeed);
_playerState = new CombatantState(_runManager.EffectiveMaxUptime(), _runManager.EffectiveRam(), _runManager.EffectiveMaxCycles());
ApplyVersionState(_playerState);
@@ -726,6 +777,13 @@ namespace KernelPanic.Combat
_deckController.Initialize(startingCards);
_interpreterQueue.Clear();
_enemies.Clear();
_enemiesPendingExit.Clear();
_enemyExitQueue.Clear();
_pendingDeathSpawns.Clear();
_completedDeathVisuals.Clear();
_deathSpawnSequencesActive = 0;
_enemyExitQueueDraining = false;
_enemyExitAwaitingVisual = null;
SpawnStructuralEnemies();
RecalculateKernelRamPressure();
PickEnemyIntents();
@@ -877,7 +935,13 @@ namespace KernelPanic.Combat
return;
}
int archBtwStacksBeforeReset = _playerState?.ArchBtwStacks ?? 0;
ResetArchBtwAtEndOfTurn();
if (IsDistro("arch") && archBtwStacksBeforeReset > 0)
{
GameEvents.RaiseArchBtwTurnEnded(new ArchBtwTurnEndedEvent(archBtwStacksBeforeReset, _playerState.ArchBtwStacks > 0));
}
RemoveDefeatedEnemies();
CheckWinOrLoss();
StateChanged?.Invoke();
@@ -952,7 +1016,7 @@ namespace KernelPanic.Combat
}
int lookCount = _runConfig.DistroVersion >= 2 ? 3 : 2;
if (!_deckController.TryDrawCheapestFromTop(lookCount, out CardInstance card))
if (!_deckController.TryDrawCheapestFromTop(lookCount, out CardInstance card, out IReadOnlyList<PeekedCardInfo> peeked, out bool wasTie))
{
return;
}
@@ -965,6 +1029,7 @@ namespace KernelPanic.Combat
if (_handController.Add(card))
{
Log($"apt update: staged {GetCardName(card)} from top {lookCount}");
GameEvents.RaiseUbuntuAptUpdatePeeked(new UbuntuAptUpdatePeekedEvent(peeked, card, wasTie, lookCount));
}
else
{
@@ -1836,17 +1901,7 @@ namespace KernelPanic.Combat
{
if (_enemies[i].State.IsDefeated)
{
EnemyInstance enemy = _enemies[i];
if (enemy.PendingRevive)
{
continue;
}
Log($"{enemy.Name} exited");
_runManager?.AddBits(CombatTuning.BitsPerKill);
HandleEnemyDeath(enemy);
_enemies.RemoveAt(i);
RecalculateKernelRamPressure();
QueueDefeatedEnemyForRemoval(_enemies[i]);
}
}
@@ -1854,15 +1909,42 @@ namespace KernelPanic.Combat
{
_selectedEnemyIndex = -1;
}
if (_enemyExitQueue.Count > 0 && !_enemyExitQueueDraining)
{
StartCoroutine(DrainEnemyExitQueue());
}
}
private void HandleEnemyDeath(EnemyInstance enemy)
private void QueueDefeatedEnemyForRemoval(EnemyInstance enemy)
{
if (enemy == null)
if (enemy == null || enemy.State == null || !enemy.State.IsDefeated || enemy.PendingRevive || _enemiesPendingExit.Contains(enemy))
{
return;
}
Log($"{enemy.Name} exited");
_runManager?.AddBits(CombatTuning.BitsPerKill);
List<DeathSpawnRequest> deathSpawns = HandleEnemyDeath(enemy);
enemy.MarkDefeatedPendingRemoval();
_enemiesPendingExit.Add(enemy);
_pendingDeathSpawns[enemy] = deathSpawns;
_enemyExitQueue.Enqueue(enemy);
if (!_enemyExitQueueDraining)
{
StartCoroutine(DrainEnemyExitQueue());
}
}
private List<DeathSpawnRequest> HandleEnemyDeath(EnemyInstance enemy)
{
List<DeathSpawnRequest> deathSpawns = new();
if (enemy == null)
{
return deathSpawns;
}
if (enemy.HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath))
{
ApplySegfaultDeathPayload(enemy);
@@ -1875,7 +1957,148 @@ namespace KernelPanic.Combat
if (enemy.HasBehavior(EnemyBehaviorFlags.LeavesOrphan))
{
SpawnOrphanFromDeath(enemy);
deathSpawns.Add(CreateOrphanDeathSpawn(enemy));
}
return deathSpawns;
}
private void HandleCombatantDeathVisualCompleted(CombatantDeathVisualCompletedEvent payload)
{
EnemyInstance enemy = EnemyForState(payload.Combatant);
if (enemy == null)
{
return;
}
_completedDeathVisuals.Add(enemy);
}
private EnemyInstance EnemyForState(CombatantState state)
{
if (state == null)
{
return null;
}
for (int i = 0; i < _enemies.Count; i++)
{
if (_enemies[i].State == state)
{
return _enemies[i];
}
}
return null;
}
private IEnumerator DrainEnemyExitQueue()
{
if (_enemyExitQueueDraining)
{
yield break;
}
_enemyExitQueueDraining = true;
float elapsedMs = 0f;
while (_enemyExitQueue.Count > 0)
{
bool removedAny = false;
int pendingCount = _enemyExitQueue.Count;
for (int i = 0; i < pendingCount; i++)
{
EnemyInstance enemy = _enemyExitQueue.Dequeue();
bool visualComplete = _completedDeathVisuals.Contains(enemy);
bool timedOut = elapsedMs >= CombatTuning.VisualQueueMaxDrainMs;
if (!visualComplete && !timedOut)
{
_enemyExitQueue.Enqueue(enemy);
continue;
}
_enemyExitAwaitingVisual = enemy;
_completedDeathVisuals.Remove(enemy);
yield return CompleteEnemyExit(enemy);
_enemyExitAwaitingVisual = null;
removedAny = true;
}
if (removedAny)
{
elapsedMs = 0f;
continue;
}
elapsedMs += Time.unscaledDeltaTime * 1000f;
yield return null;
}
_enemyExitQueueDraining = false;
CheckWinOrLoss();
StateChanged?.Invoke();
if (!IsCombatPaused && currentPhase != TurnPhase.Execute)
{
yield return Wait(CombatTuning.PhaseTransitionDelaySeconds);
AdvancePhase();
}
}
private IEnumerator CompleteEnemyExit(EnemyInstance enemy)
{
if (enemy == null)
{
yield break;
}
_pendingDeathSpawns.TryGetValue(enemy, out IReadOnlyList<DeathSpawnRequest> deathSpawns);
_pendingDeathSpawns.Remove(enemy);
int removedIndex = _enemies.IndexOf(enemy);
if (removedIndex >= 0)
{
enemy.ClearDefeatedPendingRemoval();
_enemies.RemoveAt(removedIndex);
if (_selectedEnemyIndex >= _enemies.Count)
{
_selectedEnemyIndex = -1;
}
RecalculateKernelRamPressure();
StateChanged?.Invoke();
}
_enemiesPendingExit.Remove(enemy);
if (deathSpawns != null)
{
_deathSpawnSequencesActive++;
for (int i = 0; i < deathSpawns.Count; i++)
{
yield return WaitSequencedMs(CombatTuning.SpawnDelayMs, CombatTuning.ReducedMotionSpawnDelayMs);
EnemyInstance spawned = SpawnEnemy(deathSpawns[i].ArchetypeId, deathSpawns[i].UptimeBonus);
spawned.MarkSpawnedFromDeath();
spawned.PickNextIntent();
Log(deathSpawns[i].LogMessage);
StateChanged?.Invoke();
GameEvents.RaiseDeathSpawnedEnemy(new DeathSpawnedEnemyEvent(deathSpawns[i].Source, spawned));
yield return WaitSequencedMs(
CombatTuning.SpawnMaterializeMs + CombatTuning.SpawnTelegraphDelayMs,
CombatTuning.ReducedMotionSpawnMaterializeMs + CombatTuning.ReducedMotionSpawnTelegraphDelayMs);
spawned.RevealDeathSpawnIntent();
StateChanged?.Invoke();
}
_deathSpawnSequencesActive = Mathf.Max(0, _deathSpawnSequencesActive - 1);
}
}
private static IEnumerator WaitSequencedMs(int normalMs, int reducedMotionMs)
{
int durationMs = UIPreferences.ReducedMotion ? reducedMotionMs : normalMs;
if (durationMs > 0)
{
yield return new WaitForSeconds(durationMs / 1000f);
}
}
@@ -2004,11 +2227,9 @@ namespace KernelPanic.Combat
Log($"{survivor.Name} race link broke; survivor enraged");
}
private void SpawnOrphanFromDeath(EnemyInstance source)
private static DeathSpawnRequest CreateOrphanDeathSpawn(EnemyInstance source)
{
EnemyInstance orphan = SpawnEnemy("orphan_process", 0);
orphan.MarkSpawnedFromDeath();
Log($"{source.Name} left an orphan process");
return new DeathSpawnRequest(source?.State, "orphan_process", 0, $"{source?.Name ?? "enemy"} left an orphan process");
}
private void SpawnStructuralEnemies()
@@ -2473,11 +2694,16 @@ namespace KernelPanic.Combat
private bool CheckWin()
{
if (_awaitingWaveContinue || _runLost || _enemies.Count > 0)
if (_awaitingWaveContinue || _runLost)
{
return _awaitingWaveContinue;
}
if (LivingEnemyStates().Count > 0 || VisualResolutionPending)
{
return false;
}
_awaitingWaveContinue = true;
_selectedEnemyIndex = -1;
_pendingTargetCard = null;
@@ -2512,7 +2738,13 @@ namespace KernelPanic.Combat
return string.IsNullOrWhiteSpace(card.Definition.DisplayName) ? card.Definition.Id : card.Definition.DisplayName;
}
private bool IsCombatPaused => _awaitingWaveContinue || _runLost;
private bool VisualResolutionPending => _enemiesPendingExit.Count > 0
|| _enemyExitQueue.Count > 0
|| _enemyExitQueueDraining
|| _enemyExitAwaitingVisual != null
|| _deathSpawnSequencesActive > 0;
private bool IsCombatPaused => _awaitingWaveContinue || _runLost || VisualResolutionPending;
private enum DelayedCombatEffectKind
{
@@ -2543,5 +2775,21 @@ namespace KernelPanic.Combat
return new DelayedCombatEffect(DelayedCombatEffectKind.TimeshiftRestore, uptime, garbageCollectionsRemaining);
}
}
private readonly struct DeathSpawnRequest
{
public DeathSpawnRequest(CombatantState source, string archetypeId, int uptimeBonus, string logMessage)
{
Source = source;
ArchetypeId = archetypeId;
UptimeBonus = uptimeBonus;
LogMessage = logMessage;
}
public CombatantState Source { get; }
public string ArchetypeId { get; }
public int UptimeBonus { get; }
public string LogMessage { get; }
}
}
}
+22 -3
View File
@@ -41,9 +41,9 @@ namespace KernelPanic.Combat
public const int StatUpgradeHealPercent = 30;
public const int StatUpgradeRam = 1;
public const int UpgradeMagnitudeBonus = 2;
public const int FedoraBleedingEdgeBaseCrashChance = 5;
public const int FedoraBleedingEdgeCrashChanceStep = 5;
public const int FedoraBleedingEdgeMaxCrashChance = 90;
public const float FedoraBleedingEdgeBaseCrashChance = 5f;
public const float FedoraBleedingEdgeCrashChanceStep = 2.5f;
public const float FedoraBleedingEdgeMaxCrashChance = 90f;
public const int HitMagnitudeModeratePercent = 10;
public const int HitMagnitudeMajorPercent = 25;
public const int HitMagnitudeMassivePercent = 45;
@@ -66,6 +66,25 @@ namespace KernelPanic.Combat
public const float QueueCardResolveDelaySeconds = 0.45f;
public const float EnemyTelegraphDelaySeconds = 0.35f;
public const float EnemyActionDelaySeconds = 0.45f;
public const int DeathBeatMs = 420;
public const int PostDeathPauseMs = 180;
public const int OverflowTravelMs = 180;
public const int SpawnDelayMs = 220;
public const int SpawnMaterializeMs = 220;
public const int SpawnTelegraphDelayMs = 120;
public const int CombatBeatDefaultMs = 120;
public const int CombatBeatSettleMs = 60;
public const int VisualQueueMaxDrainMs = 2400;
public const int ReducedMotionDeathBeatMs = 90;
public const int ReducedMotionPostDeathPauseMs = 45;
public const int ReducedMotionOverflowTravelMs = 55;
public const int ReducedMotionSpawnDelayMs = 60;
public const int ReducedMotionSpawnMaterializeMs = 70;
public const int ReducedMotionSpawnTelegraphDelayMs = 35;
public const int ReducedMotionCombatBeatDefaultMs = 45;
public const int ReducedMotionCombatBeatMinMs = 35;
public const int ReducedMotionCombatBeatMaxMs = 90;
}
public enum HitMagnitudeTier
+23 -4
View File
@@ -61,6 +61,21 @@ namespace KernelPanic.Combat
{
public System.Func<DamageRequest, int, int> ResistanceResolver { get; set; }
public DamageResult DefeatCombatant(CombatantState target, CombatantState source, Language language)
{
if (target == null || target.IsDefeated)
{
return new DamageResult(0, false);
}
int uptimeDamage = Mathf.Max(0, target.CurrentUptime);
target.CurrentUptime = 0;
GameEvents.RaiseDamageDealt(new DamageDealtEvent(source, target, uptimeDamage, language, uptimeDamage, 0, false, 0, uptimeDamage, true));
target.IsDefeated = true;
GameEvents.RaiseCombatantDefeated(new CombatantDefeatedEvent(target));
return new DamageResult(uptimeDamage, true, false, 0, 0, uptimeDamage, uptimeDamage);
}
public DamageResult DealDamage(DamageRequest request)
{
if (request.Target == null || request.Target.IsDefeated)
@@ -70,9 +85,10 @@ namespace KernelPanic.Combat
int amount = Mathf.Max(0, request.Amount);
bool wasCritical = false;
int archBtwBonus = 0;
if (request.ApplySourceModifiers)
{
amount = ApplyFlatAdditions(amount, request);
amount = ApplyFlatAdditions(amount, request, out archBtwBonus);
amount = ApplyMultipliers(amount, request);
amount = ApplyCrit(amount, request, out wasCritical);
}
@@ -81,7 +97,8 @@ namespace KernelPanic.Combat
int finalAmount = ApplyShieldAndUptime(amount, request, out int absorbedAmount, out int shieldDamage, out int uptimeDamage);
ApplyArchRollingReleaseSave(request);
GameEvents.RaiseDamageDealt(new DamageDealtEvent(request.Source, request.Target, finalAmount, request.Language, amount, absorbedAmount, wasCritical, shieldDamage, uptimeDamage, request.TrueDamage));
bool rollingReleaseSaveTriggered = request.Target != null && request.Target.ArchRollingReleaseRecoveredThisHit;
GameEvents.RaiseDamageDealt(new DamageDealtEvent(request.Source, request.Target, finalAmount, request.Language, amount, absorbedAmount, wasCritical, shieldDamage, uptimeDamage, request.TrueDamage, archBtwBonus, rollingReleaseSaveTriggered));
bool defeated = false;
if (request.Target.CurrentUptime <= 0 && !request.Target.IsDefeated)
@@ -113,8 +130,9 @@ namespace KernelPanic.Combat
return Mathf.RoundToInt(amount * (multiplier / 100f) * (currentCardMultiplier / 100f));
}
private static int ApplyFlatAdditions(int amount, DamageRequest request)
private static int ApplyFlatAdditions(int amount, DamageRequest request, out int archBtwBonus)
{
archBtwBonus = 0;
if (amount == int.MaxValue)
{
return amount;
@@ -127,7 +145,8 @@ namespace KernelPanic.Combat
if (request.Source != null && (request.Language == Language.C || request.Language == Language.Rust))
{
amount += Mathf.Max(0, request.Source.ArchBtwStacks) * Mathf.Max(1, request.Source.ArchBtwDamagePerStack);
archBtwBonus = Mathf.Max(0, request.Source.ArchBtwStacks) * Mathf.Max(1, request.Source.ArchBtwDamagePerStack);
amount += archBtwBonus;
}
return amount;
+28 -1
View File
@@ -3,6 +3,21 @@ using System.Linq;
namespace KernelPanic.Combat
{
/// <summary>
/// Describes one card inspected by a top-of-deck peek (e.g. Ubuntu's apt update), for UI display.
/// </summary>
public readonly struct PeekedCardInfo
{
public PeekedCardInfo(CardInstance card, int cost)
{
Card = card;
Cost = cost;
}
public CardInstance Card { get; }
public int Cost { get; }
}
/// <summary>
/// Manages draw, discard, and exhaust piles for a combat deck.
/// </summary>
@@ -55,9 +70,11 @@ namespace KernelPanic.Combat
return drawn;
}
public bool TryDrawCheapestFromTop(int lookCount, out CardInstance card)
public bool TryDrawCheapestFromTop(int lookCount, out CardInstance card, out IReadOnlyList<PeekedCardInfo> peeked, out bool wasTie)
{
card = null;
peeked = System.Array.Empty<PeekedCardInfo>();
wasTie = false;
if (lookCount <= 0)
{
return false;
@@ -76,17 +93,27 @@ namespace KernelPanic.Combat
int inspectCount = System.Math.Min(lookCount, _drawPile.Count);
int selectedIndex = _drawPile.Count - 1;
int selectedCost = CombatManager.GetCardCost(_drawPile[selectedIndex]);
List<PeekedCardInfo> inspected = new(inspectCount) { new PeekedCardInfo(_drawPile[selectedIndex], selectedCost) };
int tieCount = 1;
for (int offset = 1; offset < inspectCount; offset++)
{
int index = _drawPile.Count - 1 - offset;
int cost = CombatManager.GetCardCost(_drawPile[index]);
inspected.Add(new PeekedCardInfo(_drawPile[index], cost));
if (cost < selectedCost)
{
selectedCost = cost;
selectedIndex = index;
tieCount = 1;
}
else if (cost == selectedCost)
{
tieCount++;
}
}
peeked = inspected;
wasTie = tieCount > 1;
card = _drawPile[selectedIndex];
_drawPile.RemoveAt(selectedIndex);
return card != null;
+19
View File
@@ -44,10 +44,12 @@ namespace KernelPanic.Combat
public bool PendingRevive { get; private set; }
public bool Reaped { get; private set; }
public bool LethalHitThisTurn { get; private set; }
public bool DefeatedPendingRemoval { get; private set; }
public int PairId { get; private set; } = -1;
public int RaceEnrageStacks { get; private set; }
public int TelemetryStacks { get; private set; }
public bool SpawnedFromDeath { get; private set; }
public bool DeathSpawnIntentRevealed { get; private set; } = true;
public bool HasPendingMarker => PendingRevive || HasRevived;
public bool HasSpecialSignal => HasSplitSignal || HasSegfaultSignal || HasRaceSignal || HasRootkitSignal || HasEliteSignal || HasRamPressureSignal || HasTelemetrySignal || HasCardLockerSignal || SpawnedFromDeath;
public bool HasSplitSignal => HasBehavior(EnemyBehaviorFlags.Split);
@@ -133,6 +135,12 @@ namespace KernelPanic.Combat
public void MarkSpawnedFromDeath()
{
SpawnedFromDeath = true;
DeathSpawnIntentRevealed = false;
}
public void RevealDeathSpawnIntent()
{
DeathSpawnIntentRevealed = true;
}
public void EnrageRaceSurvivor()
@@ -162,8 +170,19 @@ namespace KernelPanic.Combat
LethalHitThisTurn = true;
}
public void MarkDefeatedPendingRemoval()
{
DefeatedPendingRemoval = true;
}
public void ClearDefeatedPendingRemoval()
{
DefeatedPendingRemoval = false;
}
public void MarkPendingRevive()
{
DefeatedPendingRemoval = false;
PendingRevive = true;
State.IsDefeated = false;
State.CurrentUptime = 0;
+140 -1
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using KernelPanic.Combat;
namespace KernelPanic.Core
@@ -12,7 +13,10 @@ namespace KernelPanic.Core
public static event Action<CardResolvedEvent> CardResolved;
public static event Action<PhaseChangedEvent> PhaseChanged;
public static event Action<DamageDealtEvent> DamageDealt;
public static event Action<OverflowDamageTravelEvent> OverflowDamageTravel;
public static event Action<CombatantDefeatedEvent> CombatantDefeated;
public static event Action<CombatantDeathVisualCompletedEvent> CombatantDeathVisualCompleted;
public static event Action<DeathSpawnedEnemyEvent> DeathSpawnedEnemy;
public static event Action<EnemyWouldActEvent> EnemyWouldAct;
public static event Action<EnemyActedEvent> EnemyActed;
public static event Action<PlayerDamagedEvent> PlayerDamaged;
@@ -24,6 +28,9 @@ namespace KernelPanic.Core
public static event Action<StatusExpiredEvent> StatusExpired;
public static event Action<StatusCleansedEvent> StatusCleansed;
public static event Action<RunEndedEvent> RunEnded;
public static event Action<UbuntuAptUpdatePeekedEvent> UbuntuAptUpdatePeeked;
public static event Action<FedoraBleedingEdgeEvent> FedoraBleedingEdgeTriggered;
public static event Action<ArchBtwTurnEndedEvent> ArchBtwTurnEnded;
public static void RaiseCardPlayed(CardPlayedEvent payload)
{
@@ -45,11 +52,26 @@ namespace KernelPanic.Core
DamageDealt?.Invoke(payload);
}
public static void RaiseOverflowDamageTravel(OverflowDamageTravelEvent payload)
{
OverflowDamageTravel?.Invoke(payload);
}
public static void RaiseCombatantDefeated(CombatantDefeatedEvent payload)
{
CombatantDefeated?.Invoke(payload);
}
public static void RaiseCombatantDeathVisualCompleted(CombatantDeathVisualCompletedEvent payload)
{
CombatantDeathVisualCompleted?.Invoke(payload);
}
public static void RaiseDeathSpawnedEnemy(DeathSpawnedEnemyEvent payload)
{
DeathSpawnedEnemy?.Invoke(payload);
}
public static void RaiseEnemyWouldAct(EnemyWouldActEvent payload)
{
EnemyWouldAct?.Invoke(payload);
@@ -104,6 +126,21 @@ namespace KernelPanic.Core
{
RunEnded?.Invoke(payload);
}
public static void RaiseUbuntuAptUpdatePeeked(UbuntuAptUpdatePeekedEvent payload)
{
UbuntuAptUpdatePeeked?.Invoke(payload);
}
public static void RaiseFedoraBleedingEdgeTriggered(FedoraBleedingEdgeEvent payload)
{
FedoraBleedingEdgeTriggered?.Invoke(payload);
}
public static void RaiseArchBtwTurnEnded(ArchBtwTurnEndedEvent payload)
{
ArchBtwTurnEnded?.Invoke(payload);
}
}
/// <summary>
@@ -171,7 +208,7 @@ namespace KernelPanic.Core
{
}
public DamageDealtEvent(CombatantState source, CombatantState target, int amount, Language language, int incomingAmount, int absorbedAmount, bool wasCritical, int shieldDamage, int uptimeDamage, bool trueDamage)
public DamageDealtEvent(CombatantState source, CombatantState target, int amount, Language language, int incomingAmount, int absorbedAmount, bool wasCritical, int shieldDamage, int uptimeDamage, bool trueDamage, int archBtwBonusAmount = 0, bool archRollingReleaseSaveTriggered = false)
{
Source = source;
Target = target;
@@ -183,6 +220,8 @@ namespace KernelPanic.Core
ShieldDamage = shieldDamage;
UptimeDamage = uptimeDamage;
TrueDamage = trueDamage;
ArchBtwBonusAmount = archBtwBonusAmount;
ArchRollingReleaseSaveTriggered = archRollingReleaseSaveTriggered;
}
public CombatantState Source { get; }
@@ -195,6 +234,8 @@ namespace KernelPanic.Core
public int ShieldDamage { get; }
public int UptimeDamage { get; }
public bool TrueDamage { get; }
public int ArchBtwBonusAmount { get; }
public bool ArchRollingReleaseSaveTriggered { get; }
public bool WasFullyBlocked => IncomingAmount > 0 && UptimeDamage <= 0;
public bool WasMitigated => ShieldDamage > 0 || AbsorbedAmount > 0 || WasFullyBlocked;
}
@@ -212,6 +253,46 @@ namespace KernelPanic.Core
public CombatantState Combatant { get; }
}
public readonly struct OverflowDamageTravelEvent
{
public OverflowDamageTravelEvent(CombatantState source, CombatantState from, CombatantState to, int amount, Language language)
{
Source = source;
From = from;
To = to;
Amount = amount;
Language = language;
}
public CombatantState Source { get; }
public CombatantState From { get; }
public CombatantState To { get; }
public int Amount { get; }
public Language Language { get; }
}
public readonly struct CombatantDeathVisualCompletedEvent
{
public CombatantDeathVisualCompletedEvent(CombatantState combatant)
{
Combatant = combatant;
}
public CombatantState Combatant { get; }
}
public readonly struct DeathSpawnedEnemyEvent
{
public DeathSpawnedEnemyEvent(CombatantState source, EnemyInstance enemy)
{
Source = source;
Enemy = enemy;
}
public CombatantState Source { get; }
public EnemyInstance Enemy { get; }
}
/// <summary>
/// Describes an enemy placeholder reaching its structural action point.
/// </summary>
@@ -348,4 +429,62 @@ namespace KernelPanic.Core
public bool PlayerDied { get; }
public int EntropyEarned { get; }
}
/// <summary>
/// Describes an Ubuntu "apt update" peek-and-pick: the cards inspected at the top of the draw
/// pile and which one was added to hand.
/// </summary>
public readonly struct UbuntuAptUpdatePeekedEvent
{
public UbuntuAptUpdatePeekedEvent(IReadOnlyList<PeekedCardInfo> peeked, CardInstance chosen, bool wasTie, int lookCount)
{
Peeked = peeked;
Chosen = chosen;
WasTie = wasTie;
LookCount = lookCount;
}
public IReadOnlyList<PeekedCardInfo> Peeked { get; }
public CardInstance Chosen { get; }
public bool WasTie { get; }
public int LookCount { get; }
}
/// <summary>
/// Describes the outcome of Fedora's "Bleeding Edge" crash roll for a bonus-eligible card.
/// </summary>
public readonly struct FedoraBleedingEdgeEvent
{
public FedoraBleedingEdgeEvent(CardInstance card, bool crashed, bool firstBonusThisTurn, int damageMultiplierPercent, float crashChanceRolled, float crashChanceAfter)
{
Card = card;
Crashed = crashed;
FirstBonusThisTurn = firstBonusThisTurn;
DamageMultiplierPercent = damageMultiplierPercent;
CrashChanceRolled = crashChanceRolled;
CrashChanceAfter = crashChanceAfter;
}
public CardInstance Card { get; }
public bool Crashed { get; }
public bool FirstBonusThisTurn { get; }
public int DamageMultiplierPercent { get; }
public float CrashChanceRolled { get; }
public float CrashChanceAfter { get; }
}
/// <summary>
/// Describes the end-of-turn resolution of Arch's btw stacks: whether they reset or persisted.
/// </summary>
public readonly struct ArchBtwTurnEndedEvent
{
public ArchBtwTurnEndedEvent(int stacksBefore, bool persisted)
{
StacksBefore = stacksBefore;
Persisted = persisted;
}
public int StacksBefore { get; }
public bool Persisted { get; }
}
}
File diff suppressed because it is too large Load Diff