237 lines
7.4 KiB
Go
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
|
|
}
|