Files
Ilya Groshev 02f511f40c Initial commit
2026-04-14 09:28:26 +03:00

224 lines
5.9 KiB
Go

package gacha
import (
"log"
"math/rand"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model"
)
type RateTier struct {
Weight int
PossessionType int32
RarityType model.RarityType
}
type DrawnItem struct {
PossessionType int32
PossessionId int32
RarityType model.RarityType
CharacterId int32
}
var premiumRates = []RateTier{
{200, int32(model.PossessionTypeCostume), model.RaritySSRare},
{300, int32(model.PossessionTypeWeapon), model.RaritySSRare},
{500, int32(model.PossessionTypeCostume), model.RaritySRare},
{1000, int32(model.PossessionTypeWeapon), model.RaritySRare},
{8000, int32(model.PossessionTypeWeapon), model.RarityRare},
}
func DrawPremium(bp *masterdata.BannerPool, count int, fixedRarityMin int32, fixedCount int, rateMultiplier float64) []DrawnItem {
result := make([]DrawnItem, 0, count)
rates := adjustRates(premiumRates, rateMultiplier)
totalWeight := 0
for _, r := range rates {
totalWeight += r.Weight
}
for i := range count {
isGuaranteeSlot := fixedCount > 0 && i >= count-fixedCount
item := rollOne(bp, rates, totalWeight)
if isGuaranteeSlot && item.RarityType < fixedRarityMin {
item = rollAtMinRarity(bp, rates, fixedRarityMin)
}
result = append(result, item)
}
return result
}
func DrawBox(items []BoxItem, count int) []DrawnItem {
var available []int
for i, item := range items {
remaining := item.MaxCount - item.DrewCount
for range remaining {
available = append(available, i)
}
}
result := make([]DrawnItem, 0, count)
for i := 0; i < count && len(available) > 0; i++ {
pick := rand.Intn(len(available))
idx := available[pick]
item := items[idx]
result = append(result, DrawnItem{
PossessionType: item.PossessionType,
PossessionId: item.PossessionId,
RarityType: item.RarityType,
})
items[idx].DrewCount++
available = append(available[:pick], available[pick+1:]...)
}
return result
}
func DrawReward(materials []masterdata.GachaPoolItem, count int) []DrawnItem {
if len(materials) == 0 {
return nil
}
result := make([]DrawnItem, 0, count)
for range count {
m := materials[rand.Intn(len(materials))]
result = append(result, DrawnItem{
PossessionType: m.PossessionType,
PossessionId: m.PossessionId,
RarityType: m.RarityType,
})
}
return result
}
type BoxItem struct {
PossessionType int32
PossessionId int32
RarityType model.RarityType
Count int32
MaxCount int32
DrewCount int32
IsTarget bool
}
func adjustRates(base []RateTier, multiplier float64) []RateTier {
if multiplier == 1.0 || multiplier == 0 {
return base
}
adjusted := make([]RateTier, len(base))
copy(adjusted, base)
var fourStarExtra int
var nonFourStar int
for i, r := range adjusted {
if r.RarityType >= model.RaritySSRare {
extra := int(float64(r.Weight) * (multiplier - 1.0))
adjusted[i].Weight += extra
fourStarExtra += extra
} else {
nonFourStar += r.Weight
}
}
if nonFourStar > 0 && fourStarExtra > 0 {
for i, r := range adjusted {
if r.RarityType < model.RaritySSRare {
reduction := fourStarExtra * r.Weight / nonFourStar
adjusted[i].Weight -= reduction
if adjusted[i].Weight < 1 {
adjusted[i].Weight = 1
}
}
}
}
return adjusted
}
func rollOne(bp *masterdata.BannerPool, rates []RateTier, totalWeight int) DrawnItem {
roll := rand.Intn(totalWeight)
cumulative := 0
var tier RateTier
for _, r := range rates {
cumulative += r.Weight
if roll < cumulative {
tier = r
break
}
}
if item, ok := tryFeaturedRateUp(bp, tier); ok {
return item
}
return pickFromPool(bp, tier.PossessionType, tier.RarityType)
}
func tryFeaturedRateUp(bp *masterdata.BannerPool, tier RateTier) (DrawnItem, bool) {
var matches []masterdata.GachaPoolItem
for _, f := range bp.Featured {
if f.PossessionType == tier.PossessionType && f.RarityType == tier.RarityType {
matches = append(matches, f)
}
}
if len(matches) == 0 {
return DrawnItem{}, false
}
if rand.Intn(model.FeaturedRateUpDenom) >= model.FeaturedRateUpPercent {
return DrawnItem{}, false
}
f := matches[rand.Intn(len(matches))]
return DrawnItem{
PossessionType: f.PossessionType,
PossessionId: f.PossessionId,
RarityType: f.RarityType,
CharacterId: f.CharacterId,
}, true
}
func rollAtMinRarity(bp *masterdata.BannerPool, rates []RateTier, minRarity model.RarityType) DrawnItem {
var filtered []RateTier
filteredTotal := 0
for _, r := range rates {
if r.RarityType >= minRarity {
filtered = append(filtered, r)
filteredTotal += r.Weight
}
}
if filteredTotal == 0 {
return pickFromPool(bp, int32(model.PossessionTypeWeapon), minRarity)
}
return rollOne(bp, filtered, filteredTotal)
}
func pickFromPool(bp *masterdata.BannerPool, possessionType int32, rarityType model.RarityType) DrawnItem {
if possessionType == int32(model.PossessionTypeCostume) {
items := bp.CostumesByRarity[rarityType]
if len(items) == 0 {
items = bp.CostumesByRarity[model.RaritySSRare]
}
if len(items) == 0 {
log.Printf("[pickFromPool] empty costume pool for rarity=%d, returning phantom item", rarityType)
return DrawnItem{PossessionType: int32(model.PossessionTypeWeapon), RarityType: rarityType}
}
pick := items[rand.Intn(len(items))]
return DrawnItem{
PossessionType: pick.PossessionType,
PossessionId: pick.PossessionId,
RarityType: pick.RarityType,
CharacterId: pick.CharacterId,
}
}
items := bp.WeaponsByRarity[rarityType]
if len(items) == 0 {
items = bp.WeaponsByRarity[model.RarityRare]
}
if len(items) == 0 {
log.Printf("[pickFromPool] empty weapon pool for rarity=%d, returning phantom item", rarityType)
return DrawnItem{PossessionType: int32(model.PossessionTypeWeapon), RarityType: rarityType}
}
pick := items[rand.Intn(len(items))]
return DrawnItem{
PossessionType: pick.PossessionType,
PossessionId: pick.PossessionId,
RarityType: pick.RarityType,
}
}