package game import ( "fmt" "path/filepath" "strings" "gopkg.in/yaml.v3" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) type TechEffects struct { AccuracyPercent int `yaml:"accuracy_percent"` StrengthPercent int `yaml:"strength_percent"` DefensePercent int `yaml:"defense_percent"` RangedPercent int `yaml:"ranged_percent"` SciencePercent int `yaml:"science_percent"` ProtectMelee bool `yaml:"protect_melee"` ProtectRanged bool `yaml:"protect_ranged"` ProtectScience bool `yaml:"protect_science"` DamageReduction float64 `yaml:"damage_reduction"` HPRegenMulti float64 `yaml:"hp_regen_multi"` PreserveDrain float64 `yaml:"preserve_drain"` RetributionPct float64 `yaml:"retribution_pct"` } type TechDef struct { ID string `yaml:"id"` Name string `yaml:"name"` Level int `yaml:"level"` DrainRate float64 `yaml:"drain_rate"` Category string `yaml:"category"` Group string `yaml:"group"` Effects TechEffects `yaml:"effects"` } var AllTechs []*TechDef var techByID map[string]*TechDef func (g *Game) LoadTechs() error { dir := filepath.Join(g.DataDir, "techs") AllTechs = nil techByID = make(map[string]*TechDef) behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error { var t TechDef if err := yaml.Unmarshal(data, &t); err != nil { return nil } t.ID = id AllTechs = append(AllTechs, &t) return nil }) techByID = make(map[string]*TechDef, len(AllTechs)) for _, t := range AllTechs { techByID[t.ID] = t } return nil } func GetTechDef(id string) *TechDef { return techByID[id] } func TechByPrefixMatch(input string) []*TechDef { input = strings.ToLower(input) var exact []*TechDef var prefix []*TechDef for _, t := range AllTechs { lower := strings.ToLower(t.Name) lowerID := strings.ToLower(t.ID) if lower == input || lowerID == input { exact = append(exact, t) } else if strings.HasPrefix(lower, input) || strings.HasPrefix(lowerID, input) { prefix = append(prefix, t) } } if len(exact) > 0 { return exact } return prefix } func techEffectString(tech *TechDef) string { var parts []string e := tech.Effects if e.AccuracyPercent > 0 { parts = append(parts, fmt.Sprintf("+%d%% Accuracy", e.AccuracyPercent)) } if e.StrengthPercent > 0 { parts = append(parts, fmt.Sprintf("+%d%% Strength", e.StrengthPercent)) } if e.DefensePercent > 0 { parts = append(parts, fmt.Sprintf("+%d%% Defense", e.DefensePercent)) } if e.RangedPercent > 0 { parts = append(parts, fmt.Sprintf("+%d%% Ranged", e.RangedPercent)) } if e.SciencePercent > 0 { parts = append(parts, fmt.Sprintf("+%d%% Science", e.SciencePercent)) } if e.ProtectMelee { parts = append(parts, fmt.Sprintf("%.0f%% melee protection", e.DamageReduction*100)) } if e.ProtectRanged { parts = append(parts, fmt.Sprintf("%.0f%% ranged protection", e.DamageReduction*100)) } if e.ProtectScience { parts = append(parts, fmt.Sprintf("%.0f%% science protection", e.DamageReduction*100)) } if e.HPRegenMulti > 0 { parts = append(parts, fmt.Sprintf("%.0fx HP regen", e.HPRegenMulti)) } if e.PreserveDrain > 0 { parts = append(parts, fmt.Sprintf("%.0f%% drain reduction", e.PreserveDrain*100)) } if e.RetributionPct > 0 { parts = append(parts, fmt.Sprintf("%.0f%% retribution", e.RetributionPct*100)) } return strings.Join(parts, ", ") } func (g *Game) totalTechBonus(p *player.Player) int { total := 0 for _, itemID := range p.Equipment { def, err := g.ItemStore.Load(itemID) if err == nil { total += def.Stats().TechnologyBonus } } return total } func (g *Game) techLevelBonus(p *player.Player, stat string) int { if len(p.ActiveTechs) == 0 { return 0 } totalPercent := 0 for id := range p.ActiveTechs { def := GetTechDef(id) if def == nil { continue } switch stat { case "accuracy": totalPercent += def.Effects.AccuracyPercent case "strength": totalPercent += def.Effects.StrengthPercent case "defense": totalPercent += def.Effects.DefensePercent case "ranged": totalPercent += def.Effects.RangedPercent case "science": totalPercent += def.Effects.SciencePercent } } if totalPercent == 0 { return 0 } var baseLevel int switch stat { case "accuracy": baseLevel = p.Level(player.Accuracy) case "strength": baseLevel = p.Level(player.Strength) case "defense": baseLevel = p.Level(player.Defense) case "ranged": baseLevel = p.Level(player.Ranged) case "science": baseLevel = p.Level(player.Science) } return baseLevel * totalPercent / 100 } func (g *Game) tryRecharge(sess *net.Session, p *player.Player, input string) bool { lower := strings.ToLower(strings.TrimSpace(input)) instances := g.World.FindObjInstances(p.RoomID, lower) for _, st := range instances { if st.DefID == "charging_station" || st.DefID == "power_conduit" { if p.Battery >= p.MaxBattery() { sess.WriteLine("Your battery is already full.") } else { p.Battery = p.MaxBattery() g.AccountStore.SaveCharacter(p) sess.WriteLine(g.colorize(sess, "battery", "You connect to the charging station. Your battery is fully recharged.")) } return true } } return false } func (g *Game) applyTechProtection(p *player.Player, mob *world.MobInstance, dmg int) int { return g.damageAfterTechProtection(p, mob.AttackType, dmg) } // damageAfterTechProtection reduces incoming damage if the player has the // protection tech matching the given attack type active. Shared by mob combat // and room hazards. // // Reserved tech IDs (must exist in data/techs/): // // protect_melee — mapped to stab/slash/crush attacks // protect_ranged — mapped to ranged attacks // protect_science — mapped to science attacks // // Also retribution — checked by name in killPlayer (cmd_attack.go). func (g *Game) damageAfterTechProtection(p *player.Player, attackType string, dmg int) int { if len(p.ActiveTechs) == 0 { return dmg } if attackType == "" { attackType = "crush" } var protectTechID string switch attackType { case "stab", "slash", "crush": protectTechID = "protect_melee" case "ranged": protectTechID = "protect_ranged" case "science": protectTechID = "protect_science" } if protectTechID != "" && p.HasActiveTech(protectTechID) { def := GetTechDef(protectTechID) if def != nil { reduced := int(float64(dmg) * (1.0 - def.Effects.DamageReduction)) if reduced < 0 { reduced = 0 } return reduced } } return dmg }