NPC (Custom Brain, or No Brain)

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!
Post Reply
User avatar
The_Morlock
Posts: 24
Joined: Sun Jun 02, 2024 6:15 am

NPC (Custom Brain, or No Brain)

Post by The_Morlock »

Hey everyone,

I've been researching and working on creating an NPC in my mod. I've attended the LoG2 Discord and they (mostly minmay (Thank you, BTW!)) have been gracious enough to give me some pointers to use the monster brain instead of just monster commands (I assume because LoG2 was designed with using monster brains only and not just commands with no brain? I've noticed that there are fewer non-brain monster commands like seek() and goTo(). Note that this monster is there only to perform a set sequence of events, nothing more. It is not going to interact with the party or other monsters.), but I've been running into hurdles I can't seem to figure out. With each set of brain commands, the monster will wander around and go where the party is and I can't seem to stop this behavior. If I use no brain, the sequence of events is rather... mechanical. 1 second for each square move, etc. It just isn't as smooth. I've used both a timer object as well as delayedCall() which works better than the timer object as I can more easily set second limits until the next command.

I've been looking into this via the following forum post:
https://mail.grimrock.net/forum/viewtop ... ion#p93615

It informs that the wait() command sometimes is not followed when using the monster brain...
Note that calling wait() doesn’t appear to override the predefined brain behavior.
unlike if adding it to a predefined brain, where this will accomplish nothing
Forgive me if you've seen this before, I am just needing more clarification since if I simply add the commands in order, they are not followed and seemingly, they are all run at the same time instead of waiting for one command to complete before going to the next command.

Basically, I need an NPC that does a specific set of instructions, like the following (waiting until each one completes before continuing to the next):

Code: Select all

monster spawns at x5,y16,z1 facing 3(west)
monster moveForward() twice (or two spaces/cells) (could be seek x3,y16,z0 if using brain since it seems that non-brain commands only move one space at a time)
monster at x3,y16,z0
monster waits for x seconds for dialogue to play
monster turnLeft()
monster moveForward()
monster at x3,y17,z0
monster turnRight()
monster moveForward()
monster at x2,y17,z0
monster turnLeft()
monster moveForward()
monster at x2,y18,z0
monster waits for lever at x2,y18,z0
monster waits for x seconds for dialogue to play
monster seeks to x4,y24,z0
monster waits for lever at x3,y24,z0 and then moves downstairs
If I can add to the LUA defineObject onThink (self) instruction set to perform a command and then just wait for the next command, that would be ideal as this won't be the only NPC in the mod.

I appreciate anyone who gives assistance.

Much appreciated,
TheMorlock
My Project(s): Grimstone Keep, a reimagining of the 1995 Interplay game, Stonekeep.
Discord: https://discord.gg/AfZVYHc8JX
Patreon: https://patreon.com/GrimstoneKeep
User avatar
DaggorathMaster
Posts: 53
Joined: Thu Sep 08, 2022 7:29 pm

Re: NPC (Custom Brain, or No Brain)

Post by DaggorathMaster »

You could disable turning, so it won't happen randomly - I believe brain:performAction() will still work if called from onThink().
Then the monster will walk and pause as usual, not so mechanically, and it will only turn when you tell it to.
And also disable attacks for NPCs, so that no default behavior will actually do those.

A more complicated solution for turning, would be to disable turning in a direction that would put the monster farther away from the target.
But let it turn if that turn faces in a direction that gets it closer to the target.
So if the target is northeast in an open space, north and east are valid facings, and what path the monster takes to get to the target is more flexible.
- This way gets complicated if the monster can end up running into anything it shouldn't, like the party.
User avatar
The_Morlock
Posts: 24
Joined: Sun Jun 02, 2024 6:15 am

Re: NPC (Custom Brain, or No Brain)

Post by The_Morlock »

DaggorathMaster wrote: Tue Oct 08, 2024 2:39 am You could disable turning, so it won't happen randomly - I believe brain:performAction() will still work if called from onThink().
Then the monster will walk and pause as usual, not so mechanically, and it will only turn when you tell it to.
And also disable attacks for NPCs, so that no default behavior will actually do those.

A more complicated solution for turning, would be to disable turning in a direction that would put the monster farther away from the target.
But let it turn if that turn faces in a direction that gets it closer to the target.
So if the target is northeast in an open space, north and east are valid facings, and what path the monster takes to get to the target is more flexible.
- This way gets complicated if the monster can end up running into anything it shouldn't, like the party.
I appreciate the reply.

I've attempted to use onThink(){...}, (minmay has suggested this) but struggled to get it formatted properly. I could not find a proper example to ensure that what I was doing was accurate and attempts at this have failed thus far.

Sadly, I've gone through many iterations and don't have the onThink script I had... but it was something similar to this:

Code: Select all

		{
			class = "GoromorgBrain", 
			name = "brain",
			sight = 0, -- Only want this NPC to do as instructed, no need to interact with anything else.
			morale = 100,
			seeInvisible = false, -- See Sight comment
			onThink(self)
				startGuarding() -- Thinking that this was the default action each time if no "if" logic was defined below.
				if PerformAct == Act1 then
					self:performAction("Act1")
				end
				if PerformAct == Act2 then
					self:performAction("Act2")
				end
				if PerformAct == Act3 then
					self:performAction("Act3")
				end
			end
		},
My first NPC script is like this:

Code: Select all

function Act1()
	local monster = NPC1
	monster.brain:seek(3,16)
end

function Act2()
	local monster = NPC1
	monster.brain:seek(3,17)
end

function Act3()
	local monster = NPC1
	monster.brain:seek(3,24)
end
There are other non-NPC-related commands like playing sounds and showing text and dialogue. The idea is after each monster.brain command, the NPC should wait for the next trigger, lever, party to arrive, etc. until it is commanded to do something else.

My initial thought was to have all this within the script entity I am using rather than telling the NPC to do x when x is called from the script antity, but if this is what is needed, then OK. For instance, I have a series of delayedCalls (delayedCall(self.go.id, 0, "theraMove", "turnRight")) that told the NPC to turn, move forward, etc. However, this was with the brain turned off so doing something like moving more than once space away from the NPC was rather annoying since I could not tell it to monster.brain:goTo("floor_trigger_1") since the GoTo, Seek, etc. commands aren't available without the monster brain.

I appreciate the effort to assist.
My Project(s): Grimstone Keep, a reimagining of the 1995 Interplay game, Stonekeep.
Discord: https://discord.gg/AfZVYHc8JX
Patreon: https://patreon.com/GrimstoneKeep
User avatar
DaggorathMaster
Posts: 53
Joined: Thu Sep 08, 2022 7:29 pm

Re: NPC (Custom Brain, or No Brain)

Post by DaggorathMaster »

Another possibility for forcing the monster to stay still, is in the monster's def file, add an action:

Code: Select all

{
	class = “MonsterAction”,
	name = “stayStill”,
	animation = “idle”,
},
That uses the animation of "idle", but skips the hardcoded logic for brain:wait() and lets you return true to stop the brain from trying other actions.

Then something like:

Code: Select all

if math.random() < 0.25 then
        self:performAction ("stayStill")
        return true -- Important.
elseif PerformAct == ...
User avatar
The_Morlock
Posts: 24
Joined: Sun Jun 02, 2024 6:15 am

Re: NPC (Custom Brain, or No Brain)

Post by The_Morlock »

DaggorathMaster wrote: Wed Oct 09, 2024 3:49 am Another possibility for forcing the monster to stay still, is in the monster's def file, add an action:

Code: Select all

{
	class = “MonsterAction”,
	name = “stayStill”,
	animation = “idle”,
},
That uses the animation of "idle", but skips the hardcoded logic for brain:wait() and lets you return true to stop the brain from trying other actions.

Then something like:

Code: Select all

if math.random() < 0.25 then
        self:performAction ("stayStill")
        return true -- Important.
elseif PerformAct == ...
Thank you for that.

What am I missing? This is what I have and the editor is informing:
unexpected symbol near ','
If I remove the , then I get:
unexpected symbol near '}'
I am not seeing what I am missing, syntax-wise.

Code: Select all

		{
			class = "MonsterAction",
			name = "stayStill",
			animation = "idle",
		},
		{
			class = "GoromorgBrain",
			name = "brain",
			sight = 0,
			morale = 100,
			seeInvisible = false,
			onThink = function(self)
				if math.random() < 0.25 then
					self:performAction ("stayStill")
					return true -- Important.
				elseif PerformAct == theraA1A then
					self.performAction("theraA1A")
				elseif PerformAct == theraA1B then
					self.performAction("theraA1B")
				elseif PerformAct == theraA1C then
					self.performAction("theraA1C")
				elseif PerformAct == theraA1D then
					self.performAction("theraA1D")
				elseif PerformAct == theraA1E then
					self.performAction("theraA1E")
				elseif PerformAct == theraA1F then
					self.performAction("theraA1F")
				end,
		},
My Project(s): Grimstone Keep, a reimagining of the 1995 Interplay game, Stonekeep.
Discord: https://discord.gg/AfZVYHc8JX
Patreon: https://patreon.com/GrimstoneKeep
User avatar
DaggorathMaster
Posts: 53
Joined: Thu Sep 08, 2022 7:29 pm

Re: NPC (Custom Brain, or No Brain)

Post by DaggorathMaster »

You need one more "end" (without a comma).

The first "end" finishes your "if elseif" statement.
The second one finishes the function.
- The first one has no comma, the second one does.

Code: Select all

				elseif PerformAct == theraA1F then
					self.performAction("theraA1F")
				end,
		},
Should be:

Code: Select all

				elseif PerformAct == theraA1F then
					self.performAction("theraA1F")
				end
			end,
		},
User avatar
The_Morlock
Posts: 24
Joined: Sun Jun 02, 2024 6:15 am

Re: NPC (Custom Brain, or No Brain)

Post by The_Morlock »

DaggorathMaster wrote: Fri Oct 11, 2024 6:25 pm You need one more "end" (without a comma).

The first "end" finishes your "if elseif" statement.
The second one finishes the function.
- The first one has no comma, the second one does.

Code: Select all

				elseif PerformAct == theraA1F then
					self.performAction("theraA1F")
				end,
		},
Should be:

Code: Select all

				elseif PerformAct == theraA1F then
					self.performAction("theraA1F")
				end
			end,
		},
You're right, I miscounted the end's. I'll give this a go and report back. Thank you!
My Project(s): Grimstone Keep, a reimagining of the 1995 Interplay game, Stonekeep.
Discord: https://discord.gg/AfZVYHc8JX
Patreon: https://patreon.com/GrimstoneKeep
User avatar
The_Morlock
Posts: 24
Joined: Sun Jun 02, 2024 6:15 am

Re: NPC (Custom Brain, or No Brain)

Post by The_Morlock »

DaggorathMaster wrote: Wed Oct 09, 2024 3:49 am Another possibility for forcing the monster to stay still, is in the monster's def file, add an action:

Code: Select all

{
	class = “MonsterAction”,
	name = “stayStill”,
	animation = “idle”,
},
That uses the animation of "idle", but skips the hardcoded logic for brain:wait() and lets you return true to stop the brain from trying other actions.

Then something like:

Code: Select all

if math.random() < 0.25 then
        self:performAction ("stayStill")
        return true -- Important.
elseif PerformAct == ...
I appreciate your patience with me on this... I get a "bad self" error with the below, do the self.performAction() statements only refer to named components within the monster defineObject? I assume this is the error when it the editor can't find the named action within the defineObject:

Code: Select all

		{
			class = "GoromorgBrain",
			name = "brain",
			sight = 0,
			morale = 100,
			seeInvisible = false,
			onThink = function(self)
				if math.random() < 0.25 then
					self:performAction ("stayStill")
					return true -- Important.
				-- Below events within Level 1 "NPCScript1" script object.
				elseif PerformAct == NPCA1A then
					self.performAction("NPCA1A")
				elseif PerformAct == NPCA1B then
					self.performAction("NPCA1B")
				elseif PerformAct == NPCA1C then
					self.performAction("NPCA1C")
				elseif PerformAct == NPCA1D then
					self.performAction("NPCA1D")
				elseif PerformAct == NPCA1E then
					self.performAction("NPCA1E")
				elseif PerformAct == NPCA2 then
					self.performAction("NPCA2")
				elseif PerformAct == NPCA3 then
					self.performAction("NPCA3")
				elseif PerformAct == NPCA4 then
					self.performAction("NPCA4")
				end
			end,
		},
Each of the self.performAction calls are functions I've created within a script object within the dungeon. Each function handles a specific set of actions that I want the NPC, via its brain, to follow. If there is a better way to do this, I am eager to learn how!

An example of one of the acts:

Code: Select all

function NPCA1A()
	local monster = NPC_1
--	Non Monster Brain
--		delayedCall(self.go.id, 0, "NPCMove", "Forward")
--		delayedCall(self.go.id, 1, "NPCMove", "Forward")
--	Monster Brain
		monster.brain:changeAltitude(-1) --Works with Monster Brain
		monster.brain:seek(3,16)
	playSound("NPCDialogue1")
	local text = "Wait! We must be careful!"
	local duration = 3
	GTKGui.Basic.showInfoMessage(text, duration)
	actDone = "NPCA1A"
end
I was using monster commands without a brain as the NPC would wait once the command was done (hence the delayedCall commands). I found that there are several monster brain commands that are not in the non-brain monster command list (missing non-brain seek, goTo, etc.). This is where I found issues trying to control the monster brain, etc.

Thank you!
My Project(s): Grimstone Keep, a reimagining of the 1995 Interplay game, Stonekeep.
Discord: https://discord.gg/AfZVYHc8JX
Patreon: https://patreon.com/GrimstoneKeep
User avatar
DaggorathMaster
Posts: 53
Joined: Thu Sep 08, 2022 7:29 pm

Re: NPC (Custom Brain, or No Brain)

Post by DaggorathMaster »

self.performAction() needs to use a colon, not a dot
self:performAction()

That should fix up the "bad self" errors.
(Although that is not the only reason that error can happen.)

Generally, if there are more than one of something, like monsters, champions, items, they use a colon to call functions.
But it is also true of the party, which there is one of.

Dungeon, on the other hand, and a few other "overall" types use dot.
e.g., Dungeon.getMaxLevels(), GameMode.getStatistic ("play_time")

If you get as far as doing any object oriented programming,
a "class" works one way, while "objects" created by a class work another way.
But Lua's object oriented programming is not the same as other languages.
User avatar
The_Morlock
Posts: 24
Joined: Sun Jun 02, 2024 6:15 am

Re: NPC (Custom Brain, or No Brain)

Post by The_Morlock »

DaggorathMaster wrote: Mon Nov 04, 2024 3:26 pm If you get as far as doing any object oriented programming,
a "class" works one way, while "objects" created by a class work another way.
But Lua's object oriented programming is not the same as other languages.
Thanks for the reply, that is kinda where some things are throwing me for a loop. Some things I was expecting to work one way, end up not working that way.

I had started to work on other things since this (not giving up, just knew I could start other areas and come back to this). Let me get back into the NPC and I'll see how far I get and give what you provided another go.
My Project(s): Grimstone Keep, a reimagining of the 1995 Interplay game, Stonekeep.
Discord: https://discord.gg/AfZVYHc8JX
Patreon: https://patreon.com/GrimstoneKeep
Post Reply