Resetting an NPC's sequence of events

I’ve got an NPC ghost who manifests when the PC enters a room, performs a series of tasks, and vanishes if the player leaves. If the player comes back, they do the same series of tasks again, from the beginning.

Would this be more efficient to implement with ActorStates and an EventList, or AgendaItems? If the former, how do I “reset” an EventList that hadn’t reached its conclusion? I don’t want the sequence to cycle, I want it to start over if the player leaves and comes back.

For EventList, I think it’s as simple as setting its curScriptState property to 1 to reset it.

1 Like

Depending on your tasks… Agenda might be better, since they wait to fire until their conditions are right (the task is done). Most EventList s just keep on truckin unless you’re using ExternalEventList and manually advancing the state after task completion…

With AgendaItems will I need to manually reset all of the isDone properties to false to get the ghost to repeat the sequence?

Answering my own question; I need to call addToAgenda on each agendaItem to restore and reset them if and when the ghost re-manifests.

3 Likes

Whenever you’re thinking of an NPC looping through a set of tasks, or performing a specific set of tasks when some specific condition is met, it’s probably worth considering using a “formal” finite state machine.

In this case, you could just use a property on the Actor that takes one of a specific set of values (one for each state you need to model). You then have an AgendaItem for each state, and each agenda’s isReady() checks the value of the property.

That’s kinda a mouthful, so here’s a trivial/silly example:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

// These are identifiers for our various states
enum aliceFoo, aliceBar, aliceBaz;

// A class to hold our FSM logic.
class ActorFSM: Actor
        actorFSM = nil

        setActorFSM(v?) { actorFSM = v; }
        getActorFSM() { return(actorFSM); }
        checkActorFSM(v?) { return(actorFSM == v); }
;

startRoom:      Room 'Void'
        "This is a featureless void.  "
;
+me:    Person;
// We mix in our ActorFSM class so Alice now has a FSM.
+alice: Person, ActorFSM 'Alice' 'Alice'
        "She looks like the first person you'd turn to in a problem. "
        isProperName = true
        isHer = true
        // Set the initial value for the FSM
        actorFSM = aliceFoo
;
// We now define an AgendaItem for each state, such as it is.
++AgendaItem
        initiallyActive = true
        // In this case all we do is check the state, but we could implement additional
        // checks if we needed to.
        isReady = (alice.checkActorFSM(aliceFoo))
        invokeItem() {
                // All the agenda actually does is a scrap of output and then set
                // the next state.
                "Alice mutters <q>Foo</q>. ";
                alice.setActorFSM(aliceBar);
        }
;
// Additional agendas are functionally identical to the first.
++AgendaItem
        initiallyActive = true
        isReady = (alice.checkActorFSM(aliceBar))
        invokeItem() {
                "Alice mutters <q>Bar</q>. ";
                alice.setActorFSM(aliceBaz);
        }
;
++AgendaItem
        initiallyActive = true
        isReady = (alice.checkActorFSM(aliceBaz))
        invokeItem() {
                "Alice mutters <q>Baz</q>. ";
                alice.setActorFSM(aliceFoo);
        }
;

versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
;

Here we define an ActorFSM class for all our actors that need the new functionality. The class just adds a property (to hold the value of the current state) and a couple convenience methods for getting and setting the state (if we wanted to implement any sanity checking to verify that our state value is always valid—verify a new value before setting it, returning a failsafe value if no state has been set, and so on—we could do it here).

In each AgendaItem we just check the value of the state, but we could also query other bits of the game state (is Alice in a specific room? is the player in the same room with Alice? and so on) if we need/want to (or, alternately, implement these conditions as explicit states in our FSM model).

In this example we use an enum for the allowed state values, but we don’t have to—we could use arbitrary strings or numeric values or even objects if we wanted to.

We could also stuff all our NPC logic into a single AgendaItem and just use a big switch() statement with a case for each state (and possibly a default as a fallback to reset things if we’ve somehow managed to set a bogus value as the state).

The thrilling transcript:

Void
This is a featureless void.  A television is mounted on one wall.

Alice is standing here.

>z
Time passes...

Alice mutters "Foo".

>z
Time passes...

Alice mutters "Bar".

>z
Time passes...

Alice mutters "Baz".

>z
Time passes...

Alice mutters "Foo".

…and so on.

I think the single-Agenda FSM is what I used to do 15 years (and several hard drives) ago. Relearning TADS has been a journey of remembrance.

Yeah, I don’t know about TADS code specifically, but a big switch() statement that calls a bunch of different “behavior” methods is a pretty common model for simple NPC “AI” in games in general.

1 Like