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.

288 lines
8.6 KiB
Go

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) ([]model.Enemy, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance,
min_level, max_level, base_level, level_variance_pct, max_hero_level_diff,
hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_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()
var out []model.Enemy
for rows.Next() {
var (
e model.Enemy
slug string
specialAbilities []string
)
if err := rows.Scan(
&e.ID, &slug, &e.Archetype, &e.Biome, &e.Name, &e.HP, &e.MaxHP, &e.Attack, &e.Defense, &e.Speed, &e.CritChance,
&e.MinLevel, &e.MaxLevel, &e.BaseLevel, &e.LevelVariance, &e.MaxHeroLevelDiff,
&e.HPPerLevel, &e.AttackPerLevel, &e.DefensePerLevel, &e.XPPerLevel, &e.GoldPerLevel,
&e.XPReward, &e.GoldReward, &specialAbilities, &e.IsElite,
); err != nil {
return nil, fmt.Errorf("scan enemy row: %w", err)
}
e.Slug = slug
e.SpecialAbilities = make([]model.SpecialAbility, 0, len(specialAbilities))
for _, a := range specialAbilities {
e.SpecialAbilities = append(e.SpecialAbilities, model.SpecialAbility(a))
}
out = append(out, 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"` // slug
Archetype string `json:"archetype"`
Biome string `json:"biome"`
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"`
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, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance,
min_level, max_level, base_level, level_variance_pct, max_hero_level_diff,
hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level,
xp_reward, gold_reward, special_abilities, is_elite
FROM enemies
ORDER BY min_level, archetype, 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.Archetype, &r.Biome, &r.Name, &r.HP, &r.MaxHP, &r.Attack, &r.Defense, &r.Speed, &r.CritChance,
&r.MinLevel, &r.MaxLevel, &r.BaseLevel, &r.LevelVariance, &r.MaxHeroLevelDiff,
&r.HPPerLevel, &r.AttackPerLevel, &r.DefensePerLevel, &r.XPPerLevel, &r.GoldPerLevel,
&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
archetype = $2,
biome = $3,
name = $4,
hp = $5,
max_hp = $6,
attack = $7,
defense = $8,
speed = $9,
crit_chance = $10,
min_level = $11,
max_level = $12,
base_level = $13,
level_variance_pct = $14,
max_hero_level_diff = $15,
hp_per_level = $16,
attack_per_level = $17,
defense_per_level = $18,
xp_per_level = $19,
gold_per_level = $20,
xp_reward = $21,
gold_reward = $22,
special_abilities = $23::text[],
is_elite = $24
WHERE type = $1
`, typ, e.Archetype, e.Biome, e.Name, e.MaxHP, e.MaxHP, e.Attack, e.Defense, e.Speed, e.CritChance,
e.MinLevel, e.MaxLevel, e.BaseLevel, e.LevelVariance, e.MaxHeroLevelDiff,
e.HPPerLevel, e.AttackPerLevel, e.DefensePerLevel, e.XPPerLevel, e.GoldPerLevel,
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
}