111 lines
4.1 KiB
C#
111 lines
4.1 KiB
C#
using SVSim.Database.Enums;
|
|
using SVSim.Database.Models;
|
|
|
|
namespace SVSim.EmulatedEntrypoint.Services;
|
|
|
|
/// <summary>
|
|
/// Draws cards from a pack's pool using the original Shadowverse Classic rates:
|
|
/// Slots 1-7: Bronze 67.44% / Silver 25% / Gold 6% / Legendary 1.5%
|
|
/// Slot 8: Silver 76.92% / Gold 18.46% / Legendary 4.62% (no Bronze)
|
|
/// Legendary-special packs (category 2/3, base >= 90001): slot 8 forced to Legendary.
|
|
///
|
|
/// The 0.06% slack in slots 1-7 (rates sum to 99.94%) is folded into Bronze so cumulative
|
|
/// weights add to exactly 1.0 — any RNG roll past the Gold band lands in either Legendary or
|
|
/// Bronze, and we put it in Bronze to err on the player-unfriendly side of the spec.
|
|
/// </summary>
|
|
public class PackOpenService
|
|
{
|
|
private const int CardsPerPack = 8;
|
|
|
|
public DrawResult Draw(
|
|
PackConfigEntry pack,
|
|
ICardPoolProvider pools,
|
|
int packNumber,
|
|
IReadOnlyCollection<long> excludeCardIds,
|
|
IRandom rng)
|
|
{
|
|
var pool = pools.GetPool(pack);
|
|
if (pool.Count == 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"PackOpenService: pool for pack {pack.Id} (category {pack.PackCategory}) is empty.");
|
|
}
|
|
|
|
var poolByRarity = pool
|
|
.Where(c => !excludeCardIds.Contains(c.Id))
|
|
.GroupBy(c => c.Rarity)
|
|
.ToDictionary(g => g.Key, g => g.ToList());
|
|
|
|
bool isLegendarySpecial =
|
|
pack.PackCategory == PackCategory.SpecialCardPack ||
|
|
pack.PackCategory == PackCategory.LimitedSpecialCardPack;
|
|
|
|
var slots = new List<DrawnCard>(packNumber * CardsPerPack);
|
|
for (int p = 0; p < packNumber; p++)
|
|
{
|
|
for (int s = 0; s < CardsPerPack; s++)
|
|
{
|
|
Rarity rarity;
|
|
if (s == CardsPerPack - 1)
|
|
{
|
|
// Slot 8
|
|
if (isLegendarySpecial)
|
|
{
|
|
rarity = Rarity.Legendary;
|
|
}
|
|
else
|
|
{
|
|
rarity = PickSlot8Rarity(rng);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
rarity = PickSlot1To7Rarity(rng);
|
|
}
|
|
|
|
var card = PickCardOfRarity(rarity, poolByRarity, rng);
|
|
slots.Add(new DrawnCard(card.Id, card.Rarity));
|
|
}
|
|
}
|
|
return new DrawResult(slots);
|
|
}
|
|
|
|
private static Rarity PickSlot1To7Rarity(IRandom rng)
|
|
{
|
|
double r = rng.NextDouble();
|
|
// Build cumulative bands in this order: Legendary -> Gold -> Silver -> Bronze (catch-all).
|
|
if (r < 0.0150) return Rarity.Legendary; // 1.5%
|
|
if (r < 0.0750) return Rarity.Gold; // +6% = 7.5%
|
|
if (r < 0.3250) return Rarity.Silver; // +25% = 32.5%
|
|
return Rarity.Bronze; // remaining (~67.5%; absorbs 0.06% slack)
|
|
}
|
|
|
|
private static Rarity PickSlot8Rarity(IRandom rng)
|
|
{
|
|
double r = rng.NextDouble();
|
|
// Renormalized over 32.5: Legendary 4.62%, Gold 18.46%, Silver 76.92%.
|
|
if (r < 0.0462) return Rarity.Legendary;
|
|
if (r < 0.2308) return Rarity.Gold; // 0.0462 + 0.1846
|
|
return Rarity.Silver;
|
|
}
|
|
|
|
private static ShadowverseCardEntry PickCardOfRarity(
|
|
Rarity rarity,
|
|
Dictionary<Rarity, List<ShadowverseCardEntry>> poolByRarity,
|
|
IRandom rng)
|
|
{
|
|
// Fallback if the rolled rarity has no cards (e.g. pool has no Legendaries):
|
|
// walk down to Gold -> Silver -> Bronze. This is a safety net for sparse master data;
|
|
// healthy production pools have all four rarities.
|
|
Rarity[] fallback = { rarity, Rarity.Gold, Rarity.Silver, Rarity.Bronze };
|
|
foreach (var r in fallback)
|
|
{
|
|
if (poolByRarity.TryGetValue(r, out var list) && list.Count > 0)
|
|
{
|
|
return list[rng.Next(list.Count)];
|
|
}
|
|
}
|
|
throw new InvalidOperationException("PackOpenService: pool empty after exclude filter.");
|
|
}
|
|
}
|