TADS 3 newbie question

Greetings, I’m completely new to writing in TADS 3 and mostly programming in general. I’ve (very slowly) been making my way through Eric Eve’s Learning T3 tutorial and trying things along the way and I hit a snag at getting a simple method to work

The first example of method in the tutorial deals with how to create a method to change the name propriety of an item. I tried to do it with the ‘desc’ propriety instead, as follows:

Mike: Person 'man/guy/mike' 'Mike' @SecondRoom "A man” ; +tattoo: Component 'mike\'s tattoo' 'tattoo' desc = "A tattoo" changeDesc(newDesc) { desc = newDesc; } ;

This ‘tattoo’ being a component of the NPC Mike. The tutorial goes on to explain how to make a hidden item (a ring in this case) appear by looking for it, so I decided to hijack the setting of the ‘discovered’ propriety of the ring to also serve as the call for changing the description of the tattoo, like this:

[code]

  • ring: Hidden, Wearable ‘ring’ ‘gold ring’
    “A golden ring”
    ;
  • rock: Immovable ‘grey rock’ ‘rock’
    “A big grey rock”
    dobjFor(LookUnder)
    {
    action()
    {
    tattoo.changeDesc(‘A dragon tattoo’);
    {
    if(ring.discovered)
    “There is nothing else.”;
    else
    {
    ring.discover();
    “You find a ring”;
    }
    }
    }
    }
    ;[/code]

All the code here deals with making the ring show up if I look under the rock, which works perfectly, except for the line that I added “tattoo.changeDesc(‘A dragon tattoo’);” that is also supposed to change the ‘desc’ propriety of the tattoo.

Now, something works here, just not all of it. When I run the game, I can examine the tattoo and get the first description (“A tattoo”) just fine, but once I find the ring, examining the tattoo makes the game return the default “Nothing obvious happens” response, meaning that SOMETHING happened to the ‘desc’ propriety, but it was not correctly replaced by ‘A dragon tattoo’ like I wanted.

I’ve been scratching my head for a good while and I can’t figure out what’s wrong, and I really don’t want to keep reading until I can sort out something as basic as this. Any help would be really appreciated.

You are running into the distinction between single-quoted and double-quoted (aka self-printing) strings.

The initial desc string is double-quoted, so when the examine action invokes that property, it immediately displays “A tattoo” in the output.

When you replace its value with a single-quoted string, you store the text in the property, but you aren’t introducing a mechanism to display it on the screen.

The simplest fix is to change desc to a function that displays another property, then update that property in your changeDesc method.

Mike: Person 'man/guy/mike'  'Mike' @SecondRoom
    "A man”
;
+tattoo: Component 'mike\'s tattoo' 'tattoo'
    desc
    {
        "<<myDesc>>";
    }
    changeDesc(newDesc)
    {
        myDesc = newDesc;
    }
    myDesc = 'A tattoo'
;

I see. I had a feeling that it had to be the double quotes since it was the only difference with other proprieties I tested. I just tried it and it worked as expected.

Thank you very much.

Another basic one. Trying to understand how to set and check for variables, I created the following object:

moon: Distant 'moon' 'moon' @ForestStart "You see the new moon shining overhead" tooDistantMsg = 'The moon is way too far away to do that' changeTooDistantMsg(newTooDistantMsg) { tooDistantMsg = newTooDistantMsg; } dobjFor(Take) { action() { if(moon.triedToTake == true) moon.changeTooDistantMsg('Yeah, you just keep trying that'); else moon.triedToTake = true; } } triedToTake = nil ;

The idea is that trying to ‘take’ the moon once will display the regular tooDistantMsg I set, but trying to do so again will call the changeTooDistantMsg method, but trying to take the moon repeatedly just returns the first tooDistantMsg. I’m guessing whatever it is that I’m screwing up has to do with my poor understanding of how to set and check for variables. I tried to make the call check for a “triedToTake” boolean variable I made up.

Additionally, two semi-related questions:

  1. Seeing as all this code happens within the same object, how would I signal the triedToTake variable as local to this object only?
  2. Can I set an object’s location to more than one room? The moon here is set to @ForestStart, for instance, but could I make it so that THIS moon should be visible in multiple locations, or do I need to create copies for each individual room?

I had to look up the source code to the Distant object to figure this out. Because the action gets ruled out in the verify() stage, it never made it to your action() routine and therefore your message update never fired.

This will do what you want. Basically we just replace the default dobj / iobj verify routines with a modified version that updates the triedToTake property.

Another catch is that the verify() routine runs multiple times before it actually displays the message. This happens because you could have multiple “moon” objects in scope, only one of which would be logical to take. The parser runs through each of the objects with a vocabulary match before deciding on the most likely match. In my testing, verification happened three times, so I set an appropriate condition in the moonVerify() routine to switch the message after three passes.

moon: Distant 'moon' 'the moon' @kitchen
    "You see the new moon shining overhead"
    tooDistantMsg = 'The moon is way too far away to do that'

    changeTooDistantMsg(newTooDistantMsg)
    {
        tooDistantMsg = newTooDistantMsg;
    }

    dobjFor(Default)
    {
        verify()
        {
            moonVerify();
            illogical(&tooDistantMsg, self);
        }
    }

    iobjFor(Default)
    {
        verify()
        {
            moonVerify();
            illogical(&tooDistantMsg, self);
        }
    }

    moonVerify()
    {
        if (triedToTake > 3)
            changeTooDistantMsg('Yeah, you just keep trying that');
        else
            triedToTake ++;
    }

    triedToTake = 0
;
  1. The triedToTake property is automatically a local property. Other objects can only access it via moon.triedToTake, or through a macro that expands to the same. Macro expansion is the way that the libGlobal pseudo-globals work; for instance, gDobj is really libGlobal.curAction.getDobj().

  2. If you define the moon with MultiLoc, Distant in its class list, you can add locations to its locationList property.

moon: MultiLoc, Distant 'moon' 'the moon'
    "You see the new moon shining overhead"
    locationList = [kitchen, patio, forest]
;

Always keep in mind that properties can also be defined as methods. There are some exceptions, but mostly you can. So all you need to do is:

moon: Distant 'moon' 'moon' @ForestStart "You see the new moon shining overhead. " triedToTake = nil tooDistantMsg() { if (self.triedToTake) { return 'Yeah, you just keep trying that. '; } self.triedToTake = true; return 'It\'s far away. '; } ;

On an unrelated matter, you should include a space at the end of strings that are to be printed, unless you have a reason not to. This avoids sentences running together with no space between them. This happens when TADS automatically appends something (like “It’s closed.”) Imagine for example: “It’s a drawer.It’s closed.” Obviously, you don’t want that. Also note that the extra space at the end has no ill effect, since TADS will always join multiple spaces into one, so you don’t have to fear that you end up with sentences separated by too much spaces.

Let me see if I got this right: Since the Distant property inherently rules out actions like “Take” as illogical to begin with, any code depending on executing said action was ignored until said inherent behavior was overriden?

Anyhow, that did the trick, although I had to replace the (Default) in “iobjFor/dobjFor(Default)” for (Take), else the game reacted to ANY action as if I had just tried to use Take.

Thank you very much.

Oops, I didn’t notice that you want the modified reply only for the Take action. In which case you can either follow bcressey’s advice of defining a verify() routine, or check whether the current action is “Take” in tooDistantMsg():

moon: Distant 'moon' 'moon' @ForestStart "You see the new moon shining overhead. " triedToTake = nil tooDistantMsg() { if (gActionIs(Take)) { if (self.triedToTake) { return 'Yeah, you just keep trying that. '; } self.triedToTake = true; } return 'It\'s far away. '; } ;

This does work, but it results in the same behavior as leaving (Default) in the other method: The game reacts to any action in the same way, so for instance, using Examine results in the same response as trying to use Take.

EDIT: Okay, nevermind.

Ah, and thanks for the heads up on leaving the space. I was wondering why the tutorial samples were all written like that.

Hi again. Did some good progress in understanding what the whole deal with the verify routine was all about. Not too sure on the whole “runs multiple times” thing yet but I guess I’ll work that out eventually.

Now I’m trying to monkey around with Switch Statements. I previously took the Moon object up there and started adding other If checks and responses. It worked just fine, but now I’m trying to convert all that into a switch statement as seen below.

moonVerify() { switch(triedToTake) { case 3: { changeTooDistantMsg('Yeah, you just keep trying that '); triedToTake ++; } break; case 6: { changeTooDistantMsg('Still at it? '); triedToTake ++; } break; case 9: { fakeMoon.moveInto(me); changeTooDistantMsg('You see a small round object appear out of nowhere '); triedToTake ++; } break; case 12: changeTooDistantMsg('ENOUGH! '); break; default: triedToTake ++; break; } } triedToTake = 0

Now, this works, but there’s something I can’t work out:

This one should be kind of basic syntax. If I’m getting this right, I made it so that if triedToTake is 3, the first response will trigger. If it’s 6, the second will, and so on until triedToTake is 12 and the last case is triggered any subsequent time. The first time the Take action is attempted, the ‘default’ case will trigger since triedToTake will be 0. I get it up to here. Don’t pay attention to what happens in 9, I made that one do some other stuff just to see if I could.

However, I’d like the variables to be checked as ‘larger than x/lower than y’, not ‘exactly x’. For instance, I’d like to have the first case trigger if triedToTake is larger or equal to 3 but smaller than 6. That’s not important in THIS item, but I can see myself needing to make checks work that way later on. I know how to make that happen in regular If checks, but I can’t seem to work out how to embed that into the switch statement.

Switch statements require the case to test a discrete value, not a range, but cases that do not lead to a break will fall through, so you can pile up conditions and handle them as a group.

[spoiler][code]
moonVerify()
{
switch(triedToTake)
{
case 3:
case 4:
case 5:
{
changeTooDistantMsg('Yeah, you just keep trying that ');
break;
}

        case 6:
        case 7:
        case 8:
        {
            changeTooDistantMsg('Still at it? ');
            break;
        }
        
        case 9:
        {
            fakeMoon.moveInto(gPlayerChar.getOutermostRoom());
            changeTooDistantMsg('You see a small round object appear out of nowhere ');
            break;
        }
        
        case 12:
        {
            changeTooDistantMsg('ENOUGH! ');
            break;
        }            
        
        case  1:
        case  2:            
        case 10:
        case 11:
        default:
        {
            break;
        }
    }
    
    triedToTake ++;        
}

triedToTake = 0

[/code][/spoiler]

However, in this particular instance you are much better off adapting Nikos’ suggestion and example code. This avoids having to account for the fact that verification routines are called multiple times (to decide on logic, scope, accessibility, etc), and it’s far more readable as a result.

moon: Distant 'moon' 'moon' @ForestStart
    "You see the new moon shining overhead. "
    tooDistantMsg()
    {
        if (gActionIs(Take))
        {
            triedToTake ++;
            
            switch (triedToTake)
            {
                case 1:
                    return 'The moon is way too far away to do that ';
                
                case 2:
                    return 'Yeah, you just keep trying that ';
                    
                case 3:
                    return 'Still at it? ';
                    
                case 4:
                    fakeMoon.moveInto(gPlayerChar.getOutermostRoom());
                    return 'You see a small round object appear out of nowhere ';
                    
                case 5:
                default:
                    return 'ENOUGH ';             
            }
        }
        else
        {
            return 'The moon is way too far away to do that ';
        }
    }
    triedToTake = 0
;

fakeMoon: Thing 'fakemoon' 'FAKEMOON';

I get it, so you can’t use ranges with switch statements. Thank you very much.

While attempting to work out what object the player was most likely referring to in a command, the parser may call a verify() routine several times. This is because the parser wants to see which object is the most logical in cases of ambiguity.

While doing this, the parser is suppressing the text output from macros such as illogical, so you don’t see it running multiple times. But that’s why it’s vital not to make any changes in the game state in a verify() routine.

–JA

Yet again. Now I’m messing with Daemons. I made this:

[spoiler][code]me: Actor
location = StartRoom
;

+MyHP: Component ‘my hp/health/life/point*points’
desc = “Current Health: <>/<>”
MaxHP = 10
CurrentHP = 1
RegenAmount = 1
regenerateHP()
{
if (CurrentHP < MaxHP)
{
CurrentHP += RegenAmount;
"You feel a little better. ";
}
{
if (CurrentHP > MaxHP)
CurrentHP = MaxHP;
}
else
"You are already fully healed. ";
}
;

DefineIAction(Regenerate)
execAction()
{
MyHP.regenerateHP();
}
;

VerbRule(Regenerate)
‘Regenerate’ | ‘heal’
: RegenerateAction
;

StartRoom = Room ‘Start Room’
;[/code][/spoiler]

This was my attempt to screw around with defining actions, and I got a bit carried away while testing checking conditions. Simply put, the character has a certain amount of HP, starting at 1 and going up to 10, and whenever he uses the command ‘regenerate’ or ‘heal’, he gains one extra point up to a maximum of 10. I can tweak the Current, Maximum and Regenerate amounts from outside quite easily if I wanted to without anything going tits up as well, and I added a check to make sure CurrentHP never exceeds MaxHP in the case I ever increase RegenAmount… though I’d probably have to add that check to any other external modifications I do to CurrentHP unless there’s a tidier way to do it. Well, I’ll figure that out later, I’m rather inordinately proud of having gotten this running at all.

However, now I want to see if I can have a daemon running perpetually in the background that will execute MyHP.regenerateHP() once per turn.

regenDaemon = new Daemon (MyHP, &regenerateHP, 1); 

The thing is, I can’t figure out how to have this fire on its own. I tried including it as part of the MyHP object like so

+MyHP: Component 'my hp/health/life/point*points' desc = "Current Health: <<CurrentHP>>/<<MaxHP>>" MaxHP = 10 CurrentHP = 1 RegenAmount = 1 regenerateHP() { if (CurrentHP < MaxHP) { CurrentHP += RegenAmount; "You feel a little better. "; } else "You are already fully healed. "; } autoRegenHP() { regenDaemon = new Daemon(self, &regenerateHP, 1); } regenDaemon = true

…but that doesn’t work either.

I see. So is there a way to quickly get how many times the verify routine runs in an action, or is it necessary to do something like having your action increment a variable and then guessing by how much the variable was increased?

Since Component inherits from Thing, it has an initializeThing() method that runs during pre-init (after compile, but before the game is launched.) You can override that method to add any startup code your object needs.

I also adjusted your regenerateHP() routine to cap the value of CurrentHP at MaxHP, regardless of the value of RegenAmount.

+MyHP: Component 'my hp/health/life/point*points'
    desc = "Current Health: <<CurrentHP>>/<<MaxHP>>"
    MaxHP = 10
    CurrentHP = 1
    RegenAmount = 1
    regenerateHP()
    {
        if (CurrentHP < MaxHP)
        {
            CurrentHP += RegenAmount;
            CurrentHP = min(CurrentHP, MaxHP);
            "You feel a little better. ";
        }
        else
            "You are already fully healed. ";
    } 
    autoRegenHP()
    {
        regenDaemon = new Daemon(self, &regenerateHP, 1); 
    }
    initializeThing()
    {
        inherited();
        autoRegenHP();
    }
    regenDaemon = nil

Since there are countless ways to build a mousetrap, here’s my take on this :smiley:

HP is just an abstract numerical value. It’s not a game object, so there’s no reason for you to make an actual object that derives from the Thing class out of it. Unless of course you want things like PICK UP HP, PUT HP IN THE BOX, etc, to work. With HP, what I would expect from such a game is to show it in a banner and perhaps as a response to X ME or STATS, but not X HP. So you only need to have a few variables in the “me” object. Like:

me: Actor
    maxHP = 10
    currentHP = 1
    regenAmount = 1
    regenerateHP()
    {
        if (currentHP < maxHP) {
            currentHP += regenAmount;
            if (currentHP > maxHP) {
                currentHP = maxHP;
            }
            "You feel a little better. ";
        } else {
            "You are already fully healed. ";
        }
    }
;

As for the Daemon not working, you need to execute the code that creates the Daemon somewhere. I suppose what you want is an additional method that lowers HP. You call that when the PC loses health. In that method, you check whether a Daemon already exists. If not, you create one. In regenerateHP you then check whether you reached max HP. If yes, you delete the Daemon. So the complete code would look like this:

[spoiler][code]+ me: Actor
maxHP = 10
currentHP = 1
regenAmount = 1
regenDaemon = nil

regenerateHP()
{
	if (currentHP < maxHP) {
		currentHP += regenAmount;
		if (currentHP >= maxHP) {
			currentHP = maxHP;
			// We reached full health. Delete the daemon.
			regenDaemon.removeEvent();
		}
		"You feel a little better. ";
	} else {
		"You are already fully healed. ";
	}
}

loseHP(amount)
{
	currentHP -= amount;
	if (currentHP < 1) {
		// Kill the player, end the game, whatever.
		// ...
		return;
	}
	if (regenDaemon == nil) {
		// We lost health and there's no regen daemon. Create one.
		regenDaemon = new Daemon(self, &regenerateHP, 1);
	}
}

;[/code][/spoiler]

Thanks for the min(CurrentHP, MaxHP) check. I see how that works now. Actually, I took it off from the regenerateHP() method and made it into a new PromptDaemon:

capMaxHP() { CurrentHP = min(CurrentHP, MaxHP); } capHP { HPCapDaemon = new PromptDaemon(MyHP, &capMaxHP); } HPCapDaemon = nil

And had initializeThing() trigger this one too. This way, no matter how I mess around with CurrentHP from the outside, I can be certain that it will never exceed MaxHP.

One question though, why set the Daemons to ‘nil’ before the start? There doesn’t seem to be any difference if I have the game start with them as ‘true’.

Actually, I had made it a Component simply so I could put all the relevant code inside that object and not clutter the Me character’s code. I hadn’t thought about a proper display method yet.

Looking at the two methods, I think hijacking the Component’s initializeThing() serves my idea a bit better. The point was for the regeneration to always be active at all times, and while the loseHP() method looks like it’d serve most possible scenarios, having it running permanently gives me a bit more leeway with how I mess around with the values, else I’d have to remember to reroute everything through loseHP(). For example, if I ever increase MaxHP for any reason, CurrentHP would be less than MaxHP without having called the loseHP() method.

That said, thanks for the loseHP() method, that’s the next thing I was going to get into. Conveniently, I’m a few pages away from learning how to actually end a game, so that’ll come in handy real soon.

I should mention, these are just random theoretical “I wonder how I would do that” ideas I get while trying to get the hang of programming. These are just proof of concept, I’m not making a game just yet.

It can be anything you like. But nil is used just to make it clear that it’s initially just an empty property. That is, make it clear to you, not the compiler.

I also found a bug in my code above. After deleting the daemon, this is needed:

regenDaemon = nil;

To make the property nil again. Otherwise, the check for nil will fail later.

This means you will have code that is executed at all times, even when not needed. This is something that should be avoided since it slows down the game. This one check of course won’t slow it down, but when things like this add up it can have an effect. The rule of thumb is to have code execute only when needed.

The correct way to do this in an object oriented language is to never touch those properties directly. Only the object or class that defines them should do that. You always define methods to manipulate them, and those methods then can make sure that things stay consistent. To increase HP for example, an increaseHP() method should be used.

Since TADS doesn’t offer a way to protect properties from outside access, the norm is to append an underscore to properties and methods that are not intended to be used directly from the outside.

With multiple PromptDaemons, I would recommend setting the eventOrder property on at least one, so that they execute in a controlled order.

The default eventOrder is 100, so setting HPCapDaemon to 200 will make it run after the regen daemon. This avoids the situation where the health is capped first and then the regen amount is added later.

I do it to facilitate a sanity check on whether the daemon has already been initialized. If implementing capHP in an actual game, I would write:

    capHP
    {
        if (HPCapDaemon != nil)
        {
            HPCapDaemon.removeEvent();
            HPCapDaemon = nil;
        }
        HPCapDaemon = new PromptDaemon(MyHP, &capMaxHP);
        HPCapDaemon.eventOrder = 200
    }
    HPCapDaemon = nil

That way, if you ever call capHP() a second time, you will not end up with a second daemon. It wouldn’t matter for that one, but for the regen daemon it could lead to a bug where health was regenerated at double the correct rate.