Day/night city populace? Preferably for dummies?

I tried to use the day/night waypoints, but that’s very unsatisfactory. In theory, creatures should make their way to the “day” waypoint at dawn and to the “night” waypoint at dusk, but in practice, this only works sensibly within a single area; if you try to use it to cycle a population between street and house areas, it only works as long as the PC is there at the same time as the creature needing to move. (I think that if the PC is not there, the creature sort of doesn’t exist? so it doesn’t follow scripts?)

I think it can be done through a script attached to an area which creates and destroys creatures? Or something? Is there a preferred way to do this? (One that’s not too resource-heavy and preferably dumb-creator-proof?)

Have a look at this, see if this is something you might be interested in. It uses custom waypoints for npcs to follow and animate dependent upon variables and include day/night schedules
https://neverwintervault.org/project/nwn1/script/ultima-npc-scheduled-activities

1 Like

When PC is not in the area, creature AI is by default at AI_LEVEL_VERY_LOW, which causes the default heartbeat script (nw_c2_default1) to bail out immediately. You can add an extra check for walk waypoints condition to that script to circumvent this issue.

For additional / missing functionality, you also have this in x0_i0_walkway:

// GZ: 2003-09-03
// Since this wasnt implemented and we we don't have time for this either, I
// added this code to allow builders to react to NW_FLAG_SLEEPING_AT_NIGHT.
if(GetIsNight())
{
    if(GetSpawnInCondition(NW_FLAG_SLEEPING_AT_NIGHT))
    {
        string sScript = GetLocalString(GetModule(),"X2_S_SLEEP_AT_NIGHT_SCRIPT");
        if (sScript != "")
        {
            ExecuteScript(sScript,OBJECT_SELF);
        }
    }
}

This might allow you to plug into the waypoint system to facilitate a day/night cross-area jump. Note: I haven’t verified this.

1 Like

For my own modules, I simply trashed the NPCs physically moving at certain times, and simply spawned them in the area or not depending on the time of day. The only area I really had to worry about is the one the player is in at that moment.

If they enter during the day, certain NPCs are spawned at their day waypoint.

If they enter during the night, those NPCs are spawned at their night waypoint - either inside taverns, inns, or just not spawned at all. In that event, if the player needs that NPC, they fast forward the time or wait around until the sun rises. The same for NPCs that only appear at night.

FP!

1 Like

You mean you don’t paint creatures in the toolset, and create blueprints instead to spawn and destroy them? Whoa. Does that work with conversation variables set on NPCs, for instance, or are those destroyed when the NPC pops off for the night?

Yes, that is how I worked it.

I simply saved the variables to the player - it is a single player module, after all.

FP!

Heh. I’m not admitting how long it took me to realise that I didn’t have to set all the variables always on the PC simply because that’s what the first codes I learned happened to do. Still, since I have (still) no idea what I’m doing with any of this stuff, I thought I’d ask.

@Wall3t, I’ve checked out the demo module for that and it does look like mostly what I need. Thank you. I wish it had a better readme or something, though, because it’s certainly not “for dummies.” I understand the waypoints and tags thing, but as it is I can’t see how to tweak that material to choose only the actions I actually need out of quite an extensive list.

Seems to me that the ideal for my module would be to combine these two methods - I don’t have houses for all the extras I’d like to wander the streets, so I’d probably be best to spawn and de-spawn those NPCs, Others do have homes to possibly go back to and for those the schedule system would work better. Ot at least some sort of very abridged version of it, if I can ever make that…

I’m a fan of Meaglyn’s Advanced Walk Waypoints scripts. They clean up many of the annoying bits about the default walkwaypoint scripts and provide some additional flexibility.

1 Like

@QuenGalad In support of your comments, a variant of FP’s method is to have one instance of each NPC, but jump it to the correct location on area enter. That way, NPC variables are conserved.

You will need to set the AI level to normal before each jump (via a function that does both).

It can be made more efficient by storing a day/night flag, so you can do nothing if the NPCs are already in the correct position for the time of day.

This is really for plot characters - FP’s method is definitely simpler for citizens of no importance, with the happy bonus of rebooting random walkers who occasionally get stuck.

As always, keep it simple. In my EI3 module, a silly amount of debugging time was spent simply because I made it too complicated, with day/night overlaid with multiple events such as war/peace.

1 Like

This has been one of the bigger hurdles for me when I tried to do this some time ago. They would spawn in fine the first time, but depending on the player behaviour (player leaves the area while it’s still day, doesn’t come back during the night for them to despawn, comes back the next day etc.) there would be multiple copies of NPCs or they’d never be there at all and things like that.
Scripts on entering area give me problems in general, especially since, say, loading the game is also considering entering an area (which, for instance, defeats autosaves on enter) and other stuff like that.

It doesn’t help that I only have a sort of vague suspicion what “storing a day/night flag” might mean and no idea how to really implement it. I, uh, really mean the “dummies” part.

BIG EDIT because I’m grappling with this stuff right now due to some unexpected free time.

I experimented a bit with the ultima schedule system, which looks pretty nice. It has an include file where the various actions for NPCs are defined, and a hartbeat script which actually makes them do it, using a nifty combination of waypoints nad variables.

I tried editing the include file, for some starter cosmetic changes, such as making creatures walk instead of run or speak different strings (let’s be honest, children running around saying “Tag! Thou art it!” is really silly). I think I’m supposed to recompile the script that uses the include in order for the changes to the include to be implemented, right? But the actual heartbeat script gives me a compiling error and given how complicated it is, I have no idea what to do about it.

The script:

Summary

#include “_actions_inc”

void main()
{
if (GetLocalInt(OBJECT_SELF,“WAITING”) == TRUE) return;

if (GetLocalInt(OBJECT_SELF,"INPARTY")==TRUE) return;

if (IsInConversation(OBJECT_SELF) || GetIsInCombat()) return;

SetAILevel(OBJECT_SELF,AI_LEVEL_LOW); // Faster Offscreen Action

int iAction = ACT_FOLLOW; // For Henchmen and Other Followers

string sMyName = GetTag(OBJECT_SELF);

iAction = 0;

// Build Schedule
int i12AM = GetLocalInt(OBJECT_SELF,"12AM");
int i3AM = GetLocalInt(OBJECT_SELF,"3AM");
int i6AM = GetLocalInt(OBJECT_SELF,"6AM");
int i9AM = GetLocalInt(OBJECT_SELF,"9AM");
int i12PM = GetLocalInt(OBJECT_SELF,"12PM");
int i3PM = GetLocalInt(OBJECT_SELF,"3PM");
int i6PM = GetLocalInt(OBJECT_SELF,"6PM");
int i9PM = GetLocalInt(OBJECT_SELF,"9PM");

if (i12AM == 0) // Get Midnight
    if (i9PM > 0)i12AM = i9PM;
    else if (i6PM > 0)i12AM = i6PM;
    else if (i3PM > 0)i12AM = i3PM;
    else if (i12PM > 0)i12AM = i12PM;
    else if (i9AM > 0)i12AM = i9AM;
    else if (i6AM > 0)i12AM = i6AM;
    else if (i3AM > 0)i12AM = i3AM;
    else if (i12AM == 0) // empty schedule, abort
    {
        if (AUTO_FIX_SCHED == TRUE)
        {
            if ((GetRacialType(OBJECT_SELF) != RACIAL_TYPE_UNDEAD) &&
                (GetRacialType(OBJECT_SELF) != RACIAL_TYPE_CONSTRUCT))
            {
                SetLocalInt(OBJECT_SELF,"9PM",14); // sleep
                CreateObject(OBJECT_TYPE_WAYPOINT,"newwaypoint",GetLocation(OBJECT_SELF),FALSE,sMyName+"_9PM");
            }
            SetLocalInt(OBJECT_SELF,"6AM",11); // loiter
            CreateObject(OBJECT_TYPE_WAYPOINT,"newwaypoint",GetLocation(OBJECT_SELF),FALSE,sMyName+"_6AM");
        }
        else
        {
            if (DEBUG_SCHED > 0) SendMessageToPC(GetFirstPC(),sMyName+" in "+GetName(GetArea(OBJECT_SELF))+" has a blank schedule!");
            return;
        }
    }

// Fill in the Blanks
if (i3AM == 0) i3AM = i12AM;
if (i6AM == 0) i6AM = i3AM;
if (i9AM == 0) i9AM = i6AM;
if (i12PM == 0) i12PM = i9AM;
if (i3PM == 0) i3PM = i12PM;
if (i6PM == 0) i6PM = i3PM;
if (i9PM == 0) i9PM = i6PM;

// The Time
int iTheTime = GetTimeHour()+1; // Odee-Odee-O!
if (iTheTime == 24) iTheTime = 0;

// Determine the proper course of Action

if ((iTheTime >= 0) && (iTheTime < 3)) iAction = i12AM;
if ((iTheTime >= 3) && (iTheTime < 6)) iAction = i3AM;
if ((iTheTime >= 6) && (iTheTime < 9)) iAction = i6AM;
if ((iTheTime >= 9) && (iTheTime < 12)) iAction = i9AM;
if ((iTheTime >= 12) && (iTheTime < 15)) iAction = i12PM;
if ((iTheTime >= 15) && (iTheTime < 18)) iAction = i3PM;
if ((iTheTime >= 18) && (iTheTime < 21)) iAction = i6PM;
if ((iTheTime >= 21) && (iTheTime < 24)) iAction = i9PM;

SetLocalInt(OBJECT_SELF,"ACTIVITY",iAction);
string sMyJob = "";

// Get a real job
if ((iTheTime >= 21) && (GetIsObjectValid(GetObjectByTag(sMyName+"_9PM")))) sMyJob = sMyName+"_9PM";
else if ((iTheTime >= 18) && (GetIsObjectValid(GetObjectByTag(sMyName+"_6PM"))))  sMyJob = sMyName+"_6PM";
else if ((iTheTime >= 15) && (GetIsObjectValid(GetObjectByTag(sMyName+"_3PM"))))  sMyJob = sMyName+"_3PM";
else if ((iTheTime >= 12) && (GetIsObjectValid(GetObjectByTag(sMyName+"_12PM")))) sMyJob = sMyName+"_12PM";
else if ((iTheTime >= 9)  && (GetIsObjectValid(GetObjectByTag(sMyName+"_9AM"))))  sMyJob = sMyName+"_9AM";
else if ((iTheTime >= 6)  && (GetIsObjectValid(GetObjectByTag(sMyName+"_6AM"))))  sMyJob = sMyName+"_6AM";
else if ((iTheTime >= 3)  && (GetIsObjectValid(GetObjectByTag(sMyName+"_3AM"))))  sMyJob = sMyName+"_3AM";
else if (GetIsObjectValid(GetObjectByTag(sMyName+"_12AM"))) sMyJob = sMyName+"_12AM";
else if (GetIsObjectValid(GetObjectByTag(sMyName+"_9PM")))  sMyJob = sMyName+"_9PM";
else if (GetIsObjectValid(GetObjectByTag(sMyName+"_6PM")))  sMyJob = sMyName+"_6PM";
else if (GetIsObjectValid(GetObjectByTag(sMyName+"_3PM")))  sMyJob = sMyName+"_3PM";
else if (GetIsObjectValid(GetObjectByTag(sMyName+"_12PM"))) sMyJob = sMyName+"_12PM";
else if (GetIsObjectValid(GetObjectByTag(sMyName+"_9AM")))  sMyJob = sMyName+"_9AM";
else if (GetIsObjectValid(GetObjectByTag(sMyName+"_6AM")))  sMyJob = sMyName+"_6AM";
else sMyJob = sMyName+"_3AM";

object oMyWork = GetObjectByTag(sMyJob); // posible non-wp obj?

SetLocalObject(OBJECT_SELF,"WORKPLACE",oMyWork);

if (GetIsObjectValid(oMyWork) != TRUE)  // no work
if (AUTO_FIX_SCHED == TRUE) CreateObject(OBJECT_TYPE_WAYPOINT,"newwaypoint",GetLocation(OBJECT_SELF),FALSE,sMyJob);
else
{   // abort
    SendMessageToPC(GetFirstPC(),sMyName+" in "+GetName(GetArea(OBJECT_SELF))+" can't find "+sMyJob+".");
    return;
}

float fWorkRange = 3.0;

if (iAction == ACT_WAITER) fWorkRange = 20.0;
if (iAction == ACT_WANDER) fWorkRange = 40.0;

object oMyExit = GetNearestObject(OBJECT_TYPE_DOOR,oMyWork,1);
int iMyExit = 1;

if (GetArea(OBJECT_SELF) != GetArea(oMyWork)) //Offsite Work
    {
        if (GetLocalInt(OBJECT_SELF,"SITTING")==TRUE) SetLocalInt(OBJECT_SELF,"SITTING",FALSE);
        ClearAllActions();
        if (DEBUG_SCHED > 1) SendMessageToPC(GetFirstPC(),sMyName+" in "+GetName(GetArea(OBJECT_SELF))+" exiting to "+GetTag(oMyWork)+" in "+GetName(GetArea(oMyWork)));
            if (GetArea(GetFirstPC())==GetArea(OBJECT_SELF))
            {
                MoveToNewLocation(GetLocation(oMyWork));
                return;
            }
            else
            {
                if (GetArea(GetFirstPC()) == GetArea(oMyWork))
                {
                    while (GetTransitionTarget(oMyExit)==OBJECT_INVALID)
                    {
                        iMyExit = iMyExit + 1;
                        oMyExit = GetNearestObject(OBJECT_TYPE_DOOR,oMyWork,iMyExit);
                        if (oMyExit==OBJECT_INVALID)
                        {
                            ActionJumpToObject(oMyWork);
                            return;
                        }
                    }
                    ActionJumpToObject(oMyExit);
                    DelayCommand(0.2,ActionMoveToObject(oMyWork));
                    return;
                }
                else
                {
                    ActionJumpToObject(oMyWork);
                    return;
                }
            }
    }

else if (GetDistanceToObject(oMyWork)>fWorkRange) //Too Far
    {
        if (GetLocalInt(OBJECT_SELF,"SITTING")==TRUE) SetLocalInt(OBJECT_SELF,"SITTING",FALSE);
        ClearAllActions();
        if (GetArea(GetFirstPC())==GetArea(OBJECT_SELF))
        {
            if (DEBUG_SCHED > 1) SendMessageToPC(GetFirstPC(),sMyName+" in "+GetName(GetArea(OBJECT_SELF))+" moving to "+GetTag(oMyWork));
           MoveToNewLocation(GetLocation(oMyWork));
           return;
        }
        else
        {
            ActionJumpToObject(oMyWork);
            return;
        }
    }

int iAsleep = GetLocalInt(OBJECT_SELF,"ASLEEP");

if ((iAsleep == TRUE)&&(iAction != ACT_SLEEP))
{ // The sleeper must awaken!
    ClearAllActions();
    SetLocalInt(OBJECT_SELF,"ASLEEP",FALSE);
    iAsleep = FALSE;
    effect eSleep = GetFirstEffect(OBJECT_SELF);
    while(GetIsEffectValid(eSleep))
    {
        if (GetEffectType(eSleep) == EFFECT_TYPE_SLEEP)
            {
                RemoveEffect(OBJECT_SELF, eSleep);
                break;
            }
        eSleep = GetNextEffect(OBJECT_SELF);
    }
}

if ((GetArea(GetFirstPC())!=GetArea(OBJECT_SELF))&& (iAction!=ACT_SLEEP))
    if ((iAction!=ACT_DESK_WORK)&&((iAction!=ACT_EAT_AT_INN)&&(iAction!=ACT_MAJOR_SIT))) return;

if (DEBUG_SCHED > 1) SendMessageToPC(GetFirstPC(),sMyName+" in "+GetName(GetArea(OBJECT_SELF))+" : action #"+IntToString(iAction)+" during "+IntToString(iTheTime));

DoActivity(iAction);

}

The bit where it says

float fWorkRange = 3.0;
if (iAction == ACT_WAITER) fWorkRange = 20.0;
if (iAction == ACT_WANDER) fWorkRange = 40.0;

gives me a “variable defined without type” error, and while I know how to deal with that in my own simplistic scripts (usually means I misspelled a previously defined variable or that I forgot to say it’s an object or whatever), this one seems to throw that sort of thing about everywhere just fine and I have no idea how to handle that. I’d like to use this system, because I sort-of get how, and it’s not hard to slap onto an already messily complicated module, but without being able to tweak the scripts to my setting it’s not possible.

There are multiple spawn systems out there of varying complexity, so it really depends what your desired primary use is.

A relatively simple one is BESIE, which has easily implemented day/night schedules and cleanup; only areas with PCs active will activate the spawns. It’s an invisible object-based system that you can easily control the parameters of by modifying the object properties. I use it primarily for random commoners in populated areas, with different spawners for day and night hours (when traffic would be expected to be more or less). It can also be used for regular NPCs you want to spawn at a particular location and for encounter spawning. What it doesn’t have is complex behaviors for the NPCs, beyond adjustable random walking and conversations.

Vault link: https://neverwintervault.org/project/nwn1/script/besie-random-encounter-system-v181

If you’re really looking for a lot of power and control over NPC activities, then the namesake package by Deva Bryson Winblood is available: https://neverwintervault.org/project/nwn1/script/npc-activities-61

I think carcerian did fine with the system. I would still test it out regardless if you get any compilation errors, I guarantee you it would work just fine. this is one of his older works, so i expect some “wonkiness” when it comes to assuring it works with you’re module.

I use it extensively alongside other A.I activity scripts in my P.W. it gives npcs actual activities and they walk across areas to do their other activities instead of sitting still all day.

I’ve looked into it some more and it looks really good - just two scripts for a variety of behaviours - but unfortunately, as long as I can’t compile the heartbeat script, I can’t use that system for anything else than what the demonstration does, because I can’t get the changes in the behaviour library to actually work.

I would love to use it, but this is a very serous stumbling block.
Also, assuming I can somehow get around it, I’d also love to expand the time schedule - the original script is set to work in three hour segments - but I can’t understand anything in the script’s time schedule section.

Did you ever get these changes to compile? I downloaded the system and your snippet. The two match exactly and mine compiled without modification. Are you still getting the error?

Also, looking at the time schedule, doesn’t look like it’s be too difficult to modify to an hourly schedule, register a job to start and end at a certain time, then run the code on game hours instead of heartbeat. If you’re still interested, I’ll see if we can work through something there.

1 Like

Yes, I’m still getting the compiler error. I got around it by commenting out these two lines:

if (iAction == ACT_WAITER) fWorkRange = 20.0;
if (iAction == ACT_WANDER) fWorkRange = 40.0;

Which is extremely weird, because I just re-downloaded the system, opened a clean copy and it compiled without error. So I used mergely.com to compare the freshly downloaded script with the one in my module, and there is no difference between them at all, apart from the comment-out strokes that is.

I have no idea what’s going on.

As for the schedule timing, well, it would be better and more versatile to be able to fine-tune it to, say, every hour, because then you wouldn’t have everyone suddenly turn around and leave at the strike of noon (which is a bit creepy) but I figure the three-hour system is kinda good enough for my purposes. I think I should probably get other, glaringly wrong things right first :slight_smile:

Without looking at the scripts in question, is fWorkRange defined anywhere else in the script? If so, then I can’t imagine why it doesn’t compile. If it isn’t, just define it just before these statements?

float fWorkRange = 20.0;// or whatever you want it to default to
if (iAction == ACT_WAITER) fWorkRange = 20.0;
if (iAction == ACT_WANDER) fWorkRange = 40.0;

T̶h̶a̶t̶’̶s̶ ̶a̶c̶t̶u̶a̶l̶l̶y̶ ̶e̶x̶a̶c̶t̶l̶y̶ ̶w̶h̶a̶t̶ ̶t̶h̶e̶ ̶s̶c̶r̶i̶p̶t̶ ̶l̶o̶o̶k̶s̶ ̶l̶i̶k̶e̶.̶ ̶T̶h̶e̶ ̶l̶i̶n̶e̶ ̶b̶e̶f̶o̶r̶e̶ ̶i̶s̶ ̶f̶l̶o̶a̶t̶ ̶f̶W̶o̶r̶k̶R̶a̶n̶g̶e̶ ̶=̶ ̶3̶.̶0̶

OMG I figured it out! I went to actually take a screenshot of the damn thing because I was frustrated, and I thought about what you wrote and how it seemed to me it must work that way (I know that much by now! I’m improving! Yay!).

So I stared at the damn complier error and I had a brainwave. There’s other stuff in that line, not just the fWorkRange.

The problem wasn’t the float! The problem was that, when editing the include, I changed those various ACT_* thingies and I renamed some of them to things easier to remember to me, and these two happened to be among the renamed stuff!
Edit: there was no difference between the two scripts at all, and the problem was that there should have been. This is some Poirot stuff, people.

Thanks for all the help, guys, you all continue to kick all kinds of butt.

1 Like

Good catch, glad you got it figured out.

1 Like