Implement many UX improvements, packages and their assets and a UI overhaul in collection

This commit is contained in:
2026-07-09 10:18:24 +02:00
parent 61030d84b7
commit 844e81488c
111 changed files with 4480 additions and 258 deletions
+4 -6
View File
@@ -84,8 +84,7 @@ namespace KernelPanic.Combat
return;
}
context.Source.Shield += amount;
context.CombatManager.ReportEffectResult($"gained {amount} shield");
context.CombatManager.GrantPlayerShield(amount, "card");
}
}
@@ -110,8 +109,7 @@ namespace KernelPanic.Combat
return;
}
context.Source.Shield += amount;
context.CombatManager.ReportEffectResult($"gained {amount} shield");
context.CombatManager.GrantPlayerShield(amount, "card");
}
}
@@ -502,8 +500,7 @@ namespace KernelPanic.Combat
continue;
}
context.Source.Shield += overkill;
context.CombatManager.ReportEffectResult($"overkill -> {overkill} shield");
context.CombatManager.GrantPlayerShield(overkill, "overkill");
}
}
}
@@ -572,6 +569,7 @@ namespace KernelPanic.Combat
if (context.HandController.Remove(junk))
{
context.DeckController.Exhaust(junk);
context.CombatManager.HandleCardExhausted(junk);
int cycleGain = UpgradeMath.ScaleAmount(_cycleGain, context.Card);
context.Source.Cycles += cycleGain;
context.CombatManager.ReportEffectResult($"autoremove exhausted {GetCardName(junk)}: +{cycleGain} cycle");
+401 -1
View File
@@ -41,6 +41,12 @@ namespace KernelPanic.Combat
private int fedoraCardsDiscountedThisTurn;
private int fedoraCrashChance = 10;
private int cardsPlayedThisTurn;
private int cardsPlayedThisWave;
private int turnNumberThisWave;
private bool packageShieldBonusTriggeredThisTurn;
private bool packageTimeshiftTriggeredThisWave;
private bool packageInterpreterQueueShieldTriggeredThisWave;
private bool packageNativeDamageTriggeredThisWave;
private int javaCardsPlayedThisCombat;
private int javaCardsDiscountThisTurn;
private int rawhideBonusCharges;
@@ -72,6 +78,16 @@ namespace KernelPanic.Combat
runManager = GetComponent<RunManager>();
}
private void OnEnable()
{
GameEvents.DamageDealt += HandlePackageDamageDealt;
}
private void OnDisable()
{
GameEvents.DamageDealt -= HandlePackageDamageDealt;
}
public void StartCombat()
{
if (runConfig == null)
@@ -197,6 +213,7 @@ namespace KernelPanic.Combat
ResolutionTrack track = card.Definition.ResolutionTrack;
card.MarkPlayedThisTurn(cardsPlayedThisTurn == 0);
cardsPlayedThisTurn++;
cardsPlayedThisWave++;
if (card.Definition.Language == Language.Java)
{
javaCardsPlayedThisCombat++;
@@ -256,6 +273,7 @@ namespace KernelPanic.Combat
}
playerState.DamageMultiplierPercent = 100;
ApplyPackageCardPlayedEffects(card);
pendingTargetCard = null;
TryApplyUbuntuEmptyHandRefill();
@@ -281,6 +299,7 @@ namespace KernelPanic.Combat
cost -= javaCardsPlayedThisCombat + javaCardsDiscountThisTurn;
}
cost = ApplyPackageCostRules(card, cost);
return Mathf.Max(0, cost);
}
@@ -360,6 +379,16 @@ namespace KernelPanic.Combat
ReportEffectResult($"next {charges} enemy attack(s) halved");
}
public void HandleCardExhausted(CardInstance card)
{
if (card == null || !HasPackageEffect(PackageEffectKind.ExhaustShield, out PackageEffectData effect))
{
return;
}
GrantPlayerShield(Mathf.Max(0, effect.Amount), "logrotate");
}
public void ScheduleUpdateManagerRepeat(int damageAmount)
{
delayedEffects.Add(DelayedCombatEffect.UpdateManagerRepeat(damageAmount, 2));
@@ -536,6 +565,12 @@ namespace KernelPanic.Combat
fedoraCrashChance = 10;
fedoraCardsDiscountedThisTurn = 0;
cardsPlayedThisTurn = 0;
cardsPlayedThisWave = 0;
turnNumberThisWave = 0;
packageShieldBonusTriggeredThisTurn = false;
packageTimeshiftTriggeredThisWave = false;
packageInterpreterQueueShieldTriggeredThisWave = false;
packageNativeDamageTriggeredThisWave = false;
javaCardsPlayedThisCombat = 0;
javaCardsDiscountThisTurn = 0;
rawhideBonusCharges = 0;
@@ -545,6 +580,7 @@ namespace KernelPanic.Combat
RandomRoll.Seed(runConfig.RunSeed);
playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles());
ApplyVersionState(playerState);
ApplyPackageState(playerState);
yield return StartWaveSequenced(preservePlayerUptime: false);
Log($"booted {runConfig.Distro.DisplayName} with {runManager.RunDeck.Count} cards");
StateChanged?.Invoke();
@@ -557,6 +593,7 @@ namespace KernelPanic.Combat
{
playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles());
ApplyVersionState(playerState);
ApplyPackageState(playerState);
}
else
{
@@ -570,11 +607,16 @@ namespace KernelPanic.Combat
playerState.IsDefeated = false;
playerState.MutableStatuses.Clear();
ApplyVersionState(playerState);
ApplyPackageState(playerState);
}
handController = new HandController(playerState.Ram);
selectedEnemyIndex = -1;
pendingTargetCard = null;
cardsPlayedThisWave = 0;
turnNumberThisWave = 0;
packageShieldBonusTriggeredThisTurn = false;
packageTimeshiftTriggeredThisWave = false;
List<CardInstance> startingCards = new();
for (int i = 0; i < runManager.RunDeck.Count; i++)
@@ -594,6 +636,7 @@ namespace KernelPanic.Combat
PickEnemyIntents();
StateChanged?.Invoke();
yield return DrawOpeningHandSequenced();
ApplyPackageWaveStartEffects();
skipNextAllocateDraw = true;
}
@@ -607,7 +650,10 @@ namespace KernelPanic.Combat
playerState.Cycles = playerState.MaxCycles;
fedoraCardsDiscountedThisTurn = 0;
cardsPlayedThisTurn = 0;
turnNumberThisWave++;
packageShieldBonusTriggeredThisTurn = false;
javaCardsDiscountThisTurn = 0;
ApplyPackageTurnStartEffects();
if (nextTurnCycleBonus > 0)
{
playerState.Cycles += nextTurnCycleBonus;
@@ -852,7 +898,7 @@ namespace KernelPanic.Combat
return false;
}
int limit = runConfig.DistroVersion >= 5 ? 2 : 1;
int limit = runConfig.DistroVersion >= 5 || HasOnDistroPackageEffect(PackageEffectKind.DnfFedoraPassive, out PackageEffectData dnfEffect) && dnfEffect.EnableFedoraSecondCardPassive ? 2 : 1;
return fedoraCardsDiscountedThisTurn < limit;
}
@@ -870,6 +916,360 @@ namespace KernelPanic.Combat
}
}
private int ApplyPackageCostRules(CardInstance card, int cost)
{
if (card == null || cost <= 0)
{
return cost;
}
IReadOnlyList<PackageInstance> packages = runConfig?.EquippedPackages;
if (packages == null)
{
return cost;
}
int nextTurnCard = cardsPlayedThisTurn + 1;
int nextWaveCard = cardsPlayedThisWave + 1;
for (int i = 0; i < packages.Count; i++)
{
PackageInstance package = packages[i];
if (package?.Definition == null)
{
continue;
}
PackageEffectData effect = package.EffectFor(runConfig?.Distro?.Id);
switch (effect.Kind)
{
case PackageEffectKind.FirstCardEachWaveCostReduction:
if (nextWaveCard == 1)
{
cost -= Mathf.Max(0, effect.Amount);
}
break;
case PackageEffectKind.EveryNthCardEachWaveFree:
if (effect.Threshold > 0 && nextWaveCard % effect.Threshold == 0)
{
cost = 0;
}
break;
case PackageEffectKind.FirstCardsEachTurnCostReduction:
if (nextTurnCard <= Mathf.Max(0, effect.Threshold))
{
cost -= Mathf.Max(0, effect.Amount);
}
break;
}
}
return cost;
}
private void ApplyPackageState(CombatantState state)
{
if (state == null)
{
return;
}
state.JavaScriptFlatDamageBonus = 0;
IReadOnlyList<PackageInstance> packages = runConfig?.EquippedPackages;
if (packages == null)
{
return;
}
for (int i = 0; i < packages.Count; i++)
{
PackageEffectData effect = packages[i] == null ? default : packages[i].EffectFor(runConfig?.Distro?.Id);
if (effect.Kind == PackageEffectKind.JavaScriptFlatDamage)
{
state.JavaScriptFlatDamageBonus += Mathf.Max(0, effect.Amount);
}
}
}
private void ApplyPackageWaveStartEffects()
{
IReadOnlyList<PackageInstance> packages = runConfig?.EquippedPackages;
if (packages == null)
{
return;
}
for (int i = 0; i < packages.Count; i++)
{
PackageInstance package = packages[i];
if (package?.Definition == null)
{
continue;
}
PackageEffectData effect = package.EffectFor(runConfig?.Distro?.Id);
switch (effect.Kind)
{
case PackageEffectKind.WaveDraw:
StartCoroutine(DrawCardsToHandSequenced(Mathf.Max(0, effect.Amount), package.Definition.DisplayName));
break;
case PackageEffectKind.WaveStartShield:
GrantPlayerShield(Mathf.Max(0, effect.Amount), package.Definition.DisplayName);
break;
case PackageEffectKind.FirstTurnEachWaveShield:
if (turnNumberThisWave == 0)
{
GrantPlayerShield(Mathf.Max(0, effect.Amount), package.Definition.DisplayName);
}
break;
case PackageEffectKind.FirstTurnFirstWaveDraw:
if (CurrentWaveNumber == 1 && turnNumberThisWave == 0)
{
StartCoroutine(DrawCardsToHandSequenced(Mathf.Max(0, effect.Amount), package.Definition.DisplayName));
}
break;
case PackageEffectKind.WaveGenerateBasicCard:
GeneratePackageLanguageCard(package.Definition.DisplayName);
break;
}
}
}
private void ApplyPackageTurnStartEffects()
{
if (HasPackageEffect(PackageEffectKind.EveryNthTurnShield, out PackageEffectData effect)
&& effect.Threshold > 0
&& turnNumberThisWave % effect.Threshold == 0)
{
GrantPlayerShield(Mathf.Max(0, effect.Amount), "cron");
}
if (HasPackageEffect(PackageEffectKind.EveryNthTurnDraw, out effect)
&& effect.Threshold > 0
&& turnNumberThisWave % effect.Threshold == 0)
{
StartCoroutine(DrawCardsToHandSequenced(Mathf.Max(0, effect.Amount), "udev"));
}
if (turnNumberThisWave == 1
&& HasPackageEffect(PackageEffectKind.FirstTurnEachWaveCycle, out effect)
&& effect.Amount > 0)
{
playerState.Cycles += effect.Amount;
Log($"apt-fast: gained {effect.Amount} Cycle");
}
if (HasPackageEffect(PackageEffectKind.StartTurnNoDebuffShield, out effect)
&& !HasHarmfulStatus(playerState))
{
GrantPlayerShield(Mathf.Max(0, effect.Amount), "auditd");
}
}
private void ApplyPackageCardPlayedEffects(CardInstance card)
{
if (card == null || playerState == null)
{
return;
}
if (HasPackageEffect(PackageEffectKind.ThirdCardEachTurnGenerate, out PackageEffectData aptEffect)
&& cardsPlayedThisTurn == Mathf.Max(1, aptEffect.Threshold))
{
GeneratePackageLanguageCard("apt");
if (aptEffect.RefundCycle)
{
playerState.Cycles += 1;
Log("apt refunded 1 Cycle");
}
}
if (HasPackageEffect(PackageEffectKind.DnfFedoraPassive, out PackageEffectData dnfEffect)
&& cardsPlayedThisTurn == 1
&& dnfEffect.Amount > 0)
{
playerState.FlatEffectBonus += dnfEffect.Amount;
Log($"DNF wave buff +{dnfEffect.Amount} damage");
}
if (!packageInterpreterQueueShieldTriggeredThisWave
&& card.Definition.ResolutionTrack == ResolutionTrack.InterpreterQueue
&& HasPackageEffect(PackageEffectKind.FirstInterpreterQueueCardEachWaveShield, out PackageEffectData queueEffect))
{
packageInterpreterQueueShieldTriggeredThisWave = true;
GrantPlayerShield(Mathf.Max(0, queueEffect.Amount), "dbus");
}
if (!packageNativeDamageTriggeredThisWave
&& card.Definition.ResolutionTrack == ResolutionTrack.Native
&& HasPackageEffect(PackageEffectKind.FirstNativeCardEachWaveFlatDamage, out PackageEffectData nativeEffect)
&& nativeEffect.Amount > 0)
{
packageNativeDamageTriggeredThisWave = true;
playerState.FlatEffectBonus += nativeEffect.Amount;
Log($"make: first Native card +{nativeEffect.Amount} damage");
}
if (HasPackageEffect(PackageEffectKind.EveryNthCardEachWaveCycle, out PackageEffectData cycleEffect)
&& cycleEffect.Threshold > 0
&& cardsPlayedThisWave % cycleEffect.Threshold == 0
&& cycleEffect.Amount > 0)
{
playerState.Cycles += cycleEffect.Amount;
Log($"systemd-timer: gained {cycleEffect.Amount} Cycle");
}
}
public void GrantPlayerShield(int amount, string label)
{
if (playerState == null || amount <= 0)
{
return;
}
int total = amount;
if (!packageShieldBonusTriggeredThisTurn
&& HasPackageEffect(PackageEffectKind.FirstShieldEachTurnBonus, out PackageEffectData effect))
{
int bonus = Mathf.Max(0, effect.Amount);
if (bonus > 0)
{
total += bonus;
packageShieldBonusTriggeredThisTurn = true;
Log($"SELinux shield +{bonus}");
}
}
playerState.Shield += total;
if (!string.IsNullOrWhiteSpace(label))
{
Log($"{label}: gained {total} Shield");
}
}
private void GeneratePackageLanguageCard(string label)
{
if (handController == null || handController.RemainingRam <= 0)
{
return;
}
Language language = RandomRoll.RollRange(0, 1, new RollContext(playerState)) == 0
? runConfig.PrimaryLanguage
: runConfig.SecondaryLanguage;
if (!TryCreateGeneratedCard(language, Rarity.Common, out CardInstance generated))
{
Log($"{label}: TODO card-generation pool unavailable");
return;
}
if (handController.Add(generated))
{
Log($"{label}: generated {GetCardName(generated)} at 0c");
StateChanged?.Invoke();
}
}
private void HandlePackageDamageDealt(DamageDealtEvent payload)
{
if (payload.Target != playerState
|| payload.UptimeDamage <= 0
|| packageTimeshiftTriggeredThisWave
|| !HasPackageEffect(PackageEffectKind.WaveThresholdRestore, out PackageEffectData effect))
{
return;
}
int thresholdPercent = Mathf.Clamp(effect.Threshold, 1, 100);
int restoreTarget = Mathf.CeilToInt(playerState.MaxUptime * (thresholdPercent / 100f));
if (playerState.CurrentUptime >= restoreTarget)
{
return;
}
packageTimeshiftTriggeredThisWave = true;
playerState.CurrentUptime = restoreTarget;
if (effect.CleanseDebuffs)
{
statusEffects.CleanseHarmful(playerState);
}
Log($"Timeshift restored uptime to {thresholdPercent}%");
StateChanged?.Invoke();
}
private bool HasOnDistroPackageEffect(PackageEffectKind kind, out PackageEffectData effect)
{
effect = default;
IReadOnlyList<PackageInstance> packages = runConfig?.EquippedPackages;
if (packages == null)
{
return false;
}
for (int i = 0; i < packages.Count; i++)
{
PackageInstance package = packages[i];
if (package?.Definition == null || !package.Definition.IsIntendedFor(runConfig?.Distro?.Id))
{
continue;
}
effect = package.EffectFor(runConfig?.Distro?.Id);
if (effect.Kind == kind)
{
return true;
}
}
return false;
}
private bool HasPackageEffect(PackageEffectKind kind, out PackageEffectData effect)
{
effect = default;
IReadOnlyList<PackageInstance> packages = runConfig?.EquippedPackages;
if (packages == null)
{
return false;
}
for (int i = 0; i < packages.Count; i++)
{
PackageInstance package = packages[i];
if (package?.Definition == null)
{
continue;
}
effect = package.EffectFor(runConfig?.Distro?.Id);
if (effect.Kind == kind)
{
return true;
}
}
return false;
}
private static bool HasHarmfulStatus(CombatantState state)
{
if (state == null)
{
return false;
}
for (int i = 0; i < state.Statuses.Count; i++)
{
if (!StatusEffectController.GetDescriptor(state.Statuses[i].Type).IsBeneficial)
{
return true;
}
}
return false;
}
private static bool IsCrashImmune(CardInstance card)
{
string id = card?.Definition?.Id;
+1
View File
@@ -30,6 +30,7 @@ namespace KernelPanic.Combat
public bool IgnoreDamageMultipliers { get; set; }
public bool AllowFlatDamageBuffs { get; set; }
public int FlatEffectBonus { get; set; }
public int JavaScriptFlatDamageBonus { get; set; }
public int DamageMultiplierPercent { get; set; } = 100;
public int IncomingAttackHalfCharges { get; set; }
public IReadOnlyList<StatusInstance> Statuses => _statuses;
+11
View File
@@ -64,6 +64,7 @@ namespace KernelPanic.Combat
}
int amount = Mathf.Max(0, request.Amount);
amount = ApplyFlatAdditions(amount, request);
amount = ApplyMultipliers(amount, request);
amount = ApplyCrit(amount, request, out bool wasCritical);
amount = ApplyResistancesAndWeaknesses(amount, request);
@@ -95,6 +96,16 @@ namespace KernelPanic.Combat
return Mathf.RoundToInt(amount * (multiplier / 100f));
}
private static int ApplyFlatAdditions(int amount, DamageRequest request)
{
if (request.Source != null && request.Language == Language.JavaScript)
{
amount += Mathf.Max(0, request.Source.JavaScriptFlatDamageBonus);
}
return amount;
}
private static int ApplyCrit(int amount, DamageRequest request, out bool wasCritical)
{
wasCritical = false;
+69
View File
@@ -0,0 +1,69 @@
using System.Collections.Generic;
using UnityEngine;
namespace KernelPanic.Data
{
public sealed class PackageDatabase : ScriptableObject
{
[SerializeField] private List<PackageDefinition> allPackages = new();
public IReadOnlyList<PackageDefinition> AllPackages => allPackages;
public PackageDefinition FindById(string id)
{
if (string.IsNullOrWhiteSpace(id))
{
return null;
}
for (int i = 0; i < allPackages.Count; i++)
{
PackageDefinition package = allPackages[i];
if (package != null && string.Equals(package.Id, id, System.StringComparison.OrdinalIgnoreCase))
{
return package;
}
}
return null;
}
public PackageDefinition FindByRarity(int rarity, int index)
{
int safeRarity = Mathf.Clamp(rarity, 1, 5);
int matchIndex = 0;
for (int i = 0; i < allPackages.Count; i++)
{
PackageDefinition package = allPackages[i];
if (package == null || package.Rarity != safeRarity)
{
continue;
}
if (matchIndex == index)
{
return package;
}
matchIndex++;
}
return null;
}
public int CountByRarity(int rarity)
{
int safeRarity = Mathf.Clamp(rarity, 1, 5);
int count = 0;
for (int i = 0; i < allPackages.Count; i++)
{
if (allPackages[i] != null && allPackages[i].Rarity == safeRarity)
{
count++;
}
}
return count;
}
}
}
+166
View File
@@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace KernelPanic.Data
{
public enum PackageSlot
{
Kernel,
Runtime,
Daemon
}
public enum PackageEffectKind
{
None = 0,
MaxUptime = 1,
MaxCycles = 2,
WaveDraw = 3,
WaveGenerateBasicCard = 4,
FirstCardEachWaveCostReduction = 5,
EveryNthTurnShield = 6,
ExhaustShield = 7,
EveryNthCardEachWaveFree = 8,
FirstCardsEachTurnCostReduction = 9,
ThirdCardEachTurnGenerate = 10,
JavaScriptFlatDamage = 11,
WaveThresholdRestore = 12,
FirstShieldEachTurnBonus = 13,
DnfFedoraPassive = 14,
WaveStartShield = 15,
FirstTurnEachWaveShield = 16,
FirstTurnFirstWaveDraw = 17,
FirstTurnEachWaveCycle = 18,
MaxRam = 19,
FirstNativeCardEachWaveFlatDamage = 20,
FirstInterpreterQueueCardEachWaveShield = 21,
EveryNthTurnDraw = 22,
StartTurnNoDebuffShield = 23,
EveryNthCardEachWaveCycle = 24
}
[Serializable]
public struct PackageEffectData
{
[SerializeField] private PackageEffectKind kind;
[SerializeField] private int amount;
[SerializeField] private int threshold;
[SerializeField] private bool refundCycle;
[SerializeField] private bool cleanseDebuffs;
[SerializeField] private bool enableFedoraSecondCardPassive;
public PackageEffectData(PackageEffectKind kind, int amount, int threshold, bool refundCycle, bool cleanseDebuffs, bool enableFedoraSecondCardPassive)
{
this.kind = kind;
this.amount = amount;
this.threshold = threshold;
this.refundCycle = refundCycle;
this.cleanseDebuffs = cleanseDebuffs;
this.enableFedoraSecondCardPassive = enableFedoraSecondCardPassive;
}
public PackageEffectKind Kind => kind;
public int Amount => amount;
public int Threshold => threshold;
public bool RefundCycle => refundCycle;
public bool CleanseDebuffs => cleanseDebuffs;
public bool EnableFedoraSecondCardPassive => enableFedoraSecondCardPassive;
}
/// <summary>
/// Defines persistent meta packages equipped into distro Kernel/Runtime/Daemon slots.
/// Package loadouts are meta progression and carry into every run for that distro.
/// Card loadouts remain run setup only.
/// </summary>
public sealed class PackageDefinition : ScriptableObject
{
[SerializeField] private string id;
[SerializeField] private string displayName;
[SerializeField] private PackageSlot slot;
[SerializeField, Range(1, 5)] private int rarity = 1;
[SerializeField, TextArea] private string description;
[SerializeField] private string flavourText;
[SerializeField, TextArea] private string designNotes;
[SerializeField] private string intendedDistroId;
[SerializeField] private List<string> requiresSystem = new();
[SerializeField] private PackageEffectData offDistroEffect;
[SerializeField] private PackageEffectData onDistroEffect;
public string Id => id;
public string DisplayName => displayName;
public PackageSlot Slot => slot;
public int Rarity => Mathf.Clamp(rarity, 1, 5);
public string Description => description;
public string FlavourText => flavourText;
public string FlavorText => flavourText;
public string DesignNotes => designNotes;
public string IntendedDistroId => intendedDistroId;
public IReadOnlyList<string> RequiresSystem => requiresSystem;
public PackageEffectData OffDistroEffect => offDistroEffect;
public PackageEffectData OnDistroEffect => onDistroEffect;
public bool IsIntendedFor(string distroId)
{
return !string.IsNullOrWhiteSpace(intendedDistroId)
&& string.Equals(intendedDistroId, distroId, StringComparison.OrdinalIgnoreCase);
}
public PackageEffectData EffectFor(string distroId)
{
return IsIntendedFor(distroId) ? onDistroEffect : offDistroEffect;
}
}
[Serializable]
public sealed class PackageInstance
{
public PackageInstance(PackageDefinition definition, int upgradeLevel)
{
Definition = definition;
UpgradeLevel = Mathf.Clamp(upgradeLevel, 0, KernelPanic.Meta.PackageTuning.MaxPackageLevel);
}
public PackageDefinition Definition { get; }
public int UpgradeLevel { get; }
public PackageEffectData EffectFor(string distroId)
{
PackageEffectData effect = Definition == null ? default : Definition.EffectFor(distroId);
return PackageEffectScaling.Scale(effect, UpgradeLevel);
}
}
public static class PackageEffectScaling
{
public static PackageEffectData Scale(PackageEffectData effect, int upgradeLevel)
{
int level = Mathf.Clamp(upgradeLevel, 0, KernelPanic.Meta.PackageTuning.MaxPackageLevel);
if (level <= 0 || effect.Kind == PackageEffectKind.None)
{
return effect;
}
int amount = effect.Amount + GetAmountPerLevel(effect.Kind, effect.Amount) * level;
return new PackageEffectData(effect.Kind, amount, effect.Threshold, effect.RefundCycle, effect.CleanseDebuffs, effect.EnableFedoraSecondCardPassive);
}
private static int GetAmountPerLevel(PackageEffectKind kind, int baseAmount)
{
return kind switch
{
PackageEffectKind.MaxUptime => Mathf.Max(1, Mathf.CeilToInt(Mathf.Max(1, baseAmount) * 0.5f)),
PackageEffectKind.MaxCycles => 1,
PackageEffectKind.MaxRam => 1,
PackageEffectKind.WaveDraw => 1,
PackageEffectKind.FirstTurnFirstWaveDraw => 1,
PackageEffectKind.FirstTurnEachWaveCycle => 1,
PackageEffectKind.FirstCardsEachTurnCostReduction => 1,
PackageEffectKind.FirstCardEachWaveCostReduction => 1,
PackageEffectKind.FirstNativeCardEachWaveFlatDamage => 1,
PackageEffectKind.JavaScriptFlatDamage => 1,
_ => 1
};
}
}
}
+7 -2
View File
@@ -5,7 +5,12 @@ namespace KernelPanic.Meta
/// </summary>
public enum GachaCurrencyType
{
StandardPull,
LimitedPull
StandardPull
}
public enum PullPaymentSource
{
Commits,
Entropy
}
}
+7 -5
View File
@@ -16,9 +16,9 @@ namespace KernelPanic.Meta
public static bool HasPendingPull => pendingPull != null;
public static bool HasCompletedPull => completedPull != null;
public static void SetPending(string bannerId, int pullCount, int entropyTokenCount, DistroDatabase distroDatabase, string focusUnitId)
public static void SetPending(string bannerId, int pullCount, int entropyTokenCount, DistroDatabase distroDatabase, PackageDatabase packageDatabase, string focusUnitId)
{
pendingPull = new PendingGachaPull(bannerId, pullCount, entropyTokenCount, distroDatabase, focusUnitId);
pendingPull = new PendingGachaPull(bannerId, pullCount, entropyTokenCount, distroDatabase, packageDatabase, focusUnitId);
completedPull = null;
}
@@ -44,12 +44,13 @@ namespace KernelPanic.Meta
public sealed class PendingGachaPull
{
public PendingGachaPull(string bannerId, int pullCount, int entropyTokenCount, DistroDatabase distroDatabase, string focusUnitId)
public PendingGachaPull(string bannerId, int pullCount, int entropyTokenCount, DistroDatabase distroDatabase, PackageDatabase packageDatabase, string focusUnitId)
{
BannerId = string.IsNullOrWhiteSpace(bannerId) ? GachaService.BeginnerBannerId : bannerId;
PullCount = Math.Max(1, pullCount);
EntropyTokenCount = Math.Max(0, entropyTokenCount);
DistroDatabase = distroDatabase;
PackageDatabase = packageDatabase;
FocusUnitId = focusUnitId;
}
@@ -57,6 +58,7 @@ namespace KernelPanic.Meta
public int PullCount { get; }
public int EntropyTokenCount { get; }
public DistroDatabase DistroDatabase { get; }
public PackageDatabase PackageDatabase { get; }
public string FocusUnitId { get; }
}
@@ -131,7 +133,7 @@ namespace KernelPanic.Meta
public bool PityTriggered { get; }
public bool IsCharacter => RewardType == GachaRewardType.Distro || RewardType == GachaRewardType.FutureStandardFiveStarDistro;
public string TypeText => IsCharacter ? "character" : "equipment";
public string TypeText => IsCharacter ? "character" : "package";
public string StatusText
{
@@ -173,7 +175,7 @@ namespace KernelPanic.Meta
return "unknown result";
}
if (rewardType != GachaRewardType.Equipment)
if (rewardType != GachaRewardType.Package)
{
return displayName;
}
+1 -1
View File
@@ -5,7 +5,7 @@ namespace KernelPanic.Meta
/// </summary>
public enum GachaRewardType
{
Equipment,
Package,
Distro,
FutureStandardFiveStarDistro
}
+72 -87
View File
@@ -14,11 +14,12 @@ namespace KernelPanic.Meta
public const string LimitedBannerId = "limited";
public const int BeginnerMaxPulls = 50;
public const int BeginnerSinglePullCost = 1;
public const int BeginnerTenPullCost = 8;
public const int BeginnerTenPullCost = 10;
public const int FourStarHardPity = 10;
public const int FiveStarSoftPityStart = 66;
public const int FiveStarHardPity = 80;
public const int EntropyPerPullToken = 100;
public const int EntropyPerCommit = 100;
public const int RootCreditsToEntropy = 1;
public const int TestRootCreditsBalance = 10000;
private const double FourStarBaseChance = 0.12d;
@@ -41,11 +42,10 @@ namespace KernelPanic.Meta
{
this._random = random ?? new Random();
_currencyBalances[GachaCurrencyType.StandardPull] = 0;
_currencyBalances[GachaCurrencyType.LimitedPull] = 0;
}
public int PullTokens => GetCurrencyBalance(GachaCurrencyType.StandardPull);
public int LimitedPullTokens => GetCurrencyBalance(GachaCurrencyType.LimitedPull);
public int Commits => PullTokens;
public int RootCredits { get; private set; }
public IReadOnlyList<DistroDefinition> BannerPool => _bannerPool;
public GachaBannerState BeginnerState => _beginnerState;
@@ -62,9 +62,8 @@ namespace KernelPanic.Meta
}
data.EnsureLists();
RootCredits = Math.Max(TestRootCreditsBalance, data.rootCredits);
SetCurrencyBalanceSilently(GachaCurrencyType.StandardPull, data.standardPullCurrency);
SetCurrencyBalanceSilently(GachaCurrencyType.LimitedPull, data.limitedPullCurrency);
RootCredits = Math.Max(0, data.rootCredits);
SetCurrencyBalanceSilently(GachaCurrencyType.StandardPull, data.commitsBalance);
_beginnerState = data.beginnerBannerState ?? new GachaBannerState(BeginnerBannerId);
_beginnerState.bannerId = BeginnerBannerId;
_beginnerState.EnsureLists();
@@ -87,8 +86,7 @@ namespace KernelPanic.Meta
data.EnsureLists();
data.rootCredits = RootCredits;
data.standardPullCurrency = GetCurrencyBalance(GachaCurrencyType.StandardPull);
data.limitedPullCurrency = GetCurrencyBalance(GachaCurrencyType.LimitedPull);
data.commitsBalance = GetCurrencyBalance(GachaCurrencyType.StandardPull);
data.beginnerBannerState = CloneState(_beginnerState);
}
@@ -125,6 +123,11 @@ namespace KernelPanic.Meta
}
public bool ConvertRootCreditsToEntropy(EntropyWallet wallet, int amount, out string failureReason)
{
return BuyEntropy(wallet, amount, out failureReason);
}
public bool BuyEntropy(EntropyWallet wallet, int rootCredits, out string failureReason)
{
if (wallet == null)
{
@@ -132,20 +135,20 @@ namespace KernelPanic.Meta
return false;
}
if (amount <= 0)
if (rootCredits <= 0)
{
failureReason = "no root-credits selected";
failureReason = "no Root Credits selected";
return false;
}
if (RootCredits < amount)
if (RootCredits < rootCredits)
{
failureReason = $"need {amount} root-credits";
failureReason = $"need {rootCredits} Root Credits";
return false;
}
RootCredits -= amount;
wallet.Add(amount);
RootCredits -= rootCredits;
wallet.Add(rootCredits * RootCreditsToEntropy);
failureReason = null;
Changed?.Invoke();
return true;
@@ -187,6 +190,11 @@ namespace KernelPanic.Meta
}
public bool CanPullBeginner(int pullCount, out string failureReason)
{
return CanAffordPull(pullCount, null, PullPaymentSource.Commits, out failureReason);
}
public bool CanAffordPull(int pullCount, EntropyWallet wallet, PullPaymentSource source, out string failureReason)
{
if (pullCount != 1 && pullCount != 10)
{
@@ -208,49 +216,73 @@ namespace KernelPanic.Meta
}
int cost = GetBeginnerPullCost(pullCount);
if (GetCurrencyBalance(GachaCurrencyType.StandardPull) < cost)
if (source == PullPaymentSource.Commits && GetCurrencyBalance(GachaCurrencyType.StandardPull) < cost)
{
failureReason = $"need {cost} {FormatCurrencyName(GachaCurrencyType.StandardPull)}";
return false;
}
int entropyCost = cost * EntropyPerCommit;
if (source == PullPaymentSource.Entropy && (wallet == null || wallet.Balance < entropyCost))
{
failureReason = $"need {entropyCost} Entropy";
return false;
}
failureReason = null;
return true;
}
public bool PayForPull(int pullCount, EntropyWallet wallet, PullPaymentSource source, out int commitsSpent, out int entropySpent, out string failureReason)
{
commitsSpent = 0;
entropySpent = 0;
if (!CanAffordPull(pullCount, wallet, source, out failureReason))
{
return false;
}
int cost = GetBeginnerPullCost(pullCount);
if (source == PullPaymentSource.Commits)
{
commitsSpent = cost;
SetCurrencyBalanceSilently(GachaCurrencyType.StandardPull, GetCurrencyBalance(GachaCurrencyType.StandardPull) - commitsSpent);
}
else
{
entropySpent = cost * EntropyPerCommit;
wallet.Spend(entropySpent);
}
return true;
}
public int GetMissingPullTokens(GachaCurrencyType currencyType, int pullCost)
{
return Math.Max(0, pullCost - GetCurrencyBalance(currencyType));
}
public bool CanCoverMissingPullTokensWithEntropy(EntropyWallet wallet, int missingPullTokens)
{
return missingPullTokens <= 0 || (wallet != null && wallet.Balance >= missingPullTokens * EntropyPerPullToken);
}
public GachaPullResult PerformBeginnerPull(int pullCount)
{
return PerformBeginnerPull(pullCount, null, 0);
return PerformBeginnerPull(pullCount, null, PullPaymentSource.Commits);
}
public GachaPullResult PerformBeginnerPull(int pullCount, EntropyWallet wallet, int entropyTokenCount)
{
if (!CanPullBeginnerWithEntropy(pullCount, wallet, entropyTokenCount, out string failureReason))
PullPaymentSource source = entropyTokenCount > 0 ? PullPaymentSource.Entropy : PullPaymentSource.Commits;
return PerformBeginnerPull(pullCount, wallet, source);
}
public GachaPullResult PerformBeginnerPull(int pullCount, EntropyWallet wallet, PullPaymentSource source)
{
if (!CanAffordPull(pullCount, wallet, source, out string failureReason))
{
return GachaPullResult.Failed(BeginnerBannerId, failureReason);
}
int cost = GetBeginnerPullCost(pullCount);
int tokenSpend = cost - entropyTokenCount;
if (tokenSpend > 0)
if (!PayForPull(pullCount, wallet, source, out int commitSpend, out int entropySpent, out failureReason))
{
SetCurrencyBalanceSilently(GachaCurrencyType.StandardPull, GetCurrencyBalance(GachaCurrencyType.StandardPull) - tokenSpend);
}
int entropySpent = entropyTokenCount * EntropyPerPullToken;
if (entropySpent > 0)
{
wallet.Spend(entropySpent);
return GachaPullResult.Failed(BeginnerBannerId, failureReason);
}
List<GachaReward> rewards = new();
@@ -266,14 +298,13 @@ namespace KernelPanic.Meta
}
Changed?.Invoke();
return new GachaPullResult(true, BeginnerBannerId, GachaCurrencyType.StandardPull, tokenSpend, entropySpent, rewards, null);
return new GachaPullResult(true, BeginnerBannerId, GachaCurrencyType.StandardPull, commitSpend, entropySpent, rewards, null);
}
public static string FormatCurrencyName(GachaCurrencyType currencyType)
{
return currencyType switch
{
GachaCurrencyType.LimitedPull => "Root Credits",
_ => "Commits"
};
}
@@ -335,7 +366,7 @@ namespace KernelPanic.Meta
if (!hitFourStarOrCharacter)
{
_beginnerState.pityCounter++;
return GachaReward.Equipment(3, "3-star equipment", false, false);
return GachaReward.Package(3, "3-star package", false, false);
}
_beginnerState.pityCounter = 0;
@@ -345,7 +376,7 @@ namespace KernelPanic.Meta
return GachaReward.DistroReward(distro, 4, pityTriggered, false);
}
return GachaReward.Equipment(4, "4-star equipment", pityTriggered, false);
return GachaReward.Package(4, "4-star package", pityTriggered, false);
}
private bool TryRollFiveStar(GachaBannerState state, out bool pityTriggered)
@@ -379,7 +410,7 @@ namespace KernelPanic.Meta
{
if (_bannerPool.Count == 0 || _random.NextDouble() >= DistroChanceOnFeaturedTier)
{
return GachaReward.Equipment(5, "5-star equipment", pityTriggered, false);
return GachaReward.Package(5, "5-star package", pityTriggered, false);
}
DistroDefinition distro = _bannerPool.Count == 0 ? null : _bannerPool[_random.Next(_bannerPool.Count)];
@@ -420,7 +451,7 @@ namespace KernelPanic.Meta
distro ??= _bannerPool.Count == 0 ? null : _bannerPool[Math.Min(guaranteeIndex, _bannerPool.Count - 1)];
return distro == null
? GachaReward.Equipment(4, "4-star equipment", true, true)
? GachaReward.Package(4, "4-star package", true, true)
: GachaReward.DistroReward(distro, 4, true, true, reason);
}
@@ -448,52 +479,6 @@ namespace KernelPanic.Meta
_currencyBalances[currencyType] = Math.Max(0, amount);
}
private bool CanPullBeginnerWithEntropy(int pullCount, EntropyWallet wallet, int entropyTokenCount, out string failureReason)
{
if (pullCount != 1 && pullCount != 10)
{
failureReason = "only single and ten pulls are supported";
return false;
}
if (!IsBeginnerBannerAvailable)
{
failureReason = "beginner banner unavailable";
return false;
}
int remainingPulls = BeginnerMaxPulls - _beginnerState.totalPulls;
if (pullCount > remainingPulls)
{
failureReason = $"only {remainingPulls} beginner pull(s) remain";
return false;
}
int cost = GetBeginnerPullCost(pullCount);
if (entropyTokenCount < 0 || entropyTokenCount > cost)
{
failureReason = "invalid entropy substitute amount";
return false;
}
int tokenSpend = cost - entropyTokenCount;
if (GetCurrencyBalance(GachaCurrencyType.StandardPull) < tokenSpend)
{
failureReason = $"need {tokenSpend} {FormatCurrencyName(GachaCurrencyType.StandardPull)}";
return false;
}
int entropySpend = entropyTokenCount * EntropyPerPullToken;
if (entropySpend > 0 && (wallet == null || wallet.Balance < entropySpend))
{
failureReason = $"need {entropySpend} entropy";
return false;
}
failureReason = null;
return true;
}
private static GachaBannerState CloneState(GachaBannerState state)
{
state ??= new GachaBannerState(BeginnerBannerId);
@@ -542,9 +527,9 @@ namespace KernelPanic.Meta
public bool Guaranteed { get; }
public string GuaranteeReason { get; }
public static GachaReward Equipment(int starRating, string displayName, bool pityTriggered, bool guaranteed)
public static GachaReward Package(int starRating, string displayName, bool pityTriggered, bool guaranteed)
{
return new GachaReward(GachaRewardType.Equipment, starRating, displayName, null, pityTriggered, guaranteed, null);
return new GachaReward(GachaRewardType.Package, starRating, displayName, null, pityTriggered, guaranteed, null);
}
public static GachaReward DistroReward(DistroDefinition distro, int starRating, bool pityTriggered, bool guaranteed, string guaranteeReason = null)
+190
View File
@@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using KernelPanic.Data;
namespace KernelPanic.Meta
{
public enum PackageLoadoutFailureReason
{
None,
WrongSlot,
NotOwned,
SlotOccupied,
NotEquipped
}
/// <summary>
/// Pure meta model for persistent package slots. Unlike CardLoadout, this is not
/// run-only: equipped package ids are saved per distro and reused in every run.
/// </summary>
public sealed class PackageLoadout
{
private readonly PlayerCollection _collection;
private readonly PackageDatabase _database;
private readonly Dictionary<string, Dictionary<PackageSlot, string>> _equippedByDistro = new(StringComparer.OrdinalIgnoreCase);
public PackageLoadout(PlayerCollection collection, PackageDatabase database)
{
_collection = collection;
_database = database;
}
public IReadOnlyDictionary<PackageSlot, string> GetEquippedPackageIds(string distroId)
{
return !string.IsNullOrWhiteSpace(distroId) && _equippedByDistro.TryGetValue(distroId, out Dictionary<PackageSlot, string> equipped)
? equipped
: Empty;
}
public PackageDefinition GetEquippedPackage(string distroId, PackageSlot slot)
{
if (string.IsNullOrWhiteSpace(distroId)
|| !_equippedByDistro.TryGetValue(distroId, out Dictionary<PackageSlot, string> equipped)
|| !equipped.TryGetValue(slot, out string packageId))
{
return null;
}
return _database == null ? null : _database.FindById(packageId);
}
public IReadOnlyList<PackageDefinition> GetEquippedPackages(string distroId)
{
List<PackageDefinition> packages = new();
AddIfEquipped(distroId, PackageSlot.Kernel, packages);
AddIfEquipped(distroId, PackageSlot.Runtime, packages);
AddIfEquipped(distroId, PackageSlot.Daemon, packages);
return packages;
}
public IReadOnlyList<PackageInstance> GetEquippedPackageInstances(string distroId)
{
List<PackageInstance> packages = new();
AddInstanceIfEquipped(distroId, PackageSlot.Kernel, packages);
AddInstanceIfEquipped(distroId, PackageSlot.Runtime, packages);
AddInstanceIfEquipped(distroId, PackageSlot.Daemon, packages);
return packages;
}
public bool IsEquipped(string packageId)
{
if (string.IsNullOrWhiteSpace(packageId))
{
return false;
}
foreach (KeyValuePair<string, Dictionary<PackageSlot, string>> distro in _equippedByDistro)
{
foreach (KeyValuePair<PackageSlot, string> slot in distro.Value)
{
if (string.Equals(slot.Value, packageId, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
}
public bool TryEquip(string distroId, PackageSlot slot, string packageId, out PackageLoadoutFailureReason reason)
{
PackageDefinition package = _database == null ? null : _database.FindById(packageId);
if (package == null || _collection == null || !_collection.IsPackageOwned(package.Id))
{
reason = PackageLoadoutFailureReason.NotOwned;
return false;
}
if (package.Slot != slot)
{
reason = PackageLoadoutFailureReason.WrongSlot;
return false;
}
Dictionary<PackageSlot, string> equipped = EnsureDistro(distroId);
if (equipped.TryGetValue(slot, out string current) && !string.Equals(current, package.Id, StringComparison.OrdinalIgnoreCase))
{
reason = PackageLoadoutFailureReason.SlotOccupied;
return false;
}
equipped[slot] = package.Id;
reason = PackageLoadoutFailureReason.None;
return true;
}
public bool TryUnequip(string distroId, PackageSlot slot, out PackageLoadoutFailureReason reason)
{
if (string.IsNullOrWhiteSpace(distroId)
|| !_equippedByDistro.TryGetValue(distroId, out Dictionary<PackageSlot, string> equipped)
|| !equipped.Remove(slot))
{
reason = PackageLoadoutFailureReason.NotEquipped;
return false;
}
reason = PackageLoadoutFailureReason.None;
return true;
}
public void Load(string distroId, IEnumerable<PackageLoadoutSlotSaveEntry> slots)
{
if (string.IsNullOrWhiteSpace(distroId))
{
return;
}
Dictionary<PackageSlot, string> equipped = EnsureDistro(distroId);
equipped.Clear();
foreach (PackageLoadoutSlotSaveEntry slotEntry in slots ?? Array.Empty<PackageLoadoutSlotSaveEntry>())
{
PackageDefinition package = _database == null ? null : _database.FindById(slotEntry?.packageId);
if (package == null || !_collection.IsPackageOwned(package.Id) || package.Slot != slotEntry.slot)
{
continue;
}
equipped[package.Slot] = package.Id;
}
}
public void Clear()
{
_equippedByDistro.Clear();
}
private void AddIfEquipped(string distroId, PackageSlot slot, List<PackageDefinition> packages)
{
PackageDefinition package = GetEquippedPackage(distroId, slot);
if (package != null)
{
packages.Add(package);
}
}
private void AddInstanceIfEquipped(string distroId, PackageSlot slot, List<PackageInstance> packages)
{
PackageDefinition definition = GetEquippedPackage(distroId, slot);
OwnedPackageInstance owned = definition == null ? null : _collection.GetOwnedPackage(definition.Id);
if (owned != null)
{
packages.Add(owned.ToRuntimeInstance());
}
}
private Dictionary<PackageSlot, string> EnsureDistro(string distroId)
{
string key = string.IsNullOrWhiteSpace(distroId) ? string.Empty : distroId;
if (!_equippedByDistro.TryGetValue(key, out Dictionary<PackageSlot, string> equipped))
{
equipped = new Dictionary<PackageSlot, string>();
_equippedByDistro[key] = equipped;
}
return equipped;
}
private static readonly IReadOnlyDictionary<PackageSlot, string> Empty = new Dictionary<PackageSlot, string>();
}
}
+64
View File
@@ -0,0 +1,64 @@
using System;
using KernelPanic.Data;
namespace KernelPanic.Meta
{
public enum PackageScrapFailureReason
{
None,
NotOwned,
Equipped
}
public readonly struct PackageScrapResult
{
public PackageScrapResult(bool success, PackageScrapFailureReason failureReason, int cacheGranted, int bandwidthRefunded)
{
Success = success;
FailureReason = failureReason;
CacheGranted = cacheGranted;
BandwidthRefunded = bandwidthRefunded;
}
public bool Success { get; }
public PackageScrapFailureReason FailureReason { get; }
public int CacheGranted { get; }
public int BandwidthRefunded { get; }
}
public static class PackageScrapper
{
public static PackageScrapResult Scrap(PlayerCollection collection, PackageLoadout loadout, SaveData wallet, string packageId)
{
if (collection == null || wallet == null || string.IsNullOrWhiteSpace(packageId))
{
return new PackageScrapResult(false, PackageScrapFailureReason.NotOwned, 0, 0);
}
OwnedPackageInstance package = collection.GetOwnedPackage(packageId);
if (package == null)
{
return new PackageScrapResult(false, PackageScrapFailureReason.NotOwned, 0, 0);
}
if (loadout != null && loadout.IsEquipped(packageId))
{
return new PackageScrapResult(false, PackageScrapFailureReason.Equipped, 0, 0);
}
PackageDefinition definition = package.Definition;
int rarity = definition == null ? 1 : definition.Rarity;
int cache = PackageTuning.GetCacheForRarity(rarity) + PackageTuning.GetRefundedInvestedCache(package.UpgradeLevel, rarity);
int bandwidth = PackageTuning.GetRefundedInvestedBandwidth(package.UpgradeLevel, rarity);
if (!collection.RemovePackage(packageId))
{
return new PackageScrapResult(false, PackageScrapFailureReason.NotOwned, 0, 0);
}
wallet.cacheBalance += Math.Max(0, cache);
wallet.bandwidthBalance += Math.Max(0, bandwidth);
return new PackageScrapResult(true, PackageScrapFailureReason.None, cache, bandwidth);
}
}
}
+69
View File
@@ -0,0 +1,69 @@
using System;
using UnityEngine;
namespace KernelPanic.Meta
{
public static class PackageTuning
{
public const int MaxPackageLevel = 5;
public const float RefundFraction = 1f;
public const float RarityCostMultiplier = 1f;
private static readonly int[] CachePerRarityValues = { 5, 12, 30, 80, 200 };
private static readonly int[] UpgradeCacheCostValues = { 10, 25, 50, 90, 150 };
private static readonly int[] UpgradeBandwidthCostValues = { 20, 50, 100, 180, 300 };
public static int GetCacheForRarity(int rarity)
{
return CachePerRarityValues[Mathf.Clamp(rarity, 1, CachePerRarityValues.Length) - 1];
}
public static int GetUpgradeCacheCost(int nextLevel, int rarity)
{
return ApplyRarityMultiplier(UpgradeCacheCostValues[Mathf.Clamp(nextLevel, 1, UpgradeCacheCostValues.Length) - 1], rarity);
}
public static int GetUpgradeBandwidthCost(int nextLevel, int rarity)
{
return ApplyRarityMultiplier(UpgradeBandwidthCostValues[Mathf.Clamp(nextLevel, 1, UpgradeBandwidthCostValues.Length) - 1], rarity);
}
public static int GetInvestedCache(int level, int rarity)
{
int total = 0;
for (int nextLevel = 1; nextLevel <= Mathf.Clamp(level, 0, MaxPackageLevel); nextLevel++)
{
total += GetUpgradeCacheCost(nextLevel, rarity);
}
return total;
}
public static int GetInvestedBandwidth(int level, int rarity)
{
int total = 0;
for (int nextLevel = 1; nextLevel <= Mathf.Clamp(level, 0, MaxPackageLevel); nextLevel++)
{
total += GetUpgradeBandwidthCost(nextLevel, rarity);
}
return total;
}
public static int GetRefundedInvestedCache(int level, int rarity)
{
return Mathf.RoundToInt(GetInvestedCache(level, rarity) * RefundFraction);
}
public static int GetRefundedInvestedBandwidth(int level, int rarity)
{
return Mathf.RoundToInt(GetInvestedBandwidth(level, rarity) * RefundFraction);
}
private static int ApplyRarityMultiplier(int baseCost, int rarity)
{
float multiplier = RarityCostMultiplier <= 0f ? 1f : RarityCostMultiplier;
return Math.Max(0, Mathf.RoundToInt(baseCost * multiplier));
}
}
}
+72
View File
@@ -0,0 +1,72 @@
using System;
using KernelPanic.Data;
namespace KernelPanic.Meta
{
public enum PackageUpgradeFailureReason
{
None,
NotOwned,
MaxLevel,
InsufficientCache,
InsufficientBandwidth
}
public readonly struct PackageUpgradeResult
{
public PackageUpgradeResult(bool success, PackageUpgradeFailureReason failureReason, int cacheCost, int bandwidthCost, int newLevel)
{
Success = success;
FailureReason = failureReason;
CacheCost = cacheCost;
BandwidthCost = bandwidthCost;
NewLevel = newLevel;
}
public bool Success { get; }
public PackageUpgradeFailureReason FailureReason { get; }
public int CacheCost { get; }
public int BandwidthCost { get; }
public int NewLevel { get; }
}
public static class PackageUpgrader
{
public static event Action<OwnedPackageInstance> Upgraded;
public static PackageUpgradeResult TryUpgrade(OwnedPackageInstance packageInstance, SaveData wallet)
{
if (packageInstance == null || wallet == null)
{
return new PackageUpgradeResult(false, PackageUpgradeFailureReason.NotOwned, 0, 0, 0);
}
int currentLevel = Math.Max(0, packageInstance.UpgradeLevel);
if (currentLevel >= PackageTuning.MaxPackageLevel)
{
return new PackageUpgradeResult(false, PackageUpgradeFailureReason.MaxLevel, 0, 0, currentLevel);
}
int nextLevel = currentLevel + 1;
PackageDefinition definition = packageInstance.Definition;
int rarity = definition == null ? 1 : definition.Rarity;
int cacheCost = PackageTuning.GetUpgradeCacheCost(nextLevel, rarity);
int bandwidthCost = PackageTuning.GetUpgradeBandwidthCost(nextLevel, rarity);
if (wallet.cacheBalance < cacheCost)
{
return new PackageUpgradeResult(false, PackageUpgradeFailureReason.InsufficientCache, cacheCost, bandwidthCost, currentLevel);
}
if (wallet.bandwidthBalance < bandwidthCost)
{
return new PackageUpgradeResult(false, PackageUpgradeFailureReason.InsufficientBandwidth, cacheCost, bandwidthCost, currentLevel);
}
wallet.cacheBalance -= cacheCost;
wallet.bandwidthBalance -= bandwidthCost;
packageInstance.SetUpgradeLevel(nextLevel);
Upgraded?.Invoke(packageInstance);
return new PackageUpgradeResult(true, PackageUpgradeFailureReason.None, cacheCost, bandwidthCost, nextLevel);
}
}
}
+123
View File
@@ -10,11 +10,14 @@ namespace KernelPanic.Meta
public sealed class PlayerCollection
{
private readonly List<DistroDefinition> _ownedUnits = new();
private readonly List<OwnedPackageInstance> _ownedPackages = new();
private readonly Dictionary<string, int> _versions = new(StringComparer.OrdinalIgnoreCase);
private int _featuredIndex;
// TODO: Populate owned units from SaveService during bootstrap.
public IReadOnlyList<DistroDefinition> OwnedUnits => _ownedUnits;
public IReadOnlyList<OwnedPackageInstance> OwnedPackageInstances => _ownedPackages;
public IReadOnlyList<PackageDefinition> OwnedPackages => BuildOwnedPackageDefinitions();
public DistroDefinition FeaturedUnit => _ownedUnits.Count == 0 ? null : _ownedUnits[Math.Clamp(_featuredIndex, 0, _ownedUnits.Count - 1)];
public event Action Changed;
@@ -34,6 +37,35 @@ namespace KernelPanic.Meta
Add(unit, version, false);
}
public void AddPackage(PackageDefinition package)
{
AddPackage(package, 0, true);
}
public void AddPackageSilently(PackageDefinition package)
{
AddPackage(package, 0, false);
}
public void AddPackageSilently(PackageDefinition package, int upgradeLevel)
{
AddPackage(package, upgradeLevel, false);
}
private void AddPackage(PackageDefinition package, int upgradeLevel, bool notify)
{
if (package == null || IsPackageOwned(package.Id))
{
return;
}
_ownedPackages.Add(new OwnedPackageInstance(package, upgradeLevel));
if (notify)
{
Changed?.Invoke();
}
}
private void Add(DistroDefinition unit, int version, bool notify)
{
if (unit == null)
@@ -82,6 +114,60 @@ namespace KernelPanic.Meta
return count;
}
public bool IsPackageOwned(string packageId)
{
if (string.IsNullOrWhiteSpace(packageId))
{
return false;
}
for (int i = 0; i < _ownedPackages.Count; i++)
{
OwnedPackageInstance package = _ownedPackages[i];
if (package != null && string.Equals(package.PackageId, packageId, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
public OwnedPackageInstance GetOwnedPackage(string packageId)
{
if (string.IsNullOrWhiteSpace(packageId))
{
return null;
}
for (int i = 0; i < _ownedPackages.Count; i++)
{
OwnedPackageInstance package = _ownedPackages[i];
if (package != null && string.Equals(package.PackageId, packageId, StringComparison.OrdinalIgnoreCase))
{
return package;
}
}
return null;
}
public bool RemovePackage(string packageId)
{
for (int i = _ownedPackages.Count - 1; i >= 0; i--)
{
OwnedPackageInstance package = _ownedPackages[i];
if (package != null && string.Equals(package.PackageId, packageId, StringComparison.OrdinalIgnoreCase))
{
_ownedPackages.RemoveAt(i);
Changed?.Invoke();
return true;
}
}
return false;
}
public int GetVersion(string unitId)
{
if (string.IsNullOrWhiteSpace(unitId))
@@ -131,5 +217,42 @@ namespace KernelPanic.Meta
_featuredIndex = (_featuredIndex + 1) % _ownedUnits.Count;
}
private IReadOnlyList<PackageDefinition> BuildOwnedPackageDefinitions()
{
List<PackageDefinition> definitions = new();
for (int i = 0; i < _ownedPackages.Count; i++)
{
if (_ownedPackages[i]?.Definition != null)
{
definitions.Add(_ownedPackages[i].Definition);
}
}
return definitions;
}
}
public sealed class OwnedPackageInstance
{
public OwnedPackageInstance(PackageDefinition definition, int upgradeLevel)
{
Definition = definition;
UpgradeLevel = Math.Max(0, Math.Min(PackageTuning.MaxPackageLevel, upgradeLevel));
}
public PackageDefinition Definition { get; }
public string PackageId => Definition == null ? string.Empty : Definition.Id;
public int UpgradeLevel { get; private set; }
public PackageInstance ToRuntimeInstance()
{
return new PackageInstance(Definition, UpgradeLevel);
}
public void SetUpgradeLevel(int level)
{
UpgradeLevel = Math.Max(0, Math.Min(PackageTuning.MaxPackageLevel, level));
}
}
}
+1 -1
View File
@@ -93,7 +93,7 @@ namespace KernelPanic.Meta
owned.merges = Math.Max(0, owned.merges) + merges;
if (GachaTuning.DupeConsolationBandwidth > 0)
{
context.SaveData.standardPullCurrency += GachaTuning.DupeConsolationBandwidth;
context.SaveData.bandwidthBalance += GachaTuning.DupeConsolationBandwidth;
}
context.Collection?.AddSilently(unit, version);
+219
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using KernelPanic.Data;
using UnityEngine;
namespace KernelPanic.Meta
@@ -60,16 +61,28 @@ namespace KernelPanic.Meta
public bool starterChosen;
public int entropyBalance;
public int rootCredits = GachaService.TestRootCreditsBalance;
public int commitsBalance;
public int bandwidthBalance;
// Legacy conflated balance. Older builds used this for both Commits and Bandwidth.
// Migration preserves it as Bandwidth and starts Commits at 0 because the source is ambiguous.
public int standardPullCurrency;
// Legacy pull-token field kept only so older saves deserialize cleanly.
public int limitedPullCurrency;
public int cacheBalance;
public int packageMerges;
// Legacy global balance kept only so older saves deserialize cleanly.
// New duplicate-pull merges live on OwnedUnitSaveEntry.merges.
public int merges;
public List<OwnedUnitSaveEntry> ownedUnits = new();
public List<OwnedPackageSaveEntry> ownedPackages = new();
public List<string> ownedPackageIds = new();
public List<PackageLoadoutSaveEntry> packageLoadouts = new();
public List<string> ownedUnitIds = new();
public List<string> bannerPoolIds = new();
public GachaBannerState beginnerBannerState = new(GachaService.BeginnerBannerId);
public LastRunLoadoutSaveEntry lastRunLoadout = new();
public string lastRunDistroId;
public List<DistroBestWaveSaveEntry> distroBestWaves = new();
public static SaveData CreateDefault()
{
@@ -79,19 +92,56 @@ namespace KernelPanic.Meta
public void EnsureLists()
{
ownedUnits ??= new List<OwnedUnitSaveEntry>();
ownedPackages ??= new List<OwnedPackageSaveEntry>();
ownedPackageIds ??= new List<string>();
packageLoadouts ??= new List<PackageLoadoutSaveEntry>();
ownedUnitIds ??= new List<string>();
bannerPoolIds ??= new List<string>();
distroBestWaves ??= new List<DistroBestWaveSaveEntry>();
beginnerBannerState ??= new GachaBannerState(GachaService.BeginnerBannerId);
beginnerBannerState.bannerId = GachaService.BeginnerBannerId;
beginnerBannerState.EnsureLists();
lastRunLoadout ??= new LastRunLoadoutSaveEntry();
lastRunLoadout.EnsureLists();
if (string.IsNullOrWhiteSpace(lastRunDistroId) && !string.IsNullOrWhiteSpace(lastRunLoadout.distroId))
{
lastRunDistroId = lastRunLoadout.distroId;
}
MigrateLegacyConflatedCurrency();
entropyBalance = Math.Max(0, entropyBalance);
rootCredits = Math.Max(0, rootCredits);
commitsBalance = Math.Max(0, commitsBalance);
bandwidthBalance = Math.Max(0, bandwidthBalance);
merges = Math.Max(0, merges);
packageMerges = Math.Max(0, packageMerges);
cacheBalance = Math.Max(0, cacheBalance);
MigrateLegacyOwnedUnitIds();
MigrateLegacyOwnedPackageIds();
NormalizeOwnedUnits();
NormalizeOwnedPackages();
NormalizePackageLoadouts();
NormalizeDistroBestWaves();
MigrateLegacyGlobalMerges();
}
private void MigrateLegacyConflatedCurrency()
{
if (standardPullCurrency <= 0)
{
return;
}
if (bandwidthBalance <= 0)
{
bandwidthBalance = Math.Max(0, standardPullCurrency);
Debug.Log("Migrated legacy standardPullCurrency to Bandwidth. Commits defaulted to 0 because the old field conflated pull tokens and upgrade currency.");
}
standardPullCurrency = 0;
limitedPullCurrency = 0;
}
public OwnedUnitSaveEntry FindOwnedUnit(string id)
{
if (string.IsNullOrWhiteSpace(id))
@@ -176,6 +226,133 @@ namespace KernelPanic.Meta
}
}
private void NormalizeOwnedPackages()
{
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
for (int i = ownedPackages.Count - 1; i >= 0; i--)
{
OwnedPackageSaveEntry entry = ownedPackages[i];
if (entry == null || string.IsNullOrWhiteSpace(entry.id) || !seen.Add(entry.id))
{
ownedPackages.RemoveAt(i);
continue;
}
entry.upgradeLevel = Math.Max(0, Math.Min(PackageTuning.MaxPackageLevel, entry.upgradeLevel));
}
ownedPackageIds.Clear();
for (int i = 0; i < ownedPackages.Count; i++)
{
ownedPackageIds.Add(ownedPackages[i].id);
}
}
public int GetBestWave(string distroId)
{
if (string.IsNullOrWhiteSpace(distroId))
{
return 0;
}
EnsureLists();
for (int i = 0; i < distroBestWaves.Count; i++)
{
DistroBestWaveSaveEntry entry = distroBestWaves[i];
if (entry != null && string.Equals(entry.distroId, distroId, StringComparison.OrdinalIgnoreCase))
{
return Math.Max(0, entry.bestWave);
}
}
return 0;
}
public void RecordRunStats(string distroId, int waveReached)
{
if (string.IsNullOrWhiteSpace(distroId))
{
return;
}
EnsureLists();
lastRunDistroId = distroId;
lastRunLoadout ??= new LastRunLoadoutSaveEntry();
lastRunLoadout.distroId = distroId;
int safeWave = Math.Max(0, waveReached);
for (int i = 0; i < distroBestWaves.Count; i++)
{
DistroBestWaveSaveEntry entry = distroBestWaves[i];
if (entry != null && string.Equals(entry.distroId, distroId, StringComparison.OrdinalIgnoreCase))
{
entry.bestWave = Math.Max(entry.bestWave, safeWave);
return;
}
}
distroBestWaves.Add(new DistroBestWaveSaveEntry { distroId = distroId, bestWave = safeWave });
}
private void NormalizeDistroBestWaves()
{
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
for (int i = distroBestWaves.Count - 1; i >= 0; i--)
{
DistroBestWaveSaveEntry entry = distroBestWaves[i];
if (entry == null || string.IsNullOrWhiteSpace(entry.distroId) || !seen.Add(entry.distroId))
{
distroBestWaves.RemoveAt(i);
continue;
}
entry.bestWave = Math.Max(0, entry.bestWave);
}
}
private void MigrateLegacyOwnedPackageIds()
{
for (int i = 0; i < ownedPackageIds.Count; i++)
{
string id = ownedPackageIds[i];
if (string.IsNullOrWhiteSpace(id) || HasOwnedPackageEntry(id))
{
continue;
}
ownedPackages.Add(new OwnedPackageSaveEntry { id = id, upgradeLevel = 0 });
}
}
private bool HasOwnedPackageEntry(string id)
{
for (int i = 0; i < ownedPackages.Count; i++)
{
OwnedPackageSaveEntry entry = ownedPackages[i];
if (entry != null && string.Equals(entry.id, id, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private void NormalizePackageLoadouts()
{
HashSet<string> seenDistros = new(StringComparer.OrdinalIgnoreCase);
for (int i = packageLoadouts.Count - 1; i >= 0; i--)
{
PackageLoadoutSaveEntry entry = packageLoadouts[i];
if (entry == null || string.IsNullOrWhiteSpace(entry.distroId) || !seenDistros.Add(entry.distroId))
{
packageLoadouts.RemoveAt(i);
continue;
}
entry.EnsureLists();
}
}
private void MigrateLegacyGlobalMerges()
{
if (merges <= 0 || ownedUnits.Count == 0)
@@ -223,6 +400,13 @@ namespace KernelPanic.Meta
public int merges;
}
[Serializable]
public sealed class OwnedPackageSaveEntry
{
public string id;
public int upgradeLevel;
}
[Serializable]
public sealed class LastRunLoadoutSaveEntry
{
@@ -234,4 +418,39 @@ namespace KernelPanic.Meta
cardIds ??= new List<string>();
}
}
[Serializable]
public sealed class DistroBestWaveSaveEntry
{
public string distroId;
public int bestWave;
}
[Serializable]
public sealed class PackageLoadoutSaveEntry
{
public string distroId;
public List<PackageLoadoutSlotSaveEntry> slots = new();
public void EnsureLists()
{
slots ??= new List<PackageLoadoutSlotSaveEntry>();
HashSet<PackageSlot> seen = new();
for (int i = slots.Count - 1; i >= 0; i--)
{
PackageLoadoutSlotSaveEntry slot = slots[i];
if (slot == null || string.IsNullOrWhiteSpace(slot.packageId) || !seen.Add(slot.slot))
{
slots.RemoveAt(i);
}
}
}
}
[Serializable]
public sealed class PackageLoadoutSlotSaveEntry
{
public PackageSlot slot;
public string packageId;
}
}
+34 -2
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using KernelPanic.Core;
using KernelPanic.Data;
@@ -10,21 +11,32 @@ namespace KernelPanic.Run
public sealed class RunConfig
{
public RunConfig(DistroDefinition distro, Language primaryLanguage, Language secondaryLanguage, IReadOnlyList<CardDefinition> startingDeck)
: this(distro, primaryLanguage, secondaryLanguage, startingDeck, 0, 1)
: this(distro, primaryLanguage, secondaryLanguage, startingDeck, Array.Empty<PackageDefinition>(), 0, 1)
{
}
public RunConfig(DistroDefinition distro, Language primaryLanguage, Language secondaryLanguage, IReadOnlyList<CardDefinition> startingDeck, int runSeed)
: this(distro, primaryLanguage, secondaryLanguage, startingDeck, runSeed, 1)
: this(distro, primaryLanguage, secondaryLanguage, startingDeck, Array.Empty<PackageDefinition>(), runSeed, 1)
{
}
public RunConfig(DistroDefinition distro, Language primaryLanguage, Language secondaryLanguage, IReadOnlyList<CardDefinition> startingDeck, int runSeed, int distroVersion)
: this(distro, primaryLanguage, secondaryLanguage, startingDeck, Array.Empty<PackageDefinition>(), runSeed, distroVersion)
{
}
public RunConfig(DistroDefinition distro, Language primaryLanguage, Language secondaryLanguage, IReadOnlyList<CardDefinition> startingDeck, IReadOnlyList<PackageDefinition> equippedPackages, int runSeed, int distroVersion)
: this(distro, primaryLanguage, secondaryLanguage, startingDeck, ToInstances(equippedPackages), runSeed, distroVersion)
{
}
public RunConfig(DistroDefinition distro, Language primaryLanguage, Language secondaryLanguage, IReadOnlyList<CardDefinition> startingDeck, IReadOnlyList<PackageInstance> equippedPackages, int runSeed, int distroVersion)
{
Distro = distro;
PrimaryLanguage = primaryLanguage;
SecondaryLanguage = secondaryLanguage;
StartingDeck = startingDeck;
EquippedPackages = equippedPackages ?? Array.Empty<PackageInstance>();
RunSeed = runSeed;
DistroVersion = UnityEngine.Mathf.Clamp(distroVersion, 1, KernelPanic.Meta.GachaTuning.MaxVersion);
}
@@ -33,7 +45,27 @@ namespace KernelPanic.Run
public Language PrimaryLanguage { get; }
public Language SecondaryLanguage { get; }
public IReadOnlyList<CardDefinition> StartingDeck { get; }
public IReadOnlyList<PackageInstance> EquippedPackages { get; }
public int RunSeed { get; }
public int DistroVersion { get; }
private static IReadOnlyList<PackageInstance> ToInstances(IReadOnlyList<PackageDefinition> packages)
{
if (packages == null)
{
return Array.Empty<PackageInstance>();
}
List<PackageInstance> instances = new();
for (int i = 0; i < packages.Count; i++)
{
if (packages[i] != null)
{
instances.Add(new PackageInstance(packages[i], 0));
}
}
return instances;
}
}
}
+47 -4
View File
@@ -18,12 +18,22 @@ namespace KernelPanic.Run
public static void Set(DistroDefinition distro, IReadOnlyList<CardDefinition> equippedCards, Language primaryLanguage, Language secondaryLanguage)
{
Set(distro, equippedCards, primaryLanguage, secondaryLanguage, 1);
Set(distro, equippedCards, Array.Empty<PackageDefinition>(), primaryLanguage, secondaryLanguage, 1);
}
public static void Set(DistroDefinition distro, IReadOnlyList<CardDefinition> equippedCards, Language primaryLanguage, Language secondaryLanguage, int distroVersion)
{
pendingRun = new PendingRun(distro, equippedCards, primaryLanguage, secondaryLanguage, Environment.TickCount, distroVersion);
Set(distro, equippedCards, Array.Empty<PackageDefinition>(), primaryLanguage, secondaryLanguage, distroVersion);
}
public static void Set(DistroDefinition distro, IReadOnlyList<CardDefinition> equippedCards, IReadOnlyList<PackageDefinition> equippedPackages, Language primaryLanguage, Language secondaryLanguage, int distroVersion)
{
pendingRun = new PendingRun(distro, equippedCards, ToInstances(equippedPackages), primaryLanguage, secondaryLanguage, Environment.TickCount, distroVersion);
}
public static void Set(DistroDefinition distro, IReadOnlyList<CardDefinition> equippedCards, IReadOnlyList<PackageInstance> equippedPackages, Language primaryLanguage, Language secondaryLanguage, int distroVersion)
{
pendingRun = new PendingRun(distro, equippedCards, equippedPackages, primaryLanguage, secondaryLanguage, Environment.TickCount, distroVersion);
}
public static bool TryCreateRunConfig(LanguageDeckDatabase languageDeckDatabase, out RunConfig config)
@@ -43,12 +53,13 @@ namespace KernelPanic.Run
{
private readonly DistroDefinition distro;
private readonly List<CardDefinition> equippedCards = new();
private readonly List<PackageInstance> equippedPackages = new();
private readonly Language primaryLanguage;
private readonly Language secondaryLanguage;
private readonly int runSeed;
private readonly int distroVersion;
public PendingRun(DistroDefinition distro, IReadOnlyList<CardDefinition> cards, Language primaryLanguage, Language secondaryLanguage, int runSeed, int distroVersion)
public PendingRun(DistroDefinition distro, IReadOnlyList<CardDefinition> cards, IReadOnlyList<PackageInstance> packages, Language primaryLanguage, Language secondaryLanguage, int runSeed, int distroVersion)
{
this.distro = distro;
this.primaryLanguage = primaryLanguage;
@@ -68,6 +79,19 @@ namespace KernelPanic.Run
equippedCards.Add(cards[i]);
}
}
if (packages == null)
{
return;
}
for (int i = 0; i < packages.Count; i++)
{
if (packages[i]?.Definition != null)
{
equippedPackages.Add(packages[i]);
}
}
}
public DistroDefinition Distro => distro;
@@ -77,7 +101,7 @@ namespace KernelPanic.Run
List<CardDefinition> startingDeck = new(equippedCards);
AddLanguageDeck(languageDeckDatabase, primaryLanguage, startingDeck);
AddLanguageDeck(languageDeckDatabase, secondaryLanguage, startingDeck);
return new RunConfig(distro, primaryLanguage, secondaryLanguage, startingDeck, runSeed, distroVersion);
return new RunConfig(distro, primaryLanguage, secondaryLanguage, startingDeck, equippedPackages, runSeed, distroVersion);
}
private static void AddLanguageDeck(LanguageDeckDatabase languageDeckDatabase, Language language, List<CardDefinition> target)
@@ -101,5 +125,24 @@ namespace KernelPanic.Run
}
}
}
private static IReadOnlyList<PackageInstance> ToInstances(IReadOnlyList<PackageDefinition> packages)
{
if (packages == null)
{
return Array.Empty<PackageInstance>();
}
List<PackageInstance> instances = new();
for (int i = 0; i < packages.Count; i++)
{
if (packages[i] != null)
{
instances.Add(new PackageInstance(packages[i], 0));
}
}
return instances;
}
}
}
+41 -3
View File
@@ -26,6 +26,9 @@ namespace KernelPanic.Run
private int maxUptimeBonus;
private int maxCyclesBonus;
private int ramBonus;
private int packageMaxUptimeBonus;
private int packageMaxCyclesBonus;
private int packageRamBonus;
private int wavesCleared;
private int accruedBandwidth;
private int accruedEntropy;
@@ -74,10 +77,14 @@ namespace KernelPanic.Run
maxUptimeBonus = 0;
maxCyclesBonus = 0;
ramBonus = 0;
packageMaxUptimeBonus = 0;
packageMaxCyclesBonus = 0;
packageRamBonus = 0;
wavesCleared = 0;
accruedBandwidth = 0;
accruedEntropy = 0;
rewardsSettled = false;
ApplyKernelPackageBonuses(config);
InitializeRunDeck(config);
StartCombat();
}
@@ -171,17 +178,48 @@ namespace KernelPanic.Run
public int EffectiveMaxUptime()
{
return Mathf.Max(1, (CurrentConfig?.Distro?.BaseUptime ?? 1) + maxUptimeBonus);
return Mathf.Max(1, (CurrentConfig?.Distro?.BaseUptime ?? 1) + maxUptimeBonus + packageMaxUptimeBonus);
}
public int EffectiveRam()
{
return Mathf.Max(1, (CurrentConfig?.Distro?.BaseRam ?? 1) + ramBonus);
return Mathf.Max(1, (CurrentConfig?.Distro?.BaseRam ?? 1) + ramBonus + packageRamBonus);
}
public int EffectiveMaxCycles()
{
return Mathf.Max(1, (CurrentConfig?.Distro?.BaseCyclesPerTurn ?? 1) + maxCyclesBonus);
return Mathf.Max(1, (CurrentConfig?.Distro?.BaseCyclesPerTurn ?? 1) + maxCyclesBonus + packageMaxCyclesBonus);
}
private void ApplyKernelPackageBonuses(RunConfig config)
{
if (config?.EquippedPackages == null)
{
return;
}
for (int i = 0; i < config.EquippedPackages.Count; i++)
{
PackageInstance package = config.EquippedPackages[i];
if (package?.Definition == null || package.Definition.Slot != PackageSlot.Kernel)
{
continue;
}
PackageEffectData effect = package.EffectFor(config.Distro?.Id);
switch (effect.Kind)
{
case PackageEffectKind.MaxUptime:
packageMaxUptimeBonus += Mathf.Max(0, effect.Amount);
break;
case PackageEffectKind.MaxCycles:
packageMaxCyclesBonus += Mathf.Max(0, effect.Amount);
break;
case PackageEffectKind.MaxRam:
packageRamBonus += Mathf.Max(0, effect.Amount);
break;
}
}
}
public bool TrySettleRunRewards(out int bandwidth, out int entropy)
File diff suppressed because it is too large Load Diff
+8 -7
View File
@@ -329,10 +329,10 @@ namespace KernelPanic.UI
waveLabel = AddStatusReadout(bar, "wave", "--", false);
phaseLabel = AddStatusReadout(bar, "phase", "--", true);
bitsLabel = AddStatusReadout(bar, "bits", "0", false);
entropyLabel = AddStatusReadout(bar, "entropy", "0", false);
bandwidthLabel = AddStatusReadout(bar, "bandwidth", "0", false);
rootCreditsLabel = AddStatusReadout(bar, "root credits", "TODO", false);
bitsLabel = AddStatusReadout(bar, "Bits", "0", false);
entropyLabel = AddStatusReadout(bar, "Entropy", "0", false);
bandwidthLabel = AddStatusReadout(bar, "Bandwidth", "0", false);
rootCreditsLabel = AddStatusReadout(bar, "Root Credits", "0", false);
VisualElement spacer = new();
spacer.AddToClassList("status-spacer");
@@ -405,8 +405,8 @@ namespace KernelPanic.UI
phaseLabel.text = PhaseText(combatManager.CurrentPhase);
bitsLabel.text = runManager.Bits.ToString();
entropyLabel.text = FormatWalletWithAccrual(data.entropyBalance, runManager.AccruedEntropy);
bandwidthLabel.text = FormatWalletWithAccrual(data.standardPullCurrency, runManager.AccruedBandwidth);
rootCreditsLabel.text = "TODO";
bandwidthLabel.text = FormatWalletWithAccrual(data.bandwidthBalance, runManager.AccruedBandwidth);
rootCreditsLabel.text = data.rootCredits.ToString();
seedLabel.text = $"seed {config?.RunSeed ?? 0}";
}
@@ -575,8 +575,9 @@ namespace KernelPanic.UI
}
SaveData data = saveService.Load();
data.standardPullCurrency = Math.Max(0, data.standardPullCurrency) + bandwidth;
data.bandwidthBalance = Math.Max(0, data.bandwidthBalance) + bandwidth;
data.entropyBalance = Math.Max(0, data.entropyBalance) + entropy;
data.RecordRunStats(runManager.CurrentConfig?.Distro?.Id, runManager.CurrentWaveNumber);
saveService.Save(data);
}
+19 -2
View File
@@ -30,11 +30,16 @@ namespace KernelPanic.UI
private FontAsset _monospaceFont;
private AsciiArtFitter _asciiFitter;
private IReadOnlyList<DistroDefinition> _units = Array.Empty<DistroDefinition>();
private Func<DistroDefinition, int> _getBestWave;
private Action<DistroDefinition> _activateUnit;
private DistroDefinition _currentUnit;
private int _selectedIndex;
public void Bind(VisualElement root, FontAsset artFont)
public void Bind(VisualElement root, FontAsset artFont, Func<DistroDefinition, int> getBestWave = null, Action<DistroDefinition> activateUnit = null)
{
_monospaceFont = artFont;
_getBestWave = getBestWave;
_activateUnit = activateUnit;
_panel = root.Q<VisualElement>("FeaturedUnitPanel");
_titleLabel = root.Q<Label>("FeaturedUnitTitle");
_asciiLabel = root.Q<Label>("FeaturedUnitAscii");
@@ -52,6 +57,7 @@ namespace KernelPanic.UI
_asciiPlaceholder = DistroArtPresenter.CreatePlaceholder();
_asciiPlaceholder.AddToClassList("hidden");
_populatedState.Insert(0, _asciiPlaceholder);
_panel?.RegisterCallback<ClickEvent>(_ => ActivateCurrentUnit());
}
public void Refresh(IReadOnlyList<DistroDefinition> ownedUnits)
@@ -114,6 +120,7 @@ namespace KernelPanic.UI
{
_populatedState.AddToClassList("hidden");
_emptyState.RemoveFromClassList("hidden");
_currentUnit = null;
_titleLabel.text = "[ neofetch ]";
_emptyTitleLabel.text = "no units installed";
@@ -125,16 +132,26 @@ namespace KernelPanic.UI
{
_populatedState.RemoveFromClassList("hidden");
_emptyState.AddToClassList("hidden");
_currentUnit = unit;
_titleLabel.text = "[ neofetch ]";
_asciiFitter.SetArt(DistroArtPresenter.Render(_asciiLabel, _asciiPlaceholder, unit));
_unitNameLabel.text = DistroPresentation.DisplayName(unit);
_languagesLabel.text = DistroPresentation.FormatLanguages(unit);
_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.
int bestWave = Math.Max(0, _getBestWave?.Invoke(unit) ?? 0);
_bestWaveLabel.text = bestWave > 0 ? bestWave.ToString() : "--";
SetAccent(ColorUtility.ToHtmlStringRGB(unit.AccentColor));
}
public void ActivateCurrentUnit()
{
if (_currentUnit != null)
{
_activateUnit?.Invoke(_currentUnit);
}
}
private void SetAccent(string htmlColor)
{
string color = $"#{htmlColor}";
+23 -24
View File
@@ -161,13 +161,14 @@ namespace KernelPanic.UI
title.AddToClassList("gacha-detail-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("Commits", _gachaService.PullTokens.ToString()));
_bannerDetail.Add(BuildInfoLine("pulls", $"{state.totalPulls}/{GachaService.BeginnerMaxPulls}"));
_bannerDetail.Add(BuildInfoLine("4-star pity", $"{state.pityCounter}/{GachaService.FourStarHardPity}"));
_bannerDetail.Add(BuildInfoLine("ten pull", $"{GachaService.BeginnerTenPullCost} Commits for 10 pulls"));
_bannerDetail.Add(BuildInfoLine("cost", $"1 Commit / {GachaService.EntropyPerCommit} Entropy per pull"));
_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 package cache. A 4-star package cache or starter distro can appear early, with one 4-star+ result guaranteed every 10 pulls.");
rules.AddToClassList("gacha-rules");
_bannerDetail.Add(rules);
@@ -185,7 +186,7 @@ namespace KernelPanic.UI
Button tenButton = new(() => RequestPullSelectedBanner(10))
{
text = $"git pull --ten\n8x {GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)}"
text = $"git pull --ten\n10x {GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)}"
};
tenButton.AddToClassList("terminal-button");
tenButton.AddToClassList("gacha-action-button");
@@ -214,10 +215,7 @@ namespace KernelPanic.UI
title.AddToClassList("gacha-detail-title");
_bannerDetail.Add(title);
string currency = bannerId == GachaService.StandardBannerId
? GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)
: GachaService.FormatCurrencyName(GachaCurrencyType.LimitedPull);
_bannerDetail.Add(BuildInfoLine("pull currency", currency));
_bannerDetail.Add(BuildInfoLine("pull currency", $"Commits or Entropy ({GachaService.EntropyPerCommit}:1)"));
_bannerDetail.Add(BuildInfoLine("status", "not implemented yet"));
_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" });
@@ -250,7 +248,8 @@ namespace KernelPanic.UI
return;
}
GachaPullResult result = _gachaService.PerformBeginnerPull(pullCount, _wallet, entropyTokenCount);
PullPaymentSource source = entropyTokenCount > 0 ? PullPaymentSource.Entropy : PullPaymentSource.Commits;
GachaPullResult result = _gachaService.PerformBeginnerPull(pullCount, _wallet, source);
if (!result.Success)
{
_resultBannerId = bannerId;
@@ -262,11 +261,9 @@ namespace KernelPanic.UI
}
List<string> header = new();
header.Add($"git pull complete: spent {result.CurrencySpent} {GachaService.FormatCurrencyName(result.CurrencyType)}");
if (result.EntropySpent > 0)
{
header.Add($"entropy fallback: spent {result.EntropySpent} entropy");
}
header.Add(result.EntropySpent > 0
? $"git pull complete: spent {result.EntropySpent} Entropy"
: $"git pull complete: spent {result.CurrencySpent} {GachaService.FormatCurrencyName(result.CurrencyType)}");
List<DistroDefinition> distroRewards = new();
for (int i = 0; i < result.Rewards.Count; i++)
@@ -419,7 +416,7 @@ namespace KernelPanic.UI
private static string FormatRewardDisplayName(GachaReward reward)
{
if (reward.RewardType != GachaRewardType.Equipment || string.IsNullOrWhiteSpace(reward.DisplayName))
if (reward.RewardType != GachaRewardType.Package || string.IsNullOrWhiteSpace(reward.DisplayName))
{
return reward.DisplayName;
}
@@ -450,7 +447,8 @@ namespace KernelPanic.UI
return;
}
if (!_gachaService.CanCoverMissingPullTokensWithEntropy(_wallet, missingTokens))
int entropyCost = cost * GachaService.EntropyPerCommit;
if (_wallet == null || _wallet.Balance < entropyCost)
{
if (_gachaService.RootCredits > 0)
{
@@ -459,13 +457,13 @@ namespace KernelPanic.UI
}
_resultBannerId = bannerId;
_resultText = $"git pull failed: need {missingTokens} {GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)} or {missingTokens * GachaService.EntropyPerPullToken} entropy";
_resultText = $"git pull failed: need {cost} {GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)} or {entropyCost} Entropy";
RenderSelectedBanner();
return;
}
_pendingPullCount = pullCount;
_pendingMissingTokens = missingTokens;
_pendingMissingTokens = cost;
_resultBannerId = bannerId;
_resultText = null;
RenderSelectedBanner();
@@ -476,8 +474,8 @@ namespace KernelPanic.UI
VisualElement prompt = new();
prompt.AddToClassList("gacha-entropy-prompt");
int entropyCost = _pendingMissingTokens * GachaService.EntropyPerPullToken;
Label copy = new($"missing {_pendingMissingTokens} Commits. spend {entropyCost} entropy to complete git pull?");
int entropyCost = _pendingMissingTokens * GachaService.EntropyPerCommit;
Label copy = new($"spend {entropyCost} Entropy instead of {_pendingMissingTokens} Commits?");
copy.AddToClassList("gacha-result");
prompt.Add(copy);
@@ -499,9 +497,9 @@ namespace KernelPanic.UI
Button confirm = new(() =>
{
int pullCount = _pendingPullCount;
int missingTokens = _pendingMissingTokens;
int entropyPulls = _pendingMissingTokens;
ClearEntropyPrompt();
PullSelectedBanner(pullCount, missingTokens);
PullSelectedBanner(pullCount, entropyPulls);
})
{
text = $"git pull --use-entropy={entropyCost}"
@@ -526,9 +524,10 @@ namespace KernelPanic.UI
return false;
}
int missingTokens = _gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, _gachaService.GetBeginnerPullCost(pullCount));
int cost = _gachaService.GetBeginnerPullCost(pullCount);
int missingTokens = _gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, cost);
return missingTokens > 0 &&
(_gachaService.CanCoverMissingPullTokensWithEntropy(_wallet, missingTokens) ||
((_wallet != null && _wallet.Balance >= cost * GachaService.EntropyPerCommit) ||
_gachaService.RootCredits > 0);
}
+78 -11
View File
@@ -173,8 +173,8 @@ namespace KernelPanic.UI
commandLabel.text = pending.PullCount == 10 ? "git pull --ten" : "git pull --single";
int pullCost = pending.PullCount == 10 ? GachaService.BeginnerTenPullCost : GachaService.BeginnerSinglePullCost;
costLabel.text = pending.EntropyTokenCount > 0
? $"cost: {pullCost} Commits / entropy fallback: {pending.EntropyTokenCount * GachaService.EntropyPerPullToken}"
: $"cost: {pullCost} Commits";
? $"cost: {pending.EntropyTokenCount * GachaService.EntropyPerCommit} Entropy"
: $"cost: {pullCost} Commits / {pullCost * GachaService.EntropyPerCommit} Entropy";
flourishLabel.text = "$ fetch-pack | verify | unpack";
CompletedGachaPull completed = ResolvePull(pending);
@@ -248,13 +248,13 @@ namespace KernelPanic.UI
EntropyWallet wallet = new();
wallet.SetBalance(saveData.entropyBalance);
PlayerCollection collection = BuildCollection(saveData, pending.DistroDatabase);
PlayerCollection collection = BuildCollection(saveData, pending.DistroDatabase, pending.PackageDatabase);
GachaService gacha = new();
RestoreBannerPool(gacha, saveData, pending.DistroDatabase);
gacha.LoadProgress(saveData);
GachaPullResult result = pending.BannerId == GachaService.BeginnerBannerId
? gacha.PerformBeginnerPull(pending.PullCount, wallet, pending.EntropyTokenCount)
? gacha.PerformBeginnerPull(pending.PullCount, wallet, pending.EntropyTokenCount > 0 ? PullPaymentSource.Entropy : PullPaymentSource.Commits)
: GachaPullResult.Failed(pending.BannerId, "remote not implemented yet");
if (!result.Success)
@@ -264,12 +264,10 @@ namespace KernelPanic.UI
List<string> header = new()
{
$"spent {result.CurrencySpent} {GachaService.FormatCurrencyName(result.CurrencyType)}"
result.EntropySpent > 0
? $"spent {result.EntropySpent} Entropy"
: $"spent {result.CurrencySpent} {GachaService.FormatCurrencyName(result.CurrencyType)}"
};
if (result.EntropySpent > 0)
{
header.Add($"spent {result.EntropySpent} entropy");
}
List<DistroDefinition> distroRewards = new();
for (int i = 0; i < result.Rewards.Count; i++)
@@ -296,7 +294,27 @@ namespace KernelPanic.UI
PullOutcomeKind outcomeKind = outcome?.Kind ?? PullOutcomeKind.Invalid;
int merges = outcome?.MergesAwarded ?? 0;
IReadOnlyList<Language> languages = outcome?.LanguagesNewlyUnlocked ?? Array.Empty<Language>();
completedRewards.Add(new CompletedGachaReward(i + 1, reward.StarRating, reward.DisplayName, reward.RewardType, outcomeKind, merges, languages, reward.Guaranteed, reward.PityTriggered));
string displayName = reward.DisplayName;
if (reward.RewardType == GachaRewardType.Package)
{
PackageDefinition package = GrantRandomPackageByRarity(pending.PackageDatabase, collection, saveData, reward.StarRating, out bool duplicatePackage);
if (package != null)
{
displayName = string.IsNullOrWhiteSpace(package.DisplayName) ? package.Id : package.DisplayName;
if (duplicatePackage)
{
outcomeKind = PullOutcomeKind.Dupe;
merges = Mathf.Max(1, GachaTuning.MergesPerDupe);
saveData.packageMerges += merges;
}
else
{
outcomeKind = PullOutcomeKind.Granted;
}
}
}
completedRewards.Add(new CompletedGachaReward(i + 1, reward.StarRating, displayName, reward.RewardType, outcomeKind, merges, languages, reward.Guaranteed, reward.PityTriggered));
}
WriteState(saveService, saveData, wallet, collection, gacha);
@@ -424,7 +442,7 @@ namespace KernelPanic.UI
return string.Join(", ", names);
}
private static PlayerCollection BuildCollection(SaveData saveData, DistroDatabase distroDatabase)
private static PlayerCollection BuildCollection(SaveData saveData, DistroDatabase distroDatabase, PackageDatabase packageDatabase)
{
PlayerCollection collection = new();
for (int i = 0; i < saveData.ownedUnits.Count; i++)
@@ -437,9 +455,46 @@ namespace KernelPanic.UI
}
}
for (int i = 0; i < saveData.ownedPackages.Count; i++)
{
OwnedPackageSaveEntry entry = saveData.ownedPackages[i];
PackageDefinition package = packageDatabase == null ? null : packageDatabase.FindById(entry?.id);
if (package != null)
{
collection.AddPackageSilently(package, entry.upgradeLevel);
}
}
return collection;
}
private static PackageDefinition GrantRandomPackageByRarity(PackageDatabase packageDatabase, PlayerCollection collection, SaveData saveData, int rarity, out bool duplicate)
{
duplicate = false;
int count = packageDatabase == null ? 0 : packageDatabase.CountByRarity(rarity);
if (count <= 0)
{
return null;
}
int index = UnityEngine.Random.Range(0, count);
PackageDefinition package = packageDatabase.FindByRarity(rarity, index);
if (package == null)
{
return null;
}
duplicate = collection.IsPackageOwned(package.Id) || saveData.ownedPackageIds.Exists(id => string.Equals(id, package.Id, StringComparison.OrdinalIgnoreCase));
if (!duplicate)
{
collection.AddPackageSilently(package);
saveData.ownedPackages.Add(new OwnedPackageSaveEntry { id = package.Id, upgradeLevel = 0 });
saveData.ownedPackageIds.Add(package.Id);
}
return package;
}
private static void RestoreBannerPool(GachaService gacha, SaveData saveData, DistroDatabase distroDatabase)
{
for (int i = 0; i < saveData.bannerPoolIds.Count; i++)
@@ -475,6 +530,8 @@ namespace KernelPanic.UI
saveData.entropyBalance = wallet.Balance;
saveData.ownedUnits.Clear();
saveData.ownedPackages.Clear();
saveData.ownedPackageIds.Clear();
saveData.ownedUnitIds.Clear();
saveData.bannerPoolIds.Clear();
gacha.WriteProgress(saveData);
@@ -493,6 +550,16 @@ namespace KernelPanic.UI
}
}
for (int i = 0; i < collection.OwnedPackageInstances.Count; i++)
{
OwnedPackageInstance package = collection.OwnedPackageInstances[i];
if (package != null && !string.IsNullOrWhiteSpace(package.PackageId))
{
saveData.ownedPackages.Add(new OwnedPackageSaveEntry { id = package.PackageId, upgradeLevel = package.UpgradeLevel });
saveData.ownedPackageIds.Add(package.PackageId);
}
}
for (int i = 0; i < gacha.BannerPool.Count; i++)
{
DistroDefinition unit = gacha.BannerPool[i];
+192 -14
View File
@@ -89,6 +89,7 @@ namespace KernelPanic.UI
[SerializeField] private string motdBody = "unstable userspace detected; keep a rollback shell open.";
[SerializeField] private DistroDatabase distroDatabase;
[SerializeField] private CardDatabase cardDatabase;
[SerializeField] private PackageDatabase packageDatabase;
[SerializeField] private LanguageDeckDatabase languageDeckDatabase;
[SerializeField] private FeaturedUnitPanel featuredUnitPanel = new();
[SerializeField] private CollectionScreenController collectionScreen = new();
@@ -149,6 +150,7 @@ namespace KernelPanic.UI
private GachaService _gachaService;
private PlayerCollection _playerCollection;
private CardLoadout _cardLoadout;
private PackageLoadout _packageLoadout;
private BackgroundLogRingBuffer _backgroundLog;
private IEventBannerSource _eventBannerSource;
private int _selectedCommandIndex;
@@ -189,8 +191,8 @@ namespace KernelPanic.UI
BindScreenFrames();
starterSelection.Bind(_root, distroDatabase, HandleStarterConfirmed);
FontAsset resolvedFont = ResolveMonospaceFont();
featuredUnitPanel.Bind(_root, resolvedFont);
collectionScreen.Bind(_root, resolvedFont, languageDeckDatabase, cardDatabase, _playerCollection, GetMergesBalance, UpgradeCollectionUnit);
featuredUnitPanel.Bind(_root, resolvedFont, GetBestWaveForUnit, OpenFeaturedUnitDetail);
collectionScreen.Bind(_root, resolvedFont, languageDeckDatabase, cardDatabase, _playerCollection, GetMergesBalance, UpgradeCollectionUnit, packageDatabase, _packageLoadout, SaveCurrentState, () => _saveData, UpgradeCollectionPackage, ScrapCollectionPackage);
gachaScreen.Bind(_root, distroDatabase, resolvedFont, _gachaService, _playerCollection, _wallet, ResolvePulledDistros, HandleMetaStateChanged, OpenRootCreditExchange, StartGachaPullCutscene);
LoadMetaState();
_playerCollection.Changed += HandleMetaStateChanged;
@@ -205,6 +207,7 @@ namespace KernelPanic.UI
_gachaService = new GachaService();
_playerCollection = new PlayerCollection();
_cardLoadout = new CardLoadout(_playerCollection.OwnedUnits);
_packageLoadout = new PackageLoadout(_playerCollection, packageDatabase);
}
private void OnEnable()
@@ -216,7 +219,7 @@ namespace KernelPanic.UI
RefreshStaticText();
RefreshCurrencyReadouts();
RefreshEventBanner();
featuredUnitPanel.Refresh(_playerCollection.OwnedUnits, _playerCollection.FeaturedUnit);
RefreshFeaturedUnitPanel();
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
gachaScreen.Refresh();
SelectCommand(0);
@@ -603,7 +606,7 @@ namespace KernelPanic.UI
if (_rootCreditExchangePreviewLabel != null)
{
_rootCreditExchangePreviewLabel.text = $"+{amount} entropy";
_rootCreditExchangePreviewLabel.text = $"+{amount * GachaService.RootCreditsToEntropy} Entropy";
}
int max = _gachaService.RootCredits;
@@ -665,6 +668,18 @@ namespace KernelPanic.UI
}
}
for (int i = 0; i < _saveData.ownedPackages.Count; i++)
{
OwnedPackageSaveEntry entry = _saveData.ownedPackages[i];
PackageDefinition package = packageDatabase == null ? null : packageDatabase.FindById(entry?.id);
if (package != null)
{
_playerCollection.AddPackageSilently(package, entry.upgradeLevel);
}
}
LoadPackageLoadoutsFromSave();
for (int i = 0; i < _saveData.bannerPoolIds.Count; i++)
{
DistroDefinition unit = ResolveSavedDistro(_saveData.bannerPoolIds[i]);
@@ -718,12 +733,51 @@ namespace KernelPanic.UI
private void HandleMetaStateChanged()
{
SaveCurrentState();
featuredUnitPanel.Refresh(_playerCollection.OwnedUnits, _playerCollection.FeaturedUnit);
RefreshFeaturedUnitPanel();
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
gachaScreen.Refresh();
RefreshCurrencyReadouts();
}
private void RefreshFeaturedUnitPanel()
{
featuredUnitPanel.Refresh(_playerCollection.OwnedUnits, ResolveFeaturedUnitForNeofetch());
}
private DistroDefinition ResolveFeaturedUnitForNeofetch()
{
DistroDefinition lastRun = string.IsNullOrWhiteSpace(_saveData?.lastRunDistroId) ? null : ResolveSavedDistro(_saveData.lastRunDistroId);
if (lastRun != null && _playerCollection.IsOwned(lastRun.Id))
{
return lastRun;
}
return _playerCollection.FeaturedUnit;
}
private int GetBestWaveForUnit(DistroDefinition unit)
{
_saveData ??= SaveData.CreateDefault();
_saveData.EnsureLists();
return unit == null ? 0 : _saveData.GetBestWave(unit.Id);
}
private void OpenFeaturedUnitDetail(DistroDefinition unit)
{
if (unit == null)
{
return;
}
_collectionShowingLanguages = false;
_collectionUnitsButton.EnableInClassList(SelectedClassName, true);
_collectionLanguagesButton.EnableInClassList(SelectedClassName, false);
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
collectionScreen.ShowUnitDetail(unit, false);
SyncCollectionFrame();
ShowScreen(MenuScreen.Collection);
}
private void HandleStarterConfirmed(DistroDefinition picked, IReadOnlyList<DistroDefinition> remaining)
{
_playerCollection.Add(picked, 1);
@@ -731,7 +785,7 @@ namespace KernelPanic.UI
_gachaService.SetBeginnerGuaranteedDistros(remaining);
_saveData.starterChosen = true;
SaveCurrentState();
featuredUnitPanel.Refresh(_playerCollection.OwnedUnits, _playerCollection.FeaturedUnit);
RefreshFeaturedUnitPanel();
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
gachaScreen.Refresh();
}
@@ -752,6 +806,9 @@ namespace KernelPanic.UI
_saveData.entropyBalance = _wallet == null ? 0 : _wallet.Balance;
_saveData.ownedUnits.Clear();
_saveData.ownedPackages.Clear();
_saveData.ownedPackageIds.Clear();
_saveData.packageLoadouts.Clear();
_saveData.ownedUnitIds.Clear();
_saveData.bannerPoolIds.Clear();
_gachaService.WriteProgress(_saveData);
@@ -770,6 +827,22 @@ namespace KernelPanic.UI
}
}
for (int i = 0; i < _playerCollection.OwnedPackageInstances.Count; i++)
{
OwnedPackageInstance package = _playerCollection.OwnedPackageInstances[i];
if (package != null && !string.IsNullOrWhiteSpace(package.PackageId))
{
_saveData.ownedPackages.Add(new OwnedPackageSaveEntry
{
id = package.PackageId,
upgradeLevel = package.UpgradeLevel
});
_saveData.ownedPackageIds.Add(package.PackageId);
}
}
SavePackageLoadouts();
for (int i = 0; i < _gachaService.BannerPool.Count; i++)
{
DistroDefinition unit = _gachaService.BannerPool[i];
@@ -791,7 +864,7 @@ namespace KernelPanic.UI
private void StartGachaPullCutscene(string bannerId, int pullCount, int entropyTokenCount)
{
SaveCurrentState();
GachaPullContext.SetPending(bannerId, pullCount, entropyTokenCount, distroDatabase, _playerCollection.FeaturedUnit?.Id);
GachaPullContext.SetPending(bannerId, pullCount, entropyTokenCount, distroDatabase, packageDatabase, _playerCollection.FeaturedUnit?.Id);
SceneLoader.LoadGambling(_root);
}
@@ -814,7 +887,33 @@ namespace KernelPanic.UI
if (result.Success)
{
SaveCurrentState();
featuredUnitPanel.Refresh(_playerCollection.OwnedUnits, _playerCollection.FeaturedUnit);
RefreshFeaturedUnitPanel();
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
RefreshCurrencyReadouts();
}
return result;
}
private PackageUpgradeResult UpgradeCollectionPackage(OwnedPackageInstance package)
{
PackageUpgradeResult result = PackageUpgrader.TryUpgrade(package, _saveData);
if (result.Success)
{
SaveCurrentState();
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
RefreshCurrencyReadouts();
}
return result;
}
private PackageScrapResult ScrapCollectionPackage(OwnedPackageInstance package)
{
PackageScrapResult result = PackageScrapper.Scrap(_playerCollection, _packageLoadout, _saveData, package?.PackageId);
if (result.Success)
{
SaveCurrentState();
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
RefreshCurrencyReadouts();
}
@@ -837,6 +936,77 @@ namespace KernelPanic.UI
SaveCurrentState();
}
private void LoadPackageLoadoutsFromSave()
{
_packageLoadout?.Clear();
if (_saveData?.packageLoadouts == null)
{
return;
}
for (int i = 0; i < _saveData.packageLoadouts.Count; i++)
{
PackageLoadoutSaveEntry entry = _saveData.packageLoadouts[i];
if (entry == null)
{
continue;
}
entry.EnsureLists();
_packageLoadout?.Load(entry.distroId, entry.slots);
}
}
private void SavePackageLoadouts()
{
if (_saveData == null || _packageLoadout == null)
{
return;
}
for (int unitIndex = 0; unitIndex < _playerCollection.OwnedUnits.Count; unitIndex++)
{
DistroDefinition unit = _playerCollection.OwnedUnits[unitIndex];
if (unit == null || string.IsNullOrWhiteSpace(unit.Id))
{
continue;
}
IReadOnlyDictionary<PackageSlot, string> equipped = _packageLoadout.GetEquippedPackageIds(unit.Id);
if (equipped.Count == 0)
{
continue;
}
PackageLoadoutSaveEntry entry = new()
{
distroId = unit.Id
};
AddPackageSlotSave(entry, equipped, PackageSlot.Kernel);
AddPackageSlotSave(entry, equipped, PackageSlot.Runtime);
AddPackageSlotSave(entry, equipped, PackageSlot.Daemon);
if (entry.slots.Count > 0)
{
_saveData.packageLoadouts.Add(entry);
}
}
}
private static void AddPackageSlotSave(PackageLoadoutSaveEntry entry, IReadOnlyDictionary<PackageSlot, string> equipped, PackageSlot slot)
{
if (entry == null || equipped == null || !equipped.TryGetValue(slot, out string packageId) || string.IsNullOrWhiteSpace(packageId))
{
return;
}
entry.slots.Add(new PackageLoadoutSlotSaveEntry
{
slot = slot,
packageId = packageId
});
}
private void RefreshStaticText()
{
_appIdLabel.text = $"kernel-panic v{Application.version} - tty1";
@@ -862,11 +1032,11 @@ namespace KernelPanic.UI
return;
}
_entropyLabel.text = $"entropy={_wallet.Balance}";
_entropyLabel.text = $"Entropy={_wallet.Balance}";
_rootCreditsToEntropyButton?.SetEnabled(true);
bool showPullTokens = IsGachaVisible();
_pullTokensLabel.text = showPullTokens
? $"commits={_gachaService.PullTokens} root-credits={_gachaService.LimitedPullTokens} distro-merges={GetTotalDistroMerges()}"
? $"Commits={_gachaService.PullTokens} Root Credits={_gachaService.RootCredits} Bandwidth={_saveData.bandwidthBalance} Cache={_saveData.cacheBalance} Merges={GetTotalDistroMerges()}"
: string.Empty;
_pullTokensLabel.EnableInClassList(HiddenClassName, !showPullTokens);
}
@@ -1053,7 +1223,16 @@ namespace KernelPanic.UI
if (IsCollectionVisible())
{
if (!collectionScreen.IsCardSubview &&
if (collectionScreen.IsUnitDetail &&
(evt.keyCode == KeyCode.LeftArrow || evt.keyCode == KeyCode.RightArrow || evt.keyCode == KeyCode.Tab))
{
collectionScreen.SwitchUnitDetailTabRelative(evt.keyCode == KeyCode.LeftArrow ? -1 : 1);
SyncCollectionFrame();
evt.StopPropagation();
return;
}
if (!collectionScreen.IsPushedSubview &&
(evt.keyCode == KeyCode.LeftArrow || evt.keyCode == KeyCode.RightArrow || evt.keyCode == KeyCode.Tab))
{
ToggleCollectionTab();
@@ -1113,8 +1292,7 @@ namespace KernelPanic.UI
if (evt.keyCode == KeyCode.Tab)
{
_playerCollection.SelectNextFeatured();
featuredUnitPanel.Refresh(_playerCollection.OwnedUnits, _playerCollection.FeaturedUnit);
featuredUnitPanel.SelectNext();
evt.StopPropagation();
return;
}
@@ -1626,7 +1804,7 @@ namespace KernelPanic.UI
}
SaveLastRunLoadout(unit, equippedCardIds);
RunContext.Set(unit, BuildEquippedCardDefinitions(unit, equippedCardIds), _selectedRunLanguages[0], _selectedRunLanguages[1], _playerCollection.GetVersion(unit.Id));
RunContext.Set(unit, BuildEquippedCardDefinitions(unit, equippedCardIds), _packageLoadout.GetEquippedPackageInstances(unit.Id), _selectedRunLanguages[0], _selectedRunLanguages[1], _playerCollection.GetVersion(unit.Id));
SceneLoader.LoadGame(_root);
}