470 lines
18 KiB
Go
470 lines
18 KiB
Go
package gameserver
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"math"
|
|
"math/big"
|
|
|
|
"gac-hkrpg-go/internal/common"
|
|
pb "gac-hkrpg-go/protocol/proto"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// Current Client _UpdateHasSelected (assembly identity recorded in the phase
|
|
// contract document) compares phase >= 2. FFGO's selected=1 is incompatible.
|
|
const (
|
|
cwBonfireChoosing = pb.KMJMHDPPAKH_KMJMHDPPAKH_HLLKDFFMIEN
|
|
cwBonfireSelected = pb.KMJMHDPPAKH_KMJMHDPPAKH_BGENLPCJGJF
|
|
)
|
|
|
|
// Entry is a navigation request (empty request, retcode-only response in the
|
|
// revision-tagged schema). Reward choices are separate pending transactions;
|
|
// opening the menu must not consume a choice or award HP/items by itself.
|
|
func init() {
|
|
cwHandle("GridFightEnterBonfire", func(s *Session, _ *pb.GridFightEnterBonfireCsReq, rsp *pb.GridFightEnterBonfireScRsp) error {
|
|
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH)
|
|
catalog, err := common.LoadCurrencyWarsCatalog()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data, revision, err := readCurrencyWarsData(catalog)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if data == nil || data.DHFOFLFCLAH == nil {
|
|
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_NOT_IN_GAMEPLAY)
|
|
return nil
|
|
}
|
|
if revision != s.currencyWarsRevision {
|
|
return common.ErrCurrencyWarsSaveConflict
|
|
}
|
|
if _, active := CURRENT_BATTLE.Load(s.Token); active {
|
|
return fmt.Errorf("CW Bonfire cannot open during Battle")
|
|
}
|
|
stage, node := cwStage(data.DHFOFLFCLAH)
|
|
if stage == nil || node == nil {
|
|
return nil
|
|
}
|
|
// FFGO revision 440df54730ba's gfBonfireEnter resynchronizes the
|
|
// current Bonfire action before acknowledging entry. Our current
|
|
// protocol maps that action to tag 1920 (IIDBLCFAAPC), independently
|
|
// of its renamed reference type. Opening never consumes its rewards.
|
|
pending := data.DHFOFLFCLAH.FMIGOKIDKJL
|
|
if pending != nil {
|
|
if pending.GetIIDBLCFAAPC() == nil {
|
|
return fmt.Errorf("CW Bonfire entry is blocked by pending queue %d", pending.QueuePosition)
|
|
}
|
|
copyPending := proto.Clone(pending).(*pb.CHCHAJIANIM)
|
|
if err := cwSend(s, cwNotify(&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FMIGOKIDKJL{FMIGOKIDKJL: copyPending}})); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if !node.FCNNBODJNIK || len(node.HKKLNENEPNE) == 0 {
|
|
return fmt.Errorf("CW active node is not a Bonfire")
|
|
}
|
|
book, err := common.LoadCurrencyWarsBonfire(catalog)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
player, _, _ := cwComponents(data.DHFOFLFCLAH)
|
|
if player == nil || revision >= uint64(^uint32(0)) {
|
|
return fmt.Errorf("CW Bonfire player/queue missing")
|
|
}
|
|
interest, err := catalog.BaseInterest(player.Gold, player.KNFOCCPOJEA)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
streak, err := cwBonfireStreakGold(catalog, player.GDAKCOICIDA)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
offer := &pb.NCEPOLNLNKH{Phase: cwBonfireChoosing, EIFICNFGGHI: node.PICMDONCKCO, AHJJJNBOMPG: interest, DANCNIMJOLE: streak}
|
|
for _, event := range node.HKKLNENEPNE {
|
|
if _, exists := book.Events[event]; !exists {
|
|
return fmt.Errorf("CW Bonfire event %d missing", event)
|
|
}
|
|
count := len(offer.KFNMIEMBJGD)
|
|
for _, option := range book.Options {
|
|
if option.EventID != event {
|
|
continue
|
|
}
|
|
if option.OptionShowType != "None" {
|
|
return fmt.Errorf("CW Bonfire display %s unsupported", option.OptionShowType)
|
|
}
|
|
offer.KFNMIEMBJGD = append(offer.KFNMIEMBJGD, &pb.MHCOIBONODA{EventId: event, HAFOKMHCGFM: option.OptionID})
|
|
}
|
|
if count == len(offer.KFNMIEMBJGD) {
|
|
return fmt.Errorf("CW Bonfire event %d has no options", event)
|
|
}
|
|
}
|
|
pending = &pb.CHCHAJIANIM{QueuePosition: uint32(revision + 1), FGLIODAJFML: &pb.CHCHAJIANIM_IIDBLCFAAPC{IIDBLCFAAPC: offer}}
|
|
data.DHFOFLFCLAH.FMIGOKIDKJL = pending
|
|
if err = s.saveCurrencyWarsData(data, catalog); err != nil {
|
|
return err
|
|
}
|
|
if err = cwSend(s, cwNotify(&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FMIGOKIDKJL{FMIGOKIDKJL: pending}})); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
rsp.Retcode = 0
|
|
PacketLog(fmt.Sprintf("CW_BONFIRE_ENTRY route=%d chapter=%d section=%d revision=%d", stage.EMOHLJHPOPC, stage.DCAHJELNJNP, stage.SectionId, s.currencyWarsRevision))
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func cwBonfireStreakGold(c *common.CurrencyWarsCatalog, streak uint32) (uint32, error) {
|
|
raw, e := common.ReadResourceFile("resources/CurrencyWarsSettlementRules.json")
|
|
if e != nil {
|
|
return 0, e
|
|
}
|
|
var r struct {
|
|
Client string `json:"target_client_version"`
|
|
Assembly string `json:"target_assembly_sha256"`
|
|
Victories []struct{ Low, Up, Bonus uint32 } `json:"victory_rules"`
|
|
}
|
|
if e = json.Unmarshal(raw, &r); e != nil {
|
|
return 0, e
|
|
}
|
|
if r.Client != c.ClientVersion || r.Assembly != c.Provenance.GameAssemblySHA256 {
|
|
return 0, fmt.Errorf("CW Bonfire income revision differs")
|
|
}
|
|
var result uint32
|
|
found := false
|
|
for _, v := range r.Victories {
|
|
if v.Low <= streak && streak <= v.Up {
|
|
if found {
|
|
return 0, fmt.Errorf("CW streak ranges overlap")
|
|
}
|
|
result = v.Bonus
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return 0, fmt.Errorf("CW streak income missing")
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func cwBonfireRuntime(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH) (*common.CurrencyWarsRuntimeState, error) {
|
|
cfg, e := common.LoadRuntimeConfig()
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
snapshot, e := common.ReadCurrencyWarsSnapshot(cfg.PlayerUID, c.ClientVersion, c.Provenance.GameAssemblySHA256)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
rules := map[string]uint32{}
|
|
if snapshot != nil && snapshot.Runtime != nil && snapshot.Runtime.RunID == run.UniqueId {
|
|
maps.Copy(rules, snapshot.Runtime.Rules)
|
|
}
|
|
return &common.CurrencyWarsRuntimeState{RunID: run.UniqueId, Rules: rules}, nil
|
|
}
|
|
|
|
func cwSelectBonfire(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, request *pb.CAMIICJDOBO) (*pb.GridFightSyncUpdateResultScNotify, *common.CurrencyWarsRuntimeState, error) {
|
|
pending := run.GetFMIGOKIDKJL()
|
|
offer := pending.GetIIDBLCFAAPC()
|
|
if offer == nil || request == nil {
|
|
return nil, nil, fmt.Errorf("CW Bonfire choice is not active")
|
|
}
|
|
if request.GetECDACDNDHGO() != nil {
|
|
return cwTakeBonfire(c, run, offer)
|
|
}
|
|
if offer.Phase == cwBonfireSelected {
|
|
runtime, e := cwBonfireRuntime(c, run)
|
|
if e != nil {
|
|
return nil, nil, e
|
|
}
|
|
stage, _ := cwStage(run)
|
|
key := fmt.Sprintf("BonfireChoice/%d/%d/", stage.DCAHJELNJNP, stage.SectionId)
|
|
if offer.Phase != pb.KMJMHDPPAKH_KMJMHDPPAKH_POMADDDKFHN {
|
|
return nil, nil, fmt.Errorf("CW Bonfire choice was already selected")
|
|
}
|
|
if request.GetNCGOOEMOBIC().GetLGAJNMNOAEM() {
|
|
book, e := common.LoadCurrencyWarsBonfire(c)
|
|
if e != nil { return nil, nil, e }
|
|
stage, _ := cwStage(run)
|
|
player, _, _ := cwComponents(run)
|
|
if stage == nil || player == nil || player.GridFightLineupHp > player.PPKKHHNEOHB {
|
|
return nil, nil, fmt.Errorf("CW Bonfire heal components invalid")
|
|
}
|
|
heal, ok := book.Heal[stage.DCAHJELNJNP]
|
|
if !ok { return nil, nil, fmt.Errorf("CW Bonfire chapter heal missing") }
|
|
hp := uint64(player.GridFightLineupHp) + uint64(heal)
|
|
if hp > uint64(player.PPKKHHNEOHB) { hp = uint64(player.PPKKHHNEOHB) }
|
|
player.GridFightLineupHp = uint32(hp)
|
|
// gfPendingMarkBonfireSelected uses phase 1 for both heal and option.
|
|
offer.Phase = pb.KMJMHDPPAKH_KMJMHDPPAKH_HLLKDFFMIEN
|
|
return cwNotify(
|
|
&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_GridFightLineupHp{GridFightLineupHp: &pb.FMJDDIHBELC{GridFightLineupHp: player.GridFightLineupHp, PPKKHHNEOHB: player.PPKKHHNEOHB}}},
|
|
&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FMIGOKIDKJL{FMIGOKIDKJL: pending}},
|
|
), nil, nil
|
|
}
|
|
selectReq := request.GetNCGOOEMOBIC().GetALEGIFEHGLH()
|
|
replay := selectReq != nil && runtime.Rules[key+"event"] == selectReq.EventId && runtime.Rules[key+"option"] == selectReq.HAFOKMHCGFM
|
|
if selectReq != nil && !replay {
|
|
event, option, proven, proofErr := cwLegacyBonfireChoice(c, run, runtime)
|
|
if proofErr != nil {
|
|
return nil, nil, proofErr
|
|
}
|
|
replay = proven && event == selectReq.EventId && option == selectReq.HAFOKMHCGFM
|
|
}
|
|
if selectReq == nil && request.GetNCGOOEMOBIC() != nil && runtime.Rules[key+"heal"] != 0 {
|
|
replay = true
|
|
}
|
|
if replay {
|
|
return cwNotify(&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FMIGOKIDKJL{FMIGOKIDKJL: pending}}), runtime, nil
|
|
}
|
|
return nil, nil, fmt.Errorf("CW Bonfire option was already selected")
|
|
}
|
|
if offer.Phase != cwBonfireChoosing && offer.Phase != pb.KMJMHDPPAKH_KMJMHDPPAKH_POMADDDKFHN {
|
|
return nil, nil, fmt.Errorf("CW Bonfire phase invalid")
|
|
}
|
|
if offer.Phase != pb.KMJMHDPPAKH_KMJMHDPPAKH_POMADDDKFHN {
|
|
return nil, nil, fmt.Errorf("CW Bonfire choice was already selected")
|
|
}
|
|
if request.GetNCGOOEMOBIC().GetLGAJNMNOAEM() {
|
|
book, e := common.LoadCurrencyWarsBonfire(c)
|
|
if e != nil { return nil, nil, e }
|
|
stage, _ := cwStage(run)
|
|
player, _, _ := cwComponents(run)
|
|
if stage == nil || player == nil || player.GridFightLineupHp > player.PPKKHHNEOHB {
|
|
return nil, nil, fmt.Errorf("CW Bonfire heal components invalid")
|
|
}
|
|
heal, ok := book.Heal[stage.DCAHJELNJNP]
|
|
if !ok { return nil, nil, fmt.Errorf("CW Bonfire chapter heal missing") }
|
|
hp := uint64(player.GridFightLineupHp) + uint64(heal)
|
|
if hp > uint64(player.PPKKHHNEOHB) { hp = uint64(player.PPKKHHNEOHB) }
|
|
player.GridFightLineupHp = uint32(hp)
|
|
// gfPendingMarkBonfireSelected uses phase 1 for both heal and option.
|
|
offer.Phase = pb.KMJMHDPPAKH_KMJMHDPPAKH_HLLKDFFMIEN
|
|
return cwNotify(
|
|
&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_GridFightLineupHp{GridFightLineupHp: &pb.FMJDDIHBELC{GridFightLineupHp: player.GridFightLineupHp, PPKKHHNEOHB: player.PPKKHHNEOHB}}},
|
|
&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FMIGOKIDKJL{FMIGOKIDKJL: pending}},
|
|
), nil, nil
|
|
}
|
|
selectReq := request.GetNCGOOEMOBIC().GetALEGIFEHGLH()
|
|
if selectReq == nil {
|
|
if _, ok := request.GetNCGOOEMOBIC().GetFFBBOFAGGDI().(*pb.MLAENDOFBCK_LGAJNMNOAEM); !ok {
|
|
return nil, nil, fmt.Errorf("CW Bonfire action missing")
|
|
}
|
|
book, e := common.LoadCurrencyWarsBonfire(c)
|
|
if e != nil {
|
|
return nil, nil, e
|
|
}
|
|
stage, _ := cwStage(run)
|
|
player, _, _ := cwComponents(run)
|
|
if stage == nil || player == nil || player.GridFightLineupHp > player.PPKKHHNEOHB {
|
|
return nil, nil, fmt.Errorf("CW Bonfire heal state invalid")
|
|
}
|
|
heal, ok := book.Heal[stage.DCAHJELNJNP]
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("CW Bonfire chapter heal missing")
|
|
}
|
|
if remaining := player.PPKKHHNEOHB - player.GridFightLineupHp; heal > remaining {
|
|
heal = remaining
|
|
}
|
|
player.GridFightLineupHp += heal
|
|
offer.Phase = cwBonfireSelected
|
|
runtime, e := cwBonfireRuntime(c, run)
|
|
if e == nil {
|
|
runtime.Rules[fmt.Sprintf("BonfireChoice/%d/%d/heal", stage.DCAHJELNJNP, stage.SectionId)] = 1
|
|
}
|
|
return cwNotify(cwBonfireHPSync(player), &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FMIGOKIDKJL{FMIGOKIDKJL: pending}}), runtime, e
|
|
}
|
|
found := false
|
|
for _, v := range offer.KFNMIEMBJGD {
|
|
if v.EventId == selectReq.EventId && v.HAFOKMHCGFM == selectReq.HAFOKMHCGFM {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return nil, nil, fmt.Errorf("CW Bonfire selection is outside current offer")
|
|
}
|
|
book, e := common.LoadCurrencyWarsBonfire(c)
|
|
if e != nil {
|
|
return nil, nil, e
|
|
}
|
|
runtime, e := cwBonfireRuntime(c, run)
|
|
if e != nil {
|
|
return nil, nil, e
|
|
}
|
|
for _, option := range book.Options {
|
|
if option.EventID != selectReq.EventId || option.OptionID != selectReq.HAFOKMHCGFM {
|
|
continue
|
|
}
|
|
rules, e := book.OptionRules(option)
|
|
if e != nil {
|
|
return nil, nil, e
|
|
}
|
|
maps.Copy(runtime.Rules, rules)
|
|
offer.Phase = cwBonfireSelected
|
|
stage, _ := cwStage(run)
|
|
key := fmt.Sprintf("BonfireChoice/%d/%d/", stage.DCAHJELNJNP, stage.SectionId)
|
|
runtime.Rules[key+"event"] = selectReq.EventId
|
|
runtime.Rules[key+"option"] = selectReq.HAFOKMHCGFM
|
|
return cwNotify(&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FMIGOKIDKJL{FMIGOKIDKJL: pending}}), runtime, nil
|
|
}
|
|
return nil, nil, fmt.Errorf("CW Bonfire selected recipe missing")
|
|
}
|
|
|
|
func cwBonfireHPSync(player *pb.NIHNGPKHEPC) *pb.KFNCAIHIHCM {
|
|
return &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_GridFightLineupHp{GridFightLineupHp: &pb.FMJDDIHBELC{PPKKHHNEOHB: player.PPKKHHNEOHB, GridFightLineupHp: player.GridFightLineupHp}}}
|
|
}
|
|
|
|
// Reference gfPendingHandleBonfire's take branch pays the saved offer and wave
|
|
// EXP, then finishes the action and creates the next node. Everything here is
|
|
// detached: the caller commits income, inventory and route with one revision CAS.
|
|
func cwTakeBonfire(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, offer *pb.NCEPOLNLNKH) (*pb.GridFightSyncUpdateResultScNotify, *common.CurrencyWarsRuntimeState, error) {
|
|
if offer.Phase != cwBonfireSelected {
|
|
return nil, nil, fmt.Errorf("CW Bonfire take requires a selected option or heal")
|
|
}
|
|
player, _, shop := cwComponents(run)
|
|
if player == nil || shop == nil {
|
|
return nil, nil, fmt.Errorf("CW Bonfire reward components missing")
|
|
}
|
|
runtime, err := cwBonfireRuntime(c, run)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
sum := uint64(player.Gold) + uint64(offer.EIFICNFGGHI) + uint64(offer.AHJJJNBOMPG) + uint64(offer.DANCNIMJOLE)
|
|
if sum > math.MaxUint32 {
|
|
return nil, nil, fmt.Errorf("CW Bonfire income overflow")
|
|
}
|
|
gain, err := c.IntegerRule("GridFight_GainExpWhenWaveEnd")
|
|
if err != nil || gain < 0 || uint64(gain) > math.MaxUint32 {
|
|
return nil, nil, fmt.Errorf("CW Bonfire EXP rule invalid")
|
|
}
|
|
player.Gold = uint32(sum)
|
|
if err = cwApplyBaseExperience(c, player, shop, uint32(gain)); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
notify := cwNotify(cwGoldSync(player.Gold), cwCapacitySync(player), cwShopSync(shop),
|
|
&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_PLPHEIBLPDL{PLPHEIBLPDL: &pb.PIHKLPJGEPM{Level: player.EHNJDIDABHD, Exp: player.GridFightCurLevelExp, HFMKLAPEKFA: player.NDAINFLOLPJ}}})
|
|
for _, drop := range offer.DJOBNPIAEJP {
|
|
// The reference take branch's saved drops are Orb IDs (drop enum 6),
|
|
// not a fresh random roll. Never grant a different item on resume/retry.
|
|
if drop == nil || drop.FKKGLFOHGGA != pb.MJJIFGINJAJ_MJJIFGINJAJ_ANMDMBGNOHL || drop.Num != 1 {
|
|
return nil, nil, fmt.Errorf("CW Bonfire saved drop is not a supported Orb")
|
|
}
|
|
added, e := cwApplyCommand(c, run, common.CurrencyWarsCommand{Field: "orb", Value: drop.FNLMKJLKLFH, Quantity: 1})
|
|
if e != nil {
|
|
return nil, nil, e
|
|
}
|
|
notify.GCPNIHHBGGD = append(notify.GCPNIHHBGGD, added.GCPNIHHBGGD...)
|
|
}
|
|
next, err := cwAdvanceRouteNode(c, run)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
notify.GCPNIHHBGGD = append(notify.GCPNIHHBGGD, next.GCPNIHHBGGD...)
|
|
return notify, runtime, nil
|
|
}
|
|
|
|
// Prove a legacy selection from the old writer's same-node effect marker and
|
|
// exactly one offered option whose supported rule values are already saved.
|
|
// A bare phase=1 is insufficient: new offers legitimately use that phase.
|
|
func cwLegacyBonfireChoice(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, runtime *common.CurrencyWarsRuntimeState) (event, option uint32, proven bool, err error) {
|
|
stage, _ := cwStage(run)
|
|
if runtime == nil || runtime.RunID != run.UniqueId || stage == nil || runtime.Rules[fmt.Sprintf("PetPalStarDownApplied/%d/%d", stage.DCAHJELNJNP, stage.SectionId)] == 0 {
|
|
return
|
|
}
|
|
book, e := common.LoadCurrencyWarsBonfire(c)
|
|
if e != nil {
|
|
err = e
|
|
return
|
|
}
|
|
count := 0
|
|
for _, offered := range run.GetFMIGOKIDKJL().GetIIDBLCFAAPC().GetKFNMIEMBJGD() {
|
|
for _, candidate := range book.Options {
|
|
if candidate.EventID != offered.EventId || candidate.OptionID != offered.HAFOKMHCGFM {
|
|
continue
|
|
}
|
|
rules, e := book.OptionRules(candidate)
|
|
if e != nil {
|
|
continue
|
|
}
|
|
matches := len(rules) > 0
|
|
for k, v := range rules {
|
|
if saved, ok := runtime.Rules[k]; !ok || saved != v {
|
|
matches = false
|
|
}
|
|
}
|
|
if matches {
|
|
count++
|
|
event = candidate.EventID
|
|
option = candidate.OptionID
|
|
}
|
|
}
|
|
}
|
|
proven = count == 1
|
|
return
|
|
}
|
|
|
|
// Projection only; snapshot readers never rewrite persistent or replay effects.
|
|
func cwProjectLegacyBonfirePhase(c *common.CurrencyWarsCatalog, data *pb.GridFightGetDataScRsp, runtime *common.CurrencyWarsRuntimeState) error {
|
|
run := data.GetDHFOFLFCLAH()
|
|
offer := run.GetFMIGOKIDKJL().GetIIDBLCFAAPC()
|
|
if offer == nil || offer.Phase != cwBonfireChoosing {
|
|
return nil
|
|
}
|
|
_, _, proven, err := cwLegacyBonfireChoice(c, run, runtime)
|
|
if err == nil && proven {
|
|
offer.Phase = cwBonfireSelected
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Reference gfRouteLowerEnemyStars: for each newly generated enemy above one
|
|
// star, roll [1,100] and reduce one star at the selected percentage. The saved
|
|
// marker makes retries/resume deterministic instead of rolling repeatedly.
|
|
func cwApplyBonfireEnemyRules(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, runtime *common.CurrencyWarsRuntimeState) (*common.CurrencyWarsRuntimeState, error) {
|
|
if runtime == nil {
|
|
var e error
|
|
runtime, e = cwBonfireRuntime(c, run)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
}
|
|
chance := runtime.Rules["PetPalStarDown"]
|
|
if chance == 0 {
|
|
return runtime, nil
|
|
}
|
|
if chance > 100 {
|
|
return nil, fmt.Errorf("CW enemy-star probability invalid")
|
|
}
|
|
stage, _ := cwStage(run)
|
|
if stage == nil {
|
|
return runtime, nil
|
|
}
|
|
layer := stage.GetIMPNAHBCADN().GetMPOBBIPMNNJ()
|
|
if layer == nil || len(layer.BPDABHGFBFO) == 0 {
|
|
return runtime, nil
|
|
}
|
|
key := fmt.Sprintf("PetPalStarDownApplied/%d/%d", stage.DCAHJELNJNP, stage.SectionId)
|
|
if runtime.Rules[key] != 0 {
|
|
return runtime, nil
|
|
}
|
|
for _, branch := range layer.BPDABHGFBFO {
|
|
for _, wave := range branch.MonsterWaveList {
|
|
for _, monster := range wave.PBLBBDBDLEK {
|
|
if monster.RoleStar <= 1 {
|
|
continue
|
|
}
|
|
roll, e := rand.Int(rand.Reader, big.NewInt(100))
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if uint32(roll.Uint64()+1) <= chance {
|
|
monster.RoleStar--
|
|
}
|
|
}
|
|
}
|
|
}
|
|
runtime.Rules[key] = 1
|
|
return runtime, nil
|
|
}
|