Note that this will mark the room as visited when any Actor
(or vehicle!) enters the location. If there are no NPCs or vehicles in the game (or if they never move without the player) this is safe, but if you have anything else that’ll ping travelerArriving()
that’ll mark the location as visited
independent of who or what is doing the travelling.
That’s a good catch, because this isn’t how I wrote the code in my game.
I had a separate block in travelerArriving
that only gets entered if traveler==gPlayerChar
which is where I flipped the visited
, but then I didn’t notice that when I slimmed the method down for public consumption. It was the same story with noteTraversal
… in my game it was only triggered by the PC.
Will add the if
clauses…
Edit: I rewrote both cases as counters so authors can test for visited/traversed or the number of visits/traversals with one property.
Checking if the traveler is gPlayerChar
is good, although I think the “correct” approach would also need to check to see if the traveler is a vehicle containing the player as well. Although I guess that depends on whether or not you care about the difference between the player being directly in a location versus being in a vehicle in the location. And if you have vehicles at all.
I use a couple convenience methods on Thing
patterned after getOutermostRoom()
to handle this sort of thing (checking if an object’s containers or contents match some criteria):
searchContainment(fn) {
local obj;
if(location && ((obj = location.searchContainment(fn)) != nil))
return(obj);
return(((fn)(self) == true) ? self : nil);
}
searchContents(fn, recurse?) {
local v;
v = new Vector();
if(recurse) {
contents.forEach({
x: v.appendAll(x.searchContents(fn, recurse))
});
}
v.appendAll(contents.subset({ x: (fn)(x) == true }));
return(v);
}
Those both take a test function and return (respectively): the outermost containing object the method returns true
for; and all the objects in the Thing
’s contents
for which the function returns true
, recursively if the second arg is true
.
Then I add special-purpose checks built on these methods as needed, like:
getOutermostClass(cls) {
return(searchContainment({ x: x.ofKind(cls) }));
}
getContentsMatching(cls, recurse?) {
return(searchContents({ x: x.ofKind(cls) }, recurse));
}
…which just specifically check for objects matching an ofKind()
check. As an example of usage, you could re-write getOutermostRoom()
to be getOutermostClass(Room)
.
That all said, I also tend to approach things like keeping track of a visited
flag on locations by using a table on the actor instead of a flag on the instance. Because I’m usually worried about keeping track of multiple actors instead of just the player…but that’s probably an idiosyncrasy of what I’m doing.
Yep, no doubt; and this is almost getting comical because this is also something I did in my game, but failed to account for when trying to resurrect concepts for the purpose of this thread. I’ve been away from intensive TADS coding for about a year now, so many of my practices that were second nature have grown hazy already.
I like your idea of recursive containment utility functions. In practice, for these traveler methods we’re talking about, I simply used a macro (surprise!) that expanded to if(gActor==gPlayerChar && gPlayerChar.isOrIsIn(traveler))
which takes care of the recursion for you in this particular case.
For indeed, I utilized Vehicles
and I wanted the Room
s or TravelConnector
s to register as visited/traversed even if the PC was in a Vehicle
.
I’m going to go back and update that post again to say isOrIsIn(traveler)
…
Do you use travelMemory
at all, then? It sounds like the kind of info you’re tracking. I set my travelMemory
to nil
on account of the overhead, because although I have some traveling NPCs, they’re pretty well governed and I didn’t have need for that detailed of info.
Not for that kind of thing. I haven’t stared to intently at the travelMemory
code, but it’s all keyed by actor and connector (I think the only thing adv3 uses it for is travel connector descriptions). And I was already doing per-actor tracking of this kind of thing from my rewrite of the sense memory stuff, so one more flag to keep track of wasn’t a big deal.
Not related to any of that, since I think this is a general T3 techniques thread: is there a neater way to handle comma-separated adjective lists than adding a comma to the object’s adjective list. That is given;
+pebble: Thing '(small) (round) pebble' 'pebble' "A small, round pebble. ";
…you get by default…
You see a pebble here.
>x pebble
A small, round pebble.
>x small, round pebble
You see no small here.
The trivial-ish “fix” is to just add a comma to the vocabulary for the object:
+pebble: Thing '(small) (,) (round) pebble' 'pebble' "A small, round pebble. ";
…giving…
You see a pebble here.
>x small, round pebble
A small, round pebble.
…or more generally, something like:
modify Thing
initializeThing() {
inherited();
initializeAdjComma();
}
initializeAdjComma() {
if(adjective == nil || (adjective.length < 2))
return;
if(adjective.indexOf(',') != nil)
return;
adjective += ',';
cmdDict.addWord(self, ',', &adjective);
if(weakTokens)
weakTokens += ',';
else
weakTokens = [ ',' ];
}
;
But this feels like a bit of a kludge.
Looking at the grammar I would have naively expected something like this to work:
// THIS DOES NOT WORK DO NOT COPY
grammar adjPhrase(adjCommaAdj): adjective->adj_ ',' adjPhrase->ap_
: NounPhraseWithVocab
getVocabMatchList(resolver, results, extraFlags) {
return(intersectWordMatches(adj_, &adjective, resolver,
extraFlags, VocabTruncated,
ap_.getVocabMatchList(resolver, results, extraFlags)));
}
getAdjustedTokens() {
return([ adj_, &adjective ] + ap_.getAdjustedTokens());
}
;
…but it doesn’t; the parser will preferentially handle >X SMALL, ROUND PEBBLE
as one verb and two noun phrases (so equivalent to >X SMALL
and >X ROUND PEBBLE
) and fail out before considering any adjective productions.
I haven’t dug in and tried coming up with alternate noun phrase productions that would pre-empt the stock behavior yet though.
On this modify to BasicLocation, I note that the issue of “room marked visited by any Actor” you point is what one actually needs in coding a Suspended style game…
Best regards from Italy,
dott. Piergiorgio.
RANDOM TADBITS #12
I’ll share another way in which I made prolific use of the <.timereveal arg [int]>
tag. (In practice, it was a <.timereveal arg [int]>
tag, but I actually made a custom tag for simplicity.) In my case, it involved a sidekick NPC which you could count on being present with the PC all game long except for a very few isolated occasions. I suspect, however, that the same principle could find other Actor
-related applications. Before I go any further, I will say, that there are certainly more sophisticated approaches one could take to coordinating what Actor
s do or don’t do on a given turn based on what other Actor
s might be doing. But this is at least a very painless way to get a lot of effectiveness.
Here’s the situation I had in my game: Prince Quisborne is your (virtually) constant companion, and due to the massiveness of the game world and the exorbitant volume of background activity that the prince is equipped to do (our @jjmcc referred to it as “a frankly deranged amount of time…devoted to incidental dialogue”), absolute control of exactly when his “fiddles” take place is completely untenable. Moreover, believe me when I say that there was no ever-loving chance that PQ’s every word was going to be elegantly compartmentalized into AgendaItems
, EventList
s, InitiateTopic
s etc. Nope, in addition to a bewildering passel of all those items, his voice is hard-coded into certain action responses and other sequences in hundreds of places throughout the code (there’s not really a way to shake him off your tail, so it’s not as risky as it sounds).
So when it’s PQ’s “turn”, how does he know not to do a “fidget” move when his voice has already appeared through hard code in the PC’s turn? I just drop a <.pq>
tag in any string where his voice or actions are hard-coded. The <.pq>
tag is just shorthand for <.timereveal suspendPQ 0>
, which means that from the end of the PC’s turn to the end of the global turn, 'suspendPQ'
will be gRevealed
. Which also means that on Prince Quisborne’s “turn”, he can check if(gRevealed('suspendPQ'))
before he determines whether or not to do background chatter. Important AgendaItems
don’t have to be deterred by the <.pq>
tag if they don’t wish.
Secondly, there are also lots of action sequences triggered by the player’s input which may not reference PQ’s words or actions directly, but which are significant enough happenings that it would sound rather trite of Prince Quisborne to try to catch a butterfly or whatnot on the same turn that something semi-important happened. Voilá, the <.pq>
comes in again. I doubt that I 100% prevented the prince from saying something a tad out of place, but I can tell you that it would be absolute chaos without the <.timereveal>
tags!
As mentioned, you can probably find reasons to silence subsequent Actor
s after a <.yourTag>
tag shows, so here is all you need to add, if you already have the unreveal and timereveal code copied:
modify conversationManager
customTags = '...|yourTag'
doCustomTag(tag, arg) {
...
else if(tag=='yourTag') {
setRevealed('suspendXYZ');
pendingUnreveals.append(['suspendXYZ',0]);
}
}
I found that when you are in a room on one side of a DistanceConnector
and you try to interact with something in a room on the other side, the default tooDistantMsg
is often unsatisfactory. For one thing, tooDistantMsg
is a very generic apology that is utilized by:
-almost every action attempted on a Distant
object
-a great many actions attempted on an OutOfReach
object
-fail conditions in the TouchObjCondition
class
-as well as trying to reach or throw across a DistanceConnector
.
I defined a response method on the Room
class, so that if gActor
tries to touch an object in that Room
, while they are in a remote one, a tailored message can be printed, with the targeted object and the actor’s room as parameters. Á la:
catwalk: Room //Distance connected to room1North and room1South
tooFarDConn(obj,actorRoom) {
if(actorRoom==room1North)
return 'You can\'t access the catwalk from the north end of the area. ';
else return 'Even though you\'re in the south end, there are reasons
why you can\'t access the catwalk from here. ';
}
;
Where, if you don’t care to override the method for a given Room
, the normal tooDistantMsg
will be used.
modify playerActionMessages
tooDistantMsg(obj) {
if(!obj.isIn(gActor.getOutermostRoom))
return obj.getOutermostRoom.tooFarDConn(obj, gActor.getOutermostRoom);
else return tooFarTxt(obj);
}
tooFarTxt(obj) {
gMessageParams(obj);
return '{The obj/he} {is} too far away. ';
}
;
modify Room
tooFarDConn(obj,actorRoom) {
return gActor.getActionMessageObj.tooFarTxt(obj);
}
;
The changes could potentially have been overridden in DistanceConnector.checkTouchThrough
and checkThrowThrough
but the idea is about the same.
I have touched on this little tidbit in a past thread, but for consolidation’s sake, I’ll revisit it. Unthing
s: a great deal of the time that we use them they are representing another one of our game objects that is not currently present. So why retype the vocabulary and possibly get it out of sync with the real object anyway? Set an unObject
property to the real object it correlates to, and be done.
modify Unthing
unObject = nil
initializeThing {
inherited;
if(unObject) {
initializeVocabWith(unObject.vocabWords);
name = unObject.name;
}
}
;
//IN A HEADER
Unthing template @unObject 'notHereMsg'? ;
And now you can:
+unEdna: Unthing @edna 'Edna isn\'t here presently. ' ;
With the further suggestion that you could
class Unperson
notHereMsg {
if(unObject)
return '\^<<unObject.theName>> <<unObject.isPlural ?
'aren\'t' : 'isn\'t'>> here presently. ';
else return 'You\'re referring to someone you don\'t see here. '; }
;
so that the notHereMsg
sounds acceptable for people if you don't override it.
I don’t have time to check at the moment but I suspect it’s because the fundamental grammars for command structures involve commas and they’re taking precedence by some criterion or other…
@jbg did you use MJR’s grammar debug function to see if your grammar was being recognized but beaten out by a winner?
Commas are used as a command separator as well, but that’s not what’s happening here. Like I said, the parser is preferring to handle “small, round pebble” as a noun list.
Turning parse-debug on
will tell you this, although it unfortunately won’t tell you why.
The grammar debugger only gives information about the productions it matches that have vocabulary. So it’ll tell you that the “winner” for “small, round pebble” is nounList(list)
, and then it’ll break down the productions it’s selecting for “small” and “round pebble”, but you have to sift through the grammar rules (in lib/adv3/en_us/en_us.t
) to figure out that nonTerminalNounMultiList(pair)
includes completeNounPhrase->np1_ nounConjunction completeNounPhrase->np2_
and nounConjunction
includes “,”.
So like I said I think you’d have to add an alternate completeNounPhrase
production that includes the adjective phrase production and one of the noun phrase productions, but I don’t know if that’ll cause any problems (because normally the adjective phrase productions are already part of the noun phrase).
Stealth edit: after typing out the above but before posting it, I tried this, which I think works:
grammar adjectiveConjunction(main):
','
: BasicProd
isEndOfSentence() { return(nil); }
;
grammar completeNounPhrase(adjConj):
adjPhrase->ap_ adjectiveConjunction completeNounPhrase->np_
: NounListProd
resolveNouns(resolver, results) {
return(np_.resolveNouns(resolver, results));
}
getAdjustedTokens() {
return(ap_.getAdjustedTokens());
}
;
Actual edit: I’ve extended the above to include “and” (and so also matching >X SMALL AND ROUND PEBBLE
) and added it to a little module of adv3 patches I’ve been accumulating: adv3Patches github repo.
The module in theory includes only “pure” patches…fixes instead of things that extend functionality. There’s a bit of judgement involved in drawing the line, but my reasoning here is that responding to >X SMALL, ROUND PEBBLE
with “You see no small here.” is never the desired behavior, so this is a fix instead of a feature add.
The other things currently in the module are the fix for FOO, BAR
throwing an exception (a bug in NounPhraseWithVocab
) and a fix for token parsing in ThingState
(making token matching case-insensitive (like token parsing everywhere else in adv3) and correctly handling possessive forms with an apostrophe-S (like they are handled everywhere else in adv3)).
I see what you’re saying, but if the player understands that usually periods string multiple commands together and commas string multiple dobjs together for one verb, one could say that the parser is in fact doing what it’s expected to, by trying to first examine a “small” and then examine a “round pebble”. But to be sure, if you’re trying to cater for players unused to the conventions then it is a nice add for the parser to recognize the adjective list if no dobj list pans out…
Whether it’s a fix or a feature I think it’s a good add… have you done much testing to see if any preexisting parser behavior is affected by the presence of the new grammar?
What if you had a ‘small toy’ in scope, whose “small” was not a weak token, would the parser take “x small, round pebble” as x toy and x pebble?
In general that kind of thing works fine (I’ll cover the cases I could think of below), but I did find a different problem: something like >X SMALL, ROUND PEBBLE
would match the pebble if a) “small” was a valid vocabulary word at all, and b) “small” was not in the pebble’s vocabulary. That is, parsing would match an object when given an adjective list, even if all the adjectives didn’t match the object.
I think this works where the previous version doesn’t:
grammar simpleNounPhrase(adjConjNP):
adjWord->adj_ adjectiveConjunction simpleNounPhrase->np_
: NounPhraseWithVocab
getVocabMatchList(resolver, results, extraFlags) {
return(intersectNounLists(
adj_.getVocabMatchList(resolver, results, extraFlags),
np_.getVocabMatchList(resolver, results, extraFlags)));
}
getAdjustedTokens() {
return(adj_.getAdjustedTokens() + np_.getAdjustedTokens());
}
;
This handles >X SMALL, ROUND PEBBLE
as:
>X PEBBLE
if the pebble has “small” and “round” in its vocabulary, regardless of whether there are other objects with “small” or “round” in their vocabularies>X SMALL
andX ROUND PEBBLE
ifpebble
does not have “small” in its vocabulary and a different object in scope does- A failure if
pebble
does not have “small” in its vocabulary and nothing else in scope does either
That is, if “small, round pebble” can be successfully resolved to a single in-scope object, it will be. If that is not true, and “small” and “round pebble” individually can be successfully resolved into two in-scope objects, that’s what will happen. And if none of the above is true, the action will fail, with a no matching objects in scope failure message.
And speaking of that, I’ve added a patch to noMatchCannotSee()
that checks to see if the failed token is exclusively used as an adjective in the game, and if so, uses a failure message of the form “You do not see anything small here” instead of "You see no small here. "
Addendum:
In the general spirit of the thread’s “we’re trying to point out pitfalls and traps to each other here”, I’ll add that the reason why the prior version of my “fix” didn’t always work as expected is that for some reason the adjPhrase
productions are (apart from my addition) only ever used by adv3 in plural noun phrase productions. Which is something it never even occurred to me might be true until I tried to hunt down the weird behavior. The indetPluralNounPhrase
productions are the only things in stock adv3 that reference adjPhrase
.
That’s pretty slick!
RANDOM TADBITS #13: ACTION MODES
In which I merely offer some reference material which can be cumbersome to track down in individual bits and may prove handy to be found in one place.
We have basic, “top-level” actions, we have remapped actions, we have nested actions, we have replaced actions, we have implicit actions, we have newAction
s, and we have EventAction
s. What does it all mean, how do they differ, and what are the ramifications of the different uses?
First let’s mention a couple properties that an Action
possesses, which will be used by many of the variations. These are parentAction
and originalAction
, which have slightly different applications and implications. Library comment: “an action with an original action is effectively part of the original action for the purposes of its reported results.” Other properties such as isImplicit
are rather clearer in what they mean.
The broadest subcategory is “nested action”. Every nested action has a parentAction
, which is probably the action that the parser resolved from the player’s typed input. The “generic” nested action is called internally by something ultimately triggered by code in the main action, and when it is finished the control flow returns to whatever part of the main action called it and continues. From the player’s point of view it is supposed to be a part of the main action, and not a separate action. The askForObj
macros and other retryWithMissing
routines run as nested actions.
NESTED ACTION
- has a non-nil
parentAction
- also has a non-nil
originalAction
UNLESS it is also an implicit action! - doesn’t add a command separator to the transcript
- doesn’t create any new transcript: uses its
parentAction
’s - does not call
gPlayerChar.noteConditionsAfter
indoAction
- does not have option to cancel remainder of command line in
afterActionMain
- uses
parentAction
’sgetOrigTokenList
and other token info - does not set any pronoun information in
TAction.doActionMain
- doesn’t add “new iteration” marker to transcript when looping through
dobjList_
fordoActionOnce
- doesn’t use
savepoint
or store undo information inexecuteAction
- not included by
saveActionForAgain
actionTime
is zeroed except as noted below- when a nested action is run, it starts as a prepared
Action
instance whosedoAction
method is called with sense context - its output does not overshadow any
defaultReport
s of itsparentAction
- once the nested action is complete, the main action continues executing
We all know and love the replaceAction
macro. A “replaced action” IS A NESTED ACTION with some key changes to the above list:
REPLACED ACTION
- after the replacing/nested action executes, the
exit
macro is invoked, thus the actor’s turn ends immediately after this nested action - in the case of a replacing/nested action, the
parentAction
’s (and any enclosing actions’)actionTime
is zeroed, and the replacing action’sactionTime
is used for the turn
“Remapped” actions are a separate concept which can yet have some overlap with the other modes of action. They can be, in essence, “top-level”, nested, or implicit.
REMAPPED ACTION
- in any case, the
remappedFrom
andoriginalAction
properties will be non-nil - remapped actions DO NOT have a
parentAction
just by being remapped: potentially could have one for another reason - remapped actions can be generated/run in two ways:
• they are triggered by aRemapActionSignal
while resolution is still taking place (caught byexecuteAction
), in which case the original action is swapped out for the remapped one and the process continues a “normal” execution
• they are triggered after objects have been resolved (theremapTo
macro, etc.), in which case they are run as a replaced action. The action instance created to be run copies its token info and implicit status from the main action, and sets itsoriginalAction
to that action before running. - remapped actions may use a different approach in the transcript: there may be a special fully phrased announcement where ordinarily a single object might have been named for clarification
“Implicit” (which I think is the same thing as “implied” in the library comments?) actions are perhaps a bit more recognizable than the other kinds, mostly because the transcript transforms them so obviously. Implicit actions are most frequently triggered by PreCondition
s trying to do the tedious-but-realistic drudge work for the player, but can of course be called explicitly by game code.
IMPLICIT ACTION
- is also “nested” in the sense that it has a
parentAction
; the library says “all implicit commands are nested” - differs from “nested” in that it DOES NOT set
originalAction
to the main action. Library comment: “implied action does not count as nested or replacement action for purposes ofgetOriginalAction
” showDefaultReports
is set to nilincludeInUndo
is set to nilisImplicit
set to trueimplicitMsg
typically set to a value- does not
actor.addBusyTime
inafterActionMain
- aborts in
doActionOnce
if the verify results do notallowImplicit
- regular action output is converted by the transcript into the parenthetical “first verbing…” forms if determined that its output should show at all
- adds command separator to transcript if performed by NPC, both before and after the action
The newAction
macro is slightly sneaky. It does create a fresh Action
with no parentAction
s or originalAction
s, and it is called with its own fresh transcript, using sense context in the usual way, that if it is an NPC they have to be in sight for the transcript to show. But note that newAction
DOES NOT invoke exit
after it completes, which means that any outputted text in the original action after the call to newAction
will just be tacked on right after the output from newAction
, without any command separation. So this
action {
"Text. ";
newAction(OtherVerb,otherObj);
"More text. ";
}
is dubious, and you should probably either always put newAction
as the final statement, or use exit
afterward, or add your own command separator unless you’re really going for some kind of integrated text (but then it might make more sense for it to be a nested action?)
An EventAction
is just the skeleton of an Action
that is used to provide a gAction
, gActor
, and a command transcript in sections of code that don’t otherwise come thus equipped. The library runs the first lookAround
of the game as an EventAction
; Daemon
s and Fuse
s are run within EventAction
s, and even executeTurn
starts by calling executeActorTurn
within an EventAction
environment.
RANDOM TADBITS #14: Actors not repeating themselves
It’s entirely possible that most players aren’t a bit fazed by NPCs giving verbatim responses to the same inquiry, but I felt better about trying to recap what the NPC said before, rather than simulating them uttering the exact words an indefinite number of times. If you had to hand-write the event list for every TopicEntry
in the game, it wouldn’t be worth the trouble, but…
With just a little bit of implementation code and almost no overhead in our declaration code we can get the following behavior from any Ask or Tell topic that we choose:
> ask igor about xyz
"Of all the xyz's I've ever seen, it's one of them. "
> g
Igor had said:
"Of all the xyz's I've ever seen, it's one of them. "
That is, if the topic is not one for which it is important to provide a differing second or third response, we get an automated prefix recapping the first response rather than simulating the actor repeating themselves verbatim over and over again. Programmatically, the non-first invocations of the TopicEntry
are !isConversational
because we’re simulating that the PC doesn’t actually address the actor about this topic again, but is rather recalling the previous response.
Critical topics that need to change or offer additional information with subsequent ASKs shouldn’t use this TopicEntry
subclass, but the class will give greater polish to a whole host of secondary-importance topics.
The prefix used in the above example works best with topicResponse
s that are pure quotation. But if you prefer writing your responses more like
"Of all the xyz's I've ever seen," Igor sighs, "it's one of them. "
the prefix string could be modified to something like
[Recalling what transpired previously]
"Of all the xyz's..."
Here’s the code:
/* NV here stands for "non-verbatim".
Modify the class naming as desired.
*/
// usage
+ Actor 'igor' ;
++ NV, AskTopic @xyz
//OR ++ AskNV @xyz
'\"Of all the xyz\'s I\'ve ever seen, it\'s one of them.\" '
;
// implementation
class NV : StopEventList
eventList = [
txt,
prefixStr + sep + txt
]
txt = ''
sep = '\n'
prefixStr = '\^<<getActor.theName>> had said: '
isConversational = (talkCount == 0)
;
/* If used as a mix-in and using templates,
NV has to come before (to the left of) the
TopicEntry so its template will be chosen.
Or use/add to the combo subclasses below:
*/
// class AskNV : NV, AskTopic ;
// class TellNV : NV, TellTopic ;
// class AskTellNV : NV, AskTellTopic ;
// class AltNV : NV, AltTopic ;
// PUT IN HEADER FILE
NV template +matchScore? @matchObj 'txt' ;
NV template +matchScore? [matchObj] 'txt' ;
/* Note that we're defining these topics with
single-quote strings instead of the usual
double. And clearly we can use the "sep"
property to eliminate line breaking or to
insert a blank line instead.
*/
/* An approach to avoid excessive subclassing
per actor:
class NV ...
prefixStr = getActor.NVprefix
...
;
modify Actor
NVprefix = '\^<<theName>> had said: '
;
*/
RANDOM TADBITS #15
Here is a tip that will be old news to programmers who have been around the block, but will be a boon for those who aren’t aware of its existence, because I don’t remember it being clearly pointed out in the docs.
First, the problem. You want to comment out a big section of something for testing or troubleshooting purposes (perhaps a long copy-pasted method from the library with small tweaks in it), but that big section contains numerous /* block comments */ within it, and the compiler has issues with nesting of block comments. So, you can do
#if 0
method {
...lots of lines here
/* comments galore */
...more code
}
...more stuff to comment out
#endif
Works for C++ too.
Of asExit
: a caution.
It’s a common practice to define a direction property to return different TravelConnector
s under different circumstances, like so:
room1 : Room
east = thicket.described ? wrigglePathThruThicket : east2
east2: NoTravelMessage { "It appears to just be a whole lot of thicket. " }
Now, if the land is going downhill to the east, or if the thicket is sort of east and southeast of us, we might
room1 : Room
east = thicket.described ? wrigglePathThruThicket : east2
east2: NoTravelMessage { "It appears to just be a whole lot of thicket. " }
southeast asExit(east)
down asExit(east)
Don’t do it! You will rue the day! All will be lost! Actually, what will happen is that the proxy connectors created by asExit
will evaluate that ternary expression at the beginning of the game and they will permanently link to that initial evaluation (the NoTravelMessage
).
Remember that if you try to get around this by
southeast = east
you’ll end up with both “east” and “southeast” in the exit lister, which could possibly ruffle players a bit if both exits do the same thing.
One possible solution would be:
southeast: TravelConnector {
dobjFor(TravelVia) remapTo(East)
isConnectorListed = nil
}
A word on the parser and vocabulary words. The learning docs are pretty clear early on that in the case of nouns, the parser will automatically recognize an “of” for you in between any two nouns in your list. Thus we can define 'pair/shoes'
and “pair of shoes” will be accepted. Now, it’s been a long time since I’ve gone all the way through the docs, but I don’t remember there being clear direction that this gratuitous “of” can apply to adjectives too. As a matter of fact, if we give the 'Village of Anatevka'
the following vocab:
'intimate obstinate village/anatevka'
the parser will pick this object in response to
>x village of anatevka (noun-noun)
>x village of obstinate (noun-adj)
>x obstinate of village (adj-noun)
>x intimate of obstinate (adj-adj)
I typed a whole lot of 'adj adj (of) noun/noun'
before realizing that!
Alas, in the case of “anatevka village”, the preceding vocab won’t suffice. I’m not aware of any way to deal with that other than moving ‘anatevka’ to the adj section (suboptimal if other objs like buildings use ‘anatevka’ as an adj, because then a plain “anatevka” may get a disambig prompt) or writing ‘anatevka’ in both the adj and the noun sections (which I have done repeatedly with less than complete satisfaction. I didn’t feel like rewriting the vocabWords
parsing routine at the time. It seems to me a simple symbol preceding a word could indicate that it be added to both noun and adj properties. )
For instance 'clothes/pile'
will get you “clothes”, “pile”, “pile of clothes”, but not “clothes pile”. Whereas 'clothes pile'
would get you all of the above plus “clothes pile”, but if there was a “clothes rack” or something else in view, a bare “clothes” would prompt for disambig when the player obviously means the clothes.
A separate form of parser behavior worth being aware of.
In the case of
class Flower '*flowers' 'flower' ;
room1 : Room ;
+ Thing 'bunch/flowers' 'bunch of flowers' isPlural = true;
+ Flower;
+ Flower;
+ Flower;
you will get
>x flowers
flower: You see nothing unusual about it.
flower: You see nothing unusual about it.
flower: You see nothing unusual about it.
that is, plural vocab (words after the asterisk in vocabWords
) has precedence over noun words (that may or may not be plural in sense).
At least, this is the case whenever the verb allows multiple objects. If you >put coin on flowers
, it will attempt to put the coin on the bunch of flowers after all, because multiple indirect objects cannot be used with PutOn
.
Just to be clear, there are certainly times when it may be valid to let the parser prompt for disambiguation between similar objects, because the outcome of that action may be significant. However, much more often, the parser will dutifully but brainlessly provide disambiguation to the player when the result is of no consequence whatsoever (the objects in question will all yield a nearly identical result). My opinion is that players will thank you and respect your game more highly if you set up the parser to instead pick one of these inconsequentials (it will announce which one it chose anyway) and just proceed with the action. In the rare case that they expected this action to happen to a very specific individual amongst many similar, they can see by the announcement whether it was their intended target or not, and can use more specific vocab on the next try.
Because this situation happens very often in a custom class, I suggest that you could add this line to the class:
vocabLikelihood = self == objChosenAsDefault ? 100 : 99
// OR, if the objects are not named, but have some distinct property like "top", "middle", "bottom"
vocabLikelihood = self.name == 'top drawer' ? 100 : 99
Now the player can type >feel drawer
and you’ll get
(the top drawer)
The bureau drawers are all polished smooth.
rather than a disambig prompt, all of whose options will yield the same answer. Note that in the given example, we’re assuming that this bureau is rather inconsequential: if there were a really important reason that one or other of the drawers should or shouldn’t be opened, we wouldn’t want vocabLikelihood
to pick one at random. If that were the case you could
vocabLikelihood = !gActionIs(Open) && self.name == 'top drawer' ? 100 : 99
Embedded expressions: points to be aware of!
It is ultimately very handy that within a string we can use those nifty double angle brackets to enclose either 1-quote string values or 2-quote string values. But that versatility introduces an element of complexity that can possibly trip you up.
If you’re planning to create a string property on an object, and that string will be embedded in another string somewhere, choose a 1-q string when you have a choice. 2-q’s have the potential to print out of order from what is intended.
For instance, let us say we have a doorish object with writing on it. We’ll want it to be Readable
so players can >read gate
. So we’ll likely be defining a 2-q readDesc
. We might try:
desc = "This is a blockading gate that is both adjective1 and adjective2.
<<!isOpen ? 'It\'s not openable at the moment, but an included
inscription seems to give instructions to that end. It says:
<<readDesc>>' : 'It\'s open and not so much a blockade anymore,
so you\'re probably not that interested in the writing
that\'s on it. '>>"
readDesc = "JUMP. JUMP. WAIT. "
If we haven't opened the gate yet, that will print:
This is a blockading gate that is both adjective1 and adjective2.
JUMP. JUMP. WAIT. It\'s not openable at the moment, but an included
inscription seems to give instructions to that end. It says:
In other words, the 2-q readDesc
gets immediately executed before the ternary operator has finished deciding which of its strings it's going to pick.
I’d like to point out that the same desc could be executed in proper order with this differing syntax:
desc = "This is a blockading gate that is both adjective1 and adjective2.
<<if isOpen>>It\'s not openable at the moment, but an included
inscription seems to give instructions to that end. It says:
<<readDesc>><<else>>It\'s open and not so much a blockade anymore,
so you\'re probably not that interested in the writing
that\'s on it. "
But the point is that embedded 2-q strings have the potential to complicate things, so try to embed 1-q strings whenever possible, and be wary and test thoroughly when embedding 2-q’s. Additionally, I’d recommend making a habit of the second syntax except when the two alternatives are just words or short phrases, rather than whole sentences or paragraphs.
RANDOM TADBITS #15b
Since I just spent the last installment wailing about how adv3 doesn’t allow vocab words to act as both noun and adjective (without typing it twice), I thought I’d do something about it.
Now if you define “London Town” like this:
vocabWords = '$london/town'
you get not only “london” (as noun), “town” (as noun), and “town of london”, you also get “london town”. As you may have surmised, ‘london’ has been added to both the &noun and &adjective properties of the London Town object, so it will always respond with noun seniority when applicable, but it will be accepted in an adjective-only slot as well.
Just put a dollar sign before any token that you want to serve as both adjective and noun. Within either section is fine; this would’ve worked too:
'jolly $london old town/city'
Here’s the code, which is nothing but a few extra lines added to initializeVocabWith
:
Summary
modify VocabObject
initializeVocabWith(str)
{
local sectPart;
local modList = [];
sectPart = &adjective;
while (str != '')
{
local len;
local cur;
if (str.startsWith('"'))
{
len = str.find('"', 2);
}
else
{
len = rexMatch('<^space|star|/>*', str);
}
if (len == nil)
len = str.length();
if (len != 0)
{
cur = str.substr(1, len);
if (sectPart == &adjective
&& (len == str.length()
|| str.substr(len + 1, 1) != ' '))
{
sectPart = &noun;
}
if (cur != '-')
{
local wordPart = sectPart;
local nounAndAdj = nil; //--->//
if (cur.startsWith('$')) {
cur = cur.substr(2);
nounAndAdj = true;
} //<---//
if (cur.startsWith('(') && cur.endsWith(')'))
{
cur = cur.substr(2, cur.length() - 2);
if (weakTokens == nil)
weakTokens = [];
weakTokens += cur;
}
if (cur.startsWith('"'))
{
if (cur.endsWith('"'))
cur = cur.substr(2, cur.length() - 2);
else
cur = cur.substr(2);
wordPart = &literalAdjective;
}
else if (cur.endsWith('\'s'))
{
wordPart = &adjApostS;
cur = cur.substr(1, cur.length() - 2);
}
if (nounAndAdj) { //--->//
foreach (local prop in [&noun, &adjective]) {
if (self.(prop) == nil)
self.(prop) = [cur];
else
self.(prop) += cur;
cmdDict.addWord(self, cur, prop);
if (modList.indexOf(prop) == nil)
modList += prop;
}
} //<----//
else { //
if (self.(wordPart) == nil)
self.(wordPart) = [cur];
else
self.(wordPart) += cur;
cmdDict.addWord(self, cur, wordPart);
} //
if (cur.endsWith('.'))
{
local abbr;
abbr = cur.substr(1, cur.length() - 1);
self.(wordPart) += abbr;
cmdDict.addWord(self, abbr, wordPart);
}
if (modList.indexOf(wordPart) == nil
&& !nounAndAdj //
)
modList += wordPart;
}
}
if (len + 1 < str.length())
{
switch(str.substr(len + 1, 1))
{
case ' ':
break;
case '*':
sectPart = &plural;
break;
case '/':
sectPart = &noun;
break;
}
str = str.substr(len + 2);
if ((len = rexMatch('<space>+', str)) != nil)
str = str.substr(len + 1);
}
else
{
break;
}
}
foreach (local p in modList)
self.(p) = self.(p).getUnique();
}
;
Oo, nifty. I always wondered why TADS 2 could do this (but only by duplicating words in the noun/adjective properties), but T3 seemingly couldn’t. There’s always some new feature, that I never would’ve known about beforehand, that makes me glad I joined Club T3. Now I’m just waiting for a TADS powered robot to come on the scene.
@blindHunter My apologies, I left one of my macros in that code (dollar sign for noun-and-adjective) so it’s not likely to run if you copied and pasted it. I’ve made the changes to the post.
You are just a gift that keeps on giving! I had resigned myself to 'adj noun1 noun1/noun2'
with exactly the reservations you expressed.
Now I have to figure out if I have crested the ‘too much code to refactor’ line or not.