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 int fiveStarPityCounter;
public bool exhausted; public bool exhausted;
public bool featuredFiveStarGuaranteed; public bool featuredFiveStarGuaranteed;
public int fiveStarSelectorsClaimed;
public List<string> guaranteedDistroIds = new(); public List<string> guaranteedDistroIds = new();
public GachaBannerState() public GachaBannerState()
+3 -1
View File
@@ -64,16 +64,18 @@ namespace KernelPanic.Meta
public sealed class CompletedGachaPull 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; BannerId = string.IsNullOrWhiteSpace(bannerId) ? GachaService.BeginnerBannerId : bannerId;
HeaderText = headerText; HeaderText = headerText;
Rewards = rewards ?? Array.Empty<CompletedGachaReward>(); Rewards = rewards ?? Array.Empty<CompletedGachaReward>();
PendingFiveStarSelectorCount = Math.Max(0, pendingFiveStarSelectorCount);
} }
public string BannerId { get; } public string BannerId { get; }
public string HeaderText { get; } public string HeaderText { get; }
public IReadOnlyList<CompletedGachaReward> Rewards { get; } public IReadOnlyList<CompletedGachaReward> Rewards { get; }
public int PendingFiveStarSelectorCount { get; }
public IReadOnlyList<string> RewardLines public IReadOnlyList<string> RewardLines
{ {
+136 -8
View File
@@ -15,6 +15,9 @@ namespace KernelPanic.Meta
public const int BeginnerMaxPulls = 50; public const int BeginnerMaxPulls = 50;
public const int BeginnerSinglePullCost = 1; public const int BeginnerSinglePullCost = 1;
public const int BeginnerTenPullCost = 8; 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 FourStarHardPity = 10;
public const int FiveStarSoftPityStart = 66; public const int FiveStarSoftPityStart = 66;
public const int FiveStarHardPity = 80; public const int FiveStarHardPity = 80;
@@ -32,6 +35,7 @@ namespace KernelPanic.Meta
private readonly Dictionary<GachaCurrencyType, int> _currencyBalances = new(); private readonly Dictionary<GachaCurrencyType, int> _currencyBalances = new();
private readonly Random _random; private readonly Random _random;
private GachaBannerState _beginnerState = new(BeginnerBannerId); private GachaBannerState _beginnerState = new(BeginnerBannerId);
private GachaBannerState _standardState = new(StandardBannerId);
public GachaService() public GachaService()
: this(new Random()) : this(new Random())
@@ -49,7 +53,9 @@ namespace KernelPanic.Meta
public int RootCredits { get; private set; } public int RootCredits { get; private set; }
public IReadOnlyList<DistroDefinition> BannerPool => _bannerPool; public IReadOnlyList<DistroDefinition> BannerPool => _bannerPool;
public GachaBannerState BeginnerState => _beginnerState; public GachaBannerState BeginnerState => _beginnerState;
public GachaBannerState StandardState => _standardState;
public bool IsBeginnerBannerAvailable => !_beginnerState.exhausted && _beginnerState.totalPulls < BeginnerMaxPulls && _bannerPool.Count > 0; 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 BannerPoolChanged;
public event Action Changed; public event Action Changed;
@@ -74,6 +80,12 @@ namespace KernelPanic.Meta
_beginnerState.fiveStarPityCounter = Math.Max(0, _beginnerState.fiveStarPityCounter); _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(); Changed?.Invoke();
} }
@@ -88,6 +100,7 @@ namespace KernelPanic.Meta
data.rootCredits = RootCredits; data.rootCredits = RootCredits;
data.commitsBalance = GetCurrencyBalance(GachaCurrencyType.StandardPull); data.commitsBalance = GetCurrencyBalance(GachaCurrencyType.StandardPull);
data.beginnerBannerState = CloneState(_beginnerState); data.beginnerBannerState = CloneState(_beginnerState);
data.standardBannerState = CloneState(_standardState);
} }
public int GetCurrencyBalance(GachaCurrencyType currencyType) public int GetCurrencyBalance(GachaCurrencyType currencyType)
@@ -195,6 +208,11 @@ namespace KernelPanic.Meta
} }
public bool CanAffordPull(int pullCount, EntropyWallet wallet, PullPaymentSource source, out string failureReason) 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) if (pullCount != 1 && pullCount != 10)
{ {
@@ -202,6 +220,8 @@ namespace KernelPanic.Meta
return false; return false;
} }
if (bannerId == BeginnerBannerId)
{
if (!IsBeginnerBannerAvailable) if (!IsBeginnerBannerAvailable)
{ {
failureReason = "beginner banner unavailable"; failureReason = "beginner banner unavailable";
@@ -214,8 +234,14 @@ namespace KernelPanic.Meta
failureReason = $"only {remainingPulls} beginner pull(s) remain"; failureReason = $"only {remainingPulls} beginner pull(s) remain";
return false; return false;
} }
}
else if (bannerId != StandardBannerId)
{
failureReason = $"banner '{bannerId}' not implemented yet";
return false;
}
int cost = GetBeginnerPullCost(pullCount); int cost = GetPullCost(bannerId, pullCount);
if (source == PullPaymentSource.Commits && GetCurrencyBalance(GachaCurrencyType.StandardPull) < cost) if (source == PullPaymentSource.Commits && GetCurrencyBalance(GachaCurrencyType.StandardPull) < cost)
{ {
failureReason = $"need {cost} {FormatCurrencyName(GachaCurrencyType.StandardPull)}"; failureReason = $"need {cost} {FormatCurrencyName(GachaCurrencyType.StandardPull)}";
@@ -233,16 +259,36 @@ namespace KernelPanic.Meta
return true; 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) 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; commitsSpent = 0;
entropySpent = 0; entropySpent = 0;
if (!CanAffordPull(pullCount, wallet, source, out failureReason)) if (!CanAffordPull(pullCount, wallet, source, bannerId, out failureReason))
{ {
return false; return false;
} }
int cost = GetBeginnerPullCost(pullCount); int cost = GetPullCost(bannerId, pullCount);
if (source == PullPaymentSource.Commits) if (source == PullPaymentSource.Commits)
{ {
commitsSpent = cost; commitsSpent = cost;
@@ -301,6 +347,82 @@ namespace KernelPanic.Meta
return new GachaPullResult(true, BeginnerBannerId, GachaCurrencyType.StandardPull, commitSpend, entropySpent, rewards, null); 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) public static string FormatCurrencyName(GachaCurrencyType currencyType)
{ {
return currencyType switch return currencyType switch
@@ -372,20 +494,25 @@ namespace KernelPanic.Meta
return RewardGuaranteedBeginnerFiveStar("50-pull beginner guarantee"); 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); return RewardRandomFiveStar(fiveStarPityTriggered);
} }
bool pityTriggered = _beginnerState.pityCounter >= FourStarHardPity - 1; bool pityTriggered = state.pityCounter >= FourStarHardPity - 1;
bool hitFourStarOrCharacter = pityTriggered || _random.NextDouble() < FourStarBaseChance; bool hitFourStarOrCharacter = pityTriggered || _random.NextDouble() < FourStarBaseChance;
if (!hitFourStarOrCharacter) if (!hitFourStarOrCharacter)
{ {
_beginnerState.pityCounter++; state.pityCounter++;
return GachaReward.Package(3, "3-star package", false, false); return GachaReward.Package(3, "3-star package", false, false);
} }
_beginnerState.pityCounter = 0; state.pityCounter = 0;
if (_bannerPool.Count > 0 && _random.NextDouble() < DistroChanceOnFeaturedTier) if (_bannerPool.Count > 0 && _random.NextDouble() < DistroChanceOnFeaturedTier)
{ {
DistroDefinition distro = FindRandomBannerPoolDistroByStars(4); DistroDefinition distro = FindRandomBannerPoolDistroByStars(4);
@@ -560,7 +687,8 @@ namespace KernelPanic.Meta
pityCounter = state.pityCounter, pityCounter = state.pityCounter,
fiveStarPityCounter = state.fiveStarPityCounter, fiveStarPityCounter = state.fiveStarPityCounter,
exhausted = state.exhausted, exhausted = state.exhausted,
featuredFiveStarGuaranteed = state.featuredFiveStarGuaranteed featuredFiveStarGuaranteed = state.featuredFiveStarGuaranteed,
fiveStarSelectorsClaimed = state.fiveStarSelectorsClaimed
}; };
clone.guaranteedDistroIds.AddRange(state.guaranteedDistroIds); clone.guaranteedDistroIds.AddRange(state.guaranteedDistroIds);
return clone; return clone;
+4
View File
@@ -81,6 +81,7 @@ namespace KernelPanic.Meta
public List<string> ownedUnitIds = new(); public List<string> ownedUnitIds = new();
public List<string> bannerPoolIds = new(); public List<string> bannerPoolIds = new();
public GachaBannerState beginnerBannerState = new(GachaService.BeginnerBannerId); public GachaBannerState beginnerBannerState = new(GachaService.BeginnerBannerId);
public GachaBannerState standardBannerState = new(GachaService.StandardBannerId);
public LastRunLoadoutSaveEntry lastRunLoadout = new(); public LastRunLoadoutSaveEntry lastRunLoadout = new();
public string lastRunDistroId; public string lastRunDistroId;
public List<DistroBestWaveSaveEntry> distroBestWaves = new(); public List<DistroBestWaveSaveEntry> distroBestWaves = new();
@@ -109,6 +110,9 @@ namespace KernelPanic.Meta
beginnerBannerState ??= new GachaBannerState(GachaService.BeginnerBannerId); beginnerBannerState ??= new GachaBannerState(GachaService.BeginnerBannerId);
beginnerBannerState.bannerId = GachaService.BeginnerBannerId; beginnerBannerState.bannerId = GachaService.BeginnerBannerId;
beginnerBannerState.EnsureLists(); beginnerBannerState.EnsureLists();
standardBannerState ??= new GachaBannerState(GachaService.StandardBannerId);
standardBannerState.bannerId = GachaService.StandardBannerId;
standardBannerState.EnsureLists();
lastRunLoadout ??= new LastRunLoadoutSaveEntry(); lastRunLoadout ??= new LastRunLoadoutSaveEntry();
lastRunLoadout.EnsureLists(); lastRunLoadout.EnsureLists();
if (string.IsNullOrWhiteSpace(lastRunDistroId) && !string.IsNullOrWhiteSpace(lastRunLoadout.distroId)) 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 const string SelectedClassName = "selected";
private enum SelectorStep
{
Character,
Equipment,
Confirm
}
private readonly List<VisualElement> _bannerRows = new(); private readonly List<VisualElement> _bannerRows = new();
private readonly string[] _bannerIds = private readonly string[] _bannerIds =
{ {
@@ -24,20 +31,28 @@ namespace KernelPanic.UI
GachaService.LimitedBannerId GachaService.LimitedBannerId
}; };
private VisualElement _root;
private VisualElement _bannerList; private VisualElement _bannerList;
private VisualElement _bannerDetail; private VisualElement _bannerDetail;
private VisualElement _selectorOverlay;
private VisualElement _selectorDialog;
private DistroDatabase _distroDatabase; private DistroDatabase _distroDatabase;
private PackageDatabase _packageDatabase;
private FontAsset _monospaceFont; private FontAsset _monospaceFont;
private GachaService _gachaService; private GachaService _gachaService;
private PlayerCollection _playerCollection; private PlayerCollection _playerCollection;
private EntropyWallet _wallet; private EntropyWallet _wallet;
private Func<IReadOnlyList<DistroDefinition>, PullResolutionResult> _resolvePulledDistros; private Func<IReadOnlyList<DistroDefinition>, PullResolutionResult> _resolvePulledDistros;
private Func<DistroDefinition, PackageDefinition, string> _claimSelectorChoice;
private Action _onChanged; private Action _onChanged;
private Action _requestRootCreditExchange; private Action _requestRootCreditExchange;
private Action<string, int, int> _requestPullCutscene; private Action<string, int, int> _requestPullCutscene;
private int _selectedBannerIndex; private int _selectedBannerIndex;
private int _pendingPullCount; private int _pendingPullCount;
private int _pendingMissingTokens; private int _pendingMissingTokens;
private SelectorStep _selectorStep;
private DistroDefinition _selectorChosenDistro;
private string _selectorResultText;
private bool _hasOpened; private bool _hasOpened;
private string _resultBannerId; private string _resultBannerId;
private string _resultText; private string _resultText;
@@ -45,14 +60,17 @@ namespace KernelPanic.UI
private readonly List<int> _resultRewardStars = new(); private readonly List<int> _resultRewardStars = new();
private bool _resultRevealed; 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; _distroDatabase = database;
_packageDatabase = packageDatabase;
_monospaceFont = artFont; _monospaceFont = artFont;
_gachaService = service; _gachaService = service;
_playerCollection = collection; _playerCollection = collection;
_wallet = entropyWallet; _wallet = entropyWallet;
_resolvePulledDistros = pullResolver; _resolvePulledDistros = pullResolver;
_claimSelectorChoice = claimSelectorChoice;
_onChanged = changedCallback; _onChanged = changedCallback;
_requestRootCreditExchange = rootCreditExchangeCallback; _requestRootCreditExchange = rootCreditExchangeCallback;
_requestPullCutscene = pullCutsceneCallback; _requestPullCutscene = pullCutsceneCallback;
@@ -149,6 +167,12 @@ namespace KernelPanic.UI
return; return;
} }
if (bannerId == GachaService.StandardBannerId)
{
RenderStandardBanner();
return;
}
RenderFutureBanner(bannerId); 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) private void RenderFutureBanner(string bannerId)
{ {
_bannerDetail.Add(BuildBannerHero(GetBannerTitle(bannerId))); _bannerDetail.Add(BuildBannerHero(GetBannerTitle(bannerId)));
@@ -238,7 +474,7 @@ namespace KernelPanic.UI
return; return;
} }
if (bannerId != GachaService.BeginnerBannerId) if (bannerId != GachaService.BeginnerBannerId && bannerId != GachaService.StandardBannerId)
{ {
_resultBannerId = bannerId; _resultBannerId = bannerId;
_resultText = $"git pull origin {bannerId}: remote not implemented yet"; _resultText = $"git pull origin {bannerId}: remote not implemented yet";
@@ -249,7 +485,9 @@ namespace KernelPanic.UI
} }
PullPaymentSource source = entropyTokenCount > 0 ? PullPaymentSource.Entropy : PullPaymentSource.Commits; 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) if (!result.Success)
{ {
_resultBannerId = bannerId; _resultBannerId = bannerId;
@@ -323,6 +561,19 @@ namespace KernelPanic.UI
} }
_resultRevealed = true; _resultRevealed = true;
int bannerIndex = Array.IndexOf(_bannerIds, result.BannerId);
if (bannerIndex >= 0)
{
_selectedBannerIndex = bannerIndex;
}
_hasOpened = true;
if (result.PendingFiveStarSelectorCount > 0)
{
OpenFiveStarSelector();
}
} }
private VisualElement BuildPullResultReveal() private VisualElement BuildPullResultReveal()
@@ -432,13 +683,13 @@ namespace KernelPanic.UI
private void RequestPullSelectedBanner(int pullCount) private void RequestPullSelectedBanner(int pullCount)
{ {
string bannerId = _bannerIds[_selectedBannerIndex]; string bannerId = _bannerIds[_selectedBannerIndex];
if (bannerId != GachaService.BeginnerBannerId) if (bannerId != GachaService.BeginnerBannerId && bannerId != GachaService.StandardBannerId)
{ {
PullSelectedBanner(pullCount, 0); PullSelectedBanner(pullCount, 0);
return; return;
} }
int cost = _gachaService.GetBeginnerPullCost(pullCount); int cost = _gachaService.GetPullCost(bannerId, pullCount);
int missingTokens = _gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, cost); int missingTokens = _gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, cost);
if (missingTokens <= 0) if (missingTokens <= 0)
{ {
@@ -531,6 +782,20 @@ namespace KernelPanic.UI
_gachaService.RootCredits > 0); _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() private void ClearEntropyPrompt()
{ {
_pendingPullCount = 0; _pendingPullCount = 0;
@@ -626,7 +891,7 @@ namespace KernelPanic.UI
return bannerId switch return bannerId switch
{ {
GachaService.BeginnerBannerId => _gachaService.IsBeginnerBannerAvailable ? $"{_gachaService.BeginnerState.totalPulls}/{GachaService.BeginnerMaxPulls}" : "closed", 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", 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"; _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 _costLabel.text = pending.EntropyTokenCount > 0
? $"cost: {pending.EntropyTokenCount * GachaService.EntropyPerCommit} Entropy" ? $"cost: {pending.EntropyTokenCount * GachaService.EntropyPerCommit} Entropy"
: $"cost: {pullCost} Commits / {pullCost * GachaService.EntropyPerCommit} Entropy"; : $"cost: {pullCost} Commits / {pullCost * GachaService.EntropyPerCommit} Entropy";
@@ -253,9 +255,13 @@ namespace KernelPanic.UI
RestoreBannerPool(gacha, saveData, pending.DistroDatabase); RestoreBannerPool(gacha, saveData, pending.DistroDatabase);
gacha.LoadProgress(saveData); gacha.LoadProgress(saveData);
GachaPullResult result = pending.BannerId == GachaService.BeginnerBannerId PullPaymentSource paymentSource = pending.EntropyTokenCount > 0 ? PullPaymentSource.Entropy : PullPaymentSource.Commits;
? gacha.PerformBeginnerPull(pending.PullCount, wallet, pending.EntropyTokenCount > 0 ? PullPaymentSource.Entropy : PullPaymentSource.Commits) GachaPullResult result = pending.BannerId switch
: GachaPullResult.Failed(pending.BannerId, "remote not implemented yet"); {
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) if (!result.Success)
{ {
@@ -310,7 +316,7 @@ namespace KernelPanic.UI
} }
WriteState(saveService, saveData, wallet, collection, gacha); 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() private void BuildRows()
+42 -1
View File
@@ -194,7 +194,7 @@ namespace KernelPanic.UI
FontAsset resolvedFont = ResolveMonospaceFont(); FontAsset resolvedFont = ResolveMonospaceFont();
featuredUnitPanel.Bind(_root, resolvedFont, GetBestWaveForUnit, OpenFeaturedUnitDetail); featuredUnitPanel.Bind(_root, resolvedFont, GetBestWaveForUnit, OpenFeaturedUnitDetail);
collectionScreen.Bind(_root, resolvedFont, languageDeckDatabase, cardDatabase, _playerCollection, GetMergesBalance, UpgradeCollectionUnit, packageDatabase, _packageLoadout, SaveCurrentState, () => _saveData, UpgradeCollectionPackage, ScrapCollectionPackage); 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(); LoadMetaState();
_playerCollection.Changed += HandleMetaStateChanged; _playerCollection.Changed += HandleMetaStateChanged;
_gachaService.Changed += HandleMetaStateChanged; _gachaService.Changed += HandleMetaStateChanged;
@@ -890,6 +890,47 @@ namespace KernelPanic.UI
return PullResolver.Resolve(pulledDistros, context); 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) private void StartGachaPullCutscene(string bannerId, int pullCount, int entropyTokenCount)
{ {
SaveCurrentState(); SaveCurrentState();
+20 -2
View File
@@ -1617,7 +1617,8 @@ Button {
.modal-overlay > .starter-dialog, .modal-overlay > .starter-dialog,
.modal-overlay > .root-credit-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; scale: 0.94 0.94;
transition-property: scale; transition-property: scale;
transition-duration: 160ms; transition-duration: 160ms;
@@ -1625,7 +1626,8 @@ Button {
.modal-overlay.modal-open > .starter-dialog, .modal-overlay.modal-open > .starter-dialog,
.modal-overlay.modal-open > .root-credit-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; scale: 1 1;
} }
@@ -1676,6 +1678,21 @@ Button {
background-color: var(--kp-bg-solid); 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 { .package-equip-header {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@@ -2001,6 +2018,7 @@ Button {
.reduced-motion .modal-overlay > .starter-dialog, .reduced-motion .modal-overlay > .starter-dialog,
.reduced-motion .modal-overlay > .root-credit-dialog, .reduced-motion .modal-overlay > .root-credit-dialog,
.reduced-motion .modal-overlay > .package-equip-dialog, .reduced-motion .modal-overlay > .package-equip-dialog,
.reduced-motion .modal-overlay > .five-star-selector-dialog,
.reduced-motion .command-row, .reduced-motion .command-row,
.reduced-motion .gacha-banner-row, .reduced-motion .gacha-banner-row,
.reduced-motion .collection-tab, .reduced-motion .collection-tab,