package gameserver import ( "fmt" "maps" "math/rand/v2" "strconv" "strings" "gac-hkrpg-go/internal/common" pb "gac-hkrpg-go/protocol/proto" ) const ( cwTrailblazeTrait uint32 = 100502 cwTrailblazeEffect uint32 = 1005021 cwTrailblazeModifierUID uint32 = 1005021 cwTrailblazeSlots int = 4 ) // cwGetTrailblazeLayer returns the active layer (2, 4, 6, 8, 10 or 0) of the Trailblaze Silver Rail trait. func cwGetTrailblazeLayer(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH) uint32 { traits := cwCalculateBoardTraits(catalog, run) for _, t := range traits { if t.TraitId == cwTrailblazeTrait { return t.KOJFHFAKKFO } } return 0 } func cwTrailblazeActive(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, runtime *common.CurrencyWarsRuntimeState) bool { if runtime != nil && (runtime.Rules["TbOpened"] != 0 || runtime.Rules["TbPower"] > 0) { return true } return cwGetTrailblazeLayer(catalog, run) > 0 } func tbOffered(runtime *common.CurrencyWarsRuntimeState, goodID uint32) bool { if runtime == nil { return false } for slot := 0; slot < cwTrailblazeSlots; slot++ { if runtime.Rules[fmt.Sprintf("TbGood/%d", slot)] == goodID { return true } } return false } func tbBoughtCount(runtime *common.CurrencyWarsRuntimeState, goodID uint32) uint32 { if runtime == nil { return 0 } return runtime.Rules[fmt.Sprintf("TbBought/%d", goodID)] } func tbIsEligible(tbCat *common.CurrencyWarsTrailblazeCatalog, runtime *common.CurrencyWarsRuntimeState, row common.CurrencyWarsTrailblazeGood, tier uint32) bool { if row.Kind == "refresh" || row.ID == tbCat.InitialGoodID { return false } if row.ID/100 > tier+1 { return false } if row.Father != 0 && tbBoughtCount(runtime, row.Father) == 0 { return false } if row.Kind == "maze_buff" && tbBoughtCount(runtime, row.ID) != 0 { return false } pomItem := row.Kind == "item" && len(row.Params) == 1 && (row.Params[0] == 353201 || row.Params[0] == 353202) pomUnlocked := runtime != nil && runtime.Rules["TbCosted"] >= tbCat.PomThreshold if (row.ID == tbCat.PomGoodID || pomItem) && !pomUnlocked { return false } return !tbOffered(runtime, row.ID) } func cwTrailblazeRNG(run *pb.MPIMAEMCEFH, runtime *common.CurrencyWarsRuntimeState) *rand.Rand { var runID uint64 = 1 var nodeKey uint64 = 0 if run != nil { runID = uint64(run.UniqueId) if stage, _ := cwStage(run); stage != nil { nodeKey = uint64(stage.DCAHJELNJNP)*10000 + uint64(stage.SectionId)*100 } } var refresh uint64 = 0 if runtime != nil && runtime.Rules != nil { refresh = uint64(runtime.Rules["TbRefreshCount"]) } seed := runID*1000003 + nodeKey*31 + refresh*7 + 100502 return rand.New(rand.NewPCG(seed, seed^0x5DEECE66D)) } func cwTrailblazeRoll(tbCat *common.CurrencyWarsTrailblazeCatalog, run *pb.MPIMAEMCEFH, runtime *common.CurrencyWarsRuntimeState, tier uint32) { if runtime == nil || runtime.Rules == nil { return } rng := cwTrailblazeRNG(run, runtime) for slot := 0; slot < cwTrailblazeSlots; slot++ { runtime.Rules[fmt.Sprintf("TbGood/%d", slot)] = 0 var eligible []common.CurrencyWarsTrailblazeGood for _, row := range tbCat.GoodsList { if tbIsEligible(tbCat, runtime, row, tier) { eligible = append(eligible, row) } } if len(eligible) == 0 { continue } pick := eligible[rng.IntN(len(eligible))] runtime.Rules[fmt.Sprintf("TbGood/%d", slot)] = pick.ID } } func cwTrailblazeNodeEntered(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, runtime *common.CurrencyWarsRuntimeState) { layer := cwGetTrailblazeLayer(catalog, run) if layer == 0 { return } tbCat, err := common.LoadCurrencyWarsTrailblaze() if err != nil { return } tier := tbCat.Tier(layer) if runtime.Rules == nil { runtime.Rules = make(map[string]uint32) } stage, _ := cwStage(run) var currentNode uint32 if stage != nil { currentNode = stage.DCAHJELNJNP*1000 + stage.SectionId } if runtime.Rules["TbOpened"] == 0 { runtime.Rules["TbOpened"] = 1 runtime.Rules["TbNode"] = currentNode runtime.Rules["TbRefreshCount"] = 0 cwTrailblazeRoll(tbCat, run, runtime, tier) if tbBoughtCount(runtime, tbCat.InitialGoodID) == 0 { runtime.Rules["TbGood/0"] = tbCat.InitialGoodID } } else if runtime.Rules["TbNode"] != currentNode { runtime.Rules["TbNode"] = currentNode runtime.Rules["TbRefreshCount"] = 0 cwTrailblazeRoll(tbCat, run, runtime, tier) if tbBoughtCount(runtime, tbCat.InitialGoodID) == 0 { runtime.Rules["TbGood/0"] = tbCat.InitialGoodID } } } func cwTrailblazeShopProto(tbCat *common.CurrencyWarsTrailblazeCatalog, runtime *common.CurrencyWarsRuntimeState) *pb.GGFHGKGJOOB { shop := &pb.GGFHGKGJOOB{ OBHKKOFDEFG: make(map[uint32]uint32), } if runtime != nil { shop.MJIGJEGMCML = runtime.Rules["TbPower"] for slot := 0; slot < cwTrailblazeSlots; slot++ { gid := runtime.Rules[fmt.Sprintf("TbGood/%d", slot)] if gid != 0 { if good, ok := tbCat.Goods[gid]; ok { shop.OGDKJPDFFAI = append(shop.OGDKJPDFFAI, &pb.AMNAMDLMADK{ GoodsId: gid, OICGFNGNLOE: good.Cost, }) } } } for k, v := range runtime.Rules { if strings.HasPrefix(k, "TbBought/") { idStr := strings.TrimPrefix(k, "TbBought/") if gid, err := strconv.ParseUint(idStr, 10, 32); err == nil && v > 0 { shop.OBHKKOFDEFG[uint32(gid)] = v } } } } shop.OGDKJPDFFAI = append(shop.OGDKJPDFFAI, &pb.AMNAMDLMADK{ GoodsId: tbCat.RefreshGoodID, OICGFNGNLOE: 1, }) return shop } func cwTrailblazeModifierProto(tbCat *common.CurrencyWarsTrailblazeCatalog, runtime *common.CurrencyWarsRuntimeState) *pb.NJKFOJJDGGG { costed := uint32(0) if runtime != nil { costed = runtime.Rules["TbCosted"] } return &pb.NJKFOJJDGGG{ UniqueId: cwTrailblazeModifierUID, FMEKLFFDPGG: &pb.NJKFOJJDGGG_ONKMJAGNECC{ ONKMJAGNECC: &pb.CONKAFGLHFE{ IANFMIJBKAF: costed, PLAIPMBIHGJ: tbCat.PomThreshold, }, }, } } func cwTrailblazeTraitEffectProto(tbCat *common.CurrencyWarsTrailblazeCatalog, runtime *common.CurrencyWarsRuntimeState) *pb.MGBHKCMHDOK { return &pb.MGBHKCMHDOK{ EffectId: cwTrailblazeEffect, NEBNJDMFELO: &pb.MGBHKCMHDOK_KDBKIPPHMFI{ KDBKIPPHMFI: cwTrailblazeShopProto(tbCat, runtime), }, } } type tbListedGood struct { id uint32 cost uint32 slot int } func cwTrailblazeBuy(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, runtime *common.CurrencyWarsRuntimeState, index uint32) (*pb.GridFightSyncUpdateResultScNotify, error) { if !cwTrailblazeActive(catalog, run, runtime) { return nil, fmt.Errorf("Trailblaze Silver Rail trait is inactive") } layer := cwGetTrailblazeLayer(catalog, run) tbCat, err := common.LoadCurrencyWarsTrailblaze() if err != nil { return nil, err } if runtime == nil || runtime.Rules == nil { return nil, fmt.Errorf("runtime rules missing") } if runtime.Rules["TbOpened"] == 0 { cwTrailblazeNodeEntered(catalog, run, runtime) } var listed []tbListedGood for slot := 0; slot < cwTrailblazeSlots; slot++ { gid := runtime.Rules[fmt.Sprintf("TbGood/%d", slot)] if gid != 0 { if good, ok := tbCat.Goods[gid]; ok { listed = append(listed, tbListedGood{id: gid, cost: good.Cost, slot: slot}) } } } listed = append(listed, tbListedGood{id: tbCat.RefreshGoodID, cost: 1, slot: -1}) if int(index) >= len(listed) { return nil, fmt.Errorf("goods index %d out of bounds (listed len %d)", index, len(listed)) } target := listed[index] good, ok := tbCat.Goods[target.id] if !ok { return nil, fmt.Errorf("goods %d definition missing", target.id) } power := runtime.Rules["TbPower"] if power < target.cost { return nil, fmt.Errorf("insufficient Trailblaze Power: have %d need %d", power, target.cost) } player, team, _ := cwComponents(run) bag := cwEquipmentBag(run) if player == nil || team == nil || bag == nil { return nil, fmt.Errorf("CW components missing") } var grantUpdates []*pb.KFNCAIHIHCM switch good.Kind { case "role": if len(good.Params) < 1 { return nil, fmt.Errorf("role goods %d has empty params", good.ID) } roleID := good.Params[0] star := uint32(1) if len(good.Params) >= 2 && good.Params[1] > 0 { star = good.Params[1] } front, e := catalog.IntegerRule("GridFight_Front_AvatarMaxNum") if e != nil { return nil, e } back, e := catalog.IntegerRule("GridFight_Back_AvatarMaxNum") if e != nil { return nil, e } overflow, e := catalog.IntegerRule("GridFight_Bench_OverFlow_AvatarNum") if e != nil { return nil, e } occupied := map[uint32]bool{} for _, r := range team.GridGameRoleList { occupied[r.Pos] = true } var pos uint32 for p := uint64(front+back) + 1; p <= uint64(front+back)+uint64(player.MICEOJBAKGN)+uint64(overflow); p++ { if !occupied[uint32(p)] { pos = uint32(p) break } } if pos == 0 { return nil, fmt.Errorf("bench is full") } uid, e := cwNextEntityID(run) if e != nil { return nil, e } var scales map[uint32]uint32 if len(team.GridGameRoleList) > 0 { scales = maps.Clone(team.GridGameRoleList[0].ConvertPropertyToFixpoint) } newRole := &pb.CDEAABODKCP{ Id: roleID, RoleStar: star, UniqueId: uid, Pos: pos, ConvertPropertyToFixpoint: scales, } team.GridGameRoleList = append(team.GridGameRoleList, newRole) grantUpdates = append(grantUpdates, &pb.KFNCAIHIHCM{ MNLCHEJKDHO: &pb.KFNCAIHIHCM_PIDDDHKPDDM{PIDDDHKPDDM: newRole}, }) merged, e := cwMergeRoles(catalog, run, uid) if e == nil && len(merged) > 0 { for _, m := range merged { grantUpdates = append(grantUpdates, m.JKPJFKCBIEM...) } } case "item": if len(good.Params) < 1 { return nil, fmt.Errorf("item goods %d has empty params", good.ID) } itemID := good.Params[0] rewards, _ := common.LoadCurrencyWarsRewards(catalog) isConsumable := false if rewards != nil { _, isConsumable = rewards.Consumables[itemID] } if isConsumable { result, err := cwApplyCommand(catalog, run, common.CurrencyWarsCommand{Field: "consumable", Value: itemID, Quantity: 1}, runtime) if err != nil { return nil, err } for _, g := range result.GCPNIHHBGGD { grantUpdates = append(grantUpdates, g.JKPJFKCBIEM...) } } else { result, err := cwApplyCommand(catalog, run, common.CurrencyWarsCommand{Field: "equip", Value: itemID, Quantity: 1}, runtime) if err != nil { return nil, err } for _, g := range result.GCPNIHHBGGD { grantUpdates = append(grantUpdates, g.JKPJFKCBIEM...) } } case "power": if len(good.Params) >= 1 { runtime.Rules["TbPower"] += good.Params[0] } case "maze_buff": if tbBoughtCount(runtime, target.id) > 0 { return nil, fmt.Errorf("buff %d is already purchased", target.id) } case "stat": // Handled via recorded bought count } wasUnlocked := runtime.Rules["TbCosted"] >= tbCat.PomThreshold runtime.Rules["TbPower"] -= target.cost runtime.Rules["TbCosted"] += target.cost if target.id == tbCat.RefreshGoodID { runtime.Rules["TbRefreshCount"]++ cwTrailblazeRoll(tbCat, run, runtime, tbCat.Tier(layer)) if tbBoughtCount(runtime, tbCat.InitialGoodID) == 0 { runtime.Rules["TbGood/0"] = tbCat.InitialGoodID } } else { runtime.Rules[fmt.Sprintf("TbBought/%d", target.id)]++ if target.slot >= 0 && target.slot < cwTrailblazeSlots { runtime.Rules[fmt.Sprintf("TbGood/%d", target.slot)] = 0 } } if !wasUnlocked && runtime.Rules["TbCosted"] >= tbCat.PomThreshold { slotToPut := target.slot if slotToPut < 0 || slotToPut >= cwTrailblazeSlots || runtime.Rules[fmt.Sprintf("TbGood/%d", slotToPut)] != 0 { slotToPut = 0 for s := 0; s < cwTrailblazeSlots; s++ { if runtime.Rules[fmt.Sprintf("TbGood/%d", s)] == 0 { slotToPut = s break } } } runtime.Rules[fmt.Sprintf("TbGood/%d", slotToPut)] = tbCat.PomGoodID } cwSyncTraitComponent(catalog, run, runtime) bundle := &pb.ABPLALCHNFA{ GridUpdateSrc: pb.JJDFGMMMCMI_JJDFGMMMCMI_GLKAGBGLENM, OOEOCGLLHFO: []uint32{cwTrailblazeTrait, cwTrailblazeEffect}, JKPJFKCBIEM: []*pb.KFNCAIHIHCM{ { MNLCHEJKDHO: &pb.KFNCAIHIHCM_HKHJJCMKKJE{ HKHJJCMKKJE: &pb.JLKCBOIFJKC{ TraitId: cwTrailblazeTrait, ENPLACDLJGA: cwTrailblazeTraitEffectProto(tbCat, runtime), }, }, }, { MNLCHEJKDHO: &pb.KFNCAIHIHCM_GOOCHGKBIPN{ GOOCHGKBIPN: cwTrailblazeModifierProto(tbCat, runtime), }, }, }, } bundle.JKPJFKCBIEM = append(bundle.JKPJFKCBIEM, grantUpdates...) return &pb.GridFightSyncUpdateResultScNotify{ GCPNIHHBGGD: []*pb.ABPLALCHNFA{bundle}, }, nil } func init() { cwHandle("GridFightTraitUpdate", func(s *Session, req *pb.GridFightTraitUpdateCsReq, rsp *pb.GridFightTraitUpdateScRsp) error { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) if req == nil { return nil } 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 } s.currencyWarsRevision = revision run := data.DHFOFLFCLAH runtime, err := cwBonfireRuntime(catalog, run) if err != nil { return err } if req.GetTraitId() == cwTrailblazeTrait && req.GetEffectId() == cwTrailblazeEffect && req.GetKDBKIPPHMFI() != nil { notify, buyErr := cwTrailblazeBuy(catalog, run, runtime, req.GetKDBKIPPHMFI().Pos) if buyErr != nil { PacketLog(fmt.Sprintf("CW_TRAIT_UPDATE Trailblaze rejected: %v", buyErr)) rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) return nil } if err = s.saveCurrencyWarsDataWithRuntime(data, catalog, runtime); err != nil { return err } if err = s.Send(notify); err != nil { return err } rsp.Retcode = 0 PacketLog(fmt.Sprintf("CW_TRAIT_UPDATE Trailblaze bought pos=%d power=%d costed=%d", req.GetKDBKIPPHMFI().Pos, runtime.Rules["TbPower"], runtime.Rules["TbCosted"])) return nil } rsp.Retcode = 0 PacketLog(fmt.Sprintf("CW_TRAIT_UPDATE trait=%d effect=%d", req.GetTraitId(), req.GetEffectId())) return nil }) }