Added Standard Banner with a 5-star selector

This commit is contained in:
2026-07-10 11:02:27 +02:00
parent e3ea42c93e
commit ceab7dea5a
8 changed files with 497 additions and 32 deletions
+1
View File
@@ -15,6 +15,7 @@ namespace KernelPanic.Meta
public int fiveStarPityCounter;
public bool exhausted;
public bool featuredFiveStarGuaranteed;
public int fiveStarSelectorsClaimed;
public List<string> guaranteedDistroIds = new();
public GachaBannerState()
+3 -1
View File
@@ -64,16 +64,18 @@ namespace KernelPanic.Meta
public sealed class CompletedGachaPull
{
public CompletedGachaPull(string bannerId, string headerText, IReadOnlyList<CompletedGachaReward> rewards)
public CompletedGachaPull(string bannerId, string headerText, IReadOnlyList<CompletedGachaReward> rewards, int pendingFiveStarSelectorCount = 0)
{
BannerId = string.IsNullOrWhiteSpace(bannerId) ? GachaService.BeginnerBannerId : bannerId;
HeaderText = headerText;
Rewards = rewards ?? Array.Empty<CompletedGachaReward>();
PendingFiveStarSelectorCount = Math.Max(0, pendingFiveStarSelectorCount);
}
public string BannerId { get; }
public string HeaderText { get; }
public IReadOnlyList<CompletedGachaReward> Rewards { get; }
public int PendingFiveStarSelectorCount { get; }
public IReadOnlyList<string> RewardLines
{
+145 -17
View File
@@ -15,6 +15,9 @@ namespace KernelPanic.Meta
public const int BeginnerMaxPulls = 50;
public const int BeginnerSinglePullCost = 1;
public const int BeginnerTenPullCost = 8;
public const int StandardSinglePullCost = 1;
public const int StandardTenPullCost = 10;
public const int StandardFiveStarSelectorInterval = 200;
public const int FourStarHardPity = 10;
public const int FiveStarSoftPityStart = 66;
public const int FiveStarHardPity = 80;
@@ -32,6 +35,7 @@ namespace KernelPanic.Meta
private readonly Dictionary<GachaCurrencyType, int> _currencyBalances = new();
private readonly Random _random;
private GachaBannerState _beginnerState = new(BeginnerBannerId);
private GachaBannerState _standardState = new(StandardBannerId);
public GachaService()
: this(new Random())
@@ -49,7 +53,9 @@ namespace KernelPanic.Meta
public int RootCredits { get; private set; }
public IReadOnlyList<DistroDefinition> BannerPool => _bannerPool;
public GachaBannerState BeginnerState => _beginnerState;
public GachaBannerState StandardState => _standardState;
public bool IsBeginnerBannerAvailable => !_beginnerState.exhausted && _beginnerState.totalPulls < BeginnerMaxPulls && _bannerPool.Count > 0;
public int PendingStandardFiveStarSelectors => Math.Max(0, _standardState.totalPulls / StandardFiveStarSelectorInterval - _standardState.fiveStarSelectorsClaimed);
public event Action BannerPoolChanged;
public event Action Changed;
@@ -74,6 +80,12 @@ namespace KernelPanic.Meta
_beginnerState.fiveStarPityCounter = Math.Max(0, _beginnerState.fiveStarPityCounter);
_standardState = data.standardBannerState ?? new GachaBannerState(StandardBannerId);
_standardState.bannerId = StandardBannerId;
_standardState.EnsureLists();
_standardState.fiveStarPityCounter = Math.Max(0, _standardState.fiveStarPityCounter);
_standardState.fiveStarSelectorsClaimed = Math.Max(0, _standardState.fiveStarSelectorsClaimed);
Changed?.Invoke();
}
@@ -88,6 +100,7 @@ namespace KernelPanic.Meta
data.rootCredits = RootCredits;
data.commitsBalance = GetCurrencyBalance(GachaCurrencyType.StandardPull);
data.beginnerBannerState = CloneState(_beginnerState);
data.standardBannerState = CloneState(_standardState);
}
public int GetCurrencyBalance(GachaCurrencyType currencyType)
@@ -195,6 +208,11 @@ namespace KernelPanic.Meta
}
public bool CanAffordPull(int pullCount, EntropyWallet wallet, PullPaymentSource source, out string failureReason)
{
return CanAffordPull(pullCount, wallet, source, BeginnerBannerId, out failureReason);
}
public bool CanAffordPull(int pullCount, EntropyWallet wallet, PullPaymentSource source, string bannerId, out string failureReason)
{
if (pullCount != 1 && pullCount != 10)
{
@@ -202,20 +220,28 @@ namespace KernelPanic.Meta
return false;
}
if (!IsBeginnerBannerAvailable)
if (bannerId == BeginnerBannerId)
{
failureReason = "beginner banner unavailable";
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;
}
}
else if (bannerId != StandardBannerId)
{
failureReason = $"banner '{bannerId}' not implemented yet";
return false;
}
int remainingPulls = BeginnerMaxPulls - _beginnerState.totalPulls;
if (pullCount > remainingPulls)
{
failureReason = $"only {remainingPulls} beginner pull(s) remain";
return false;
}
int cost = GetBeginnerPullCost(pullCount);
int cost = GetPullCost(bannerId, pullCount);
if (source == PullPaymentSource.Commits && GetCurrencyBalance(GachaCurrencyType.StandardPull) < cost)
{
failureReason = $"need {cost} {FormatCurrencyName(GachaCurrencyType.StandardPull)}";
@@ -233,16 +259,36 @@ namespace KernelPanic.Meta
return true;
}
public int GetPullCost(string bannerId, int pullCount)
{
if (bannerId == BeginnerBannerId)
{
return GetBeginnerPullCost(pullCount);
}
if (bannerId == StandardBannerId)
{
return pullCount == 10 ? StandardTenPullCost : StandardSinglePullCost;
}
return pullCount;
}
public bool PayForPull(int pullCount, EntropyWallet wallet, PullPaymentSource source, out int commitsSpent, out int entropySpent, out string failureReason)
{
return PayForPull(pullCount, wallet, source, BeginnerBannerId, out commitsSpent, out entropySpent, out failureReason);
}
public bool PayForPull(int pullCount, EntropyWallet wallet, PullPaymentSource source, string bannerId, out int commitsSpent, out int entropySpent, out string failureReason)
{
commitsSpent = 0;
entropySpent = 0;
if (!CanAffordPull(pullCount, wallet, source, out failureReason))
if (!CanAffordPull(pullCount, wallet, source, bannerId, out failureReason))
{
return false;
}
int cost = GetBeginnerPullCost(pullCount);
int cost = GetPullCost(bannerId, pullCount);
if (source == PullPaymentSource.Commits)
{
commitsSpent = cost;
@@ -301,6 +347,82 @@ namespace KernelPanic.Meta
return new GachaPullResult(true, BeginnerBannerId, GachaCurrencyType.StandardPull, commitSpend, entropySpent, rewards, null);
}
public GachaPullResult PerformStandardPull(int pullCount, EntropyWallet wallet, PullPaymentSource source)
{
if (!CanAffordPull(pullCount, wallet, source, StandardBannerId, out string failureReason))
{
return GachaPullResult.Failed(StandardBannerId, failureReason);
}
if (!PayForPull(pullCount, wallet, source, StandardBannerId, out int commitSpend, out int entropySpent, out failureReason))
{
return GachaPullResult.Failed(StandardBannerId, failureReason);
}
List<GachaReward> rewards = new();
for (int i = 0; i < pullCount; i++)
{
_standardState.totalPulls++;
rewards.Add(RollGenericReward(_standardState));
}
Changed?.Invoke();
return new GachaPullResult(true, StandardBannerId, GachaCurrencyType.StandardPull, commitSpend, entropySpent, rewards, null);
}
public int GetStandardFiveStarSelectorProgress()
{
return _standardState.totalPulls % StandardFiveStarSelectorInterval;
}
public bool TryClaimStandardFiveStarSelector(out string failureReason)
{
if (PendingStandardFiveStarSelectors <= 0)
{
failureReason = "no 5-star selector available";
return false;
}
_standardState.fiveStarSelectorsClaimed++;
failureReason = null;
Changed?.Invoke();
return true;
}
public IReadOnlyList<DistroDefinition> GetStandardFiveStarCharacterChoices()
{
List<DistroDefinition> choices = new();
for (int i = 0; i < _bannerPool.Count; i++)
{
DistroDefinition distro = _bannerPool[i];
if (distro != null && GetDistroStars(distro) == 5)
{
choices.Add(distro);
}
}
return choices;
}
public IReadOnlyList<PackageDefinition> GetStandardFiveStarEquipmentChoices(PackageDatabase packageDatabase)
{
List<PackageDefinition> choices = new();
if (packageDatabase != null)
{
IReadOnlyList<PackageDefinition> packages = packageDatabase.AllPackages;
for (int i = 0; i < packages.Count; i++)
{
PackageDefinition package = packages[i];
if (package != null && package.Rarity == 5)
{
choices.Add(package);
}
}
}
return choices;
}
public static string FormatCurrencyName(GachaCurrencyType currencyType)
{
return currencyType switch
@@ -372,20 +494,25 @@ namespace KernelPanic.Meta
return RewardGuaranteedBeginnerFiveStar("50-pull beginner guarantee");
}
if (TryRollFiveStar(_beginnerState, out bool fiveStarPityTriggered))
return RollGenericReward(_beginnerState);
}
private GachaReward RollGenericReward(GachaBannerState state)
{
if (TryRollFiveStar(state, out bool fiveStarPityTriggered))
{
return RewardRandomFiveStar(fiveStarPityTriggered);
}
bool pityTriggered = _beginnerState.pityCounter >= FourStarHardPity - 1;
bool pityTriggered = state.pityCounter >= FourStarHardPity - 1;
bool hitFourStarOrCharacter = pityTriggered || _random.NextDouble() < FourStarBaseChance;
if (!hitFourStarOrCharacter)
{
_beginnerState.pityCounter++;
state.pityCounter++;
return GachaReward.Package(3, "3-star package", false, false);
}
_beginnerState.pityCounter = 0;
state.pityCounter = 0;
if (_bannerPool.Count > 0 && _random.NextDouble() < DistroChanceOnFeaturedTier)
{
DistroDefinition distro = FindRandomBannerPoolDistroByStars(4);
@@ -560,7 +687,8 @@ namespace KernelPanic.Meta
pityCounter = state.pityCounter,
fiveStarPityCounter = state.fiveStarPityCounter,
exhausted = state.exhausted,
featuredFiveStarGuaranteed = state.featuredFiveStarGuaranteed
featuredFiveStarGuaranteed = state.featuredFiveStarGuaranteed,
fiveStarSelectorsClaimed = state.fiveStarSelectorsClaimed
};
clone.guaranteedDistroIds.AddRange(state.guaranteedDistroIds);
return clone;
+4
View File
@@ -81,6 +81,7 @@ namespace KernelPanic.Meta
public List<string> ownedUnitIds = new();
public List<string> bannerPoolIds = new();
public GachaBannerState beginnerBannerState = new(GachaService.BeginnerBannerId);
public GachaBannerState standardBannerState = new(GachaService.StandardBannerId);
public LastRunLoadoutSaveEntry lastRunLoadout = new();
public string lastRunDistroId;
public List<DistroBestWaveSaveEntry> distroBestWaves = new();
@@ -109,6 +110,9 @@ namespace KernelPanic.Meta
beginnerBannerState ??= new GachaBannerState(GachaService.BeginnerBannerId);
beginnerBannerState.bannerId = GachaService.BeginnerBannerId;
beginnerBannerState.EnsureLists();
standardBannerState ??= new GachaBannerState(GachaService.StandardBannerId);
standardBannerState.bannerId = GachaService.StandardBannerId;
standardBannerState.EnsureLists();
lastRunLoadout ??= new LastRunLoadoutSaveEntry();
lastRunLoadout.EnsureLists();
if (string.IsNullOrWhiteSpace(lastRunDistroId) && !string.IsNullOrWhiteSpace(lastRunLoadout.distroId))
+271 -6
View File
@@ -16,6 +16,13 @@ namespace KernelPanic.UI
{
private const string SelectedClassName = "selected";
private enum SelectorStep
{
Character,
Equipment,
Confirm
}
private readonly List<VisualElement> _bannerRows = new();
private readonly string[] _bannerIds =
{
@@ -24,20 +31,28 @@ namespace KernelPanic.UI
GachaService.LimitedBannerId
};
private VisualElement _root;
private VisualElement _bannerList;
private VisualElement _bannerDetail;
private VisualElement _selectorOverlay;
private VisualElement _selectorDialog;
private DistroDatabase _distroDatabase;
private PackageDatabase _packageDatabase;
private FontAsset _monospaceFont;
private GachaService _gachaService;
private PlayerCollection _playerCollection;
private EntropyWallet _wallet;
private Func<IReadOnlyList<DistroDefinition>, PullResolutionResult> _resolvePulledDistros;
private Func<DistroDefinition, PackageDefinition, string> _claimSelectorChoice;
private Action _onChanged;
private Action _requestRootCreditExchange;
private Action<string, int, int> _requestPullCutscene;
private int _selectedBannerIndex;
private int _pendingPullCount;
private int _pendingMissingTokens;
private SelectorStep _selectorStep;
private DistroDefinition _selectorChosenDistro;
private string _selectorResultText;
private bool _hasOpened;
private string _resultBannerId;
private string _resultText;
@@ -45,14 +60,17 @@ namespace KernelPanic.UI
private readonly List<int> _resultRewardStars = new();
private bool _resultRevealed;
public void Bind(VisualElement root, DistroDatabase database, FontAsset artFont, GachaService service, PlayerCollection collection, EntropyWallet entropyWallet, Func<IReadOnlyList<DistroDefinition>, PullResolutionResult> pullResolver, Action changedCallback, Action rootCreditExchangeCallback, Action<string, int, int> pullCutsceneCallback)
public void Bind(VisualElement root, DistroDatabase database, PackageDatabase packageDatabase, FontAsset artFont, GachaService service, PlayerCollection collection, EntropyWallet entropyWallet, Func<IReadOnlyList<DistroDefinition>, PullResolutionResult> pullResolver, Func<DistroDefinition, PackageDefinition, string> claimSelectorChoice, Action changedCallback, Action rootCreditExchangeCallback, Action<string, int, int> pullCutsceneCallback)
{
_root = root;
_distroDatabase = database;
_packageDatabase = packageDatabase;
_monospaceFont = artFont;
_gachaService = service;
_playerCollection = collection;
_wallet = entropyWallet;
_resolvePulledDistros = pullResolver;
_claimSelectorChoice = claimSelectorChoice;
_onChanged = changedCallback;
_requestRootCreditExchange = rootCreditExchangeCallback;
_requestPullCutscene = pullCutsceneCallback;
@@ -149,6 +167,12 @@ namespace KernelPanic.UI
return;
}
if (bannerId == GachaService.StandardBannerId)
{
RenderStandardBanner();
return;
}
RenderFutureBanner(bannerId);
}
@@ -207,6 +231,218 @@ namespace KernelPanic.UI
}
}
private void RenderStandardBanner()
{
GachaBannerState state = _gachaService.StandardState;
_bannerDetail.Add(BuildBannerHero("stable branch"));
Label title = new("stable branch");
title.AddToClassList("gacha-detail-title");
_bannerDetail.Add(title);
_bannerDetail.Add(BuildInfoLine("Entropy", _wallet == null ? "--" : _wallet.Balance.ToString()));
_bannerDetail.Add(BuildInfoLine("Commits", _gachaService.PullTokens.ToString()));
_bannerDetail.Add(BuildInfoLine("pulls", state.totalPulls.ToString()));
_bannerDetail.Add(BuildInfoLine("4-star pity", $"{state.pityCounter}/{GachaService.FourStarHardPity}"));
_bannerDetail.Add(BuildInfoLine("5-star pity", $"{state.fiveStarPityCounter}/{GachaService.FiveStarHardPity}"));
_bannerDetail.Add(BuildInfoLine("5-star selector", $"{_gachaService.GetStandardFiveStarSelectorProgress()}/{GachaService.StandardFiveStarSelectorInterval}"));
_bannerDetail.Add(BuildInfoLine("cost", $"{GachaService.StandardSinglePullCost} Commit / {GachaService.StandardTenPullCost} for ten"));
Label rules = new("Base result is a 3-star package cache. A 4-star package cache or install-media distro is guaranteed every 10 pulls; 5-star odds ramp up starting pull 66 of the pity cycle and are guaranteed by pull 80. Every 200 pulls, choose a standard 5-star character and a standard 5-star package outright.");
rules.AddToClassList("gacha-rules");
_bannerDetail.Add(rules);
VisualElement actions = new();
actions.AddToClassList("gacha-actions");
Button singleButton = new(() => RequestPullSelectedBanner(1))
{
text = $"git pull --single\n{GachaService.StandardSinglePullCost}x {GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)}"
};
singleButton.AddToClassList("terminal-button");
singleButton.AddToClassList("gacha-action-button");
singleButton.SetEnabled(CanAttemptStandardPull(1));
actions.Add(singleButton);
Button tenButton = new(() => RequestPullSelectedBanner(10))
{
text = $"git pull --ten\n{GachaService.StandardTenPullCost}x {GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)}"
};
tenButton.AddToClassList("terminal-button");
tenButton.AddToClassList("gacha-action-button");
tenButton.SetEnabled(CanAttemptStandardPull(10));
actions.Add(tenButton);
_bannerDetail.Add(actions);
if (_pendingPullCount > 0)
{
_bannerDetail.Add(BuildEntropyPrompt());
}
if (_resultBannerId == GachaService.StandardBannerId &&
(_resultRewardLines.Count > 0 || !string.IsNullOrWhiteSpace(_resultText)))
{
_bannerDetail.Add(BuildPullResultReveal());
}
if (_gachaService.PendingStandardFiveStarSelectors > 0)
{
Button claimButton = new(OpenFiveStarSelector)
{
text = $"claim 5★ selector x{_gachaService.PendingStandardFiveStarSelectors}"
};
claimButton.AddToClassList("terminal-button");
claimButton.AddToClassList("gacha-action-button");
_bannerDetail.Add(claimButton);
}
}
private void EnsureSelectorOverlay()
{
if (_root == null || _selectorOverlay != null)
{
return;
}
_selectorOverlay = new VisualElement();
_selectorOverlay.AddToClassList("modal-overlay");
_selectorOverlay.AddToClassList("five-star-selector-overlay");
_selectorOverlay.AddToClassList("hidden");
_selectorDialog = new VisualElement();
_selectorDialog.AddToClassList("five-star-selector-dialog");
_selectorOverlay.Add(_selectorDialog);
_root.Add(_selectorOverlay);
}
private void OpenFiveStarSelector()
{
if (_gachaService.PendingStandardFiveStarSelectors <= 0)
{
return;
}
EnsureSelectorOverlay();
_selectorStep = SelectorStep.Character;
_selectorChosenDistro = null;
_selectorResultText = null;
RenderFiveStarSelectorDialog();
_selectorOverlay.RemoveFromClassList("hidden");
_selectorOverlay.AddToClassList("modal-open");
}
private void CloseFiveStarSelector()
{
_selectorOverlay?.RemoveFromClassList("modal-open");
_selectorOverlay?.AddToClassList("hidden");
_selectorChosenDistro = null;
_selectorResultText = null;
}
private void RenderFiveStarSelectorDialog()
{
if (_selectorDialog == null)
{
return;
}
_selectorDialog.Clear();
VisualElement header = new();
header.AddToClassList("package-equip-header");
string stepTitle = _selectorStep switch
{
SelectorStep.Character => "5-star selector: choose a standard character",
SelectorStep.Equipment => "5-star selector: choose standard equipment",
_ => "5-star selector"
};
Label title = new(stepTitle);
title.AddToClassList("gacha-detail-title");
header.Add(title);
Button close = new(CloseFiveStarSelector) { text = "x", focusable = false };
close.AddToClassList("modal-close-button");
header.Add(close);
_selectorDialog.Add(header);
if (_selectorStep == SelectorStep.Confirm)
{
Label result = new(_selectorResultText);
result.AddToClassList("gacha-result");
_selectorDialog.Add(result);
Button continueButton = new(AdvanceAfterSelectorConfirm) { text = "> continue" };
continueButton.AddToClassList("terminal-button");
continueButton.AddToClassList("gacha-action-button");
_selectorDialog.Add(continueButton);
return;
}
VisualElement grid = new();
grid.AddToClassList("gacha-actions");
_selectorDialog.Add(grid);
if (_selectorStep == SelectorStep.Character)
{
IReadOnlyList<DistroDefinition> choices = _gachaService.GetStandardFiveStarCharacterChoices();
for (int i = 0; i < choices.Count; i++)
{
DistroDefinition choice = choices[i];
Button choiceButton = new(() => ChooseSelectorCharacter(choice))
{
text = $"5★ {DistroPresentation.DisplayName(choice)}"
};
choiceButton.AddToClassList("terminal-button");
choiceButton.AddToClassList("gacha-action-button");
grid.Add(choiceButton);
}
}
else
{
IReadOnlyList<PackageDefinition> choices = _gachaService.GetStandardFiveStarEquipmentChoices(_packageDatabase);
for (int i = 0; i < choices.Count; i++)
{
PackageDefinition choice = choices[i];
string name = string.IsNullOrWhiteSpace(choice.DisplayName) ? choice.Id : choice.DisplayName;
Button choiceButton = new(() => ChooseSelectorEquipment(choice))
{
text = $"5★ {name}"
};
choiceButton.AddToClassList("terminal-button");
choiceButton.AddToClassList("gacha-action-button");
grid.Add(choiceButton);
}
}
}
private void ChooseSelectorCharacter(DistroDefinition distro)
{
_selectorChosenDistro = distro;
_selectorStep = SelectorStep.Equipment;
RenderFiveStarSelectorDialog();
}
private void ChooseSelectorEquipment(PackageDefinition package)
{
_selectorResultText = _claimSelectorChoice?.Invoke(_selectorChosenDistro, package) ?? "selector claim failed";
_selectorStep = SelectorStep.Confirm;
RenderFiveStarSelectorDialog();
}
private void AdvanceAfterSelectorConfirm()
{
if (_gachaService.PendingStandardFiveStarSelectors > 0)
{
_selectorStep = SelectorStep.Character;
_selectorChosenDistro = null;
_selectorResultText = null;
RenderFiveStarSelectorDialog();
return;
}
CloseFiveStarSelector();
RenderSelectedBanner();
}
private void RenderFutureBanner(string bannerId)
{
_bannerDetail.Add(BuildBannerHero(GetBannerTitle(bannerId)));
@@ -238,7 +474,7 @@ namespace KernelPanic.UI
return;
}
if (bannerId != GachaService.BeginnerBannerId)
if (bannerId != GachaService.BeginnerBannerId && bannerId != GachaService.StandardBannerId)
{
_resultBannerId = bannerId;
_resultText = $"git pull origin {bannerId}: remote not implemented yet";
@@ -249,7 +485,9 @@ namespace KernelPanic.UI
}
PullPaymentSource source = entropyTokenCount > 0 ? PullPaymentSource.Entropy : PullPaymentSource.Commits;
GachaPullResult result = _gachaService.PerformBeginnerPull(pullCount, _wallet, source);
GachaPullResult result = bannerId == GachaService.BeginnerBannerId
? _gachaService.PerformBeginnerPull(pullCount, _wallet, source)
: _gachaService.PerformStandardPull(pullCount, _wallet, source);
if (!result.Success)
{
_resultBannerId = bannerId;
@@ -323,6 +561,19 @@ namespace KernelPanic.UI
}
_resultRevealed = true;
int bannerIndex = Array.IndexOf(_bannerIds, result.BannerId);
if (bannerIndex >= 0)
{
_selectedBannerIndex = bannerIndex;
}
_hasOpened = true;
if (result.PendingFiveStarSelectorCount > 0)
{
OpenFiveStarSelector();
}
}
private VisualElement BuildPullResultReveal()
@@ -432,13 +683,13 @@ namespace KernelPanic.UI
private void RequestPullSelectedBanner(int pullCount)
{
string bannerId = _bannerIds[_selectedBannerIndex];
if (bannerId != GachaService.BeginnerBannerId)
if (bannerId != GachaService.BeginnerBannerId && bannerId != GachaService.StandardBannerId)
{
PullSelectedBanner(pullCount, 0);
return;
}
int cost = _gachaService.GetBeginnerPullCost(pullCount);
int cost = _gachaService.GetPullCost(bannerId, pullCount);
int missingTokens = _gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, cost);
if (missingTokens <= 0)
{
@@ -531,6 +782,20 @@ namespace KernelPanic.UI
_gachaService.RootCredits > 0);
}
private bool CanAttemptStandardPull(int pullCount)
{
if (_gachaService.CanAffordPull(pullCount, _wallet, PullPaymentSource.Commits, GachaService.StandardBannerId, out _))
{
return true;
}
int cost = _gachaService.GetPullCost(GachaService.StandardBannerId, pullCount);
int missingTokens = _gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, cost);
return missingTokens > 0 &&
((_wallet != null && _wallet.Balance >= cost * GachaService.EntropyPerCommit) ||
_gachaService.RootCredits > 0);
}
private void ClearEntropyPrompt()
{
_pendingPullCount = 0;
@@ -626,7 +891,7 @@ namespace KernelPanic.UI
return bannerId switch
{
GachaService.BeginnerBannerId => _gachaService.IsBeginnerBannerAvailable ? $"{_gachaService.BeginnerState.totalPulls}/{GachaService.BeginnerMaxPulls}" : "closed",
GachaService.StandardBannerId => "soon",
GachaService.StandardBannerId => _gachaService.PendingStandardFiveStarSelectors > 0 ? "selector ready" : $"{_gachaService.GetStandardFiveStarSelectorProgress()}/{GachaService.StandardFiveStarSelectorInterval}",
GachaService.LimitedBannerId => "soon",
_ => "--"
};
+11 -5
View File
@@ -171,7 +171,9 @@ namespace KernelPanic.UI
}
_commandLabel.text = pending.PullCount == 10 ? "git pull --ten" : "git pull --single";
int pullCost = pending.PullCount == 10 ? GachaService.BeginnerTenPullCost : GachaService.BeginnerSinglePullCost;
int pullCost = pending.BannerId == GachaService.StandardBannerId
? (pending.PullCount == 10 ? GachaService.StandardTenPullCost : GachaService.StandardSinglePullCost)
: (pending.PullCount == 10 ? GachaService.BeginnerTenPullCost : GachaService.BeginnerSinglePullCost);
_costLabel.text = pending.EntropyTokenCount > 0
? $"cost: {pending.EntropyTokenCount * GachaService.EntropyPerCommit} Entropy"
: $"cost: {pullCost} Commits / {pullCost * GachaService.EntropyPerCommit} Entropy";
@@ -253,9 +255,13 @@ namespace KernelPanic.UI
RestoreBannerPool(gacha, saveData, pending.DistroDatabase);
gacha.LoadProgress(saveData);
GachaPullResult result = pending.BannerId == GachaService.BeginnerBannerId
? gacha.PerformBeginnerPull(pending.PullCount, wallet, pending.EntropyTokenCount > 0 ? PullPaymentSource.Entropy : PullPaymentSource.Commits)
: GachaPullResult.Failed(pending.BannerId, "remote not implemented yet");
PullPaymentSource paymentSource = pending.EntropyTokenCount > 0 ? PullPaymentSource.Entropy : PullPaymentSource.Commits;
GachaPullResult result = pending.BannerId switch
{
GachaService.BeginnerBannerId => gacha.PerformBeginnerPull(pending.PullCount, wallet, paymentSource),
GachaService.StandardBannerId => gacha.PerformStandardPull(pending.PullCount, wallet, paymentSource),
_ => GachaPullResult.Failed(pending.BannerId, "remote not implemented yet")
};
if (!result.Success)
{
@@ -310,7 +316,7 @@ namespace KernelPanic.UI
}
WriteState(saveService, saveData, wallet, collection, gacha);
return new CompletedGachaPull(result.BannerId, string.Join(" / ", header), completedRewards);
return new CompletedGachaPull(result.BannerId, string.Join(" / ", header), completedRewards, gacha.PendingStandardFiveStarSelectors);
}
private void BuildRows()
+42 -1
View File
@@ -194,7 +194,7 @@ namespace KernelPanic.UI
FontAsset resolvedFont = ResolveMonospaceFont();
featuredUnitPanel.Bind(_root, resolvedFont, GetBestWaveForUnit, OpenFeaturedUnitDetail);
collectionScreen.Bind(_root, resolvedFont, languageDeckDatabase, cardDatabase, _playerCollection, GetMergesBalance, UpgradeCollectionUnit, packageDatabase, _packageLoadout, SaveCurrentState, () => _saveData, UpgradeCollectionPackage, ScrapCollectionPackage);
gachaScreen.Bind(_root, distroDatabase, resolvedFont, _gachaService, _playerCollection, _wallet, ResolvePulledDistros, HandleMetaStateChanged, OpenRootCreditExchange, StartGachaPullCutscene);
gachaScreen.Bind(_root, distroDatabase, packageDatabase, resolvedFont, _gachaService, _playerCollection, _wallet, ResolvePulledDistros, ClaimStandardFiveStarSelector, HandleMetaStateChanged, OpenRootCreditExchange, StartGachaPullCutscene);
LoadMetaState();
_playerCollection.Changed += HandleMetaStateChanged;
_gachaService.Changed += HandleMetaStateChanged;
@@ -890,6 +890,47 @@ namespace KernelPanic.UI
return PullResolver.Resolve(pulledDistros, context);
}
private string ClaimStandardFiveStarSelector(DistroDefinition distro, PackageDefinition package)
{
if (!_gachaService.TryClaimStandardFiveStarSelector(out string failureReason))
{
return $"selector claim failed: {failureReason}";
}
List<string> parts = new();
if (distro != null)
{
PullResolutionResult resolution = ResolvePulledDistros(new[] { distro });
RefreshFeaturedUnitPanel();
PullResolutionOutcome outcome = resolution != null && resolution.Outcomes.Count > 0 ? resolution.Outcomes[0] : default;
parts.Add(outcome.Kind == PullOutcomeKind.Granted
? $"{DistroPresentation.DisplayName(distro)}: granted"
: $"{DistroPresentation.DisplayName(distro)}: duplicate +{outcome.MergesAwarded} merges");
}
if (package != null)
{
string packageName = string.IsNullOrWhiteSpace(package.DisplayName) ? package.Id : package.DisplayName;
bool duplicate = _playerCollection.IsPackageOwned(package.Id);
if (duplicate)
{
int cache = PackageTuning.GetCacheForRarity(package.Rarity);
_saveData.cacheBalance += cache;
parts.Add($"{packageName}: duplicate +{cache} cache");
}
else
{
_playerCollection.AddPackageSilently(package);
parts.Add($"{packageName}: granted");
}
}
SaveCurrentState();
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
return string.Join(" / ", parts);
}
private void StartGachaPullCutscene(string bannerId, int pullCount, int entropyTokenCount)
{
SaveCurrentState();
+20 -2
View File
@@ -1617,7 +1617,8 @@ Button {
.modal-overlay > .starter-dialog,
.modal-overlay > .root-credit-dialog,
.modal-overlay > .package-equip-dialog {
.modal-overlay > .package-equip-dialog,
.modal-overlay > .five-star-selector-dialog {
scale: 0.94 0.94;
transition-property: scale;
transition-duration: 160ms;
@@ -1625,7 +1626,8 @@ Button {
.modal-overlay.modal-open > .starter-dialog,
.modal-overlay.modal-open > .root-credit-dialog,
.modal-overlay.modal-open > .package-equip-dialog {
.modal-overlay.modal-open > .package-equip-dialog,
.modal-overlay.modal-open > .five-star-selector-dialog {
scale: 1 1;
}
@@ -1676,6 +1678,21 @@ Button {
background-color: var(--kp-bg-solid);
}
.five-star-selector-dialog {
position: relative;
width: 60%;
padding: 22px;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: var(--kp-amber);
border-right-color: var(--kp-amber);
border-top-color: var(--kp-amber);
border-bottom-color: var(--kp-amber);
background-color: var(--kp-bg-solid);
}
.package-equip-header {
flex-direction: row;
align-items: center;
@@ -2001,6 +2018,7 @@ Button {
.reduced-motion .modal-overlay > .starter-dialog,
.reduced-motion .modal-overlay > .root-credit-dialog,
.reduced-motion .modal-overlay > .package-equip-dialog,
.reduced-motion .modal-overlay > .five-star-selector-dialog,
.reduced-motion .command-row,
.reduced-motion .gacha-banner-row,
.reduced-motion .collection-tab,