Add Packages inventory screen and auto-scrap duplicate package pulls
Packages are unique-per-type; duplicate pulls now auto-scrap into Cache (mirroring the distro dupe->Merges path) instead of a dead-end merges pool, and the pull reveal shows "-> N cache" for the dupe card. Save loading collapses any pre-existing duplicate package entries to the highest-level instance and grants Cache for the extras, logging the migration. Also adds a dedicated Packages tab in the Collection screen (master list with slot filter/sort + affordability hints, detail panel with upgrade/scrap actions) so the whole inventory can be managed from one place, sharing state with the existing per-distro Packages sub-tab. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MQmck8EqqWxSq5uVZ4E7yN
This commit is contained in:
@@ -127,6 +127,9 @@ namespace KernelPanic.Meta
|
|||||||
public string DisplayName { get; }
|
public string DisplayName { get; }
|
||||||
public GachaRewardType RewardType { get; }
|
public GachaRewardType RewardType { get; }
|
||||||
public PullOutcomeKind OutcomeKind { get; }
|
public PullOutcomeKind OutcomeKind { get; }
|
||||||
|
|
||||||
|
// Holds distro dupe merges, or (when RewardType is Package) the Cache a duplicate package
|
||||||
|
// pull was auto-scrapped for. See StatusText for which unit applies.
|
||||||
public int MergesAwarded { get; }
|
public int MergesAwarded { get; }
|
||||||
public IReadOnlyList<Language> LanguagesUnlocked { get; }
|
public IReadOnlyList<Language> LanguagesUnlocked { get; }
|
||||||
public bool Guaranteed { get; }
|
public bool Guaranteed { get; }
|
||||||
@@ -139,11 +142,12 @@ namespace KernelPanic.Meta
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
string dupeUnit = RewardType == GachaRewardType.Package ? "cache" : "merges";
|
||||||
return OutcomeKind switch
|
return OutcomeKind switch
|
||||||
{
|
{
|
||||||
PullOutcomeKind.Granted => "NEW",
|
PullOutcomeKind.Granted => "NEW",
|
||||||
PullOutcomeKind.Dupe => $"→ {MergesAwarded} merges",
|
PullOutcomeKind.Dupe => $"→ {MergesAwarded} {dupeUnit}",
|
||||||
PullOutcomeKind.DupeOverflow => $"→ {MergesAwarded} merges",
|
PullOutcomeKind.DupeOverflow => $"→ {MergesAwarded} {dupeUnit}",
|
||||||
_ => Guaranteed ? "guaranteed" : PityTriggered ? "pity" : string.Empty
|
_ => Guaranteed ? "guaranteed" : PityTriggered ? "pity" : string.Empty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ namespace KernelPanic.Meta
|
|||||||
// Legacy pull-token field kept only so older saves deserialize cleanly.
|
// Legacy pull-token field kept only so older saves deserialize cleanly.
|
||||||
public int limitedPullCurrency;
|
public int limitedPullCurrency;
|
||||||
public int cacheBalance;
|
public int cacheBalance;
|
||||||
|
// Legacy per-pull package-dupe pool. Package dupes now auto-scrap directly to Cache.
|
||||||
public int packageMerges;
|
public int packageMerges;
|
||||||
// Legacy global balance kept only so older saves deserialize cleanly.
|
// Legacy global balance kept only so older saves deserialize cleanly.
|
||||||
// New duplicate-pull merges live on OwnedUnitSaveEntry.merges.
|
// New duplicate-pull merges live on OwnedUnitSaveEntry.merges.
|
||||||
@@ -84,6 +85,13 @@ namespace KernelPanic.Meta
|
|||||||
public string lastRunDistroId;
|
public string lastRunDistroId;
|
||||||
public List<DistroBestWaveSaveEntry> distroBestWaves = new();
|
public List<DistroBestWaveSaveEntry> distroBestWaves = new();
|
||||||
|
|
||||||
|
// Populated by NormalizeOwnedPackages when a save contains more than one instance of the
|
||||||
|
// same package id (packages are unique-per-type). Not serialized: the owner (bootstrap code
|
||||||
|
// with access to PackageDatabase) consumes this once per load to grant Cache for the extras,
|
||||||
|
// then clears it. See MainMenuController.LoadMetaState.
|
||||||
|
[NonSerialized]
|
||||||
|
public List<CollapsedPackageDuplicate> collapsedPackageDuplicates = new();
|
||||||
|
|
||||||
public static SaveData CreateDefault()
|
public static SaveData CreateDefault()
|
||||||
{
|
{
|
||||||
return new SaveData();
|
return new SaveData();
|
||||||
@@ -228,17 +236,36 @@ namespace KernelPanic.Meta
|
|||||||
|
|
||||||
private void NormalizeOwnedPackages()
|
private void NormalizeOwnedPackages()
|
||||||
{
|
{
|
||||||
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
|
collapsedPackageDuplicates.Clear();
|
||||||
for (int i = ownedPackages.Count - 1; i >= 0; i--)
|
Dictionary<string, OwnedPackageSaveEntry> highestById = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
Dictionary<string, int> extraCountById = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (int i = 0; i < ownedPackages.Count; i++)
|
||||||
{
|
{
|
||||||
OwnedPackageSaveEntry entry = ownedPackages[i];
|
OwnedPackageSaveEntry entry = ownedPackages[i];
|
||||||
if (entry == null || string.IsNullOrWhiteSpace(entry.id) || !seen.Add(entry.id))
|
if (entry == null || string.IsNullOrWhiteSpace(entry.id))
|
||||||
{
|
{
|
||||||
ownedPackages.RemoveAt(i);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.upgradeLevel = Math.Max(0, Math.Min(PackageTuning.MaxPackageLevel, entry.upgradeLevel));
|
entry.upgradeLevel = Math.Max(0, Math.Min(PackageTuning.MaxPackageLevel, entry.upgradeLevel));
|
||||||
|
if (highestById.TryGetValue(entry.id, out OwnedPackageSaveEntry existing))
|
||||||
|
{
|
||||||
|
extraCountById[entry.id] = extraCountById.TryGetValue(entry.id, out int count) ? count + 1 : 1;
|
||||||
|
if (entry.upgradeLevel > existing.upgradeLevel)
|
||||||
|
{
|
||||||
|
highestById[entry.id] = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
highestById[entry.id] = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ownedPackages = new List<OwnedPackageSaveEntry>(highestById.Values);
|
||||||
|
foreach (KeyValuePair<string, int> extra in extraCountById)
|
||||||
|
{
|
||||||
|
collapsedPackageDuplicates.Add(new CollapsedPackageDuplicate(extra.Key, extra.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
ownedPackageIds.Clear();
|
ownedPackageIds.Clear();
|
||||||
@@ -407,6 +434,18 @@ namespace KernelPanic.Meta
|
|||||||
public int upgradeLevel;
|
public int upgradeLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class CollapsedPackageDuplicate
|
||||||
|
{
|
||||||
|
public CollapsedPackageDuplicate(string id, int extraCount)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
ExtraCount = extraCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id { get; }
|
||||||
|
public int ExtraCount { get; }
|
||||||
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public sealed class LastRunLoadoutSaveEntry
|
public sealed class LastRunLoadoutSaveEntry
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ namespace KernelPanic.UI
|
|||||||
private int _selectedCardIndex;
|
private int _selectedCardIndex;
|
||||||
private string _emptyCardMessage = "no cards installed";
|
private string _emptyCardMessage = "no cards installed";
|
||||||
private string _pendingScrapPackageId;
|
private string _pendingScrapPackageId;
|
||||||
|
private IReadOnlyList<OwnedPackageInstance> _packageInventory = Array.Empty<OwnedPackageInstance>();
|
||||||
|
private int _selectedPackageInventoryIndex;
|
||||||
|
private PackageSlot? _packageInventoryFilter;
|
||||||
|
|
||||||
public event Action ViewChanged;
|
public event Action ViewChanged;
|
||||||
|
|
||||||
@@ -98,6 +101,16 @@ namespace KernelPanic.UI
|
|||||||
ViewChanged?.Invoke();
|
ViewChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ShowPackages()
|
||||||
|
{
|
||||||
|
_mode = CollectionMode.Packages;
|
||||||
|
_pendingScrapPackageId = null;
|
||||||
|
CurrentTitle = "$ ls ~/packages";
|
||||||
|
CurrentHint = "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [click] upgrade/scrap";
|
||||||
|
RenderPackages();
|
||||||
|
ViewChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
public bool BackFromSubview()
|
public bool BackFromSubview()
|
||||||
{
|
{
|
||||||
if (_mode == CollectionMode.UnitDetail)
|
if (_mode == CollectionMode.UnitDetail)
|
||||||
@@ -147,6 +160,12 @@ namespace KernelPanic.UI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_mode == CollectionMode.Packages)
|
||||||
|
{
|
||||||
|
SelectPackageInventory(_selectedPackageInventoryIndex + delta);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_mode == CollectionMode.UnitDetail)
|
if (_mode == CollectionMode.UnitDetail)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -309,6 +328,268 @@ namespace KernelPanic.UI
|
|||||||
SelectLanguage(_selectedLanguageIndex);
|
SelectLanguage(_selectedLanguageIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RenderPackages()
|
||||||
|
{
|
||||||
|
if (_list == null || _detail == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rows.Clear();
|
||||||
|
_list.Clear();
|
||||||
|
_detail.Clear();
|
||||||
|
_list.RemoveFromClassList("hidden");
|
||||||
|
_detailScroll?.RemoveFromClassList("collection-detail-full");
|
||||||
|
if (_detailScroll != null)
|
||||||
|
{
|
||||||
|
_detailScroll.verticalScrollerVisibility = ScrollerVisibility.Auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveData saveData = _getSaveData?.Invoke();
|
||||||
|
int cache = Math.Max(0, saveData?.cacheBalance ?? 0);
|
||||||
|
int bandwidth = Math.Max(0, saveData?.bandwidthBalance ?? 0);
|
||||||
|
|
||||||
|
VisualElement filterBar = new();
|
||||||
|
filterBar.AddToClassList("collection-actions");
|
||||||
|
AddPackageFilterButton(filterBar, null, "all");
|
||||||
|
AddPackageFilterButton(filterBar, PackageSlot.Kernel, "kernel");
|
||||||
|
AddPackageFilterButton(filterBar, PackageSlot.Runtime, "runtime");
|
||||||
|
AddPackageFilterButton(filterBar, PackageSlot.Daemon, "daemon");
|
||||||
|
_list.Add(filterBar);
|
||||||
|
_list.Add(BuildDetailLine("wallet", $"Cache={cache} Bandwidth={bandwidth}"));
|
||||||
|
|
||||||
|
List<OwnedPackageInstance> packages = BuildSortedOwnedPackages(_packageInventoryFilter);
|
||||||
|
_packageInventory = packages;
|
||||||
|
|
||||||
|
if (packages.Count == 0)
|
||||||
|
{
|
||||||
|
_detail.Add(new Label("no packages owned") { name = "CollectionEmptyTitle" });
|
||||||
|
_detail.Add(new Label("pull packages via gacha, or check a different slot filter") { name = "CollectionEmptyHint" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedPackageInventoryIndex = Mathf.Clamp(_selectedPackageInventoryIndex, 0, packages.Count - 1);
|
||||||
|
PackageSlot? lastSlot = null;
|
||||||
|
for (int i = 0; i < packages.Count; i++)
|
||||||
|
{
|
||||||
|
int index = i;
|
||||||
|
OwnedPackageInstance owned = packages[i];
|
||||||
|
if (!_packageInventoryFilter.HasValue && owned.Definition.Slot != lastSlot)
|
||||||
|
{
|
||||||
|
lastSlot = owned.Definition.Slot;
|
||||||
|
Label header = new(lastSlot.Value.ToString().ToUpperInvariant());
|
||||||
|
header.AddToClassList("detail-section-title");
|
||||||
|
_list.Add(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
VisualElement row = BuildPackageInventoryRow(owned, cache, bandwidth);
|
||||||
|
row.RegisterCallback<PointerEnterEvent>(_ => SelectPackageInventory(index));
|
||||||
|
row.RegisterCallback<ClickEvent>(_ => SelectPackageInventory(index));
|
||||||
|
_list.Add(row);
|
||||||
|
_rows.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectPackageInventory(_selectedPackageInventoryIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddPackageFilterButton(VisualElement target, PackageSlot? slot, string label)
|
||||||
|
{
|
||||||
|
Button button = new(() =>
|
||||||
|
{
|
||||||
|
_packageInventoryFilter = slot;
|
||||||
|
_selectedPackageInventoryIndex = 0;
|
||||||
|
RenderPackages();
|
||||||
|
ViewChanged?.Invoke();
|
||||||
|
})
|
||||||
|
{
|
||||||
|
text = label,
|
||||||
|
focusable = false
|
||||||
|
};
|
||||||
|
button.AddToClassList("collection-tab");
|
||||||
|
button.EnableInClassList("selected", _packageInventoryFilter == slot);
|
||||||
|
target.Add(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<OwnedPackageInstance> BuildSortedOwnedPackages(PackageSlot? filter)
|
||||||
|
{
|
||||||
|
List<OwnedPackageInstance> packages = new();
|
||||||
|
if (_playerCollection == null)
|
||||||
|
{
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _playerCollection.OwnedPackageInstances.Count; i++)
|
||||||
|
{
|
||||||
|
OwnedPackageInstance owned = _playerCollection.OwnedPackageInstances[i];
|
||||||
|
if (owned?.Definition == null || (filter.HasValue && owned.Definition.Slot != filter.Value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
packages.Add(owned);
|
||||||
|
}
|
||||||
|
|
||||||
|
packages.Sort((a, b) =>
|
||||||
|
{
|
||||||
|
int slotCompare = ((int)a.Definition.Slot).CompareTo((int)b.Definition.Slot);
|
||||||
|
if (slotCompare != 0)
|
||||||
|
{
|
||||||
|
return slotCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rarityCompare = b.Definition.Rarity.CompareTo(a.Definition.Rarity);
|
||||||
|
return rarityCompare != 0 ? rarityCompare : string.Compare(PackageName(a.Definition), PackageName(b.Definition), StringComparison.OrdinalIgnoreCase);
|
||||||
|
});
|
||||||
|
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VisualElement BuildPackageInventoryRow(OwnedPackageInstance owned, int cache, int bandwidth)
|
||||||
|
{
|
||||||
|
PackageDefinition package = owned.Definition;
|
||||||
|
VisualElement row = new();
|
||||||
|
row.AddToClassList("collection-row");
|
||||||
|
row.AddToClassList("package-slot-row");
|
||||||
|
|
||||||
|
Label name = new(PackageName(package));
|
||||||
|
name.AddToClassList("package-slot-name");
|
||||||
|
name.style.color = new StyleColor(RarityPresentation.ForStars(package.Rarity).Color);
|
||||||
|
row.Add(name);
|
||||||
|
|
||||||
|
Label slot = new(package.Slot.ToString().ToUpperInvariant());
|
||||||
|
slot.AddToClassList("package-slot-label");
|
||||||
|
row.Add(slot);
|
||||||
|
|
||||||
|
RarityStyle rarity = RarityPresentation.ForStars(package.Rarity);
|
||||||
|
Label rarityLabel = new(rarity.Badge);
|
||||||
|
rarityLabel.AddToClassList("package-slot-rarity");
|
||||||
|
rarityLabel.AddToClassList(rarity.ClassName);
|
||||||
|
row.Add(rarityLabel);
|
||||||
|
|
||||||
|
Label level = new($"Lv {owned.UpgradeLevel}/{PackageTuning.MaxPackageLevel}");
|
||||||
|
level.AddToClassList("package-slot-level");
|
||||||
|
row.Add(level);
|
||||||
|
|
||||||
|
Label effect = new(OneLineEffect(package, owned.UpgradeLevel, null));
|
||||||
|
effect.AddToClassList("package-slot-effect");
|
||||||
|
row.Add(effect);
|
||||||
|
|
||||||
|
string equippedOn = FindEquippedDistroName(package.Id);
|
||||||
|
if (equippedOn != null)
|
||||||
|
{
|
||||||
|
Label tag = new($"@ {equippedOn}");
|
||||||
|
tag.AddToClassList("package-notice");
|
||||||
|
row.Add(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CanAffordNextUpgrade(owned, cache, bandwidth))
|
||||||
|
{
|
||||||
|
Label hint = new("upgrade");
|
||||||
|
hint.AddToClassList("unit-overview-upgrade");
|
||||||
|
row.Add(hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CanAffordNextUpgrade(OwnedPackageInstance owned, int cache, int bandwidth)
|
||||||
|
{
|
||||||
|
if (owned.UpgradeLevel >= PackageTuning.MaxPackageLevel)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextLevel = owned.UpgradeLevel + 1;
|
||||||
|
int rarity = owned.Definition.Rarity;
|
||||||
|
return cache >= PackageTuning.GetUpgradeCacheCost(nextLevel, rarity) && bandwidth >= PackageTuning.GetUpgradeBandwidthCost(nextLevel, rarity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FindEquippedDistroName(string packageId)
|
||||||
|
{
|
||||||
|
if (_packageLoadout == null || string.IsNullOrWhiteSpace(packageId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _units.Count; i++)
|
||||||
|
{
|
||||||
|
DistroDefinition unit = _units[i];
|
||||||
|
if (unit == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (KeyValuePair<PackageSlot, string> equipped in _packageLoadout.GetEquippedPackageIds(unit.Id))
|
||||||
|
{
|
||||||
|
if (string.Equals(equipped.Value, packageId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return DistroPresentation.DisplayName(unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectPackageInventory(int index)
|
||||||
|
{
|
||||||
|
if (_packageInventory.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedPackageInventoryIndex = Mathf.Clamp(index, 0, _packageInventory.Count - 1);
|
||||||
|
ApplySelection(_selectedPackageInventoryIndex);
|
||||||
|
RenderPackageInventoryDetail(_packageInventory[_selectedPackageInventoryIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderPackageInventoryDetail(OwnedPackageInstance owned)
|
||||||
|
{
|
||||||
|
_detail.Clear();
|
||||||
|
if (owned?.Definition == null)
|
||||||
|
{
|
||||||
|
_detail.Add(new Label("no packages owned") { name = "CollectionEmptyTitle" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PackageDefinition package = owned.Definition;
|
||||||
|
SaveData saveData = _getSaveData?.Invoke();
|
||||||
|
int cache = Math.Max(0, saveData?.cacheBalance ?? 0);
|
||||||
|
int bandwidth = Math.Max(0, saveData?.bandwidthBalance ?? 0);
|
||||||
|
|
||||||
|
Label name = new(PackageName(package));
|
||||||
|
name.AddToClassList("collection-detail-name");
|
||||||
|
name.style.color = new StyleColor(RarityPresentation.ForStars(package.Rarity).Color);
|
||||||
|
_detail.Add(name);
|
||||||
|
|
||||||
|
_detail.Add(BuildDetailLine("wallet", $"Cache={cache} Bandwidth={bandwidth}"));
|
||||||
|
_detail.Add(BuildDetailLine("slot", package.Slot.ToString()));
|
||||||
|
_detail.Add(BuildDetailLine("rarity", RarityPresentation.ForStars(package.Rarity).Stars));
|
||||||
|
_detail.Add(BuildDetailLine("level", $"Lv {owned.UpgradeLevel}/{PackageTuning.MaxPackageLevel}"));
|
||||||
|
_detail.Add(BuildDetailLine("equipped", FindEquippedDistroName(package.Id) ?? "not equipped"));
|
||||||
|
_detail.Add(BuildDetailLine("current", FormatPackageEffect(package, owned.UpgradeLevel, null)));
|
||||||
|
|
||||||
|
int nextLevel = owned.UpgradeLevel + 1;
|
||||||
|
bool maxLevel = owned.UpgradeLevel >= PackageTuning.MaxPackageLevel;
|
||||||
|
int upgradeCacheCost = maxLevel ? 0 : PackageTuning.GetUpgradeCacheCost(nextLevel, package.Rarity);
|
||||||
|
int upgradeBandwidthCost = maxLevel ? 0 : PackageTuning.GetUpgradeBandwidthCost(nextLevel, package.Rarity);
|
||||||
|
bool canUpgrade = !maxLevel && cache >= upgradeCacheCost && bandwidth >= upgradeBandwidthCost && _tryUpgradePackage != null;
|
||||||
|
string upgradeState = maxLevel ? "max level" : cache < upgradeCacheCost ? "insufficient cache" : bandwidth < upgradeBandwidthCost ? "insufficient bandwidth" : "ready";
|
||||||
|
string nextPreview = maxLevel
|
||||||
|
? "max level reached"
|
||||||
|
: $"{FormatPackageEffect(package, owned.UpgradeLevel, null)} -> {FormatPackageEffect(package, nextLevel, null)}";
|
||||||
|
_detail.Add(BuildDetailLine("next", nextPreview));
|
||||||
|
_detail.Add(BuildCompactUpgradeCommand($"> make upgrade (Cache {upgradeCacheCost} / Bandwidth {upgradeBandwidthCost})", upgradeState, canUpgrade, () => UpgradePackage(owned)));
|
||||||
|
|
||||||
|
bool equipped = _packageLoadout != null && _packageLoadout.IsEquipped(package.Id);
|
||||||
|
int scrapCache = PackageTuning.GetCacheForRarity(package.Rarity) + PackageTuning.GetRefundedInvestedCache(owned.UpgradeLevel, package.Rarity);
|
||||||
|
int scrapBandwidth = PackageTuning.GetRefundedInvestedBandwidth(owned.UpgradeLevel, package.Rarity);
|
||||||
|
bool confirming = string.Equals(_pendingScrapPackageId, package.Id, StringComparison.OrdinalIgnoreCase);
|
||||||
|
string scrapCommand = confirming ? $"> rm --purge --yes (+{scrapCache} cache/+{scrapBandwidth} bandwidth)" : $"> rm --purge (+{scrapCache} cache)";
|
||||||
|
string scrapState = equipped ? "equipped: unequip first" : confirming ? "confirm permanent scrap" : "requires confirm";
|
||||||
|
_detail.Add(BuildCompactUpgradeCommand(scrapCommand, scrapState, !equipped && _tryScrapPackage != null, () => ScrapPackage(owned)));
|
||||||
|
}
|
||||||
|
|
||||||
private VisualElement BuildUnitOverviewRow(DistroDefinition unit)
|
private VisualElement BuildUnitOverviewRow(DistroDefinition unit)
|
||||||
{
|
{
|
||||||
VisualElement row = new();
|
VisualElement row = new();
|
||||||
@@ -814,82 +1095,6 @@ namespace KernelPanic.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddPackageLoadoutPanel(VisualElement target, DistroDefinition unit)
|
|
||||||
{
|
|
||||||
if (unit == null || _packageLoadout == null || _playerCollection == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Label title = new("meta packages (persistent)");
|
|
||||||
title.AddToClassList("detail-section-title");
|
|
||||||
target.Add(title);
|
|
||||||
target.Add(BuildDetailLine("scope", "package slots persist across every run; card loadout is run-only"));
|
|
||||||
|
|
||||||
AddPackageSlot(target, unit, PackageSlot.Kernel);
|
|
||||||
AddPackageSlot(target, unit, PackageSlot.Runtime);
|
|
||||||
AddPackageSlot(target, unit, PackageSlot.Daemon);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddPackageSlot(VisualElement target, DistroDefinition unit, PackageSlot slot)
|
|
||||||
{
|
|
||||||
VisualElement slotBlock = new();
|
|
||||||
slotBlock.AddToClassList("package-slot-block");
|
|
||||||
PackageDefinition equipped = _packageLoadout.GetEquippedPackage(unit.Id, slot);
|
|
||||||
string equippedName = equipped == null ? "empty" : PackageName(equipped);
|
|
||||||
slotBlock.Add(BuildDetailLine(slot.ToString().ToLowerInvariant(), equippedName));
|
|
||||||
if (equipped != null)
|
|
||||||
{
|
|
||||||
Label effect = new(equipped.Description);
|
|
||||||
effect.AddToClassList("package-description");
|
|
||||||
slotBlock.Add(effect);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < _playerCollection.OwnedPackages.Count; i++)
|
|
||||||
{
|
|
||||||
PackageDefinition package = _playerCollection.OwnedPackages[i];
|
|
||||||
if (package == null || package.Slot != slot)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isEquipped = equipped != null && string.Equals(equipped.Id, package.Id, StringComparison.OrdinalIgnoreCase);
|
|
||||||
VisualElement row = new();
|
|
||||||
row.AddToClassList("package-row");
|
|
||||||
row.EnableInClassList("equipped", isEquipped);
|
|
||||||
row.EnableInClassList($"rarity-{package.Rarity}", true);
|
|
||||||
row.RegisterCallback<ClickEvent>(_ => TogglePackage(unit, slot, package, isEquipped));
|
|
||||||
|
|
||||||
VisualElement summary = new();
|
|
||||||
summary.AddToClassList("package-summary");
|
|
||||||
Label marker = new(isEquipped ? "[x]" : "[ ]");
|
|
||||||
marker.AddToClassList("package-marker");
|
|
||||||
Label name = new(PackageName(package));
|
|
||||||
name.AddToClassList("package-name");
|
|
||||||
Label meta = new($"{package.Rarity}* / {package.Slot}");
|
|
||||||
meta.AddToClassList("package-meta");
|
|
||||||
summary.Add(marker);
|
|
||||||
summary.Add(name);
|
|
||||||
summary.Add(meta);
|
|
||||||
row.Add(summary);
|
|
||||||
|
|
||||||
Label description = new(package.Description);
|
|
||||||
description.AddToClassList("package-description");
|
|
||||||
row.Add(description);
|
|
||||||
|
|
||||||
if (package.IsIntendedFor(unit.Id))
|
|
||||||
{
|
|
||||||
Label intended = new("on-distro");
|
|
||||||
intended.AddToClassList("package-notice");
|
|
||||||
row.Add(intended);
|
|
||||||
}
|
|
||||||
|
|
||||||
slotBlock.Add(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
target.Add(slotBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TogglePackage(DistroDefinition unit, PackageSlot slot, PackageDefinition package, bool isEquipped)
|
private void TogglePackage(DistroDefinition unit, PackageSlot slot, PackageDefinition package, bool isEquipped)
|
||||||
{
|
{
|
||||||
if (unit == null || package == null || _packageLoadout == null)
|
if (unit == null || package == null || _packageLoadout == null)
|
||||||
@@ -989,72 +1194,11 @@ namespace KernelPanic.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddPackageInventoryPanel(VisualElement target)
|
|
||||||
{
|
|
||||||
if (_playerCollection == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveData saveData = _getSaveData?.Invoke();
|
|
||||||
int cache = Math.Max(0, saveData?.cacheBalance ?? 0);
|
|
||||||
int bandwidth = Math.Max(0, saveData?.bandwidthBalance ?? 0);
|
|
||||||
Label title = new("package inventory");
|
|
||||||
title.AddToClassList("detail-section-title");
|
|
||||||
target.Add(title);
|
|
||||||
target.Add(BuildDetailLine("wallet", $"Cache={cache} Bandwidth={bandwidth}"));
|
|
||||||
target.Add(BuildDetailLine("scrap", "permanent; equipped packages are protected, unequip first"));
|
|
||||||
|
|
||||||
for (int i = 0; i < _playerCollection.OwnedPackageInstances.Count; i++)
|
|
||||||
{
|
|
||||||
OwnedPackageInstance owned = _playerCollection.OwnedPackageInstances[i];
|
|
||||||
PackageDefinition package = owned?.Definition;
|
|
||||||
if (package == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
VisualElement row = new();
|
|
||||||
row.AddToClassList("package-row");
|
|
||||||
row.EnableInClassList($"rarity-{package.Rarity}", true);
|
|
||||||
|
|
||||||
VisualElement summary = new();
|
|
||||||
summary.AddToClassList("package-summary");
|
|
||||||
Label name = new(PackageName(package));
|
|
||||||
name.AddToClassList("package-name");
|
|
||||||
Label meta = new($"{package.Rarity}* / {package.Slot} / Lv {owned.UpgradeLevel}/{PackageTuning.MaxPackageLevel}");
|
|
||||||
meta.AddToClassList("package-meta");
|
|
||||||
summary.Add(name);
|
|
||||||
summary.Add(meta);
|
|
||||||
row.Add(summary);
|
|
||||||
|
|
||||||
Label description = new(FormatPackageEffect(package, owned.UpgradeLevel, null));
|
|
||||||
description.AddToClassList("package-description");
|
|
||||||
row.Add(description);
|
|
||||||
|
|
||||||
int nextLevel = owned.UpgradeLevel + 1;
|
|
||||||
bool maxLevel = owned.UpgradeLevel >= PackageTuning.MaxPackageLevel;
|
|
||||||
int cacheCost = maxLevel ? 0 : PackageTuning.GetUpgradeCacheCost(nextLevel, package.Rarity);
|
|
||||||
int bandwidthCost = maxLevel ? 0 : PackageTuning.GetUpgradeBandwidthCost(nextLevel, package.Rarity);
|
|
||||||
bool canUpgrade = !maxLevel && cache >= cacheCost && bandwidth >= bandwidthCost && _tryUpgradePackage != null;
|
|
||||||
string upgradeState = maxLevel ? "max level" : cache < cacheCost ? "insufficient cache" : bandwidth < bandwidthCost ? "insufficient bandwidth" : "ready";
|
|
||||||
row.Add(BuildCompactUpgradeCommand($"> make upgrade ({cacheCost} cache/{bandwidthCost} bandwidth)", upgradeState, canUpgrade, () => UpgradePackage(owned)));
|
|
||||||
|
|
||||||
bool equipped = _packageLoadout != null && _packageLoadout.IsEquipped(package.Id);
|
|
||||||
int scrapCache = PackageTuning.GetCacheForRarity(package.Rarity) + PackageTuning.GetRefundedInvestedCache(owned.UpgradeLevel, package.Rarity);
|
|
||||||
int scrapBandwidth = PackageTuning.GetRefundedInvestedBandwidth(owned.UpgradeLevel, package.Rarity);
|
|
||||||
bool confirming = string.Equals(_pendingScrapPackageId, package.Id, StringComparison.OrdinalIgnoreCase);
|
|
||||||
string scrapCommand = confirming ? $"> rm --purge --yes (+{scrapCache} cache/+{scrapBandwidth} bandwidth)" : $"> rm --purge (+{scrapCache} cache)";
|
|
||||||
string scrapState = equipped ? "equipped: unequip first" : confirming ? "confirm permanent scrap" : "requires confirm";
|
|
||||||
row.Add(BuildCompactUpgradeCommand(scrapCommand, scrapState, !equipped && _tryScrapPackage != null, () => ScrapPackage(owned)));
|
|
||||||
target.Add(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpgradePackage(OwnedPackageInstance package)
|
private void UpgradePackage(OwnedPackageInstance package)
|
||||||
{
|
{
|
||||||
|
bool inventoryMode = _mode == CollectionMode.Packages;
|
||||||
PackageUpgradeResult result = _tryUpgradePackage == null ? new PackageUpgradeResult(false, PackageUpgradeFailureReason.NotOwned, 0, 0, 0) : _tryUpgradePackage(package);
|
PackageUpgradeResult result = _tryUpgradePackage == null ? new PackageUpgradeResult(false, PackageUpgradeFailureReason.NotOwned, 0, 0, 0) : _tryUpgradePackage(package);
|
||||||
RenderUnitDetail(_units[_selectedUnitIndex]);
|
RefreshAfterPackageAction(inventoryMode);
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
_detail.Add(BuildDetailLine("package upgrade", FormatPackageUpgradeFailure(result.FailureReason)));
|
_detail.Add(BuildDetailLine("package upgrade", FormatPackageUpgradeFailure(result.FailureReason)));
|
||||||
@@ -1068,22 +1212,41 @@ namespace KernelPanic.UI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool inventoryMode = _mode == CollectionMode.Packages;
|
||||||
if (!string.Equals(_pendingScrapPackageId, package.PackageId, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(_pendingScrapPackageId, package.PackageId, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_pendingScrapPackageId = package.PackageId;
|
_pendingScrapPackageId = package.PackageId;
|
||||||
RenderUnitDetail(_units[_selectedUnitIndex]);
|
RefreshAfterPackageAction(inventoryMode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PackageScrapResult result = _tryScrapPackage == null ? new PackageScrapResult(false, PackageScrapFailureReason.NotOwned, 0, 0) : _tryScrapPackage(package);
|
PackageScrapResult result = _tryScrapPackage == null ? new PackageScrapResult(false, PackageScrapFailureReason.NotOwned, 0, 0) : _tryScrapPackage(package);
|
||||||
_pendingScrapPackageId = null;
|
_pendingScrapPackageId = null;
|
||||||
RenderUnitDetail(_units[_selectedUnitIndex]);
|
RefreshAfterPackageAction(inventoryMode);
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
_detail.Add(BuildDetailLine("package scrap", FormatPackageScrapFailure(result.FailureReason)));
|
_detail.Add(BuildDetailLine("package scrap", FormatPackageScrapFailure(result.FailureReason)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wired upgrade/scrap callbacks (MainMenuController) may call RefreshUnits() internally,
|
||||||
|
// which forces CollectionMode.Units and re-renders. Restore the caller's actual view so
|
||||||
|
// the Packages inventory screen (and its keyboard navigation) don't get bumped to Units.
|
||||||
|
private void RefreshAfterPackageAction(bool inventoryMode)
|
||||||
|
{
|
||||||
|
if (inventoryMode)
|
||||||
|
{
|
||||||
|
_mode = CollectionMode.Packages;
|
||||||
|
RenderPackages();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_units.Count > 0)
|
||||||
|
{
|
||||||
|
RenderUnitDetail(_units[_selectedUnitIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void RenderLanguageDetail(LanguageCatalogEntry language)
|
private void RenderLanguageDetail(LanguageCatalogEntry language)
|
||||||
{
|
{
|
||||||
_detail.Clear();
|
_detail.Clear();
|
||||||
@@ -1579,6 +1742,7 @@ namespace KernelPanic.UI
|
|||||||
{
|
{
|
||||||
Units,
|
Units,
|
||||||
Languages,
|
Languages,
|
||||||
|
Packages,
|
||||||
UnitDetail,
|
UnitDetail,
|
||||||
CardSubview
|
CardSubview
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,20 +297,12 @@ namespace KernelPanic.UI
|
|||||||
string displayName = reward.DisplayName;
|
string displayName = reward.DisplayName;
|
||||||
if (reward.RewardType == GachaRewardType.Package)
|
if (reward.RewardType == GachaRewardType.Package)
|
||||||
{
|
{
|
||||||
PackageDefinition package = GrantRandomPackageByRarity(pending.PackageDatabase, collection, saveData, reward.StarRating, out bool duplicatePackage);
|
PackageDefinition package = GrantRandomPackageByRarity(pending.PackageDatabase, collection, saveData, reward.StarRating, out bool duplicatePackage, out int cacheAwarded);
|
||||||
if (package != null)
|
if (package != null)
|
||||||
{
|
{
|
||||||
displayName = string.IsNullOrWhiteSpace(package.DisplayName) ? package.Id : package.DisplayName;
|
displayName = string.IsNullOrWhiteSpace(package.DisplayName) ? package.Id : package.DisplayName;
|
||||||
if (duplicatePackage)
|
outcomeKind = duplicatePackage ? PullOutcomeKind.Dupe : PullOutcomeKind.Granted;
|
||||||
{
|
merges = cacheAwarded;
|
||||||
outcomeKind = PullOutcomeKind.Dupe;
|
|
||||||
merges = Mathf.Max(1, GachaTuning.MergesPerDupe);
|
|
||||||
saveData.packageMerges += merges;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
outcomeKind = PullOutcomeKind.Granted;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +365,7 @@ namespace KernelPanic.UI
|
|||||||
SummaryTotals totals = CalculateTotals();
|
SummaryTotals totals = CalculateTotals();
|
||||||
if (!animate)
|
if (!animate)
|
||||||
{
|
{
|
||||||
summaryTotalsLabel.text = FormatTotals(totals.Units, totals.Merges, totals.LanguagesText);
|
summaryTotalsLabel.text = FormatTotals(totals.Units, totals.Merges, totals.Cache, totals.LanguagesText);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +379,8 @@ namespace KernelPanic.UI
|
|||||||
float t = capturedStep / (float)SummaryTickSteps;
|
float t = capturedStep / (float)SummaryTickSteps;
|
||||||
int units = Mathf.RoundToInt(Mathf.Lerp(0, totals.Units, t));
|
int units = Mathf.RoundToInt(Mathf.Lerp(0, totals.Units, t));
|
||||||
int merges = Mathf.RoundToInt(Mathf.Lerp(0, totals.Merges, t));
|
int merges = Mathf.RoundToInt(Mathf.Lerp(0, totals.Merges, t));
|
||||||
summaryTotalsLabel.text = FormatTotals(units, merges, capturedStep == SummaryTickSteps ? totals.LanguagesText : "...");
|
int cache = Mathf.RoundToInt(Mathf.Lerp(0, totals.Cache, t));
|
||||||
|
summaryTotalsLabel.text = FormatTotals(units, merges, cache, capturedStep == SummaryTickSteps ? totals.LanguagesText : "...");
|
||||||
if (capturedStep == SummaryTickSteps)
|
if (capturedStep == SummaryTickSteps)
|
||||||
{
|
{
|
||||||
summaryTotalsLabel.RemoveFromClassList("summary-tick");
|
summaryTotalsLabel.RemoveFromClassList("summary-tick");
|
||||||
@@ -400,6 +393,7 @@ namespace KernelPanic.UI
|
|||||||
{
|
{
|
||||||
int units = 0;
|
int units = 0;
|
||||||
int merges = 0;
|
int merges = 0;
|
||||||
|
int cache = 0;
|
||||||
HashSet<Language> languages = new();
|
HashSet<Language> languages = new();
|
||||||
for (int i = 0; i < rewards.Count; i++)
|
for (int i = 0; i < rewards.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -408,21 +402,30 @@ namespace KernelPanic.UI
|
|||||||
units++;
|
units++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rewards[i].RewardType == GachaRewardType.Package)
|
||||||
|
{
|
||||||
|
cache += rewards[i].MergesAwarded;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
merges += rewards[i].MergesAwarded;
|
merges += rewards[i].MergesAwarded;
|
||||||
|
}
|
||||||
|
|
||||||
for (int languageIndex = 0; languageIndex < rewards[i].LanguagesUnlocked.Count; languageIndex++)
|
for (int languageIndex = 0; languageIndex < rewards[i].LanguagesUnlocked.Count; languageIndex++)
|
||||||
{
|
{
|
||||||
languages.Add(rewards[i].LanguagesUnlocked[languageIndex]);
|
languages.Add(rewards[i].LanguagesUnlocked[languageIndex]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SummaryTotals(units, merges, FormatLanguages(languages));
|
return new SummaryTotals(units, merges, cache, FormatLanguages(languages));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatTotals(int units, int merges, string languagesText)
|
private static string FormatTotals(int units, int merges, int cache, string languagesText)
|
||||||
{
|
{
|
||||||
string unitText = units == 0 ? "none" : units.ToString();
|
string unitText = units == 0 ? "none" : units.ToString();
|
||||||
string mergeText = merges == 0 ? "none" : merges.ToString();
|
string mergeText = merges == 0 ? "none" : merges.ToString();
|
||||||
return $"units gained: {unitText} merges: {mergeText} languages unlocked: {languagesText}";
|
string cacheText = cache == 0 ? "none" : cache.ToString();
|
||||||
|
return $"units gained: {unitText} merges: {mergeText} cache: {cacheText} languages unlocked: {languagesText}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatLanguages(HashSet<Language> languages)
|
private static string FormatLanguages(HashSet<Language> languages)
|
||||||
@@ -468,9 +471,10 @@ namespace KernelPanic.UI
|
|||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PackageDefinition GrantRandomPackageByRarity(PackageDatabase packageDatabase, PlayerCollection collection, SaveData saveData, int rarity, out bool duplicate)
|
private static PackageDefinition GrantRandomPackageByRarity(PackageDatabase packageDatabase, PlayerCollection collection, SaveData saveData, int rarity, out bool duplicate, out int cacheAwarded)
|
||||||
{
|
{
|
||||||
duplicate = false;
|
duplicate = false;
|
||||||
|
cacheAwarded = 0;
|
||||||
int count = packageDatabase == null ? 0 : packageDatabase.CountByRarity(rarity);
|
int count = packageDatabase == null ? 0 : packageDatabase.CountByRarity(rarity);
|
||||||
if (count <= 0)
|
if (count <= 0)
|
||||||
{
|
{
|
||||||
@@ -484,8 +488,15 @@ namespace KernelPanic.UI
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
duplicate = collection.IsPackageOwned(package.Id) || saveData.ownedPackageIds.Exists(id => string.Equals(id, package.Id, StringComparison.OrdinalIgnoreCase));
|
duplicate = collection.IsPackageOwned(package.Id);
|
||||||
if (!duplicate)
|
if (duplicate)
|
||||||
|
{
|
||||||
|
// Packages are unique-per-type: a dupe never creates a second instance, it
|
||||||
|
// auto-scraps into Cache immediately, mirroring the distro dupe -> Merges path.
|
||||||
|
cacheAwarded = PackageTuning.GetCacheForRarity(package.Rarity);
|
||||||
|
saveData.cacheBalance += cacheAwarded;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
collection.AddPackageSilently(package);
|
collection.AddPackageSilently(package);
|
||||||
saveData.ownedPackages.Add(new OwnedPackageSaveEntry { id = package.Id, upgradeLevel = 0 });
|
saveData.ownedPackages.Add(new OwnedPackageSaveEntry { id = package.Id, upgradeLevel = 0 });
|
||||||
@@ -758,15 +769,17 @@ namespace KernelPanic.UI
|
|||||||
|
|
||||||
private readonly struct SummaryTotals
|
private readonly struct SummaryTotals
|
||||||
{
|
{
|
||||||
public SummaryTotals(int units, int merges, string languagesText)
|
public SummaryTotals(int units, int merges, int cache, string languagesText)
|
||||||
{
|
{
|
||||||
Units = units;
|
Units = units;
|
||||||
Merges = merges;
|
Merges = merges;
|
||||||
|
Cache = cache;
|
||||||
LanguagesText = languagesText;
|
LanguagesText = languagesText;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Units { get; }
|
public int Units { get; }
|
||||||
public int Merges { get; }
|
public int Merges { get; }
|
||||||
|
public int Cache { get; }
|
||||||
public string LanguagesText { get; }
|
public string LanguagesText { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ namespace KernelPanic.UI
|
|||||||
private Button _rootCreditExchangeCloseButton;
|
private Button _rootCreditExchangeCloseButton;
|
||||||
private Button _collectionUnitsButton;
|
private Button _collectionUnitsButton;
|
||||||
private Button _collectionLanguagesButton;
|
private Button _collectionLanguagesButton;
|
||||||
|
private Button _collectionPackagesButton;
|
||||||
private readonly ScreenFrameController _runSetupFrame = new();
|
private readonly ScreenFrameController _runSetupFrame = new();
|
||||||
private readonly ScreenFrameController _collectionFrame = new();
|
private readonly ScreenFrameController _collectionFrame = new();
|
||||||
private readonly ScreenFrameController _gachaFrame = new();
|
private readonly ScreenFrameController _gachaFrame = new();
|
||||||
@@ -156,7 +157,7 @@ namespace KernelPanic.UI
|
|||||||
private int _selectedCommandIndex;
|
private int _selectedCommandIndex;
|
||||||
private int _selectedPackageIndex;
|
private int _selectedPackageIndex;
|
||||||
private string _runSetupNotice;
|
private string _runSetupNotice;
|
||||||
private bool _collectionShowingLanguages;
|
private CollectionTopTab _collectionTab;
|
||||||
private float _bootIntroElapsed;
|
private float _bootIntroElapsed;
|
||||||
private int _bootIntroCharacterCount;
|
private int _bootIntroCharacterCount;
|
||||||
private bool _cursorVisible;
|
private bool _cursorVisible;
|
||||||
@@ -293,6 +294,7 @@ namespace KernelPanic.UI
|
|||||||
_rootCreditExchangeCloseButton = _root.Q<Button>("RootCreditExchangeCloseButton");
|
_rootCreditExchangeCloseButton = _root.Q<Button>("RootCreditExchangeCloseButton");
|
||||||
_collectionUnitsButton = _root.Q<Button>("CollectionUnitsButton");
|
_collectionUnitsButton = _root.Q<Button>("CollectionUnitsButton");
|
||||||
_collectionLanguagesButton = _root.Q<Button>("CollectionLanguagesButton");
|
_collectionLanguagesButton = _root.Q<Button>("CollectionLanguagesButton");
|
||||||
|
_collectionPackagesButton = _root.Q<Button>("CollectionPackagesButton");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadSharedStyles()
|
private void LoadSharedStyles()
|
||||||
@@ -415,6 +417,7 @@ namespace KernelPanic.UI
|
|||||||
collectionScreen.ViewChanged += SyncCollectionFrame;
|
collectionScreen.ViewChanged += SyncCollectionFrame;
|
||||||
_collectionUnitsButton.RegisterCallback<ClickEvent>(HandleCollectionUnitsTabClicked);
|
_collectionUnitsButton.RegisterCallback<ClickEvent>(HandleCollectionUnitsTabClicked);
|
||||||
_collectionLanguagesButton.RegisterCallback<ClickEvent>(HandleCollectionLanguagesTabClicked);
|
_collectionLanguagesButton.RegisterCallback<ClickEvent>(HandleCollectionLanguagesTabClicked);
|
||||||
|
_collectionPackagesButton.RegisterCallback<ClickEvent>(HandleCollectionPackagesTabClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UnregisterCallbacks()
|
private void UnregisterCallbacks()
|
||||||
@@ -484,6 +487,7 @@ namespace KernelPanic.UI
|
|||||||
collectionScreen.ViewChanged -= SyncCollectionFrame;
|
collectionScreen.ViewChanged -= SyncCollectionFrame;
|
||||||
_collectionUnitsButton.UnregisterCallback<ClickEvent>(HandleCollectionUnitsTabClicked);
|
_collectionUnitsButton.UnregisterCallback<ClickEvent>(HandleCollectionUnitsTabClicked);
|
||||||
_collectionLanguagesButton.UnregisterCallback<ClickEvent>(HandleCollectionLanguagesTabClicked);
|
_collectionLanguagesButton.UnregisterCallback<ClickEvent>(HandleCollectionLanguagesTabClicked);
|
||||||
|
_collectionPackagesButton.UnregisterCallback<ClickEvent>(HandleCollectionPackagesTabClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleRootCreditsToEntropyClicked()
|
private void HandleRootCreditsToEntropyClicked()
|
||||||
@@ -652,9 +656,18 @@ namespace KernelPanic.UI
|
|||||||
evt.StopPropagation();
|
evt.StopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleCollectionPackagesTabClicked(ClickEvent evt)
|
||||||
|
{
|
||||||
|
ShowCollectionPackages();
|
||||||
|
evt.StopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
private void LoadMetaState()
|
private void LoadMetaState()
|
||||||
{
|
{
|
||||||
_saveData = _saveService.Load();
|
_saveData = _saveService.Load();
|
||||||
|
// Snapshot before the next EnsureLists() call below, which re-runs NormalizeOwnedPackages
|
||||||
|
// and would otherwise clear this (no new duplicates survive a second normalization pass).
|
||||||
|
List<CollapsedPackageDuplicate> collapsedPackageDuplicates = new(_saveData.collapsedPackageDuplicates);
|
||||||
_saveData.EnsureLists();
|
_saveData.EnsureLists();
|
||||||
_wallet.SetBalance(_saveData.entropyBalance);
|
_wallet.SetBalance(_saveData.entropyBalance);
|
||||||
|
|
||||||
@@ -678,6 +691,7 @@ namespace KernelPanic.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MigrateCollapsedPackageDuplicatesToCache(collapsedPackageDuplicates);
|
||||||
LoadPackageLoadoutsFromSave();
|
LoadPackageLoadoutsFromSave();
|
||||||
|
|
||||||
for (int i = 0; i < _saveData.bannerPoolIds.Count; i++)
|
for (int i = 0; i < _saveData.bannerPoolIds.Count; i++)
|
||||||
@@ -704,6 +718,18 @@ namespace KernelPanic.UI
|
|||||||
SaveCurrentState();
|
SaveCurrentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void MigrateCollapsedPackageDuplicatesToCache(List<CollapsedPackageDuplicate> collapsedPackageDuplicates)
|
||||||
|
{
|
||||||
|
foreach (CollapsedPackageDuplicate duplicate in collapsedPackageDuplicates)
|
||||||
|
{
|
||||||
|
PackageDefinition package = packageDatabase == null ? null : packageDatabase.FindById(duplicate.Id);
|
||||||
|
int rarity = package == null ? 1 : package.Rarity;
|
||||||
|
int cache = PackageTuning.GetCacheForRarity(rarity) * duplicate.ExtraCount;
|
||||||
|
_saveData.cacheBalance += cache;
|
||||||
|
Debug.Log($"Migrated {duplicate.ExtraCount} duplicate save copies of package '{duplicate.Id}' into +{cache} Cache (packages are unique-per-type; kept the highest-level instance).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void AddAllFourStarDistrosToBeginnerBannerPool()
|
private void AddAllFourStarDistrosToBeginnerBannerPool()
|
||||||
{
|
{
|
||||||
if (distroDatabase == null)
|
if (distroDatabase == null)
|
||||||
@@ -769,9 +795,8 @@ namespace KernelPanic.UI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectionShowingLanguages = false;
|
_collectionTab = CollectionTopTab.Units;
|
||||||
_collectionUnitsButton.EnableInClassList(SelectedClassName, true);
|
ApplyCollectionTabButtons();
|
||||||
_collectionLanguagesButton.EnableInClassList(SelectedClassName, false);
|
|
||||||
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
|
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
|
||||||
collectionScreen.ShowUnitDetail(unit, false);
|
collectionScreen.ShowUnitDetail(unit, false);
|
||||||
SyncCollectionFrame();
|
SyncCollectionFrame();
|
||||||
@@ -1917,31 +1942,49 @@ namespace KernelPanic.UI
|
|||||||
|
|
||||||
private void ShowCollectionUnits()
|
private void ShowCollectionUnits()
|
||||||
{
|
{
|
||||||
_collectionShowingLanguages = false;
|
_collectionTab = CollectionTopTab.Units;
|
||||||
_collectionUnitsButton.EnableInClassList(SelectedClassName, true);
|
ApplyCollectionTabButtons();
|
||||||
_collectionLanguagesButton.EnableInClassList(SelectedClassName, false);
|
|
||||||
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
|
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
|
||||||
SyncCollectionFrame();
|
SyncCollectionFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowCollectionLanguages()
|
private void ShowCollectionLanguages()
|
||||||
{
|
{
|
||||||
_collectionShowingLanguages = true;
|
_collectionTab = CollectionTopTab.Languages;
|
||||||
_collectionUnitsButton.EnableInClassList(SelectedClassName, false);
|
ApplyCollectionTabButtons();
|
||||||
_collectionLanguagesButton.EnableInClassList(SelectedClassName, true);
|
|
||||||
collectionScreen.ShowLanguages();
|
collectionScreen.ShowLanguages();
|
||||||
SyncCollectionFrame();
|
SyncCollectionFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShowCollectionPackages()
|
||||||
|
{
|
||||||
|
_collectionTab = CollectionTopTab.Packages;
|
||||||
|
ApplyCollectionTabButtons();
|
||||||
|
collectionScreen.ShowPackages();
|
||||||
|
SyncCollectionFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyCollectionTabButtons()
|
||||||
|
{
|
||||||
|
_collectionUnitsButton.EnableInClassList(SelectedClassName, _collectionTab == CollectionTopTab.Units);
|
||||||
|
_collectionLanguagesButton.EnableInClassList(SelectedClassName, _collectionTab == CollectionTopTab.Languages);
|
||||||
|
_collectionPackagesButton.EnableInClassList(SelectedClassName, _collectionTab == CollectionTopTab.Packages);
|
||||||
|
}
|
||||||
|
|
||||||
private void ToggleCollectionTab()
|
private void ToggleCollectionTab()
|
||||||
{
|
{
|
||||||
if (_collectionShowingLanguages)
|
switch (_collectionTab)
|
||||||
{
|
{
|
||||||
|
case CollectionTopTab.Units:
|
||||||
|
ShowCollectionLanguages();
|
||||||
|
return;
|
||||||
|
case CollectionTopTab.Languages:
|
||||||
|
ShowCollectionPackages();
|
||||||
|
return;
|
||||||
|
default:
|
||||||
ShowCollectionUnits();
|
ShowCollectionUnits();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowCollectionLanguages();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SyncCollectionFrame()
|
private void SyncCollectionFrame()
|
||||||
@@ -2034,6 +2077,13 @@ namespace KernelPanic.UI
|
|||||||
Settings
|
Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum CollectionTopTab
|
||||||
|
{
|
||||||
|
Units,
|
||||||
|
Languages,
|
||||||
|
Packages
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class CommandMenuEntry
|
private sealed class CommandMenuEntry
|
||||||
{
|
{
|
||||||
private readonly Label _cursor;
|
private readonly Label _cursor;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<ui:VisualElement class="collection-actions">
|
<ui:VisualElement class="collection-actions">
|
||||||
<ui:Button name="CollectionUnitsButton" text="units" focusable="false" class="collection-tab" />
|
<ui:Button name="CollectionUnitsButton" text="units" focusable="false" class="collection-tab" />
|
||||||
<ui:Button name="CollectionLanguagesButton" text="languages" focusable="false" class="collection-tab" />
|
<ui:Button name="CollectionLanguagesButton" text="languages" focusable="false" class="collection-tab" />
|
||||||
|
<ui:Button name="CollectionPackagesButton" text="packages" focusable="false" class="collection-tab" />
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
<ui:VisualElement name="CollectionContent" class="collection-shell">
|
<ui:VisualElement name="CollectionContent" class="collection-shell">
|
||||||
<ui:ScrollView name="CollectionList" class="collection-list" />
|
<ui:ScrollView name="CollectionList" class="collection-list" />
|
||||||
|
|||||||
Reference in New Issue
Block a user