Sittable chairs - help with scripting [MOSTLY SOLVED]

I’ve been struggling long and hard with this, and now I see no other way than to ask for help here on the forums. In all my modules, and especially the one I’m working on now, I’m using sittable chairs. At the moment I’m using these scripts and placeables:

https://neverwintervault.org/project/nwn2/script/sit-objects-v176

I know most people keep talking about Kemo custom chairs:

https://neverwintervault.org/project/nwn2/script/kemo-custom-chairs

Using “To Sit on Objects” is pretty straight forward and easy, that’s why I’ve always preferred using that one. The problem is that it only seems to work about 50 % of the time which is getting me really frustrated. On my latest module I’m working on right now, this really needs to work, as I have conversations between sitting people. Ok, why don’t I just use Kemo Custom Chairs then? Well, it uses a GUI for the PC to click and choose how to sit, and I don’t want that. I need my NPCs to sit down when the PC is entering the room (an Inn for example) and I don’t quite know how to modify the scripts in Kemo Custom Chairs to ignore the GUI. I’m not sure that Kemo will work better than Patcha’s To Sit On Objects but I’ve used other Kemo animations in my modules that have been really good so…

If any of you script wizards out there can help with this I would be grateful. This whole thing is wearing me down a bit. I really thought I had got it working with To Sit On Objects, but when I, a few days ago, did some longer playtesting on my module, to see how it felt, up 'till the point where I’m at right now in the development, I got quite discouraged when I saw that once again the characters didn’t sit down when the PC entered one of the inns.

I just make the chairs walkable and run one of the sitting animations in the heartbeat script.

Well, Patcha’s sittable chairs are all walkable and they all have a this script attached OnUsed of the chairs (the chairs also have variables with autofit and degree):

//::///////////////////////////////////////////////
//:: OnUse: Sit
//:: pat_sitted
//:://////////////////////////////////////////////
/*
Simple script to make PCs sit on a placeable
*/
//:://////////////////////////////////////////////
//:: Created By: Patcha
//:: Created On: 2006-12-08
//:: v1.76 By: Patcha
//:: v1.73 On: 2007-06-15
//:: dates: aaaa-mm-gg
//:://////////////////////////////////////////////

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 main()
{
object oChair = OBJECT_SELF;
object oSitter = GetLastUsedBy();
object oLastSitter = GetLocalObject(oChair, "lastsitted");
string sChair = GetTag(oChair);
string sAutofit = GetLocalString(oChair, "autofit");
int iHeading  = GetLocalInt(oChair, "degree");
int iPC_size  = GetLocalInt(oChair, "size");
//Assign the heading degrees
location lChair_o = GetLocation(oChair);
location lChair = Location(GetArea(oChair), GetPositionFromLocation(lChair_o), GetNormalizedDirection(GetFacingFromLocation(lChair_o) + iHeading));

//Check if seat is free
if(GetDistanceBetween(oLastSitter, oChair) == 0.0f && GetArea(oLastSitter) == GetArea(oChair))
{
	SetLocalInt(oChair, "taken", 1);
	SpeakOneLinerConversation("", OBJECT_INVALID, TALKVOLUME_WHISPER);
}
else //if seat is free...
{
	SetLocalInt(oChair, "taken", 0);

	//Check for Character Race with original Creature Size
	switch (iPC_size)
	{
	case 0:
		//Check for Character Race with original Creature Size
		if(	((GetRacialType(oSitter) == RACIAL_TYPE_ELF) && (GetCreatureSize(oSitter) == CREATURE_SIZE_MEDIUM)) ||
			((GetRacialType(oSitter) == RACIAL_TYPE_HALFELF) && (GetCreatureSize(oSitter) == CREATURE_SIZE_MEDIUM)) ||
			((GetRacialType(oSitter) == RACIAL_TYPE_HALFORC) && (GetCreatureSize(oSitter) == CREATURE_SIZE_MEDIUM)) ||
			((GetRacialType(oSitter) == RACIAL_TYPE_HUMAN) && (GetCreatureSize(oSitter) == CREATURE_SIZE_MEDIUM)) ||
			((GetSubRace(oSitter) == RACIAL_SUBTYPE_AASIMAR) && (GetCreatureSize(oSitter) == CREATURE_SIZE_MEDIUM)) ||
			((GetSubRace(oSitter) == RACIAL_SUBTYPE_TIEFLING) && (GetCreatureSize(oSitter) == CREATURE_SIZE_MEDIUM)))
		{
			if(GetIsObjectValid(oChair) && GetIsObjectValid(oSitter))
			{
				AssignCommand(oSitter, ActionJumpToLocation(lChair));
				AssignCommand(oSitter, ActionPlayCustomAnimation(oSitter, "sitidle", 1));
				SetLocalObject(oChair, "lastsitted", oSitter);
			}
		}
		else
		{
			if(sAutofit != "")
			{
				AssignCommand(oChair, SetIsDestroyable(TRUE, FALSE, FALSE));
				AssignCommand(oSitter, DestroyObject(oChair));
				oChair = CreateObject(OBJECT_TYPE_PLACEABLE, "pat_low_" + sAutofit, lChair_o, FALSE, sChair);
				if(!(GetIsObjectValid(oChair)))
					oChair = CreateObject(OBJECT_TYPE_PLACEABLE, "pat_low_stool01", lChair_o, FALSE, sChair);
				AssignCommand(oSitter, ActionJumpToLocation(lChair));
				AssignCommand(oSitter, ActionPlayCustomAnimation(oSitter, "sitidle", 1));
				SetLocalString(oChair, "autofit", sAutofit);
				SetLocalInt(oChair, "degree", iHeading);
				SetLocalInt(oChair, "size", 1);
				SetLocalObject(oChair, "lastsitted", oSitter);
			}
			else
			SpeakOneLinerConversation("", OBJECT_INVALID, TALKVOLUME_WHISPER);
		}
		break;

	case 1:
		//Check for Character Race with original Creature Size
		if(	((GetRacialType(oSitter) == RACIAL_TYPE_DWARF) && (GetCreatureSize(oSitter) == CREATURE_SIZE_MEDIUM)) ||
			((GetRacialType(oSitter) == RACIAL_TYPE_GNOME) && (GetCreatureSize(oSitter) == CREATURE_SIZE_SMALL)) ||
			((GetRacialType(oSitter) == RACIAL_TYPE_HALFLING) && (GetCreatureSize(oSitter) == CREATURE_SIZE_SMALL)))
		{
			if(GetIsObjectValid(oChair) && GetIsObjectValid(oSitter))
			{
				AssignCommand(oSitter, ActionJumpToLocation(lChair));
				AssignCommand(oSitter, ActionPlayCustomAnimation(oSitter, "sitidle", 1));
				SetLocalObject(oChair, "lastsitted", oSitter);
			}
		}
		else
		{
			if(sAutofit != "")
			{
				AssignCommand(oChair, SetIsDestroyable(TRUE, FALSE, FALSE));
				AssignCommand(oSitter, DestroyObject(oChair));
				oChair = CreateObject(OBJECT_TYPE_PLACEABLE, "pat_mid_" + sAutofit, lChair_o, FALSE, sChair);
				if(!(GetIsObjectValid(oChair)))
					oChair = CreateObject(OBJECT_TYPE_PLACEABLE, "pat_mid_stool01", lChair_o, FALSE, sChair);
				AssignCommand(oSitter, ActionJumpToLocation(lChair));
				AssignCommand(oSitter, ActionPlayCustomAnimation(oSitter, "sitidle", 1));
				SetLocalString(oChair, "autofit", sAutofit);
				SetLocalInt(oChair, "degree", iHeading);
				SetLocalInt(oChair, "size", 0);
				SetLocalObject(oChair, "lastsitted", oSitter);
			}
			else
			SpeakOneLinerConversation("", OBJECT_INVALID, TALKVOLUME_WHISPER);
		}
		break;

	default:
		//Character with no original Race and/or Creature size
		SpeakOneLinerConversation("", OBJECT_INVALID, TALKVOLUME_WHISPER);
		break;
	}
}
}

@rjshae Do you use the sitting animations on the heartbeat script of the area or on the characters that are supposed to sit?

EDIT: I remember trying in some case to make the table walkable and that helped in one instance but in another inn I’m using it was pointless doing it like that.

Yes, I struggled with this one for a while … Some things to note …

  1. Make sure there are no other non-walkables near the chairs. E.g. Chandeliers above chairs.
  2. Make sure no other creature is competing for the same chair.
  3. Make sure you do not ClearAllActions at any time on sitting creature.

I have written these scripts into my own module, so if you want to look closer, then take a look there. However, be warned, there are considerable number of checks and alterations due to the way sitting NPCs need careful attention.

However, if you check those three points above, then that may catch the issue.

Cheers, Lance.

Thanks for the reply, Lance. If there are other chairs that are not walkable near, they may also cause a problem?

To be frank, I had the same great amount of issues getting this to work … and I don’t think it’s a fault of the script as such, but may be down to what else we are doing with the NPC in question.

In the end, I took out most of the original code, simplified it, and then made it probably a little more complex with other checks to ensure my NPCs did actually seat more often than not. :wink:

In the end, i think it was point 1 above that caused me the most failures, even with my won alterations, as I often forget to make sure chandeliers did not cause a walk mesh to the seat issue.

Cheers, Lance.

Check out this list of checks …

I use ClearAllActions before they are to sit. Is this wrong? They are just standing in the area before the PC is entering and I run this script OnClientEnter of the area (because if they are sitting and the PC leaves the area and then comes back I teleport them to a waypoint behind the chairs and then they jump to their chairs again and sit down when the PC is reentering the area):

const string sDEST_NPC      		= "npc1wp";
const string sDEST_NPC2      		= "npc2wp";
const string sDEST_NPC3      		= "npc3wp";
const string sDEST_NPC4      		= "npc4wp";
const string sDEST_NPC5     		= "npc5wp";
const string sDEST_NPC6      		= "npc6wp";
const string sDEST_NPC7      		= "npc7wp";
const string sDEST_NPC8      		= "npc8wp";
const string sDEST_NPC9      		= "npc9wp";



void transition(object oSitter, string sDest)
{    
    AssignCommand(oSitter, ClearAllActions());
    AssignCommand(oSitter, JumpToObject(GetWaypointByTag(sDest)));
}


void main()
{



object oNPC = GetObjectByTag("commhum2");
object oNPC2 = GetObjectByTag("commelf1");
object oNPC3 = GetObjectByTag("commhum1");
object oNPC4 = GetObjectByTag("commelf2");
object oNPC5 = GetObjectByTag("commhum4");
object oNPC6 = GetObjectByTag("commelf4");
object oNPC7 = GetObjectByTag("commhum3");
object oNPC8 = GetObjectByTag("chalfelf1");
object oNPC9 = GetObjectByTag("commelf3");

	if(GetGlobalInt("innsitting"))
	{

 	transition(oNPC, sDEST_NPC);
 	transition(oNPC2, sDEST_NPC2);
 	transition(oNPC3, sDEST_NPC3);
 	transition(oNPC4, sDEST_NPC4);
 	transition(oNPC5, sDEST_NPC5);
 	transition(oNPC6, sDEST_NPC6);
 	transition(oNPC7, sDEST_NPC7);
 	transition(oNPC8, sDEST_NPC8);
 	transition(oNPC9, sDEST_NPC9);
	}


SetGlobalInt("innsitting",1);

AssignCommand(oNPC, ActionInteractObject(GetObjectByTag("sitbench11")));
AssignCommand(oNPC2, ActionInteractObject(GetObjectByTag("sitbench2")));
AssignCommand(oNPC3, ActionInteractObject(GetObjectByTag("sitbench5")));
AssignCommand(oNPC4, ActionInteractObject(GetObjectByTag("sitbench10")));
AssignCommand(oNPC5, ActionInteractObject(GetObjectByTag("chairy1")));
AssignCommand(oNPC6, ActionInteractObject(GetObjectByTag("chairy2")));
AssignCommand(oNPC7, ActionInteractObject(GetObjectByTag("chairy3")));
AssignCommand(oNPC8, ActionInteractObject(GetObjectByTag("chairy4")));
AssignCommand(oNPC9, ActionInteractObject(GetObjectByTag("chairy5")));


}

Edit: Gaah, that’s a lot of checks on your list, Lance.

Running on a character means fewer object lookups during the heartbeat cycle. I just give them all the same script, and randomize the animation once per heartbeat loop.

I was trying to put together a version of my script to show you, but for some strange reason I just cannot find where a function is … even checking every include, and yet it does compile, so I have no idea where that is at the moment.

Hopefully I will find it and have something for you soon.

Cheers, Lance.

OK, I found the functions in the official stuff, and you have them at the top of your script, so just grab them from there … NB: This script WILL now compile for you, as I have removed/changed some of my own references, but you will need to look over it and test it for your own situation … but as I say, there may be a lot more to consider at some point.

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));
        }

The GetNormalizedDirection is already in Patcha’s script so I doubt I will have to add that again. You mean that I could/should add the SitDown function? Maybe the SetBumpState could be something that makes a difference…

Well, the SitDown was my own function, which incorporated some other ideas from those mentioned, to help alleviate the issues that were occurring. How much any one part does to “fix” the issue, I cannot say … although, as I say above, my main issue (after more trial and error) was simply that I had a walkmesh issue due to a chandelier above the seat that was being used by the NPC that I had not made walkable!

EDIT: My original script here (before edits) also took into account if a player wanted their PC to use the chair as well as any scripted NPCs.

EDIT: I have a function in an on enter script that checks if an NPC is a “sitter”, which then runs the script to make them try to sit via this script and interaction call.

GetNormalizedDirection is from x0_i0_position.

Better go sleep now. I’ve tried different things now but nothing seems to work at the moment (tried making all tables walkable, there are two chandelliers that are also walkable, tried setting Dynamic Collision to TRUE but that did nothing…)

Maybe I should try a heartbeat thing or…or something…

When I hit a wall like this, I remove all other code and just go back to a basic script to ensure that that works by itself at least. Then I will start re-applying surrounding code.

One other thing I would add is to avoid trying to apply an action to a creature that has just jumped. It can get messy. To test this stage, just try to make an NPC sit on a chair within an area they are already in. It may be that you will have to use duplicated NPCs to give the impression they had moved and the do away with them again.

Keep us updated with your progress all the same.

Cheers, Lance.

For a pub I set NX2_NO_ORIENT_ON_DIALOG=1 on the seated customers and ran this heartbeat script:

// ms_phlan_pub_hb
/*
	This is a custom heartbeat script adopted from
	3030_fplace_dwarf_hb in the original campaign.
	It is used by seated patrons of a pub.
*/
// 22apr12 RJH

#include "ginc_misc"
#include "ginc_math"
#include "ginc_wp"

const string BARKSTRING_ID = "mnsea_bark_phlan_pub";

void PlayCustomLoopingAnimation(object oObject, string sAnimationName)
{
	PlayCustomAnimation(oObject, sAnimationName, 1);
}

void PlayCustomOneShotAnimation(object oObject, string sAnimationName)
{
	PlayCustomAnimation(oObject, sAnimationName, 0);
}

void main()
{
	if (GetAILevel(OBJECT_SELF) == AI_LEVEL_VERY_LOW)
	{
		return;
	}
	
	object oActor = OBJECT_SELF;
	
	if (GetLocalInt(oActor, "SetDefaultAnimation") == 0)
	{
		AssignCommand(oActor, PlayCustomLoopingAnimation(oActor, "sitidle"));
		SetLocalInt(oActor, "SetDefaultAnimation", 1);
	}
	
	int nRandom = RandomIntBetween(1, 20);
	
	switch(nRandom)
	{
		case 1:
		case 2:
		case 3:
			PlayCustomLoopingAnimation(oActor, "sitdrink");
			if (RandomIntBetween(1, 10) == 1)
			{
				ActionSpeakOneLiner(BARKSTRING_ID);
			}
			break;
		case 4:
			PlayCustomLoopingAnimation(oActor, "sittalk01");
			if (RandomIntBetween(1, 10) == 1)
			{
				ActionSpeakOneLiner(BARKSTRING_ID);
			}
			break;
		case 5:
			PlayCustomLoopingAnimation(oActor, "sittalk02");
			if (RandomIntBetween(1, 10) == 1)
			{
				ActionSpeakOneLiner(BARKSTRING_ID);
			}
			break;
		case 6:
		case 7:
			PlayCustomLoopingAnimation(oActor, "sitfidget");
			if (RandomIntBetween(1, 10) == 1)
			{
				ActionSpeakOneLiner(BARKSTRING_ID);
			}
			break;
		case 8:
			if (RandomIntBetween(1, 10) == 1)
			{
				ActionSpeakOneLiner(BARKSTRING_ID);
			}
			break;
		case 9:
			PlayCustomLoopingAnimation(oActor, "sitdrinkidle");
			if (RandomIntBetween(1, 10) == 1)
			{
				ActionSpeakOneLiner(BARKSTRING_ID);
			}
			break;
		default:
			PlayCustomLoopingAnimation(oActor, "sitdrinkidle");
			break;
	}
}

But I’m spawning the patrons on PC entry and unspawning them on exit, so the heartbeat scripts aren’t running constantly.

Same here. Mostly.

Ok. I’ll try some more when I get home from work tonight. I’m not spawning the NPCs as of right now when the PC enters the area. Maybe that’s a better way to go about it. I was also thinking that I should maybe remove some of the scripts on the NPCs. Most of them are just sitting down and not doing anything. There are no fights in the area either. And if I’m spawning and respawning them I wouldn’t need the jump script they would just be spawned on the waypoints.

I’ve tried for about an hour now…Nothing seems to work deep sigh. Tried with taking all the NPCs away and spawning them on the waypoints when the PC walks in and then destroy them when he walks out. The characters still wouldn’t sit. Or, as usual, some of them or none of them or a few of them would.
I also tried to take away all the NPCs scripts except the OnSpawn and OnConversation scripts. No effect doing that whatsoever.
When exiting the area and reentering another bug was created. The NPCs were not, for some strange reason, destroyed when moving out of the area, so when reentering there was a double version of each NPC.

So I went back to an earlier version of the mod without all the tinkering done. I have tried to make every placeable, candles and chandaliers around the NPCs walkable to help with the walkmesh (if that is indeed the problem). With this earlier version the characters still jump, but as it didn’t help with them not jumping…urrghh. I have also tried with spawning them and do ClearAllActions on them, again no effect…

The only thing I haven’t tried is the heartbeat script. Don’t quite know how to implement that. What I would like to try (if possible) is to maybe have a heartbeat script of the area and check if the 9 NPCs that I have are sitting and if the do not, to try and sit again. Is there perhaps a way to write such a code? Is there a function for checking if the characters are in an animation, and if not try to sit, start the sitting animation loop?

I’m quite tired right now after a full days work and trying this without any kind progress. I think I’ll take a break for the moment.

I have to say, I don’t fully understand what’s happening in rjshae’s heartbeat script. There are a lot of barkstrings and random things the characters do and different animations but…I can’t see how that code could help me out.

Hi andgalf,

I can appreciate the tiredness, so take it easy.

I am trying to fix some ankheg AI at the moment, with results as annoying as yours by the sounds of it … stuff just not firing when it should or double firing and confusing everything.

Anyway, when I have that sorted, I will try to knock together a basic area with chairs and a sit script based on what I have. It is a bit more difficult than it sounds as I have a lot of tailored scripts around the OC stuff, which may be making mine work more easily than yours, which I forgot I did.

Hopefully, sooner than later.

Cheers, Lance.

Hi Lance,

Thank you so much for trying to help. I tried something else now but that didn’t help either.

I’m looking at the script on the chairs again and it has
AssignCommand(oSitter, ActionJumpToLocation(lChair));
Since you said that you shouldn’t run ClearAllActions after a jump, I just guessed that perhaps
it is the jump that makes the script or animation not firing correctly. Will continue to experiment a bit.

Hmmm. That was interesting. When I took away the ActionJumpToLocation something happened alright. Now I entered and re-entered the area quite a few times, and EVERY time the characters sit down, just not on the chairs but beside them. I guess you have to have the ActionJumpToLocation to really have the NPCs be at the chairs. Still, the animations worked now every time, so it seems it has something to do with the chairs themselves perhaps, that the game doesn’t like the NPCs being that close to the objects…or something. Just speculating.

EDIT: I just realized I hadn’t tried your script yet, Lance. Will try and put that on the chairs and see what happens.