package main import ( "encoding/json" "fmt" "os" "github.com/denisovdennis/autohero/internal/model" ) // enemyPartial mirrors model.Enemy with pointer fields so JSON omits mean "keep DB value". type enemyPartial struct { ID *int64 `json:"id"` Type *string `json:"type"` Name *string `json:"name"` HP *int `json:"hp"` MaxHP *int `json:"maxHp"` Attack *int `json:"attack"` Defense *int `json:"defense"` Speed *float64 `json:"speed"` CritChance *float64 `json:"critChance"` MinLevel *int `json:"minLevel"` MaxLevel *int `json:"maxLevel"` BaseLevel *int `json:"baseLevel"` LevelVariance *float64 `json:"levelVariance"` MaxHeroLevelDiff *int `json:"maxHeroLevelDiff"` HPPerLevel *float64 `json:"hpPerLevel"` AttackPerLevel *float64 `json:"attackPerLevel"` DefensePerLevel *float64 `json:"defensePerLevel"` XPPerLevel *float64 `json:"xpPerLevel"` GoldPerLevel *float64 `json:"goldPerLevel"` Level *int `json:"level"` XPReward *int64 `json:"xpReward"` GoldReward *int64 `json:"goldReward"` SpecialAbilities *[]model.SpecialAbility `json:"specialAbilities"` IsElite *bool `json:"isElite"` } // applyEnemyOverlayJSON reads a JSON object keyed by enemy type (string), merges each partial onto templates. // Unknown keys log a warning and are skipped. Keys for types not present in templates log a warning. func applyEnemyOverlayJSON(path string, templates map[string]model.Enemy) (map[string]model.Enemy, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read overlay %q: %w", path, err) } var raw map[string]json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { return nil, fmt.Errorf("parse overlay JSON: %w", err) } out := make(map[string]model.Enemy, len(templates)) for k, v := range templates { out[k] = v } for typeKey, rawMsg := range raw { base, ok := out[typeKey] if !ok { fmt.Fprintf(os.Stderr, "balanceall overlay: skip unknown type %q (not in loaded templates)\n", typeKey) continue } var p enemyPartial if err := json.Unmarshal(rawMsg, &p); err != nil { return nil, fmt.Errorf("overlay %q: %w", typeKey, err) } mergeEnemyPartial(&base, &p) out[typeKey] = base } return out, nil } func mergeEnemyPartial(dst *model.Enemy, p *enemyPartial) { if p.ID != nil { dst.ID = *p.ID } if p.Type != nil { dst.Slug = *p.Type } if p.Name != nil { dst.Name = *p.Name } if p.HP != nil { dst.HP = *p.HP } if p.MaxHP != nil { dst.MaxHP = *p.MaxHP } if p.Attack != nil { dst.Attack = *p.Attack } if p.Defense != nil { dst.Defense = *p.Defense } if p.Speed != nil { dst.Speed = *p.Speed } if p.CritChance != nil { dst.CritChance = *p.CritChance } if p.MinLevel != nil { dst.MinLevel = *p.MinLevel } if p.MaxLevel != nil { dst.MaxLevel = *p.MaxLevel } if p.BaseLevel != nil { dst.BaseLevel = *p.BaseLevel } if p.LevelVariance != nil { dst.LevelVariance = *p.LevelVariance } if p.MaxHeroLevelDiff != nil { dst.MaxHeroLevelDiff = *p.MaxHeroLevelDiff } if p.HPPerLevel != nil { dst.HPPerLevel = *p.HPPerLevel } if p.AttackPerLevel != nil { dst.AttackPerLevel = *p.AttackPerLevel } if p.DefensePerLevel != nil { dst.DefensePerLevel = *p.DefensePerLevel } if p.XPPerLevel != nil { dst.XPPerLevel = *p.XPPerLevel } if p.GoldPerLevel != nil { dst.GoldPerLevel = *p.GoldPerLevel } if p.Level != nil { dst.Level = *p.Level } if p.XPReward != nil { dst.XPReward = *p.XPReward } if p.GoldReward != nil { dst.GoldReward = *p.GoldReward } if p.SpecialAbilities != nil { dst.SpecialAbilities = *p.SpecialAbilities } if p.IsElite != nil { dst.IsElite = *p.IsElite } // If only one of hp/maxHp was overridden, keep them aligned for template rows. if p.MaxHP != nil && p.HP == nil { dst.HP = dst.MaxHP } if p.HP != nil && p.MaxHP == nil { dst.MaxHP = dst.HP } }