[TUTORIAL] Smart Monsters Method
Posted: Thu May 15, 2014 1:40 pm
Tuturial:
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:
2) Now you have to add some lines in your "smart_spider_ranged". First one is new brain, because the original spider is melee. Change it to "Uggardian" or "SkeletonArcher", who are ranged. Let's try the first one.
3) Because ranged monsters like Uggardian and Skeleton archer is strafing left and right and there is not a spider animation for that kind of move, you have to prevent that move in your spider definition.
You also define strafeLeft and Right animations and use an "idle" animation for that. Your should have smth like this:
Note: If you have strafe animations for your monster, you can skip missing anmation handler in onMove hook and just define strafing anims them in animations.
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:
This "garbager_targed" works like safe place where monster can be moved from scene. I will explan it.
5) Let's add new lines in your monsters definitions. It's rangedAttack definition and "onRangedAttack" Hook with one local and returning false:
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.
7) But there is not onInit (we need it!) hook in monsters, so we have to call it using animation event. Open your mod_assets/animations folder and copy "spider_idle" from asset pack and copy it there.
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:
8) So when game starts added "smart_spider_melee" monsters will create inmediatelly garbage targets. We need these garbagers to manage monster switchering i.e their behaviour.
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.
9) It's time for cleaning up. Monster moved to garbage_targed is removed from game. Before that happens, new monster will set HP as the old one had:
10) So we are allmost there. Lets finish it. Add onAttack function for you "smart_spider_ranged" monster. For now, he will shoot rock insteed of spider web. If you wanna spider web, define your own projectile.
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.
Hope this helps you to get some new funkcionalities in your mods. I'm sorry for typos or some bugs in scripts but the point was to show main idea, not to write whole scripts etc.
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.
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.