Files
bd2/go/internal/server/domain/battle/hunting/hunting.go
T

215 lines
6.6 KiB
Go

// Package hunting owns ordinary HuntingGround progress and repeatable rewards.
package hunting
import (
"bd2server/internal/server/design/gamedata"
"bd2server/internal/server/domain/command"
assets "bd2server/internal/server/domain/inventory"
"bd2server/internal/server/platform/versionconfig"
"bd2server/internal/server/storage/stateio"
"encoding/json"
"fmt"
"maps"
"math"
"slices"
"strconv"
"time"
)
const BattleMode uint64 = 5
type packState struct {
Current, Highest uint64
Auto bool
Defeated []uint64
}
type snapshot struct {
Version string `json:"version"`
Free uint64 `json:"free"`
Bonus uint64 `json:"bonus"`
Packs map[string]packState `json:"packs"`
Receipts map[string]bool `json:"receipts"`
}
// Rules supplies shared immutable hunting and dispatch designs.
type Rules interface {
HuntingPack(int) (*gamedata.HuntingPack, error)
DispatchDesign(uint64, uint64) (*gamedata.DispatchDesign, error)
}
type Service struct {
storage stateio.Store
inventory *assets.Inventory
wallet *assets.Wallet
state snapshot
currentPack func(command.Context) (int, error)
load func(int) (*gamedata.HuntingPack, error)
dispatchLoad func(uint64, uint64) (*gamedata.DispatchDesign, error)
dispatchEligibility func(command.Context, *gamedata.DispatchDesign) error
dispatchCosts func(command.Context, *gamedata.DispatchDesign, uint64) ([]gamedata.Reward, error)
dispatchExchange func(command.Context, string, []gamedata.Reward, []gamedata.Reward) ([]byte, error)
dispatchBonus func(*gamedata.DispatchDesign, []gamedata.BattleReward) ([]gamedata.BattleReward, error)
dispatchProgress func(command.Context, uint64) error
eligibility func(ctx command.Context, _ int, _ uint64) error
grant func(command.Context, string, []gamedata.Reward) ([]byte, error)
apDesign *gamedata.HuntingAPDesign
now func() time.Time
}
func Open(ctx command.Context, store stateio.Store, rules Rules, inventory *assets.Inventory, wallet *assets.Wallet, currentPack func(command.Context) (int, error), free, bonus uint64) (*Service, error) {
if store == nil || rules == nil || inventory == nil || wallet == nil || currentPack == nil {
return nil, fmt.Errorf("hunting: invalid configuration")
}
s := &Service{storage: store, inventory: inventory, wallet: wallet, currentPack: currentPack, state: snapshot{versionconfig.State(), free, bonus, map[string]packState{}, map[string]bool{}}}
s.dispatchLoad = rules.DispatchDesign
s.load = rules.HuntingPack
b, err := store.Load(ctx.State, "hunting")
if err != nil {
return nil, err
}
if b != nil {
if err := stateio.RequireExactJSONObject(b, "version", "free", "bonus", "packs", "receipts"); err != nil {
return nil, err
}
if err := json.Unmarshal(b, &s.state); err != nil {
return nil, err
}
if s.state.Version != versionconfig.State() || s.state.Packs == nil || s.state.Receipts == nil {
return nil, fmt.Errorf("hunting: incompatible state")
}
}
if s.state.Free > math.MaxInt32 || s.state.Bonus > math.MaxInt32 {
return nil, fmt.Errorf("hunting: AP exceeds protocol range")
}
for key, st := range s.state.Packs {
pack, err := strconv.Atoi(key)
if err != nil || pack <= 0 || strconv.Itoa(pack) != key {
return nil, fmt.Errorf("hunting: invalid saved pack")
}
d, err := s.load(pack)
if err != nil {
return nil, err
}
if _, ok := ground(d, st.Current); !ok {
return nil, fmt.Errorf("hunting: saved current ground missing from GameData")
}
if st.Highest != 0 {
if _, ok := ground(d, st.Highest); !ok {
return nil, fmt.Errorf("hunting: saved highest ground missing from GameData")
}
}
g, _ := ground(d, st.Current)
seen := map[uint64]bool{}
for _, id := range st.Defeated {
member := id == g.BossID
for _, candidate := range g.Monsters {
member = member || id == candidate
}
if !member || seen[id] {
return nil, fmt.Errorf("hunting: invalid defeated monster")
}
seen[id] = true
}
}
for key, claimed := range s.state.Receipts {
if key == "" || !claimed {
return nil, fmt.Errorf("hunting: invalid receipt")
}
}
return s, nil
}
func (s *Service) HuntingAP(ctx command.Context) (uint64, uint64, error) {
if e := s.refreshAP(ctx); e != nil {
return 0, 0, e
}
return s.state.Free, s.state.Bonus, nil
}
func ground(d *gamedata.HuntingPack, id uint64) (gamedata.HuntingGround, bool) {
for _, g := range d.Grounds {
if g.ID == id {
return g, true
}
}
return gamedata.HuntingGround{}, false
}
func (s *Service) ValidateBattle(ctx command.Context, pack int, mode, monster, deck uint64) error {
if mode != BattleMode {
return nil
}
if e := s.refreshAP(ctx); e != nil {
return e
}
_, _, _, err := s.validate(pack, monster, deck)
return err
}
func (s *Service) validate(pack int, monster, deck uint64) (*gamedata.HuntingPack, gamedata.HuntingGround, gamedata.HuntingMonster, error) {
d, err := s.load(pack)
if err != nil {
return nil, gamedata.HuntingGround{}, gamedata.HuntingMonster{}, err
}
st := s.state.Packs[strconv.Itoa(pack)]
g, ok := ground(d, st.Current)
if !ok {
return nil, g, gamedata.HuntingMonster{}, fmt.Errorf("hunting: battle before ground enter")
}
member := monster == g.BossID
for _, id := range g.Monsters {
member = member || id == monster
}
m := d.Monsters[monster]
if monster == g.BossID {
for _, id := range g.Monsters {
done := false
for _, dead := range st.Defeated {
done = done || dead == id
}
if !done {
return nil, g, m, fmt.Errorf("hunting: boss locked until ordinary monsters are defeated")
}
}
}
if slices.Contains(st.Defeated, monster) {
return nil, g, m, fmt.Errorf("hunting: defeated monster requires reentry")
}
known := false
for _, id := range m.Decks {
known = known || id == deck
}
if !member || !known {
return nil, g, m, fmt.Errorf("hunting: monster/deck does not belong to selected ground")
}
cost := d.NormalAP
if m.Type == 1 {
cost = d.BossAP
}
if s.state.Free < cost && s.state.Bonus < cost-s.state.Free {
return nil, g, m, fmt.Errorf("hunting: insufficient hunting AP")
}
return d, g, m, nil
}
func (s *Service) clone() snapshot {
next := s.state
next.Packs = map[string]packState{}
maps.Copy(next.Packs, s.state.Packs)
next.Receipts = map[string]bool{}
maps.Copy(next.Receipts, s.state.Receipts)
return next
}
func (s *Service) persist(ctx command.Context, next snapshot) error {
b, err := json.Marshal(next)
if err != nil {
return err
}
if err := s.storage.Save(ctx.State, "hunting", b); err != nil {
return err
}
s.state = next
return nil
}