Monday, June 23, 2008

Scripting: Roster Companions and Sitting NPCs

I know I said I wasn't going to be working on Fate of a City for two weeks, but I've managed to steal some time in my hectic schedule (no, it's not a holiday, unfortunately) to work on a few things, from tweaks and bugfixes to NPC verisimilitude.

Little things being the operative word here, but I must admit, I do get a small of a kick out making those little touches that add a little bit of extra class. To that end, I've done some minor fiddling with the visual effects editor, which although didn't result in massive production, it has at least made one encounter a little bit nicer.

Two bugfixes were in regards to the handling of roster companions. In numerous generic scripts I've written, it is only the main PC who should be affected or interact with a specific object. I discovered that my previous solution to this problem was actually a little flaky, and unfortunately I was forced to resort to using GetFirstPC(), which basically ends any notion I had of trying to support cooperative multiplayer in Fate of a City.

The second bugfix, and this is rather a big flaw, I feel, is that the default ga_take_item does not function as it should when the item is in a rostered companion's inventory - namely gc_check_item return true, yet ga_take_item will not remove the object! Obviously, this is very significant if you have rostered companions, so I coded up a replacement version to deal with that instead.

// ga_take_item
/*
This takes an item from a player
sItemTag = This is the string name of the item's tag
nQuantity = The number of items (default is 1). -1 is all of the Player's items of that tag.
bAllPartyMembers = If set to 1 it gives the item to all PCs in party (MP only)
*/
// FAB 9/30
// MDiekmann 4/9/07 -- using GetFirst and NextFactionMember() instead of GetFirst and NextPC(), changed nAllPCs to bAllPartyMembers
// AmstradHero 22/06/08 -- Recoded it to go through Roster members instead of faction members

#include "nw_i0_plot"

void main(string sItemTag, int nQuantity, int bAllPartyMembers)
{

int nTotalItem;
object oPC = (GetPCSpeaker()==OBJECT_INVALID?OBJECT_SELF:GetPCSpeaker());
object oTarg;
object oItem; // Items in inventory

if ( nQuantity == 0 ) nQuantity = 1;

if ( bAllPartyMembers == 0 )
{
if ( nQuantity < 0 ) // Destroy all instances of the item
{
nTotalItem = GetNumItems( oPC,sItemTag );
TakeNumItems( oPC,sItemTag,nTotalItem );
}
else
{
TakeNumItems( oPC,sItemTag,nQuantity );
}
}
else // For companions
{
string sTarg = GetFirstRosterMember();
while ( sTarg != "" )
{
oTarg = GetObjectFromRosterName(sTarg);
if ( nQuantity < 0 ) // Destroy all instances of the item
{
nTotalItem = GetNumItems( oTarg,sItemTag );
TakeNumItems( oTarg,sItemTag,nTotalItem );
}
else
{
TakeNumItems( oTarg,sItemTag,nQuantity );
}
sTarg = GetNextRosterMember();
}
}

}
Note that I've removed the GetFirst/NextFactionMember calls entirely, so this will probably only function correctly if you have rostered companions only. I haven't experimented with all the different methods of adding people to your party, so I can't really comment further. All I know is that this is working for me.

I finally decided to bite the bullet and fiddle with making sitting NPC function in a reasonable manner, which took a bit of effort as I tweaked various things. I realise a lot of people have probably already implemented a similar system, but I thought I'd present mine here. The first was producing a heartbeat script for NPCs to have them sit and then randomly talk occasionally while staying seated.

//b_hb_sit
//Heartbeat script for creatures to simulate sitting and talking occasionally

//Function used to delay the playing of the sitidle animation to avoid "skipping"
void delayIdle()
{
PlayCustomAnimation(OBJECT_SELF, "sitidle", TRUE, 1.0);
}


void main()
{
//Determine if I want to sit randomly or if I want to talk
int nAnim = d2();
if (nAnim == 1)
{
PlayCustomAnimation(OBJECT_SELF, "sitidle", TRUE, 1.0);
}
else
{
//I'm not idling - grab a different animation
//These animations last for 5.33 seconds, hence the delay command to switch it to idle once they're done
nAnim = d3();
switch (nAnim)
{
case 1:
PlayCustomAnimation(OBJECT_SELF, "sitfidget", FALSE, 1.0);
DelayCommand(5.33, delayIdle());
break;

case 2:
PlayCustomAnimation(OBJECT_SELF, "sittalk01", FALSE, 1.0);
DelayCommand(5.33, delayIdle());
break;

case 3:
PlayCustomAnimation(OBJECT_SELF, "sittalk02", FALSE, 1.0);
DelayCommand(5.33, delayIdle());
break;
}

}
}
The delay function is necessary because you can't use DelayCommand() directly on PlayCustomAnimation(), and you want to make sure that you play "sitidle" immediately after any other sit animation finishes because otherwise the NPC may decide to stand up "in" their seat, which looks weird. You also can't loop the other commands because then you get a jump as it switches between them. I also steered away from the "sitdrink" and "siteat" animations because it looks a little silly when people are drink or eating without anything in their hand. If anyone has a good solution to this, I'd love to hear about it.

The second issue was making it so that anyone sitting down would not turn to face the PC when clicked on, which typically makes their legs or some other part of their body magically pass through the seat they are sitting on. To solve this problem, all that is needed is a simple one line script added as an action to each start node of their conversation. (I'm only using barkstrings, but it should work for larger conversations too because I set bLockOrientation to TRUE.)

//ga_stay_face
//Makes an NPC stay facing the same direction rather than turning to face the PC
void main()
{
SetFacing(GetFacing(OBJECT_SELF), TRUE);
}
So with those two scripts, voila! People will happily sit down, occasionally animating to talk to the person opposite, and will not turn to face the player for a conversation.

All that you need to do is to create a seat placeable, make sure it is not static and doesn't have dynamic collisions, and then place the person "inside" the seat. Basically the torso of the character will not move forward from where you place them in the toolset, only their legs will move forward when they go into a sitting pose. Also, if you're using benches (as is often the cases with taverns), you may need to create a walkmesh cutter to prevent PCs from walking through it - or just place more people on the bench! :-) Also make sure that you set your NPC's bump state to un-bumpable.

I'd like to thank weby and GrinningFool from the NWN2 IRC chat server for their help and input on the various issues I was having with some of the scripts I've mentioned.

Finally, congratulations to all the winners of the AME Golden Dragon Awards! They were all well-deserved accolades.

No comments: