package storage import ( "context" "fmt" "strings" "github.com/jackc/pgx/v5/pgxpool" "github.com/denisovdennis/autohero/internal/model" ) type ContentStore struct { pool *pgxpool.Pool } func NewContentStore(pool *pgxpool.Pool) *ContentStore { return &ContentStore{pool: pool} } func (s *ContentStore) LoadEnemyTemplates(ctx context.Context) (map[model.EnemyType]model.Enemy, error) { rows, err := s.pool.Query(ctx, ` SELECT type, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite FROM enemies `) if err != nil { return nil, fmt.Errorf("load enemies from db: %w", err) } defer rows.Close() out := make(map[model.EnemyType]model.Enemy) for rows.Next() { var ( t string e model.Enemy specialAbilities []string ) if err := rows.Scan( &t, &e.Name, &e.HP, &e.MaxHP, &e.Attack, &e.Defense, &e.Speed, &e.CritChance, &e.MinLevel, &e.MaxLevel, &e.XPReward, &e.GoldReward, &specialAbilities, &e.IsElite, ); err != nil { return nil, fmt.Errorf("scan enemy row: %w", err) } e.Type = model.EnemyType(t) e.SpecialAbilities = make([]model.SpecialAbility, 0, len(specialAbilities)) for _, a := range specialAbilities { e.SpecialAbilities = append(e.SpecialAbilities, model.SpecialAbility(a)) } out[e.Type] = e } if err := rows.Err(); err != nil { return nil, fmt.Errorf("enemy rows: %w", err) } return out, nil } // EnemyRow is one row from the enemies table (admin / tooling). type EnemyRow 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"` XPReward int64 `json:"xpReward"` GoldReward int64 `json:"goldReward"` SpecialAbilities []string `json:"specialAbilities"` IsElite bool `json:"isElite"` } // ListEnemyRows returns all enemy templates ordered by min_level, type. func (s *ContentStore) ListEnemyRows(ctx context.Context) ([]EnemyRow, error) { rows, err := s.pool.Query(ctx, ` SELECT id, type, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite FROM enemies ORDER BY min_level, type `) if err != nil { return nil, fmt.Errorf("list enemies: %w", err) } defer rows.Close() var out []EnemyRow for rows.Next() { var r EnemyRow if err := rows.Scan( &r.ID, &r.Type, &r.Name, &r.HP, &r.MaxHP, &r.Attack, &r.Defense, &r.Speed, &r.CritChance, &r.MinLevel, &r.MaxLevel, &r.XPReward, &r.GoldReward, &r.SpecialAbilities, &r.IsElite, ); err != nil { return nil, fmt.Errorf("scan enemy row: %w", err) } out = append(out, r) } if err := rows.Err(); err != nil { return nil, err } return out, nil } // UpdateEnemyByType persists one template and sets hp = max_hp = MaxHP from e. func (s *ContentStore) UpdateEnemyByType(ctx context.Context, typ string, e model.Enemy) error { abilities := make([]string, 0, len(e.SpecialAbilities)) for _, a := range e.SpecialAbilities { abilities = append(abilities, string(a)) } tag, err := s.pool.Exec(ctx, ` UPDATE enemies SET name = $2, hp = $3, max_hp = $4, attack = $5, defense = $6, speed = $7, crit_chance = $8, min_level = $9, max_level = $10, xp_reward = $11, gold_reward = $12, special_abilities = $13::text[], is_elite = $14 WHERE type = $1 `, typ, e.Name, e.MaxHP, e.MaxHP, e.Attack, e.Defense, e.Speed, e.CritChance, e.MinLevel, e.MaxLevel, e.XPReward, e.GoldReward, abilities, e.IsElite) if err != nil { return fmt.Errorf("update enemy: %w", err) } if tag.RowsAffected() == 0 { return fmt.Errorf("no enemy row with type %q", typ) } return nil } func normalizeEquipmentSlot(raw string) model.EquipmentSlot { v := strings.TrimSpace(strings.ToLower(raw)) v = strings.TrimPrefix(v, "gear.slot.") switch v { case "weapon", "mainhand", "main_hand": return model.SlotMainHand case "armor", "chest": return model.SlotChest case "head": return model.SlotHead case "feet": return model.SlotFeet case "neck": return model.SlotNeck case "hands": return model.SlotHands case "legs": return model.SlotLegs case "cloak": return model.SlotCloak case "finger", "ring": return model.SlotFinger case "wrist": return model.SlotWrist default: return model.EquipmentSlot(v) } } func (s *ContentStore) LoadGearFamilies(ctx context.Context) ([]model.GearFamily, error) { out := make([]model.GearFamily, 0, 128) weaponRows, err := s.pool.Query(ctx, ` SELECT name, type, damage, speed, crit_chance, special_effect FROM weapons `) if err != nil { return nil, fmt.Errorf("load weapons from db: %w", err) } for weaponRows.Next() { var name, typ, special string var damage int var speed, crit float64 if err := weaponRows.Scan(&name, &typ, &damage, &speed, &crit, &special); err != nil { weaponRows.Close() return nil, fmt.Errorf("scan weapon row: %w", err) } out = append(out, model.GearFamily{ Slot: model.SlotMainHand, FormID: "gear.form.main_hand." + typ, Name: name, Subtype: typ, BasePrimary: damage, StatType: "attack", SpeedModifier: speed, BaseCrit: crit, SpecialEffect: special, }) } weaponRows.Close() armorRows, err := s.pool.Query(ctx, ` SELECT name, type, defense, speed_modifier, agility_bonus, set_name, special_effect FROM armor `) if err != nil { return nil, fmt.Errorf("load armor from db: %w", err) } for armorRows.Next() { var name, typ, setName, special string var defense, agi int var speed float64 if err := armorRows.Scan(&name, &typ, &defense, &speed, &agi, &setName, &special); err != nil { armorRows.Close() return nil, fmt.Errorf("scan armor row: %w", err) } out = append(out, model.GearFamily{ Slot: model.SlotChest, FormID: "gear.form.chest." + typ, Name: name, Subtype: typ, BasePrimary: defense, StatType: "defense", SpeedModifier: speed, AgilityBonus: agi, SetName: setName, SpecialEffect: special, }) } armorRows.Close() eqRows, err := s.pool.Query(ctx, ` SELECT slot, form_id, name, base_primary, stat_type FROM equipment_items `) if err != nil { return nil, fmt.Errorf("load equipment_items from db: %w", err) } for eqRows.Next() { var slot, formID, name, statType string var basePrimary int if err := eqRows.Scan(&slot, &formID, &name, &basePrimary, &statType); err != nil { eqRows.Close() return nil, fmt.Errorf("scan equipment_item row: %w", err) } out = append(out, model.GearFamily{ Slot: normalizeEquipmentSlot(slot), FormID: formID, Name: name, BasePrimary: basePrimary, StatType: statType, SpeedModifier: 1.0, }) } eqRows.Close() return out, nil }