Persist claims, AP, teams and presets with session receipts and atomic request rollback. Read rewards and growth from GameData; replace fixed Fiend Hunt user state and preserve the account initialization contract.
275 lines
7.1 KiB
Go
275 lines
7.1 KiB
Go
package gamedata
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
type MonsterHuntRewards struct{ Clear, Daily []BattleReward }
|
|
type MonsterHuntRankReward struct {
|
|
ID, Type uint64
|
|
Ranking float64
|
|
Rewards []BattleReward
|
|
}
|
|
|
|
// MonsterHunt is the immutable common database design. Hunt decks live in
|
|
// common.db, as BattleDeckInfo.GetBattleDeckInfo(id, true) specifies.
|
|
type MonsterHunt struct {
|
|
ID, PackID, MonsterID, DeckID, RewardGroupID, MaxLevel, RewardLevel, ChallengeableLevel uint64
|
|
TeamOpenLevels []uint64
|
|
Rewards map[uint64]MonsterHuntRewards
|
|
Ranks map[uint64][]MonsterHuntRankReward
|
|
baseHP, healthRate, healthSlope, stage2Ratio, stage3Ratio float64
|
|
stage2Level, stage3Level uint64
|
|
}
|
|
|
|
func LoadMonsterHunt(root, version string, id uint64) (*MonsterHunt, error) {
|
|
db, done, err := openStatDatabase(root, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer done()
|
|
return loadMonsterHunt(db, id)
|
|
}
|
|
func loadMonsterHunt(db *sql.DB, id uint64) (*MonsterHunt, error) {
|
|
if id == 0 {
|
|
return nil, fmt.Errorf("gamedata: zero monster hunt")
|
|
}
|
|
var raw []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM MonsterHuntTable WHERE id=?", id).Scan(&raw); err != nil {
|
|
return nil, err
|
|
}
|
|
d := &MonsterHunt{Rewards: map[uint64]MonsterHuntRewards{}, Ranks: map[uint64][]MonsterHuntRankReward{}}
|
|
for n, p := range map[int]*uint64{8: &d.ID, 18: &d.PackID, 16: &d.MonsterID, 1: &d.DeckID, 22: &d.RewardGroupID, 23: &d.RewardLevel, 14: &d.ChallengeableLevel, 26: &d.stage2Level, 28: &d.stage3Level} {
|
|
v, e := optionalScalar(raw, n)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
*p = v
|
|
}
|
|
for n, p := range map[int]*float64{11: &d.healthRate, 12: &d.healthSlope, 27: &d.stage2Ratio, 29: &d.stage3Ratio} {
|
|
v, _, e := fixed64Double(raw, n)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
*p = v
|
|
}
|
|
var err error
|
|
d.TeamOpenLevels, err = packedInts(raw, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
group, err := optionalScalar(raw, 19)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := db.Query("SELECT ProtoBuf FROM MonsterHuntPartsTable WHERE groupId=? ORDER BY id", group)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var body uint64
|
|
for rows.Next() {
|
|
var part []byte
|
|
if err = rows.Scan(&part); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
v, e := optionalScalar(part, 7)
|
|
if e != nil {
|
|
rows.Close()
|
|
return nil, e
|
|
}
|
|
if body == 0 && v > 0 {
|
|
body = v
|
|
}
|
|
}
|
|
err = rows.Err()
|
|
rows.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if body == 0 {
|
|
return nil, fmt.Errorf("gamedata: hunt %d missing body", id)
|
|
}
|
|
var deck []byte
|
|
if err = db.QueryRow("SELECT ProtoBuf FROM BattleDeckTable WHERE id=?", d.DeckID).Scan(&deck); err != nil {
|
|
return nil, err
|
|
}
|
|
chars, err := packedInts(deck, 14)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
levels, err := packedInts(deck, 30)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(chars) != len(levels) {
|
|
return nil, fmt.Errorf("gamedata: hunt deck level arrays differ")
|
|
}
|
|
level := uint64(1)
|
|
for i, c := range chars {
|
|
if c == body {
|
|
level = levels[i]
|
|
break
|
|
}
|
|
}
|
|
stats, err := loadCharacterStatDesign(db)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
base, err := stats.BaseStats(body, level)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
deckRate, _, err := fixed64Double(deck, 28)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d.baseHP = math.Trunc(base.Health*deckRate*100) / 100
|
|
rows, err = db.Query("SELECT level, ProtoBuf FROM MonsterHuntRewardTable WHERE groupId=? ORDER BY level", d.RewardGroupID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var lv uint64
|
|
var r []byte
|
|
if err = rows.Scan(&lv, &r); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
clear, e := monsterHuntRewardArrays(r, 8, 7, 6)
|
|
if e != nil {
|
|
rows.Close()
|
|
return nil, e
|
|
}
|
|
daily, e := monsterHuntRewardArrays(r, 3, 2, 1)
|
|
if e != nil {
|
|
rows.Close()
|
|
return nil, e
|
|
}
|
|
d.Rewards[lv] = MonsterHuntRewards{clear, daily}
|
|
if lv > d.MaxLevel {
|
|
d.MaxLevel = lv
|
|
}
|
|
}
|
|
err = rows.Err()
|
|
rows.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err = db.Query("SELECT groupId,id,ProtoBuf FROM MonsterHuntRankTable ORDER BY groupId,id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var g uint64
|
|
var r MonsterHuntRankReward
|
|
var p []byte
|
|
if err = rows.Scan(&g, &r.ID, &p); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
r.Type, err = optionalScalar(p, 6)
|
|
if err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
r.Ranking, _, err = fixed64Double(p, 5)
|
|
if err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
r.Rewards, err = monsterHuntRewardArrays(p, 9, 8, 7)
|
|
if err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
d.Ranks[g] = append(d.Ranks[g], r)
|
|
}
|
|
err = rows.Err()
|
|
rows.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if d.ID != id || d.PackID == 0 || d.DeckID == 0 || d.MaxLevel == 0 || d.baseHP <= 0 {
|
|
return nil, fmt.Errorf("gamedata: invalid hunt %d design", id)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// HP matches MonsterHuntInfo.ApplyMonsterHuntStatRate, including banker
|
|
// rounding and truncation to three significant decimal digits.
|
|
func (d *MonsterHunt) HP(level uint64) (uint64, error) {
|
|
if d == nil || level == 0 || level > d.MaxLevel {
|
|
return 0, fmt.Errorf("gamedata: invalid hunt level %d", level)
|
|
}
|
|
ratio := 1.0
|
|
if level > d.stage2Level {
|
|
ratio = d.stage2Ratio
|
|
if level > d.stage3Level {
|
|
ratio = d.stage3Ratio
|
|
}
|
|
}
|
|
rate := 1 + float64(level-1)*d.healthRate*.01*math.Pow(float64(level), d.healthSlope)*ratio
|
|
hp := math.RoundToEven(d.baseHP * rate)
|
|
if math.IsNaN(hp) || math.IsInf(hp, 0) || hp < 1 || hp >= float64(math.MaxInt64) {
|
|
return 0, fmt.Errorf("gamedata: hunt HP overflow")
|
|
}
|
|
value := uint64(hp)
|
|
scale := uint64(1)
|
|
for value/scale >= 1000 {
|
|
scale *= 10
|
|
}
|
|
return value / scale * scale, nil
|
|
}
|
|
func monsterHuntRewardArrays(raw []byte, typeField, idField, countField int) ([]BattleReward, error) {
|
|
types, e := packedInts(raw, typeField)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
ids, e := packedInts(raw, idField)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
counts, e := packedInts(raw, countField)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if len(types) != len(ids) || len(ids) != len(counts) {
|
|
return nil, fmt.Errorf("gamedata: hunt reward arrays differ")
|
|
}
|
|
var result []BattleReward
|
|
for i, t := range types {
|
|
if t > 0 && counts[i] > 0 {
|
|
result = append(result, BattleReward{Type: t, ID: ids[i], Count: counts[i]})
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func LoadMonsterHuntPresetDesign(root, version string) (*PresetDesign, error) {
|
|
db, done, err := openStatDatabase(root, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer done()
|
|
d, err := loadPresetDesign(db)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var raw []byte
|
|
if err = db.QueryRow("SELECT ProtoBuf FROM GameDefaultTable WHERE id=0").Scan(&raw); err != nil {
|
|
return nil, err
|
|
}
|
|
d.BaseCount, err = optionalScalar(raw, 84)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d.Maximum, err = optionalScalar(raw, 85)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return d, d.Validate()
|
|
}
|