How to create your own "smart spider" or other "smart monsters" without frameworks or aditional scripting.
Do you wanna smart spider for your dungeon? Spider who can bite you in melee attack or shoot spider web from a distance? It's easy. yust follow these steps:
1) Create your own spider monster clones in your mod_assets/monsters.lua and name it "smart_spider_melee" and "smart_spider_ranged".
You should have smth like:
Code: Select all
cloneObject{
name = "smart_spider_melee",
baseObject = "spider",
}
cloneObject{
name = "smart_spider_ranged",
baseObject = "spider",
}
Code: Select all
cloneObject{
name = "smart_spider_ranged",
baseObject = "spider",
brain = "Uggardian",
}
You also define strafeLeft and Right animations and use an "idle" animation for that. Your should have smth like this:
Code: Select all
cloneObject{
name = "smart_spider_ranged",
baseObject = "spider",
animations = {
strafeLeft = "assets/animations/monsters/spider/spider_idle.fbx",
strafeRight = "assets/animations/monsters/spider/spider_idle.fbx",
},
brain = "Uggardian",
onMove = function (monster, dir)
-- locals definitions:
local dx, dy, lev, x, y, fac = 0, 0, monster.level, monster.x, monster.y, monster.facing
-- missing animation handler will cancel move if it's strafe
if fac == 0 and dir = 1 or fac == 0 and dir = 3 then return false
if fac == 1 and dir = 0 or fac == 1 and dir = 2 then return false
if fac == 2 and dir = 1 or fac == 2 and dir = 3 then return false
if fac == 3 and dir = 0 or fac == 0 and dir = 2 then return false
end,
}
4) Now it's time do create monster switcher hack.
There is no way how to change monster brain, but you can replace monster one by one. In this case between "smart_spider_melee" and "smart_spider_ranged".
To prevent console warnings etc, we need smth I called "garbager_target" - a simple item based e.g. on the rock or any other item you like. Lets use rock for that and oyu can write it in your mod_assets/items.lua:
Code: Select all
cloneObject{
name = "garbager_target",
baseObject = "rock",
}
5) Let's add new lines in your monsters definitions. It's rangedAttack definition and "onRangedAttack" Hook with one local and returning false:
Code: Select all
cloneObject{
name = "smart_spider_melee",
baseObject = "spider",
rangedAttack = "poison_bolt",
onRangedAttack = function (monster)
local garbager_id = "dm_coffin_skeleton_sword_shield_garbager_"..string.sub(monster.id, string.len(monster.id), -1)
return false
end,
}
cloneObject{
name = "smart_spider_ranged",
baseObject = "spider",
animations = {
strafeLeft = "assets/animations/monsters/spider/spider_idle.fbx",
strafeRight = "assets/animations/monsters/spider/spider_idle.fbx",
},
brain = "Uggardian",
rangedAttack = "poison_bolt",
onMove = function (monster, dir)
-- locals definitions:
local dx, dy, lev, x, y, fac = 0, 0, monster.level, monster.x, monster.y, monster.facing
-- missing animation handler will cancel move if it's strafe
if fac == 0 and dir = 1 or fac == 0 and dir = 3 then return false
if fac == 1 and dir = 0 or fac == 1 and dir = 2 then return false
if fac == 2 and dir = 1 or fac == 2 and dir = 3 then return false
if fac == 3 and dir = 0 or fac == 0 and dir = 2 then return false
end,
onRangedAttack = function (monster)
local garbager_id = "smart_spider_garbager_"..string.sub(monster.id, string.len(monster.id), -1)
return false
end,
}
6) We gonna use "smart_spider_melee" as main monster - monster who is placed into dungeon.
This main monster will create his personal "garbager_target". There is one guaranted location in dungeon, where it can be spawned and it's a starting location. So lets find it and spawn garbager there with an unique name based on his owner.
Code: Select all
cloneObject{
name = "smart_spider_melee",
baseObject = "spider",
rangedAttack = "poison_bolt",
onRangedAttack = function (monster)
-- garbarer handler
local garbager_id = "smart_spider_garbager_"..string.sub(monster.id, string.len(monster.id), -1)
if findEntity(garbager_id) == nil then
for level = 1, getMaxLevels() do
for e in allEntities(level) do
if (e.class == "StartingLocation") then
local target = e.id
spawn("dm_garbager_target", findEntity(target).level, findEntity(target).x, findEntity(target).y, monster.facing, garbager_id)
return false
end
end
end
end
return false
end,
}
Then add this animation in definitions and also create animation event as well. i.e. onRangedAttack function in monster definition will be called via event. You should have smth like this:
Code: Select all
cloneObject{
name = "smart_spider_melee",
baseObject = "spider",
animations = {
idle = "mod_assets/animations/spider_idle.fbx",
},
rangedAttack = "poison_bolt",
onRangedAttack = function (monster)
-- garbarer handler
local garbager_id = "smart_spider_garbager_"..string.sub(monster.id, string.len(monster.id), -1)
if findEntity(garbager_id) == nil then
for level = 1, getMaxLevels() do
for e in allEntities(level) do
if (e.class == "StartingLocation") then
local target = e.id
spawn("dm_garbager_target", findEntity(target).level, findEntity(target).x, findEntity(target).y, monster.facing, garbager_id)
return false
end
end
end
end
return false
end,
}
cloneObject{
name = "smart_spider_ranged",
baseObject = "spider",
animations = {
idle = "mod_assets/animations/spider_idle.fbx",
strafeLeft = "assets/animations/monsters/spider/spider_idle.fbx",
strafeRight = "assets/animations/monsters/spider/spider_idle.fbx",
},
brain = "Uggardian",
rangedAttack = "poison_bolt",
onMove = function (monster, dir)
-- locals definitions:
local dx, dy, lev, x, y, fac = 0, 0, monster.level, monster.x, monster.y, monster.facing
-- missing animation handler will cancel move if it's strafe
if fac == 0 and dir = 1 or fac == 0 and dir = 3 then return false
if fac == 1 and dir = 0 or fac == 1 and dir = 2 then return false
if fac == 2 and dir = 1 or fac == 2 and dir = 3 then return false
if fac == 3 and dir = 0 or fac == 0 and dir = 2 then return false
end,
onRangedAttack = function (monster)
local garbager_id = "smart_spider_garbager_"..string.sub(monster.id, string.len(monster.id), -1)
return false
end,
}
defineAnimationEvent{
animation = "mod_assets/animations/spider_idle.fbx",
event = "ranged_attack",
frame = 1,
}
Lets add more lines into onRangedAttack hook. You can add there any conditions you wand, like enviromnent reading is and switch brain to ranged if party is 2+ cell far etc etc.
For now, we gonna add only 50% chance to switch brain. Add locals and brain switcher lines. As you can see there is a monster:setPosition function. To prevent console warnings and stuff like that,
monster is moved to his targed and new monster is spawned on his original position.
Code: Select all
cloneObject{
name = "smart_spider_melee",
baseObject = "spider",
animations = {
idle = "mod_assets/animations/spider_idle.fbx",
},
rangedAttack = "poison_bolt",
onRangedAttack = function (monster)
-- locals
local dx, dy, lev, x, y, fac = 0, 0, monster.level, monster.x, monster.y, monster.facing
-- garbarer handler
local garbager_id = "smart_spider_garbager_"..string.sub(monster.id, string.len(monster.id), -1)
if findEntity(garbager_id) == nil then
for level = 1, getMaxLevels() do
for e in allEntities(level) do
if (e.class == "StartingLocation") then
local target = e.id
spawn("dm_garbager_target", findEntity(target).level, findEntity(target).x, findEntity(target).y, monster.facing, garbager_id)
return false
end
end
end
end
-- brain switcher
if math.random() <= 0.5 then
monster:setPosition(findEntity(garbager_id).x, findEntity(garbager_id).y, monster.facing, findEntity(garbager_id).level)
local spider_id = "smart_spider_ranged_"..string.sub(monster.id, string.len(monster.id), -1)
spawn("smart_spider_ranged", lev, x, y, fac, spider_id)
end
return false
end,
}
cloneObject{
name = "smart_spider_ranged",
baseObject = "spider",
animations = {
idle = "mod_assets/animations/spider_idle.fbx",
strafeLeft = "assets/animations/monsters/spider/spider_idle.fbx",
strafeRight = "assets/animations/monsters/spider/spider_idle.fbx",
},
brain = "Uggardian",
rangedAttack = "poison_bolt",
onMove = function (monster, dir)
-- locals
local dx, dy, lev, x, y, fac = 0, 0, monster.level, monster.x, monster.y, monster.facing
-- missing animation handler will cancel move if it's strafe
if fac == 0 and dir = 1 or fac == 0 and dir = 3 then return false
if fac == 1 and dir = 0 or fac == 1 and dir = 2 then return false
if fac == 2 and dir = 1 or fac == 2 and dir = 3 then return false
if fac == 3 and dir = 0 or fac == 0 and dir = 2 then return false
end,
onRangedAttack = function (monster)
-- locals
local dx, dy, lev, x, y, fac = 0, 0, monster.level, monster.x, monster.y, monster.facing
local garbager_id = "smart_spider_garbager_"..string.sub(monster.id, string.len(monster.id), -1)
-- brain switcher
if math.random() <= 0.5 then
monster:setPosition(findEntity(garbager_id).x, findEntity(garbager_id).y, monster.facing, findEntity(garbager_id).level)
local spider_id = "smart_spider_ranged_"..string.sub(monster.id, string.len(monster.id), -1)
spawn("smart_spider_ranged", lev, x, y, fac, spider_id)
end
return false
end,
}
Code: Select all
cloneObject{
name = "smart_spider_melee",
baseObject = "spider",
animations = {
idle = "mod_assets/animations/spider_idle.fbx",
},
rangedAttack = "poison_bolt",
onRangedAttack = function (monster)
-- locals
local dx, dy, lev, x, y, fac = 0, 0, monster.level, monster.x, monster.y, monster.facing
-- remove previous monster if exists
local destroy_id = "smart_spider_ranged_"..string.sub(monster.id, string.len(monster.id), -1)
if findEntity(destroy_id) ~= nil then
hp = findEntity(destroy_id):getHealth()
monster:setHealth(hp)
findEntity(destroy_id):destroy()
else
-- garbarer handler
local garbager_id = "smart_spider_garbager_"..string.sub(monster.id, string.len(monster.id), -1)
if findEntity(garbager_id) == nil then
for level = 1, getMaxLevels() do
for e in allEntities(level) do
if (e.class == "StartingLocation") then
local target = e.id
spawn("garbager_target", findEntity(target).level, findEntity(target).x, findEntity(target).y, monster.facing, garbager_id)
return false
end
end
end
end
-- brain switcher
if math.random() <= 0.5 then
monster:setPosition(findEntity(garbager_id).x, findEntity(garbager_id).y, monster.facing, findEntity(garbager_id).level)
local spider_id = "smart_spider_ranged_"..string.sub(monster.id, string.len(monster.id), -1)
spawn("smart_spider_ranged", lev, x, y, fac, spider_id)
end
return false
end,
}
cloneObject{
name = "smart_spider_ranged",
baseObject = "spider",
animations = {
idle = "mod_assets/animations/spider_idle.fbx",
strafeLeft = "assets/animations/monsters/spider/spider_idle.fbx",
strafeRight = "assets/animations/monsters/spider/spider_idle.fbx",
},
brain = "Uggardian",
rangedAttack = "poison_bolt",
onMove = function (monster, dir)
-- locals
local dx, dy, lev, x, y, fac = 0, 0, monster.level, monster.x, monster.y, monster.facing
-- missing animation handler will cancel move if it's strafe
if fac == 0 and dir = 1 or fac == 0 and dir = 3 then return false
if fac == 1 and dir = 0 or fac == 1 and dir = 2 then return false
if fac == 2 and dir = 1 or fac == 2 and dir = 3 then return false
if fac == 3 and dir = 0 or fac == 0 and dir = 2 then return false
end,
onRangedAttack = function (monster)
-- locals
local dx, dy, lev, x, y, fac = 0, 0, monster.level, monster.x, monster.y, monster.facing
local garbager_id = "smart_spider_garbager_"..string.sub(monster.id, string.len(monster.id), -1)
-- remove previous monster if exists
local destroy_id = "smart_spider_melee_"..string.sub(monster.id, string.len(monster.id), -1)
if findEntity(destroy_id) ~= nil then
hp = findEntity(destroy_id):getHealth()
monster:setHealth(hp)
findEntity(destroy_id):destroy()
else
-- brain switcher
if math.random() <= 0.5 then
monster:setPosition(findEntity(garbager_id).x, findEntity(garbager_id).y, monster.facing, findEntity(garbager_id).level)
local spider_id = "smart_spider_ranged_"..string.sub(monster.id, string.len(monster.id), -1)
spawn("smart_spider_ranged", lev, x, y, fac, spider_id)
end
return false
end,
}
We gonna also define onProjectileHook returning false. I know some of you are really curious how I solved projectiles issue if there is monster:destroy().
Well my dears, let me say it's very simple: You have to spawn "personal" alcove as you do it with garbager and when projectile hits monster, just add that projectile into alcove. When monster dies, in OnDie hook go through items in his personal alcove and spawns them on monster position. Then alcove is destroyed and garbager as well.
Code: Select all
cloneObject{
name = "smart_spider_melee",
baseObject = "spider",
animations = {
idle = "mod_assets/animations/spider_idle.fbx",
},
rangedAttack = "poison_bolt",
onRangedAttack = function (monster)
-- locals
local dx, dy, lev, x, y, fac = 0, 0, monster.level, monster.x, monster.y, monster.facing
-- remove previous monster if exists
local destroy_id = "smart_spider_ranged_"..string.sub(monster.id, string.len(monster.id), -1)
if findEntity(destroy_id) ~= nil then
hp = findEntity(destroy_id):getHealth()
monster:setHealth(hp)
findEntity(destroy_id):destroy()
else
-- garbarer handler
local garbager_id = "smart_spider_"..string.sub(monster.id, string.len(monster.id), -1)
if findEntity(garbager_id) == nil then
for level = 1, getMaxLevels() do
for e in allEntities(level) do
if (e.class == "StartingLocation") then
local target = e.id
spawn("dm_garbager_target", findEntity(target).level, findEntity(target).x, findEntity(target).y, monster.facing, garbager_id)
return false
end
end
end
end
-- brain switcher
if math.random() <= 0.5 then
monster:setPosition(findEntity(garbager_id).x, findEntity(garbager_id).y, monster.facing, findEntity(garbager_id).level)
local spider_id = "smart_spider_ranged_"..string.sub(monster.id, string.len(monster.id), -1)
spawn("smart_spider_ranged", lev, x, y, fac, spider_id)
end
return false
end,
onDie = function (monster)
local garbager_id = "smart_spider_"..string.sub(monster.id, string.len(monster.id), -1)
if findEntity(garbager_id) ~= nil then
findEntity(garbager_id):destroy()
end
end,
onProjectileHit = function(monster, projectile, dmgAmmount, dmgType)
-- you can use alcove trich described in 10) to manage projectiles
return false
end,
}
cloneObject{
name = "smart_spider_ranged",
baseObject = "spider",
animations = {
idle = "mod_assets/animations/spider_idle.fbx",
strafeLeft = "assets/animations/monsters/spider/spider_idle.fbx",
strafeRight = "assets/animations/monsters/spider/spider_idle.fbx",
},
brain = "Uggardian",
rangedAttack = "poison_bolt",
onMove = function (monster, dir)
-- locals
local dx, dy, lev, x, y, fac = 0, 0, monster.level, monster.x, monster.y, monster.facing
-- missing animation handler will cancel move if it's strafe
if fac == 0 and dir = 1 or fac == 0 and dir = 3 then return false
if fac == 1 and dir = 0 or fac == 1 and dir = 2 then return false
if fac == 2 and dir = 1 or fac == 2 and dir = 3 then return false
if fac == 3 and dir = 0 or fac == 0 and dir = 2 then return false
end,
onRangedAttack = function (monster)
-- locals
local dx, dy, lev, x, y, fac = 0, 0, monster.level, monster.x, monster.y, monster.facing
local garbager_id = "smart_spider_"..string.sub(monster.id, string.len(monster.id), -1)
-- remove previous monster if exists
local destroy_id = "smart_spider_melee_"..string.sub(monster.id, string.len(monster.id), -1)
if findEntity(destroy_id) ~= nil then
hp = findEntity(destroy_id):getHealth()
monster:setHealth(hp)
findEntity(destroy_id):destroy()
else
-- brain switcher
if math.random() <= 0.5 then
monster:setPosition(findEntity(garbager_id).x, findEntity(garbager_id).y, monster.facing, findEntity(garbager_id).level)
local spider_id = "smart_spider_ranged_"..string.sub(monster.id, string.len(monster.id), -1)
spawn("smart_spider_ranged", lev, x, y, fac, spider_id)
end
return false
end,
onDie = function (monster)
local garbager_id = "smart_spider_"..string.sub(monster.id, string.len(monster.id), -1)
if findEntity(garbager_id) ~= nil then
findEntity(garbager_id):destroy()
end
end,
onProjectileHit = function(monster, projectile, dmgAmmount, dmgType)
-- this will drop projectiles in front of monster and prevent vanishing of them when monster is replaced
-- you can use alcove trich described in 10) to manage projectiles
return false
end,
onAttack = function(monster)
-- shootProjectile(projectile, level, x, y, direction, speed, gravity, velocityUp, offsetX, offsetY, offsetZ, attackPower, ignoreEntity, fragile, championOrdinal)
return false
end,
}
On the other hand it's very simple as you can see... when the ice is broken.
It works good - see skeletons in coffins for example, so if copy paste of this code does not work, you have to improve it by yourself and find the bug
[/color]
Conclusion:
1) Most iportant think is alcove used as projectiles storage (make your own, located under floor, so player cannot see it if he returns for some reason to starting position).
2) onRangedAttack animation event used for calling of the function and conditions there to manage your stuff.
3) you cannot use onMove/onTurn to call hook and use setPosition, it can be safelly caled only from idle or attack
4) new monster destroys old one because self:destroy() id prohibed
5) names are important to spawn personal targets and alcoves, if you wanna spawn smart monsters on fly, you can do it, but you have to change name generator line in definitions( do smth like "base_name"..monster.id and then change findEntity())
You can play a lot with this method and it's fun. I will show you some stuff I made in last days soon.
Thanks for reading and have a fun.