Added Standard Banner with a 5-star selector

This commit is contained in:
2026-07-10 11:02:27 +02:00
parent e3ea42c93e
commit ceab7dea5a
8 changed files with 497 additions and 32 deletions
+1
View File
@@ -15,6 +15,7 @@ namespace KernelPanic.Meta
public int fiveStarPityCounter;
public bool exhausted;
public bool featuredFiveStarGuaranteed;
public int fiveStarSelectorsClaimed;
public List<string> guaranteedDistroIds = new();
public GachaBannerState()
+3 -1
View File
@@ -64,16 +64,18 @@ namespace KernelPanic.Meta
public sealed class CompletedGachaPull
{
public CompletedGachaPull(string bannerId, string headerText, IReadOnlyList<CompletedGachaReward> rewards)
public CompletedGachaPull(string bannerId, string headerText, IReadOnlyList<CompletedGachaReward> rewards, int pendingFiveStarSelectorCount = 0)
{
BannerId = string.IsNullOrWhiteSpace(bannerId) ? GachaService.BeginnerBannerId : bannerId;
HeaderText = headerText;
Rewards = rewards ?? Array.Empty<CompletedGachaReward>();
PendingFiveStarSelectorCount = Math.Max(0, pendingFiveStarSelectorCount);
}
public string BannerId { get; }
public string HeaderText { get; }
public IReadOnlyList<CompletedGachaReward> Rewards { get; }
public int PendingFiveStarSelectorCount { get; }
public IReadOnlyList<string> RewardLines
{
+145 -17
View File
@@ -15,6 +15,9 @@ namespace KernelPanic.Meta
public const int BeginnerMaxPulls = 50;
public const int BeginnerSinglePullCost = 1;
public const int BeginnerTenPullCost = 8;
public const int StandardSinglePullCost = 1;
public const int StandardTenPullCost = 10;
public const int StandardFiveStarSelectorInterval = 200;
public const int FourStarHardPity = 10;
public const int FiveStarSoftPityStart = 66;
public const int FiveStarHardPity = 80;
@@ -32,6 +35,7 @@ namespace KernelPanic.Meta
private readonly Dictionary<GachaCurrencyType, int> _currencyBalances = new();
private readonly Random _random;
private GachaBannerState _beginnerState = new(BeginnerBannerId);
private GachaBannerState _standardState = new(StandardBannerId);
public GachaService()
: this(new Random())
@@ -49,7 +53,9 @@ namespace KernelPanic.Meta
public int RootCredits { get; private set; }
public IReadOnlyList<DistroDefinition> BannerPool => _bannerPool;
public GachaBannerState BeginnerState => _beginnerState;
public GachaBannerState StandardState => _standardState;
public bool IsBeginnerBannerAvailable => !_beginnerState.exhausted && _beginnerState.totalPulls < BeginnerMaxPulls && _bannerPool.Count > 0;
public int PendingStandardFiveStarSelectors => Math.Max(0, _standardState.totalPulls / StandardFiveStarSelectorInterval - _standardState.fiveStarSelectorsClaimed);
public event Action BannerPoolChanged;
public event Action Changed;
@@ -74,6 +80,12 @@ namespace KernelPanic.Meta
_beginnerState.fiveStarPityCounter = Math.Max(0, _beginnerState.fiveStarPityCounter);
_standardState = data.standardBannerState ?? new GachaBannerState(StandardBannerId);
_standardState.bannerId = StandardBannerId;
_standardState.EnsureLists();
_standardState.fiveStarPityCounter = Math.Max(0, _standardState.fiveStarPityCounter);
_standardState.fiveStarSelectorsClaimed = Math.Max(0, _standardState.fiveStarSelectorsClaimed);
Changed?.Invoke();
}
@@ -88,6 +100,7 @@ namespace KernelPanic.Meta
data.rootCredits = RootCredits;
data.commitsBalance = GetCurrencyBalance(GachaCurrencyType.StandardPull);
data.beginnerBannerState = CloneState(_beginnerState);
data.standardBannerState = CloneState(_standardState);
}
public int GetCurrencyBalance(GachaCurrencyType currencyType)
@@ -195,6 +208,11 @@ namespace KernelPanic.Meta
}
public bool CanAffordPull(int pullCount, EntropyWallet wallet, PullPaymentSource source, out string failureReason)
{
return CanAffordPull(pullCount, wallet, source, BeginnerBannerId, out failureReason);
}
public bool CanAffordPull(int pullCount, EntropyWallet wallet, PullPaymentSource source, string bannerId, out string failureReason)
{
if (pullCount != 1 && pullCount != 10)
{
@@ -202,20 +220,28 @@ namespace KernelPanic.Meta
return false;
}
if (!IsBeginnerBannerAvailable)
if (bannerId == BeginnerBannerId)
{
failureReason = "beginner banner unavailable";
if (!IsBeginnerBannerAvailable)
{
failureReason = "beginner banner unavailable";
return false;
}
int remainingPulls = BeginnerMaxPulls - _beginnerState.totalPulls;
if (pullCount > remainingPulls)
{
failureReason = $"only {remainingPulls} beginner pull(s) remain";
return false;
}
}
else if (bannerId != StandardBannerId)
{
failureReason = $"banner '{bannerId}' not implemented yet";
return false;
}
int remainingPulls = BeginnerMaxPulls - _beginnerState.totalPulls;
if (pullCount > remainingPulls)
{
failureReason = $"only {remainingPulls} beginner pull(s) remain";
return false;
}
int cost = GetBeginnerPullCost(pullCount);
int cost = GetPullCost(bannerId, pullCount);
if (source == PullPaymentSource.Commits && GetCurrencyBalance(GachaCurrencyType.StandardPull) < cost)
{
failureReason = $"need {cost} {FormatCurrencyName(GachaCurrencyType.StandardPull)}";
@@ -233,16 +259,36 @@ namespace KernelPanic.Meta
return true;
}
public int GetPullCost(string bannerId, int pullCount)
{
if (bannerId == BeginnerBannerId)
{
return GetBeginnerPullCost(pullCount);
}
if (bannerId == StandardBannerId)
{
return pullCount == 10 ? StandardTenPullCost : StandardSinglePullCost;
}
return pullCount;
}
public bool PayForPull(int pullCount, EntropyWallet wallet, PullPaymentSource source, out int commitsSpent, out int entropySpent, out string failureReason)
{
return PayForPull(pullCount, wallet, source, BeginnerBannerId, out commitsSpent, out entropySpent, out failureReason);
}
public bool PayForPull(int pullCount, EntropyWallet wallet, PullPaymentSource source, string bannerId, out int commitsSpent, out int entropySpent, out string failureReason)
{
commitsSpent = 0;
entropySpent = 0;
if (!CanAffordPull(pullCount, wallet, source, out failureReason))
if (!CanAffordPull(pullCount, wallet, source, bannerId, out failureReason))
{
return false;
}
int cost = GetBeginnerPullCost(pullCount);
int cost = GetPullCost(bannerId, pullCount);
if (source == PullPaymentSource.Commits)
{
commitsSpent = cost;
@@ -301,6 +347,82 @@ namespace KernelPanic.Meta
return new GachaPullResult(true, BeginnerBannerId, GachaCurrencyType.StandardPull, commitSpend, entropySpent, rewards, null);
}
public GachaPullResult PerformStandardPull(int pullCount, EntropyWallet wallet, PullPaymentSource source)
{
if (!CanAffordPull(pullCount, wallet, source, StandardBannerId, out string failureReason))
{
return GachaPullResult.Failed(StandardBannerId, failureReason);
}
if (!PayForPull(pullCount, wallet, source, StandardBannerId, out int commitSpend, out int entropySpent, out failureReason))
{
return GachaPullResult.Failed(StandardBannerId, failureReason);
}
List<GachaReward> rewards = new();
for (int i = 0; i < pullCount; i++)
{
_standardState.totalPulls++;
rewards.Add(RollGenericReward(_standardState));
}
Changed?.Invoke();
return new GachaPullResult(true, StandardBannerId, GachaCurrencyType.StandardPull, commitSpend, entropySpent, rewards, null);
}
public int GetStandardFiveStarSelectorProgress()
{
return _standardState.totalPulls % StandardFiveStarSelectorInterval;
}
public bool TryClaimStandardFiveStarSelector(out string failureReason)
{
if (PendingStandardFiveStarSelectors <= 0)
{
failureReason = "no 5-star selector available";
return false;
}
_standardState.fiveStarSelectorsClaimed++;
failureReason = null;
Changed?.Invoke();
return true;
}
public IReadOnlyList<DistroDefinition> GetStandardFiveStarCharacterChoices()
{
List<DistroDefinition> choices = new();
for (int i = 0; i < _bannerPool.Count; i++)
{
DistroDefinition distro = _bannerPool[i];
if (distro != null && GetDistroStars(distro) == 5)
{
choices.Add(distro);
}
}
return choices;
}
public IReadOnlyList<PackageDefinition> GetStandardFiveStarEquipmentChoices(PackageDatabase packageDatabase)
{
List<PackageDefinition> choices = new();
if (packageDatabase != null)
{
IReadOnlyList<PackageDefinition> packages = packageDatabase.AllPackages;
for (int i = 0; i < packages.Count; i++)
{
PackageDefinition package = packages[i];
if (package != null && package.Rarity == 5)
{
choices.Add(package);
}
}
}
return choices;
}
public static string FormatCurrencyName(GachaCurrencyType currencyType)
{
return currencyType switch
@@ -372,20 +494,25 @@ namespace KernelPanic.Meta
return RewardGuaranteedBeginnerFiveStar("50-pull beginner guarantee");
}
if (TryRollFiveStar(_beginnerState, out bool fiveStarPityTriggered))
return RollGenericReward(_beginnerState);
}
private GachaReward RollGenericReward(GachaBannerState state)
{
if (TryRollFiveStar(state, out bool fiveStarPityTriggered))
{
return RewardRandomFiveStar(fiveStarPityTriggered);
}
bool pityTriggered = _beginnerState.pityCounter >= FourStarHardPity - 1;
bool pityTriggered = state.pityCounter >= FourStarHardPity - 1;
bool hitFourStarOrCharacter = pityTriggered || _random.NextDouble() < FourStarBaseChance;
if (!hitFourStarOrCharacter)
{
_beginnerState.pityCounter++;
state.pityCounter++;
return GachaReward.Package(3, "3-star package", false, false);
}
_beginnerState.pityCounter = 0;
state.pityCounter = 0;
if (_bannerPool.Count > 0 && _random.NextDouble() < DistroChanceOnFeaturedTier)
{
DistroDefinition distro = FindRandomBannerPoolDistroByStars(4);
@@ -560,7 +687,8 @@ namespace KernelPanic.Meta
pityCounter = state.pityCounter,
fiveStarPityCounter = state.fiveStarPityCounter,
exhausted = state.exhausted,
featuredFiveStarGuaranteed = state.featuredFiveStarGuaranteed
featuredFiveStarGuaranteed = state.featuredFiveStarGuaranteed,
fiveStarSelectorsClaimed = state.fiveStarSelectorsClaimed
};
clone.guaranteedDistroIds.AddRange(state.guaranteedDistroIds);
return clone;
+4
View File
@@ -81,6 +81,7 @@ namespace KernelPanic.Meta
public List<string> ownedUnitIds = new();
public List<string> bannerPoolIds = new();
public GachaBannerState beginnerBannerState = new(GachaService.BeginnerBannerId);
public GachaBannerState standardBannerState = new(GachaService.StandardBannerId);
public LastRunLoadoutSaveEntry lastRunLoadout = new();
public string lastRunDistroId;
public List<DistroBestWaveSaveEntry> distroBestWaves = new();
@@ -109,6 +110,9 @@ namespace KernelPanic.Meta
beginnerBannerState ??= new GachaBannerState(GachaService.BeginnerBannerId);
beginnerBannerState.bannerId = GachaService.BeginnerBannerId;
beginnerBannerState.EnsureLists();
standardBannerState ??= new GachaBannerState(GachaService.StandardBannerId);
standardBannerState.bannerId = GachaService.StandardBannerId;
standardBannerState.EnsureLists();
lastRunLoadout ??= new LastRunLoadoutSaveEntry();
lastRunLoadout.EnsureLists();
if (string.IsNullOrWhiteSpace(lastRunDistroId) && !string.IsNullOrWhiteSpace(lastRunLoadout.distroId))