From 24b463f0aafda5fe4ca98cdecd0b8296279f98fa Mon Sep 17 00:00:00 2001 From: xinian Date: Sun, 5 Apr 2026 22:27:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=20Boss=20=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=20HookAction=20=E6=8E=A5=E5=85=A5=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入 BossHookActionContext 封装战斗上下文,并支持脚本调用 useSkill 和 switchPet 函数控制战斗行为。 --- ...boss-script-hookaction-guide-2026-04-05.md | 173 ++++++++++++++++++ logic/service/fight/input/ai.go | 116 +++++++++--- modules/config/model/boss_pet.go | 87 ++++++++- modules/config/model/boss_pet_test.go | 63 ++++++- 4 files changed, 396 insertions(+), 43 deletions(-) create mode 100644 docs/boss-script-hookaction-guide-2026-04-05.md diff --git a/docs/boss-script-hookaction-guide-2026-04-05.md b/docs/boss-script-hookaction-guide-2026-04-05.md new file mode 100644 index 000000000..8cf20c9a3 --- /dev/null +++ b/docs/boss-script-hookaction-guide-2026-04-05.md @@ -0,0 +1,173 @@ +# Boss Script(HookAction)接入说明 + +日期:2026-04-05 + +## 1. 执行流程 + +1. 先执行战斗效果链 `HookAction()` +2. 执行脚本 `hookAction(hookaction)` +3. 用脚本返回值决定是否继续出手 +4. 脚本可直接调用 Go 绑定函数:`useSkill()`、`switchPet()` + +## 2. JS 可调用的 Go 函数 + +1. `useSkill(skillId: number)` + - 作用:指定本回合使用技能 + - 示例:`useSkill(5001)` + +2. `switchPet(catchTime: number)` + - 作用:指定本回合切换精灵 + - 示例:`switchPet(2)` + +## 3. `hookaction` 参数字段 + +1. `hookaction.hookaction: boolean` + - effect 链原始判定 +2. `hookaction.round: number` + - 当前回合数 +3. `hookaction.is_first: boolean` + - 是否先手 +4. `hookaction.our: { pet_id, catch_time, hp, max_hp } | null` + - 我方当前精灵 +5. `hookaction.opp: { pet_id, catch_time, hp, max_hp } | null` + - 对方当前精灵 +6. `hookaction.skills: Array<{ skill_id, pp, can_use }>` + - 我方当前技能快照 + +返回值: + +- `true`:继续行动 +- `false`:阻止行动 +- 不返回:默认回退到 `hookaction.hookaction` + +## 4. 脚本示例 + +### 4.1 第3回合固定放技能 + +```js +function hookAction(hookaction) { + if (!hookaction.hookaction) return false; + + if (hookaction.round === 3) { + useSkill(5001); + } + + return true; +} +``` + +### 4.2 低血切宠 + +```js +function hookAction(hookaction) { + if (!hookaction.hookaction) return false; + if (!hookaction.our) return true; + + var hpRate = hookaction.our.max_hp > 0 ? hookaction.our.hp / hookaction.our.max_hp : 1; + if (hpRate < 0.3) { + switchPet(2); + } + + return true; +} +``` + +### 4.3 读取技能可用性后出招 + +```js +function hookAction(hookaction) { + if (!hookaction.hookaction) return false; + + for (var i = 0; i < hookaction.skills.length; i++) { + var s = hookaction.skills[i]; + if (s.can_use && s.skill_id === 5001) { + useSkill(s.skill_id); + break; + } + } + + return true; +} +``` + +## 5. Monaco 类型提示 + +```ts +import * as monaco from "monaco-editor"; + +monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + allowNonTsExtensions: true, + checkJs: true, + target: monaco.languages.typescript.ScriptTarget.ES2020, +}); + +monaco.languages.typescript.javascriptDefaults.addExtraLib( + ` +interface BossHookPetContext { + pet_id: number; + catch_time: number; + hp: number; + max_hp: number; +} + +interface BossHookSkillContext { + skill_id: number; + pp: number; + can_use: boolean; +} + +interface BossHookActionContext { + hookaction: boolean; + round: number; + is_first: boolean; + our: BossHookPetContext | null; + opp: BossHookPetContext | null; + skills: BossHookSkillContext[]; +} + +declare function hookAction(hookaction: BossHookActionContext): boolean; +declare function HookAction(hookaction: BossHookActionContext): boolean; +declare function hookaction(hookaction: BossHookActionContext): boolean; + +declare function useSkill(skillId: number): void; +declare function switchPet(catchTime: number): void; +`, + "ts:boss-script.d.ts" +); +``` + +## 6. Monaco 补全(可选) + +```ts +monaco.languages.registerCompletionItemProvider("javascript", { + provideCompletionItems() { + return { + suggestions: [ + { + label: "boss hookAction", + kind: monaco.languages.CompletionItemKind.Snippet, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertText: [ + "function hookAction(hookaction) {", + " if (!hookaction.hookaction) return false;", + " if (hookaction.round >= 3) useSkill(5001);", + " return true;", + "}", + ].join("\\n"), + documentation: "Boss脚本模板:可读取回合/血量并调用 useSkill/switchPet。", + }, + ], + }; + }, +}); +``` + +## 7. 后端代码 + +- 脚本执行器与函数绑定:`modules/config/model/boss_pet.go` +- AI 出手转发:`logic/service/fight/input/ai.go` +- BOSS 脚本注入: + - `logic/controller/fight_boss野怪和地图怪.go` + - `logic/controller/fight_塔.go` + diff --git a/logic/service/fight/input/ai.go b/logic/service/fight/input/ai.go index 9f7b0e5c0..1c6035743 100644 --- a/logic/service/fight/input/ai.go +++ b/logic/service/fight/input/ai.go @@ -4,30 +4,28 @@ import ( "blazing/logic/service/fight/info" "blazing/logic/service/player" configmodel "blazing/modules/config/model" + "strings" "github.com/gogf/gf/v2/util/grand" ) // Shuffle 打乱切片顺序,使用 Fisher-Yates 洗牌算法,泛型支持任意类型 func Shuffle[T any](slice []T) { - // 从后往前遍历,逐个交换 for i := len(slice) - 1; i > 0; i-- { - // 生成 0 到 i 之间的随机索引 j := grand.Intn(i + 1) - // 交换 i 和 j 位置的元素 slice[i], slice[j] = slice[j], slice[i] } } + func (our *Input) GetAction() { - next := our.Exec(func(t Effect) bool { - return t.HookAction() }) + scriptCtx := buildBossHookActionContext(our, next) if aiPlayer, ok := our.Player.(*player.AI_player); ok && aiPlayer.BossScript != "" { scriptBoss := &configmodel.BossConfig{Script: aiPlayer.BossScript} - nextByScript, err := scriptBoss.RunHookActionScript(next) + nextByScript, err := scriptBoss.RunHookActionScript(scriptCtx) if err != nil { return } @@ -37,28 +35,30 @@ func (our *Input) GetAction() { if !next { return } - // 获取己方当前宠物和对方当前宠物 + + if applyBossScriptAction(our, scriptCtx) { + return + } + selfPet := our.FightC.GetCurrPET(our.Player) - //没血就切换精灵 + if selfPet == nil { + return + } if selfPet.Info.Hp <= 0 { for _, v := range our.AllPet { if v.Info.Hp > 0 { - // println("AI出手,切换宠物") our.FightC.ChangePet(our.Player, v.Info.CatchTime) return } } - // 如果没有可用宠物,则直接返回,不执行任何操作 return } - //oppPet := opp.FightC.GetCurrPET(opp.Player) - skills := selfPet.Skills - // 空技能列表直接返回,避免错误 + skills := selfPet.Skills if len(skills) == 0 { return } - //println("AI出手,选择技能") + var usedskill *info.SkillEntity for _, s := range skills { if s == nil { @@ -67,30 +67,25 @@ func (our *Input) GetAction() { if !s.CanUse() { continue } - // 计算技能对对方的伤害(假设CalculatePower返回伤害值,或需从技能中获取) s.DamageValue = our.CalculatePower(our.Opp, s) oppPet := our.Opp.CurrentPet() if oppPet == nil { continue } - // 判断是否能秒杀(伤害 >= 对方当前生命值) - if s.DamageValue.Cmp(oppPet.GetHP()) != -1 { // 假设oppPet.HP为对方当前剩余生命值 - + if s.DamageValue.Cmp(oppPet.GetHP()) != -1 { if usedskill != nil { if s.DamageValue.Cmp(usedskill.DamageValue) != -1 { usedskill = s } - } else { usedskill = s } - } } + Shuffle(skills) if usedskill == nil { - for _, s := range skills { if s == nil { continue @@ -99,14 +94,87 @@ func (our *Input) GetAction() { continue } usedskill = s - } - } + if usedskill != nil { our.FightC.UseSkill(our.Player, uint32(usedskill.XML.ID)) } else { our.FightC.UseSkill(our.Player, 0) } - +} + +func buildBossHookActionContext(our *Input, hookAction bool) *configmodel.BossHookActionContext { + ctx := &configmodel.BossHookActionContext{ + HookAction: hookAction, + Action: "auto", + } + ctx.UseSkillFn = func(skillID uint32) { + ctx.Action = "skill" + ctx.SkillID = skillID + } + ctx.SwitchPetFn = func(catchTime uint32) { + ctx.Action = "switch" + ctx.CatchTime = catchTime + } + + if our == nil || our.FightC == nil || our.Player == nil { + return ctx + } + + overInfo := our.FightC.GetOverInfo() + ctx.Round = overInfo.Round + ctx.IsFirst = our.FightC.IsFirst(our.Player) + + if selfPet := our.CurrentPet(); selfPet != nil { + ctx.Our = &configmodel.BossHookPetContext{ + PetID: selfPet.Info.ID, + CatchTime: selfPet.Info.CatchTime, + Hp: selfPet.Info.Hp, + MaxHp: selfPet.Info.MaxHp, + } + ctx.Skills = make([]configmodel.BossHookSkillContext, 0, len(selfPet.Skills)) + for _, s := range selfPet.Skills { + if s == nil || s.Info == nil { + continue + } + ctx.Skills = append(ctx.Skills, configmodel.BossHookSkillContext{ + SkillID: s.Info.ID, + PP: s.Info.PP, + CanUse: s.CanUse(), + }) + } + } + + if our.Opp != nil { + if oppPet := our.Opp.CurrentPet(); oppPet != nil { + ctx.Opp = &configmodel.BossHookPetContext{ + PetID: oppPet.Info.ID, + CatchTime: oppPet.Info.CatchTime, + Hp: oppPet.Info.Hp, + MaxHp: oppPet.Info.MaxHp, + } + } + } + + return ctx +} + +func applyBossScriptAction(our *Input, ctx *configmodel.BossHookActionContext) bool { + if our == nil || ctx == nil { + return false + } + + switch strings.ToLower(strings.TrimSpace(ctx.Action)) { + case "", "auto": + return false + case "skill", "use_skill", "useskill": + our.FightC.UseSkill(our.Player, ctx.SkillID) + return true + case "switch", "change_pet", "changepet": + our.FightC.ChangePet(our.Player, ctx.CatchTime) + return true + default: + return false + } } diff --git a/modules/config/model/boss_pet.go b/modules/config/model/boss_pet.go index ee38c4339..37e303684 100644 --- a/modules/config/model/boss_pet.go +++ b/modules/config/model/boss_pet.go @@ -28,6 +28,37 @@ type BossConfig struct { Rule []uint32 `gorm:"type:jsonb; ;comment:'战胜规则'" json:"rule"` } +// BossHookSkillContext 为脚本暴露当前精灵技能可用信息。 +type BossHookSkillContext struct { + SkillID uint32 `json:"skill_id"` + PP uint32 `json:"pp"` + CanUse bool `json:"can_use"` +} + +// BossHookPetContext 为脚本暴露战斗中双方精灵简要信息。 +type BossHookPetContext struct { + PetID uint32 `json:"pet_id"` + CatchTime uint32 `json:"catch_time"` + Hp uint32 `json:"hp"` + MaxHp uint32 `json:"max_hp"` +} + +// BossHookActionContext 为 boss 脚本提供可读写的出手上下文。 +type BossHookActionContext struct { + HookAction bool `json:"hookaction"` // effect 链原始 HookAction 判定 + Round uint32 `json:"round"` // 当前回合数 + IsFirst bool `json:"is_first"` // 是否先手 + Our *BossHookPetContext `json:"our"` // 我方当前精灵 + Opp *BossHookPetContext `json:"opp"` // 对方当前精灵 + Skills []BossHookSkillContext `json:"skills"` // 我方技能 + Action string `json:"action"` // auto/skill/switch + SkillID uint32 `json:"skill_id"` // action=skill + CatchTime uint32 `json:"catch_time"` // action=switch + + UseSkillFn func(skillID uint32) `json:"-"` + SwitchPetFn func(catchTime uint32) `json:"-"` +} + // TableName 指定BossConfig对应的数据库表名 func (*BossConfig) TableName() string { return TableNameBossConfig @@ -39,11 +70,8 @@ func (*BossConfig) GroupName() string { } // NewBossConfig 创建一个新的BossConfig实例(初始化通用Model字段+所有默认值) - func NewBossConfig() *BossConfig { - return &BossConfig{ - Model: cool.NewModel(), - } + return &BossConfig{Model: cool.NewModel()} } // init 程序启动时自动创建/同步boss_config表结构 @@ -51,8 +79,7 @@ func init() { cool.CreateTable(&BossConfig{}) } -// RunHookActionScript 执行BOSS脚本中的 hookAction,并传入 fight 的 hookaction 参数。 -// 返回值遵循 HookAction 语义:true 允许继续出手,false 阻止继续出手。 +// RunHookActionScript 执行 BOSS 脚本 hookAction。 func (b *BossConfig) RunHookActionScript(hookAction any) (bool, error) { if b == nil || strings.TrimSpace(b.Script) == "" { return true, nil @@ -64,6 +91,9 @@ func (b *BossConfig) RunHookActionScript(hookAction any) (bool, error) { } vm := goja.New() + vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) + bindBossScriptFunctions(vm, hookAction) + if _, err = vm.RunProgram(program); err != nil { return false, fmt.Errorf("run boss script: %w", err) } @@ -87,10 +117,49 @@ func (b *BossConfig) RunHookActionScript(hookAction any) (bool, error) { return false, fmt.Errorf("execute boss hookAction: %w", err) } - // 与既有HookAction默认行为保持一致:未显式返回时视为允许继续出手。 if goja.IsUndefined(result) || goja.IsNull(result) { - return true, nil + return defaultHookActionResult(hookAction), nil } - return result.ToBoolean(), nil } + +func bindBossScriptFunctions(vm *goja.Runtime, hookAction any) { + ctx, ok := hookAction.(*BossHookActionContext) + if !ok || ctx == nil { + return + } + + _ = vm.Set("useSkill", func(call goja.FunctionCall) goja.Value { + if ctx.UseSkillFn == nil || len(call.Arguments) == 0 { + return goja.Undefined() + } + skillID := call.Arguments[0].ToInteger() + if skillID < 0 { + return goja.Undefined() + } + ctx.UseSkillFn(uint32(skillID)) + return goja.Undefined() + }) + + _ = vm.Set("switchPet", func(call goja.FunctionCall) goja.Value { + if ctx.SwitchPetFn == nil || len(call.Arguments) == 0 { + return goja.Undefined() + } + catchTime := call.Arguments[0].ToInteger() + if catchTime < 0 { + return goja.Undefined() + } + ctx.SwitchPetFn(uint32(catchTime)) + return goja.Undefined() + }) +} + +func defaultHookActionResult(hookAction any) bool { + if ctx, ok := hookAction.(*BossHookActionContext); ok { + return ctx.HookAction + } + if val, ok := hookAction.(bool); ok { + return val + } + return true +} diff --git a/modules/config/model/boss_pet_test.go b/modules/config/model/boss_pet_test.go index 1ed5d122e..f33fb5cca 100644 --- a/modules/config/model/boss_pet_test.go +++ b/modules/config/model/boss_pet_test.go @@ -2,21 +2,17 @@ package model import "testing" -type testHookAction struct { - Allow bool - Round int -} - func TestBossConfigRunHookActionScript(t *testing.T) { boss := &BossConfig{ Script: ` function hookAction(hookaction) { - return hookaction.Allow && hookaction.Round >= 2; + return hookaction.hookaction === true; } `, } - ok, err := boss.RunHookActionScript(testHookAction{Allow: true, Round: 2}) + ctx := &BossHookActionContext{HookAction: true} + ok, err := boss.RunHookActionScript(ctx) if err != nil { t.Fatalf("RunHookActionScript returned error: %v", err) } @@ -25,20 +21,67 @@ func TestBossConfigRunHookActionScript(t *testing.T) { } } -func TestBossConfigRunHookActionScriptEmptyReturnDefaultsTrue(t *testing.T) { +func TestBossConfigRunHookActionScriptCallUseSkillFn(t *testing.T) { boss := &BossConfig{ Script: ` function hookAction(hookaction) { - var _ = hookaction; + if (hookaction.round >= 2) { + useSkill(5001); + } + return true; } `, } - ok, err := boss.RunHookActionScript(testHookAction{Allow: false, Round: 1}) + ctx := &BossHookActionContext{ + HookAction: true, + Round: 2, + Action: "auto", + } + ctx.UseSkillFn = func(skillID uint32) { + ctx.Action = "skill" + ctx.SkillID = skillID + } + + ok, err := boss.RunHookActionScript(ctx) if err != nil { t.Fatalf("RunHookActionScript returned error: %v", err) } if !ok { t.Fatalf("RunHookActionScript = false, want true") } + if ctx.Action != "skill" || ctx.SkillID != 5001 { + t.Fatalf("useSkill not applied, got action=%q skill_id=%d", ctx.Action, ctx.SkillID) + } +} + +func TestBossConfigRunHookActionScriptCallSwitchPetFn(t *testing.T) { + boss := &BossConfig{ + Script: ` + function hookAction(hookaction) { + switchPet(3); + return true; + } + `, + } + + ctx := &BossHookActionContext{ + HookAction: true, + Action: "auto", + } + ctx.SwitchPetFn = func(catchTime uint32) { + ctx.Action = "switch" + ctx.CatchTime = catchTime + } + + ok, err := boss.RunHookActionScript(ctx) + if err != nil { + t.Fatalf("RunHookActionScript returned error: %v", err) + } + if !ok { + t.Fatalf("RunHookActionScript = false, want true") + } + if ctx.Action != "switch" || ctx.CatchTime != 3 { + t.Fatalf("switchPet not applied, got action=%q catch_time=%d", ctx.Action, ctx.CatchTime) + } }