#charset "us-ascii"
#include <tads.h>
#include "advlite.h"
/*
* sensory.t
*
* The SENSORY EXTENSION is intended for use with the adv3Lite library. It
* adds slightly more sophisticated handling for smells and sounds, as well as
* a new SensoryEvent class which can be useful for making actors and other
* objects respond to sensory events.
*
* VERSION 2
*. 19-Jul-14
*
* Usage: include this extension after the adv3Lite library but before your
* own game-specific files. Make sure that events.t is also included in your
* build.
*/
sensoryID: ModuleID
name = 'Sensory'
byline = 'by Eric Eve'
htmlByline = 'by Eric Eve'
version = '2'
;
property remoteSmellDesc;
property remoteListenDesc;
property tooFarAwayToHearMsg;
property tooFarAwayToSmellMsg;
/*
* The SensoryEmanation class is the base class for sensory emanations such as
* smells and noises. [MODIFIED FOR SENSORY EXTENSION]
*/
modify SensoryEmanation
/*
* By default we vary our description according to whether the player
* character can see the object whose sound or smell we represent. If you
* don't these this variation, you can just override desc directly.
* [MODIFIED FOR SENSORY EXTENSION]
*/
desc
{
if(Q.canSee(gPlayerChar, location))
descWithSource;
else
descWithoutSource;
}
/*
* Our description when the player character can see our source
* [DEFINED IN SENSORY EXTENSION]
*/
descWithSource = nil
/*
* Our description when the player character can't see our source
* [DEFINED IN SENSORY EXTENSION]
*/
descWithoutSource = nil
/*
* Are we actually emanating? We may not be if something stops us, e.g.
* breaking a ticking clock [DEFINED IN SENSORY EXTENSION]
*/
isEmanating = true
/*
* If we're not emanating we can't be sensed at all, so we're hidden
* [DEFINED IN SENSORY EXTENSION]
*/
isHidden = !isEmanating
/*
* The emanate method is called on each turn that the player character can
* sense us, and can be used to display a message announcing our presence,
* such as "There's an awful stink here" or "A loud ticking noise comes
* from somewhere. " [DEFINED IN SENSORY EXTENSION]
*/
emanate()
{
/* If our schedule is nil (see below) we don't do anything at all */
if(schedule == nil)
return;
/* Get the interval before we're due to show another message */
local interval = schedule[scheduleState];
/*
* If this is nil, we don't want to show another message, so simply
* end the routine here.
*/
if(interval == nil)
return;
/*
* If incrementing our emanation state by one makes it greater than
* the its value when we last showed a message plus the interval to
* the next one, show our emanation desc
*/
if(++emanationState >= lastEmanationTime + interval)
{
/* Display our emanation description */
emanationDesc();
/*
* If we haven't reached the end of the emanation schedule yet,
* increase our scheduleState by one.
*/
if(nilToList(schedule).length > scheduleState)
scheduleState++;
/* Note when we last displayed a message. */
lastEmanationTime = emanationState;
/* Note that the player character must now know about us. */
setKnown();
}
}
/*
* The message to display to announce our presence. This is overridden on
* our subclasses.[DEFINED IN SENSORY EXTENSION]
*/
emanationDesc() { }
/*
* A counter to keep track of when we're next due to display an emanation
* message [DEFINED IN SENSORY EXTENSION]
*/
emanationState = 0
/*
* A counter to keep track of where we are in our emanation schedule.
* [DEFINED IN SENSORY EXTENSION]
*/
scheduleState = 1
/*
* The last time we emanated, relevant to when we started emanating
* [DEFINED IN SENSORY EXTENSION]
*/
lastEmanationTime = 0
/*
* Our emanation schedule. If this is just nil we won't show any emanation
* meessages at all. Otherwise this should be a list of numbers. The first
* number is the first interval between emanations, the second number the
* second interval and so on. When we get to the end of the list we keep
* using the last number in the list as the interval. If the last entry in
* the list is nil we stop showing emanation messages. This can be used to
* reduce the frequency of messages to model the player character becoming
* less aware of us. [DEFINED IN SENSORY EXTENSION]
*/
schedule = [1]
/* Reset all out counters to their initial states [DEFINED IN SENSORY EXTENSION] */
reset()
{
emanationState = 0;
scheduleState = 1;
if(ofKind(Script))
curScriptState = 1;
lastEmanationTime = 0;
}
/*
* The message to display when the player tries to do something with us
* other than sense us. [DEFINED IN SENSORY EXTENSION]
*/
notImportantMsg = BMsg(cannot do to sensory, '{I} {can\'t} do that to {a
cobj}. ')
;
/* An Odor is a SensoryEmanation representing a Smell [MODIFIED FOR SENSORY EXTENSION]*/
modify Odor
/*
* The message to be displayed to show that there's a smell here. The
* default implementation should be serviceable in many cases, but game
* code can easily override this method if something different is
* required. By default we execute our script if we are one, otherwise we use
* our own smellDesc or remoteSmellDesc as appropriate.
* [DEFINED IN SENSORY EXTENSION]
*/
emanationDesc()
{
/*
* If we're mixed in with an EventList class, display the next item
* from our eventList.
*/
if(ofKind(Script))
doScript();
/*
* Otherwise use our location's smellDesc or remoteSmellDesc, as
* appropriate
*/
else
{
if(!location.isIn(gPlayerChar.getOutermostRoom)
&& location.propDefined(&remoteSmellDesc))
location.remoteSmellDesc(gPlayerChar);
else
location.smellDesc;
}
}
/*
* Only carry out the inherited handling if the player hasn't issued a
* SMELL command on this turn, otherwise there's the risk of duplicate
* messages. [DEFINED IN SENSORY EXTENSION]
*/
emanate()
{
if(!gActionIn(Smell, SmellSomething))
inherited;
}
;
/*
* A SimpleOdor is an object representing a free-standing smell directly
* present in a location rather than attached to any specific object. It can
* be used to display atmospheric smells either according to its schedule or
* in response to a SMELL command. [DEFINED IN SENSORY EXTENSION]
*/
class SimpleOdor: Odor
/*
* Unless this is overridden, our desc property simply executes our
* script. [DEFINED IN SENSORY EXTENSION]
*/
desc() { doScript(); }
/* The smellDesc of a SimpleOdor is simply its desc.[DEFINED IN SENSORY EXTENSION] */
smellDesc = desc
/*
* A SimpleOdor is a prominent smell by default, since we want it to show
* up in response to a SMELL command. [DEFINED IN SENSORY EXTENSION]
*/
isProminentSmell = true
/*
* The message to be displayed to show that there's a smell here.
* We display either our own smellDesc or our remoteSmellDesc,
* as appropriate.
* DEFINED IN SENSORY EXTENSION]
*/
emanationDesc()
{
if(!isIn(gRoom) && propDefined(&remoteSmellDesc))
remoteSmellDesc(gPlayerChar);
else
smellDesc;
}
;
/* An Noise is a SensoryEmanation representing a Sound [MODIFIED FOR SENSORY EXTENSION] */
modify Noise
/*
* The message to be displayed to show that there's a sound here. The
* default implementation should be serviceable in many cases, but game
* code can easily override this method if something different is
* required. By default we execute our script if we are one, otherwise we use
* our own listenDesc or remoteListenDesc as appropriate.
* [DEFINED IN SENSORY EXTENSION]
*/
emanationDesc()
{
/*
* If we're mixed in with an EventList class, display the next item
* from our eventList.
*/
if(ofKind(Script))
doScript();
/*
* Otherwise use our location's listenDesc, or remoteListenDesc, as
* appropriate
*/
else
{
if(!location.isIn(gPlayerChar.getOutermostRoom)
&& location.propDefined(&remoteListenDesc))
location.remoteListenDesc(gPlayerChar);
else
location.listenDesc;
}
}
/*
* Only carry out the inherited handling if the player hasn't issued a
* LISTEN command on this turn, otherwise there's the risk of duplicate
* messages. [DEFINED IN SENSORY EXTENSION]
*/
emanate()
{
if(!gActionIn(Listen, ListenTo))
inherited;
}
;
/*
* A SimpleNoise is an object representing a free-standing sound directly
* present in a location rather than attached to any specific object. It can
* be used to display atmospheric sounds either according to its schedule or
* in response to a LISTEN command. [DEFINED IN SENSORY EXTENSION]
*/
class SimpleNoise: Noise
/*
* Unless this is overridden, our desc property simply executes our
* script. [DEFINED IN SENSORY EXTENSION]
*/
desc() { doScript(); }
/* The listenDesc of a SimpleNoise is simply its desc.[DEFINED IN SENSORY EXTENSION] */
listenDesc = desc
/*
* A SimpleNoise is a prominent noise by default, since we want it to show
* up in response to a LISTEN command.
*/
isProminentNoise = true
/*
* The message to be displayed to show that there's a noise here.
* We display either our own listenDesc or our remoteListenDesc,
* as appropriate.
* [DEFINED IN SENSORY EXTENSION]
*/
emanationDesc()
{
if(!isIn(gRoom) && propDefined(&remoteListenDesc))
remoteListenDesc(gPlayerChar);
else
listenDesc;
}
;
/*
* The object which drives emanation messages for Odors and Noises
* [DEFINED IN SENSORY EXTENSION]
*/
emanationControl: InitObject
/* Set up our Daemon at the start of play. [DEFINED IN SENSORY EXTENSION] */
execute()
{
new Daemon(self, &emanate, 1);
}
/*
* Each turn, execute the emanate() method for every item in our list of
* emanations. [DEFINED IN SENSORY EXTENSION]
*/
emanate()
{
/* Get a list of items potentially due to emanate */
local lst = buildEmanationList;
/* Make every item in our list execute its emanate method */
for(local e in lst)
e.emanate();
}
/*
* Construct a list of SensoryEmanations that can currently be sensed by
* the player character. [DEFINED IN SENSORY EXTENSION]
*/
buildEmanationList
{
local pc = gPlayerChar;
/*
* First get a list of all the SensoryEmanations in the player
* character's current room that can be sensed by the player character
*/
local lst = pc.getOutermostRoom.allContents.subset(
{o: canSense(pc, o)});
/*
* If the SenseRegion class is defined then add all the
* SensoryEmanations that can be sensed in remote locations
*/
if(defined(SenseRegion))
{
/* Set up an empty list of remote SensoryEmanations */
local remoteLst = [];
/*
* For each room that's audible from the player character's
* current location, all all the currently emanating Noises that
* the player character can hear.
*/
foreach(local rm in valToList(pc.getOutermostRoom.audibleRooms))
remoteLst += rm.allContents.subset(
{o: o.isEmanating && o.ofKind(Noise) && Q.canHear(pc, o)});
/*
* For each room that's audible from the player character's
* current location, all all the currently emanating Odors that
* the player character can smell.
*/
foreach(local rm in valToList(pc.getOutermostRoom.smellableRooms))
remoteLst += rm.allContents.subset(
{o: o.isEmanating && o.ofKind(Odor) && Q.canSmell(pc, o)});
/*
* Add the remote list to the local list, ensuring that each item
* appears only once.
*/
lst = lst.appendUnique(remoteLst);
}
/* Return the resulting list. */
return lst;
}
/*
* The pc can sense o if o is currently emanating and its a Noise the pc
* can currently hear or an Odor the pc can currently smell. [DEFINED IN SENSORY EXTENSION]
*/
canSense(pc, o)
{
return o.isEmanating && ((o.ofKind(Noise) && Q.canHear(pc, o))
|| (o.ofKind(Odor) && Q.canSmell(pc, o)));
}
;
/*
* A SensoryEvent is a brief event in time, such as a sudden noise, to which
* other actors or objects in the vicinity may react. [DEFINED IN SENSORY EXTENSION]
*/
class SensoryEvent: object
/*
* We call the trigger event method of a SensoryEvent to simulate the
* occurrence of that event. The obj parameter is the object associated
* with the event, for example the source of a sudden explosion.
* [DEFINED IN SENSORY EXTENSION]
*/
triggerEvent(obj)
{
/*
* Construct a list containing all the items in the player character's
* current room that the pc can sense via the relevant sense
*/
local notifyList = obj.getOutermostRoom.allContents.subset({
o: Q.(senseProp)(o, obj) });
/*
* Add any items from remote locations that meet the same conditions,
* making the list unique so we don't get any duplicates.
*/
notifyList = notifyList.appendUnique(remoteList(obj));
/*
* Notify every item in our notification list that this event has just
* occurred.
*/
for(local cur in notifyList)
cur.(notifyProp)(self, obj);
/*
* Presumablty obj has just made its presence known to the player
* character, even if it wasn't before.
*/
obj.setKnown();
}
/*
* A property pointer to the property on each notified object that needs
* to be executed when it's notified of this SensoryEvent (e.g.
* ¬ifySoundEvent). [DEFINED IN SENSORY EXTENSION]
*/
notifyProp = nil
/*
* The property pointer relating to the Q method that needs to be called
* to determined whethet this SensoryEvent can be sensed (e.g. &canHear).
* [DEFINED IN SENSORY EXTENSION]
*/
senseProp = nil
/*
* The property pointer to the property of Room defining which list of
* rooms also needs to be checked for remote items that might sense this
* event (e.g. &audibleRooms). [DEFINED IN SENSORY EXTENSION]
*/
remoteProp = nil
/* Construct a list of notifiable objects in remote locations [DEFINED IN SENSORY EXTENSION]*/
remoteList(obj)
{
/* Start with an empty list */
local lst = [];
/*
* If the SenseRegion class isn't present, there's no point trying to
* look for objects in remote rooms.
*/
if(defined(SenseRegion))
{
/*
* Got through each room in the appropriate list of remote rooms
* that can be sensed from us through the appropriate sense to
* build a list of all their contents which can be senses via the
* appropriate sense.
*/
for(local rm in valToList(obj.getOutermostRoom.(remoteProp)))
lst = lst.appendUnique(rm.allContents.({
o: Q.(senseProp)(o, obj) }));
}
/* Return the resulting list */
return lst;
}
;
/*
* A SoundEvent represents any sudden noise to which other objects or people
* might react. [DEFINED IN SENSORY EXTENSION]
*/
class SoundEvent: SensoryEvent
notifyProp = ¬ifySoundEvent
senseProp = &canHear
remoteProp = &audibleRooms
;
/*
* A SoundEvent represents any sudden smell to which other objects or people
* might react. [DEFINED IN SENSORY EXTENSION]
*/
class SmellEvent: SensoryEvent
notifyProp = ¬ifySmellEvent
senseProp = &canSmell
remoteProp = &smellableRooms
;
/*
* A SightEvent represents any visible event to which other objects or people
* might react. [DEFINED IN SENSORY EXTENSION]
*/
class SightEvent: SensoryEvent
notifyProp = ¬ifySightEvent
senseProp = &canSee
remoteProp = &visibleRooms
;
/* Modifications to Thing to work with the <i>SENSORY EXTENSION</i> */
modify Thing
/*
* The methods that define our reactions to SoundEvents, SmellEvents and
* SightEvents respectively. By default all three methods defer to a
* common handler. [DEFINED IN SENSORY EXTENSION]
*/
notifySoundEvent(event, source) { notifyEvent(event, source); }
/* Our reaction to a SmellEvent. By default we defer to the common handler. */
notifySmellEvent(event, source) { notifyEvent(event, source); }
/* Our reaction to a SightEvent. By default we defer to the common handler. */
notifySightEvent(event, source) { notifyEvent(event, source); }
/*
* Our common handler for SensoryEvents; it may often be more convenient
* to use this than to write separate handlers for each kind of
* SensoryEvent, since in any case the event parameter (containing the
* SensoryEvent that's just been triggered) tells us what kind of
* SensoryEvent it is. The source parameter is the object associated with
* the event. [DEFINED IN SENSORY EXTENSION]
*/
notifyEvent(event, source) { }
/*
* By default we split our smellDesc into smellDescWithoutSource (when the
* player character can't see us) and smellDescWithSource (when the pc
* can). If we don't need this distinction we can override this method
* directly. [MODIFIED FOR SENSORY EXTENSION]
*/
smellDesc()
{
if(Q.canSee(gActor, self))
smellDescWithSource;
else
smellDescWithoutSource;
}
/*
* The response to SMELLing this object when the actor can see us.
* [DEFINED IN SENSORY EXTENSION]
*/
smellDescWithSource = nil
/*
* The response to SMELLing this object when the actor can't see us.
* [DEFINED IN SENSORY EXTENSION]
*/
smellDescWithoutSource = nil
/*
* By default we split our listenDesc into listenDescWithoutSource (when
* the player character can't hear us) and listenDescWithSource (when the
* pc can). If we don't need this distinction we can override this method
* directly. [MODIFIED FOR SENSORY EXTENSION]
*/
listenDesc()
{
if(Q.canSee(gActor, self) )
listenDescWithSource;
else
listenDescWithoutSource;
}
/*
* The response to LISTENing TO this object when the actor can see us.
* [DEFINED IN SENSORY EXTENSION]
*/
listenDescWithSource = nil
/*
* The response to LISTENing TO this object when the actor can't see us.
* [DEFINED IN SENSORY EXTENSION]
*/
listenDescWithoutSource = nil
/*
* [MODIFIED FOR SENSORY EXTENSION]
* If I have an associated Noise object which isn't emanating, assume
* I have fallen silent, otherwise carry out the inherited handling.
*/
dobjFor(ListenTo)
{
action()
{
/*
* If I have an associated Noise object which isn't emanating,
* then assume I have fallen silent.
*/
if(soundObj != nil && !soundObj.isEmanating)
say(hearNothingMsg);
else
inherited;
}
}
/*
* [MODIFIED FOR SENSORY EXTENSION]
* If I have an associated Odor object which isn't emanating, assume
* I no longer smell of anything, otherwise carry out the inherited handling.
*/
dobjFor(SmellSomething)
{
action()
{
/*
* If I have an associated Odor object which isn't emanating,
* then assume I no longer smell.
*/
if(smellObj != nil && !smellObj.isEmanating)
say(smellNothingMsg);
else
inherited;
}
}
/*
* We don't have a prominent smell if we have an associated Odor object
* that isn't emanating. [MODIFIED FOR SENSORY EXTENSION]
*/
isProminentSmell
{
if(smellObj && !smellObj.isEmanating)
return nil;
return true;
}
/*
* We don't have a prominent noise if we have an associated Noise object
* that isn't emanating. [MODIFIED FOR SENSORY EXTENSION]
*/
isProminentNoise
{
if(soundObj && !soundObj.isEmanating)
return nil;
return true;
}
/* Our associated Odor object, if we have one [SENSORY EXTENSION]*/
smellObj = (contents.valWhich({o: o.ofKind(Odor)}))
/* Our associated Noise object, if we have one. [SENSORY EXTENSION]*/
soundObj = (contents.valWhich({o: o.ofKind(Noise)}))
;
/* MODIFICATIONS FOR SENSORY EXTENSION */
modify Room
/*
* Reset every SensoryEmanation in this room to its initial state when the
* player character leaves this room. [MODIFIED FOR SENSORY EXTENSION]
*/
notifyDeparture(traveler, dest)
{
inherited(traveler, dest);
if(traveler == gPlayerChar)
{
local lst = allContents.subset({o: o.ofKind(SensoryEmanation)});
/*
* If the SenseRegion class is included, then we need to deal with
* SensoryEmanations in remote rooms.
*/
if(defined(SenseRegion))
{
local sp = defined(scopeProbe_) ? scopeProbe_ : object: Thing {};
try
{
sp.moveInto(dest);
/* First add all the Noises in the remote rooms we can hear */
for(local rm in getOutermostRoom.audibleRooms)
lst.appendUnique(rm.allContents.subset({o:
o.ofKind(Noise)}));
/* Then add all the Odors in the remote rooms we can smell */
for(local rm in getOutermostRoom.smellableRooms)
lst.appendUnique(rm.allContents.subset({o:
o.ofKind(Odor)}));
/*
* Finally remove all the Odors that can't be smelled from
* the destination and all the Noises than can't be heard
* from the destination.
*/
lst = lst.subset({o: (o.ofKind(Noise) &&
!Q.canHear(sp, o))
|| (o.ofKind(Odor)
&& !Q.canSmell(sp, o))});
}
finally
{
sp.moveInto(nil);
}
}
/* Reset every SensoryEmanation in our list */
lst.forEach({o: o.reset() });
}
}
;
Adv3Lite Library Reference Manual
Generated on 25/04/2024 from adv3Lite version 2.0