package nbattle import ( "fmt" "io" "log" "strings" "github.com/chompy/nbattle_go/event" luago "github.com/rosbit/luago" ) type luaEffect struct { luaCtx *luago.LuaContext } // NewEffect creates a new EffectDef from a Lua script. func (c *Context) NewLuaEffect(script io.Reader) (*EffectDef, error) { scriptBytes, err := io.ReadAll(script) if err != nil { return nil, err } luaCtx, err := loadLuaScript(c, scriptBytes) if err != nil { return nil, err } nameIf, err := luaCtx.CallFunc("Name") if err != nil { return nil, err } name, ok := nameIf.(string) if !ok { return nil, ErrUnexpectedLuaFuncReturn } return c.NewEffectDef(name, func() Effect { luaCtx, err := loadLuaScript(c, scriptBytes) if err != nil { return nil } return &luaEffect{ luaCtx: luaCtx, } }), nil } func luaGlobals(ctx *Context) map[string]any { globals := make(map[string]any) globals["ctx"] = contextToLua(ctx) for _, statDef := range ctx.GetStatDefs() { globals["STAT_"+strings.ToUpper(statDef.GetName())] = statDefToLua(statDef) } for name, value := range ctx.GetFlags() { globals["FLAG_"+strings.ToUpper(name)] = value } return globals } func contextToLua(ctx *Context) map[string]any { return map[string]any{ "getTick": ctx.GetTick, "getCombatants": func() []map[string]any { out := make([]map[string]any, 0) for _, combatant := range ctx.GetCombatants() { out = append(out, combatantToLua(ctx, combatant)) } return out }, "getObject": func(obj any) map[string]any { return objectToLua(ctx, obj) }, } } func errorToLua(err error) map[string]any { log.Println("WARNING: Error during Lua call:", err) return map[string]any{ "type": ObjectTypeError, "error": err.Error(), } } func objectIDFromLua(object any) (int, error) { switch object := object.(type) { case map[string]any: id, ok := object["id"].(int) if !ok { return 0, ErrUnexpectedObjectType } return id, nil case float64: return int(object), nil case float32: return int(object), nil case int: return object, nil } return 0, ErrUnexpectedObjectType } func objectFromLua(ctx *Context, object any) (Object, error) { id, err := objectIDFromLua(object) if err != nil { return nil, err } obj, err := ctx.GetObjectByID(id) if err != nil { return nil, err } return obj, nil } func objectToLua(ctx *Context, object any) map[string]any { switch object := object.(type) { case *StatDef: return statDefToLua(object) case *EffectDef: return map[string]any{ "id": object.GetID(), "type": object.GetType(), "name": object.GetName(), } case *TriggerDef: return map[string]any{ "id": object.GetID(), "type": object.GetType(), "name": object.GetName(), } case *Stat: return statToLua(ctx, object) case *Combatant: return combatantToLua(ctx, object) default: return errorToLua(ErrUnexpectedObjectType) } } func combatantToLua(ctx *Context, combatant *Combatant) map[string]any { if combatant == nil { return map[string]any{ "type": ObjectTypeUnknown, "error": ErrNilObject, } } return map[string]any{ "id": combatant.GetID(), "type": combatant.GetType(), "getStat": func(statDef any) map[string]any { stat, err := combatant.GetStat(statDef) if err != nil { logLuaFuncCallError(err, fmt.Sprintf("combatant.%d.getStat", combatant.GetID())) return errorToLua(err) } return statToLua(ctx, stat) }, "setEffect": func(effectDef any, potency int, sourceObj any) { if err := combatant.SetEffect(effectDef, potency, sourceObj); err != nil { logLuaFuncCallError(err, fmt.Sprintf("combatant.%d.setEffect", combatant.GetID())) } }, "hasEffect": combatant.HasEffect, "removeEffect": func(effectDef any) { if err := combatant.SetEffect(effectDef, 0, nil); err != nil { logLuaFuncCallError(err, fmt.Sprintf("combatant.%d.removeEffect", combatant.GetID())) } }, "setFlag": combatant.SetFlag, "hasFlag": combatant.HasFlag, } } func loadLuaScript(ctx *Context, scriptBytes []byte) (*luago.LuaContext, error) { luaCtx, err := luago.NewContext() if err != nil { return nil, err } if err := luaCtx.LoadScript(string(scriptBytes), luaGlobals(ctx)); err != nil { return nil, err } return luaCtx, nil } // luaCall wraps a Lua CallFunc invocation, tracking depth to enable // deferred effect removal when removeEffect is called during Lua execution. func luaCall(ctx *Context, luaCtx *luago.LuaContext, name string, args ...any) (any, error) { ctx.luaCallDepth++ defer func() { ctx.luaCallDepth-- if ctx.luaCallDepth == 0 { ctx.processDeferredRemovals() } }() return luaCtx.CallFunc(name, args...) } func (e *luaEffect) OnAdd(ctx *EffectCtx) { if _, err := luaCall(ctx.Ctx, e.luaCtx, "OnAdd", effectCtxToLua(ctx)); err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+".OnAdd") } } func (e *luaEffect) OnRemove(ctx *EffectCtx) { if _, err := luaCall(ctx.Ctx, e.luaCtx, "OnRemove", effectCtxToLua(ctx)); err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+".OnRemove") } } func (e *luaEffect) OnEvent(ctx *EffectCtx, evt event.Event) { switch evt := evt.(type) { case *event.Tick: if _, err := luaCall(ctx.Ctx, e.luaCtx, "OnTick", effectCtxToLua(ctx), map[string]any{"tick": evt.Tick}); err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+".OnTick") } case *event.CombatantUpdate: combatant, err := ctx.Ctx.GetCombatantByID(evt.CombatantID) if err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+".OnCombatantUpdate") break } if _, err := luaCall(ctx.Ctx, e.luaCtx, "OnCombatantUpdate", effectCtxToLua(ctx), map[string]any{ "combatant": combatantToLua(ctx.Ctx, combatant), "active": evt.Active, "flags": evt.Flags, }); err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+".OnCombatantUpdate") } case *event.CombatantStatBase: funcName := "OnCombatantStatBase" combatant, err := ctx.Ctx.GetCombatantByID(evt.CombatantID) if err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) break } statDef, err := ctx.Ctx.GetStatDefByID(evt.StatDefID) if err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) break } if _, err := luaCall(ctx.Ctx, e.luaCtx, funcName, effectCtxToLua(ctx), map[string]any{ "combatant": combatantToLua(ctx.Ctx, combatant), "statDef": statDefToLua(statDef), "value": evt.Value, "setValue": func(value int) { evt.Value = value }, }); err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) } case *event.CombatantStatMod: funcName := "OnCombatantStatMod" combatant, err := ctx.Ctx.GetCombatantByID(evt.CombatantID) if err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) break } statDef, err := ctx.Ctx.GetStatDefByID(evt.StatDefID) if err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) break } if _, err := luaCall(ctx.Ctx, e.luaCtx, funcName, effectCtxToLua(ctx), map[string]any{ "combatant": combatantToLua(ctx.Ctx, combatant), "statDef": statDefToLua(statDef), "value": evt.ModValue, "setValue": func(value int) { evt.ModValue = value }, }); err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) } case *event.CombatantEffect: funcName := "OnCombatantEffect" target, err := ctx.Ctx.GetCombatantByID(evt.TargetID) if err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) break } effectDef, err := ctx.Ctx.GetEffectDefByID(evt.EffectDefID) if err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) break } source, _ := ctx.Ctx.GetObject(evt.SourceID) if _, err := luaCall(ctx.Ctx, e.luaCtx, funcName, effectCtxToLua(ctx), map[string]any{ "target": combatantToLua(ctx.Ctx, target), "effect": effectDef.GetName(), "potency": evt.Potency, "source": objectToLua(ctx.Ctx, source), }); err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) } case *event.Trigger: funcName := "OnTrigger" triggerDefObj, err := ctx.Ctx.GetObject(evt.TriggerDefID) if err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) break } triggerDef, ok := triggerDefObj.(*TriggerDef) if !ok { logLuaFuncCallError(ErrUnexpectedObjectType, ctx.Def.GetName()+"."+funcName) break } effectDef, err := ctx.Ctx.GetEffectDefByID(evt.EffectDefID) if err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) break } effectTarget, err := ctx.Ctx.GetCombatantByID(evt.EffectTargetID) if err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) break } effectSource, _ := ctx.Ctx.GetObject(evt.EffectSourceID) if _, err := luaCall(ctx.Ctx, e.luaCtx, funcName, effectCtxToLua(ctx), map[string]any{ "trigger": triggerDef.GetName(), "target": combatantToLua(ctx.Ctx, effectTarget), "effect": effectDef.GetName(), "potency": evt.EffectPotency, "source": objectToLua(ctx.Ctx, effectSource), }); err != nil { logLuaFuncCallError(err, ctx.Def.GetName()+"."+funcName) } default: log.Printf("WARNING: Effect %s received unknown event type: %T", ctx.Def.GetName(), evt.Type()) } } func effectCtxToLua(effectCtx *EffectCtx) map[string]any { return map[string]any{ "target": combatantToLua(effectCtx.Ctx, effectCtx.Target), "source": objectToLua(effectCtx.Ctx, effectCtx.Source), "effect": effectCtx.Def.GetName(), "potency": effectCtx.Potency, "emitTrigger": effectCtx.EmitTrigger, } } func statDefToLua(statDef *StatDef) map[string]any { if statDef == nil { return errorToLua(ErrNilObject) } return map[string]any{ "type": statDef.GetType(), "name": statDef.GetName(), "min": statDef.GetMin(), "max": statDef.GetMax(), } } func statToLua(ctx *Context, stat *Stat) map[string]any { return map[string]any{ "def": statDefToLua(stat.GetDef()), "getBase": stat.GetBase, "setBase": func(value float64) { stat.SetBase(int(value)) }, "addBase": func(value float64) { stat.AddBase(int(value)) }, "subBase": func(value float64) { stat.SubBase(int(value)) }, "getValue": stat.GetValue, "setMod": func(source any, value int) { sourceObj, err := objectFromLua(ctx, source) if err != nil { logLuaFuncCallError(err, "stat.setMod") return } stat.SetMod(sourceObj, value) }, "getCombatant": func() map[string]any { combatant, err := ctx.GetCombatantWithStat(stat) if err != nil { return errorToLua(err) } return combatantToLua(ctx, combatant) }, } }