Sharing a ResettableEvent class for TADS 3

Hey TADS 3 users! I’m just sharing a class I created… nothing terribly deep, but it simplifies the process of adding little touches of realism and interactivity.

The idea is just to streamline the use of Daemons and Fuses. Sometimes with complex simulation objects, it gets messy adding more properties and methods which are just dedicated to Daemons or Fuses related to the object. If the nature of the event is such that it starts and stops frequently during the game, one must keep track of the creation of new Daemons, the storage of the ID, removing the event from the manager, etc.

I’m calling the base class ResettableEvent, and calling the subclasses RDaemon and RFuse, but in reality these objects are more of a DaemonManager (or FuseManager). To use, you just create an object, and for most cases, you only need to define its events() method. If it’s an RDaemon it has a built-in counter called ct, so you can refer to this in your events() method. For example:

breathingDmn: RDaemon
	events() {  if(ct==3) "It's starting to get uncomfortable holding your breath. ";
		else if(ct==5) "Your lungs are starting to burn... you'd better think about resurfacing. ";
		else if(ct==7) "You are on the verge of perishing from lack of oxygen!"; 
		else if(ct==8) endGame('You wouldn\'t cut it as a fish');  }
;

Then, each ResettableEvent has a start() and a reset() method. These methods take care of creating and removing the actual Daemons, and they also do the checking for you if an older daemon is still in place. So that any time you call start(), it will clear/reset any previous daemons and start the counter and the events method afresh. Similarly, you can call reset() without worrying whether there actually a daemon in progress. Thus, for our breathing example, you could do something like:

  class UnderwaterLoc: BasicLocation
	enteringRoom(traveler) { inherited(traveler); breathingDmn.start(); }
;
  class WaterSurface: OutdoorRoom
	enteringRoom(traveler) { inherited(traveler); breathingDmn.reset(); }
		// notice that it doesn't matter if the PC entered this room from a non-underwater loc:
		// it doesn't hurt to reset the Daemon if it's not actually running

The way I have it set up, the RDaemon automatically works as a SenseDaemon instead of a generic Daemon if you set the linkObj property. The linkObj is the same object that serves as the source parameter in a SenseDaemon. Furthermore, if you define a ResettableEvent as a nested object, its lexicalParent automatically becomes its linkObj. Thus, as another example:

  class HeatDaemon: RDaemon
	events() { if(linkObj.heatLevel < linkObj.maxHeat) ++linkObj.heatLevel;
		switch(linkObj.heatLevel) {
		 	case 1: "The object starts to heat up. "; break;
			case 3: ...
			case 5: "The object is glowing red hot. "...
	} } ;
  class CoolDaemon: RDaemon
	events() { //roughly the inverse of HeatDaemon } ;

metalObject: Thing
	heatLevel = 0
	maxHeat = 5
	myHeatDmn: HeatDaemon {}
	myCoolDmn: CoolDaemon {}
;
	
fire: Container, Fixture
	notifyInsert(obj,newCont) { obj.myCoolDmn.reset(); obj.myHeatDmn.start(); inherited(obj,newCont); }
	notifyRemove(obj) { obj.myHeatDmn.reset(); obj.myCoolDmn.start(); inherited(obj); }
;
	// note: in the events() method (or perhaps startEffects(), I can't remember if it matters),
   // you could turn off the opposite daemon automatically, rather than having to call both the
   // heat.start and the cool.reset (and v.v.) in notifyXX, as above

The above objects will not announce their messages if the PC is not around, because by being nested objects with a lexicalParent, they are created as SenseDaemons tied to the parent. If you want a class of objects that can use resettable daemons, you can try (using the same example):

  class MetalObject: Thing
	myHeatDmn = perInstance(new HeatDaemon)
	myCoolDmn = perInstance(new CoolDaemon)
		// note: these ^^ are not nested objects so their linkObjs are not automatically set
	initializeThing() { inherited; myCoolDmn.linkObj = self; myHeatDmn.linkObj = self; }
;

And you can obviously override more than just the events() method; you can set the eventOrder, interval, sense (for those with linkObjs) and test it with isActive. You can optionally add a message or effects in resetEffects() or startEffects(): perhaps for a breathing daemon, resetEffects might be something like

{ if(ct>4) "You break through the surface and fill your lungs with pure air again. "; }

You can call reset(nil) if you need a silent reset.
The only slight downside I’ve noticed is that you can’t refer directly to properties of the related simulation object, as you could if your daemon events methods were defined in that object, instead you have to say linkObj.heatLevel and the like. For that reason I added ‘lo’ as a property that refers to the linkObj: lo.heatLevel…

Here is the source code as it stands:

  class ResettableEvent: PresentLater, SecretFixture   
		/* I *think* the reason I used ^^ these superclasses was so that you could
		 * define a non-nested object anywhere convenient in your containment hierarchy, 
		 * without actually adding it to anything's contents...
	linkObj = (lexicalParent != nil ? lexicalParent : nil)	//auto-sense-connect to lexicalParent if one
	lo = (linkObj)		// a convenience reference
	createType() { if(ID != nil) ID.eventOrder = eventOrder; }
	ID = nil
	sense = sight
	interval = 1
	eventOrder = 100
	ct = 0
	isActive = nil
	start(trigger?) { 
        startEffects(trigger); 
        if(ID != nil) reset(nil); 
        createType(); 
        isActive = true; }
	startEffects(trigger) {  }
	events_ { ++ct; events(); }
	events() { }
	reset(eff = true) { 
		if(ID != nil) { 
           if(eff) { resetEffects(); } 
           ID.removeEvent(); 
           ID = nil; } 
		ct = 0; 
        isActive = nil; }
	resetEffects() { resetMsg; }
	resetMsg = ""
	;

  class RDaemon: ResettableEvent
	createType { if(linkObj != nil) ID = new SenseDaemon(self, &events_, interval, linkObj, sense); 
		else ID = new Daemon(self, &events_, interval); inherited; }
	;
  class RFuse: ResettableEvent
	createType { if(linkObj != nil) ID = new SenseFuse(self, &events_, interval, linkObj, sense); 
		else ID = new Fuse(self, &events_, interval); inherited; }
	;
  class RRealTimeDaemon: ResettableEvent
	createType { if(linkObj != nil) ID = new RealTimeSenseDaemon(self, &events_, interval, linkObj, sense); 
		else ID = new RealTimeDaemon(self, &events_, interval); inherited; }
	;

(in a header file:)

ResettableEvent template +interval? ->linkObj?  @sense? ;

Please feel free to make use of this if you wish, and let me know if you see ways of improving how it works. Thanks everyone!

2 Likes

hey john,
are you missing the code for RDeamon and RFuse, or did i understand this wrong?

Are you able to scroll down in the source code section? The site posted that section of code so that it doesn’t all show at once…

wow, i was sure that i did that…
ok, i’m giving this a spin later on. looks really useful and i was just about to start messing around with daemons.

Hope it helps for you… once I made the class, I found myself using them all over the place…

As for the template, I had it in a source file rather than in a header. Works now… oops!