package gameserver import ( "encoding/json" "fmt" "strings" "sync" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" ) // Game IDs and character exceptions are resource data with an upstream revision, // not executable defaults. Missing or invalid rules must not silently produce a // different battle. type battleRules struct { EntryAttackBuffs map[string]uint32 `json:"entry_attack_buffs"` Source struct { Revision string `json:"revision"` } `json:"source"` TechniqueBuffs map[uint32][]struct { ID uint32 `json:"id"` Owner string `json:"owner"` DynamicValues map[string]float32 `json:"dynamic_values"` } `json:"technique_buffs"` GlobalPassives []struct { Setting string `json:"setting"` BuffID uint32 `json:"buff_id"` OwnerSlot uint32 `json:"owner_slot"` } `json:"global_passives"` IgnoreToughness struct { Avatars []uint32 `json:"avatars"` BuffID uint32 `json:"buff_id"` } `json:"ignore_toughness"` BattleTargets []struct { BattleType string `json:"battle_type"` Targets map[uint32][]struct { ID uint32 `json:"id"` Progress uint32 `json:"progress"` TotalProgress uint32 `json:"total_progress"` } `json:"targets"` } `json:"battle_targets"` InvasionBuffs map[uint32]uint32 `json:"invasion_buffs"` InvasionSupportBuff uint32 `json:"invasion_support_buff"` } var battleRulesOnce sync.Once var loadedBattleRules battleRules var battleRulesErr error func loadBattleRules() (*battleRules, error) { battleRulesOnce.Do(func() { raw, err := common.ReadResourceFile("resources/BattleRules.json", "BattleRules.json") if err != nil { battleRulesErr = err return } if err := json.Unmarshal(raw, &loadedBattleRules); err != nil { battleRulesErr = err return } if loadedBattleRules.Source.Revision == "" { battleRulesErr = fmt.Errorf("battle rules need a source revision") return } for _, rules := range loadedBattleRules.TechniqueBuffs { for _, rule := range rules { if rule.ID == 0 || (rule.Owner != "avatar" && rule.Owner != "leader") { battleRulesErr = fmt.Errorf("invalid technique buff rule: %+v", rule) return } } } for _, rule := range loadedBattleRules.GlobalPassives { if rule.BuffID == 0 || rule.OwnerSlot >= 4 { battleRulesErr = fmt.Errorf("invalid global passive rule: %+v", rule) return } switch rule.Setting { case "enable_sw_global", "enable_castorice_global", "enable_aha_global": default: battleRulesErr = fmt.Errorf("unknown global passive setting %q", rule.Setting) return } } }) if battleRulesErr != nil { return nil, fmt.Errorf("load resources/BattleRules.json: %w", battleRulesErr) } return &loadedBattleRules, nil } func battleBuff(id, owner uint32, dynamic map[string]float32) *goproto.BattleBuff { values := map[string]float32{"SkillIndex": 0} for key, value := range dynamic { values[key] = value } return &goproto.BattleBuff{Id: id, Level: 1, OwnerId: owner, WaveFlag: ^uint32(0), TargetIndexList: []uint32{0}, DynamicValues: values} } func (r *battleRules) techniqueBuffs(a *common.AvatarJson, slot, leader uint32, imported []*goproto.BattleBuff) []*goproto.BattleBuff { if len(a.Techniques) == 0 { return nil } buffs := imported // Explicit character rules in PearlSR replace the generic technique. The // user's empty techniques array still disables techniques for that avatar. if rules, ok := r.TechniqueBuffs[a.AvatarID]; ok { buffs = nil for _, rule := range rules { owner := slot if rule.Owner == "leader" { owner = leader } buffs = append(buffs, battleBuff(rule.ID, owner, rule.DynamicValues)) } } if containsU32(r.IgnoreToughness.Avatars, a.AvatarID) { found := false for _, buff := range buffs { if buff.Id == r.IgnoreToughness.BuffID { found = true } } if !found { buffs = append(buffs, battleBuff(r.IgnoreToughness.BuffID, slot, nil)) } } // Global passives are handled once, with their configured owner and setting. result := make([]*goproto.BattleBuff, 0, len(buffs)) for _, buff := range buffs { global := false for _, rule := range r.GlobalPassives { if buff.Id == rule.BuffID { global = true break } } if !global { result = append(result, buff) } } return result } func (r *battleRules) addGlobalPassives(battle *goproto.SceneBattleInfo, player *common.FreesrData) { for _, rule := range r.GlobalPassives { var enabled *bool switch rule.Setting { case "enable_sw_global": enabled = player.EnableSWGlobal case "enable_castorice_global": enabled = player.EnableCastoriceGlobal case "enable_aha_global": enabled = player.EnableAhaGlobal } if enabled != nil && !*enabled { continue } owner := rule.OwnerSlot // A shorter lineup must not refer to an absent actor. present := false for _, avatar := range battle.BattleAvatarList { if avatar.Index == owner { present = true } } if !present { owner = battle.BattleAvatarList[0].Index } battle.BuffList = append(battle.BuffList, battleBuff(rule.BuffID, owner, nil)) } } func (r *battleRules) addTargets(battle *goproto.SceneBattleInfo, kind common.BattleType) { for _, rule := range r.BattleTargets { if !strings.EqualFold(rule.BattleType, string(kind)) { continue } for key, targets := range rule.Targets { list := &goproto.BattleTargetList{} for _, target := range targets { list.BattleTargetList = append(list.BattleTargetList, &goproto.BattleTarget{ Id: target.ID, Progress: target.Progress, TotalProgress: target.TotalProgress}) } battle.BattleTargetInfo[key] = list } } }