package game import ( "fmt" "path/filepath" "sort" "strings" "gopkg.in/yaml.v3" "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) type ModCategory string const ( ModCombat ModCategory = "combat" ModUtility ModCategory = "utility" ModEnchant ModCategory = "enchant" ModProcessing ModCategory = "processing" ModTransport ModCategory = "transport" ) type ModDef struct { ID string `yaml:"id"` Name string `yaml:"name"` Level int `yaml:"level"` MaxHit int `yaml:"max_hit"` BaseXP int `yaml:"base_xp"` JunkCost map[string]int `yaml:"junk_cost"` Category ModCategory `yaml:"category"` Element string `yaml:"element"` Destination int `yaml:"destination"` } var AllMods []*ModDef var modByID map[string]*ModDef func (g *Game) LoadMods() error { dir := filepath.Join(g.DataDir, "modules") AllMods = nil modByID = make(map[string]*ModDef) action.WalkYAMLDir(dir, func(path, id string, data []byte) error { var m ModDef if err := yaml.Unmarshal(data, &m); err != nil { return nil } AllMods = append(AllMods, &m) return nil }) modByID = make(map[string]*ModDef, len(AllMods)) for _, m := range AllMods { modByID[m.ID] = m } return nil } func GetMod(id string) *ModDef { return modByID[id] } func FindMod(input string) *ModDef { if m, ok := modByID[input]; ok { return m } lower := strings.ToLower(strings.ReplaceAll(input, " ", "_")) for _, m := range AllMods { if strings.HasPrefix(m.ID, lower) { return m } } lowerSpace := strings.ToLower(input) for _, m := range AllMods { if strings.HasPrefix(strings.ToLower(m.Name), lowerSpace) { return m } } return nil } func findAllModMatches(input string) []*ModDef { inputWords := strings.Fields(strings.ToLower(input)) if len(inputWords) == 0 { return nil } var matches []*ModDef for _, m := range AllMods { lowerName := strings.ToLower(m.Name) lowerID := strings.ToLower(m.ID) allMatch := true for _, word := range inputWords { if !strings.Contains(lowerName, word) && !strings.Contains(lowerID, word) { allMatch = false break } } if allMatch { matches = append(matches, m) } } return matches } func FindModForPlayer(input string, sciLevel int) *ModDef { if m, ok := modByID[input]; ok { return m } lower := strings.ToLower(strings.ReplaceAll(input, " ", "_")) for _, m := range AllMods { if strings.HasPrefix(m.ID, lower) { return m } } matches := findAllModMatches(input) if len(matches) == 0 { return nil } if len(matches) == 1 { return matches[0] } sort.Slice(matches, func(i, j int) bool { return matches[i].Level > matches[j].Level }) for _, m := range matches { if m.Level <= sciLevel { return m } } return matches[len(matches)-1] } type chipEntry struct { Input string Output string Qty int } var enchantMap = map[string]map[string]string{ "enchant_1": { "sapphire_ring": "ring_of_recoil", "sapphire_necklace": "necklace_of_passage", "sapphire_bracelet": "bracelet_of_clay", }, "enchant_2": { "emerald_ring": "ring_of_dueling", "emerald_necklace": "binding_necklace", "emerald_bracelet": "bracelet_of_slaughter", }, "enchant_3": { "ruby_ring": "ring_of_forging", "ruby_necklace": "digsite_pendant", "ruby_bracelet": "inoculation_bracelet", }, "enchant_4": { "diamond_ring": "ring_of_life", "diamond_necklace": "phoenix_necklace", "diamond_bracelet": "abyssal_bracelet", }, } var chipMap = map[string]chipEntry{ "chip_sapphire": {"sapphire_bolts", "sapphire_bolts_e", 10}, "chip_emerald": {"emerald_bolts", "emerald_bolts_e", 10}, "chip_ruby": {"ruby_bolts", "ruby_bolts_e", 10}, "chip_diamond": {"diamond_bolts", "diamond_bolts_e", 10}, } func (g *Game) hasDeckEquipped(p *player.Player) bool { itemID, ok := p.Equipment[object.SlotMainHand] if !ok { return false } def, err := g.ItemStore.Load(itemID) if err != nil { return false } return def.WeaponType == object.WeaponScience } func (g *Game) providesJunk(p *player.Player) string { itemID, ok := p.Equipment[object.SlotMainHand] if !ok { return "" } def, err := g.ItemStore.Load(itemID) if err != nil { return "" } return def.ProvidesJunk } func (g *Game) effectiveJunkCost(p *player.Player, mod *ModDef) map[string]int { cost := make(map[string]int) for k, v := range mod.JunkCost { cost[k] = v } hasDeck := g.hasDeckEquipped(p) if hasDeck { delete(cost, "scrap") } providesJunk := g.providesJunk(p) if providesJunk != "" { delete(cost, providesJunk) } return cost } func (g *Game) hasJunkCost(p *player.Player, mod *ModDef) bool { cost := g.effectiveJunkCost(p, mod) for itemID, qty := range cost { if p.CountItem(itemID) < qty { return false } } return true } func (g *Game) consumeJunkCost(p *player.Player, mod *ModDef) bool { cost := g.effectiveJunkCost(p, mod) for itemID, qty := range cost { if !p.RemoveItem(itemID, qty) { return false } } g.AccountStore.SaveCharacter(p) return true } func (g *Game) triggerModReward(sess *net.Session, p *player.Player, mod *ModDef, multiplier float64, extraGains ...xpGain) int { g.consumeJunkCost(p, mod) sciXP := int(float64(mod.BaseXP) * multiplier) if sciXP < 1 { sciXP = 1 } g.awardSkillXP(sess, p, player.Science, sciXP) for _, gain := range extraGains { g.awardSkillXP(sess, p, player.SkillName(gain.Skill), gain.XP) } g.AccountStore.SaveCharacter(p) if p.OptionBool("xp_drops") { parts := []string{fmt.Sprintf("+%dxp %s", sciXP, player.SkillAbbr[player.Science])} for _, gain := range extraGains { parts = append(parts, fmt.Sprintf("+%dxp %s", gain.XP, player.SkillAbbr[player.SkillName(gain.Skill)])) } sess.WriteLine(g.colorize(sess, "xp", "("+strings.Join(parts, ", ")+")")) } return sciXP } func (g *Game) junkCostString(p *player.Player, mod *ModDef) string { cost := g.effectiveJunkCost(p, mod) delete(cost, "scrap") if len(cost) == 0 { return "free" } var parts []string for itemID, qty := range cost { def, _ := g.ItemStore.Load(itemID) name := itemID if def != nil { name = def.Name } name = strings.TrimSuffix(name, "junk") if qty > 1 { parts = append(parts, fmt.Sprintf("%d %s", qty, name)) } else { parts = append(parts, name) } } sort.Strings(parts) return strings.Join(parts, ", ") } func (g *Game) totalScienceAttack(p *player.Player) int { total := 0 for _, itemID := range p.Equipment { def, err := g.ItemStore.Load(itemID) if err == nil { total += def.Stats.ScienceAttack } } return total }