[TUTORIAL] Smart Monsters Method

Talk about creating Grimrock 1 levels and mods here. Warning: forum contains spoilers!
Post Reply
User avatar
Leki
Posts: 550
Joined: Wed Sep 12, 2012 3:49 pm

[TUTORIAL] Smart Monsters Method

Post by Leki »

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:

Code: Select all

cloneObject{
   name = "smart_spider_melee",
   baseObject = "spider",
}

cloneObject{
   name = "smart_spider_ranged",
   baseObject = "spider",
}
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.

Code: Select all

cloneObject{
   name = "smart_spider_ranged",
   baseObject = "spider",
   brain = "Uggardian",
}
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:

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,
}
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:

Code: Select all

cloneObject{
   name = "garbager_target",
   baseObject = "rock",
}
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:

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,
}
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:

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,
}

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.

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,
}
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:

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,
}
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.

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,
}
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.
I'm the Gate I'm the Key.
Dawn of Lore
User avatar
AdrTru
Posts: 223
Joined: Sat Jan 19, 2013 10:10 pm
Location: Trutnov, Czech Republic

Re: [TUTORIAL] Smart Monsters Method

Post by AdrTru »

Thank you for your complex tutorial. There are much tricks for us.
My LOG2 projects: virtual money, Forge recipes, liquid potions and
MultiAlcoveManager, Toolbox, Graphic text,
User avatar
msyblade
Posts: 792
Joined: Fri Oct 12, 2012 4:40 am
Location: New Mexico, USA
Contact:

Re: [TUTORIAL] Smart Monsters Method

Post by msyblade »

This is definitely something I may delve into down the road. It opens many possibilities that could change combat as we have come to know it in LoG. Very innovative backdoors, such as spawning an alcove to "catch" projectiles thrown at a monster you are destroying and recreating on the fly, iterate how creative we can all be when trying to bypass the "rules" of the engine. I'm betting Leki has devised all of this complexity because he is soon going to show us something that will leave us speechless. Again.
Currently conspiring with many modders on the "Legends of the Northern Realms"project.

"You have been captured by a psychopathic diety who needs a new plaything to torture."
Hotel Hades
Post Reply