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.1–8.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 }