A TADS3/adv3 module a providing low-level mechanism for implementing dynamic vocabulary on objects

A module for handling dynamic vocabulary on objects: dynamicVocab github repo.

This is intended to provide a mechanism for twiddling the vocabulary of objects at runtime, specifically when there’s the need for more customization that you can get out of adv3-native ThingState.

This is the result of a refactor of dynamicThing module. That module provides both the low-level mechanics (methods for changing the vocabulary on objects) and the high-level interface stuff (how to programatically apply the changes due to in-game events).

The new module just provides the low-level methods. My plan is to do another module integrating the memoryEngine module via an interface to trigger vocabulary updates due to changes in in-game player knowledge.

USAGE

First you add theDynamicVocab class as a mixin in a Thing’s declaration:

// Declare an object whose vocabulary can be updated. 
pebble: Thing, DynamicVocab '(small) (round) pebble' 'pebble'
        "A small, round pebble. "
;

Then you declare one or more VocabCfg instances. These have a vocabWords property that uses exactly the same syntax as vocabWords on a Thing:

// Declare the dynamic part of the vocabulary. 
alien: VocabCfg '(alien) artifact';

Having done that, you can add the additional vocabulary to the object via:

// Add the "alien" vocabulary to the pebble.
pebble.addVocab(alien);

Now something like >X ARTIFACT or >X ALIEN ARTIFACT will resolve to the pebble.

You can remove the additional vocabulary via:

// Remove the "alien" vocabulary to the pebble.
pebble.removeVocab(alien);

This is pretty trivial, and most of the (fairly short) code is devoted to making sure vocabulary is never removed when it shouldn’t be. For example, if you do something like:

// Create a couple VocabCfg instances.
alien = new VocabCfg('(alien) artifact');
weird = new VocabCfg('(weird) artifact');

// Add them to the pebble.
pebble.addVocab(alien);         // >X ALIEN ARTIFACT now works
pebble.addVocab(weird);         // >X WEIRD ARTIFACT now works

// Remove the "alien" vocabulary.
pebble.removeVocab(alien);

…then >X ARTIFACT will still work, because the module will figure out that removeVocab(alien) should only remove the adjective “alien” and not the noun “artifact”, because it’s still referred to by a different VocabCfg. It will also avoid removing vocabulary that was on the Thing declaration originally. In other words, you don’t have to worry about VocabCfg vocabulary overlapping with vocabulary declared on the object elsewhere.

Additionally, by default an Unthing sharing the VocabCfg’s vocabulary will be created when the VocabCfg is (or at preinit, if it’s declared in source). This means that >X ARTIFACT will produce a “You see no artifact here.” response if the VocabCfg hasn’t been applied to any objects, instead of a confusing “The word ‘artifact’ is not necessary in this story.” failure message.

All told it’s pretty simple, but there’s enough bookkeeping involved that it feels like it deserves to be its own thing.

And like I said I intend to add more of the “fancy” stuff (like DynamicThing does)—providing message parameter substituions that can be used in object names, room titles, and so on—but I can also see uses cases for just using this as a slightly more flexible alternative to ThingState.

2 Likes

To complement this subject, I’ll submit a simple method I made use of… It’s nothing but a wrapper for initializeVocabWith but where you can also remove vocab in the same move. The first parameter is a string of words to add, formatted like vocabWords. The noun, adjective, and plural parameters can be submitted as a single string or a list of strings to be removed, or nil (if you’re trying to get to a later parameter).
In a lot of situations it was sufficient for me to not have to make extra ThingStates. Or say you want to implement just one catchable fish object, but want to randomize its description each time a fish is caught. A statement like this will take care of the vocab:
changeVocab('salmon',['bass','trout','mackerel']);
If it didn’t have “salmon” before, it does now, and if any of the others were still around from a previous generation, they’re gone now.

modify Thing
	changeVocab(addWords,delNouns?,delAdjs?,delPlurals?) { 
		if(addWords) initializeVocabWith(addWords);
		if(delNouns) cmdDict.removeWord(self,delNouns,&noun);
		if(delAdjs) cmdDict.removeWord(self,delAdjs,&adjective);		
		if(delPlurals) cmdDict.removeWord(self,delPlurals,&plural);
	}
;
2 Likes

Yeah, something like that is where I more or less started out, but then I realized that since I wanted to add multiple descriptors to individual objects I was actually laying a series of traps for myself. Because if you handle it as an all-at-once replacement then you have to figure out a bunch of elaborate procedural logic for figuring out which combination of vocabulary words is currently valid for the object.

1 Like

Just pushed an update that changes some of the method names.

Notably addVocab() is now activateVocab() and removeVocab() is deactivateVocab().

Previously each DynamicVocab instance had a list of VocabCfgs that it was currently using, and addVocab() and removeVocab() added and removed them from the list to keep track. Now they stay on the list but are marked active or inactive.

It also keeps track of the state of the active flag for each VocabCfg as of the last update, making it easier to keep track of anything that might’ve been updated externally.

And there’s a DynamicVocab.syncVocab() that’ll apply any changes since the last update.

This is all intended to make it easier to work with situations like:

+pebble: Thing, DynamicVocab '(small) (round) pebble' 'pebble'
        // The description sets the reveal tag "alien".
        "A small, round pebble.  Which may be an alien
        artifact, apparently.<.reveal alien> "
;
++alien: VocabCfg '(alien) artifact'
        // The "alien" vocabulary is enabled by the "alien" reveal tag.
        isActive = (gRevealed('alien'))
;

…where some vocabulary is being toggled on or off by something external. Note that something (in this case) still needs to call pebble.syncVocab() to apply the changes. In the demo it’s the room’s roomAfterAction(), but my goal is to tie all this into the player/NPC knowledge abstraction layer I’m working on (which requires some fiddling because in a TADS3 game all vocabulary is implicitly understood from the POV of the player).

1 Like