JWZ's TADbits (#13 : Action Modes)

That’s a good thing for the library to have, and I certainly could have modified the individual Direction objects to have those properties, but my need for it only arose in the endgame after a marathon of a creation period, and it was simpler to just bang out a function for one use case and be done…

Yeah, in game dev there’s always a balancing act between “what can I do to get this thing done” and “how are you actually ‘supposed’ to do this”?

In a lot of programming you have to worry a lot more about the latter, because once you deploy something and it has a user base then it becomes much more difficult to architect around the “oh crap, I should have done this a different way” bits.

In game design I do think that in most cases “well, it works and the game shipped” is a very good argument for a design. But that said, it’s also very easy to paint yourself into a corner where updating something or other on day 100 of the design process suddenly becomes a nightmare because you have to touch a dozen bits of code to make a change instead of one bit. Or whatever.

In the case of things like procedural code that can be encapsulated in a data structure, that’s one of those things where tech debt can really catch up to you unless your design is very stable before you start coding. Which, I don’t know about you, but for me it never is.

3 Likes

This whole thread is gold, but man did this one hit home! I have bemoaned this lack MANY times, yet blithely refused to general-fix it every time. Thanks! Refactor ahoy…

Testify, brother!

2 Likes

Just noticed this. Here’s a substantially more efficient way to do that:

randomInt(min, max) { return(rand(max - min + 1) + min); }

I’m not sure why you’d want to use a Vector in there, but it’s going to make the performance of the function vary with the range of number being generated. For example randRange(1, 10000) would be a thousand times slower than randRange(1, 10). Which is probably not what you want.

1 Like

That’s easy :stuck_out_tongue:

When I got into C++ later, I wrote nearly what you posted:

inline int randRange(int min, int max) {
    return rand() % (max - min + 1) + min;
}
1 Like

RANDOM TADBITS #7: PUSH TRAVEL

There are some idiosyncrasies about using PushTravel verbs, so here I go: I’ll point out the things that either threw me for a loop or seemed helpful to realize.


First, let’s distinguish the different kinds of push travel. Probably the most common kind is that using compass directions: PUSH BOX NORTH. Then we have five verbs of the form PUSH BOX PREPOSITION IOBJ, corresponding to “through”, “into”, “out of”, “up”, “down”. The iobj kinds are all descended from an additional subclass.
Back to compass directions: what is the name of this Action? It’s PushTravelDirAction. If you debug print gAction in the process of PUSH BOX NORTH, that’s the name you’ll get. If you’d like to control how an (probably non-TravelPushable) object responds to this verb, do you type

dobjFor(PushTravelDir) { ... }

Nope! You use dobjFor(PushTravel).

Here's the explanation:

Because all of the library verbs are defined with the use of macros, the actions get set up with certain properties based on which macro is used. If you use DefineTAction(Frump), you get verDobjProp = &verifyDobjFrump, actionDobjProp = &actionDobjFrump and all the rest defined for you. If you use, however, the simpler DefineAction(Frump,SomeActionClass), you are not defining verDobjProp et al for “Frump”, even if SomeActionClass is a TAction, you’re just inheriting whatever SomeActionClass gives you.
Knowing this, we see that the library has DefineTAction(PushTravel) and DefineAction(PushTravelDir,PushTravelAction) and thus PushTravelDirAction’s verDobjProp is still &verifyDobjPushTravel, because it inherited it.

Then we have the iobj forms, which are:
PushTravelEnterAction
PushTravelThroughAction
PushTravelGetOutOfAction
PushTravelClimbUpAction
PushTravelClimbDownAction
These five are defined with the DefineTIActionSub macro, which is just a TIAction in which you can specify other subclasses. Because of that, these verbs do define their own verify/action/etc. methods, so that you can do this:

dobjFor(PushTravelThrough) {
   verify { illogical('You may be able push this object other 
          ways, but you can\'t push it THROUGH.' ); }
}

Does this mean gulp that if you have an object that looks pushable, but is not in fact a TravelPushable, and you’d simply like to give the player a custom message about why they can’t push it, that you have to override all six dobjForPush... blocks?! Happily, no. The library uses the mapPushTravelHandlers macro to set the default behavior of every iobj-based push travel to asDobjFor(PushTravel). Therefore,

dobjFor(PushTravel) {
   verify { illogical('It may seem like it, but you can\'t push this. ); }
}

will catch all six push travel forms. But of course, in the case of the five iobj push travels, you have the option to override any of those with their own dobjFor block if they need specific handling.

Bottom line: don’t ever try to use dobjFor(PushTravelDir); dobjFor(PushTravel) is what to use for directional pushing; and it will also catch the iobj push travels unless you override them specifically (dobjFor(PushTravelGetOutOf) etc.)


It may be helpful to understand the sequence of events under the hood in a push travel action, because it’s not obvious unless you study the source code.
Besides the actual PushTravel… verbs, the other key players are the TravelPushable class and the PushTraveler class. TravelPushable is the class that you give to the luggage trolley, the lawn mower, or whatever is being pushed. PushTraveler is an abstract weirdity that we’ll return to momentarily.
Most Things fail dobjFor(PushTravel) in verify, but TravelPushable allows it (assuming the iobj, if there is one, doesn’t object). In its action phase (let’s say the pushable is a mower) the mower first wraps the gActor in a PushTraveler and then calls gAction.performTravel, which will do a nested action of whatever type of travel action is in play (compass travel, entering, etc.). TravelConnectors don’t primarily care about the Actor that’s traversing them, they care about the Traveler, whether it be a plain Actor, a vehicle that the Actor is operating, or the case we have here. You could think of a PushTraveler like an invisible bubble wrapped around the pushing actor (this all applies to pulling/dragging just as well, by the way) and the pushed object: the TravelConnector recognizes this bubble and views it as a special kind of traveler.
At any rate – going back to the fact that performTravel has called for a basic travel-type action – one of the most fundamental things that happens when a TravelConnector is traversed is that the travelerTravelTo method is called on the Traveler. As our gActor has been previously wrapped in a PushTraveler, it’s the PushTraveler version of that method which gets called, instead of the basic Actor version. In it:
-The mower calls its beforeMovePushable;
-The mower gets baseMoveInto the new destination so that any side effects of its presence are accounted for when the room description is printed;
-The Actor calls its travelerTravelTo which does the notifications and the actual moving of the player;
-The mower gets baseMoveInto back to the origin!;
-And finally if the destination is different from the origin, the mower calls its movePushable, in which the mower itself is moved by moveIntoForTravel and the report is generated by describeMovePushable.

Brainless, right?


If you have a TravelConnector that needs, in its noteTraversal method, to check on things or trigger things related to pushed objects or their contents, here’s what you’ll have to do. PushTraveler contains the properties traveler_ and obj_ for the pushing actor and the pushed object, respectively, so:

noteTraversal(traveler) {
   if(traveler.ofKind(PushTraveler) && traveler.obj_==mower &&
        mower.someCondition)
     // trigger or note something
   else inherited(traveler);
}

This syntax can also be used in canTravelerPass methods of connectors, if for instance you want to make sure that a wheelbarrow doesn’t leave an area if it’s carrying something that shouldn’t leave the area. However, in the specific case of preventing a TravelPushable from using a connector under certain conditions, it’s probably easier to use TravelPushable’s canPushTravelVia method, coupled with explainNoPushTravelVia.


There are also PushNorthAction, PushEastAction etc. If you’re in a block of code that is checking gAction, don’t do this:

if(gActionIs(PushNorth)) ...

because when the player types PUSH BOX NORTH, gAction will be a PushTravelDirAction. PushNorthAction is just a shortcut for coding, if you want to write if(blahblah) replaceAction(PushNorth, self);. If you’re doing checks on gAction you’ll have to do something like:

if( (gActionIs(PushTravelDir) && 
   gAction.getDirection==northDirection) || otherCondition ) ...

(If you used the gDir function described earlier in the thread, you can simplify to && gDir()=='north')

2 Likes

RANDOM TADBITS #8

More tidbits I jotted down as I encountered them while starting to plod through my codebase:


Let’s say you have a statement of code that you only want to be executed one time in the course of a game, maybe when a travel connector is traversed. Of course you can create a new true/nil property somewhere, and

if(!someObject.specialStatementHasFired) {
    // execute statement
    someObject.specialStatementHasFired = true;
}

But if you find yourself wanting to do this often, all of the extra boolean properties and the verbosity of the code blocks are prohibitive. Here’s a little trick that will work for most situations.

"<<first time>><<prince.addToAgenda(oneTimeItem)>>";

The gRevealed table can also be exploited to avoid having to define extra boolean properties everywhere. The above example could also be effected thus:

if(!gRevealed('addItemX')) 
    "<<prince.addToAgenda(itemX)>><.reveal addItemX>";

It’s worth noting that <.reveal> tags are case-sensitive!
You can also mock an EventList with gReveal, along the following lines:

if(!gRevealed('reaction1')) {
    ...
    gReveal('reaction1');
}
else if(!gRevealed('reaction2')) {
    ...
    gReveal('reaction2');
}
else {
    ...
}

I will parenthetically add a final method to hack event list behavior in a pinch, which is to first modify TadsObject E = 0 ; // extraBool, after which you can

if(!E && !E++) { ... } 
else if(E==1 && E++==1) { ... }
else if(E==2 && E++==2) { ... }
else { ... }

The repetitive-looking syntax makes sure that the variable doesn’t get incremented while evaluating blocks that aren’t going to be entered, and also relieves you from having to write a separate statement within the block to increment the counter.


We’re on a streak of referencing embedded expressions in double-quoted strings, so this would be an appropriate time to mention a helpful utility method.
Embedded expressions can call other existing functions and methods, but what if you’d just like to execute a simple statement? For instance, out of the box you cannot do this:

"Some text. <<myProp = nil>>";

but with

modify TadsObject
    cf(func) {  // callFunction
        func();
    }
;

we can then do things like

"Some text. <<cf({: myProp = nil })>>";
"Other text. <<cf({: new Fuse(self, &fuseProp, 5) })>>";
2 Likes

Because I was talking about <.reveal > tags in the last post, I should mention here another related pitfall I fell into.
There is a difference between using gReveal and using <.reveal> tags embedded in strings. gReveal adds the tag to the table immediately upon executing the code statement. The embedded tag, however, does not get added to the table until the very end of the turn when the transcript is showing its reports and everything is getting filtered through the conversationManager. This fact actually burned me a time or two until I figured out what was happening, because there were times I used <.reveal tagXYZ> in a string, but checked for if(gRevealed('tagXYZ')) later in some code before the result of the action got printed. Thus I was getting spurious results to the flow of my logic until I replaced <.reveal> with gReveal in those cases.

1 Like

Do you not need an <<only>> to close that out? Have I been burning 8 chars unnecessarily all this time?

Holy crap, is THAT what’s been going on? I just internalized it as ‘<.reveal X> is sketchier, avoid’ Makes perfect sense now that you say it.

Loving this thread.

2 Likes

Yep, you don’t need the <<only>> if you’re running it to the end of the string! Similarly, as you may already know, you don’t need to close with <<end>> at the end of a string (for instance after an <<if cond>>).

Ah, so that <.reveal> shenanigan bit you too, eh?! I still use <.reveal> liberally, if it’s obvious to me that nothing will be checking for the presence of that reveal on the same turn.

Very glad that it’s been somewhat beneficial to you! I remember mentioning a year or two back that I had a lot of notes from my TADS travels, and you said something like “man, I’d read the heck out of that!” :smiley:

1 Like

I forgot to mention that this was another pitfall/illumination moment for me, because I started out assuming that I could do this:

"Some text. <<{: myProp = nil }>>";

That anonymous function will not be called by the embedded expression! That’s why you have to write a cf / callFunction function…

You may ask: Why not just write that as a couple of code statements? That’s perfectly valid, but since there are so many properties in adv3 that are defined as double-quoted strings, you may find yourself wanting to write a small trigger within some already composed desc, and this saves you from having to rewrite everything as a method with several statements.

2 Likes

Lol, in the face of staggering gaps in self-awareness, sometimes I get myself exactly right!

2 Likes

RANDOM TADBITS #9: LISTERS


First, a small lecture on the default behavior of LOOK IN OBJECT, and on Listers broadly.
Most non-Room Things have four separate listers for their own contents, the context determining which one is utilized:
contentsLister is used in descriptions of rooms or other enclosing objects which contain this object
descContentsLister is used at the end of X OBJECT
lookInLister is used for LOOK IN OBJECT or SEARCH OBJECT
inlineContentsLister is used if this object is already in a list but its contents are to be shown parenthetically
For now we are dealing with the lookInLister. When the player does LOOK IN OBJECT, by default the only action that takes place is that the lookInLister either prints the object’s contents or calls its showListEmpty method. If you define lookInDesc on the object, that will show first before the lister displays. The thing about showListEmpty for this lister is that it is wrapped in a defaultDescReport; this means that if lookInDesc is defined on the object, showListEmpty won’t be printed because the presence of other text eclipses it. And normally that’s what you want, but sometimes you don’t.
Here is a modification that allows showListEmpty to print even if it is preceded by a lookInDesc.

modify Thing
    alwaysSLE = nil
;

modify thingLookInLister
	sleStr(pov, parent) { 
        gMessageParams(parent); 
		return '{You/he} {sees} nothing unusual in {the parent/him}. '; 
    }
    showListEmpty(pov, parent) { 
        gMessageParams(parent); 
        if(parent.alwaysSLE) 
            say(sleStr(pov, parent)); 
        else 
            defaultDescReport(sleStr(pov,parent)); 
    } 
;       
modify LookWhereContentsLister
	sleStr(pov, parent) { 
        gMessageParams(parent); 
		return '{You/he} {sees} nothing ' + parent.objInPrep  + ' {the parent/him}. '; 
    }
    showListEmpty(pov, parent) { 
        gMessageParams(parent); 
        if(parent.alwaysSLE) 
            say(sleStr(pov,parent)); 
        else
    	    defaultDescReport(sleStr(pov,parent)); 
    } 
;

   // an example
item: Container
    lookInDesc = "Text that always shows on LOOK IN regardless of contents. "
    alwaysSLE = true
;

Note that incorporating this modification into a WIP shouldn’t affect anywhere that you’ve already written custom showListEmpty on objects, your mods simply ignore the existence of the sleStr method.
An example usage could be something like a heavy flower pot (using LOOK UNDER instead of LOOK IN, but it’s the same concept as far as not canceling the “empty” message). You want a lookInDesc (in this case a “lookUnderDesc”) to say "You tip up the heavy pot and have a peek under. ", and then you want the lister to do its job of reporting any slips of paper or poorly hidden keys, or else to tell you "Nothing is there. ". Without that alwaysSLE defined, the library would simply say that you peek under and then leave you hanging, if there were no contents to report.


More importantly, I’d like to present a shortcut to make customizing listers much less tedious. I made use of this macro on approximately 180 occasions in Prince Quisborne, and am personally very enthusiastic about it, so I hope someone else can find it useful! First, let’s look at the required typing for an ordinary lister override:

cavity : Container, Fixture
    lookInLister : thingLookInLister {
        showListPrefixWide(itemCount, pov, parent) { 
            "Lying down in the recessed cavity <<is_are>> ";
        }
        showListSuffixWide(itemCount, pov, parent) {
            ", otherwise hidden from view. ";
        }
        showListEmpty(pov, parent) {
            "Nothing is in the cavity. ";
        } 
    }
;

To me, that’s enough typing to make a person say, “Aaahh, the default’s good enough!” Especially if you’d like to reflect your wording changes in three or all four of the listers!
But be glum no more! Now you can do this:

lookLister('Lying down in the recessed cavity <<is_are>> ', '. ', 'Nothing is in the cavity. ')

That’s an argument each for prefix, suffix, and empty list message. The way the macro is set up, you are required to supply a prefix and suffix, but you can pass nil for the showListEmpty argument if you don’t want to customize it.
More good news! There are versions for all four lister types, and not only that, the macro will select the proper base lister class depending on whether the object is a Container,Surface, Underside, RearContainer or no BulkLimiter at all. So you might have objects that look like this:

dam : Container, Fixture
    ctsLister('Stuck down amidst the tangled matter of the dam <<is_are>> ', '. ', nil)
    descLister('Down amongst its branches you see ', '. ', nil)
    lookLister('Down amongst the branches there <<is_are>> ', '. ', nil)
;

There is also the version for inlineLister, which I needed much less frequently, but still made use of. Additionally, there are openLister and abandonLister for Openables and SpaceOverlays, respectively, which each have an extra kind of lister in addition to the four standard ones. Note that you can still make use of the pov and parent parameters, as well as other embedded expressions.
Obviously, if you wish to modify showListItem or some other part of the Lister, then you have to use normal syntax. But this macro set covers the most common cases by far.

Here are the details of implementation if you're interested...

First of all, you probably noticed that <<is_are>>, which we need to define. Voilá:
#define is_are (itemCount > 1 ? 'are' : 'is')
Don’t forget to put #defines in a header, as there are a bunch to follow:

#define ctsLister(pref,suf,sle) initCL() { \
	local lister = selectLister('cl'); \
	local pov = gPlayerChar; pov.name = gPlayerChar.name; \
	local parent = self; parent.name = name; \
	local pm = method(itemCount,pov,parent) { "<<pref>>" ; } ; \
	local sm = method(itemCount,pov,parent) { "<<suf>>" ; } ; \
	lister.macroUsesCustomSLE = (sle); \
	if(lister.macroUsesCustomSLE!=nil) \
	{ local em = method(pov,parent) { "<<sle>>" ; } ; \
	lister.setMethod(&showListEmpty,em); } \
	lister.setMethod(&showListPrefixWide, pm); \
	lister.setMethod(&showListSuffixWide, sm); \
	contentsLister = lister; }

#define descLister(pref,suf,sle) initDL() { \
	local lister = selectLister('dcl'); \
	local pov = gPlayerChar; pov.name = gPlayerChar.name; \
	local parent = self; parent.name = name; \
	local pm = method(itemCount,pov,parent) { "<<pref>>" ; } ; \
	local sm = method(itemCount,pov,parent) { "<<suf>>" ; } ; \
	lister.macroUsesCustomSLE = (sle); \
	if(lister.macroUsesCustomSLE!=nil) \
	{ local em = method(pov,parent) { "<<sle>>" ; } ; \
	lister.setMethod(&showListEmpty,em); } \
	lister.setMethod(&showListPrefixWide, pm); \
	lister.setMethod(&showListSuffixWide, sm); \
	descContentsLister = lister; }

#define lookLister(pref,suf,sle) initLL() { \
	local lister = selectLister('lil'); \
	local pov = gPlayerChar; pov.name = gPlayerChar.name; \
	local parent = self; parent.name = name; \
	local pm = method(itemCount,pov,parent) { "<<pref>>" ; } ; \
	local sm = method(itemCount,pov,parent) { "<<suf>>" ; } ; \
	lister.macroUsesCustomSLE = (sle); \
	if(lister.macroUsesCustomSLE!=nil) \
	{ local em = method(pov,parent) { return(sle); } ; \
	lister.setMethod(&sleStr,em); } \
	lister.setMethod(&showListPrefixWide, pm); \
	lister.setMethod(&showListSuffixWide, sm); \
	lookInLister = lister; }

#define inlineLister(pref,suf,sle) initInL() { \
	local lister = selectLister('icl'); \
	local pov = gPlayerChar; pov.name = gPlayerChar.name; \
	local parent = self; parent.name = name; \
	local pm = method(itemCount,pov,parent) { "<<pref>>" ; } ; \
	local sm = method(itemCount,pov,parent) { "<<suf>>" ; } ; \
	lister.macroUsesCustomSLE = (sle); \
	if(lister.macroUsesCustomSLE!=nil) \
	{ local em = method(pov,parent) { "<<sle>>" ; } ; \
	lister.setMethod(&showListEmpty,em); } \
	lister.setMethod(&showListPrefixWide, pm); \
	lister.setMethod(&showListSuffixWide, sm); \
	inlineContentsLister = lister; }

#define abandonLister(pref,suf,sle) initAL() { \
	local lister = selectLister('acl'); \
	local pov = gPlayerChar; pov.name = gPlayerChar.name; \
	local parent = self; parent.name = name; \
	local pm = method(itemCount,pov,parent) { "<<pref>>" ; } ; \
	local sm = method(itemCount,pov,parent) { "<<suf>>" ; } ; \
	lister.macroUsesCustomSLE = (sle); \
	if(lister.macroUsesCustomSLE!=nil) \
	{ local em = method(pov,parent) { "<<sle>>" ; } ; \
	lister.setMethod(&showListEmpty,em); } \
	lister.setMethod(&showListPrefixWide, pm); \
	lister.setMethod(&showListSuffixWide, sm); \
	abandonContentsLister = lister; }

#define openLister(pref,suf,sle) initOL() { \
	local lister = selectLister('ool'); \
	local pov = gPlayerChar; pov.name = gPlayerChar.name; \
	local parent = self; parent.name = name; \
	local pm = method(itemCount,pov,parent) { "<<pref>>" ; } ; \
	local sm = method(itemCount,pov,parent) { "<<suf>>" ; } ; \
	lister.macroUsesCustomSLE = (sle); \
	if(lister.macroUsesCustomSLE!=nil) \
	{ local em = method(pov,parent) { "<<sle>>" ; } ; \
	lister.setMethod(&showListEmpty,em); } \
	lister.setMethod(&showListPrefixWide, pm); \
	lister.setMethod(&showListSuffixWide, sm); \
	openingLister = lister; }

Then in normal source files:

modify Lister
	macroUsesCustomSLE = nil
;

modify Thing
    initializeThing {
        inherited;
        initListers;
    }
    initListers { 
        initCL(); 
        initDL(); 
        initLL(); 
        initInL(); 
    }
	initCL() { }	
    initDL() { }	
    initLL() { }	
    initInL() { }	

	selectLister(type) {
        switch(type) {
            case 'cl' : 
                if(ofKind(Surface)) 
                    return new surfaceContentsLister;
                else if(ofKind(Underside)) 
                    return new undersideContentsLister;
                else if(ofKind(RearContainer) || ofKind(RearSurface)) 
                    return new rearContentsLister;
                else return new thingContentsLister; 
            case 'dcl' : 
                if(ofKind(Openable)) 
                    return new openableDescContentsLister;
                else if(ofKind(Surface)) 
                    return new surfaceDescContentsLister;
                else if(ofKind(Underside)) 
                    return new undersideDescContentsLister;
                else if(ofKind(RearContainer) || ofKind(RearSurface)) 
                    return new rearDescContentsLister;
                else return new thingDescContentsLister; 
            case 'lil' : 
                if(ofKind(Surface)) 
                    return new surfaceLookInLister;
                else if(ofKind(Underside)) 
                    return new undersideLookUnderLister;
                else if(ofKind(RearContainer) || ofKind(RearSurface)) 
                    return new rearLookBehindLister;
                else return new thingLookInLister; 
            case 'icl' : 
                if(ofKind(Surface)) 
                    return new surfaceInlineContentsLister;
                else if(ofKind(Underside)) 
                    return new undersideInlineContentsLister;
                else if(ofKind(RearContainer) || ofKind(RearSurface)) 
                    return new rearInlineContentsLister;
                else return new inlineListingContentsLister;
            case 'acl' : 
                if(ofKind(Underside)) 
                return new undersideAbandonContentsLister;
                else if(ofKind(RearContainer)) 
                    return new rearAbandonContentsLister;
            case 'ool' : return new openableOpeningLister;
            default: return nil; 
        } 
    }
;

modify Underside
    initListers() { 
        inherited; 
        initAL(); 
    }
    initAL() { }
;
modify RearContainer
    initListers() { 
        inherited; 
        initAL(); 
    }
    initAL() { }
;
modify Openable
    initListers() { 
        inherited; 
        initOL(); 
    }
    initOL() { }	
;

You may be wondering what in the deuce is going on with the local parent and local pov lines. To be honest, I wish I knew better what was going on. But if you don’t define parent and pov as local identifiers, the code won’t compile if you try to use something like <<if parent.isOpen>> in a string passed to the macro. If you noticed, I defined the local pov as gPlayerChar. In practice pov is not always the player. But it turns out that it doesn’t matter much what value you assign to that local variable, because when the lister is used in the real game it’s always reading the pov value that is passed to the showList... method (and same for parent). It’s like it just needs an object value there to get past a certain compilation step without balking, even though that line is never subsequently used. And the absurd parent.name = name lines are there for no other reason but to keep the compiler from warning you for all eternity about unused variables. If anyone understands macro language better than I do and knows how to execute this more elegantly, I’d be all ears!

Another part of the macro that makes me suspect there’s a cleaner way are the macroUsesCustomSLE lines. But that was the only thing I could come up with at the time, and I haven’t felt inspired to try to rewrite it since I got it working.

You should also note that we treated the lookLister slightly differently from the others, because I was using the aforementioned alwaysSLE modifications and wanted to preserve that behavior with the macro. If you would like to use this macro system but are squeamish about using the alwaysSLE modifications in the earlier segment, then replace the look-lister define with this block:

#define lookLister(pref,suf,sle) initLL() { \
	local lister = selectLister('lil'); \
	local pov = gPlayerChar; pov.name = gPlayerChar.name; \
	local parent = self; parent.name = name; \
	local pm = method(itemCount,pov,parent) { "<<pref>>" ; } ; \
	local sm = method(itemCount,pov,parent) { "<<suf>>" ; } ; \
	lister.macroUsesCustomSLE = (sle); \
	if(lister.macroUsesCustomSLE!=nil) \
	{ local em = method(pov,parent) { "<<sle>>" ; } ; \
	lister.setMethod(&showListEmpty,em); } \
	lister.setMethod(&showListPrefixWide, pm); \
	lister.setMethod(&showListSuffixWide, sm); \
	lookInLister = lister; }
1 Like

RANDOM TADBITS #10: sightPresence


Sometimes we have objects that we only want to be “present” under certain conditions. One example of many would be a hole or gap that is only “there” when the correlating covering or insertable object is not present. It can be tedious and bug-prone to try to move this object into and out of nil for every juxtaposition of the other related objects, so we’d rather be able to define one condition on the “hole” object to state when it is there and when it isn’t.
Now, in the Tour Guide (and probably other places in the docs) we are told that we can effect this by twiddling sightPresence, isListed and isListedInContents. For starters, that’s three properties we have to keep in parallel. It seems rather strange that although sightPresence may evaluate nil, the object will still show up in a room’s contents list! But there is an additional drawback to sightPresence, which is that it does not remain parallel with [gActor].canSee(self). You can set item.sightPresence = nil; but gPlayerChar.canSee(item) will still evaluate to true. This can lead to wrong results if you make use of the canSee method (as I did profusely) in condition checks elsewhere.
Enter sightCond: it’s basically what you would think you’re doing by setting sightPresence, but you don’t have to manually synchronize the isListed properties (because the listers won’t even try to print it if they think it’s not seen), and it evaluates parallel to what you get with gActor.canSee(self).

modify Thing
	canBeSensed(sense,trans,ambient) { 
		if(sense==sight) return sightCond && inherited(sense,trans,ambient);
		else return inherited(sense,trans,ambient); 
    }
      // it may be desirable to define this as
      // sightCond = !location ? true : location.sightCond
      // for reasons discussed below
    sightCond = true
    sightPresence = (sightCond && inherited)
;

To a degree you can fake the behavior of Occluder and PresentLater with this single property.
Samples of usage:
-features or markings on a door that can slide back into a pocket: the door remains in scope on opening, but the features don’t
-or a door or portcullis that completely retracts out of view when isOpen
-In the presence of DistanceConnectors you can set sightCond = gPlayerChar.isIn(getOutermostRoom) if the object could only be seen from certain vantage points
-A flame or smoke that are only present when lit
-A fixed object behind something else that hinges

Caveats:
-The seen property may not be updated the instant that sightCond comes to evaluate as true, in which case you would need to call gSetSeen on the object at that point if an immediate check on its seenness is important to you. In the silly example of pawn : Thing sightCond = isIn(gPlayerChar), pawn.seen will evaluate as nil when you start, it will evaluate as nil immediately after you >take pawn, but if your next command is >look, then pawn.seen will evaluate to true. Again, if any of that is important to you, you just have to call gSetSeen(pawn) at the appropriate point to ensure it’s tagged immediately.
-Without further modification (which I did not undertake for lack of need), contents of an object will not go out of scope even if the container is sightCond = nil. Curiously, they will not be listed, but you can interact with them. Furthermore, if they have a specialDesc they will indeed show up even if their container is sightCond = nil. So be aware that involving listed contents in sightCond magic (it’s no better with the old sightPresence way) may require extra tweaking, possibly along the lines of what I put in comments in the code above. If that form was used, you would probably have to use the sightCond property like: sightCond = inherited && newConditions where inherited would make sure that the container could be seen first. I didn’t use it that way so I can’t guarantee results.


2 Likes

RANDOM TADBITS #11: gRevealed and beyond


Often, in some condition or another, we’d like to know if the player has been to such and such a room yet. I don’t know about you, but the quickest thing that jumps to mind for me is if(certainRoom.seen). Usually that works, but if it’s important to know whether the player has been in that room, you can get fouled up by DistanceConnectors! Because they will mark distant rooms as seen even if you haven’t really gone to them yet. There may be plenty of ways around this, but I’ll submit a couple of tweaks that can be nice to have on hand:

modify BasicLocation
    pcVisitCt = 0
        // just adding one line to the library method
    travelerArriving(traveler, origin, connector, backConnector) {
    	foreach (local actor in traveler.getTravelerMotiveActors) {
        	if (actor.posture != defaultPosture) 
				actor.makePosture(defaultPosture); 
		}
        enteringRoom(traveler); 
            // make sure we cover Vehicle-riding PCs
        if(gActor==gPlayerChar && gPlayerChar.isOrIsIn(traveler))
            ++pcVisitCt;     //
            // feel free to add sophistication if you care about NPCs
            // but I think you can get that kind of info from 
            // travelMemory
        traveler.describeArrival(origin, backConnector); 
	}

Now we can check such things with if(certainRoom.pcVisitCt), and we also have access to the number of times the PC has arrived there if we want it. Let’s also be able to easily tell if we’ve crossed a TravelConnector before:

modify TravelConnector
    pcTraverseCt = 0
    noteTraversal(traveler) {
            // make sure we cover Vehicle-riding PCs
        if(gActor==gPlayerChar && gPlayerChar.isOrIsIn(traveler)) {
           ++pcTraverseCt;
        }
        // there's nothing to call inherited for
    }

We can check with if(conn.pcTraverseCt) and also know the number of traversals as well.


I’ve already hinted at my personal exploitation of the gRevealed table, so now I’ll expand on it a little more openly. It’s still perfectly valid to use the gRevealed mechanism specifically to track knowledge revealed to the PC through conversations, but since you can control what tags you define and check for in if(gRevealed(...)), I see no reason why not to use the table for other purposes as well.
Specifically, we can use the table as an effectively inexhaustible source of true/nil properties which A. we don’t have to define on any object, and B. which we can flip to true or nil right within a string, without any additional code statements. “?”, you may say, but first I have to introduce a custom tag which we will create to complement the <.reveal> tag. It’s the <.unreveal arg> tag (or <.ur arg> for short, both will work), which in the past I suggested in a thread about Adv3Lite. I’m not sure whether or not @Eric_Eve incorporated it into that library or not, but if you’re an adv3 user, I’ll go over it here.
So now, <.reveal showRefillNotice> acts like showRefillNotice = true; and <.unreveal showRefillNotice> acts like showRefillNotice = nil;, where we didn’t have to define showRefillNotice anywhere. There are endless ways this could be utilized, but the current example might be something like a room desc:

"Description of stuff in the room. <<if gRevealed('showRefillNotice')>>You notice that the bin of gumbersnudgets has been refilled since you were here last. <.ur showRefillNotice>"

where we imagine that there are other ways that the player could learn about the bin being refilled, in each of which we could use <.unreveal showRefillNotice> so that the room desc wouldn’t print that redundantly when we go there next.

But now that we can both reveal and unreveal tags, let’s reveal tags that will unreveal themselves a certain number of turns later! Now we’ve got a super simple Fuse that will turn a true to a nil. We can call this <.timereveal arg [integer]> or <.tr arg [integer]>, both will work. The [integer] slot should be filled with a number to represent the number turns for which that tag will stay revealed. After that number, the mechanism will unreveal it.
For this also, there is no end to the ways that it could be utilized, but some ideas would be:
-You have some list of special atmosphere/background color episodes that you want to show (whether in sequence or random order), but you don’t want them to all bunch up together. In each episode, you can end the string with "...end of text. <.tr showedRoomXYZSpecial 250>". Whatever the mechanism is, it does something like

if(gRevealed('showedRoomXYZSpecial'))
    // call generic atmosphere;
else 
    //call another special (optionally with random-fire odds)

-You have parser messages that could get accessed many times over the course of the entire game. You’d like to use a witty response every once in awhile but you don’t want to burn it out. So write the message as

'<<if gRevealed('wittyMessageXYZ')>>Generic message. <<else>>Witty message. <.tr wittyMessageXYZ 500>'

-Similarly, if you have tips or helpful reminders for the player that can be triggered by something that they do that seems astray, you don’t want to madden them by printing the same tip over and over if they have some reason they’re repeating something that you didn’t foresee. So you can wrap the tip in a <.tr> and it will only show once per X turns (like, even 1000+), which the player may appreciate if they are playing the game over a long span of time and hadn’t had occasion for the tip to trigger since near the beginning.

Finally, I include <.r arg> as an alternative for <.reveal>, for those who both profusely use the tag and love brevity as I do.
Here’s the code:

modify conversationManager
	patStr = static R'<Alpha>+'
	patDigit = static R'<Digit>+'
	customTags = 'unreveal|timereveal|r|ur|tr'
	doCustomTag(tag,arg) { 
		if(tag=='r') 
            setRevealed(arg);
		else if(tag is in ('ur','unreveal') 
            revealedNameTab[arg] = nil;
		else if(tag is in ('tr','timereveal') { 
			local str = rexSearch(patStr,arg)[3];
			local delay = rexSearch(patDigit,arg)[3];
			setRevealed(str);
			pendingUnreveals.append([str, unrevealDmn.ct + toInteger(delay)]); 
        }
	}
	unrevealDmn: InitObject { 
		ct = 0
		dmn = nil
		events { 
			++ct;
			local cm = conversationManager;
			if(cm.pendingUnreveals.length==0) return; 
			foreach(local cur in cm.pendingUnreveals) { 
				if(cur[2]<=ct) { 
					cm.revealedNameTab[cur[1]] = nil; 
					cm.pendingUnreveals.removeElement(cur); 
                } 
            } 
        } 
		execute { dmn = new Daemon(self,&events,1); }
	}
	pendingUnreveals = static new Vector(6)	 
;
1 Like

Little quibble: code that’s triggered via tags in strings (like <.reveal>) is evaluated during output processing at the end of the action, not the turn.

Most of the time this will work out to be the same thing, but it won’t be if there are other actors in the game (who by default will take their actions after the player, during the same numerical turn, but after things like <.reveal> tags are evaluated). And same thing’s true if there are multiple actions in a turn for other reasons (like implicit actions).

The most reliable way (that I know of) to insure that something runs after everything else in a turn (regardless of the number of actors and actions) is to use a Schedulable with an arbitrarily large scheduleOrder.

3 Likes

Yep, I could have been more distinct there. My mind was in a place of emphasizing that <.reveal> doesn’t happen right away it’s “at the end” when the transcript gets flushed.

2 Likes

I rewrote some code in the <.unreveal> / <.timereveal> section so that the mechanism isn’t dependent on using my RDaemon class…

Yeah. In general TADS3 implements a lot of things in a way that behaves very well in “mostly static” games and can get very finicky in “dynamic” games.

Although there are other hidden gotchas with code executed via tags in output. Like the fact that a <.reveal> tag won’t be evaluated at all if the action is implicit. So with something like:

+pebble: Thing '(small) (round) pebble' 'pebble'
        "A small, round pebble. "
        dobjFor(Take) {
                action() {
                        defaultReport('{You/he} take{s} the
                                pebble.<.reveal pebble> ');
                        inherited();
                }
        }
;
+box: Container, Fixture '(wooden) box' 'box' "A wooden box. ";

Then >PUT PEBBLE IN BOX will involve the player taking the pebble, but because the >TAKE is implicit the report containing the <.reveal> tag will be re-written out of the transcript by implicitGroupTransform.

I’ve had to deal with so many of these kinds of cases I’ve pretty much moved away from relying on embedding anything in output and always explicitly do whatever I want it to do in the action handler or whatever’s generating the output.

1 Like

If you by chance copied the code for the sightCond functionality, you may want to erase
modify Intangible sightCond = nil.
I didn’t actually include that statement in my game, and thought I was being thorough by tacking it on to the code in the post. But I think it messes up scope stuff. I’ve removed that part from the thread.