Understanding Monster Brains

Ask for help about creating mods and scripts for Grimrock 2 or share your tips, scripts, tools and assets with other modders here. Warning: forum contains spoilers!
Eburt
Posts: 69
Joined: Thu Feb 05, 2015 5:44 am

Understanding Monster Brains

Post by Eburt »

I've recently been playing with monster brains and the onThink hook. Since I didn't see a lot of documentation on the forum (there are some old threads from before the scripting reference was released, but I think there is a lot more to learn), I thought compiling some of my testing results might be useful for everyone. I've also included some [not-so-practical] examples to help make it easier/less intimidating to try and program a custom monster brain.


Ranty Preamble:

For each property/method, I've started with the information JKos compiled in the wiki, as well as the official documentation in the scripting reference. This is intended as an addendum to those and I have not reproduced information explained in those places.

Please note that this is intended as a work-in-progress. I've tested many things, but certainly not everything (and in several cases, I've tested but can't figure out what's going on). I'm hoping this will serve as a starting point and that the community can help fill in the blanks. I'll update this post as we learn more, but I would hope that ultimately the wiki JKos put together will be updated.


First, here are my findings on BrainComponent's properties and methods - as well as a bit about how the predefined brains interact with custom definitions. These are organized roughly alphabetically, but similar properties/methods are grouped so you might want to search if you're looking for something specific. Despite being towards the end, the onThink information is probably the most important. Unless you're really interested, I wouldn't recommend actually reading all this - think of it as a reference.

PROPERTIES
SpoilerShow
BrainComponent.blockedLeft, BrainComponent.blockedRight, BrainComponent.blockedBack, BrainComponent.blockedFront – Boolean values based on the relative position of objects to the monster. Any obstruction (e.g., walls, doors, anything with an obstacle component) except the party and probably other monsters causes these to be false, otherwise it is true. blockedAbove and blockedBelow are notably missing.

BrainComponent.seesParty – Boolean value which is true if the party is within the monsters sight range. Initially, I thought this might use occluders or something like that to check if the party was visible, but these don’t seem to matter. The only things that block sight appear to be non-sparse doors and wall tiles (but not wall objects).

BrainComponent.partyDiagonal, BrainComponent.partyAdjacent, BrainComponent.partyRight, BrainComponent.partyLeft, BrainComponent.partyStraightBehind, BrainComponent.partyStraightAhead, BrainComponent.partyBehind, BrainComponent.partyAhead, BrainComponent.partyOnLevel – Boolean values based on the party position, relative to the monster. Fairly self-explanatory, though it is worth noting that these will not change if the monster can/can’t see the party. Hence, an additional if statement is often necessary to check this.

BrainComponent.partyLastSeen – If the party becomes invisible, this is set to zero.

BrainComponent.partyDistY, BrainComponent.partyDistX – Nothing to add.

BrainComponent.partyX, BrainComponent.partyY – x and y coordinates of the party; no functional difference from using party.x and party.y.
METHODS
SpoilerShow
BrainComponent:alert() – Requires an animation defined and associated with a corresponding monsterAction (named “alert”) in the monster definition. This appears to “alert” other monsters in the area to the position of the party, almost as if those monsters had seen the party (I haven’t tested if this actually sets seesParty to true though). I do not know the exact radius in which monsters are alerted, but it appears to be hard-coded.

BrainComponent:circleStrafeHesitate(), BrainComponent:circleStrafeStall(), BrainComponent:moveBackward(), BrainComponent:moveForward(), BrainComponent:strafeLeft(), BrainComponent:strafeRight(), BrainComponent:turnAround(),BrainComponent:turnLeft(), BrainComponent:turnRight() – All require animations to be defined to use but it looks like the Brain component takes care of the “high level” movement of these functions. Note that monsters will not move into squares which are blocked or contain the party/other monsters (except for swarms, which can move into the same square as the party/monsters). Except for flying/swimming monsters, I believe this prevents them from walking off ledges as well.

BrainComponent:changeAltitude(???) – I assume this may require an argument (basically up/down) though I haven’t tested it. The scripting reference does not show it accepting any arguments. It also appears to automatically use the idle animation for the monster, so an additional animation is not required. I also haven’t tested if moveTowardsParty and similar methods utilize the ability to change altitudes but I assume they do.

BrainComponent:charge() – Requires animations to be defined for the start of the charge, the left step, the right step, and the ending (hit object/party) on both the left and right steps (five total animations). Also requires the MonsterCharge (named “charge”) component to be included in the monster definition.

BrainComponent:flee(), BrainComponent:startFleeing(),BrainComponent:stopFleeing() – Fleeing causes the monster to run from the party. Note that the fleeing speed is limited by the monsterMove cooldown. This will automatically happen sometimes as monsters get low on health so long as their morale is less than 100 (the closer to zero, the more likely this is to happen). While a monster is fleeing from low morale, the onThink hook does not get called. However, there is no obvious way to replicate this behavior using any of these methods (or any others I have found) because the onThink hook is called after every action (e.g., move, turn). As a result, these must be called repeatedly every call to onThink to be effective and I have found no functional difference between using flee() and startFleeing(). Similarly, I have found no reason to use stopFleeing() because it is more practical to simply give the monster another action to do. If there is a way to stop onThink from being called, there will be more diversity in the way these can be used.

BrainComponent:follow(), BrainComponent:moveTowardsParty(), BrainComponent:pursuit() – These all cause the monster to move towards the coordinates adjacent to the party. Note that the movement speed is limited by the monsterMove cooldown. If the monster cannot reach an adjacent square, it will get as close as it can. Note that it doesn’t matter if the monster can see the party – it will still move in the correct direction. Similar to the flee() vs startFleeing() methods, I have not found any differences in using these due to onThink being called frequently.

BrainComponent:getAllAroundSight(), BrainComponent:setAllAroundSight(boolean), BrainComponent:getSight(), BrainComponent:setSight(number) – Sight is the radius around the monster in which it can detect the party. If allAroundSight is set to true, this is 360 degrees. If it is false, it is a cone, approximately 180 degrees in front of the monster. Note that directly left/right of the monster will not be detected unless in the square adjacent to the monster (so, for example, the monster would not see the party if they were 2 squares directly left). Because this is a radius, it gets a bit weird on the diagonals as it does its best to make an approximate circle out of the tiles. Note that a sight of zero means the monster will not see anything (not infinite sight).

BrainComponent:isSafeTile(x,y) – I have no idea what makes a tile unsafe. This always seems to return true. I have tested tile damages, projectiles, obstacles, other monsters and the party. Very curious to learn how this works.

BrainComponent:meleeAttack(), BrainComponent:rangedAttack(), BrainComponent:turnAroundAttack(), BrainComponent:turnAttack(), BrainComponent:turnAttackLeft(), BrainComponent:turnAttackRight() – These are essentially shortcut methods which call specific monster actions. meleeAttack() calls the monster action/attack named “basicAttack” and rangedAttack() unsurprisingly calls the one named “rangedAttack”. I can’t find great examples of all the others, but I think they all call the “turnAttack” action but can have different animations associated.

BrainComponent:performAction(string) – This calls any monster actions or monster attacks in the monster definition. The string must be the name of the action to be taken. For example, performAction(“basicAttack”) is equivalent to meleeAttack(). Note that if the action is not defined it will result in a crash.

BrainComponent:pickUpItem(string, boolean) – This is another shortcut performAction(…) call. While its intention appears obvious, I can’t find an example of its use so I really have no idea how it works. The boolean is required, though I don’t know what for.

BrainComponent:startGuarding(), BrainComponent:stopGuarding() – startGuarding() essentially puts the monster in the same state as if you changed the AI mode to “guard” in the editor. Basically, the onThink hook isn’t called until the party is in sight. Note that once it is called, it will continue to be called repeatedly unless the monster is destroyed or startGuarding() is called again once the party is out of sight. This might be useful to prevent excessive CPU usage for monsters not on the current level, but otherwise doesn’t seem that practical. stopGuarding() might have some uses, but it must be called from a script external to onThink – since of course onThink isn’t called while the monster is guarding.

BrainComponent:stepTowards(string, number) – The string must be an object ID. I have no idea what the number is for, but it appears to be optional (it is not the number of steps taken or anything like that – unless this is an issue with onThink being called repeatedly, but I don’t think so).

BrainComponent:turnTowardsDirection(direction) – Takes a single argument, which is a direction in the 0..3 format (scripting reference indicates no argument, which is incorrect).

BrainComponent:turnTowardsTarget(string) – Takes a single argument, which is a string containing an object ID (scripting reference indicates no argument, which is incorrect).

BrainComponent:wander(), BrainComponent:wanderIfPartyNotDetected() – These are pretty self explanatory (cause the monster to move randomly). Note that the movement speed is limited by the monsterMove cooldown. Also worth noting that I typically use wander() in the else clause of an if block… which makes wanderIfPartyNotDetected() somewhat less useful than it might otherwise be.

BrainComponent:onThink(self) – This is the primary way you can control the actions of a monster. Most of the above properties/methods are primarily useful inside an onThink hook. As a result, it is very important to understand when this is called. I’ll break this into a few parts:

onThink is called every time a monster is ready to act. This is basically controlled by the length of the animations associated with their various actions/attacks – which is presumably why the animations are generally required. This appears to be the same way the MonsterComponent:isReadyToAct() works. I have yet to identify a good way to prevent onThink from being called to allow the monster to continue actions such as fleeing (there is no isFleeing() method, for example). This may be hard-coded in the “high level” brain actions built into the game.

For monsters in the “Default” AI state, onThink will just be called repeatedly regardless of the relative party position. Typical behaviors in the state include wait() and wander(). For monsters in “Guard” AI state, the onThink hook will not be called until the party enters the monster’s sight range. Once detected, it essentially sets monster AI state back to “Default” unless the startGuarding() method is called.

Finally, there is some distinction between adding an onThink hook to a predefined brain (e.g., TurtleBrainComponent, MeleeBrainComponent, etc.) and defining a new BrainComponent from scratch with onThink. When you add an onThink to a predefined brain the onThink hook overrides whatever behavior the brain would otherwise take. This is useful if you want to add a single specific action or something similar, but doesn’t really cut it if you want to change/remove logic for existing actions. Note that calling wait() doesn’t appear to override the predefined brain behavior.

When defining a new BrainComponent from scratch onThink is the majority of the work because you need to define all the different actions which can be performed and under what circumstances they should be done. Note that if the monster can’t decide on anything else to do wait() should always be called to avoid excessive CPU usage (unlike if adding it to a predefined brain, where this will accomplish nothing). If you have multiple brains (including a predefined brain and a new BrainComponent) whichever one is defined last in the monster definition will override the other. You can’t use a second BrainComponent to achieve the same behavior as if you added an onThink to a predefined brain. You also can’t enable/disable different brains because each must be named “brain” of they won’t be recognized by the monster AI.

BrainComponent:onBloodied(self) – This appears to be called at some low health threshold (~10-20% of max health, though I haven’t tested enough to verify the exact point). This is NOT called every time the monster takes damage, and appears to be called only the first time monster health drops below the threshold. Note that this and the monsters onDie() hook can be called at the same time if a single damage source would bring the monster below the threshold and kill it at the same time. I could see this potentially causing bugs if onBloodied() tries to modify something that is destroyed when the monster is killed (the monster entity isn’t entirely destroyed for several seconds, but I’m not sure if some parts are affected immediately).


BrainComponent:carrying(string), BrainComponent:dropItem(string), BrainComponent:dropItemOn(string, string), BrainComponent:getMorale(), BrainComponent:getSeeInvisibility(), BrainComponent:goTo(string), BrainComponent:here(string), BrainComponent:openLockWith(string, string), BrainComponent:operate(string), BrainComponent:setMorale(number), BrainComponent:setSeeInvisible(boolean), BrainComponent:turnTowardsParty() – These are all either self-explanatory, existing documentation covers them, or I haven’t done any real testing so I have nothing to say. I will be happy to make entries for these as needed.


Ok, with that out of the way - how about some examples! Let's start by re-defining the TurtleBrainComponent with a custom onThink hook. This has several advantages, as I described above.

DISCLAIMER: This probably isn't exactly how the predefined brain behaves, but it's close enough that most players (including myself) probably wouldn't notice.

To start, I would recommend anyone doing this for the first time to lay out the approximate behavior they are looking for. I like to sketch it out, but I'm sure a napkin scribble would work fine. Since we're replicating existing behavior, go fight a couple turtles if you want to see what I'm trying to replicate.

Here's the brain definition with some minimal comments to explain. This works fine if you comment out/delete the TurtleBrain component in the turtle definition and paste this in:
SpoilerShow

Code: Select all

		{
			class = "Brain",
			sight = 3,
			morale = 50, --makes turtle reasonably likely to flee (no need to code this behavior)
			onThink = function(self)
				if self.partyOnLevel then
					if self.seesParty and party.elevation == self.go.elevation then
						if self.partyStraightAhead then
							if (math.abs(self.partyDistX) == 1 or math.abs(self.partyDistY) == 1) then
								if math.random() < 0.9 then
									self:meleeAttack()
								else
									self:alert()
								end
							elseif (math.abs(self.partyDistX) == 2 or math.abs(self.partyDistY) == 2) then
								if math.random() < 0.2 then
									self:performAction("moveAttack")
								else
									self:moveTowardsParty()
								end
							end
						else
							self:moveTowardsParty()
						end
					elseif (Time.currentTime() - self.partyLastSeen) < 1 then
						self:turnTowardsParty() --If this isn't here the monster won't respond to backstabs
						--(which update partyLastSeen but don't call onThink to respond immediately)
					else
						if math.random() < 0.5 then
							self:wander()
						else
							self:wait()
						end
					end
				else
					self:startGuarding() --avoids onThink calls if party is not on level. The downside is that
					--this requires an external script to stopGuarding() when party re-enters the level if you
					--want the default AI state (where the monster wanders). A compromise is to replace this
					--with self:wait()
				end
			end,
		},
(For simplicity, the rest of the Turtle definition is omitted.)


And here is a slightly more complex example, building on the above to create a Ninja Turtle! (Shame on you if you don't get the reference. Go watch more cartoons.)

Feel free to use this entire definition and play around with it. You're welcome to modify or use as-is (if you actually have need of a joke monster like this). Since there are several other modifications, the entire monster definition is included. Again, it's lightly commented, but if you have any questions, feel free to ask.
SpoilerShow

Code: Select all

defineObject{
	name = "ninja_turtle",
	baseObject = "base_monster",
	components = {
		{
			class = "Model",
			model = "assets/models/monsters/turtle.fbx",
			storeSourceData = true, -- must be enabled for mesh particles to work
		},
		{
			class = "Animation",
			--These are all the base animations, which I reuse for amusing results
			animations = {
				idle = "assets/animations/monsters/turtle/turtle_idle.fbx",
				moveForward = "assets/animations/monsters/turtle/turtle_walk.fbx",
				turnLeft = "assets/animations/monsters/turtle/turtle_turn_left.fbx",
				turnRight = "assets/animations/monsters/turtle/turtle_turn_right.fbx",
				attack = "assets/animations/monsters/turtle/turtle_attack.fbx",
				attack2 = "assets/animations/monsters/turtle/turtle_attack2.fbx",
				moveAttack = "assets/animations/monsters/turtle/turtle_move_attack.fbx",
				getHitFrontLeft = "assets/animations/monsters/turtle/turtle_get_hit_front_left.fbx",
				getHitFrontRight = "assets/animations/monsters/turtle/turtle_get_hit_front_right.fbx",
				getHitBack = "assets/animations/monsters/turtle/turtle_get_hit_back.fbx",
				getHitLeft = "assets/animations/monsters/turtle/turtle_get_hit_left.fbx",
				getHitRight = "assets/animations/monsters/turtle/turtle_get_hit_right.fbx",
				fall = "assets/animations/monsters/turtle/turtle_get_hit_front_left.fbx",
				alert = "assets/animations/monsters/turtle/turtle_alert.fbx",
			},
			currentLevelOnly = true,
		},
		{
			class = "Monster",
			meshName = "turtle_mesh",
			hitSound = "turtle_hit",
			dieSound = "turtle_die",
			footstepSound = "turtle_footstep",
			hitEffect = "hit_blood",
			capsuleHeight = 0.2,
			capsuleRadius = 0.7,
			health = 200,
			evasion = 20,
			exp = 200,
			lootDrop = { 75, "turtle_steak", 50, "shuriken" },
			resistances = { ["poison"] = "immune" }, --who ever heard of a ninja dying from poison?
			traits = { "animal" },
			headRotation = vec(90, 0, 0),
		},
		{
			class = "Brain",
			sight = 5,
			morale = 80,
			onThink = function(self)
				if self.partyOnLevel then
					if self.go.model:isEnabled() then
						if self.seesParty and party.elevation == self.go.elevation then
							self.go.hasSeenParty:enable() --Enable dummy component to track if the party has been seen
							if math.random() > (self.go.monster:getHealth() / self.go.monster:getMaxHealth() + 0.5) then
								--If health is below 50% smokeBomb is an option - likelihood of use increases as health drops
								self:performAction("smokeBomb")
							elseif self.partyStraightAhead then
								if (math.abs(self.partyDistX) == 1 or math.abs(self.partyDistY) == 1) then
									if math.random() < 0.9 then
										self:meleeAttack()
									else
										self:alert()
									end
								elseif (math.abs(self.partyDistX) == 2 or math.abs(self.partyDistY) == 2) then
									if math.random() < 0.5 then
										self:performAction("moveAttack")
									else
										self:moveTowardsParty()
									end
								else
									if math.random() < 0.7 then
										self:rangedAttack() --Shuriken!
									else
										self:moveTowardsParty()
									end
								end
							else
								if math.random() < 0.9 then
									self:moveTowardsParty()
									--An interesting modification might be to encourage the monster to move into a straight
									--line with the party for ranged attacks, but not necessarily get closer to melee range.
								else
									self:alert()
								end
							end
						elseif (Time.currentTime() - self.partyLastSeen) < 1 then
							self:turnTowardsParty()
						elseif self.partyLastSeen == 0 then
							--If the party casts invisibility then the ninja turtle will search for 10 seconds by looking
							--around randomly before resuming its basic wander/wait operations. This makes backstabing harder.
							if self.go.searchTimer:isEnabled() then --Only do this for 10 seconds
								local searchDecision = math.random()
								if searchDecision < 0.2 then
									self:turnLeft()
								elseif searchDecision < 0.4 then
									self:turnRight()
								elseif searchDecision < 0.7 then
									self:moveForward()
								else
									self:wait()
								end
							else
								if self.go.hasSeenParty:isEnabled() then --Reset dummy party seen component and activate timer
									self.go.searchTimer:enable()
									self.go.hasSeenParty:disable()
								end
							end
						else
							if math.random() < 0.2 then
								self:wander()
							else
								self:wait()
							end
						end
					else
						--When invisible after using smokeBomb, the ninja turtle tries to get behind the party to backstab
						if party.elevation == self.go.elevation then
							local dx,dy = getForward(party.facing)
							local behindPartyX = party.x - dx
							local behindPartyY = party.y - dy
							if self.go.x == behindPartyX and self.go.y == behindPartyY then
								if self.partyStraightAhead then --backstab!
									self:meleeAttack()
									self.go.model:enable()
									self.go.invisTimer:disable()
									self.go.move:setSound("turtle_walk") --re-enable movement sounds
									self.go.turn:setSound("turtle_walk")
									self.go.monster:setFootstepSound("turtle_footstep")
								else
									self:turnTowardsParty()
								end
							else
								self:seek(behindPartyX, behindPartyY)
							end
						else
							if math.random() < 0.2 then
								self:wander()
							else
								self:wait()
							end
						end
					end
				else
					self:startGuarding()
				end
			end,
		},
		{
			class = "MonsterMove",
			name = "move",
			sound = "turtle_walk",
			cooldown = 1, --Turtles are slow. But not ninja turtles.
			dashChance = 50, --This seems to be the chance that there will be no cooldown on the move action.
		},
		{
			class = "MonsterTurn",
			name = "turn",
			sound = "turtle_walk",
		},
		{
			class = "MonsterAttack",
			name = "basicAttack",
			attackPower = 25,
			cooldown = 3.5,
			causeCondition = "poison",
			conditionChance = 10,
			-- sound = "turtle_attack",
			onBeginAction = function(self)
				-- randomize animation
				if math.random() < 0.8 then
					self:setAnimation("attack")
					self.go:playSound("turtle_attack")
				else
					self:setAnimation("attack2")	-- double attack
					self.go:playSound("turtle_double_attack")
				end
				
				--Backstab for double damage and guaranteed poison
				local dx,dy = getForward(party.facing)
				if (party.x == self.go.x + dx and party.y == self.go.y + dy) then
					self:setAttackPower(50)
					self:setConditionChance(100)
				else --otherwise, reset to original attack values
					self:setAttackPower(25)
					self:setConditionChance(10)
				end
			end,
		},
		{
			class = "MonsterMoveAttack",
			name = "moveAttack",
			attackPower = 30,
			cooldown = 15,
			causeCondition = "paralyzed", --because why not?
			conditionChance = 25,
			animation = "moveAttack",
			sound = "turtle_move_attack",
		},
		{
			class = "MonsterAttack",
			name = "rangedAttack",
			attackType = "projectile",
			attackPower = 20,
			cooldown = 4.5,
			animation = "attack",
			sound = "ratling2_attack", --I should probably update this...
			shootProjectile = "shuriken",
			projectileHeight = 1.1,
		},
		{
			class = "MonsterAction",
			name = "smokeBomb",
			cooldown = 45,
			animation = "attack2",
			onBeginAction = function(self)
				self.go.smokeBombEffect:restart() --show particles
				self.go.model:disable() --invisible turtle!
				self.go.move:setSound("silent") --remove movement sounds
				self.go.turn:setSound("silent")
				self.go.monster:setFootstepSound("silent")
				self.go.invisTimer:enable() --activate invisibility timer
			end,
		},
		{
			class = "MonsterAction",
			name = "alert",
			cooldown = 100,
			animation = "alert",
		},
		{
			class = "Particle",
			name = "smokeBombEffect",
			particleSystem = "trickster_death_smoke_bomb",
			emitterMesh = "assets/models/monsters/turtle.fbx",
			enabled = false,
		},
		{
			class = "Timer",
			name = "invisTimer",
			timerInterval = 10,
			--disableSelf = true,
			enabled = false,
			onActivate = function(self)
				self.go.model:enable()
				self.go.smokeBombEffect:restart()
				self.go.move:setSound("turtle_walk") --re-enable movement sounds
				self.go.turn:setSound("turtle_walk")
				self.go.monster:setFootstepSound("turtle_footstep")
				self:disable() --for some reason, disableSelf doesn't seem to be behaving, but this works
			end,
		},
		{
			class = "Timer",
			name = "searchTimer",
			timerInterval = 10,
			--disableSelf = true,
			enabled = false,
			onActivate = function(self)
				self:disable() --for some reason, disableSelf doesn't seem to be behaving, but this works
			end,
		},
		{
			class = "Null",
			name = "hasSeenParty",
			enabled = false,
			--This is a dummy component [HACK] used to keep track of if the party has been seen recently, and
			--is used in the searching for invisible party logic. Note that there are much better ways to do
			--this with, for example, some of JKos's scripts - but I wanted to keep this self-contained.
		},
	},
}

defineSound{
	name = "silent",
	filename = "assets/samples/magic/balance_cast_01.wav",
	loop = false,
	volume = 0, --silent sound to replace movement sounds, since you can't set them to nil
	minDistance = 1,
	maxDistance = 1,
}
I hope this helps to clarify at least a bit about how custom brains work! And even more so, I hope this inspires people to continue playing with them and sharing the results!
MrChoke
Posts: 324
Joined: Sat Oct 25, 2014 7:20 pm

Re: Understanding Monster Brains

Post by MrChoke »

I have done extensive learning on the AI as well (got it doing some very cool stuff in my dungeon). I did not read all of your info but what I did read looks correct. For ones you are confident of, you should update Jkos's Wiki with the info. I have done quite a few updates there as well but there is still so much that is still empty. Since you are already got a lot written up here, its basically copy and paste.
User avatar
Duncan1246
Posts: 404
Joined: Mon Jan 19, 2015 7:42 pm

Re: Understanding Monster Brains

Post by Duncan1246 »

Hi Eburt,
I am trying to use a scriptable wizard to made a sort of square dance... I write this:
SpoilerShow

Code: Select all

function think()
 posX=party.x
 posY=party.y
 fac=party.facing

 if ((posX~=23 or posX~=24) and (posY~=17 or posY~=18)) or (posX~=25  and posY~=17) then self.go.brain:wait() end
 if posX==23 and posY==18 and fac==1 then self.go.brain:goTo("floor_trigger_4") 
		if self.go.facing~=(fac+2)%4 then self.go.brain:turnTowardsDirection(3) end
  end
 if posX==23 and posY==18 and fac==0 then self.go.brain:goTo("floor_trigger_2") 
		if self.go.facing~=(fac+2)%4 then self.go.move:setTurnDir(2) end
 end
 if posX==23 and posY==17 and fac==1 then self.go.brain:goTo("floor_trigger_3") 
		if self.go.facing~=(fac+2)%4 then self.go.move:setTurnDir(3) end
 end
............others positions follows in the same way
The wizard moves correctly, but after it turns repeatedly right-left-right . I have tried with TowardsParty, same punishment, and "wait", as you said, do nothing. Do you know how I can stop it?
Duncan
The Blue Monastery (LOG1)
download at:http://www.nexusmods.com/grimrock/mods/399/?

Finisterrae(LOG2)
download at:http://www.nexusmods.com/legendofgrimrock2/mods/61/?
Eburt
Posts: 69
Joined: Thu Feb 05, 2015 5:44 am

Re: Understanding Monster Brains

Post by Eburt »

Hi Duncan

Hmmm, I'm rather puzzled. I would think that the wait() method should actually work fine here since the scriptable brain overwrites the WizardBrain component in the original definition... plus I haven't looked, but I'm pretty sure this must be used in the original campaign.

One workaround I've been using is just to disable the brain while I want the monster to wait, but that is messy and can have unwanted side-effects.

As for the repeated turning behavior, the only thing I can think of (and only because I really don't know Lua that well) is that I'm not certain that modulo uses correct order of operations, so I would try wrapping your (fac+2)%4 conditions in parenthesis... but I'm 90% sure this isn't it. Another possible bug fix would be to use elseif all inside a single if block (with one end) as opposed to a separate block for each position. Again, I doubt this is it, but I'm just trying to rule out a logic bug.

I would be happy to look at this more later, but you actually posted this at basically the worst time for me since I'm in the middle of moving my family. Maybe over the weekend if you haven't figured it out I will take another look. Good luck!
User avatar
Duncan1246
Posts: 404
Joined: Mon Jan 19, 2015 7:42 pm

Re: Understanding Monster Brains

Post by Duncan1246 »

Eburt wrote:Hi Duncan

Hmmm, I'm rather puzzled. I would think that the wait() method should actually work fine here since the scriptable brain overwrites the WizardBrain component in the original definition... plus I haven't looked, but I'm pretty sure this must be used in the original campaign.

One workaround I've been using is just to disable the brain while I want the monster to wait, but that is messy and can have unwanted side-effects.

As for the repeated turning behavior, the only thing I can think of (and only because I really don't know Lua that well) is that I'm not certain that modulo uses correct order of operations, so I would try wrapping your (fac+2)%4 conditions in parenthesis... but I'm 90% sure this isn't it. Another possible bug fix would be to use elseif all inside a single if block (with one end) as opposed to a separate block for each position. Again, I doubt this is it, but I'm just trying to rule out a logic bug.

I would be happy to look at this more later, but you actually posted this at basically the worst time for me since I'm in the middle of moving my family. Maybe over the weekend if you haven't figured it out I will take another look. Good luck!
Thanks for your time; I should try again.
The Blue Monastery (LOG1)
download at:http://www.nexusmods.com/grimrock/mods/399/?

Finisterrae(LOG2)
download at:http://www.nexusmods.com/legendofgrimrock2/mods/61/?
User avatar
Duncan1246
Posts: 404
Joined: Mon Jan 19, 2015 7:42 pm

Re: Understanding Monster Brains

Post by Duncan1246 »

Hi Eburt,
News of the dance... exit wizard, crab_scriptable enter (more convenient for my purpose). The script works well in 8 cases, don't work in two, and it's very strange.
this works:
SpoilerShow

Code: Select all

if posX==23 and posY==18 and fac==0 and acted~=2
		then 
			if monster.x~=23 or monster.y~=17 then
				monster.go.brain:goTo("floor_trigger_2")
			end
			if monster.go.brain:here("floor_trigger_2") then 
					monster.go.brain:turnTowardsParty()
					if 	monster.go.facing==(fac+3)%4 then
					acted=2
					end	
			end	
 		end
 		if posX==23 and posY==17 and fac==1 and acted~=3
		then 
			if monster.x~=24 or monster.y~=17 then
				monster.go.brain:goTo("floor_trigger_3")
			end
			if monster.go.brain:here("floor_trigger_3") then 
				monster.go.brain:turnTowardsParty()
				if 	monster.go.facing==(fac+1)%4 then
				acted=3
				end	
			end	
this don't:
SpoilerShow

Code: Select all

if posX==24 and posY==17 and fac==2 and acted~=9
		then 
			if monster.x~=24 or monster.y~=18 then
					monster.go.brain:goTo("floor_trigger_4")
			end
			if monster.go.brain:here("floor_trigger_4") then   
					monster.go.brain:turnTowardsParty()
					if 	monster.go.facing==(fac+2)%4 then
					print(monster.go.facing)
					acted=9
					end	 
			end
		end
and this don't work neither:
SpoilerShow

Code: Select all

if posX==23 and posY==17 and fac==2 and acted~=4
		then 
			if monster.x~=23 or monster.y~=18 then
					monster.go.brain:goTo("floor_trigger_1")
			end
			if monster.go.brain:here("floor_trigger_1") then
					 monster.go.brain:turnTowardsDirection(0)
					 if monster.go.facing==(fac+2)%4 then
					 acted=4
					 end	
			end	  
		end
Two things:
1) the blocks which works should not do so, because by exemple when fac=0, monster.go.facing should be 2 AND NOT 3! Test it in adding "print(monster.go.facing)" : the response is not correct.
2) the two "bad" cases can't be resolved like the others, even in changing the turn method...
I know that you have not the time now to reply, don't worry about it ...
Duncan
The Blue Monastery (LOG1)
download at:http://www.nexusmods.com/grimrock/mods/399/?

Finisterrae(LOG2)
download at:http://www.nexusmods.com/legendofgrimrock2/mods/61/?
alois
Posts: 112
Joined: Mon Feb 18, 2013 7:29 am

Re: Understanding Monster Brains

Post by alois »

Hi Duncan.

Maybe - just "maybe" - you are having problems because after the "monster.go.brain:turnTowardsParty()" you immediately check for the monster facing; i.e., before the monster has finished turning (actually, it may not even have started to turn), and so has another facing (this probably explains why the other blocks work). So - maybe - you should: 1) move to the target if you are not there; 2) in another "cycle" of onThink check if it arrived and then turn it; 3) in a third cycle of onThink, check whether the facing is correct; also, note that since the code is executed under the assumption that, for example, the party is at X = 23, Y = 18 and facing north (fact = 0), then it is enough to check whether monster.facing = 2 (which is the result of (fac+2)%4).

Hope this helps,

Alois :)
User avatar
Duncan1246
Posts: 404
Joined: Mon Jan 19, 2015 7:42 pm

Re: Understanding Monster Brains

Post by Duncan1246 »

alois wrote:Hi Duncan.

Maybe - just "maybe" - you are having problems because after the "monster.go.brain:turnTowardsParty()" you immediately check for the monster facing; i.e., before the monster has finished turning (actually, it may not even have started to turn), and so has another facing (this probably explains why the other blocks work). So - maybe - you should: 1) move to the target if you are not there; 2) in another "cycle" of onThink check if it arrived and then turn it; 3) in a third cycle of onThink, check whether the facing is correct; also, note that since the code is executed under the assumption that, for example, the party is at X = 23, Y = 18 and facing north (fact = 0), then it is enough to check whether monster.facing = 2 (which is the result of (fac+2)%4).

Hope this helps,

Alois :)
Hi Alois,
I have applied your three points in my script:
1) Monster go to tile I choose(works always):
SpoilerShow

Code: Select all

if monster.x~=23 or monster.y~=18 then
					monster.go.brain:goTo("floor_trigger_1")
			end
2) this verify with monster.go.brain:here() if 1) is done, then turns monster:
SpoilerShow

Code: Select all

if monster.go.brain:here("floor_trigger_4") then   
					monster.go.brain:turnTowardsParty()
. First issue here: print(monster.go.facing ) gives a bad result before turning and after also, at 90 degrees from the real facing... In addition, the movment has two phases in opposites directions, and I don't know why the second phase occurs.
3) I check the facing and if it's correct, actualize the exit flag:
SpoilerShow

Code: Select all

if 	monster.go.facing==(fac+2)%4 then
					print(monster.go.facing)
					acted=9
					end	 
. Second issue here: monster.go.facing gives a bad value (monster facing West, gives 2 by example so I have to replace monster.go.facing==(fac+2)%4 by monster.go.facing==(fac+1)%4 to obtain the good result!).
That's it... a true crab walking.... :mrgreen:
The Blue Monastery (LOG1)
download at:http://www.nexusmods.com/grimrock/mods/399/?

Finisterrae(LOG2)
download at:http://www.nexusmods.com/legendofgrimrock2/mods/61/?
alois
Posts: 112
Joined: Mon Feb 18, 2013 7:29 am

Re: Understanding Monster Brains

Post by alois »

Try this :)

Code: Select all

if (posX==23) and (posY==17) and (fac==1) and (acted~=3) then
	if (monster.x~=24) or (monster.y~=17) then
		monster.go.brain:goTo("floor_trigger_3") -- move the monster to its destination
	end
	if monster.go.brain:here("floor_trigger_3") then -- now it has arrived
		if (monster.go.facing ~= 3) then -- if the monster does not face the party, turn it
			monster.go.brain:turnTowardsParty()
		else
			acted = 3 -- but if already looks at the party, flag activation
		end
	end
end
Alois :)
User avatar
Duncan1246
Posts: 404
Joined: Mon Jan 19, 2015 7:42 pm

Re: Understanding Monster Brains

Post by Duncan1246 »

alois wrote:Try this :)

Code: Select all

if (posX==23) and (posY==17) and (fac==1) and (acted~=3) then
	if (monster.x~=24) or (monster.y~=17) then
		monster.go.brain:goTo("floor_trigger_3") -- move the monster to its destination
	end
	if monster.go.brain:here("floor_trigger_3") then -- now it has arrived
		if (monster.go.facing ~= 3) then -- if the monster does not face the party, turn it
			monster.go.brain:turnTowardsParty()
		else
			acted = 3 -- but if already looks at the party, flag activation
		end
	end
end
Alois :)
Done: in the case above (working in my script with the "wrong" condition of facing), crab stops face South... In the case which don't works in my script(23,17,fac=2), crab wiggles from east to south indefinitly...
The Blue Monastery (LOG1)
download at:http://www.nexusmods.com/grimrock/mods/399/?

Finisterrae(LOG2)
download at:http://www.nexusmods.com/legendofgrimrock2/mods/61/?
Post Reply