I'm doing that AI framework you know. I'm considering for a next version to make it a framework module as there is a lot of synergy to be leveraged (god, I have a career in marketing
).
What I currently do is to create a new function defineAiSwitchMonster which is to be used in place of defineObject (with a couple of caveats) for those monsters one wants to manage through the AI switcher. For this to work I need to pass a ton of data defined in the init.lua imports to the scripting entity but the imports are executed *after* my module declares its scripting entity. So I'm generating an entity which is basically a big table on-the-fly.
Code: Select all
--[[
AI Switcher for Grimrock - Version 1.3
Code by Xanathar (Marco Mastropaolo), 2013
Check README.txt for installation instructions :)
Thanks to Leki for the idea, to JKos for opening the way to this kind of tricks with his framework.
]]
-- All defined switchsets
local g_AiSwitcher_AiSwitchSets = { };
-- This will contain all generated hooks
local g_AiSwitcher_HooksCode = "";
-- This will contain all the names of the party projectile hooks
local g_AiSwitcher_PartyProjectileHooks = { };
-- This will contain all warnings
local g_AiSwitcher_Warnings = "";
-- Source code of the aiswitcher scripting entity
local g_AiSwitcher_LuaSource = [[
-- Table of all monsters to be checked for AI switching. This is needed because we can't destroy a monster in its hook,
-- so the hook simply queues the monster here, and then an onDrawGui handler does the processing as soon as possible
g_MonstersToCheck = { }
-- This table associates all monster names to their switch-set
g_SwitchSetsByName = { }
-- This table associates all monster names to their default AI - used to relay hooks to the framework
g_MasterMonsterByName = { }
-- This will be set to true once the above tables are initialized correctly
g_InitDone = false
-- Initializes the tables
function initTables()
for _1, switchset in ipairs(aiswitchdata.ai) do
for _2, m in ipairs(switchset) do
g_SwitchSetsByName[ m[1] ] = switchset
g_MasterMonsterByName[ m[1] ] = switchset[#switchset][1]
end
end
end
-- Changes a monster to a new name. m=the monster to change. newname=the name of the new monster.
-- newidx=the index of the monster in the switchset (0 for default). switch=the switchset
function changeMonster(m, newname, newidx, switch)
if (m.name == newname) then return; end
local oldidx = -1
for nidx, n in ipairs(switch) do
if (n[1] == m.name) then
oldidx = nidx
break
end
end
local x = m.x
local y = m.y
local f = m.facing
local l = m.level
local id = m.id
local hp = m:getHealth()
local lvl = m:getLevel()
if (tonumber(id) ~= nil) then
id = nil
end
local hookres = processhook(m, "onAiSwitch", oldidx, newidx)
if (hookres ~= nil) and (not hookres) then return; end
m:destroy()
spawn(newname, l, x, y, f, id)
:setLevel(lvl)
:setHealth(hp)
end
-- Entry point of the onDrawGui hook
function onTick()
if (#g_MonstersToCheck > 0) then
for _, id in ipairs(g_MonstersToCheck) do
local m = findEntity(id)
if (m ~= nil) then
realCheck(m)
end
end
end
g_MonstersToCheck = { }
end
-- Checks the specified monster for any AI change
function realCheck(m)
if (m.level ~= party.level) then return; end
if (not g_InitDone) then
initTables()
g_InitDone = true
end
local switch = g_SwitchSetsByName[m.name];
if (switch == nil) then return; end
local distance = math.abs(m.x - party.x) + math.abs(m.y - party.y)
local directdistance = (m.x == party.x) or (m.y == party.y)
for idx, n in ipairs(switch) do
if (n[2] == "distance") then
if (distance >= n[3]) and (distance <= n[4]) then
changeMonster(m, n[1], idx, switch)
break;
end
elseif (n[2] == "directdistance") and directdistance then
if (distance >= n[3]) and (distance <= n[4]) then
changeMonster(m, n[1], idx, switch)
break;
end
elseif (n[2] == "health") then
local hp = m:getHealth()
if (hp >= n[3]) and (hp <= n[4]) then
changeMonster(m, n[1], idx, switch)
break;
end
elseif (n[2] == "function") then
local chg = process_cond(m, n[1], "condition")
if (chg) then
changeMonster(m, n[1], idx, switch)
break;
end
elseif (n[2] == "default") then
changeMonster(m, n[1], 0, switch)
break;
else
changeMonster(m, n[1], idx, switch)
break;
end
end
end
-- processes an AI switching hook (onMove, onTurn, onAttack, onRangedAttack)
function process(m, param, method)
table.insert(g_MonstersToCheck, m.id)
return processhook(m, method, param)
end
-- processes hooks for a given monster
function processhook(m, method, param1, param2, param3, param4)
local fn = "hook_" .. m.name .. "_" .. method;
if (aiswitchdata ~= nil) and (aiswitchdata[fn] ~= nil) then
local res = aiswitchdata[fn](m, param1, param2, param3, param4)
if (res ~= nil) then
return res;
end
end
if ((fw ~= nil) and (fw.executeHooks ~= nil)) then
local mastername = g_MasterMonsterByName[m.name]
if (mastername ~= nil) then
return fw.executeHooks(mastername, method, param1, param2, param3, param4)
end
end
end
-- processes a condition function for AI switch
function process_cond(m, mname, method, param1, param2, param3, param4)
local fn = "hook_" .. mname .. "_" .. method;
if (aiswitchdata ~= nil) and (aiswitchdata[fn] ~= nil) then
return aiswitchdata[fn](m, param1, param2, param3, param4)
end
end
function onPartyProjectileHit(champ, proj, damage, damtype)
return aiswitchdata.onPartyProjectileHit(champ, proj, damage, damtype)
end
]];
-- Create the code for a given hook
function _aiswitcher_createHook(monster, hookname)
if (monster[hookname] ~= nil) then
local fnhooknm = "hook_" .. monster.name .. "_" .. hookname;
local code = monster[hookname];
if (type(code) == "string") then
code = "\n\n" .. fnhooknm .. " = " .. code .. "\n\n";
g_AiSwitcher_HooksCode = g_AiSwitcher_HooksCode .. code;
return fnhooknm;
else
g_AiSwitcher_Warnings = g_AiSwitcher_Warnings .. "AISWITCHER WARNING: Hook " .. monster.name .. "." .. hookname .. " is not defined as a string!\n";
return nil;
end
end
return nil;
end
-- Defines a given monster brain as a monster object in LoG
function _aiswitcher_defineMonsterObject(monster, conditions, idx)
if (monster.condition == nil) then
conditions[#conditions + 1] = { monster.name, "default" }
elseif (type(monster.condition) == "table") then
monster.name = monster.name .. "_ai_" .. idx;
conditions[#conditions + 1] = { monster.name, monster.condition[1], monster.condition[2], monster.condition[3], monster.condition[4] }
else
monster.name = monster.name .. "_ai_" .. idx;
_aiswitcher_createHook(monster, "condition")
conditions[#conditions + 1] = { monster.name, "function" }
end
monster.condition = nil
monster.brains = nil
_aiswitcher_createHook(monster, "onMove")
_aiswitcher_createHook(monster, "onTurn")
_aiswitcher_createHook(monster, "onAttack")
_aiswitcher_createHook(monster, "onRangedAttack")
_aiswitcher_createHook(monster, "onDealDamage")
_aiswitcher_createHook(monster, "onDamage")
_aiswitcher_createHook(monster, "onProjectileHit")
_aiswitcher_createHook(monster, "onDie")
_aiswitcher_createHook(monster, "onAiSwitch")
local partyhook = _aiswitcher_createHook(monster, "onPartyProjectileHit")
if (partyhook ~= nil) then
g_AiSwitcher_PartyProjectileHooks[#g_AiSwitcher_PartyProjectileHooks + 1] = partyhook
end
monster.onMove = function(self, _1) if ((aiswitcher ~= nil) and (aiswitcher.process ~= nil)) then return aiswitcher.process(self, _1, "onMove"); end; end;
monster.onTurn = function(self, _1) if ((aiswitcher ~= nil) and (aiswitcher.process ~= nil)) then return aiswitcher.process(self, _1, "onTurn"); end; end;
monster.onAttack = function(self, _1) if ((aiswitcher ~= nil) and (aiswitcher.process ~= nil)) then return aiswitcher.process(self, _1, "onAttack"); end; end;
monster.onRangedAttack = function(self, _1) if ((aiswitcher ~= nil) and (aiswitcher.process ~= nil)) then return aiswitcher.process(self, _1, "onRangedAttack"); end; end;
monster.onDamage = function(self, _1, _2) if ((aiswitcher ~= nil) and (aiswitcher.process ~= nil)) then return aiswitcher.processhook(self, "onDamage", _1, _2); end; end;
monster.onDealDamage = function(self, _1, _2) if ((aiswitcher ~= nil) and (aiswitcher.process ~= nil)) then return aiswitcher.processhook(self, "onDealDamage", _1, _2); end; end;
monster.onProjectileHit = function(self, _1, _2, _3) if ((aiswitcher ~= nil) and (aiswitcher.process ~= nil)) then return aiswitcher.processhook(self, "onProjectileHit", _1, _2, _3); end; end;
monster.onDie = function(self, _1, _2) if ((aiswitcher ~= nil) and (aiswitcher.process ~= nil)) then return aiswitcher.processhook(self, "onDie"); end; end;
monster.onAiSwitch = nil;
monster.onPartyProjectileHit = nil;
defineObject(monster);
end
-- Takes a table and creates a flat clone of it
function _aiswitcher_cloneTable(src)
local dst = { }
for k, v in pairs(src) do
dst[k] = v
end
return dst
end
-- Takes two table, creates a flat clone of the first and merges fields from the second in it
function _aiswitcher_mergeTable(t1, t2)
local dst = _aiswitcher_cloneTable(t1)
for k, v in pairs(t2) do
dst[k] = v
end
return dst
end
-- Defines a monster with AI switching facility. User callable.
function defineAiSwitchMonster(monster)
local conditions = { }
for idx, b in ipairs(monster.brains) do
local m = _aiswitcher_mergeTable(monster, b)
m.animations = _aiswitcher_mergeTable(monster.animations, m.animations)
_aiswitcher_defineMonsterObject(m, conditions, idx)
end
_aiswitcher_cloneTable(monster)
_aiswitcher_defineMonsterObject(monster, conditions)
g_AiSwitcher_AiSwitchSets[#g_AiSwitcher_AiSwitchSets + 1] = conditions
end
-- Add onDrawGui hooks to the party. Forward to grimwidgets if present. Forward to LoG-fw (even if this is custom behaviour).
cloneObject {
name = "party",
baseObject = "party",
onDrawGui = function(g)
if ((aiswitcher ~= nil) and (aiswitcher.onTick ~= nil)) then
aiswitcher.onTick()
end
if ((fw ~= nil) and (fw.executeHooks ~= nil)) then
fw.executeHooks("party","onDrawGui", g)
end
if ((gw ~= nil) and (gw._drawGUI ~= nil)) then
gw._drawGUI(g)
end
end,
onProjectileHit = function(champ, proj, damage, damtype)
if ((aiswitcher ~= nil) and (aiswitcher.onTick ~= nil)) then
return aiswitcher.onPartyProjectileHit(champ, proj, damage, damtype)
end
if ((fw ~= nil) and (fw.executeHooks ~= nil)) then
fw.executeHooks("party","onProjectileHit", champ, proj, damage, damtype)
end
end,
}
-- Creates the aiswitcher_engine object, to initialize the scripting entities
cloneObject {
name = "aiswitcher_engine",
baseObject = "dungeon_door_metal",
openSound = "spider_walk",
closeSound = "spider_walk",
lockSound = "spider_walk",
onOpen = function()
-- creates a scripting entity called aiswitchdata containing the serialized switchsets and the hooks of defined monsters
local ai = "ai = {";
for _1, switchSet in ipairs(g_AiSwitcher_AiSwitchSets) do
ai = ai .. "{";
for _2, switch in ipairs(switchSet) do
ai = ai .. "{";
for _3, entry in ipairs(switch) do
if (type(entry) == "number") then
ai = ai .. entry .. ", ";
elseif (type(entry) == "string") then
ai = ai .. "\"" .. entry .. "\"" .. ", ";
end
end
ai = ai .. "}, "
end
ai = ai .. "}, "
end
ai = ai .. "}\n\n" .. g_AiSwitcher_HooksCode
-- creates the relayer for onPartyProjectileHit hooks
local hookrelay = "function onPartyProjectileHit(c, p, d, t)\n";
for _1, projhook in ipairs(g_AiSwitcher_PartyProjectileHooks) do
hookrelay = hookrelay .. projhook .. "(c, p, d, t);\n";
end
hookrelay = hookrelay .. "end\n\n";
ai = ai .. hookrelay;
print(g_AiSwitcher_Warnings);
spawn("script_entity", 1,1,1,1,"aiswitchdata"):setSource(ai)
-- creates a scripting entity called aiswitcher with the main code (iif it does not exist)
if (aiswitcher == nil) then
spawn("script_entity", 1,1,1,1,"aiswitcher"):setSource(g_AiSwitcher_LuaSource)
end
end,
}
As you can see I defined aiswitcher_engine which is basically the same as the LoG framework object but specific. If I could rely on the framework for initialization I would probably rewrite hooks so that they would go through fw themselves and avoid some pain
Plus all of this is so heavy on hooks that it screams for being an fw module.
(as for why the hooks are to be defined in the monster declaration - I wanted to make it so that who creates monsters and AI for the community could make a deployment with a single import (once the framework is installed) instead of requiring that plus entities).