NPCs sitting on chairs properly - the eternal conundrum

Hello again,

I’m using a couple of scripts to maintain a sitting NPC & the approach seems to work - i.e. he sits in place and does not move or rotate when clicked by the PC. One script is On Heartbeat and the other is On Conversation. It’s not an elegant/“correct” way of doing this, but has the intended outcome…for the most part.

On Heartbeat:

// NPC sits in chair

void main()

{

ActionPlayAnimation(ANIMATION_LOOPING_SIT_CHAIR, 1.0f, 100.0f);

	}

On Conversation (doesn’t do anything per se, just seems to keep the NPC non-responsive/frozen in seated position without rotating or moving when clicked on):

// Put this in "On Conversation" to keep NPC in seated position / non-responsive

void main()
{

object oPC = GetClickingObject();

if (!GetIsPC(oPC)) return;

}

A couple of issues with this approach (and I’m sure there are more):

  • Issue #1: Dialogue - The NPC cannot respond or speak (even when trying a speak string from an external trigger, it just doesn’t appear). Workaround: I set up an invisible ipoint (duplicate name as the NPC), positioned slightly over his head, and have that trigger a speak string. Not ideal but a workaround that looks like he’s speaking.

  • Issue #2: Combat - The NPC keeps trying to sit (due to heartbeat of course) even when combat is engaged. I tested this by summoning a foe & watching the NPC and hostile creature duke it out. The AI for the NPC (caster) was pretty cool in terms of spells cast/etc. but he kept initiating a sitting animation even mid-combat. Another issue is if attacked by the PC, the NPC does not defend themselves or attack back - maybe more of a faction issue?

I am a bad scripter to say the least, i usually piece together solutions through Script Generator, customizing the vanilla scripts, or using other people’s existing approaches/suggestions. Any insight on how I can address the above 2 issues to minimize unintended wonkiness would be much appreciated!

Particularly if there’s a way to adjust the heartbeat script to either suspend sitting loop while in combat & eliminate/ignore the sitting loop after or having the NPC return to a waypoint (the chair’s location) and continue the previous sitting heartbeat?

Have you looked at the Scripting FAQs?

TR

Thanks for the link!

I have looked at that tutorial/FAQ - and many, many others - but maybe I’m missing where this topic or question is covered in those docs. Can you point me to it?

The only section that mentions NPCs sitting in chairs mentions using On Spawn and the following code:

ActionSit( GetNearestObjectByTag( "Chair"));

This only works in NWN1, not NWN2 (just tested it)

Dang one of the few (or so I’ve been told) NwN scripts that don’t work in NwN 2.

TR

@Lance_Botelle is really the master of this. He helped me with scripting for sitting on chairs on my third module (people sit down in a tavern) and it works without failing. @4760 also had some minor input, but for it to be really without fail, Lance did the majority of the work. It has a lot to do with (and I sometimes hate NWN2 for this) with delays and timing. So you should ask his advice on this, I think.

I could pull out the scripts I use for that tavern if you want. I don’t think, but I don’t quite remember, that I used any heartbeat scripts, but maybe I did…

1 Like

Just checked and there are no hearbeat scripts in this area, either on the area or the NPCs.

Here’s Lance’s script that’s on the OnEnter of the area:

#include "ginc_math"
#include "ginc_cutscene"

///////////////////////////////////////////////////////////////////////////////////////////////////
// SET "TRYSIT" VARIABLE TO 1 ON NPC AT BUILD TIME OR VIA SCRIPT TO MAKE THEM KEEP ATTEMPTING TO SIT
// THIS FUNCTION CAUSES PROBLEMS WHEN ADDED TO ALB_FUNCTIONS
// NB: NPCS MAY STRUGGLE TO SIT WHEN NOT CLOSE TO A CHAIR DUE TO WALKMESH ISSUES !!!!!!!!!!!!!!!!!!!
///////////////////////////////////////////////////////////////////////////////////////////////////

void ForceNPCReseat(object oTarget) 
{	
	if(GetLocalInt(oTarget, "TRYSIT") == 0){return;} // SITTING HAS BEEN CANCELLED	
			
	object oCurrentChair = GetLocalObject(oTarget, "Seated");
		
	// SendMessageToPC(GetFirstPC(TRUE), GetName(oTarget) + " IS TRYING TO SIT"); // DEBUG			
	
	// FIND THE NEAREST AVAILABLE CHAIR (USE THE FIRST 8 CHARACTERS E.g. pat_mid_chair5)
	int iCount = 1; object oChair = GetNearestObject(OBJECT_TYPE_PLACEABLE, oTarget, iCount);
	
	while(oChair != OBJECT_INVALID)
	{	
		// FIND A CHAIR WITHOUT A CURRENT USER
		if(GetStringLeft(GetTag(oChair), 8) == "pat_mid_")
		{
			object oCurrentSeater = GetLocalObject(oChair, "LastSeated");
			
			// TRY IGNORING THE FACT THAT THE LAST SEATED CHAIR HAD AN OCCUPANT (IT WAS PROBABLY THEM ANYWAY PRIOR EXIT)
			if(oCurrentSeater == OBJECT_INVALID || oCurrentSeater == oTarget)
			{
				//SendMessageToPC(GetFirstPC(TRUE), GetName(oTarget) + " FOUND A CHAIR"); // DEBUG	
				break;
			}
		} 
			
		iCount = iCount + 1; oChair = GetNearestObject(OBJECT_TYPE_PLACEABLE, oTarget, iCount);	
	}			
	
	// MAKE TARGET USE THIS CHAIR
	SetLocalObject(oChair, "LastSeated", oTarget); 
	SetLocalObject(oTarget, "Seated", oChair);	
	AssignCommand(oTarget, ClearAllActions(TRUE));
	AssignCommand(oTarget, ActionInteractObject(oChair));	
}   

void fadetoblack(float fSpeed, float fFailsafe, int nColor)
{

	object oPCh = GetEnteringObject();

	if (IsFloatNearInt(fFailsafe, 0))
		fFailsafe = 15.0f;

	FadeToBlackParty(oPCh, 1, fSpeed, fFailsafe, nColor);

}

void main()
{

	object oCreature = GetEnteringObject();
	
	// ONLY RUN WHEN PLAYER ENTERS
	if(!GetIsPC(oCreature)){return;}
	
	fadetoblack(3.2,3.2,0);
	
	
	object oArea = GetArea(oCreature);
	
	object oNPC = GetFirstObjectInArea(oArea);
	
	while(oNPC != OBJECT_INVALID)
	{		
		// RESEAT NPCS
		if(GetObjectType(oNPC) == OBJECT_TYPE_CREATURE && GetLocalInt(oNPC, "TRYSIT") == 1)
		{			
			DelayCommand(0.5, ForceNPCReseat(oNPC));		
		}
		
		oNPC = GetNextObjectInArea(oArea);
	}
}

And here’s Lance’s script that’s on the OnUsed of the chairs:

void ActionPlayCustomAnimation(object oObject, string sAnimationName, int nLooping, float fSpeed = 1.0f)
{
PlayCustomAnimation(oObject, sAnimationName, nLooping, fSpeed);
}

// float GetNormalizedDirection(float fDirection):
// * This script returns a direction normalized to the range 0.0 - 360.0
// * Copyright (c) 2002 Floodgate Entertainment
// * Created By: Naomi Novik
// * Created On: 11/08/2002
float GetNormalizedDirection(float fDirection)
{
float fNewDir = fDirection;
while (fNewDir >= 360.0) {
	fNewDir -= 360.0;
}
while (fNewDir <= 0.0) {
	fNewDir += 360.0;
}

return fNewDir;
}

        void SitDown(object oSitter, object oChair)
        {		
        	AssignCommand(oSitter, SetFacing(GetNormalizedDirection(GetFacing(oChair)+180.00+GetLocalFloat(oChair,"rotate")) ));
        	AssignCommand(oSitter, ActionPlayCustomAnimation(OBJECT_SELF, "sitidle", 1, 1.0));
        	
        	// EXTRA CODE THANKS TO SHAUGHN & EGUINTIR
        	
        	//SetOrientOnDialog(oSitter,FALSE);
        	SetBumpState(oSitter,BUMPSTATE_UNBUMPABLE);	
        }

        void main()
        {
        	object oChair = OBJECT_SELF; 
        	object oSitter = GetLastUsedBy();
            object oCheckSitter = GetLocalObject(oChair, "LastSeated");	
        	
        	//SendMessageToAllPCs("Chair used by " + GetName(oSitter));
        	    
            // RETRIEVE VARS ATTACHED TO CHAIR	
        	int iHeading  = GetLocalInt(oChair, "degree");		
        	string sName = GetFirstName(oChair); 
            
        	//Assign the heading degrees
        	location lChair_o = GetLocation(oChair);
            location lChair = Location(GetArea(oChair), GetPositionFromLocation(lChair_o), GetNormalizedDirection(GetFacingFromLocation(lChair_o) + iHeading));
        	
            // CHECK THE SEAT IS FREE
        	if(GetLocalObject(oChair, "LastSeated") != OBJECT_INVALID && oSitter != oCheckSitter)
            {		
        		FloatingTextStringOnCreature("<<< The seat is already being used by somebody. >>>", oSitter, FALSE);		
        		return;	
            }
            
        	// SEAT THE USER & ASSOCIATE SEAT WITH NPC FOR RETURN SEAT	
        	if(oSitter != OBJECT_INVALID)
        	{	
        	SetLocalObject(oChair, "LastSeated", oSitter);
        	SetLocalObject(oSitter, "Seated", oChair);
        	}
        	
        	// JUST USE THE STANDARD CHAIRS
        	AssignCommand(oSitter, ActionMoveToLocation(lChair));
        	DelayCommand(2.0, SitDown(oSitter, oChair));
        }

I hope this can help you a bit @aeliop !
As you see in the description of the first script, you need the TRYSIT variable on the NPCs you want to sit down.

If you use these, then you can speak to the characters sitting down. No issues there. Just use the default script for conversation on the NPC.

I have not tested this on combat, but I believe they would quit sitting down if there was a fight.

I forgot to mention that the chairs you use need to have the tag “pat_mid” as the first part of the tag for the scripts to work. If you look at the code here you understand why:

// FIND A CHAIR WITHOUT A CURRENT USER
		if(GetStringLeft(GetTag(oChair), 8) == "pat_mid_")
2 Likes

I’m going to try these scripts today - thank you as always, @andgalf ! Keep you posted on results

1 Like

The scripts worked nicely, thanks @andgalf + @Lance_Botelle (I didn’t yet test combat situations, but will do that next).

But my test did bring up an additional question/challenge (and only a vague notion of how to address it).

When spoken to, the NPC will pivot in their seat to face the PC (which is okay). But it would be nice to have the NPC return to a “normal”, forward-seated position after being spoken to. I imagine this would involve an On Heartbeat checking for fDirection (as seen in the On Enter script) or something similar.

I may mess around with that to see if it works, but am definitely getting over my head here! Let me know if you have any ideas or suggestions.

2 Likes

Here’s a possibility:

/*
	Place on the character's OnHeartbeat event.
	Returns the character to their original facing after conversation.
*/

void main()
{
	object oSelf = OBJECT_SELF;
	
	if (!GetLocalInt(oSelf, "Initialize"))
	{
		SetLocalInt(oSelf, "Initialize", TRUE);
		SetLocalFloat(oSelf, "facing", GetFacing(oSelf));
	}

	float fFacing = GetLocalFloat(oSelf, "facing");	
		
	if (GetFacing(oSelf) != fFacing && !IsInConversation(oSelf))
	{
		AssignCommand(oSelf, SetFacing(fFacing));
	}

	ExecuteScript("nw_c2_default1", oSelf);
}
3 Likes

@aeliop Good that it worked for you. Yes I know about the turning of heads when speaking to a sitting NPC. I like that, but I can see how one would have liked the NPC to go back to facing the way they were, even if I ignored that in my third module. @travus scripts almost always work, at least those he has done for me, so I guess give it a try.

@travus @andgalf just tested and it worked. Thank you as always!

2 Likes