338 lines
12 KiB
Markdown
338 lines
12 KiB
Markdown
# 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.
|