Implement many UX improvements, packages and their assets and a UI overhaul in collection
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user