912 lines
35 KiB
C#
912 lines
35 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using KernelPanic.Data;
|
|
using KernelPanic.Meta;
|
|
using UnityEngine;
|
|
using UnityEngine.TextCore.Text;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace KernelPanic.UI
|
|
{
|
|
/// <summary>
|
|
/// Renders banner selection and pull controls for the gacha screen.
|
|
/// </summary>
|
|
[Serializable]
|
|
public sealed class GachaScreenController
|
|
{
|
|
private const string SelectedClassName = "selected";
|
|
|
|
private enum SelectorStep
|
|
{
|
|
Character,
|
|
Equipment,
|
|
Confirm
|
|
}
|
|
|
|
private readonly List<VisualElement> _bannerRows = new();
|
|
private readonly string[] _bannerIds =
|
|
{
|
|
GachaService.BeginnerBannerId,
|
|
GachaService.StandardBannerId,
|
|
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;
|
|
private readonly List<string> _resultRewardLines = new();
|
|
private readonly List<int> _resultRewardStars = new();
|
|
private bool _resultRevealed;
|
|
|
|
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;
|
|
_bannerList = root.Q<VisualElement>("GachaBannerList");
|
|
_bannerDetail = root.Q<VisualElement>("GachaBannerDetail");
|
|
}
|
|
|
|
public void Open()
|
|
{
|
|
if (!_hasOpened)
|
|
{
|
|
_selectedBannerIndex = 0;
|
|
_hasOpened = true;
|
|
}
|
|
|
|
Refresh();
|
|
}
|
|
|
|
public void Refresh()
|
|
{
|
|
if (_bannerList == null || _bannerDetail == null || _gachaService == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_bannerRows.Clear();
|
|
_bannerList.Clear();
|
|
|
|
for (int i = 0; i < _bannerIds.Length; i++)
|
|
{
|
|
int index = i;
|
|
VisualElement row = BuildBannerRow(_bannerIds[i]);
|
|
row.RegisterCallback<ClickEvent>(_ => SelectBanner(index));
|
|
_bannerList.Add(row);
|
|
_bannerRows.Add(row);
|
|
}
|
|
|
|
_selectedBannerIndex = Mathf.Clamp(_selectedBannerIndex, 0, _bannerRows.Count - 1);
|
|
ApplySelectedBanner();
|
|
}
|
|
|
|
public void HandleKeyDown(KeyDownEvent evt)
|
|
{
|
|
// Pulls are deliberately click-only to avoid accidental currency spend.
|
|
}
|
|
|
|
private VisualElement BuildBannerRow(string bannerId)
|
|
{
|
|
VisualElement row = new();
|
|
row.AddToClassList("gacha-banner-row");
|
|
|
|
row.Add(new Label(">") { name = $"GachaBannerCursor_{bannerId}" });
|
|
row.ElementAt(0).AddToClassList("command-cursor");
|
|
|
|
row.Add(new Label(GetBannerTitle(bannerId)) { name = $"GachaBannerName_{bannerId}" });
|
|
row.ElementAt(1).AddToClassList("command-text");
|
|
|
|
row.Add(new Label(GetBannerStatus(bannerId)) { name = $"GachaBannerStatus_{bannerId}" });
|
|
row.ElementAt(2).AddToClassList("command-description");
|
|
|
|
return row;
|
|
}
|
|
|
|
private void SelectBanner(int index)
|
|
{
|
|
if (_bannerRows.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_selectedBannerIndex = (index + _bannerRows.Count) % _bannerRows.Count;
|
|
ApplySelectedBanner();
|
|
}
|
|
|
|
private void ApplySelectedBanner()
|
|
{
|
|
for (int i = 0; i < _bannerRows.Count; i++)
|
|
{
|
|
bool selected = i == _selectedBannerIndex;
|
|
_bannerRows[i].EnableInClassList(SelectedClassName, selected);
|
|
_bannerRows[i].Q<Label>(className: "command-cursor").visible = selected;
|
|
}
|
|
|
|
RenderSelectedBanner();
|
|
}
|
|
|
|
private void RenderSelectedBanner()
|
|
{
|
|
_bannerDetail.Clear();
|
|
string bannerId = _bannerIds[_selectedBannerIndex];
|
|
if (bannerId == GachaService.BeginnerBannerId)
|
|
{
|
|
RenderBeginnerBanner();
|
|
return;
|
|
}
|
|
|
|
if (bannerId == GachaService.StandardBannerId)
|
|
{
|
|
RenderStandardBanner();
|
|
return;
|
|
}
|
|
|
|
RenderFutureBanner(bannerId);
|
|
}
|
|
|
|
private void RenderBeginnerBanner()
|
|
{
|
|
GachaBannerState state = _gachaService.BeginnerState;
|
|
_bannerDetail.Add(BuildBannerHero("beginner install media"));
|
|
|
|
Label title = new("beginner install media");
|
|
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}/{GachaService.BeginnerMaxPulls}"));
|
|
_bannerDetail.Add(BuildInfoLine("4-star pity", $"{state.pityCounter}/{GachaService.FourStarHardPity}"));
|
|
_bannerDetail.Add(BuildInfoLine("cost", $"1 Commit / {GachaService.EntropyPerCommit} Entropy per pull"));
|
|
_bannerDetail.Add(BuildInfoLine("guarantees", FormatBeginnerGuarantees()));
|
|
|
|
Label rules = new("Base result is a 3-star package cache. A 4-star package cache or starter distro can appear early, with one 4-star+ result guaranteed every 10 pulls.");
|
|
rules.AddToClassList("gacha-rules");
|
|
_bannerDetail.Add(rules);
|
|
|
|
VisualElement actions = new();
|
|
actions.AddToClassList("gacha-actions");
|
|
|
|
Button singleButton = new(() => RequestPullSelectedBanner(1))
|
|
{
|
|
text = $"git pull --single\n1x {GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)}"
|
|
};
|
|
singleButton.AddToClassList("terminal-button");
|
|
singleButton.AddToClassList("gacha-action-button");
|
|
singleButton.SetEnabled(CanAttemptBeginnerPull(1));
|
|
actions.Add(singleButton);
|
|
|
|
Button tenButton = new(() => RequestPullSelectedBanner(10))
|
|
{
|
|
text = $"git pull --ten\n{_gachaService.GetBeginnerPullCost(10)}x {GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)}"
|
|
};
|
|
tenButton.AddToClassList("terminal-button");
|
|
tenButton.AddToClassList("gacha-action-button");
|
|
tenButton.SetEnabled(CanAttemptBeginnerPull(10));
|
|
actions.Add(tenButton);
|
|
|
|
_bannerDetail.Add(actions);
|
|
|
|
if (_pendingPullCount > 0)
|
|
{
|
|
_bannerDetail.Add(BuildEntropyPrompt());
|
|
}
|
|
|
|
if (_resultBannerId == GachaService.BeginnerBannerId &&
|
|
(_resultRewardLines.Count > 0 || !string.IsNullOrWhiteSpace(_resultText)))
|
|
{
|
|
_bannerDetail.Add(BuildPullResultReveal());
|
|
}
|
|
}
|
|
|
|
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)));
|
|
|
|
Label title = new(GetBannerTitle(bannerId));
|
|
title.AddToClassList("gacha-detail-title");
|
|
_bannerDetail.Add(title);
|
|
|
|
_bannerDetail.Add(BuildInfoLine("pull currency", $"Commits or Entropy ({GachaService.EntropyPerCommit}:1)"));
|
|
_bannerDetail.Add(BuildInfoLine("status", "not implemented yet"));
|
|
_bannerDetail.Add(BuildInfoLine("pity", "own counter, 10-pull 4-star+ guarantee"));
|
|
_bannerDetail.Add(new Label("Use the beginner implementation as the template: define the banner pool, persist a separate GachaBannerState, then route single/ten pulls through GachaService.") { name = "FutureBannerHint" });
|
|
_bannerDetail.ElementAt(_bannerDetail.childCount - 1).AddToClassList("gacha-rules");
|
|
|
|
if (_resultBannerId == bannerId && !string.IsNullOrWhiteSpace(_resultText))
|
|
{
|
|
Label result = new(_resultText);
|
|
result.AddToClassList("gacha-result");
|
|
_bannerDetail.Add(result);
|
|
}
|
|
}
|
|
|
|
private void PullSelectedBanner(int pullCount, int entropyTokenCount)
|
|
{
|
|
string bannerId = _bannerIds[_selectedBannerIndex];
|
|
if (_requestPullCutscene != null)
|
|
{
|
|
_requestPullCutscene.Invoke(bannerId, pullCount, entropyTokenCount);
|
|
return;
|
|
}
|
|
|
|
if (bannerId != GachaService.BeginnerBannerId && bannerId != GachaService.StandardBannerId)
|
|
{
|
|
_resultBannerId = bannerId;
|
|
_resultText = $"git pull origin {bannerId}: remote not implemented yet";
|
|
_resultRewardLines.Clear();
|
|
_resultRewardStars.Clear();
|
|
RenderSelectedBanner();
|
|
return;
|
|
}
|
|
|
|
PullPaymentSource source = entropyTokenCount > 0 ? PullPaymentSource.Entropy : PullPaymentSource.Commits;
|
|
GachaPullResult result = bannerId == GachaService.BeginnerBannerId
|
|
? _gachaService.PerformBeginnerPull(pullCount, _wallet, source)
|
|
: _gachaService.PerformStandardPull(pullCount, _wallet, source);
|
|
if (!result.Success)
|
|
{
|
|
_resultBannerId = bannerId;
|
|
_resultText = $"git pull failed: {result.FailureReason}";
|
|
_resultRewardLines.Clear();
|
|
_resultRewardStars.Clear();
|
|
RenderSelectedBanner();
|
|
return;
|
|
}
|
|
|
|
List<string> header = new();
|
|
header.Add(result.EntropySpent > 0
|
|
? $"git pull complete: spent {result.EntropySpent} Entropy"
|
|
: $"git pull complete: spent {result.CurrencySpent} {GachaService.FormatCurrencyName(result.CurrencyType)}");
|
|
|
|
List<DistroDefinition> distroRewards = new();
|
|
for (int i = 0; i < result.Rewards.Count; i++)
|
|
{
|
|
if (result.Rewards[i].Distro != null)
|
|
{
|
|
distroRewards.Add(result.Rewards[i].Distro);
|
|
}
|
|
}
|
|
|
|
PullResolutionResult resolution = _resolvePulledDistros?.Invoke(distroRewards);
|
|
int distroOutcomeIndex = 0;
|
|
_resultRewardLines.Clear();
|
|
_resultRewardStars.Clear();
|
|
for (int i = 0; i < result.Rewards.Count; i++)
|
|
{
|
|
GachaReward reward = result.Rewards[i];
|
|
PullResolutionOutcome? outcome = null;
|
|
if (reward.Distro != null && resolution != null && distroOutcomeIndex < resolution.Outcomes.Count)
|
|
{
|
|
outcome = resolution.Outcomes[distroOutcomeIndex];
|
|
distroOutcomeIndex++;
|
|
}
|
|
|
|
string suffix = reward.Guaranteed ? " guaranteed" : reward.PityTriggered ? " pity" : string.Empty;
|
|
string outcomeText = FormatPullOutcome(outcome);
|
|
_resultRewardLines.Add($"{i + 1:00}: {reward.StarRating}★ {FormatRewardDisplayName(reward)}{suffix}{outcomeText}");
|
|
_resultRewardStars.Add(reward.StarRating);
|
|
}
|
|
|
|
_resultBannerId = bannerId;
|
|
_resultText = string.Join("\n", header);
|
|
_resultRevealed = false;
|
|
_onChanged?.Invoke();
|
|
Refresh();
|
|
}
|
|
|
|
public void SetCompletedPullResult(CompletedGachaPull result)
|
|
{
|
|
if (result == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_resultBannerId = result.BannerId;
|
|
_resultText = result.HeaderText;
|
|
_resultRewardLines.Clear();
|
|
_resultRewardStars.Clear();
|
|
for (int i = 0; i < result.RewardLines.Count; i++)
|
|
{
|
|
_resultRewardLines.Add(result.RewardLines[i]);
|
|
}
|
|
|
|
for (int i = 0; i < result.RewardStars.Count; i++)
|
|
{
|
|
_resultRewardStars.Add(result.RewardStars[i]);
|
|
}
|
|
|
|
_resultRevealed = true;
|
|
|
|
int bannerIndex = Array.IndexOf(_bannerIds, result.BannerId);
|
|
if (bannerIndex >= 0)
|
|
{
|
|
_selectedBannerIndex = bannerIndex;
|
|
}
|
|
|
|
_hasOpened = true;
|
|
|
|
if (result.PendingFiveStarSelectorCount > 0)
|
|
{
|
|
OpenFiveStarSelector();
|
|
}
|
|
}
|
|
|
|
private VisualElement BuildPullResultReveal()
|
|
{
|
|
VisualElement container = new();
|
|
container.AddToClassList("gacha-result-block");
|
|
|
|
if (!string.IsNullOrWhiteSpace(_resultText))
|
|
{
|
|
Label header = new(_resultText);
|
|
header.AddToClassList("gacha-result");
|
|
container.Add(header);
|
|
}
|
|
|
|
if (_resultRewardLines.Count == 0)
|
|
{
|
|
return container;
|
|
}
|
|
|
|
VisualElement list = new();
|
|
list.AddToClassList("gacha-result-list");
|
|
container.Add(list);
|
|
|
|
bool animate = !_resultRevealed && !UIPreferences.ReducedMotion;
|
|
for (int i = 0; i < _resultRewardLines.Count; i++)
|
|
{
|
|
int stars = _resultRewardStars[i];
|
|
VisualElement card = new();
|
|
card.AddToClassList("gacha-result-card");
|
|
card.AddToClassList(RarityClassForStars(stars));
|
|
card.Add(new Label(_resultRewardLines[i]));
|
|
|
|
if (!animate)
|
|
{
|
|
card.AddToClassList("gacha-result-card-shown");
|
|
if (stars >= 5)
|
|
{
|
|
card.AddToClassList("gacha-result-card-flash");
|
|
}
|
|
|
|
list.Add(card);
|
|
continue;
|
|
}
|
|
|
|
card.AddToClassList("gacha-result-card-hidden");
|
|
list.Add(card);
|
|
// Scheduled off _bannerDetail (already attached to the panel) rather than the
|
|
// freshly created card, which has no panel to tick a scheduler on until this
|
|
// whole tree is attached by the caller.
|
|
_bannerDetail.schedule.Execute(() =>
|
|
{
|
|
card.RemoveFromClassList("gacha-result-card-hidden");
|
|
card.AddToClassList("gacha-result-card-shown");
|
|
if (stars >= 5)
|
|
{
|
|
card.AddToClassList("gacha-result-card-flash");
|
|
}
|
|
}).StartingIn(90 * i + 60);
|
|
}
|
|
|
|
_resultRevealed = true;
|
|
return container;
|
|
}
|
|
|
|
private static string RarityClassForStars(int stars)
|
|
{
|
|
return stars switch
|
|
{
|
|
>= 5 => "gacha-result-card-legendary",
|
|
4 => "gacha-result-card-rare",
|
|
_ => "gacha-result-card-common"
|
|
};
|
|
}
|
|
|
|
private static string FormatPullOutcome(PullResolutionOutcome? outcome)
|
|
{
|
|
if (!outcome.HasValue)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
PullResolutionOutcome value = outcome.Value;
|
|
return value.Kind switch
|
|
{
|
|
PullOutcomeKind.Granted => " granted",
|
|
PullOutcomeKind.Dupe => $" duplicate +{value.MergesAwarded} {value.UnitId} merges",
|
|
PullOutcomeKind.DupeOverflow => $" max-version duplicate +{value.MergesAwarded} {value.UnitId} merges",
|
|
_ => string.Empty
|
|
};
|
|
}
|
|
|
|
private static string FormatRewardDisplayName(GachaReward reward)
|
|
{
|
|
if (reward.RewardType != GachaRewardType.Package || string.IsNullOrWhiteSpace(reward.DisplayName))
|
|
{
|
|
return reward.DisplayName;
|
|
}
|
|
|
|
string trimmed = reward.DisplayName.Trim();
|
|
return trimmed.StartsWith("3-star ", StringComparison.OrdinalIgnoreCase) ||
|
|
trimmed.StartsWith("4-star ", StringComparison.OrdinalIgnoreCase) ||
|
|
trimmed.StartsWith("5-star ", StringComparison.OrdinalIgnoreCase)
|
|
? trimmed.Substring(7)
|
|
: trimmed;
|
|
}
|
|
|
|
private void RequestPullSelectedBanner(int pullCount)
|
|
{
|
|
string bannerId = _bannerIds[_selectedBannerIndex];
|
|
if (bannerId != GachaService.BeginnerBannerId && bannerId != GachaService.StandardBannerId)
|
|
{
|
|
PullSelectedBanner(pullCount, 0);
|
|
return;
|
|
}
|
|
|
|
int cost = _gachaService.GetPullCost(bannerId, pullCount);
|
|
int missingTokens = _gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, cost);
|
|
if (missingTokens <= 0)
|
|
{
|
|
ClearEntropyPrompt();
|
|
PullSelectedBanner(pullCount, 0);
|
|
return;
|
|
}
|
|
|
|
int entropyCost = cost * GachaService.EntropyPerCommit;
|
|
if (_wallet == null || _wallet.Balance < entropyCost)
|
|
{
|
|
if (_gachaService.RootCredits > 0)
|
|
{
|
|
_requestRootCreditExchange?.Invoke();
|
|
return;
|
|
}
|
|
|
|
_resultBannerId = bannerId;
|
|
_resultText = $"git pull failed: need {cost} {GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)} or {entropyCost} Entropy";
|
|
RenderSelectedBanner();
|
|
return;
|
|
}
|
|
|
|
_pendingPullCount = pullCount;
|
|
_pendingMissingTokens = cost;
|
|
_resultBannerId = bannerId;
|
|
_resultText = null;
|
|
RenderSelectedBanner();
|
|
}
|
|
|
|
private VisualElement BuildEntropyPrompt()
|
|
{
|
|
VisualElement prompt = new();
|
|
prompt.AddToClassList("gacha-entropy-prompt");
|
|
|
|
int entropyCost = _pendingMissingTokens * GachaService.EntropyPerCommit;
|
|
Label copy = new($"spend {entropyCost} Entropy instead of {_pendingMissingTokens} Commits?");
|
|
copy.AddToClassList("gacha-result");
|
|
prompt.Add(copy);
|
|
|
|
VisualElement actions = new();
|
|
actions.AddToClassList("gacha-actions");
|
|
|
|
Button cancel = new(() =>
|
|
{
|
|
ClearEntropyPrompt();
|
|
RenderSelectedBanner();
|
|
})
|
|
{
|
|
text = "git pull --abort"
|
|
};
|
|
cancel.AddToClassList("terminal-button");
|
|
cancel.AddToClassList("gacha-action-button");
|
|
actions.Add(cancel);
|
|
|
|
Button confirm = new(() =>
|
|
{
|
|
int pullCount = _pendingPullCount;
|
|
int entropyPulls = _pendingMissingTokens;
|
|
ClearEntropyPrompt();
|
|
PullSelectedBanner(pullCount, entropyPulls);
|
|
})
|
|
{
|
|
text = $"git pull --use-entropy={entropyCost}"
|
|
};
|
|
confirm.AddToClassList("terminal-button");
|
|
confirm.AddToClassList("gacha-action-button");
|
|
actions.Add(confirm);
|
|
|
|
prompt.Add(actions);
|
|
return prompt;
|
|
}
|
|
|
|
private bool CanAttemptBeginnerPull(int pullCount)
|
|
{
|
|
if (_gachaService.CanPullBeginner(pullCount, out _))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!_gachaService.IsBeginnerBannerAvailable || pullCount > GachaService.BeginnerMaxPulls - _gachaService.BeginnerState.totalPulls)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int cost = _gachaService.GetBeginnerPullCost(pullCount);
|
|
int missingTokens = _gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, cost);
|
|
return missingTokens > 0 &&
|
|
((_wallet != null && _wallet.Balance >= cost * GachaService.EntropyPerCommit) ||
|
|
_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;
|
|
_pendingMissingTokens = 0;
|
|
}
|
|
|
|
private VisualElement BuildBannerHero(string bannerTitle)
|
|
{
|
|
VisualElement hero = new();
|
|
hero.AddToClassList("gacha-hero");
|
|
|
|
VisualElement artRow = new();
|
|
artRow.AddToClassList("gacha-hero-art-row");
|
|
|
|
IReadOnlyList<DistroDefinition> distros = _distroDatabase == null ? Array.Empty<DistroDefinition>() : _distroDatabase.AllDistros;
|
|
int artCount = Mathf.Min(3, distros.Count);
|
|
for (int i = 0; i < artCount; i++)
|
|
{
|
|
artRow.Add(BuildDistroArtCard(distros[i]));
|
|
}
|
|
|
|
if (artCount == 0)
|
|
{
|
|
Label empty = new("ascii art unavailable");
|
|
empty.AddToClassList("gacha-rules");
|
|
artRow.Add(empty);
|
|
}
|
|
|
|
Label title = new(bannerTitle);
|
|
title.AddToClassList("gacha-hero-title");
|
|
|
|
hero.Add(artRow);
|
|
hero.Add(title);
|
|
return hero;
|
|
}
|
|
|
|
private VisualElement BuildDistroArtCard(DistroDefinition distro)
|
|
{
|
|
VisualElement card = new();
|
|
card.AddToClassList("gacha-art-card");
|
|
|
|
VisualElement artShell = new();
|
|
artShell.AddToClassList("gacha-art-shell");
|
|
|
|
Label artLabel = new();
|
|
DistroArtPresenter.ConfigureArtLabel(artLabel, _monospaceFont);
|
|
artLabel.AddToClassList("gacha-ascii-art");
|
|
VisualElement placeholder = DistroArtPresenter.CreatePlaceholder();
|
|
placeholder.AddToClassList("gacha-ascii-placeholder");
|
|
AsciiArtFitter artFitter = new(artLabel, _monospaceFont);
|
|
artFitter.SetArt(DistroArtPresenter.Render(artLabel, placeholder, distro));
|
|
if (distro != null)
|
|
{
|
|
artLabel.style.color = new StyleColor(distro.AccentColor);
|
|
}
|
|
|
|
artShell.Add(placeholder);
|
|
artShell.Add(artLabel);
|
|
card.Add(artShell);
|
|
|
|
Label name = new(distro == null ? "--" : DistroPresentation.DisplayName(distro));
|
|
name.AddToClassList("gacha-art-name");
|
|
if (distro != null)
|
|
{
|
|
name.style.color = new StyleColor(distro.AccentColor);
|
|
}
|
|
|
|
card.Add(name);
|
|
return card;
|
|
}
|
|
|
|
private string FormatBeginnerGuarantees()
|
|
{
|
|
IReadOnlyList<string> ids = _gachaService.BeginnerState.guaranteedDistroIds;
|
|
string first = ids.Count > 0 ? ids[0] : "--";
|
|
string second = ids.Count > 1 ? ids[1] : "--";
|
|
return $"20={first}, 40={second}, 50=guaranteed 5-star";
|
|
}
|
|
|
|
private static VisualElement BuildInfoLine(string key, string value)
|
|
{
|
|
VisualElement row = new();
|
|
row.AddToClassList("kv-row");
|
|
row.Add(new Label(key) { name = $"Gacha{key}Key" });
|
|
row.ElementAt(0).AddToClassList("kv-key");
|
|
row.Add(new Label(value) { name = $"Gacha{key}Value" });
|
|
row.ElementAt(1).AddToClassList("kv-value");
|
|
return row;
|
|
}
|
|
|
|
private string GetBannerStatus(string bannerId)
|
|
{
|
|
return bannerId switch
|
|
{
|
|
GachaService.BeginnerBannerId => _gachaService.IsBeginnerBannerAvailable ? $"{_gachaService.BeginnerState.totalPulls}/{GachaService.BeginnerMaxPulls}" : "closed",
|
|
GachaService.StandardBannerId => _gachaService.PendingStandardFiveStarSelectors > 0 ? "selector ready" : $"{_gachaService.GetStandardFiveStarSelectorProgress()}/{GachaService.StandardFiveStarSelectorInterval}",
|
|
GachaService.LimitedBannerId => "soon",
|
|
_ => "--"
|
|
};
|
|
}
|
|
|
|
private static string GetBannerTitle(string bannerId)
|
|
{
|
|
return bannerId switch
|
|
{
|
|
GachaService.BeginnerBannerId => "beginner install media",
|
|
GachaService.StandardBannerId => "stable branch",
|
|
GachaService.LimitedBannerId => "feature branch",
|
|
_ => bannerId
|
|
};
|
|
}
|
|
}
|
|
}
|