Files
bd2/go/internal/server/gamedata/talent_growth.go
T

237 lines
7.4 KiB
Go

package gamedata
import (
"database/sql"
"errors"
"fmt"
"math"
)
// CharacterTalent identifies the GameData rows which govern one character's
// talent. MaxLevel is deliberately read per talent: not every talent has five
// levels.
type CharacterTalent struct {
TalentID uint64
GrowthGroup uint64
MaxLevel uint64
}
// TalentGrowthLevel describes the upgrade from Level to Level+1. NeedExp is
// the experience earned within this level; player talent experience itself is
// cumulative and is not reset by an upgrade.
type TalentGrowthLevel struct {
Level uint64
NeedExp uint64
Costs []PromotionCost
}
type TalentUpgradeRule struct {
CharacterID uint64
TalentID uint64
GrowthGroup uint64
CurrentLevel uint64
MaxLevel uint64
RequiredTotalExp uint64
Costs []PromotionCost
}
// TalentGrowthDesign joins CharTable -> TalentTable -> TalentGrowthTable so
// the runtime never guesses a growth group or hard-codes skill-book IDs.
type TalentGrowthDesign struct {
Characters map[uint64]CharacterTalent
Levels map[[2]uint64]TalentGrowthLevel
}
func LoadTalentGrowthDesign(root, version string) (*TalentGrowthDesign, error) {
db, closeDB, err := openStatDatabase(root, version)
if err != nil {
return nil, err
}
defer closeDB()
return loadTalentGrowthDesign(db)
}
func loadTalentGrowthDesign(db *sql.DB) (*TalentGrowthDesign, error) {
if db == nil {
return nil, errors.New("gamedata: nil talent growth database")
}
talents := make(map[uint64]CharacterTalent)
rows, err := db.Query("SELECT id,ProtoBuf FROM TalentTable ORDER BY id")
if err != nil {
return nil, err
}
for rows.Next() {
var id uint64
var raw []byte
if err := rows.Scan(&id, &raw); err != nil {
rows.Close()
return nil, err
}
groups, err := packedInts(raw, 6)
if err != nil {
rows.Close()
return nil, fmt.Errorf("gamedata: talent %d growth group: %w", id, err)
}
maximum, err := packedInts(raw, 11)
if err != nil {
rows.Close()
return nil, fmt.Errorf("gamedata: talent %d maximum level: %w", id, err)
}
if len(groups) != 1 || groups[0] == 0 || len(maximum) != 1 || maximum[0] == 0 {
rows.Close()
return nil, fmt.Errorf("gamedata: talent %d has invalid growth metadata", id)
}
talents[id] = CharacterTalent{TalentID: id, GrowthGroup: groups[0], MaxLevel: maximum[0]}
}
if err := rows.Err(); err != nil {
rows.Close()
return nil, err
}
if err := rows.Close(); err != nil {
return nil, err
}
design := &TalentGrowthDesign{
Characters: make(map[uint64]CharacterTalent),
Levels: make(map[[2]uint64]TalentGrowthLevel),
}
rows, err = db.Query("SELECT id,ProtoBuf FROM CharTable ORDER BY id")
if err != nil {
return nil, err
}
for rows.Next() {
var characterID uint64
var raw []byte
if err := rows.Scan(&characterID, &raw); err != nil {
rows.Close()
return nil, err
}
talentIDs, err := packedInts(raw, 18)
if err != nil {
rows.Close()
return nil, fmt.Errorf("gamedata: character %d talent: %w", characterID, err)
}
if len(talentIDs) == 0 {
continue
}
if len(talentIDs) != 1 || talentIDs[0] == 0 {
rows.Close()
return nil, fmt.Errorf("gamedata: character %d has invalid talent reference", characterID)
}
talent, found := talents[talentIDs[0]]
if !found {
rows.Close()
return nil, fmt.Errorf("gamedata: character %d references unknown talent %d", characterID, talentIDs[0])
}
design.Characters[characterID] = talent
}
if err := rows.Err(); err != nil {
rows.Close()
return nil, err
}
if err := rows.Close(); err != nil {
return nil, err
}
rows, err = db.Query("SELECT groupId,id,ProtoBuf FROM TalentGrowthTable ORDER BY groupId,id")
if err != nil {
return nil, err
}
for rows.Next() {
var group, level uint64
var raw []byte
if err := rows.Scan(&group, &level, &raw); err != nil {
rows.Close()
return nil, err
}
counts, countErr := packedInts(raw, 2)
ids, idErr := packedInts(raw, 3)
types, typeErr := packedInts(raw, 4)
needs, needErr := packedInts(raw, 6)
if countErr != nil || idErr != nil || typeErr != nil || needErr != nil {
rows.Close()
return nil, fmt.Errorf("gamedata: talent growth %d/%d has invalid fields", group, level)
}
if group == 0 || level == 0 || len(counts) != len(ids) || len(ids) != len(types) || len(needs) > 1 {
rows.Close()
return nil, fmt.Errorf("gamedata: talent growth %d/%d has mismatched fields", group, level)
}
entry := TalentGrowthLevel{Level: level}
if len(needs) == 1 {
entry.NeedExp = needs[0]
}
for i := range counts {
if types[i] == 0 || counts[i] == 0 || (types[i] == 4 && ids[i] != 0) || (types[i] != 4 && ids[i] == 0) {
rows.Close()
return nil, fmt.Errorf("gamedata: talent growth %d/%d has invalid cost", group, level)
}
entry.Costs = append(entry.Costs, PromotionCost{Type: types[i], ID: ids[i], Count: counts[i]})
}
key := [2]uint64{group, level}
if _, exists := design.Levels[key]; exists {
rows.Close()
return nil, fmt.Errorf("gamedata: duplicate talent growth %d/%d", group, level)
}
design.Levels[key] = entry
}
if err := rows.Err(); err != nil {
rows.Close()
return nil, err
}
if err := rows.Close(); err != nil {
return nil, err
}
if len(design.Characters) == 0 || len(design.Levels) == 0 {
return nil, errors.New("gamedata: empty talent growth design")
}
validated := make(map[uint64]bool)
for _, talent := range design.Characters {
if validated[talent.TalentID] {
continue
}
validated[talent.TalentID] = true
for level := uint64(1); level < talent.MaxLevel; level++ {
entry, ok := design.Levels[[2]uint64{talent.GrowthGroup, level}]
if !ok || entry.NeedExp == 0 || len(entry.Costs) == 0 {
return nil, fmt.Errorf("gamedata: talent %d missing upgrade level %d", talent.TalentID, level)
}
}
}
return design, nil
}
// UpgradeRule returns the exact current-level requirement and cumulative
// experience threshold. It intentionally refuses max-level characters.
func (d *TalentGrowthDesign) UpgradeRule(characterID, currentLevel uint64) (TalentUpgradeRule, error) {
if d == nil || characterID == 0 || currentLevel == 0 {
return TalentUpgradeRule{}, errors.New("gamedata: invalid talent upgrade lookup")
}
talent, found := d.Characters[characterID]
if !found {
return TalentUpgradeRule{}, fmt.Errorf("gamedata: character %d has no talent growth design", characterID)
}
if currentLevel >= talent.MaxLevel {
return TalentUpgradeRule{}, fmt.Errorf("gamedata: character %d talent is already at maximum level %d", characterID, talent.MaxLevel)
}
var required uint64
for level := uint64(1); level <= currentLevel; level++ {
entry, ok := d.Levels[[2]uint64{talent.GrowthGroup, level}]
if !ok || entry.NeedExp == 0 {
return TalentUpgradeRule{}, fmt.Errorf("gamedata: talent %d missing growth level %d", talent.TalentID, level)
}
if required > math.MaxUint64-entry.NeedExp {
return TalentUpgradeRule{}, fmt.Errorf("gamedata: talent %d experience threshold overflows", talent.TalentID)
}
required += entry.NeedExp
}
current := d.Levels[[2]uint64{talent.GrowthGroup, currentLevel}]
if len(current.Costs) == 0 {
return TalentUpgradeRule{}, fmt.Errorf("gamedata: talent %d level %d has no upgrade costs", talent.TalentID, currentLevel)
}
return TalentUpgradeRule{
CharacterID: characterID, TalentID: talent.TalentID, GrowthGroup: talent.GrowthGroup,
CurrentLevel: currentLevel, MaxLevel: talent.MaxLevel, RequiredTotalExp: required,
Costs: append([]PromotionCost(nil), current.Costs...),
}, nil
}