package gameserver import ( "fmt" "math/rand/v2" "sort" "gac-hkrpg-go/internal/common" pb "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" ) // cwGenerateSupplyOffer follows the Supply option factory in the reference // executable identified in docs/cw-route-continuation-audit.md. The protocol // envelope comes from the native capture; role and equipment IDs must also be // present in this client's captured data. func cwGenerateSupplyOffer(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH) (*pb.IAKEDFLIOFD, error) { stage, _ := cwStage(run) if stage == nil || stage.DCAHJELNJNP == 0 { return nil, fmt.Errorf("CW Supply chapter missing") } runtime, err := cwBonfireRuntime(c, run) if err != nil { return nil, err } return cwGenerateSupplyOfferWithRules(c, run, runtime.Rules) } func cwGenerateSupplyOfferWithRules(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, rules map[string]uint32) (*pb.IAKEDFLIOFD, error) { stage, _ := cwStage(run) if stage == nil || stage.DCAHJELNJNP == 0 { return nil, fmt.Errorf("CW Supply chapter missing") } // Box modifies a different part of the selection flow; do not silently // grant an ordinary selection if the corresponding action is not mapped. if rules["SupplyBox"] != 0 { return nil, fmt.Errorf("CW SupplyBox action is not mapped") } count, err := c.IntegerRule("GridFight_BaseSupplyRoleNum") // gfFxHProjectRunState projects ExtraSupplyOption into the field added by // gfPendingFillSupplyOptions to the Client's captured base count. if err != nil || count <= 0 || uint64(count)+uint64(rules["ExtraSupplyOption"]) > uint64(len(c.CapturedRoles)) { return nil, fmt.Errorf("CW Supply option count invalid") } count += int64(rules["ExtraSupplyOption"]) star := rules["SupplyStar"] if star == 0 { star = 1 } // gfPendingSupplyOf clamps this rule to one. category := "Basic" if rules["SupplyEmblems"] != 0 { category = "Emblem" } else if stage.DCAHJELNJNP >= 3 { category = "Craftable" } recipes, err := common.LoadCurrencyWarsOrbRecipes() if err != nil { return nil, err } gear, err := common.LoadCurrencyWarsEquipment(c) if err != nil { return nil, err } var equipment []uint32 for _, item := range recipes.Equipment { if item.Category == category { if _, ok := gear[item.ID]; ok { equipment = append(equipment, item.ID) } } } if len(equipment) == 0 { return nil, fmt.Errorf("CW Supply equipment category %s is empty", category) } sort.Slice(equipment, func(i, j int) bool { return equipment[i] < equipment[j] }) pool, err := c.NewCapturedCardPool() if err != nil { return nil, err } byRarity := make(map[uint32][]uint32) for id, item := range pool.Roles { if item.Disabled || item.Remaining == 0 { continue } if _, err := c.CapturedRoleVariant(id, star); err != nil { continue } byRarity[item.Rarity] = append(byRarity[item.Rarity], id) } for _, ids := range byRarity { sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) } entries, err := cwNodeTransitions(c) if err != nil { return nil, err } var offer *pb.IAKEDFLIOFD for _, entry := range entries { pending := new(pb.CHCHAJIANIM) if err := proto.Unmarshal(entry.RoundWire, pending); err != nil { return nil, err } if captured := pending.GetFIKAGBAEOAP(); captured != nil { candidate := proto.Clone(captured).(*pb.IAKEDFLIOFD) candidate.MFBAKLCBPCJ = nil if offer != nil && !proto.Equal(offer, candidate) { return nil, fmt.Errorf("CW Supply envelopes differ") } offer = candidate } } if offer == nil { return nil, fmt.Errorf("CW Supply envelope not captured") } used := map[uint32]bool{} for i := int64(0); i < int64(count); i++ { var id uint32 for attempt := 0; attempt < 8; attempt++ { rarity := rules["SupplyRarity"] if rarity == 0 { rarity = min(uint32(rand.IntN(int(stage.DCAHJELNJNP+2)))+1, uint32(5)) } // gfPoolRandomRoleFiltered searches the requested rarity first, // then neighbouring rarity buckets at increasing distance. for distance := uint32(0); distance < 5 && id == 0; distance++ { for _, candidate := range []int64{int64(rarity) - int64(distance), int64(rarity) + int64(distance)} { if candidate < 1 || candidate > 5 { continue } ids := byRarity[uint32(candidate)] if len(ids) != 0 { id = ids[rand.IntN(len(ids))] break } } } if id != 0 && !used[id] { break } id = 0 } if id == 0 { continue } used[id] = true item := equipment[rand.IntN(len(equipment))] offer.MFBAKLCBPCJ = append(offer.MFBAKLCBPCJ, &pb.NBGDEFLFLMI{RoleId: id, OPPFPECOBEF: &pb.BCCONKJHDJL{FKKGLFOHGGA: pb.MJJIFGINJAJ_MJJIFGINJAJ_BDPIMPJOJBK, FNLMKJLKLFH: id, Num: 1, DisplayValue: &pb.AOHKKFHDALO{NEBNJDMFELO: &pb.AOHKKFHDALO_RoleStar{RoleStar: star}}}, GridFightItemList: []uint32{item}, DHLBEFLOOPM: []*pb.BCCONKJHDJL{{FKKGLFOHGGA: pb.MJJIFGINJAJ_MJJIFGINJAJ_NLFCABFPJHD, FNLMKJLKLFH: item, Num: 1}}}) } if offer.DAEMEBCKHIK == 0 || uint64(len(offer.MFBAKLCBPCJ)) < uint64(offer.DAEMEBCKHIK) { return nil, fmt.Errorf("CW Supply has too few valid role options") } chance, item := rules["SupplyEquipChance"], rules["SupplyEquip"] if chance > 100 { return nil, fmt.Errorf("CW Supply equipment chance invalid") } if chance != 0 && item != 0 && stage.DCAHJELNJNP >= rules["SupplyEquipChapter"] { if _, ok := gear[item]; !ok { return nil, fmt.Errorf("CW Supply bonus equipment %d is not captured", item) } if uint32(rand.IntN(100)) < chance { choice := offer.MFBAKLCBPCJ[rand.IntN(len(offer.MFBAKLCBPCJ))] choice.GridFightItemList[0] = item choice.DHLBEFLOOPM[0].FNLMKJLKLFH = item } } return offer, nil }