feat: 增强 Boss 脚本 HookAction 接入能力
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
引入 BossHookActionContext 封装战斗上下文,并支持脚本调用 useSkill 和 switchPet 函数控制战斗行为。
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user