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
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
|
|
}
|