Understanding Monster Brains
Posted: Fri Mar 27, 2015 2:35 am
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
METHODS
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:
(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.
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!
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.
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.
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.
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,
},
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,
}