Implement many UX improvements, packages and their assets and a UI overhaul in collection
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,12 @@ namespace KernelPanic.Meta
|
||||
/// </summary>
|
||||
public enum GachaCurrencyType
|
||||
{
|
||||
StandardPull,
|
||||
LimitedPull
|
||||
StandardPull
|
||||
}
|
||||
|
||||
public enum PullPaymentSource
|
||||
{
|
||||
Commits,
|
||||
Entropy
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace KernelPanic.Meta
|
||||
/// </summary>
|
||||
public enum GachaRewardType
|
||||
{
|
||||
Equipment,
|
||||
Package,
|
||||
Distro,
|
||||
FutureStandardFiveStarDistro
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user