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.

325 lines
14 KiB
Go

package model
import "math/rand"
// GearItem represents any equippable item across all slots.
type GearItem struct {
ID int64 `json:"id"`
Slot EquipmentSlot `json:"slot"`
FormID string `json:"formId"`
Name string `json:"name"`
Subtype string `json:"subtype"`
Rarity Rarity `json:"rarity"`
Ilvl int `json:"ilvl"`
BasePrimary int `json:"basePrimary"`
PrimaryStat int `json:"primaryStat"`
StatType string `json:"statType"`
SpeedModifier float64 `json:"speedModifier"`
CritChance float64 `json:"critChance"`
AgilityBonus int `json:"agilityBonus"`
SetName string `json:"setName,omitempty"`
SpecialEffect string `json:"specialEffect,omitempty"`
}
// CloneGearItem returns a deep copy with ID cleared (for ephemeral offers / re-persist).
func CloneGearItem(src *GearItem) *GearItem {
if src == nil {
return nil
}
c := *src
c.ID = 0
return &c
}
// GearFamily is a template for generating gear drops from the unified catalog.
type GearFamily struct {
Slot EquipmentSlot `json:"slot"`
FormID string `json:"formId"`
Name string `json:"name"`
Subtype string `json:"subtype"` // "daggers", "sword", "axe", "light", "medium", "heavy", ""
BasePrimary int `json:"basePrimary"`
StatType string `json:"statType"`
SpeedModifier float64 `json:"speedModifier"`
BaseCrit float64 `json:"baseCrit"`
AgilityBonus int `json:"agilityBonus"`
SetName string `json:"setName,omitempty"`
SpecialEffect string `json:"specialEffect,omitempty"`
}
// GearCatalog is the unified catalog of all gear families.
var GearCatalog []GearFamily
var defaultGearCatalog []GearFamily
// ArmorSetBonuses maps set names to their bonus description.
var GearSetBonuses = map[string]string{
"Assassin's Set": "+crit_chance",
"Knight's Set": "+defense",
"Berserker's Set": "+attack",
"Ancient Guardian's Set": "+all_stats",
}
func init() {
// Weapons -> main_hand
for _, w := range legacyWeapons {
GearCatalog = append(GearCatalog, GearFamily{
Slot: SlotMainHand,
FormID: "gear.form.main_hand." + string(w.Type),
Name: w.Name,
Subtype: string(w.Type),
BasePrimary: w.Damage,
StatType: "attack",
SpeedModifier: w.Speed,
BaseCrit: w.CritChance,
SpecialEffect: w.SpecialEffect,
})
}
// Armors -> chest
for _, a := range legacyArmors {
GearCatalog = append(GearCatalog, GearFamily{
Slot: SlotChest,
FormID: "gear.form.chest." + string(a.Type),
Name: a.Name,
Subtype: string(a.Type),
BasePrimary: a.Defense,
StatType: "defense",
SpeedModifier: a.SpeedModifier,
AgilityBonus: a.AgilityBonus,
SetName: a.SetName,
SpecialEffect: a.SpecialEffect,
})
}
// Extended equipment families -> head, feet, neck, hands, legs, cloak, finger, wrist
for slot, families := range legacyEquipmentFamilies {
for _, f := range families {
GearCatalog = append(GearCatalog, GearFamily{
Slot: slot,
FormID: f.FormID,
Name: f.Name,
BasePrimary: f.BasePrimary,
StatType: f.StatType,
SpeedModifier: 1.0,
})
}
}
// Build the by-slot-and-rarity index.
gearBySlot = make(map[EquipmentSlot][]GearFamily)
for _, gf := range GearCatalog {
gearBySlot[gf.Slot] = append(gearBySlot[gf.Slot], gf)
}
defaultGearCatalog = append([]GearFamily(nil), GearCatalog...)
}
var gearBySlot map[EquipmentSlot][]GearFamily
// GearFamilyByName returns the first catalog family with the given display name, or nil.
func GearFamilyByName(name string) *GearFamily {
for i := range GearCatalog {
if GearCatalog[i].Name == name {
return &GearCatalog[i]
}
}
return nil
}
// DefaultGearFamilies returns a snapshot of the built-in catalog (before any SetGearCatalog / DB merge).
func DefaultGearFamilies() []GearFamily {
out := make([]GearFamily, len(defaultGearCatalog))
copy(out, defaultGearCatalog)
return out
}
func SetGearCatalog(families []GearFamily) {
if len(families) == 0 {
return
}
merged := append([]GearFamily(nil), families...)
slotsFromDB := make(map[EquipmentSlot]struct{}, len(families))
for _, gf := range families {
slotsFromDB[gf.Slot] = struct{}{}
}
for _, fallback := range defaultGearCatalog {
if _, ok := slotsFromDB[fallback.Slot]; ok {
continue
}
merged = append(merged, fallback)
}
// If DB claimed a slot but supplied no rows (or bad keys), ensure defaults exist.
countBySlot := make(map[EquipmentSlot]int)
for _, gf := range merged {
countBySlot[gf.Slot]++
}
for _, slot := range AllEquipmentSlots {
if countBySlot[slot] == 0 {
for _, fallback := range defaultGearCatalog {
if fallback.Slot == slot {
merged = append(merged, fallback)
}
}
}
}
GearCatalog = merged
gearBySlot = make(map[EquipmentSlot][]GearFamily)
for _, gf := range GearCatalog {
gearBySlot[gf.Slot] = append(gearBySlot[gf.Slot], gf)
}
}
// PickGearFamily selects a random gear family for the given slot.
// Returns nil if no families exist for the slot.
func PickGearFamily(slot EquipmentSlot) *GearFamily {
families := gearBySlot[slot]
if len(families) == 0 {
return nil
}
f := families[rand.Intn(len(families))]
return &f
}
// NewGearItem creates a GearItem from a family template with ilvl and rarity scaling applied.
func NewGearItem(family *GearFamily, ilvl int, rarity Rarity) *GearItem {
primary := ScalePrimary(family.BasePrimary, ilvl, rarity)
return &GearItem{
Slot: family.Slot,
FormID: family.FormID,
Name: family.Name,
Subtype: family.Subtype,
Rarity: rarity,
Ilvl: ilvl,
BasePrimary: family.BasePrimary,
PrimaryStat: primary,
StatType: family.StatType,
SpeedModifier: family.SpeedModifier,
CritChance: family.BaseCrit,
AgilityBonus: family.AgilityBonus,
SetName: family.SetName,
SpecialEffect: family.SpecialEffect,
}
}
// ---------- Legacy data (moved from weapon.go, armor.go, equipment.go) ----------
// legacyWeaponEntry holds weapon catalog data for building the GearCatalog.
type legacyWeaponEntry struct {
Name string
Type string
Rarity Rarity
Damage int
Speed float64
CritChance float64
SpecialEffect string
}
var legacyWeapons = []legacyWeaponEntry{
// Daggers
{Name: "Rusty Dagger", Type: "daggers", Rarity: RarityCommon, Damage: 3, Speed: 1.3, CritChance: 0.05},
{Name: "Iron Dagger", Type: "daggers", Rarity: RarityUncommon, Damage: 3, Speed: 1.3, CritChance: 0.08},
{Name: "Assassin's Blade", Type: "daggers", Rarity: RarityRare, Damage: 3, Speed: 1.35, CritChance: 0.20},
{Name: "Phantom Edge", Type: "daggers", Rarity: RarityEpic, Damage: 3, Speed: 1.4, CritChance: 0.25},
{Name: "Fang of the Void", Type: "daggers", Rarity: RarityLegendary, Damage: 3, Speed: 1.5, CritChance: 0.30},
// Swords
{Name: "Iron Sword", Type: "sword", Rarity: RarityCommon, Damage: 7, Speed: 1.0, CritChance: 0.03},
{Name: "Steel Sword", Type: "sword", Rarity: RarityUncommon, Damage: 7, Speed: 1.0, CritChance: 0.05},
{Name: "Longsword", Type: "sword", Rarity: RarityRare, Damage: 7, Speed: 1.0, CritChance: 0.08},
{Name: "Excalibur", Type: "sword", Rarity: RarityEpic, Damage: 7, Speed: 1.05, CritChance: 0.10},
{Name: "Soul Reaver", Type: "sword", Rarity: RarityLegendary, Damage: 7, Speed: 1.1, CritChance: 0.12, SpecialEffect: "lifesteal"},
// Axes
{Name: "Rusty Axe", Type: "axe", Rarity: RarityCommon, Damage: 12, Speed: 0.7, CritChance: 0.02},
{Name: "Battle Axe", Type: "axe", Rarity: RarityUncommon, Damage: 12, Speed: 0.7, CritChance: 0.04},
{Name: "War Axe", Type: "axe", Rarity: RarityRare, Damage: 12, Speed: 0.75, CritChance: 0.06},
{Name: "Infernal Axe", Type: "axe", Rarity: RarityEpic, Damage: 12, Speed: 0.75, CritChance: 0.08},
{Name: "Godslayer's Edge", Type: "axe", Rarity: RarityLegendary, Damage: 12, Speed: 0.8, CritChance: 0.10, SpecialEffect: "splash"},
}
// legacyArmorEntry holds armor catalog data for building the GearCatalog.
type legacyArmorEntry struct {
Name string
Type string
Rarity Rarity
Defense int
SpeedModifier float64
AgilityBonus int
SetName string
SpecialEffect string
}
var legacyArmors = []legacyArmorEntry{
// Light armor — same base Defense per class; rarity scaling via M(rarity) in ScalePrimary.
{Name: "Leather Armor", Type: "light", Rarity: RarityCommon, Defense: 2, SpeedModifier: 1.05, AgilityBonus: 3},
{Name: "Ranger's Vest", Type: "light", Rarity: RarityUncommon, Defense: 2, SpeedModifier: 1.08, AgilityBonus: 5},
{Name: "Shadow Cloak", Type: "light", Rarity: RarityRare, Defense: 2, SpeedModifier: 1.10, AgilityBonus: 8, SetName: "Assassin's Set", SpecialEffect: "crit_bonus"},
{Name: "Phantom Garb", Type: "light", Rarity: RarityEpic, Defense: 2, SpeedModifier: 1.12, AgilityBonus: 12, SetName: "Assassin's Set", SpecialEffect: "crit_bonus"},
{Name: "Whisper of the Void", Type: "light", Rarity: RarityLegendary, Defense: 2, SpeedModifier: 1.15, AgilityBonus: 18, SetName: "Assassin's Set", SpecialEffect: "crit_bonus"},
// Medium armor
{Name: "Chainmail", Type: "medium", Rarity: RarityCommon, Defense: 4, SpeedModifier: 1.0},
{Name: "Reinforced Mail", Type: "medium", Rarity: RarityUncommon, Defense: 4, SpeedModifier: 1.0},
{Name: "Battle Armor", Type: "medium", Rarity: RarityRare, Defense: 4, SpeedModifier: 1.0, SetName: "Knight's Set", SpecialEffect: "def_bonus"},
{Name: "Royal Guard", Type: "medium", Rarity: RarityEpic, Defense: 4, SpeedModifier: 1.0, SetName: "Knight's Set", SpecialEffect: "def_bonus"},
{Name: "Crown of Eternity", Type: "medium", Rarity: RarityLegendary, Defense: 4, SpeedModifier: 1.0, SetName: "Knight's Set", SpecialEffect: "def_bonus"},
// Heavy armor
{Name: "Iron Plate", Type: "heavy", Rarity: RarityCommon, Defense: 10, SpeedModifier: 0.7, AgilityBonus: -3},
{Name: "Steel Plate", Type: "heavy", Rarity: RarityUncommon, Defense: 10, SpeedModifier: 0.7, AgilityBonus: -3},
{Name: "Fortress Armor", Type: "heavy", Rarity: RarityRare, Defense: 10, SpeedModifier: 0.7, AgilityBonus: -5, SetName: "Berserker's Set", SpecialEffect: "atk_bonus"},
{Name: "Dragon Scale", Type: "heavy", Rarity: RarityEpic, Defense: 10, SpeedModifier: 0.7, AgilityBonus: -5, SetName: "Berserker's Set", SpecialEffect: "atk_bonus"},
{Name: "Dragon Slayer", Type: "heavy", Rarity: RarityLegendary, Defense: 10, SpeedModifier: 0.7, AgilityBonus: -5, SetName: "Berserker's Set", SpecialEffect: "atk_bonus"},
// Ancient Guardian's Set
{Name: "Guardian's Plate", Type: "heavy", Rarity: RarityRare, Defense: 10, SpeedModifier: 0.7, AgilityBonus: 2, SetName: "Ancient Guardian's Set", SpecialEffect: "all_stats"},
{Name: "Guardian's Bastion", Type: "heavy", Rarity: RarityEpic, Defense: 10, SpeedModifier: 0.7, AgilityBonus: 4, SetName: "Ancient Guardian's Set", SpecialEffect: "all_stats"},
{Name: "Ancient Guardian's Aegis", Type: "heavy", Rarity: RarityLegendary, Defense: 10, SpeedModifier: 0.7, AgilityBonus: 6, SetName: "Ancient Guardian's Set", SpecialEffect: "all_stats"},
}
// legacyEquipmentFamily is the template used by the old equipment system.
type legacyEquipmentFamily struct {
Name string
FormID string
BasePrimary int
StatType string
}
var legacyEquipmentFamilies = map[EquipmentSlot][]legacyEquipmentFamily{
SlotHead: {
{Name: "Leather Cap", FormID: "gear.form.head.cap", BasePrimary: 2, StatType: "defense"},
{Name: "Iron Helmet", FormID: "gear.form.head.helmet", BasePrimary: 4, StatType: "defense"},
{Name: "Crown", FormID: "gear.form.head.crown", BasePrimary: 3, StatType: "mixed"},
},
SlotFeet: {
{Name: "Sandals", FormID: "gear.form.feet.sandals", BasePrimary: 1, StatType: "speed"},
{Name: "Leather Boots", FormID: "gear.form.feet.boots", BasePrimary: 3, StatType: "defense"},
{Name: "Iron Greaves", FormID: "gear.form.feet.greaves", BasePrimary: 5, StatType: "defense"},
},
SlotNeck: {
{Name: "Wooden Pendant", FormID: "gear.form.neck.pendant", BasePrimary: 2, StatType: "mixed"},
{Name: "Silver Amulet", FormID: "gear.form.neck.amulet", BasePrimary: 4, StatType: "attack"},
{Name: "Crystal Necklace", FormID: "gear.form.neck.necklace", BasePrimary: 3, StatType: "defense"},
},
SlotHands: {
{Name: "Leather Gloves", FormID: "gear.form.hands.gloves", BasePrimary: 2, StatType: "defense"},
{Name: "Iron Gauntlets", FormID: "gear.form.hands.gauntlets", BasePrimary: 4, StatType: "defense"},
{Name: "Dragonhide Gloves", FormID: "gear.form.hands.gloves", BasePrimary: 3, StatType: "mixed"},
},
SlotLegs: {
{Name: "Leather Leggings", FormID: "gear.form.legs.leggings", BasePrimary: 2, StatType: "defense"},
{Name: "Iron Greaves", FormID: "gear.form.legs.greaves", BasePrimary: 4, StatType: "defense"},
},
SlotCloak: {
{Name: "Leather Cloak", FormID: "gear.form.cloak.cloak", BasePrimary: 2, StatType: "defense"},
{Name: "Iron Cape", FormID: "gear.form.cloak.cape", BasePrimary: 4, StatType: "defense"},
{Name: "Dragonhide Cloak", FormID: "gear.form.cloak.cloak", BasePrimary: 3, StatType: "mixed"},
},
SlotFinger: {
{Name: "Wooden Ring", FormID: "gear.form.finger.ring", BasePrimary: 2, StatType: "mixed"},
{Name: "Silver Ring", FormID: "gear.form.finger.ring", BasePrimary: 4, StatType: "attack"},
{Name: "Crystal Ring", FormID: "gear.form.finger.ring", BasePrimary: 3, StatType: "defense"},
},
SlotWrist: {
{Name: "Leather Bracers", FormID: "gear.form.wrist.bracers", BasePrimary: 2, StatType: "defense"},
{Name: "Iron Wristguards", FormID: "gear.form.wrist.wristguards", BasePrimary: 4, StatType: "defense"},
{Name: "Dragonhide Wristguards", FormID: "gear.form.wrist.wristguards", BasePrimary: 3, StatType: "mixed"},
},
}