Am I asking too much of the daemon system?

Basically, I want this actor and eventually others to roam through the rooms, go to meals and sleep at night. I’m having a incredibly difficult time making them wander from room to room, so much that I’m using debugs to try and figure out what is wrong.

+ wright: Actor
    name = 'Wright'
    vocabWords = 'wright; brother; pup;
wolf; skunk'
    desc = "This is your eight-year-old little brother, Wright. He has black and white fur markings that make him look a bit like a skunk."
    isListed = true

    // Give Wright a starting location
    location = den

    // Property to track his last known location to prevent spamming movement messages.
    lastKnownLocation = nil
;

wrightScheduler: object
    name = 'Wright Scheduler'
;

modify wrightScheduler
    endTurn() {
        // --- ADD THIS LINE FOR TESTING ---
        say('<b>DEBUG: Wright scheduler is running. Time is <<gameTime.getTimeString()>>.</b><br>');

        local curTime = gameTime.currentTime;
        local playerRoom = me.getOutermostRoom();
        local wrightRoom = wright.location;
        local targetLocation = nil;

        // 1. Determine Wright's target location based on important schedules (sleep, meals)
        if (curTime >= 1320 || curTime < 360) {
            // It's bedtime, he should be in the den.
            targetLocation = den;
        } else if ((curTime >= 480 && curTime < 540) || (curTime >= 780 && curTime < 840) || (curTime >= 1080 && curTime < 1140)) {
            // It's mealtime, he should be in the dining room.
            targetLocation = diningRoom;
        }

        // 2. If he doesn't have a scheduled place to be, consider wandering.
        //    He will only wander if the player is NOT in the same room.
        if (targetLocation == nil && wrightRoom != playerRoom && rand(100) < 20) {
            local possibleDestinations = [den, diningRoom, roof] - [wrightRoom];
            if(possibleDestinations.length > 0) {
                targetLocation = possibleDestinations[rand(possibleDestinations.length)];
            }
        }

        // 3. If he has a destination and he's not already there, move him.
        if (targetLocation && wrightRoom != targetLocation) {
            // Announce his departure if the player can see him leave
            if (wrightRoom == playerRoom) {
                say('Wright gets a bit restless and wanders off. ');
            }

            // Move him to his new location
            wright.moveInto(targetLocation);

            // Announce his arrival if he enters the player's room
            if (targetLocation == playerRoom) {
                // Customize the message based on why he's moving.
                if (targetLocation == diningRoom && wrightRoom != diningRoom) {
                    say('Wright trots into the dining room, ready to eat. ');
                } else {
                    say('Wright wanders into the room. ');
                }
            }
        }
    }
;

2 Likes

Welcome!

Have you tried looking at AgendaItems? You can fold most of those conditionals into the isReady and the actions into invokeItem.

Something like:

+ wright: Actor
    name =  'Wright'
   [etc]
;

++wrightBedTimeAgenda: AgendaItem
    isReady {
        local curTime = gameTime.currentTime;
        return curTime >= 1320 || curTime < 360;
    }
    invokeItem {
         // Move NPC to bedroom
    }

Never move actors around with moveInto. Here’s why: NPC Travel in TADS 3. If you use travelTo then you automatically get all the movement notices so you don’t need to handle that in your own code. Unfortunately it moves them to an adjacent room, so if your map is complicated you have to move by fiat (moveIntoForTravel). Then to make the announcement depend on if the player can see them:

    invokeItem {
        // Make noises only if you can see Wright leaving
        callWithSenseContext( wright, sight, { : mainReport('Wright gets a bit restless and wanders off. '); } );

        // Do movement whichever way you need
        wright.moveIntoForTravel(wrightBedroom);

        // Make noises only if you can see Wright arriving
        callWithSenseContext( wright, sight, { : mainReport('Wright wanders into the room. '); } );

    }

This captures all the complexities of things like nested locations, darkness, being blind…

Also note, vocabWords has a very particular structure for adjectives and nouns. I think you want: vocabWords = 'little eight year old wright/brother/pup/wolf/skunk'

4 Likes

I was in the process of typing up similar response.

I’ll add the caveat that I’ve written a bunch of code for NPC automation—target-seeking behaviors, setting daily schedules, giving instructions involving objects out of the current scope, and so on. So that’s another option. But if you’re just implementing a few specific behaviors and you’re not worried about insuring that NPCs are bound by the same gameworld constraints as the player, then “just” using AgendaItem and hardcoding logic to bamf the NPCs around is probably the best approach.

4 Likes

@BrettW thanks for covering the meaty stuff.
There are also situations where takeTurn and scriptedTravelTo can do what you want.

3 Likes

I switched to agenda and it is working now but crashing. I’ve banged my head on it for a few hours but can’t get forward anymore. Here’s what’s happening in the game log.


The Den

You wake up in your cozy den. The adults are gone, and the other pups are starting to stir, making their way downstairs. Soft morning light filters through the entrance, casting a gentle glow inside. The clock shows the early hour, its cartoon characters coming into view. The windows hint at a world waking up outside. A ladder leads up to the roof, and wooden stairs descend to the dining room.


You see a cheerful clock, a left window, a glow-in-the-dark stars, a Wright, a right window, a soft pillows, a posters, a stuffed animal, a colorful blankets, a pup drawings, a family pictures, and a Lite-Brite here. 


The Wright is standing here. 


>down
Dining Room

The dining room has a large table set up with everything for meals. Every hand-crafted chair has a pup's or adult's name carved into it professionally. Stairs lead back up to the den. The table is set, and you can smell breakfast cooking.


You see a your chair here. 


>wait
Time passes... 


Wright wanders into the room. 


>eat
(the meal)
(first taking the meal)
You eat the meal. 


>wait
Time passes... 


>look
Dining Room

The dining room has a large table set up with everything for meals. Every hand-crafted chair has a pup's or adult's name carved into it professionally. Stairs lead back up to the den. The table is set, and breakfast is ready.


You see a your chair and a Wright here. 


The Wright is standing here. 


>south
Living Room

The radio is playing puppy safe music, while Max displays cartoons. 


You see a radio, a couch, a coffee table, a window, a two lounge chairs, a love seat, and a Max here. 


The Max is standing here. 


>north
Dining Room

The dining room has a large table set up with everything for meals. Every hand-crafted chair has a pup's or adult's name carved into it professionally. Stairs lead back up to the den. The table is set, and breakfast is ready.


You see a your chair here. 


>look
Dining Room

The dining room has a large table set up with everything for meals. Every hand-crafted chair has a pup's or adult's name carved into it professionally. Stairs lead back up to the den. The table is set, and breakfast is ready.


You see a your chair here. 

Here’s the current code.

// Task for wandering around
wrightWanderAgenda: AgendaItem
    initiallyActive = true
    location = wright // This tells the item which actor it belongs to.

    // Define the list of destinations as a property of the AgendaItem.
    // This is safer than defining it inside invokeItem.
    destinationList = [den, diningRoom, roof, livingRoom]

    isReady {
        // Don't wander if player is in the same room.
        if(wright.location == me.getOutermostRoom()) return nil;
        
        // For testing, wander every turn.
        return true; 
    }
    
    invokeItem {
        // Create a *copy* of our destination list to work with.
        local validDestinations = self.destinationList.sublist(1, self.destinationList.length);

        // From this copied list, remove the room Wright is currently in.
        validDestinations.removeElement(wright.location);

        // If there are any valid places left to go...
        if (validDestinations.length > 0)
        {
            // ...pick one at random.
            local destination = validDestinations[rand(validDestinations.length)];

            // Announce and move.
            callWithSenseContext(wright, sight, { : say('Wright gets a bit restless and wanders off. ')});
            wright.moveIntoForTravel(destination);
            callWithSenseContext(wright, sight, { : say('Wright wanders into the room. ')});
        }
    }
;

and then the crash.

wrightWanderAgenda.invokeItem() + 0x46
wright.executeAgenda() + 0x44
ActorState [2349].takeTurn() + 0x2F
wright.idleTurn() + 0x5F
wright.executeActorTurn() + 0x159
func#200c4() + 0x17
senseContext.withSenseContext(wright, sight, obj#4f1f (AnonFuncPtr)) + 0x1D
callWithSenseContext(wright, sight, obj#4f1f (AnonFuncPtr)) + 0x15
func#20105() + 0x3F
withCommandTranscript(CommandTranscript, obj#4f24 (AnonFuncPtr)) + 0x49
withActionEnv(EventAction, wright, obj#4f24 (AnonFuncPtr)) + 0x3C
wright.executeTurn() + 0x30
runScheduler() + 0xB6
runGame(true) + 0x2C
gameMain.newGame() + 0x1B
mainCommon(&newGame) + 0x4D
main(['debug\\The World of Sopho - A Pups Adventure.t3']) + 0x1B
flexcall(main, ['debug\\The World of Sopho - A Pups Adventure.t3']) + 0x59
_mainCommon(['debug\\The World of Sopho - A Pups Adventure.t3'], nil) + 0x53
_main(['debug\\The World of Sopho - A Pups Adventure.t3']) + 0x12
1 Like

This will produce out-of-range index errors. rand(n) will produce a value from 0 to n - 1, and TADS3 indexes from 1. So for an integer in [ 1, n ] you want (rand(n) + 1).

As a stylistic note, it’s usually a good idea to use gPlayerChar instead of me, unless you actually want to restrict the code to that specific object. In most games this won’t matter if you’ve only got one player object, but there are a surprising number of cases where you might want to fake it even if that game “really” only has one player character.

5 Likes

A couple of hopefully helpful notes:
-In TADS you can do rand(lst) and it will pick an element for you.
-You should be able to get a stack trace that tells you what kind of error you’re getting (index out of range, nil object reference…); do you have it purposely configured to not show them?
-To copy a list you can simply local validDestinations = destinationList: it won’t be a reference.
-You could probably replace moveIntoForTravel with scriptedTravelTo and not have to hardcode the travel messages (they’d be defined as sayDeparting... etc.)
-If you use NestedRooms you may want to code that as if (wright.isIn(outermostRoom)) else the code will misbehave if he’s standing on a Platform or something.
-You may be planning this already, but you probably want to give Wright and Max isProperName so you don’t get “the Wright is standing here.”

2 Likes