diff --git a/Assets/Scripts/Meta/GachaBannerState.cs b/Assets/Scripts/Meta/GachaBannerState.cs index 567191a..f3ca6cb 100644 --- a/Assets/Scripts/Meta/GachaBannerState.cs +++ b/Assets/Scripts/Meta/GachaBannerState.cs @@ -15,6 +15,7 @@ namespace KernelPanic.Meta public int fiveStarPityCounter; public bool exhausted; public bool featuredFiveStarGuaranteed; + public int fiveStarSelectorsClaimed; public List guaranteedDistroIds = new(); public GachaBannerState() diff --git a/Assets/Scripts/Meta/GachaPullContext.cs b/Assets/Scripts/Meta/GachaPullContext.cs index 6c93b9c..bbd36f1 100644 --- a/Assets/Scripts/Meta/GachaPullContext.cs +++ b/Assets/Scripts/Meta/GachaPullContext.cs @@ -64,16 +64,18 @@ namespace KernelPanic.Meta public sealed class CompletedGachaPull { - public CompletedGachaPull(string bannerId, string headerText, IReadOnlyList rewards) + public CompletedGachaPull(string bannerId, string headerText, IReadOnlyList rewards, int pendingFiveStarSelectorCount = 0) { BannerId = string.IsNullOrWhiteSpace(bannerId) ? GachaService.BeginnerBannerId : bannerId; HeaderText = headerText; Rewards = rewards ?? Array.Empty(); + PendingFiveStarSelectorCount = Math.Max(0, pendingFiveStarSelectorCount); } public string BannerId { get; } public string HeaderText { get; } public IReadOnlyList Rewards { get; } + public int PendingFiveStarSelectorCount { get; } public IReadOnlyList RewardLines { diff --git a/Assets/Scripts/Meta/GachaService.cs b/Assets/Scripts/Meta/GachaService.cs index 39369ea..c0288e1 100644 --- a/Assets/Scripts/Meta/GachaService.cs +++ b/Assets/Scripts/Meta/GachaService.cs @@ -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 _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 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 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 GetStandardFiveStarCharacterChoices() + { + List 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 GetStandardFiveStarEquipmentChoices(PackageDatabase packageDatabase) + { + List choices = new(); + if (packageDatabase != null) + { + IReadOnlyList 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; diff --git a/Assets/Scripts/Meta/SaveService.cs b/Assets/Scripts/Meta/SaveService.cs index 7e02e13..15d6984 100644 --- a/Assets/Scripts/Meta/SaveService.cs +++ b/Assets/Scripts/Meta/SaveService.cs @@ -81,6 +81,7 @@ namespace KernelPanic.Meta public List ownedUnitIds = new(); public List bannerPoolIds = new(); public GachaBannerState beginnerBannerState = new(GachaService.BeginnerBannerId); + public GachaBannerState standardBannerState = new(GachaService.StandardBannerId); public LastRunLoadoutSaveEntry lastRunLoadout = new(); public string lastRunDistroId; public List 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)) diff --git a/Assets/Scripts/UI/GachaScreenController.cs b/Assets/Scripts/UI/GachaScreenController.cs index a7c7ff6..76a56b1 100644 --- a/Assets/Scripts/UI/GachaScreenController.cs +++ b/Assets/Scripts/UI/GachaScreenController.cs @@ -16,6 +16,13 @@ namespace KernelPanic.UI { private const string SelectedClassName = "selected"; + private enum SelectorStep + { + Character, + Equipment, + Confirm + } + private readonly List _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, PullResolutionResult> _resolvePulledDistros; + private Func _claimSelectorChoice; private Action _onChanged; private Action _requestRootCreditExchange; private Action _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 _resultRewardStars = new(); private bool _resultRevealed; - public void Bind(VisualElement root, DistroDatabase database, FontAsset artFont, GachaService service, PlayerCollection collection, EntropyWallet entropyWallet, Func, PullResolutionResult> pullResolver, Action changedCallback, Action rootCreditExchangeCallback, Action pullCutsceneCallback) + public void Bind(VisualElement root, DistroDatabase database, PackageDatabase packageDatabase, FontAsset artFont, GachaService service, PlayerCollection collection, EntropyWallet entropyWallet, Func, PullResolutionResult> pullResolver, Func claimSelectorChoice, Action changedCallback, Action rootCreditExchangeCallback, Action 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 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 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", _ => "--" }; diff --git a/Assets/Scripts/UI/GamblingSceneController.cs b/Assets/Scripts/UI/GamblingSceneController.cs index dedaa0c..99586cb 100644 --- a/Assets/Scripts/UI/GamblingSceneController.cs +++ b/Assets/Scripts/UI/GamblingSceneController.cs @@ -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() diff --git a/Assets/Scripts/UI/MainMenuController.cs b/Assets/Scripts/UI/MainMenuController.cs index eaf2d42..e53066b 100644 --- a/Assets/Scripts/UI/MainMenuController.cs +++ b/Assets/Scripts/UI/MainMenuController.cs @@ -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 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(); diff --git a/Assets/UI/MainMenu.uss b/Assets/UI/MainMenu.uss index 09d92e6..37746a6 100644 --- a/Assets/UI/MainMenu.uss +++ b/Assets/UI/MainMenu.uss @@ -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,