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.
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!
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.
That’s easy
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;
}
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 Thing
s 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.). TravelConnector
s 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'
)
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) })>>";
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.
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.
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!”
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.
Lol, in the face of staggering gaps in self-awareness, sometimes I get myself exactly right!
RANDOM TADBITS #9: LISTERS
First, a small lecture on the default behavior of LOOK IN OBJECT, and on Lister
s broadly.
Most non-Room
Thing
s 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 Openable
s and SpaceOverlay
s, 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 #define
s 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; }
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 DistanceConnector
s 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 seen
ness 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.
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 DistanceConnector
s! 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)
;
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
.
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.
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.
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.