You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

208 lines
5.8 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package model
import (
"math/rand"
"time"
)
// Rarity represents item rarity tiers. Shared across weapons, armor, and loot.
type Rarity string
const (
RarityCommon Rarity = "common"
RarityUncommon Rarity = "uncommon"
RarityRare Rarity = "rare"
RarityEpic Rarity = "epic"
RarityLegendary Rarity = "legendary"
)
// DropChance maps rarity to its drop probability (0.0 to 1.0).
var DropChance = map[Rarity]float64{
RarityCommon: 0.40,
RarityUncommon: 0.10,
RarityRare: 0.02,
RarityEpic: 0.003,
RarityLegendary: 0.0005,
}
// GoldRange defines minimum and maximum gold drops per rarity.
type GoldRange struct {
Min int64
Max int64
}
// GoldRanges maps rarity to gold drop ranges.
var GoldRanges = map[Rarity]GoldRange{
RarityCommon: {Min: 0, Max: 5},
RarityUncommon: {Min: 6, Max: 20},
RarityRare: {Min: 21, Max: 50},
RarityEpic: {Min: 51, Max: 120},
RarityLegendary: {Min: 121, Max: 300},
}
// LootDrop represents a single item or gold drop from defeating an enemy.
type LootDrop struct {
ItemType string `json:"itemType"` // "gold", "potion", or EquipmentSlot ("main_hand", "chest", "head", etc.)
ItemID int64 `json:"itemId,omitempty"` // ID of the weapon or armor, 0 for gold
ItemName string `json:"itemName,omitempty"` // display name when equipped / dropped
Rarity Rarity `json:"rarity"`
GoldAmount int64 `json:"goldAmount,omitempty"` // gold value of this drop
}
// LootHistory records a loot drop event for audit/analytics.
type LootHistory struct {
ID int64 `json:"id"`
HeroID int64 `json:"heroId"`
EnemyType string `json:"enemyType"`
ItemType string `json:"itemType"`
ItemID int64 `json:"itemId,omitempty"`
Rarity Rarity `json:"rarity"`
GoldAmount int64 `json:"goldAmount"`
CreatedAt time.Time `json:"createdAt"`
}
// AutoSellPrices maps rarity to the gold value obtained by auto-selling an
// equipment drop that the hero doesn't need.
var AutoSellPrices = map[Rarity]int64{
RarityCommon: 3,
RarityUncommon: 8,
RarityRare: 20,
RarityEpic: 60,
RarityLegendary: 180,
}
// RollRarity rolls a random rarity based on the drop chance table.
// It uses a cumulative probability approach, checking from rarest to most common.
func RollRarity() Rarity {
return RarityFromRoll(rand.Float64())
}
// RarityFromRoll maps a uniform [0,1) value to a rarity tier (spec §8.1 drop bands).
func RarityFromRoll(roll float64) Rarity {
if roll < DropChance[RarityLegendary] {
return RarityLegendary
}
if roll < DropChance[RarityLegendary]+DropChance[RarityEpic] {
return RarityEpic
}
if roll < DropChance[RarityLegendary]+DropChance[RarityEpic]+DropChance[RarityRare] {
return RarityRare
}
if roll < DropChance[RarityLegendary]+DropChance[RarityEpic]+DropChance[RarityRare]+DropChance[RarityUncommon] {
return RarityUncommon
}
return RarityCommon
}
// RollGold returns a random gold amount for the given rarity.
func RollGold(rarity Rarity) int64 {
return RollGoldWithRNG(rarity, nil)
}
// RollGoldWithRNG returns spec §8.2 gold for a rarity tier; if rng is nil, uses the global RNG.
func RollGoldWithRNG(rarity Rarity, rng *rand.Rand) int64 {
gr, ok := GoldRanges[rarity]
if !ok {
return 0
}
if gr.Max <= gr.Min {
return gr.Min
}
var n int64
if rng == nil {
n = gr.Min + rand.Int63n(gr.Max-gr.Min+1)
} else {
n = gr.Min + rng.Int63n(gr.Max-gr.Min+1)
}
// MVP balance: reduce gold loot rate vs spec table (plates longer progression).
const goldLootScale = 0.5
return int64(float64(n) * goldLootScale)
}
// equipmentLootSlots maps loot ItemType strings to relative weights.
// All item types now use unified EquipmentSlot names.
var equipmentLootSlots = []struct {
itemType string
weight float64
}{
{string(SlotMainHand), 0.05},
{string(SlotChest), 0.05},
{string(SlotHead), 0.05},
{string(SlotFeet), 0.05},
{string(SlotNeck), 0.05},
{string(SlotHands), 0.05},
{string(SlotLegs), 0.05},
{string(SlotCloak), 0.05},
{string(SlotFinger), 0.05},
{string(SlotWrist), 0.05},
}
func rollEquipmentLootItemType(float01 func() float64) string {
r := float01()
var acc float64
for _, row := range equipmentLootSlots {
acc += row.weight
if r < acc {
return row.itemType
}
}
return equipmentLootSlots[len(equipmentLootSlots)-1].itemType
}
// GenerateLoot generates loot drops from defeating an enemy (preview / tests).
// Guaranteed gold uses a spec rarity band; optional equipment is independent and does not replace gold.
func GenerateLoot(enemyType EnemyType, luckMultiplier float64) []LootDrop {
return GenerateLootWithRNG(enemyType, luckMultiplier, nil)
}
// GenerateLootWithRNG is GenerateLoot with an optional RNG for deterministic tests.
func GenerateLootWithRNG(enemyType EnemyType, luckMultiplier float64, rng *rand.Rand) []LootDrop {
var drops []LootDrop
float01 := func() float64 {
if rng == nil {
return rand.Float64()
}
return rng.Float64()
}
// Gold tier roll (spec §8.18.2); independent of whether an item drops later.
goldRarity := RarityFromRoll(float01())
goldAmount := RollGoldWithRNG(goldRarity, rng)
if luckMultiplier > 1 {
goldAmount = int64(float64(goldAmount) * luckMultiplier)
}
drops = append(drops, LootDrop{
ItemType: "gold",
Rarity: goldRarity,
GoldAmount: goldAmount,
})
// 5% chance to drop a healing potion (heals 30% of maxHP).
potionRoll := float01()
if potionRoll < 0.05 {
drops = append(drops, LootDrop{
ItemType: "potion",
Rarity: RarityCommon,
})
}
equipRoll := float01()
equipChance := 0.15 * luckMultiplier
if equipChance > 1 {
equipChance = 1
}
if equipRoll < equipChance {
itemRarity := RarityFromRoll(float01())
itemType := rollEquipmentLootItemType(float01)
drops = append(drops, LootDrop{
ItemType: itemType,
Rarity: itemRarity,
})
}
return drops
}