Add metadata for new gacha assets and implement gacha system scaffolding.
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 15501d6fecab8d6c39720f46b2644bc9
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1632132e32bfc31e8a293140f5908aa3
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 04f09e15db30a0756af561fca255e85b
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4dfd40ca206341ba9a1a486d69994611
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 11e141b975f8ab190abf6c267067b575
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b6e048c6b94f10920a31d4c7069ba20f
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 856019cbd78921a9c8846de8f81029b2
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 361650f1e83cad1119b747d294991569
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 99cfb62a9ed9a6566aec84be75999c1f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3cc3bbc086d6f0d3199af9094f9912d2
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7b5e9a8f7e3bb8eb6865533dd2b9af2f
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 89c59c54d4bce36aaa836cfdc43f3c9f
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 07d51e6f24485d6aa9dfe38424e318ea
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f3cf77257a59a88518167e7e19023304
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: caab84daf052361ab87b05d78d72eb4a
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9347bcc42fb6834f1907a2c0b661ff95
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1dbc28e480e52d97b9501262af30cc78
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 19c884fde6ba18438b976aa154a729df
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 26d4526631046cf5aad90867da18ea81
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7819ca459d587093e942e1b497761d72
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: daae1c711cc8de216966b68adc63d240
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 76021ac16bc24526cbdacf1909a7a0cf
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 19deb73cec3d5dc42a5cc336222ce9df
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ded62c69f70d38a49b21972d857df4e2
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d97481a7d54be2045a96165d3977c980
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -322,6 +322,9 @@ MonoBehaviour:
|
|||||||
highQualityFiltering:
|
highQualityFiltering:
|
||||||
m_OverrideState: 1
|
m_OverrideState: 1
|
||||||
m_Value: 0
|
m_Value: 0
|
||||||
|
filter:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 0
|
||||||
downscale:
|
downscale:
|
||||||
m_OverrideState: 1
|
m_OverrideState: 1
|
||||||
m_Value: 0
|
m_Value: 0
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e9acb3a424fb6f8bfada39dfd8292a93
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d10c95dab567850aaad42c0c5c999dde
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fd9e459379f339742a14cf224cd91234
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9b1f51fb7c354203a2f93ad22fbeb101
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e003476e8a28025938acf6c377dfbaaa
|
||||||
@@ -7,6 +7,11 @@ namespace KernelPanic.Meta
|
|||||||
{
|
{
|
||||||
public int Balance { get; private set; }
|
public int Balance { get; private set; }
|
||||||
|
|
||||||
|
public void SetBalance(int amount)
|
||||||
|
{
|
||||||
|
Balance = amount < 0 ? 0 : amount;
|
||||||
|
}
|
||||||
|
|
||||||
public void Add(int amount)
|
public void Add(int amount)
|
||||||
{
|
{
|
||||||
Balance += amount;
|
Balance += amount;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace KernelPanic.Meta
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Stores per-banner counters that must persist between sessions.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public sealed class GachaBannerState
|
||||||
|
{
|
||||||
|
public string bannerId;
|
||||||
|
public int totalPulls;
|
||||||
|
public int pityCounter;
|
||||||
|
public int fiveStarPityCounter;
|
||||||
|
public bool exhausted;
|
||||||
|
public bool featuredFiveStarGuaranteed;
|
||||||
|
public List<string> guaranteedDistroIds = new();
|
||||||
|
|
||||||
|
public GachaBannerState()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public GachaBannerState(string bannerId)
|
||||||
|
{
|
||||||
|
this.bannerId = bannerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnsureLists()
|
||||||
|
{
|
||||||
|
guaranteedDistroIds ??= new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 29d7890153d140b2a25b1afb304581f9
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace KernelPanic.Meta
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies currencies spent on banner pulls.
|
||||||
|
/// </summary>
|
||||||
|
public enum GachaCurrencyType
|
||||||
|
{
|
||||||
|
StandardPull,
|
||||||
|
LimitedPull
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7262840a6d0b4569a70ca757cf4b5d19
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace KernelPanic.Meta
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Describes the persistent reward category produced by a gacha pull.
|
||||||
|
/// </summary>
|
||||||
|
public enum GachaRewardType
|
||||||
|
{
|
||||||
|
Equipment,
|
||||||
|
Distro,
|
||||||
|
FutureStandardFiveStarDistro
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6ef2e97a60574b3896db93fd09ec5313
|
||||||
@@ -5,16 +5,150 @@ using KernelPanic.Data;
|
|||||||
namespace KernelPanic.Meta
|
namespace KernelPanic.Meta
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides the contract for spending entropy on persistent unlock pulls.
|
/// Provides persistent banner pull logic and currency accounting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GachaService
|
public sealed class GachaService
|
||||||
{
|
{
|
||||||
private readonly List<DistroDefinition> bannerPool = new();
|
public const string BeginnerBannerId = "beginner";
|
||||||
|
public const string StandardBannerId = "standard";
|
||||||
|
public const string LimitedBannerId = "limited";
|
||||||
|
public const int BeginnerMaxPulls = 50;
|
||||||
|
public const int BeginnerSinglePullCost = 1;
|
||||||
|
public const int BeginnerTenPullCost = 8;
|
||||||
|
public const int FourStarHardPity = 10;
|
||||||
|
public const int FiveStarSoftPityStart = 66;
|
||||||
|
public const int FiveStarHardPity = 80;
|
||||||
|
public const int EntropyPerPullToken = 100;
|
||||||
|
|
||||||
public int PullTokens => 0; // TODO: Replace with the real pull-token wallet when that meta system exists.
|
private const double FourStarBaseChance = 0.12d;
|
||||||
|
private const double FiveStarBaseChance = 0.008d;
|
||||||
|
private const double FiveStarSoftPityStep = 0.07d;
|
||||||
|
private const double DistroChanceOnFeaturedTier = 0.5d;
|
||||||
|
private const double LimitedStandardFiveStarChance = 1d / 3d;
|
||||||
|
|
||||||
|
private readonly List<DistroDefinition> bannerPool = new();
|
||||||
|
private readonly Dictionary<GachaCurrencyType, int> currencyBalances = new();
|
||||||
|
private readonly Random random;
|
||||||
|
private GachaBannerState beginnerState = new(BeginnerBannerId);
|
||||||
|
|
||||||
|
public GachaService()
|
||||||
|
: this(new Random())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public GachaService(Random random)
|
||||||
|
{
|
||||||
|
this.random = random ?? new Random();
|
||||||
|
currencyBalances[GachaCurrencyType.StandardPull] = 0;
|
||||||
|
currencyBalances[GachaCurrencyType.LimitedPull] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int PullTokens => GetCurrencyBalance(GachaCurrencyType.StandardPull);
|
||||||
|
public int LimitedPullTokens => GetCurrencyBalance(GachaCurrencyType.LimitedPull);
|
||||||
|
public int RootCredits { get; private set; }
|
||||||
public IReadOnlyList<DistroDefinition> BannerPool => bannerPool;
|
public IReadOnlyList<DistroDefinition> BannerPool => bannerPool;
|
||||||
|
public GachaBannerState BeginnerState => beginnerState;
|
||||||
|
public bool IsBeginnerBannerAvailable => !beginnerState.exhausted && beginnerState.totalPulls < BeginnerMaxPulls && bannerPool.Count > 0;
|
||||||
|
|
||||||
public event Action BannerPoolChanged;
|
public event Action BannerPoolChanged;
|
||||||
|
public event Action Changed;
|
||||||
|
|
||||||
|
public void LoadProgress(SaveData data)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.EnsureLists();
|
||||||
|
RootCredits = Math.Max(0, data.rootCredits);
|
||||||
|
SetCurrencyBalanceSilently(GachaCurrencyType.StandardPull, data.standardPullCurrency);
|
||||||
|
SetCurrencyBalanceSilently(GachaCurrencyType.LimitedPull, data.limitedPullCurrency);
|
||||||
|
beginnerState = data.beginnerBannerState ?? new GachaBannerState(BeginnerBannerId);
|
||||||
|
beginnerState.bannerId = BeginnerBannerId;
|
||||||
|
beginnerState.EnsureLists();
|
||||||
|
if (beginnerState.totalPulls >= BeginnerMaxPulls)
|
||||||
|
{
|
||||||
|
beginnerState.exhausted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
beginnerState.fiveStarPityCounter = Math.Max(0, beginnerState.fiveStarPityCounter);
|
||||||
|
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteProgress(SaveData data)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.EnsureLists();
|
||||||
|
data.rootCredits = RootCredits;
|
||||||
|
data.standardPullCurrency = GetCurrencyBalance(GachaCurrencyType.StandardPull);
|
||||||
|
data.limitedPullCurrency = GetCurrencyBalance(GachaCurrencyType.LimitedPull);
|
||||||
|
data.beginnerBannerState = CloneState(beginnerState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetCurrencyBalance(GachaCurrencyType currencyType)
|
||||||
|
{
|
||||||
|
return currencyBalances.TryGetValue(currencyType, out int balance) ? balance : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCurrencyBalance(GachaCurrencyType currencyType, int amount)
|
||||||
|
{
|
||||||
|
SetCurrencyBalanceSilently(currencyType, amount);
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddCurrency(GachaCurrencyType currencyType, int amount)
|
||||||
|
{
|
||||||
|
if (amount <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetCurrencyBalance(currencyType, GetCurrencyBalance(currencyType) + amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRootCredits(int amount)
|
||||||
|
{
|
||||||
|
if (amount <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RootCredits += amount;
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ConvertRootCreditsToEntropy(EntropyWallet wallet, int amount, out string failureReason)
|
||||||
|
{
|
||||||
|
if (wallet == null)
|
||||||
|
{
|
||||||
|
failureReason = "entropy wallet unavailable";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount <= 0)
|
||||||
|
{
|
||||||
|
failureReason = "no root-credits selected";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RootCredits < amount)
|
||||||
|
{
|
||||||
|
failureReason = $"need {amount} root-credits";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RootCredits -= amount;
|
||||||
|
wallet.Add(amount);
|
||||||
|
failureReason = null;
|
||||||
|
Changed?.Invoke();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void AddToBannerPool(DistroDefinition distro)
|
public void AddToBannerPool(DistroDefinition distro)
|
||||||
{
|
{
|
||||||
@@ -25,26 +159,446 @@ namespace KernelPanic.Meta
|
|||||||
|
|
||||||
bannerPool.Add(distro);
|
bannerPool.Add(distro);
|
||||||
BannerPoolChanged?.Invoke();
|
BannerPoolChanged?.Invoke();
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetBeginnerGuaranteedDistros(IReadOnlyList<DistroDefinition> distros)
|
||||||
|
{
|
||||||
|
beginnerState.guaranteedDistroIds.Clear();
|
||||||
|
if (distros != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < distros.Count; i++)
|
||||||
|
{
|
||||||
|
DistroDefinition distro = distros[i];
|
||||||
|
if (distro != null && !string.IsNullOrWhiteSpace(distro.Id))
|
||||||
|
{
|
||||||
|
beginnerState.guaranteedDistroIds.Add(distro.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetBeginnerPullCost(int pullCount)
|
||||||
|
{
|
||||||
|
return pullCount == 10 ? BeginnerTenPullCost : BeginnerSinglePullCost;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanPullBeginner(int pullCount, out string failureReason)
|
||||||
|
{
|
||||||
|
if (pullCount != 1 && pullCount != 10)
|
||||||
|
{
|
||||||
|
failureReason = "only single and ten pulls are supported";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cost = GetBeginnerPullCost(pullCount);
|
||||||
|
if (GetCurrencyBalance(GachaCurrencyType.StandardPull) < cost)
|
||||||
|
{
|
||||||
|
failureReason = $"need {cost} {FormatCurrencyName(GachaCurrencyType.StandardPull)}";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
failureReason = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetMissingPullTokens(GachaCurrencyType currencyType, int pullCost)
|
||||||
|
{
|
||||||
|
return Math.Max(0, pullCost - GetCurrencyBalance(currencyType));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanCoverMissingPullTokensWithEntropy(EntropyWallet wallet, int missingPullTokens)
|
||||||
|
{
|
||||||
|
return missingPullTokens <= 0 || (wallet != null && wallet.Balance >= missingPullTokens * EntropyPerPullToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GachaPullResult PerformBeginnerPull(int pullCount)
|
||||||
|
{
|
||||||
|
return PerformBeginnerPull(pullCount, null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GachaPullResult PerformBeginnerPull(int pullCount, EntropyWallet wallet, int entropyTokenCount)
|
||||||
|
{
|
||||||
|
if (!CanPullBeginnerWithEntropy(pullCount, wallet, entropyTokenCount, out string failureReason))
|
||||||
|
{
|
||||||
|
return GachaPullResult.Failed(BeginnerBannerId, failureReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cost = GetBeginnerPullCost(pullCount);
|
||||||
|
int tokenSpend = cost - entropyTokenCount;
|
||||||
|
if (tokenSpend > 0)
|
||||||
|
{
|
||||||
|
SetCurrencyBalanceSilently(GachaCurrencyType.StandardPull, GetCurrencyBalance(GachaCurrencyType.StandardPull) - tokenSpend);
|
||||||
|
}
|
||||||
|
|
||||||
|
int entropySpent = entropyTokenCount * EntropyPerPullToken;
|
||||||
|
if (entropySpent > 0)
|
||||||
|
{
|
||||||
|
wallet.Spend(entropySpent);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GachaReward> rewards = new();
|
||||||
|
for (int i = 0; i < pullCount; i++)
|
||||||
|
{
|
||||||
|
beginnerState.totalPulls++;
|
||||||
|
rewards.Add(RollBeginnerReward(beginnerState.totalPulls));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beginnerState.totalPulls >= BeginnerMaxPulls)
|
||||||
|
{
|
||||||
|
beginnerState.exhausted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Changed?.Invoke();
|
||||||
|
return new GachaPullResult(true, BeginnerBannerId, GachaCurrencyType.StandardPull, tokenSpend, entropySpent, rewards, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatCurrencyName(GachaCurrencyType currencyType)
|
||||||
|
{
|
||||||
|
return currencyType switch
|
||||||
|
{
|
||||||
|
GachaCurrencyType.LimitedPull => "feature-pull-token",
|
||||||
|
_ => "stable-pull-token"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double GetFiveStarChance(GachaBannerState state)
|
||||||
|
{
|
||||||
|
return GetFiveStarChance(state == null ? 0 : state.fiveStarPityCounter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double GetFiveStarChance(int fiveStarPityCounter)
|
||||||
|
{
|
||||||
|
int pityPullNumber = Math.Max(0, fiveStarPityCounter) + 1;
|
||||||
|
if (pityPullNumber >= FiveStarHardPity)
|
||||||
|
{
|
||||||
|
return 1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pityPullNumber < FiveStarSoftPityStart)
|
||||||
|
{
|
||||||
|
return FiveStarBaseChance;
|
||||||
|
}
|
||||||
|
|
||||||
|
int softPitySteps = pityPullNumber - FiveStarSoftPityStart + 1;
|
||||||
|
return Math.Min(1d, FiveStarBaseChance + softPitySteps * FiveStarSoftPityStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GachaPullResult PerformPull(EntropyWallet wallet, int cost)
|
public GachaPullResult PerformPull(EntropyWallet wallet, int cost)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
return PerformBeginnerPull(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GachaReward RollBeginnerReward(int pullNumber)
|
||||||
|
{
|
||||||
|
if (pullNumber == 20)
|
||||||
|
{
|
||||||
|
AdvanceFiveStarPityAfterNonFiveStar(beginnerState);
|
||||||
|
return RewardGuaranteedBeginnerDistro(0, "20-pull starter guarantee");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pullNumber == 40)
|
||||||
|
{
|
||||||
|
AdvanceFiveStarPityAfterNonFiveStar(beginnerState);
|
||||||
|
return RewardGuaranteedBeginnerDistro(1, "40-pull starter guarantee");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pullNumber == 50)
|
||||||
|
{
|
||||||
|
beginnerState.pityCounter = 0;
|
||||||
|
return GachaReward.FutureStandardFiveStar("future standard 5-star distro", "50-pull beginner capstone");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryRollFiveStar(beginnerState, out bool fiveStarPityTriggered))
|
||||||
|
{
|
||||||
|
return RewardRandomFiveStar(fiveStarPityTriggered);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pityTriggered = beginnerState.pityCounter >= FourStarHardPity - 1;
|
||||||
|
bool hitFourStarOrCharacter = pityTriggered || random.NextDouble() < FourStarBaseChance;
|
||||||
|
if (!hitFourStarOrCharacter)
|
||||||
|
{
|
||||||
|
beginnerState.pityCounter++;
|
||||||
|
return GachaReward.Equipment(3, "3-star equipment", false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
beginnerState.pityCounter = 0;
|
||||||
|
if (bannerPool.Count > 0 && random.NextDouble() < DistroChanceOnFeaturedTier)
|
||||||
|
{
|
||||||
|
DistroDefinition distro = bannerPool[random.Next(bannerPool.Count)];
|
||||||
|
return GachaReward.DistroReward(distro, 4, pityTriggered, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GachaReward.Equipment(4, "4-star equipment", pityTriggered, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryRollFiveStar(GachaBannerState state, out bool pityTriggered)
|
||||||
|
{
|
||||||
|
state ??= beginnerState;
|
||||||
|
int pityPullNumber = Math.Max(0, state.fiveStarPityCounter) + 1;
|
||||||
|
pityTriggered = pityPullNumber >= FiveStarSoftPityStart;
|
||||||
|
|
||||||
|
double chance = GetFiveStarChance(state);
|
||||||
|
bool hit = chance >= 1d || random.NextDouble() < chance;
|
||||||
|
if (!hit)
|
||||||
|
{
|
||||||
|
state.fiveStarPityCounter++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.fiveStarPityCounter = 0;
|
||||||
|
state.pityCounter = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AdvanceFiveStarPityAfterNonFiveStar(GachaBannerState state)
|
||||||
|
{
|
||||||
|
if (state != null)
|
||||||
|
{
|
||||||
|
state.fiveStarPityCounter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GachaReward RewardRandomFiveStar(bool pityTriggered)
|
||||||
|
{
|
||||||
|
if (bannerPool.Count == 0 || random.NextDouble() >= DistroChanceOnFeaturedTier)
|
||||||
|
{
|
||||||
|
return GachaReward.Equipment(5, "5-star equipment", pityTriggered, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
DistroDefinition distro = bannerPool.Count == 0 ? null : bannerPool[random.Next(bannerPool.Count)];
|
||||||
|
return GachaReward.DistroReward(distro, 5, pityTriggered, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ResolveFeaturedTierIsDistro()
|
||||||
|
{
|
||||||
|
return random.NextDouble() < DistroChanceOnFeaturedTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ResolveLimitedFiveStarUsesStandardPool(GachaBannerState limitedState)
|
||||||
|
{
|
||||||
|
if (limitedState == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limitedState.featuredFiveStarGuaranteed)
|
||||||
|
{
|
||||||
|
limitedState.featuredFiveStarGuaranteed = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool useStandardPool = random.NextDouble() < LimitedStandardFiveStarChance;
|
||||||
|
limitedState.featuredFiveStarGuaranteed = useStandardPool;
|
||||||
|
return useStandardPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GachaReward RewardGuaranteedBeginnerDistro(int guaranteeIndex, string reason)
|
||||||
|
{
|
||||||
|
beginnerState.pityCounter = 0;
|
||||||
|
DistroDefinition distro = null;
|
||||||
|
if (guaranteeIndex >= 0 && guaranteeIndex < beginnerState.guaranteedDistroIds.Count)
|
||||||
|
{
|
||||||
|
distro = FindBannerPoolDistro(beginnerState.guaranteedDistroIds[guaranteeIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
distro ??= bannerPool.Count == 0 ? null : bannerPool[Math.Min(guaranteeIndex, bannerPool.Count - 1)];
|
||||||
|
return distro == null
|
||||||
|
? GachaReward.Equipment(4, "4-star equipment", true, true)
|
||||||
|
: GachaReward.DistroReward(distro, 4, true, true, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DistroDefinition FindBannerPoolDistro(string id)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < bannerPool.Count; i++)
|
||||||
|
{
|
||||||
|
DistroDefinition distro = bannerPool[i];
|
||||||
|
if (distro != null && string.Equals(distro.Id, id, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return distro;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetCurrencyBalanceSilently(GachaCurrencyType currencyType, int amount)
|
||||||
|
{
|
||||||
|
currencyBalances[currencyType] = Math.Max(0, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanPullBeginnerWithEntropy(int pullCount, EntropyWallet wallet, int entropyTokenCount, out string failureReason)
|
||||||
|
{
|
||||||
|
if (pullCount != 1 && pullCount != 10)
|
||||||
|
{
|
||||||
|
failureReason = "only single and ten pulls are supported";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cost = GetBeginnerPullCost(pullCount);
|
||||||
|
if (entropyTokenCount < 0 || entropyTokenCount > cost)
|
||||||
|
{
|
||||||
|
failureReason = "invalid entropy substitute amount";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int tokenSpend = cost - entropyTokenCount;
|
||||||
|
if (GetCurrencyBalance(GachaCurrencyType.StandardPull) < tokenSpend)
|
||||||
|
{
|
||||||
|
failureReason = $"need {tokenSpend} {FormatCurrencyName(GachaCurrencyType.StandardPull)}";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int entropySpend = entropyTokenCount * EntropyPerPullToken;
|
||||||
|
if (entropySpend > 0 && (wallet == null || wallet.Balance < entropySpend))
|
||||||
|
{
|
||||||
|
failureReason = $"need {entropySpend} entropy";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
failureReason = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GachaBannerState CloneState(GachaBannerState state)
|
||||||
|
{
|
||||||
|
state ??= new GachaBannerState(BeginnerBannerId);
|
||||||
|
state.EnsureLists();
|
||||||
|
GachaBannerState clone = new(state.bannerId)
|
||||||
|
{
|
||||||
|
totalPulls = state.totalPulls,
|
||||||
|
pityCounter = state.pityCounter,
|
||||||
|
fiveStarPityCounter = state.fiveStarPityCounter,
|
||||||
|
exhausted = state.exhausted,
|
||||||
|
featuredFiveStarGuaranteed = state.featuredFiveStarGuaranteed
|
||||||
|
};
|
||||||
|
clone.guaranteedDistroIds.AddRange(state.guaranteedDistroIds);
|
||||||
|
return clone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the result of a persistent unlock pull.
|
/// Describes one reward entry from one pull.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct GachaReward
|
||||||
|
{
|
||||||
|
private GachaReward(
|
||||||
|
GachaRewardType rewardType,
|
||||||
|
int starRating,
|
||||||
|
string displayName,
|
||||||
|
DistroDefinition distro,
|
||||||
|
bool pityTriggered,
|
||||||
|
bool guaranteed,
|
||||||
|
string guaranteeReason)
|
||||||
|
{
|
||||||
|
RewardType = rewardType;
|
||||||
|
StarRating = starRating;
|
||||||
|
DisplayName = displayName;
|
||||||
|
Distro = distro;
|
||||||
|
PityTriggered = pityTriggered;
|
||||||
|
Guaranteed = guaranteed;
|
||||||
|
GuaranteeReason = guaranteeReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GachaRewardType RewardType { get; }
|
||||||
|
public int StarRating { get; }
|
||||||
|
public string DisplayName { get; }
|
||||||
|
public DistroDefinition Distro { get; }
|
||||||
|
public bool PityTriggered { get; }
|
||||||
|
public bool Guaranteed { get; }
|
||||||
|
public string GuaranteeReason { get; }
|
||||||
|
|
||||||
|
public static GachaReward Equipment(int starRating, string displayName, bool pityTriggered, bool guaranteed)
|
||||||
|
{
|
||||||
|
return new GachaReward(GachaRewardType.Equipment, starRating, displayName, null, pityTriggered, guaranteed, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GachaReward DistroReward(DistroDefinition distro, int starRating, bool pityTriggered, bool guaranteed, string guaranteeReason = null)
|
||||||
|
{
|
||||||
|
string displayName = distro == null ? "unknown distro" : DistroDisplayName(distro);
|
||||||
|
return new GachaReward(GachaRewardType.Distro, starRating, displayName, distro, pityTriggered, guaranteed, guaranteeReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GachaReward FutureStandardFiveStar(string displayName, string guaranteeReason)
|
||||||
|
{
|
||||||
|
return new GachaReward(GachaRewardType.FutureStandardFiveStarDistro, 5, displayName, null, true, true, guaranteeReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DistroDisplayName(DistroDefinition distro)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(distro.DisplayName) ? distro.name : distro.DisplayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the result of a banner pull request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly struct GachaPullResult
|
public readonly struct GachaPullResult
|
||||||
{
|
{
|
||||||
public GachaPullResult(bool success, string unlockId)
|
public GachaPullResult(
|
||||||
|
bool success,
|
||||||
|
string bannerId,
|
||||||
|
GachaCurrencyType currencyType,
|
||||||
|
int currencySpent,
|
||||||
|
int entropySpent,
|
||||||
|
IReadOnlyList<GachaReward> rewards,
|
||||||
|
string failureReason)
|
||||||
{
|
{
|
||||||
Success = success;
|
Success = success;
|
||||||
UnlockId = unlockId;
|
BannerId = bannerId;
|
||||||
|
CurrencyType = currencyType;
|
||||||
|
CurrencySpent = currencySpent;
|
||||||
|
EntropySpent = entropySpent;
|
||||||
|
Rewards = rewards ?? Array.Empty<GachaReward>();
|
||||||
|
FailureReason = failureReason;
|
||||||
|
UnlockId = Rewards.Count > 0 && Rewards[0].Distro != null ? Rewards[0].Distro.Id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Success { get; }
|
public bool Success { get; }
|
||||||
|
public string BannerId { get; }
|
||||||
|
public GachaCurrencyType CurrencyType { get; }
|
||||||
|
public int CurrencySpent { get; }
|
||||||
|
public int EntropySpent { get; }
|
||||||
|
public IReadOnlyList<GachaReward> Rewards { get; }
|
||||||
|
public string FailureReason { get; }
|
||||||
public string UnlockId { get; }
|
public string UnlockId { get; }
|
||||||
|
|
||||||
|
public static GachaPullResult Failed(string bannerId, string reason)
|
||||||
|
{
|
||||||
|
return new GachaPullResult(false, bannerId, GachaCurrencyType.StandardPull, 0, 0, Array.Empty<GachaReward>(), reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ namespace KernelPanic.Meta
|
|||||||
private int featuredIndex;
|
private int featuredIndex;
|
||||||
|
|
||||||
// TODO: Populate owned units from SaveService during bootstrap.
|
// TODO: Populate owned units from SaveService during bootstrap.
|
||||||
// TODO: Add newly unlocked units from GachaService rewards when pulls exist.
|
|
||||||
public IReadOnlyList<DistroDefinition> OwnedUnits => ownedUnits;
|
public IReadOnlyList<DistroDefinition> OwnedUnits => ownedUnits;
|
||||||
public DistroDefinition FeaturedUnit => ownedUnits.Count == 0 ? null : ownedUnits[Math.Clamp(featuredIndex, 0, ownedUnits.Count - 1)];
|
public DistroDefinition FeaturedUnit => ownedUnits.Count == 0 ? null : ownedUnits[Math.Clamp(featuredIndex, 0, ownedUnits.Count - 1)];
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ namespace KernelPanic.Meta
|
|||||||
|
|
||||||
public void Add(DistroDefinition unit)
|
public void Add(DistroDefinition unit)
|
||||||
{
|
{
|
||||||
if (unit == null || ownedUnits.Contains(unit))
|
if (unit == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -32,6 +31,26 @@ namespace KernelPanic.Meta
|
|||||||
Changed?.Invoke();
|
Changed?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int GetOwnedCount(string unitId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(unitId))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < ownedUnits.Count; i++)
|
||||||
|
{
|
||||||
|
DistroDefinition unit = ownedUnits[i];
|
||||||
|
if (unit != null && string.Equals(unit.Id, unitId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
public void SelectNextFeatured()
|
public void SelectNextFeatured()
|
||||||
{
|
{
|
||||||
if (ownedUnits.Count < 2)
|
if (ownedUnits.Count < 2)
|
||||||
|
|||||||
@@ -58,8 +58,13 @@ namespace KernelPanic.Meta
|
|||||||
public sealed class SaveData
|
public sealed class SaveData
|
||||||
{
|
{
|
||||||
public bool starterChosen;
|
public bool starterChosen;
|
||||||
|
public int entropyBalance;
|
||||||
|
public int rootCredits;
|
||||||
|
public int standardPullCurrency;
|
||||||
|
public int limitedPullCurrency;
|
||||||
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 LastRunLoadoutSaveEntry lastRunLoadout = new();
|
public LastRunLoadoutSaveEntry lastRunLoadout = new();
|
||||||
|
|
||||||
public static SaveData CreateDefault()
|
public static SaveData CreateDefault()
|
||||||
@@ -71,6 +76,9 @@ namespace KernelPanic.Meta
|
|||||||
{
|
{
|
||||||
ownedUnitIds ??= new List<string>();
|
ownedUnitIds ??= new List<string>();
|
||||||
bannerPoolIds ??= new List<string>();
|
bannerPoolIds ??= new List<string>();
|
||||||
|
beginnerBannerState ??= new GachaBannerState(GachaService.BeginnerBannerId);
|
||||||
|
beginnerBannerState.bannerId = GachaService.BeginnerBannerId;
|
||||||
|
beginnerBannerState.EnsureLists();
|
||||||
lastRunLoadout ??= new LastRunLoadoutSaveEntry();
|
lastRunLoadout ??= new LastRunLoadoutSaveEntry();
|
||||||
lastRunLoadout.EnsureLists();
|
lastRunLoadout.EnsureLists();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c6c581de5a6bcbc73a451ce3906547bc
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 73880a4e893003d719622694aa5ffc02
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4b02ec0c6f7df1d3aa8211567c3ec2a3
|
||||||
@@ -0,0 +1,487 @@
|
|||||||
|
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 readonly List<VisualElement> bannerRows = new();
|
||||||
|
private readonly string[] bannerIds =
|
||||||
|
{
|
||||||
|
GachaService.BeginnerBannerId,
|
||||||
|
GachaService.StandardBannerId,
|
||||||
|
GachaService.LimitedBannerId
|
||||||
|
};
|
||||||
|
|
||||||
|
private VisualElement bannerList;
|
||||||
|
private VisualElement bannerDetail;
|
||||||
|
private DistroDatabase distroDatabase;
|
||||||
|
private FontAsset monospaceFont;
|
||||||
|
private GachaService gachaService;
|
||||||
|
private PlayerCollection playerCollection;
|
||||||
|
private EntropyWallet wallet;
|
||||||
|
private Action onChanged;
|
||||||
|
private Action requestRootCreditExchange;
|
||||||
|
private int selectedBannerIndex;
|
||||||
|
private int pendingPullCount;
|
||||||
|
private int pendingMissingTokens;
|
||||||
|
private bool hasOpened;
|
||||||
|
private string resultBannerId;
|
||||||
|
private string resultText;
|
||||||
|
|
||||||
|
public void Bind(VisualElement root, DistroDatabase database, FontAsset artFont, GachaService service, PlayerCollection collection, EntropyWallet entropyWallet, Action changedCallback, Action rootCreditExchangeCallback)
|
||||||
|
{
|
||||||
|
distroDatabase = database;
|
||||||
|
monospaceFont = artFont;
|
||||||
|
gachaService = service;
|
||||||
|
playerCollection = collection;
|
||||||
|
wallet = entropyWallet;
|
||||||
|
onChanged = changedCallback;
|
||||||
|
requestRootCreditExchange = rootCreditExchangeCallback;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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("pulls", $"{state.totalPulls}/{GachaService.BeginnerMaxPulls}"));
|
||||||
|
bannerDetail.Add(BuildInfoLine("4-star pity", $"{state.pityCounter}/{GachaService.FourStarHardPity}"));
|
||||||
|
bannerDetail.Add(BuildInfoLine("ten pull", $"{GachaService.BeginnerTenPullCost} stable-pull-tokens for 10 pulls"));
|
||||||
|
bannerDetail.Add(BuildInfoLine("guarantees", FormatBeginnerGuarantees()));
|
||||||
|
|
||||||
|
Label rules = new("Base result is a 3-star equipment cache. A 4-star equipment 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\n8x {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 && !string.IsNullOrWhiteSpace(resultText))
|
||||||
|
{
|
||||||
|
Label result = new(resultText);
|
||||||
|
result.AddToClassList("gacha-result");
|
||||||
|
bannerDetail.Add(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderFutureBanner(string bannerId)
|
||||||
|
{
|
||||||
|
bannerDetail.Add(BuildBannerHero(GetBannerTitle(bannerId)));
|
||||||
|
|
||||||
|
Label title = new(GetBannerTitle(bannerId));
|
||||||
|
title.AddToClassList("gacha-detail-title");
|
||||||
|
bannerDetail.Add(title);
|
||||||
|
|
||||||
|
string currency = bannerId == GachaService.StandardBannerId
|
||||||
|
? GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)
|
||||||
|
: GachaService.FormatCurrencyName(GachaCurrencyType.LimitedPull);
|
||||||
|
bannerDetail.Add(BuildInfoLine("pull currency", currency));
|
||||||
|
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 (bannerId != GachaService.BeginnerBannerId)
|
||||||
|
{
|
||||||
|
resultBannerId = bannerId;
|
||||||
|
resultText = $"git pull origin {bannerId}: remote not implemented yet";
|
||||||
|
RenderSelectedBanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GachaPullResult result = gachaService.PerformBeginnerPull(pullCount, wallet, entropyTokenCount);
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
resultBannerId = bannerId;
|
||||||
|
resultText = $"git pull failed: {result.FailureReason}";
|
||||||
|
RenderSelectedBanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> lines = new();
|
||||||
|
lines.Add($"git pull complete: spent {result.CurrencySpent} {GachaService.FormatCurrencyName(result.CurrencyType)}");
|
||||||
|
if (result.EntropySpent > 0)
|
||||||
|
{
|
||||||
|
lines.Add($"entropy fallback: spent {result.EntropySpent} entropy");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < result.Rewards.Count; i++)
|
||||||
|
{
|
||||||
|
GachaReward reward = result.Rewards[i];
|
||||||
|
bool duplicate = reward.Distro != null && playerCollection.GetOwnedCount(reward.Distro.Id) > 0;
|
||||||
|
if (reward.Distro != null)
|
||||||
|
{
|
||||||
|
playerCollection.Add(reward.Distro);
|
||||||
|
}
|
||||||
|
|
||||||
|
string suffix = reward.Guaranteed ? " guaranteed" : reward.PityTriggered ? " pity" : string.Empty;
|
||||||
|
string duplicateText = duplicate ? " duplicate" : string.Empty;
|
||||||
|
lines.Add($"{i + 1:00}: {reward.StarRating}-star {reward.DisplayName}{suffix}{duplicateText}");
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBannerId = bannerId;
|
||||||
|
resultText = string.Join("\n", lines);
|
||||||
|
onChanged?.Invoke();
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RequestPullSelectedBanner(int pullCount)
|
||||||
|
{
|
||||||
|
string bannerId = bannerIds[selectedBannerIndex];
|
||||||
|
if (bannerId != GachaService.BeginnerBannerId)
|
||||||
|
{
|
||||||
|
PullSelectedBanner(pullCount, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cost = gachaService.GetBeginnerPullCost(pullCount);
|
||||||
|
int missingTokens = gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, cost);
|
||||||
|
if (missingTokens <= 0)
|
||||||
|
{
|
||||||
|
ClearEntropyPrompt();
|
||||||
|
PullSelectedBanner(pullCount, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gachaService.CanCoverMissingPullTokensWithEntropy(wallet, missingTokens))
|
||||||
|
{
|
||||||
|
if (gachaService.RootCredits > 0)
|
||||||
|
{
|
||||||
|
requestRootCreditExchange?.Invoke();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBannerId = bannerId;
|
||||||
|
resultText = $"git pull failed: need {missingTokens} {GachaService.FormatCurrencyName(GachaCurrencyType.StandardPull)} or {missingTokens * GachaService.EntropyPerPullToken} entropy";
|
||||||
|
RenderSelectedBanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingPullCount = pullCount;
|
||||||
|
pendingMissingTokens = missingTokens;
|
||||||
|
resultBannerId = bannerId;
|
||||||
|
resultText = null;
|
||||||
|
RenderSelectedBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
private VisualElement BuildEntropyPrompt()
|
||||||
|
{
|
||||||
|
VisualElement prompt = new();
|
||||||
|
prompt.AddToClassList("gacha-entropy-prompt");
|
||||||
|
|
||||||
|
int entropyCost = pendingMissingTokens * GachaService.EntropyPerPullToken;
|
||||||
|
Label copy = new($"missing {pendingMissingTokens} stable-pull-token(s). spend {entropyCost} entropy to complete git pull?");
|
||||||
|
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 missingTokens = pendingMissingTokens;
|
||||||
|
ClearEntropyPrompt();
|
||||||
|
PullSelectedBanner(pullCount, missingTokens);
|
||||||
|
})
|
||||||
|
{
|
||||||
|
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 missingTokens = gachaService.GetMissingPullTokens(GachaCurrencyType.StandardPull, gachaService.GetBeginnerPullCost(pullCount));
|
||||||
|
return missingTokens > 0 &&
|
||||||
|
(gachaService.CanCoverMissingPullTokensWithEntropy(wallet, missingTokens) ||
|
||||||
|
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=future standard 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 => "soon",
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0915991bbd2d4fb08d438e4fdae30935
|
||||||
@@ -34,13 +34,16 @@ namespace KernelPanic.UI
|
|||||||
bootIntroPlayed = false;
|
bootIntroPlayed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SerializeField] private FontAsset monospaceFont; // TODO: Assign a real monospace FontAsset when typography assets exist.
|
[SerializeField]
|
||||||
|
private FontAsset monospaceFont; // TODO: Assign a real monospace FontAsset when typography assets exist.
|
||||||
|
|
||||||
[SerializeField] private string motdBody = "unstable userspace detected; keep a rollback shell open.";
|
[SerializeField] private string motdBody = "unstable userspace detected; keep a rollback shell open.";
|
||||||
[SerializeField] private DistroDatabase distroDatabase;
|
[SerializeField] private DistroDatabase distroDatabase;
|
||||||
[SerializeField] private CardDatabase cardDatabase;
|
[SerializeField] private CardDatabase cardDatabase;
|
||||||
[SerializeField] private FeaturedUnitPanel featuredUnitPanel = new();
|
[SerializeField] private FeaturedUnitPanel featuredUnitPanel = new();
|
||||||
[SerializeField] private CollectionScreenController collectionScreen = new();
|
[SerializeField] private CollectionScreenController collectionScreen = new();
|
||||||
[SerializeField] private StarterSelectionController starterSelection = new();
|
[SerializeField] private StarterSelectionController starterSelection = new();
|
||||||
|
[SerializeField] private GachaScreenController gachaScreen = new();
|
||||||
|
|
||||||
private readonly List<CommandMenuEntry> commandEntries = new();
|
private readonly List<CommandMenuEntry> commandEntries = new();
|
||||||
private readonly List<VisualElement> packageRows = new();
|
private readonly List<VisualElement> packageRows = new();
|
||||||
@@ -66,6 +69,20 @@ namespace KernelPanic.UI
|
|||||||
private Label bootIntroLogLabel;
|
private Label bootIntroLogLabel;
|
||||||
private Label motdBodyLabel;
|
private Label motdBodyLabel;
|
||||||
private VisualElement motdBlock;
|
private VisualElement motdBlock;
|
||||||
|
private Button rootCreditsToEntropyButton;
|
||||||
|
private VisualElement rootCreditExchangeModal;
|
||||||
|
private Label rootCreditExchangeBalanceLabel;
|
||||||
|
private Label rootCreditExchangePreviewLabel;
|
||||||
|
private Label rootCreditExchangeMessageLabel;
|
||||||
|
private TextField rootCreditExchangeInput;
|
||||||
|
private Button rootCreditExchangeMinusHundredButton;
|
||||||
|
private Button rootCreditExchangeMinusOneButton;
|
||||||
|
private Button rootCreditExchangeNoneButton;
|
||||||
|
private Button rootCreditExchangePlusOneButton;
|
||||||
|
private Button rootCreditExchangePlusHundredButton;
|
||||||
|
private Button rootCreditExchangeMaxButton;
|
||||||
|
private Button rootCreditExchangeConfirmButton;
|
||||||
|
private Button rootCreditExchangeCancelButton;
|
||||||
private Button collectionUnitsButton;
|
private Button collectionUnitsButton;
|
||||||
private Button collectionCardsButton;
|
private Button collectionCardsButton;
|
||||||
private readonly ScreenFrameController runSetupFrame = new();
|
private readonly ScreenFrameController runSetupFrame = new();
|
||||||
@@ -89,7 +106,12 @@ namespace KernelPanic.UI
|
|||||||
private bool cursorVisible;
|
private bool cursorVisible;
|
||||||
private bool suppressNextClick;
|
private bool suppressNextClick;
|
||||||
private bool warnedUnresolvedSaveId;
|
private bool warnedUnresolvedSaveId;
|
||||||
|
private bool rootCreditExchangeOpen;
|
||||||
private string bootIntroCopy;
|
private string bootIntroCopy;
|
||||||
|
private Action rootCreditExchangeMinusHundredClicked;
|
||||||
|
private Action rootCreditExchangeMinusOneClicked;
|
||||||
|
private Action rootCreditExchangePlusOneClicked;
|
||||||
|
private Action rootCreditExchangePlusHundredClicked;
|
||||||
private IVisualElementScheduledItem blinkSchedule;
|
private IVisualElementScheduledItem blinkSchedule;
|
||||||
private IVisualElementScheduledItem bootIntroSchedule;
|
private IVisualElementScheduledItem bootIntroSchedule;
|
||||||
|
|
||||||
@@ -104,7 +126,8 @@ namespace KernelPanic.UI
|
|||||||
document = GetComponent<UIDocument>();
|
document = GetComponent<UIDocument>();
|
||||||
saveService = new SaveService();
|
saveService = new SaveService();
|
||||||
gachaService = new GachaService();
|
gachaService = new GachaService();
|
||||||
playerCollection = new PlayerCollection(); // TODO: Replace with persistent player-collection service composition.
|
playerCollection =
|
||||||
|
new PlayerCollection(); // TODO: Replace with persistent player-collection service composition.
|
||||||
cardLoadout = new CardLoadout(playerCollection.OwnedUnits);
|
cardLoadout = new CardLoadout(playerCollection.OwnedUnits);
|
||||||
Initialize(new EntropyWallet()); // TODO: Replace with persistent wallet service composition.
|
Initialize(new EntropyWallet()); // TODO: Replace with persistent wallet service composition.
|
||||||
BindElements();
|
BindElements();
|
||||||
@@ -114,9 +137,11 @@ namespace KernelPanic.UI
|
|||||||
collectionScreen.Bind(root, monospaceFont);
|
collectionScreen.Bind(root, monospaceFont);
|
||||||
BindScreenFrames();
|
BindScreenFrames();
|
||||||
starterSelection.Bind(root, distroDatabase, HandleStarterConfirmed);
|
starterSelection.Bind(root, distroDatabase, HandleStarterConfirmed);
|
||||||
|
gachaScreen.Bind(root, distroDatabase, monospaceFont, gachaService, playerCollection, wallet,
|
||||||
|
HandleMetaStateChanged, OpenRootCreditExchange);
|
||||||
LoadMetaState();
|
LoadMetaState();
|
||||||
playerCollection.Changed += HandleMetaStateChanged;
|
playerCollection.Changed += HandleMetaStateChanged;
|
||||||
gachaService.BannerPoolChanged += HandleMetaStateChanged;
|
gachaService.Changed += HandleMetaStateChanged;
|
||||||
ApplyOptionalFont();
|
ApplyOptionalFont();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +155,7 @@ namespace KernelPanic.UI
|
|||||||
RefreshEventBanner();
|
RefreshEventBanner();
|
||||||
featuredUnitPanel.Refresh(playerCollection.OwnedUnits, playerCollection.FeaturedUnit);
|
featuredUnitPanel.Refresh(playerCollection.OwnedUnits, playerCollection.FeaturedUnit);
|
||||||
collectionScreen.RefreshUnits(playerCollection.OwnedUnits);
|
collectionScreen.RefreshUnits(playerCollection.OwnedUnits);
|
||||||
|
gachaScreen.Refresh();
|
||||||
SelectCommand(0);
|
SelectCommand(0);
|
||||||
ShowMainMenu();
|
ShowMainMenu();
|
||||||
StartAmbientSchedules();
|
StartAmbientSchedules();
|
||||||
@@ -170,15 +196,36 @@ namespace KernelPanic.UI
|
|||||||
bootIntroLogLabel = root.Q<Label>("BootIntroLogLabel");
|
bootIntroLogLabel = root.Q<Label>("BootIntroLogLabel");
|
||||||
motdBodyLabel = root.Q<Label>("MotdBodyLabel");
|
motdBodyLabel = root.Q<Label>("MotdBodyLabel");
|
||||||
|
|
||||||
|
rootCreditsToEntropyButton = root.Q<Button>("RootCreditsToEntropyButton");
|
||||||
|
if (rootCreditsToEntropyButton != null)
|
||||||
|
{
|
||||||
|
rootCreditsToEntropyButton.focusable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCreditExchangeModal = root.Q<VisualElement>("RootCreditExchangeModal");
|
||||||
|
rootCreditExchangeBalanceLabel = root.Q<Label>("RootCreditExchangeBalanceLabel");
|
||||||
|
rootCreditExchangePreviewLabel = root.Q<Label>("RootCreditExchangePreviewLabel");
|
||||||
|
rootCreditExchangeMessageLabel = root.Q<Label>("RootCreditExchangeMessageLabel");
|
||||||
|
rootCreditExchangeInput = root.Q<TextField>("RootCreditExchangeInput");
|
||||||
|
rootCreditExchangeMinusHundredButton = root.Q<Button>("RootCreditExchangeMinusHundredButton");
|
||||||
|
rootCreditExchangeMinusOneButton = root.Q<Button>("RootCreditExchangeMinusOneButton");
|
||||||
|
rootCreditExchangeNoneButton = root.Q<Button>("RootCreditExchangeNoneButton");
|
||||||
|
rootCreditExchangePlusOneButton = root.Q<Button>("RootCreditExchangePlusOneButton");
|
||||||
|
rootCreditExchangePlusHundredButton = root.Q<Button>("RootCreditExchangePlusHundredButton");
|
||||||
|
rootCreditExchangeMaxButton = root.Q<Button>("RootCreditExchangeMaxButton");
|
||||||
|
rootCreditExchangeConfirmButton = root.Q<Button>("RootCreditExchangeConfirmButton");
|
||||||
|
rootCreditExchangeCancelButton = root.Q<Button>("RootCreditExchangeCancelButton");
|
||||||
collectionUnitsButton = root.Q<Button>("CollectionUnitsButton");
|
collectionUnitsButton = root.Q<Button>("CollectionUnitsButton");
|
||||||
collectionCardsButton = root.Q<Button>("CollectionCardsButton");
|
collectionCardsButton = root.Q<Button>("CollectionCardsButton");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BindScreenFrames()
|
private void BindScreenFrames()
|
||||||
{
|
{
|
||||||
runSetupFrame.Bind(runSetupPanel, "$ ./start_run --configure", "[esc] back [arrows] navigate [enter] select [b] boot run", ShowMainMenu);
|
runSetupFrame.Bind(runSetupPanel, "$ ./start_run --configure",
|
||||||
collectionFrame.Bind(collectionPanel, "$ ls ~/collection", "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select", ShowMainMenu);
|
"[esc] back [arrows] navigate [enter] select [b] boot run", ShowMainMenu);
|
||||||
gachaFrame.Bind(gachaPanel, "$ curl gacha.sh | sh", "[esc] back", ShowMainMenu);
|
collectionFrame.Bind(collectionPanel, "$ ls ~/collection",
|
||||||
|
"[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select", ShowMainMenu);
|
||||||
|
gachaFrame.Bind(gachaPanel, "$ curl gacha.sh | sh", "[esc] back click banner click pull", ShowMainMenu);
|
||||||
settingsFrame.Bind(settingsPanel, "$ dpkg-reconfigure kernel-panic", "[esc] back", ShowMainMenu);
|
settingsFrame.Bind(settingsPanel, "$ dpkg-reconfigure kernel-panic", "[esc] back", ShowMainMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +253,60 @@ namespace KernelPanic.UI
|
|||||||
{
|
{
|
||||||
root.RegisterCallback<KeyDownEvent>(HandleKeyDown);
|
root.RegisterCallback<KeyDownEvent>(HandleKeyDown);
|
||||||
root.RegisterCallback<PointerDownEvent>(HandlePointerDown);
|
root.RegisterCallback<PointerDownEvent>(HandlePointerDown);
|
||||||
|
if (rootCreditsToEntropyButton != null)
|
||||||
|
{
|
||||||
|
rootCreditsToEntropyButton.clicked += HandleRootCreditsToEntropyClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeInput != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeInput.RegisterValueChangedCallback(HandleRootCreditExchangeInputChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeMinusHundredButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeMinusHundredClicked ??= () => AddRootCreditExchangeAmount(-100);
|
||||||
|
rootCreditExchangeMinusHundredButton.clicked += rootCreditExchangeMinusHundredClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeMinusOneButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeMinusOneClicked ??= () => AddRootCreditExchangeAmount(-1);
|
||||||
|
rootCreditExchangeMinusOneButton.clicked += rootCreditExchangeMinusOneClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeNoneButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeNoneButton.clicked += SetRootCreditExchangeNone;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangePlusOneButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangePlusOneClicked ??= () => AddRootCreditExchangeAmount(1);
|
||||||
|
rootCreditExchangePlusOneButton.clicked += rootCreditExchangePlusOneClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangePlusHundredButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangePlusHundredClicked ??= () => AddRootCreditExchangeAmount(100);
|
||||||
|
rootCreditExchangePlusHundredButton.clicked += rootCreditExchangePlusHundredClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeMaxButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeMaxButton.clicked += SetRootCreditExchangeMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeConfirmButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeConfirmButton.clicked += ConfirmRootCreditExchange;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeCancelButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeCancelButton.clicked += CloseRootCreditExchange;
|
||||||
|
}
|
||||||
|
|
||||||
collectionUnitsButton.RegisterCallback<ClickEvent>(HandleCollectionUnitsTabClicked);
|
collectionUnitsButton.RegisterCallback<ClickEvent>(HandleCollectionUnitsTabClicked);
|
||||||
collectionCardsButton.RegisterCallback<ClickEvent>(HandleCollectionCardsTabClicked);
|
collectionCardsButton.RegisterCallback<ClickEvent>(HandleCollectionCardsTabClicked);
|
||||||
}
|
}
|
||||||
@@ -214,10 +315,194 @@ namespace KernelPanic.UI
|
|||||||
{
|
{
|
||||||
root.UnregisterCallback<KeyDownEvent>(HandleKeyDown);
|
root.UnregisterCallback<KeyDownEvent>(HandleKeyDown);
|
||||||
root.UnregisterCallback<PointerDownEvent>(HandlePointerDown);
|
root.UnregisterCallback<PointerDownEvent>(HandlePointerDown);
|
||||||
|
if (rootCreditsToEntropyButton != null)
|
||||||
|
{
|
||||||
|
rootCreditsToEntropyButton.clicked -= HandleRootCreditsToEntropyClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeInput != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeInput.UnregisterValueChangedCallback(HandleRootCreditExchangeInputChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeMinusHundredButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeMinusHundredButton.clicked -= rootCreditExchangeMinusHundredClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeMinusOneButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeMinusOneButton.clicked -= rootCreditExchangeMinusOneClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeNoneButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeNoneButton.clicked -= SetRootCreditExchangeNone;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangePlusOneButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangePlusOneButton.clicked -= rootCreditExchangePlusOneClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangePlusHundredButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangePlusHundredButton.clicked -= rootCreditExchangePlusHundredClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeMaxButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeMaxButton.clicked -= SetRootCreditExchangeMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeConfirmButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeConfirmButton.clicked -= ConfirmRootCreditExchange;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeCancelButton != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeCancelButton.clicked -= CloseRootCreditExchange;
|
||||||
|
}
|
||||||
|
|
||||||
collectionUnitsButton.UnregisterCallback<ClickEvent>(HandleCollectionUnitsTabClicked);
|
collectionUnitsButton.UnregisterCallback<ClickEvent>(HandleCollectionUnitsTabClicked);
|
||||||
collectionCardsButton.UnregisterCallback<ClickEvent>(HandleCollectionCardsTabClicked);
|
collectionCardsButton.UnregisterCallback<ClickEvent>(HandleCollectionCardsTabClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleRootCreditsToEntropyClicked()
|
||||||
|
{
|
||||||
|
if (gachaService == null || wallet == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenRootCreditExchange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenRootCreditExchange()
|
||||||
|
{
|
||||||
|
if (rootCreditExchangeModal == null || gachaService == null || wallet == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCreditExchangeOpen = true;
|
||||||
|
rootCreditExchangeModal.RemoveFromClassList(HiddenClassName);
|
||||||
|
rootCreditExchangeMessageLabel?.AddToClassList(HiddenClassName);
|
||||||
|
SetRootCreditExchangeAmount(Math.Min(100, gachaService.RootCredits));
|
||||||
|
RefreshRootCreditExchange();
|
||||||
|
rootCreditExchangeInput?.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseRootCreditExchange()
|
||||||
|
{
|
||||||
|
rootCreditExchangeOpen = false;
|
||||||
|
rootCreditExchangeModal?.AddToClassList(HiddenClassName);
|
||||||
|
root.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleRootCreditExchangeInputChanged(ChangeEvent<string> evt)
|
||||||
|
{
|
||||||
|
RefreshRootCreditExchange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddRootCreditExchangeAmount(int amount)
|
||||||
|
{
|
||||||
|
SetRootCreditExchangeAmount(GetRootCreditExchangeAmount() + amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetRootCreditExchangeMax()
|
||||||
|
{
|
||||||
|
SetRootCreditExchangeAmount(gachaService == null ? 0 : gachaService.RootCredits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetRootCreditExchangeNone()
|
||||||
|
{
|
||||||
|
SetRootCreditExchangeAmount(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetRootCreditExchangeAmount(int amount)
|
||||||
|
{
|
||||||
|
int max = gachaService == null ? 0 : gachaService.RootCredits;
|
||||||
|
int clamped = Mathf.Clamp(amount, 0, max);
|
||||||
|
if (rootCreditExchangeInput != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeInput.value = clamped.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshRootCreditExchange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetRootCreditExchangeAmount()
|
||||||
|
{
|
||||||
|
if (rootCreditExchangeInput == null || string.IsNullOrWhiteSpace(rootCreditExchangeInput.value))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(rootCreditExchangeInput.value, out int amount))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int max = gachaService == null ? 0 : gachaService.RootCredits;
|
||||||
|
return Mathf.Clamp(amount, 0, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshRootCreditExchange()
|
||||||
|
{
|
||||||
|
if (gachaService == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int amount = GetRootCreditExchangeAmount();
|
||||||
|
if (rootCreditExchangeInput != null && rootCreditExchangeInput.value != amount.ToString())
|
||||||
|
{
|
||||||
|
rootCreditExchangeInput.SetValueWithoutNotify(amount.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeBalanceLabel != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeBalanceLabel.text = gachaService.RootCredits.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangePreviewLabel != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangePreviewLabel.text = $"+{amount} entropy";
|
||||||
|
}
|
||||||
|
|
||||||
|
int max = gachaService.RootCredits;
|
||||||
|
rootCreditExchangeMinusHundredButton?.SetEnabled(amount > 0);
|
||||||
|
rootCreditExchangeMinusOneButton?.SetEnabled(amount > 0);
|
||||||
|
rootCreditExchangeNoneButton?.SetEnabled(amount > 0);
|
||||||
|
rootCreditExchangePlusOneButton?.SetEnabled(amount < max);
|
||||||
|
rootCreditExchangePlusHundredButton?.SetEnabled(amount < max);
|
||||||
|
rootCreditExchangeMaxButton?.SetEnabled(max > 0 && amount < max);
|
||||||
|
rootCreditExchangeConfirmButton?.SetEnabled(amount > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfirmRootCreditExchange()
|
||||||
|
{
|
||||||
|
int amount = GetRootCreditExchangeAmount();
|
||||||
|
if (!gachaService.ConvertRootCreditsToEntropy(wallet, amount, out string failureReason))
|
||||||
|
{
|
||||||
|
if (rootCreditExchangeMessageLabel != null)
|
||||||
|
{
|
||||||
|
rootCreditExchangeMessageLabel.text = $"exchange failed: {failureReason}";
|
||||||
|
rootCreditExchangeMessageLabel.RemoveFromClassList(HiddenClassName);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshRootCreditExchange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveCurrentState();
|
||||||
|
RefreshCurrencyReadouts();
|
||||||
|
gachaScreen.Refresh();
|
||||||
|
CloseRootCreditExchange();
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleCollectionUnitsTabClicked(ClickEvent evt)
|
private void HandleCollectionUnitsTabClicked(ClickEvent evt)
|
||||||
{
|
{
|
||||||
ShowCollectionUnits();
|
ShowCollectionUnits();
|
||||||
@@ -234,6 +519,7 @@ namespace KernelPanic.UI
|
|||||||
{
|
{
|
||||||
saveData = saveService.Load();
|
saveData = saveService.Load();
|
||||||
saveData.EnsureLists();
|
saveData.EnsureLists();
|
||||||
|
wallet.SetBalance(saveData.entropyBalance);
|
||||||
|
|
||||||
for (int i = 0; i < saveData.ownedUnitIds.Count; i++)
|
for (int i = 0; i < saveData.ownedUnitIds.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -253,9 +539,35 @@ namespace KernelPanic.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gachaService.LoadProgress(saveData);
|
||||||
|
|
||||||
|
if (saveData.starterChosen)
|
||||||
|
{
|
||||||
|
AddAllFourStarDistrosToBeginnerBannerPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gachaService.BeginnerState.guaranteedDistroIds.Count == 0 && saveData.bannerPoolIds.Count > 0)
|
||||||
|
{
|
||||||
|
gachaService.BeginnerState.guaranteedDistroIds.AddRange(saveData.bannerPoolIds);
|
||||||
|
}
|
||||||
|
|
||||||
SaveCurrentState();
|
SaveCurrentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddAllFourStarDistrosToBeginnerBannerPool()
|
||||||
|
{
|
||||||
|
if (distroDatabase == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<DistroDefinition> distros = distroDatabase.AllDistros;
|
||||||
|
for (int i = 0; i < distros.Count; i++)
|
||||||
|
{
|
||||||
|
gachaService.AddToBannerPool(distros[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private DistroDefinition ResolveSavedDistro(string id)
|
private DistroDefinition ResolveSavedDistro(string id)
|
||||||
{
|
{
|
||||||
DistroDefinition unit = distroDatabase == null ? null : distroDatabase.FindById(id);
|
DistroDefinition unit = distroDatabase == null ? null : distroDatabase.FindById(id);
|
||||||
@@ -273,28 +585,31 @@ namespace KernelPanic.UI
|
|||||||
SaveCurrentState();
|
SaveCurrentState();
|
||||||
featuredUnitPanel.Refresh(playerCollection.OwnedUnits, playerCollection.FeaturedUnit);
|
featuredUnitPanel.Refresh(playerCollection.OwnedUnits, playerCollection.FeaturedUnit);
|
||||||
collectionScreen.RefreshUnits(playerCollection.OwnedUnits);
|
collectionScreen.RefreshUnits(playerCollection.OwnedUnits);
|
||||||
|
gachaScreen.Refresh();
|
||||||
|
RefreshCurrencyReadouts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleStarterConfirmed(DistroDefinition picked, IReadOnlyList<DistroDefinition> remaining)
|
private void HandleStarterConfirmed(DistroDefinition picked, IReadOnlyList<DistroDefinition> remaining)
|
||||||
{
|
{
|
||||||
playerCollection.Add(picked);
|
playerCollection.Add(picked);
|
||||||
for (int i = 0; i < remaining.Count; i++)
|
AddAllFourStarDistrosToBeginnerBannerPool();
|
||||||
{
|
|
||||||
gachaService.AddToBannerPool(remaining[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
gachaService.SetBeginnerGuaranteedDistros(remaining);
|
||||||
saveData.starterChosen = true;
|
saveData.starterChosen = true;
|
||||||
SaveCurrentState();
|
SaveCurrentState();
|
||||||
featuredUnitPanel.Refresh(playerCollection.OwnedUnits, playerCollection.FeaturedUnit);
|
featuredUnitPanel.Refresh(playerCollection.OwnedUnits, playerCollection.FeaturedUnit);
|
||||||
collectionScreen.RefreshUnits(playerCollection.OwnedUnits);
|
collectionScreen.RefreshUnits(playerCollection.OwnedUnits);
|
||||||
|
gachaScreen.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveCurrentState()
|
private void SaveCurrentState()
|
||||||
{
|
{
|
||||||
saveData ??= SaveData.CreateDefault();
|
saveData ??= SaveData.CreateDefault();
|
||||||
saveData.EnsureLists();
|
saveData.EnsureLists();
|
||||||
|
saveData.entropyBalance = wallet == null ? 0 : wallet.Balance;
|
||||||
saveData.ownedUnitIds.Clear();
|
saveData.ownedUnitIds.Clear();
|
||||||
saveData.bannerPoolIds.Clear();
|
saveData.bannerPoolIds.Clear();
|
||||||
|
gachaService.WriteProgress(saveData);
|
||||||
|
|
||||||
for (int i = 0; i < playerCollection.OwnedUnits.Count; i++)
|
for (int i = 0; i < playerCollection.OwnedUnits.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -358,7 +673,12 @@ namespace KernelPanic.UI
|
|||||||
}
|
}
|
||||||
|
|
||||||
entropyLabel.text = $"entropy={wallet.Balance}";
|
entropyLabel.text = $"entropy={wallet.Balance}";
|
||||||
pullTokensLabel.text = $"pulls={gachaService.PullTokens}";
|
rootCreditsToEntropyButton?.SetEnabled(true);
|
||||||
|
bool showPullTokens = IsGachaVisible();
|
||||||
|
pullTokensLabel.text = showPullTokens
|
||||||
|
? $"stable-pull-token={gachaService.PullTokens} feature-pull-token={gachaService.LimitedPullTokens}"
|
||||||
|
: string.Empty;
|
||||||
|
pullTokensLabel.EnableInClassList(HiddenClassName, !showPullTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshEventBanner()
|
private void RefreshEventBanner()
|
||||||
@@ -418,7 +738,8 @@ namespace KernelPanic.UI
|
|||||||
private void UpdateBootIntro()
|
private void UpdateBootIntro()
|
||||||
{
|
{
|
||||||
bootIntroElapsed += 0.016f;
|
bootIntroElapsed += 0.016f;
|
||||||
int targetCount = Mathf.Clamp(Mathf.CeilToInt(bootIntroCopy.Length * (bootIntroElapsed / BootIntroSeconds)), 0, bootIntroCopy.Length);
|
int targetCount = Mathf.Clamp(Mathf.CeilToInt(bootIntroCopy.Length * (bootIntroElapsed / BootIntroSeconds)),
|
||||||
|
0, bootIntroCopy.Length);
|
||||||
if (targetCount != bootIntroCharacterCount)
|
if (targetCount != bootIntroCharacterCount)
|
||||||
{
|
{
|
||||||
bootIntroCharacterCount = targetCount;
|
bootIntroCharacterCount = targetCount;
|
||||||
@@ -468,6 +789,26 @@ namespace KernelPanic.UI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rootCreditExchangeOpen)
|
||||||
|
{
|
||||||
|
if (evt.keyCode == KeyCode.Escape)
|
||||||
|
{
|
||||||
|
CloseRootCreditExchange();
|
||||||
|
evt.StopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
|
||||||
|
{
|
||||||
|
ConfirmRootCreditExchange();
|
||||||
|
evt.StopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.StopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (evt.keyCode == KeyCode.Escape && IsSubScreenVisible())
|
if (evt.keyCode == KeyCode.Escape && IsSubScreenVisible())
|
||||||
{
|
{
|
||||||
ShowMainMenu();
|
ShowMainMenu();
|
||||||
@@ -509,6 +850,12 @@ namespace KernelPanic.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsGachaVisible())
|
||||||
|
{
|
||||||
|
gachaScreen.HandleKeyDown(evt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (evt.keyCode == KeyCode.UpArrow)
|
if (evt.keyCode == KeyCode.UpArrow)
|
||||||
{
|
{
|
||||||
SelectCommand(selectedCommandIndex - 1);
|
SelectCommand(selectedCommandIndex - 1);
|
||||||
@@ -557,10 +904,15 @@ namespace KernelPanic.UI
|
|||||||
return collectionPanel != null && !collectionPanel.ClassListContains(HiddenClassName);
|
return collectionPanel != null && !collectionPanel.ClassListContains(HiddenClassName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsGachaVisible()
|
||||||
|
{
|
||||||
|
return gachaPanel != null && !gachaPanel.ClassListContains(HiddenClassName);
|
||||||
|
}
|
||||||
|
|
||||||
private bool IsSubScreenVisible()
|
private bool IsSubScreenVisible()
|
||||||
{
|
{
|
||||||
return IsRunSetupVisible() || IsCollectionVisible() ||
|
return IsRunSetupVisible() || IsCollectionVisible() ||
|
||||||
(gachaPanel != null && !gachaPanel.ClassListContains(HiddenClassName)) ||
|
IsGachaVisible() ||
|
||||||
(settingsPanel != null && !settingsPanel.ClassListContains(HiddenClassName));
|
(settingsPanel != null && !settingsPanel.ClassListContains(HiddenClassName));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,7 +933,8 @@ namespace KernelPanic.UI
|
|||||||
if (playerCollection.OwnedUnits.Count == 0)
|
if (playerCollection.OwnedUnits.Count == 0)
|
||||||
{
|
{
|
||||||
runSetupDetail.Add(new Label("no units installed") { name = "RunSetupEmptyTitle" });
|
runSetupDetail.Add(new Label("no units installed") { name = "RunSetupEmptyTitle" });
|
||||||
runSetupDetail.Add(new Label("install a starter or summon a unit before starting a run") { name = "RunSetupEmptyHint" });
|
runSetupDetail.Add(new Label("install a starter or summon a unit before starting a run")
|
||||||
|
{ name = "RunSetupEmptyHint" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,7 +964,9 @@ namespace KernelPanic.UI
|
|||||||
summary.Add(meta);
|
summary.Add(meta);
|
||||||
|
|
||||||
row.Add(summary);
|
row.Add(summary);
|
||||||
string passiveTitle = unit.Passive == null || string.IsNullOrWhiteSpace(unit.Passive.Name) ? "--" : unit.Passive.Name;
|
string passiveTitle = unit.Passive == null || string.IsNullOrWhiteSpace(unit.Passive.Name)
|
||||||
|
? "--"
|
||||||
|
: unit.Passive.Name;
|
||||||
row.Add(new Label($"passive: {passiveTitle}")
|
row.Add(new Label($"passive: {passiveTitle}")
|
||||||
{
|
{
|
||||||
name = $"RunSetupDescription{index}"
|
name = $"RunSetupDescription{index}"
|
||||||
@@ -652,7 +1007,8 @@ namespace KernelPanic.UI
|
|||||||
{
|
{
|
||||||
runSetupDetail?.Clear();
|
runSetupDetail?.Clear();
|
||||||
loadoutRows.Clear();
|
loadoutRows.Clear();
|
||||||
if (runSetupDetail == null || selectedPackageIndex < 0 || selectedPackageIndex >= playerCollection.OwnedUnits.Count)
|
if (runSetupDetail == null || selectedPackageIndex < 0 ||
|
||||||
|
selectedPackageIndex >= playerCollection.OwnedUnits.Count)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -678,7 +1034,10 @@ namespace KernelPanic.UI
|
|||||||
|
|
||||||
if (equippedCardIds.Count < CardLoadout.MaxEquippedCards)
|
if (equippedCardIds.Count < CardLoadout.MaxEquippedCards)
|
||||||
{
|
{
|
||||||
packageList.Add(new Label($"select {CardLoadout.MaxEquippedCards - equippedCardIds.Count} more card(s) before booting") { name = "RunSetupIncompleteNotice" });
|
packageList.Add(
|
||||||
|
new Label(
|
||||||
|
$"select {CardLoadout.MaxEquippedCards - equippedCardIds.Count} more card(s) before booting")
|
||||||
|
{ name = "RunSetupIncompleteNotice" });
|
||||||
packageList.ElementAt(packageList.childCount - 1).AddToClassList("package-notice");
|
packageList.ElementAt(packageList.childCount - 1).AddToClassList("package-notice");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -994,6 +1353,7 @@ namespace KernelPanic.UI
|
|||||||
|
|
||||||
private void ShowGacha()
|
private void ShowGacha()
|
||||||
{
|
{
|
||||||
|
gachaScreen.Open();
|
||||||
ShowPanel(gachaPanel);
|
ShowPanel(gachaPanel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1022,6 +1382,8 @@ namespace KernelPanic.UI
|
|||||||
{
|
{
|
||||||
eventBanner.AddToClassList(HiddenClassName);
|
eventBanner.AddToClassList(HiddenClassName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RefreshCurrencyReadouts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class CommandMenuEntry
|
private sealed class CommandMenuEntry
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6bfa65b8d497538499a5b84a4d7a4e9f
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 14c81f7c3ea7de425b5ba4adf237c460
|
||||||
@@ -5,8 +5,9 @@
|
|||||||
<ui:Label class="screen-frame-title" />
|
<ui:Label class="screen-frame-title" />
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
<ui:VisualElement class="screen-frame-content">
|
<ui:VisualElement class="screen-frame-content">
|
||||||
<ui:VisualElement name="GachaContent" class="stub-box">
|
<ui:VisualElement name="GachaContent" class="gacha-shell">
|
||||||
<!-- TODO: Render summon options and results here when gacha logic exists. -->
|
<ui:VisualElement name="GachaBannerList" class="gacha-banner-list" />
|
||||||
|
<ui:VisualElement name="GachaBannerDetail" class="gacha-banner-detail" />
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
<ui:VisualElement class="screen-frame-footer">
|
<ui:VisualElement class="screen-frame-footer">
|
||||||
|
|||||||
@@ -125,6 +125,29 @@ Button {
|
|||||||
margin-right: 18px;
|
margin-right: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.currency-plus-button {
|
||||||
|
width: 24px;
|
||||||
|
height: 22px;
|
||||||
|
margin-right: 12px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
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);
|
||||||
|
color: var(--kp-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-plus-button:hover {
|
||||||
|
background-color: var(--kp-amber);
|
||||||
|
color: var(--kp-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.main-panel {
|
.main-panel {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -500,6 +523,175 @@ Button {
|
|||||||
background-color: var(--kp-panel);
|
background-color: var(--kp-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gacha-shell {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-banner-list {
|
||||||
|
width: 270px;
|
||||||
|
min-width: 270px;
|
||||||
|
margin-right: 16px;
|
||||||
|
padding-top: 4px;
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right-width: 1px;
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-left-color: var(--kp-border);
|
||||||
|
border-right-color: var(--kp-border);
|
||||||
|
border-top-color: var(--kp-border);
|
||||||
|
border-bottom-color: var(--kp-border);
|
||||||
|
background-color: var(--kp-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-banner-detail {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 16px;
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right-width: 1px;
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-left-color: var(--kp-border);
|
||||||
|
border-right-color: var(--kp-border);
|
||||||
|
border-top-color: var(--kp-border);
|
||||||
|
border-bottom-color: var(--kp-border);
|
||||||
|
background-color: var(--kp-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-banner-detail .kv-value {
|
||||||
|
max-width: none;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-banner-detail .kv-key {
|
||||||
|
width: 170px;
|
||||||
|
min-width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-banner-row {
|
||||||
|
min-height: 42px;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
margin-top: 3px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
border-left-width: 2px;
|
||||||
|
border-left-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-banner-row:hover,
|
||||||
|
.gacha-banner-row.selected {
|
||||||
|
background-color: var(--kp-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-banner-row.selected {
|
||||||
|
border-left-color: var(--kp-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-detail-title {
|
||||||
|
color: var(--kp-primary);
|
||||||
|
font-size: 22px;
|
||||||
|
-unity-font-style: bold;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-hero {
|
||||||
|
min-height: 310px;
|
||||||
|
max-height: 340px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding: 14px;
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right-width: 1px;
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-left-color: var(--kp-primary-dim);
|
||||||
|
border-right-color: var(--kp-primary-dim);
|
||||||
|
border-top-color: var(--kp-primary-dim);
|
||||||
|
border-bottom-color: var(--kp-primary-dim);
|
||||||
|
background-color: var(--kp-bg-solid);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-hero-art-row {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-art-card {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-art-shell {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-height: 220px;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-art-card .ascii-art,
|
||||||
|
.gacha-ascii-art {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
height: 220px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-art-card .ascii-placeholder,
|
||||||
|
.gacha-ascii-placeholder {
|
||||||
|
width: 180px;
|
||||||
|
height: 210px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-art-name {
|
||||||
|
min-height: 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
-unity-font-style: bold;
|
||||||
|
-unity-text-align: middle-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-hero-title {
|
||||||
|
color: var(--kp-muted);
|
||||||
|
font-size: 15px;
|
||||||
|
margin-top: 8px;
|
||||||
|
-unity-text-align: middle-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-rules {
|
||||||
|
color: var(--kp-muted);
|
||||||
|
white-space: normal;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-actions {
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-action-button {
|
||||||
|
width: 220px;
|
||||||
|
min-height: 58px;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
white-space: normal;
|
||||||
|
-unity-text-align: middle-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gacha-result {
|
||||||
|
color: var(--kp-amber);
|
||||||
|
white-space: normal;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.collection-content {
|
.collection-content {
|
||||||
min-height: 280px;
|
min-height: 280px;
|
||||||
}
|
}
|
||||||
@@ -860,6 +1052,69 @@ ScrollView .unity-base-slider__dragger {
|
|||||||
background-color: var(--kp-bg-solid);
|
background-color: var(--kp-bg-solid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root-credit-dialog {
|
||||||
|
width: 82%;
|
||||||
|
height: 72%;
|
||||||
|
padding: 28px;
|
||||||
|
justify-content: center;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-summary {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-summary .kv-key {
|
||||||
|
width: 150px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-summary .kv-value {
|
||||||
|
max-width: none;
|
||||||
|
color: var(--kp-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-input {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-input Label {
|
||||||
|
color: var(--kp-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-input TextInput {
|
||||||
|
color: var(--kp-primary);
|
||||||
|
background-color: var(--kp-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-step-row,
|
||||||
|
.exchange-confirm-row {
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-step-button {
|
||||||
|
width: 120px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-confirm-button {
|
||||||
|
width: 320px;
|
||||||
|
min-height: 46px;
|
||||||
|
margin-right: 12px;
|
||||||
|
white-space: normal;
|
||||||
|
-unity-text-align: middle-center;
|
||||||
|
}
|
||||||
|
|
||||||
.starter-title {
|
.starter-title {
|
||||||
color: var(--kp-primary);
|
color: var(--kp-primary);
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<ui:Label name="AppIdLabel" class="status-text" />
|
<ui:Label name="AppIdLabel" class="status-text" />
|
||||||
<ui:VisualElement class="status-right">
|
<ui:VisualElement class="status-right">
|
||||||
<ui:Label name="EntropyLabel" class="currency-text" />
|
<ui:Label name="EntropyLabel" class="currency-text" />
|
||||||
|
<ui:Button name="RootCreditsToEntropyButton" text="+" class="currency-plus-button" focusable="false" />
|
||||||
<ui:Label name="PullTokensLabel" class="currency-text" />
|
<ui:Label name="PullTokensLabel" class="currency-text" />
|
||||||
<ui:Label text="uid=1000(player)" class="status-text" />
|
<ui:Label text="uid=1000(player)" class="status-text" />
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
@@ -188,5 +189,35 @@
|
|||||||
<ui:Label name="StarterConfirmLabel" class="starter-confirm hidden" />
|
<ui:Label name="StarterConfirmLabel" class="starter-confirm hidden" />
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
<ui:VisualElement name="RootCreditExchangeModal" class="modal-overlay hidden">
|
||||||
|
<ui:VisualElement class="root-credit-dialog">
|
||||||
|
<ui:Label text="exchange root-credits" class="starter-title" />
|
||||||
|
<ui:Label text="convert root-credits into entropy at 1:1. entropy cannot be converted back." class="starter-instruction" />
|
||||||
|
<ui:VisualElement class="exchange-summary">
|
||||||
|
<ui:VisualElement class="kv-row">
|
||||||
|
<ui:Label text="root-credits" class="kv-key" />
|
||||||
|
<ui:Label name="RootCreditExchangeBalanceLabel" class="kv-value" />
|
||||||
|
</ui:VisualElement>
|
||||||
|
<ui:VisualElement class="kv-row">
|
||||||
|
<ui:Label text="entropy gain" class="kv-key" />
|
||||||
|
<ui:Label name="RootCreditExchangePreviewLabel" class="kv-value" />
|
||||||
|
</ui:VisualElement>
|
||||||
|
</ui:VisualElement>
|
||||||
|
<ui:TextField name="RootCreditExchangeInput" label="amount" class="exchange-input" />
|
||||||
|
<ui:VisualElement class="exchange-step-row">
|
||||||
|
<ui:Button name="RootCreditExchangeMinusHundredButton" text="-100" class="terminal-button exchange-step-button" />
|
||||||
|
<ui:Button name="RootCreditExchangeMinusOneButton" text="-1" class="terminal-button exchange-step-button" />
|
||||||
|
<ui:Button name="RootCreditExchangeNoneButton" text="none" class="terminal-button exchange-step-button" />
|
||||||
|
<ui:Button name="RootCreditExchangePlusOneButton" text="+1" class="terminal-button exchange-step-button" />
|
||||||
|
<ui:Button name="RootCreditExchangePlusHundredButton" text="+100" class="terminal-button exchange-step-button" />
|
||||||
|
<ui:Button name="RootCreditExchangeMaxButton" text="max" class="terminal-button exchange-step-button" />
|
||||||
|
</ui:VisualElement>
|
||||||
|
<ui:Label name="RootCreditExchangeMessageLabel" class="starter-confirm hidden" />
|
||||||
|
<ui:VisualElement class="exchange-confirm-row">
|
||||||
|
<ui:Button name="RootCreditExchangeCancelButton" text="git merge --abort" class="terminal-button exchange-confirm-button" />
|
||||||
|
<ui:Button name="RootCreditExchangeConfirmButton" text="git merge root-credits --into entropy" class="terminal-button exchange-confirm-button" />
|
||||||
|
</ui:VisualElement>
|
||||||
|
</ui:VisualElement>
|
||||||
|
</ui:VisualElement>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
</ui:UXML>
|
</ui:UXML>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 324649d401531e204afc98d81700938e
|
||||||
|
ScriptedImporter:
|
||||||
|
internalIDToNameTable: []
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &1
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 53
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: UnityEditor.dll::UnityEditor.PackageManager.UI.Internal.PackageManagerProjectSettings
|
||||||
|
m_EnablePreReleasePackages: 0
|
||||||
|
m_AdvancedSettingsExpanded: 1
|
||||||
|
m_ScopedRegistriesSettingsExpanded: 1
|
||||||
|
m_SeeAllPackageVersions: 0
|
||||||
|
m_DismissPreviewPackagesInUse: 0
|
||||||
|
oneTimeWarningShown: 0
|
||||||
|
oneTimePackageErrorsPopUpShown: 0
|
||||||
|
m_Registries:
|
||||||
|
- m_Id: main
|
||||||
|
m_Name:
|
||||||
|
m_Url: https://packages.unity.com
|
||||||
|
m_Scopes: []
|
||||||
|
m_IsDefault: 1
|
||||||
|
m_Capabilities: 7
|
||||||
|
m_ConfigSource: 0
|
||||||
|
m_Compliance:
|
||||||
|
m_Status: 0
|
||||||
|
m_Violations: []
|
||||||
|
m_UserSelectedRegistryName:
|
||||||
|
m_UserAddingNewScopedRegistry: 0
|
||||||
|
m_RegistryInfoDraft:
|
||||||
|
m_Modified: 0
|
||||||
|
m_ErrorMessage:
|
||||||
|
m_UserModificationsEntityId:
|
||||||
|
m_Data: -940
|
||||||
|
m_OriginalEntityId:
|
||||||
|
m_Data: -942
|
||||||
|
m_LoadAssets: 0
|
||||||
@@ -298,7 +298,10 @@ PlayerSettings:
|
|||||||
m_BuildTargetShaderSettings: []
|
m_BuildTargetShaderSettings: []
|
||||||
m_BuildTargetGraphicsJobs: []
|
m_BuildTargetGraphicsJobs: []
|
||||||
m_BuildTargetGraphicsJobMode: []
|
m_BuildTargetGraphicsJobMode: []
|
||||||
m_BuildTargetGraphicsAPIs: []
|
m_BuildTargetGraphicsAPIs:
|
||||||
|
- m_BuildTarget: LinuxStandaloneSupport
|
||||||
|
m_APIs: 1500000011000000
|
||||||
|
m_Automatic: 0
|
||||||
m_BuildTargetVRSettings: []
|
m_BuildTargetVRSettings: []
|
||||||
m_DefaultShaderChunkSizeInMB: 16
|
m_DefaultShaderChunkSizeInMB: 16
|
||||||
m_DefaultShaderChunkCount: 0
|
m_DefaultShaderChunkCount: 0
|
||||||
|
|||||||
@@ -0,0 +1,337 @@
|
|||||||
|
# Gacha Implementation Guide
|
||||||
|
|
||||||
|
This project currently has a working beginner banner and scaffolding for standard and limited banners.
|
||||||
|
The main files are:
|
||||||
|
|
||||||
|
- `Assets/Scripts/Meta/GachaService.cs`: pull costs, odds, pity, currency accounting, reward rolls.
|
||||||
|
- `Assets/Scripts/Meta/GachaBannerState.cs`: persistent per-banner counters.
|
||||||
|
- `Assets/Scripts/Meta/GachaCurrencyType.cs`: pull-token currency enum.
|
||||||
|
- `Assets/Scripts/Meta/SaveService.cs`: saved currency and banner progress fields.
|
||||||
|
- `Assets/Scripts/UI/GachaScreenController.cs`: banner list, banner names, pull buttons, pull prompts, result text.
|
||||||
|
- `Assets/Scripts/UI/MainMenuController.cs`: wallet/save refresh, root-credit exchange popup, gacha status readouts.
|
||||||
|
- `Assets/UI/MainMenu.uxml` and `Assets/UI/MainMenu.uss`: exchange popup and gacha/status styling.
|
||||||
|
|
||||||
|
## Adding A New Banner
|
||||||
|
|
||||||
|
1. Add a banner id in `GachaService.cs`.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public const string EventBannerId = "event";
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add a persistent state field in `GachaService.cs`.
|
||||||
|
Standard and limited banners should each have their own `GachaBannerState`, separate from `beginnerState`, so pity does not leak between banners.
|
||||||
|
|
||||||
|
3. Add save fields in `SaveData` inside `SaveService.cs`.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public GachaBannerState eventBannerState = new(GachaService.EventBannerId);
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update `EnsureLists()`, `GachaService.LoadProgress()`, and `GachaService.WriteProgress()` to load and save that state.
|
||||||
|
|
||||||
|
4. Add the banner id to `bannerIds` in `GachaScreenController.cs`.
|
||||||
|
|
||||||
|
5. Add a title/status entry in `GetBannerTitle()` and `GetBannerStatus()` in `GachaScreenController.cs`.
|
||||||
|
|
||||||
|
6. Add rendering for the banner in `RenderSelectedBanner()`.
|
||||||
|
Use `RenderBeginnerBanner()` as the template, but use the new banner state, pull costs, and currency.
|
||||||
|
|
||||||
|
7. Add pull request and execution methods in `GachaService.cs`.
|
||||||
|
The beginner versions are:
|
||||||
|
|
||||||
|
- `GetBeginnerPullCost`
|
||||||
|
- `CanPullBeginner`
|
||||||
|
- `PerformBeginnerPull`
|
||||||
|
- `RollBeginnerReward`
|
||||||
|
- `CanPullBeginnerWithEntropy`
|
||||||
|
|
||||||
|
Copy this shape for the new banner, then change the currency, state, pity, and reward pool.
|
||||||
|
|
||||||
|
## Currency Rules
|
||||||
|
|
||||||
|
- `GachaCurrencyType.StandardPull` formats as `stable-pull-token`.
|
||||||
|
- `GachaCurrencyType.LimitedPull` formats as `feature-pull-token`.
|
||||||
|
- Beginner and standard banners should spend `StandardPull`.
|
||||||
|
- Limited and event banners should usually spend `LimitedPull`, unless the design says otherwise.
|
||||||
|
- `EntropyPerPullToken` controls the entropy fallback cost for each missing pull token.
|
||||||
|
- Root-credits convert to entropy through `GachaService.ConvertRootCreditsToEntropy()`.
|
||||||
|
|
||||||
|
To add a new pull currency:
|
||||||
|
|
||||||
|
1. Add a value to `GachaCurrencyType.cs`.
|
||||||
|
2. Initialize it in the `GachaService` constructor.
|
||||||
|
3. Add save fields in `SaveData`.
|
||||||
|
4. Load/save it in `GachaService.LoadProgress()` and `WriteProgress()`.
|
||||||
|
5. Add display text in `GachaService.FormatCurrencyName()`.
|
||||||
|
6. Update the gacha status readout in `MainMenuController.RefreshCurrencyReadouts()`.
|
||||||
|
|
||||||
|
## Adjusting Pull Costs
|
||||||
|
|
||||||
|
Beginner costs are constants in `GachaService.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public const int BeginnerSinglePullCost = 1;
|
||||||
|
public const int BeginnerTenPullCost = 8;
|
||||||
|
```
|
||||||
|
|
||||||
|
For standard or limited banners, add equivalent constants:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public const int StandardSinglePullCost = 1;
|
||||||
|
public const int StandardTenPullCost = 10;
|
||||||
|
public const int LimitedSinglePullCost = 1;
|
||||||
|
public const int LimitedTenPullCost = 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use those constants in the matching `Get...PullCost()` method and in the pull button labels in `GachaScreenController.cs`.
|
||||||
|
|
||||||
|
## Adjusting Odds
|
||||||
|
|
||||||
|
The current base roll order is:
|
||||||
|
|
||||||
|
1. Check for a 5-star result.
|
||||||
|
2. If no 5-star, check for a 4-star-or-better result.
|
||||||
|
3. If no 4-star-or-better result, award the base 3-star equipment cache.
|
||||||
|
|
||||||
|
The base non-pity tier rates are:
|
||||||
|
|
||||||
|
- 5-star item: `0.8%`
|
||||||
|
- 4-star item: `11.904%`
|
||||||
|
- 3-star equipment: `87.296%`
|
||||||
|
|
||||||
|
After a 4-star or 5-star item is rolled, the game then does a 50/50 split:
|
||||||
|
|
||||||
|
- equipment
|
||||||
|
- distro
|
||||||
|
|
||||||
|
The effective split rates are still useful for sanity checks, but they are not separate primary drop rates:
|
||||||
|
|
||||||
|
- `5-star equipment = 0.8% 5-star item * 50% equipment = 0.4%`
|
||||||
|
- `5-star distro = 0.8% 5-star item * 50% distro = 0.4%`
|
||||||
|
- `4-star item = 99.2% no 5-star * 12% 4-star item = 11.904%`
|
||||||
|
- `4-star equipment = 11.904% 4-star item * 50% equipment = 5.952%`
|
||||||
|
- `4-star distro = 11.904% 4-star item * 50% distro = 5.952%`
|
||||||
|
- `3-star equipment = 99.2% no 5-star * 88% no 4-star hit = 87.296%`
|
||||||
|
|
||||||
|
The current odds constants are in `GachaService.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private const double FiveStarBaseChance = 0.008d;
|
||||||
|
private const double FourStarBaseChance = 0.12d;
|
||||||
|
private const double DistroChanceOnFeaturedTier = 0.5d;
|
||||||
|
```
|
||||||
|
|
||||||
|
`FiveStarBaseChance` is the total base 5-star chance.
|
||||||
|
`FourStarBaseChance` decides whether a non-5-star pull upgrades from the base 3-star equipment cache to a 4-star result.
|
||||||
|
`DistroChanceOnFeaturedTier` is the 50/50 distro-vs-equipment split after a 4-star or 5-star item has already been rolled.
|
||||||
|
|
||||||
|
## Five-Star Soft Pity
|
||||||
|
|
||||||
|
Five-star pity uses `fiveStarPityCounter` in `GachaBannerState`.
|
||||||
|
It is separate from the 4-star `pityCounter`.
|
||||||
|
|
||||||
|
Current 5-star constants:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public const int FiveStarSoftPityStart = 66;
|
||||||
|
public const int FiveStarHardPity = 80;
|
||||||
|
private const double FiveStarBaseChance = 0.008d;
|
||||||
|
private const double FiveStarSoftPityStep = 0.07d;
|
||||||
|
```
|
||||||
|
|
||||||
|
The total 5-star chance table is:
|
||||||
|
|
||||||
|
- pity pulls 1-65: `0.8%`
|
||||||
|
- pity pull 66: `7.8%`
|
||||||
|
- pity pull 67: `14.8%`
|
||||||
|
- pity pull 68: `21.8%`
|
||||||
|
- pity pull 69: `28.8%`
|
||||||
|
- pity pull 70: `35.8%`
|
||||||
|
- pity pull 71: `42.8%`
|
||||||
|
- pity pull 72: `49.8%`
|
||||||
|
- pity pull 73: `56.8%`
|
||||||
|
- pity pull 74: `63.8%`
|
||||||
|
- pity pull 75: `70.8%`
|
||||||
|
- pity pull 76: `77.8%`
|
||||||
|
- pity pull 77: `84.8%`
|
||||||
|
- pity pull 78: `91.8%`
|
||||||
|
- pity pull 79: `98.8%`
|
||||||
|
- pity pull 80: guaranteed by hard pity
|
||||||
|
|
||||||
|
The raw formula reaches `105.8%` on pull 80, so `GetFiveStarChance()` clamps the effective chance to `100%`.
|
||||||
|
|
||||||
|
A random 5-star resets both:
|
||||||
|
|
||||||
|
- `fiveStarPityCounter`
|
||||||
|
- the 4-star `pityCounter`
|
||||||
|
|
||||||
|
The beginner banner's pull-50 capstone guarantee awards the placeholder 5-star but intentionally does not reset `fiveStarPityCounter`.
|
||||||
|
|
||||||
|
## Adjusting Pity
|
||||||
|
|
||||||
|
Current 4-star pity is shared as a constant:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public const int FourStarHardPity = 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
The pity check happens in `RollBeginnerReward()`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
bool pityTriggered = beginnerState.pityCounter >= FourStarHardPity - 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
When adding standard or limited banners:
|
||||||
|
|
||||||
|
1. Use that banner's own `GachaBannerState`.
|
||||||
|
2. Increment only that state's `pityCounter` after a non-upgraded result.
|
||||||
|
3. Reset only that state's `pityCounter` after a 4-star-or-better result.
|
||||||
|
4. Increment only that state's `fiveStarPityCounter` after a non-5-star result.
|
||||||
|
5. Reset only that state's `fiveStarPityCounter` after a random 5-star result.
|
||||||
|
6. Add separate constants if banners need different pity values.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public const int LimitedFourStarHardPity = 10;
|
||||||
|
public const int LimitedFiveStarHardPity = 90;
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not reuse `pityCounter` for 5-star pity. Use `fiveStarPityCounter`.
|
||||||
|
|
||||||
|
## Adjusting Rewards
|
||||||
|
|
||||||
|
Rewards are represented by `GachaReward` in `GachaService.cs`.
|
||||||
|
Current reward constructors are:
|
||||||
|
|
||||||
|
- `GachaReward.Equipment(starRating, displayName, pityTriggered, guaranteed)`
|
||||||
|
- `GachaReward.DistroReward(distro, starRating, pityTriggered, guaranteed, guaranteeReason)`
|
||||||
|
- `GachaReward.FutureStandardFiveStar(displayName, guaranteeReason)`
|
||||||
|
|
||||||
|
To add real equipment:
|
||||||
|
|
||||||
|
1. Create an equipment data type and database.
|
||||||
|
2. Add an equipment reference/id to `GachaReward`.
|
||||||
|
3. Add a reward constructor for the equipment item.
|
||||||
|
4. Update `GachaPullResult` and collection/inventory code to persist duplicates.
|
||||||
|
5. Update `GachaScreenController.PullSelectedBanner()` to add equipment to the player inventory and print the result.
|
||||||
|
|
||||||
|
To change possible distro rewards:
|
||||||
|
|
||||||
|
1. Change the pool selection in the banner's `Roll...Reward()` method.
|
||||||
|
2. For starter guarantees, update `SetBeginnerGuaranteedDistros()` and `RewardGuaranteedBeginnerDistro()`.
|
||||||
|
3. For standard/limited pools, prefer explicit pool methods over reusing the beginner `bannerPool` if the pools differ.
|
||||||
|
|
||||||
|
## Beginner Banner Guarantees
|
||||||
|
|
||||||
|
Beginner special cases currently live in `RollBeginnerReward()`:
|
||||||
|
|
||||||
|
- pull 20: guaranteed first unpicked starter distro
|
||||||
|
- pull 40: guaranteed second unpicked starter distro
|
||||||
|
- pull 50: placeholder future standard 5-star distro, without resetting 5-star pity
|
||||||
|
|
||||||
|
The guaranteed distro ids are stored in:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
beginnerState.guaranteedDistroIds
|
||||||
|
```
|
||||||
|
|
||||||
|
They are set from `MainMenuController.HandleStarterConfirmed()` through:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
gachaService.SetBeginnerGuaranteedDistros(remaining);
|
||||||
|
```
|
||||||
|
|
||||||
|
To change guarantee milestones, edit the pull-number checks in `RollBeginnerReward()` and update `FormatBeginnerGuarantees()` in `GachaScreenController.cs`.
|
||||||
|
|
||||||
|
## Limited Banner 5-Star Rule
|
||||||
|
|
||||||
|
Limited banners use a lenient featured guarantee:
|
||||||
|
|
||||||
|
- When a limited-banner 5-star item is rolled, first do the normal 50/50 equipment-vs-distro split.
|
||||||
|
- If the 50/50 lands on equipment, award 5-star equipment and do not touch the limited featured guarantee.
|
||||||
|
- If the 50/50 lands on a distro/character, there is a `1/3` chance it uses the standard 5-star pool.
|
||||||
|
- If that happens, `featuredFiveStarGuaranteed` is set on that banner's `GachaBannerState`.
|
||||||
|
- The next limited-banner 5-star distro/character ignores the `1/3` standard-pool chance and must use the limited featured pool.
|
||||||
|
- After the guaranteed featured 5-star is awarded, `featuredFiveStarGuaranteed` is cleared.
|
||||||
|
|
||||||
|
The helper for this is:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
ResolveLimitedFiveStarUsesStandardPool(limitedState)
|
||||||
|
```
|
||||||
|
|
||||||
|
It returns `true` when the 5-star should come from the standard pool and `false` when it should come from the featured limited pool.
|
||||||
|
Call it only after the pull has already rolled a 5-star on a limited banner and the 50/50 split landed on distro/character.
|
||||||
|
|
||||||
|
## Standard Banner 4-Star And 5-Star Rule
|
||||||
|
|
||||||
|
Standard banner 4-star and 5-star hits should split 50/50 between equipment and distros after the tier is rolled.
|
||||||
|
The helper for this is:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
ResolveFeaturedTierIsDistro()
|
||||||
|
```
|
||||||
|
|
||||||
|
It returns `true` for a distro and `false` for equipment.
|
||||||
|
Call it only after the banner has already rolled a 4-star or 5-star item.
|
||||||
|
|
||||||
|
## Banner Names And UI Text
|
||||||
|
|
||||||
|
Banner display names are in `GachaScreenController.GetBannerTitle()`.
|
||||||
|
Banner status text on the left list is in `GetBannerStatus()`.
|
||||||
|
Pull button labels are in the banner render method, currently `RenderBeginnerBanner()`.
|
||||||
|
|
||||||
|
Keep these in sync:
|
||||||
|
|
||||||
|
1. `GachaService` banner id constant.
|
||||||
|
2. `bannerIds` array in `GachaScreenController`.
|
||||||
|
3. `GetBannerTitle()`.
|
||||||
|
4. `GetBannerStatus()`.
|
||||||
|
5. Any result text in `PullSelectedBanner()`.
|
||||||
|
|
||||||
|
## Standard Banner Checklist
|
||||||
|
|
||||||
|
1. Add `standardState` in `GachaService`.
|
||||||
|
2. Save/load `standardBannerState` in `SaveData`.
|
||||||
|
3. Add `StandardSinglePullCost` and `StandardTenPullCost`.
|
||||||
|
4. Implement `GetStandardPullCost()`.
|
||||||
|
5. Implement `CanPullStandard()`.
|
||||||
|
6. Implement `PerformStandardPull()`.
|
||||||
|
7. Implement `RollStandardReward()`.
|
||||||
|
8. Use `GachaCurrencyType.StandardPull`.
|
||||||
|
9. On a 4-star or 5-star hit, call `ResolveFeaturedTierIsDistro()`.
|
||||||
|
10. If it returns `true`, pick from the matching standard distro pool.
|
||||||
|
11. If it returns `false`, award matching-rarity equipment.
|
||||||
|
12. Render real standard controls in `GachaScreenController` instead of `RenderFutureBanner()`.
|
||||||
|
13. Add result handling for standard rewards.
|
||||||
|
|
||||||
|
## Limited Banner Checklist
|
||||||
|
|
||||||
|
1. Add `limitedState` in `GachaService`.
|
||||||
|
2. Save/load `limitedBannerState` in `SaveData`.
|
||||||
|
3. Add `LimitedSinglePullCost` and `LimitedTenPullCost`.
|
||||||
|
4. Implement `GetLimitedPullCost()`.
|
||||||
|
5. Implement `CanPullLimited()`.
|
||||||
|
6. Implement `PerformLimitedPull()`.
|
||||||
|
7. Implement `RollLimitedReward()`.
|
||||||
|
8. Use `GachaCurrencyType.LimitedPull`.
|
||||||
|
9. On a 4-star or 5-star hit, call `ResolveFeaturedTierIsDistro()`.
|
||||||
|
10. If it returns `false`, award matching-rarity equipment.
|
||||||
|
11. If it returns `true` on a 4-star, pick from the limited 4-star distro pool.
|
||||||
|
12. If it returns `true` on a 5-star, call `ResolveLimitedFiveStarUsesStandardPool(limitedState)`.
|
||||||
|
13. If that returns `true`, pick from the standard 5-star pool.
|
||||||
|
14. If that returns `false`, pick from the featured limited pool.
|
||||||
|
15. Add featured-unit or rate-up pool configuration.
|
||||||
|
16. Render real limited controls in `GachaScreenController`.
|
||||||
|
|
||||||
|
## Testing Changes
|
||||||
|
|
||||||
|
After changing gacha code:
|
||||||
|
|
||||||
|
1. Compile the Unity runtime assembly.
|
||||||
|
2. Test single pull, ten pull, insufficient-token flow, entropy fallback, root-credit exchange, duplicate distro reward, pity reset, and saved progress reload.
|
||||||
|
3. For new banners, verify that pity counters are independent by pulling on one banner, switching banners, and checking the other banner state did not change.
|
||||||
Reference in New Issue
Block a user