feat: 支持多站位战斗控制绑定模式
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
This commit is contained in:
194
docs/fight-input-controller-binding.md
Normal file
194
docs/fight-input-controller-binding.md
Normal file
@@ -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(...)`,避免调用方歧义。
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user