From f473c5488068c4e1e105862ba1d227811d819f83 Mon Sep 17 00:00:00 2001 From: xinian Date: Sun, 5 Apr 2026 00:03:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=A4=9A=E7=AB=99?= =?UTF-8?q?=E4=BD=8D=E6=88=98=E6=96=97=E6=8E=A7=E5=88=B6=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/fight-input-controller-binding.md | 194 +++++++++++++++++++++++ logic/service/fight/input.go | 61 ++++++-- logic/service/fight/new.go | 209 ++++++++++++++++++++++--- logic/service/fight/new_options.go | 79 +++++++--- 4 files changed, 491 insertions(+), 52 deletions(-) create mode 100644 docs/fight-input-controller-binding.md diff --git a/docs/fight-input-controller-binding.md b/docs/fight-input-controller-binding.md new file mode 100644 index 000000000..6472adfa1 --- /dev/null +++ b/docs/fight-input-controller-binding.md @@ -0,0 +1,194 @@ +# Fight Input 控制绑定说明 + +日期:2026-04-04 + +## 1. 背景 + +当前战斗模型中,一个 `Input` 对应一个战斗站位(`actorIndex`)。 +每个 `Input` 通过 `Input.Player` 绑定操作者。 + +当前建战主路径已收敛为:`WithFightInputs(ourInputs, oppInputs)`。 +即:先由调用方创建并组装双方 `Input`,再传给战斗模块。 + +为了同时支持以下两种玩法,新增了可配置绑定策略: + +1. 双打:一个玩家控制多个站位(单人多 `Input`) +2. 组队:一个玩家控制一个站位(每人一个 `Input`) + +## 2. 绑定策略 + +文件:`logic/service/fight/new_options.go` + +- `InputControllerBindingKeep` + - 含义:保持输入中已有 `Input.Player` 绑定,不覆盖 + - 适用:调用方已手动构造 `Input` 绑定 + +- `InputControllerBindingSingle` + - 含义:单侧全部站位统一绑定为 `players[0]` + - 适用:双打中一个人控制多个站位 + +- `InputControllerBindingPerSlot` + - 含义:按站位顺序绑定为 `players[i]` + - 适用:组队中一人一个站位 + - 说明:当 `players` 数量不足时,回退绑定 `players[0]` + +## 3. 选项接口 + +文件:`logic/service/fight/new_options.go` + +新增选项: + +```go +WithInputControllerBinding(mode int) +``` + +## 4. 生效时机 + +文件:`logic/service/fight/new.go` + +在 `buildFight` 中,构建完 `Our/Opp` 输入后,先执行控制绑定,再执行上下文绑定: + +1. `bindInputControllers(f.Our, f.OurPlayers, opts.controllerBinding)` +2. `bindInputControllers(f.Opp, f.OppPlayers, opts.controllerBinding)` +3. `bindInputFightContext(...)` +4. `linkTeamViews()` +5. `linkOppInputs()` + +## 5. 使用示例 + +### 5.1 双打(单人控多站位) + +```go +fight.NewFightWithOptions( + fight.WithFightPlayersOnSide( + []common.PlayerI{ourPlayer}, + []common.PlayerI{oppPlayer}, + ), + fight.WithFightInputs(ourInputs, oppInputs), + fight.WithInputControllerBinding(fight.InputControllerBindingSingle), +) +``` + +### 5.2 组队(一人一个站位) + +```go +fight.NewFightWithOptions( + fight.WithFightPlayersOnSide( + []common.PlayerI{ourP1, ourP2}, + []common.PlayerI{oppP1, oppP2}, + ), + fight.WithFightInputs(ourInputs, oppInputs), + fight.WithInputControllerBinding(fight.InputControllerBindingPerSlot), +) +``` + +### 5.3 仅传已绑定 Input(推荐灵活接入) + +```go +ourInputs := []*input.Input{ + input.NewInput(nil, ourP1), // 站位0 + input.NewInput(nil, ourP2), // 站位1 +} +oppInputs := []*input.Input{ + input.NewInput(nil, oppP1), // 站位0 + input.NewInput(nil, oppP2), // 站位1 +} + +fc, err := fight.NewFightWithOptions( + fight.WithFightInputs(ourInputs, oppInputs), + // 不传 WithFightPlayersOnSide 也可 + // owner/opponent 与 side players 会从 inputs 自动提取 +) +_ = fc +_ = err +``` + +说明:`InputControllerBindingSingle/PerSlot` 会覆盖 `ourInputs/oppInputs` 中原有的 `Input.Player` 绑定;`Keep` 不覆盖。 + +## 6. 新模式绑定实例(逐模式) + +以下示例假设我方有两个站位:`ourInputs[0]`、`ourInputs[1]`。 + +### 6.1 Keep(保持输入原绑定) + +调用: + +```go +fight.NewFightWithOptions( + fight.WithFightInputs(ourInputs, oppInputs), + fight.WithInputControllerBinding(fight.InputControllerBindingKeep), +) +``` + +输入(调用前): + +- `ourInputs[0].Player = ourP1` +- `ourInputs[1].Player = ourP2` + +结果(调用后): + +- `ourInputs[0].Player = ourP1` +- `ourInputs[1].Player = ourP2` + +适用:调用方已提前把每个站位绑定好,不希望框架覆盖。 + +### 6.2 Single(单人控制全部站位) + +调用: + +```go +fight.NewFightWithOptions( + fight.WithFightPlayersOnSide( + []common.PlayerI{ourCaptain}, + []common.PlayerI{oppCaptain}, + ), + fight.WithFightInputs(ourInputs, oppInputs), + fight.WithInputControllerBinding(fight.InputControllerBindingSingle), +) +``` + +输入(调用前): + +- `ourInputs[0].Player = ourP1` +- `ourInputs[1].Player = ourP2` + +结果(调用后): + +- `ourInputs[0].Player = ourCaptain` +- `ourInputs[1].Player = ourCaptain` + +适用:双打或多站位由同一玩家操作。 + +### 6.3 PerSlot(按站位顺序绑定玩家) + +调用: + +```go +fight.NewFightWithOptions( + fight.WithFightPlayersOnSide( + []common.PlayerI{ourP1, ourP2}, + []common.PlayerI{oppP1, oppP2}, + ), + fight.WithFightInputs(ourInputs, oppInputs), + fight.WithInputControllerBinding(fight.InputControllerBindingPerSlot), +) +``` + +输入(调用前): + +- `ourInputs[0].Player = anyA` +- `ourInputs[1].Player = anyB` + +结果(调用后): + +- `ourInputs[0].Player = ourP1` +- `ourInputs[1].Player = ourP2` + +补位规则:若 `players` 数量不足(例如只传一个 `ourP1`),剩余站位回退绑定 `players[0]`。 + +## 7. 注意事项 + +1. 默认模式是 `InputControllerBindingKeep`,不影响现有调用。 +2. 若传入 `WithFightInputs(...)` 且每个 `Input.Player` 已预先绑定,可继续用默认模式。 +3. 仅传 `WithFightInputs(...)` 也可工作:框架会从 `ourInputs/oppInputs` 自动提取 `ourPlayers/oppPlayers`,并以各侧首位玩家作为 owner/opponent。 +4. 推荐在新组队逻辑中显式传 `WithInputControllerBinding(...)`,避免调用方歧义。 diff --git a/logic/service/fight/input.go b/logic/service/fight/input.go index 82bfa866b..3293b6bab 100644 --- a/logic/service/fight/input.go +++ b/logic/service/fight/input.go @@ -135,33 +135,42 @@ func (f *FightC) isOurPlayerID(userID uint32) bool { return userID == f.ownerID } -func (f *FightC) bindInputFightContext(inputs []*input.Input) { - for _, fighter := range inputs { - if fighter == nil { - continue - } - fighter.FightC = f - if fighter.Player != nil { - fighter.Player.SetFightC(f) +// bindInputFightContext 为输入站位绑定战斗上下文与玩家战斗容器。 +// 支持一次传入多组输入(如 Our/Opp)。 +func (f *FightC) bindInputFightContext(inputGroups ...[]*input.Input) { + for _, inputs := range inputGroups { + for _, fighter := range inputs { + if fighter == nil { + continue + } + fighter.FightC = f + if fighter.Player != nil { + fighter.Player.SetFightC(f) + } } } } +// linkOppInputs 仅建立“默认对手回退”关系,不参与真实目标选择。 +// 真实目标应由 action.targetIndex 决定;这里仅为旧 effect/无动作上下文链路提供默认 Opp。 func (f *FightC) linkOppInputs() { - for actorIndex, fighter := range f.Our { + defaultOpp := f.selectInput(f.Opp, 0) + for _, fighter := range f.Our { if fighter == nil { continue } - fighter.SetOPP(f.selectInput(f.Opp, actorIndex)) + fighter.SetOPP(defaultOpp) } - for actorIndex, fighter := range f.Opp { + defaultOur := f.selectInput(f.Our, 0) + for _, fighter := range f.Opp { if fighter == nil { continue } - fighter.SetOPP(f.selectInput(f.Our, actorIndex)) + fighter.SetOPP(defaultOur) } } +// linkTeamViews 建立每个输入的同阵营/对阵营视图(Team/OppTeam)。 func (f *FightC) linkTeamViews() { for _, fighter := range f.Our { if fighter == nil { @@ -179,6 +188,7 @@ func (f *FightC) linkTeamViews() { } } +// getSideInputs 按 userID 判定所属阵营后返回目标侧输入集合。 func (f *FightC) getSideInputs(userID uint32, isOpposite bool) []*input.Input { isOur := f.isOurPlayerID(userID) if isOpposite { @@ -193,6 +203,7 @@ func (f *FightC) getSideInputs(userID uint32, isOpposite bool) []*input.Input { return f.Opp } +// findInputByUserID 在双方站位中按控制者查找任一输入,并返回是否属于我方。 func (f *FightC) findInputByUserID(userID uint32) (*input.Input, bool) { for _, in := range f.Our { if in != nil && in.ControlledBy(userID) { @@ -207,6 +218,8 @@ func (f *FightC) findInputByUserID(userID uint32) (*input.Input, bool) { return nil, false } +// getInputByUserID 按 userID + 站位下标获取输入。 +// 当查询本侧站位时,要求该站位必须由 userID 控制。 func (f *FightC) getInputByUserID(userID uint32, index int, isOpposite bool) *input.Input { selected := f.selectInput(f.getSideInputs(userID, isOpposite), index) if selected == nil { @@ -219,6 +232,7 @@ func (f *FightC) getInputByUserID(userID uint32, index int, isOpposite bool) *in return selected } +// getInputByController 按控制者获取其首个可操作站位(常用于兼容单站位接口)。 func (f *FightC) getInputByController(userID uint32, isOpposite bool) *input.Input { sideInputs := f.getSideInputs(userID, isOpposite) for _, in := range sideInputs { @@ -266,6 +280,29 @@ func (f *FightC) GetInputByPlayer(c common.PlayerI, isOpposite bool) *input.Inpu return f.getInputByController(c.GetInfo().UserID, isOpposite) } +// GetInputsByPlayer 返回玩家在指定侧的全部可控站位。 +func (f *FightC) GetInputsByPlayer(c common.PlayerI, isOpposite bool) []*input.Input { + if c == nil { + return nil + } + sideInputs := f.getSideInputs(c.GetInfo().UserID, isOpposite) + result := make([]*input.Input, 0, len(sideInputs)) + for _, in := range sideInputs { + if in != nil && in.ControlledBy(c.GetInfo().UserID) { + result = append(result, in) + } + } + return result +} + +// GetInputByPlayerAt 按玩家+站位下标获取输入。 +func (f *FightC) GetInputByPlayerAt(c common.PlayerI, actorIndex int, isOpposite bool) *input.Input { + if c == nil { + return nil + } + return f.getInputByUserID(c.GetInfo().UserID, actorIndex, isOpposite) +} + func (f *FightC) GetInputByAction(c action.BattleActionI, isOpposite bool) *input.Input { if c == nil { if isOpposite { diff --git a/logic/service/fight/new.go b/logic/service/fight/new.go index b8d0f8d01..2a21c9b45 100644 --- a/logic/service/fight/new.go +++ b/logic/service/fight/new.go @@ -12,20 +12,107 @@ import ( "time" ) -// 创建新战斗,邀请方和被邀请方,或者玩家和野怪方 -func NewFight(p1, p2 common.PlayerI, b1, b2 []model.PetInfo, fn func(model.FightOverInfo)) (*FightC, errorcode.ErrorCode) { +// NewFightSingleControllerN 创建 N 打战斗(单人控制多站位)。 +// ourPetsBySlot/oppPetsBySlot 的每个元素代表一个站位携带的宠物列表。 +func NewFightSingleControllerN( + ourController common.PlayerI, + oppController common.PlayerI, + ourPetsBySlot [][]model.PetInfo, + oppPetsBySlot [][]model.PetInfo, + fn func(model.FightOverInfo), +) (*FightC, errorcode.ErrorCode) { + if ourController == nil || oppController == nil { + return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater + } + fightInfo := ourController.Getfightinfo() + + ourInputs, err := buildSideInputsByController(ourController, ourPetsBySlot, fightInfo.Mode) + if err > 0 { + return nil, err + } + oppInputs, err := buildSideInputsByController(oppController, oppPetsBySlot, fightInfo.Mode) + if err > 0 { + return nil, err + } + return NewFightWithOptions( - WithFightPlayers(p1, p2), - WithFightPets(b1, b2), + WithFightInputs(ourInputs, oppInputs), + WithFightPlayersOnSide( + []common.PlayerI{ourController}, + []common.PlayerI{oppController}, + ), + WithInputControllerBinding(InputControllerBindingSingle), WithFightCallback(fn), + WithFightInfo(fightInfo), ) } +// NewFightPerSlotControllerN 创建 N 打战斗(多人各控制一个站位)。 +// ourPlayers/oppPlayers 与 ourPetsBySlot/oppPetsBySlot 按站位一一对应。 +func NewFightPerSlotControllerN( + ourPlayers []common.PlayerI, + oppPlayers []common.PlayerI, + ourPetsBySlot [][]model.PetInfo, + oppPetsBySlot [][]model.PetInfo, + fn func(model.FightOverInfo), +) (*FightC, errorcode.ErrorCode) { + if len(ourPlayers) == 0 || len(oppPlayers) == 0 { + return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater + } + if len(ourPlayers) != len(ourPetsBySlot) || len(oppPlayers) != len(oppPetsBySlot) { + return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater + } + + fightInfo := ourPlayers[0].Getfightinfo() + ourInputs, err := buildSideInputsByPlayers(ourPlayers, ourPetsBySlot, fightInfo.Mode) + if err > 0 { + return nil, err + } + oppInputs, err := buildSideInputsByPlayers(oppPlayers, oppPetsBySlot, fightInfo.Mode) + if err > 0 { + return nil, err + } + + return NewFightWithOptions( + WithFightInputs(ourInputs, oppInputs), + WithFightPlayersOnSide(ourPlayers, oppPlayers), + WithInputControllerBinding(InputControllerBindingPerSlot), + WithFightCallback(fn), + WithFightInfo(fightInfo), + ) +} + +// 创建新战斗,邀请方和被邀请方,或者玩家和野怪方 +func NewFight(p1, p2 common.PlayerI, b1, b2 []model.PetInfo, fn func(model.FightOverInfo)) (*FightC, errorcode.ErrorCode) { + if p1 == nil || p2 == nil { + return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater + } + fightInfo := p1.Getfightinfo() + ourInput, err := buildInputFromPets(p1, b1, fightInfo.Mode) + if err > 0 { + return nil, err + } + oppInput, err := buildInputFromPets(p2, b2, fightInfo.Mode) + if err > 0 { + return nil, err + } + return NewFightWithOptions( + WithFightInputs([]*input.Input{ourInput}, []*input.Input{oppInput}), + WithFightCallback(fn), + WithFightInfo(fightInfo), + ) +} + +// buildFight 基于已准备好的双方 Inputs 构建战斗实例。 +// 约束:opts.ourInputs/opts.oppInputs 必须非空。 func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) { - if opts == nil || opts.owner == nil || opts.opponent == nil { + if opts == nil { return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater } opts.normalizePlayers() + if opts.owner == nil || opts.opponent == nil { + return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater + } f := &FightC{} f.ownerID = opts.owner.GetInfo().UserID @@ -45,17 +132,14 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) { } f.ReadyInfo.Status = f.Info.Status - var err errorcode.ErrorCode - f.Our, err = f.buildFightInputs(opts.owner, opts.ourPets, opts.ourInputs) - if err > 0 { - return nil, err + if len(opts.ourInputs) == 0 || len(opts.oppInputs) == 0 { + return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater } - f.Opp, err = f.buildFightInputs(opts.opponent, opts.oppPets, opts.oppInputs) - if err > 0 { - return nil, err - } - f.bindInputFightContext(f.Our) - f.bindInputFightContext(f.Opp) + f.Our = opts.ourInputs + f.Opp = opts.oppInputs + f.bindInputControllers(f.Our, f.OurPlayers, opts.controllerBinding) + f.bindInputControllers(f.Opp, f.OppPlayers, opts.controllerBinding) + f.bindInputFightContext(f.Our, f.Opp) f.linkTeamViews() f.linkOppInputs() @@ -106,13 +190,94 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) { return f, 0 } -func (f *FightC) buildFightInputs(defaultPlayer common.PlayerI, pets []model.PetInfo, existing []*input.Input) ([]*input.Input, errorcode.ErrorCode) { - if len(existing) > 0 { - return existing, 0 +// bindInputControllers 按配置模式重绑站位控制者(Input.Player)。 +// Keep: 不改;Single: 全部绑定 players[0];PerSlot: 按下标绑定 players[i]。 +func (f *FightC) bindInputControllers(inputs []*input.Input, players []common.PlayerI, mode int) { + if len(inputs) == 0 || len(players) == 0 { + return } - in, err := f.initplayer(defaultPlayer, pets) - if err > 0 { - return nil, err + switch mode { + case InputControllerBindingSingle: + controller := players[0] + for _, in := range inputs { + if in == nil { + continue + } + in.Player = controller + } + case InputControllerBindingPerSlot: + for idx, in := range inputs { + if in == nil { + continue + } + if idx < len(players) && players[idx] != nil { + in.Player = players[idx] + continue + } + in.Player = players[0] + } + default: + // keep existing input player binding } - return []*input.Input{in}, 0 +} + +// buildInputFromPets 根据玩家与宠物列表构建一个站位 Input。 +func buildInputFromPets(c common.PlayerI, pets []model.PetInfo, mode uint32) (*input.Input, errorcode.ErrorCode) { + if c == nil { + return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater + } + if r := c.CanFight(); r != 0 { + return nil, r + } + + in := input.NewInput(nil, c) + in.AllPet = make([]*info.BattlePetEntity, 0, len(pets)) + in.InitAttackValue() + for _, pet := range pets { + entity := info.CreateBattlePetEntity(pet) + entity.BindController(c.GetInfo().UserID) + in.AllPet = append(in.AllPet, entity) + } + + in.SortPet() + if len(in.AllPet) == 0 { + return nil, errorcode.ErrorCodes.ErrNoEligiblePokemon + } + if mode == info.BattleMode.SINGLE_MODE { + in.AllPet = in.AllPet[:1] + } + in.SetCurPetAt(0, in.AllPet[0]) + return in, 0 +} + +// buildSideInputsByController 用同一控制者构建多个站位输入(单人多站位)。 +func buildSideInputsByController(controller common.PlayerI, petsBySlot [][]model.PetInfo, mode uint32) ([]*input.Input, errorcode.ErrorCode) { + if controller == nil || len(petsBySlot) == 0 { + return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater + } + inputs := make([]*input.Input, 0, len(petsBySlot)) + for _, slotPets := range petsBySlot { + in, err := buildInputFromPets(controller, slotPets, mode) + if err > 0 { + return nil, err + } + inputs = append(inputs, in) + } + return inputs, 0 +} + +// buildSideInputsByPlayers 按站位玩家一一对应构建输入(多人分站位)。 +func buildSideInputsByPlayers(players []common.PlayerI, petsBySlot [][]model.PetInfo, mode uint32) ([]*input.Input, errorcode.ErrorCode) { + if len(players) == 0 || len(players) != len(petsBySlot) { + return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater + } + inputs := make([]*input.Input, 0, len(players)) + for idx := range players { + in, err := buildInputFromPets(players[idx], petsBySlot[idx], mode) + if err > 0 { + return nil, err + } + inputs = append(inputs, in) + } + return inputs, 0 } diff --git a/logic/service/fight/new_options.go b/logic/service/fight/new_options.go index acbce9fd1..34afd37fa 100644 --- a/logic/service/fight/new_options.go +++ b/logic/service/fight/new_options.go @@ -11,6 +11,15 @@ import ( type FightOption func(*fightBuildOptions) +const ( + // InputControllerBindingKeep 保持输入中现有的 Player 绑定,不做覆盖。 + InputControllerBindingKeep = iota + // InputControllerBindingSingle 单侧全部站位由 players[0] 控制(双打单人多站位)。 + InputControllerBindingSingle + // InputControllerBindingPerSlot 单侧按站位顺序绑定 players[i](组队每人一个站位)。 + InputControllerBindingPerSlot +) + type fightBuildOptions struct { owner common.PlayerI opponent common.PlayerI @@ -18,37 +27,25 @@ type fightBuildOptions struct { ourPlayers []common.PlayerI oppPlayers []common.PlayerI - ourPets []model.PetInfo - oppPets []model.PetInfo - ourInputs []*input.Input oppInputs []*input.Input callback func(model.FightOverInfo) startTime time.Time fightInfo *info.Fightinfo + + controllerBinding int } +// defaultFightBuildOptions 返回建战默认参数。 func defaultFightBuildOptions() *fightBuildOptions { return &fightBuildOptions{ - startTime: time.Now(), - } -} - -func WithFightPlayers(owner, opponent common.PlayerI) FightOption { - return func(opts *fightBuildOptions) { - opts.owner = owner - opts.opponent = opponent - } -} - -func WithFightPets(ourPets, oppPets []model.PetInfo) FightOption { - return func(opts *fightBuildOptions) { - opts.ourPets = ourPets - opts.oppPets = oppPets + startTime: time.Now(), + controllerBinding: InputControllerBindingKeep, } } +// WithFightInputs 注入双方站位输入(当前建战主路径)。 func WithFightInputs(ourInputs, oppInputs []*input.Input) FightOption { return func(opts *fightBuildOptions) { opts.ourInputs = ourInputs @@ -56,6 +53,7 @@ func WithFightInputs(ourInputs, oppInputs []*input.Input) FightOption { } } +// WithFightPlayersOnSide 显式指定双方操作者列表(可选)。 func WithFightPlayersOnSide(ourPlayers, oppPlayers []common.PlayerI) FightOption { return func(opts *fightBuildOptions) { opts.ourPlayers = ourPlayers @@ -63,24 +61,34 @@ func WithFightPlayersOnSide(ourPlayers, oppPlayers []common.PlayerI) FightOption } } +// WithFightCallback 设置战斗结束回调。 func WithFightCallback(fn func(model.FightOverInfo)) FightOption { return func(opts *fightBuildOptions) { opts.callback = fn } } +// WithFightInfo 覆盖战斗信息(模式/状态等)。 func WithFightInfo(fightInfo info.Fightinfo) FightOption { return func(opts *fightBuildOptions) { opts.fightInfo = &fightInfo } } +// WithFightStartTime 指定战斗创建时间(测试/回放可用)。 func WithFightStartTime(startTime time.Time) FightOption { return func(opts *fightBuildOptions) { opts.startTime = startTime } } +// WithInputControllerBinding 设置站位控制绑定策略。 +func WithInputControllerBinding(mode int) FightOption { + return func(opts *fightBuildOptions) { + opts.controllerBinding = mode + } +} + func NewFightWithOptions(opts ...FightOption) (*FightC, errorcode.ErrorCode) { buildOpts := defaultFightBuildOptions() for _, opt := range opts { @@ -91,11 +99,46 @@ func NewFightWithOptions(opts ...FightOption) (*FightC, errorcode.ErrorCode) { return buildFight(buildOpts) } +// uniquePlayersFromInputs 从输入中提取去重后的操作者列表。 +func uniquePlayersFromInputs(inputs []*input.Input) []common.PlayerI { + if len(inputs) == 0 { + return nil + } + seen := make(map[uint32]struct{}, len(inputs)) + players := make([]common.PlayerI, 0, len(inputs)) + for _, in := range inputs { + if in == nil || in.Player == nil { + continue + } + userID := in.Player.GetInfo().UserID + if _, ok := seen[userID]; ok { + continue + } + seen[userID] = struct{}{} + players = append(players, in.Player) + } + return players +} + +// normalizePlayers 归一化 owner/opponent 与双方 players。 +// 若未显式传 players,会尝试从 inputs 中自动提取。 func (o *fightBuildOptions) normalizePlayers() { + if len(o.ourPlayers) == 0 && len(o.ourInputs) > 0 { + o.ourPlayers = uniquePlayersFromInputs(o.ourInputs) + } + if len(o.oppPlayers) == 0 && len(o.oppInputs) > 0 { + o.oppPlayers = uniquePlayersFromInputs(o.oppInputs) + } if len(o.ourPlayers) == 0 && o.owner != nil { o.ourPlayers = []common.PlayerI{o.owner} } if len(o.oppPlayers) == 0 && o.opponent != nil { o.oppPlayers = []common.PlayerI{o.opponent} } + if o.owner == nil && len(o.ourPlayers) > 0 { + o.owner = o.ourPlayers[0] + } + if o.opponent == nil && len(o.oppPlayers) > 0 { + o.opponent = o.oppPlayers[0] + } }