sense.t
#charset "us-ascii"
/*
* Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved.
*
* TADS 3 Library - senses
*
* This module defines objects and functions related to senses. This
* file is language-independent.
*/
/* include the library header */
#include "adv3.h"
/* ------------------------------------------------------------------------ */
/*
* Material: the base class for library objects that specify the way
* senses pass through objects.
*/
class Material: object
/*
* Determine how a sense passes through the material. We'll return
* a transparency level. (Individual materials should not need to
* override this method, since it simply dispatches to the various
* xxxThru methods.)
*/
senseThru(sense)
{
/* dispatch to the xxxThru method for the sense */
return self.(sense.thruProp);
}
/*
* For each sense, each material must define an appropriate xxxThru
* property that returns the transparency level for that sense
* through the material. Any xxxThru property not defined in an
* individual material defaults to opaque.
*/
seeThru = opaque
hearThru = opaque
smellThru = opaque
touchThru = opaque
;
/*
* Adventium is the basic stuff of the game universe. This is the
* default material for any object that doesn't specify a different
* material. This type of material is opaque to all senses.
*/
adventium: Material
seeThru = opaque
hearThru = opaque
smellThru = opaque
touchThru = opaque
;
/*
* Paper is opaque to sight and touch, but allows sound and smell to
* pass.
*/
paper: Material
seeThru = opaque
hearThru = transparent
smellThru = transparent
touchThru = opaque
;
/*
* Glass is transparent to light, but opaque to touch, sound, and smell.
*/
glass: Material
seeThru = transparent
hearThru = opaque
smellThru = opaque
touchThru = opaque
;
/*
* Fine Mesh is transparent to all senses except touch.
*/
fineMesh: Material
seeThru = transparent
hearThru = transparent
smellThru = transparent
touchThru = opaque
;
/*
* Coarse Mesh is transparent to all senses, including touch, but
* doesn't allow large objects to pass through.
*/
coarseMesh: Material
seeThru = transparent
hearThru = transparent
smellThru = transparent
touchThru = transparent
;
/* ------------------------------------------------------------------------ */
/*
* Sense: the basic class for senses.
*/
class Sense: object
/*
* Each sense must define the property thruProp as a property
* pointer giving the xxxThru property for the sense. The xxxThru
* property is the property of a material which determines how the
* sense passes through that material.
*/
thruProp = nil
/*
* Each sense must define the property sizeProp as a property
* pointer giving the xxxSize property for the sense. The xxxSize
* property is the property of a Thing which determines how "large"
* the object is with respect to the sense. For example, sightSize
* indicates how large the object is visually, while soundSize
* indicates how loud the object is.
*
* The purpose of an object's size in a given sense is to determine
* how well the object can be sensed through an obscuring medium or
* at a distance.
*/
sizeProp = nil
/*
* Each sense must define the property presenceProp as a property
* pointer giving the xxxPresence property for the sense. The
* xxxPresence property is the property of a Thing which determines
* whether or not the object has a "presence" in this sense, which is
* to say whether or not the object is emitting any detectable
* sensory data for the sense. For example, soundPresence indicates
* whether or not a Thing is making any noise.
*
* The sensory presence is used to determine if an object is in
* scope. An object with a detectable sensory presence is normally
* in scope. Note that sounds and smells emitted by a tangible
* object are frequently represented as additional intangible
* objects, and in these cases the intangible object (the sensory
* emanation) is usually the object with a sensory presence, rather
* than the tangible object making the noise/odor. However, it is
* sometimes obvious that a particular sound or odor is coming from a
* particular kind of object, so the presence of the sound or odor
* implies the presence of the source object and thus places the
* source object in scope. In such cases, it is desirable for the
* source object to have a sensory presence of its own, in addition
* to the sensory presence of the intangible sensory emanation
* object.
*
* Note that the "presence" doesn't have any effect on whether or not
* an object can be sensed. Only the sense path matters for that: an
* object without a presence can still be sensed if there's a
* non-opaque sense path to the object. Presence only determines
* whether or not an object is *actively* calling attention to
* itself.
*/
presenceProp = nil
/*
* Each sense can define this property to specify a property pointer
* used to define a Thing's "ambient" energy emissions. Senses
* which do not use ambient energy should define this to nil.
*
* Some senses work only on directly emitted sensory data; human
* hearing, for example, has no (at least effectively no) use for
* reflected sound, and can sense objects only by the sounds they're
* actually emitting. Sight, on the other hand, can make use not
* only of light emitted by an object but of light reflected by the
* object. So, sight defines an ambience property, whereas hearing,
* touch, and smell do not.
*/
ambienceProp = nil
/*
* Determine if, in general, the given object can be sensed under
* the given conditions. Returns true if so, nil if not. By
* default, if the ambient level is zero, we'll return nil;
* otherwise, if the transparency level is 'transparent', we'll
* return true; otherwise, we'll consult the object's size:
*
* - Small objects cannot be sensed under less than transparent
* conditions.
*
* - Medium or large objects can be sensed in any conditions other
* than opaque.
*/
canObjBeSensed(obj, trans, ambient)
{
/*
* if we use "reflected" energy, and the ambient energy level is
* zero, we can't sense it
*/
if (ambienceProp != nil && ambient == 0)
return nil;
/* check the transparency level */
switch(trans)
{
case transparent:
case attenuated:
/*
* we can always sense under transparent or attenuated
* conditions
*/
return true;
case distant:
case obscured:
/*
* we can only sense medium and large objects under less
* than transparent conditions
*/
return obj.(self.sizeProp) != small;
default:
/* we can never sense under other conditions */
return nil;
}
}
;
/*
* The senses. We define sight, sound, smell, and touch. We do not
* define a separate sense for taste, since it would add nothing to our
* model: you can taste something if and only if you can touch it.
*
* To add a new sense, you must do the following:
*
* - Define the sense object itself, in parallel to the senses defined
* below.
*
* - Modify class Material to set the default transparency level for
* this sense by defining the property xxxThru - for most senses, the
* default transparency level is 'opaque', but you must decide on the
* appropriate default for your new sense.
*
* - Modify class Thing to set the default xxxSize setting, if desired.
*
* - Modify class Thing to set the default xxxPresence setting, if
* desired.
*
* - Modify each instance of class 'Material' that should have a
* non-default transparency for the sense by defining the property
* xxxThru for the material.
*
* - Modify class Actor to add the sense to the default mySenses list;
* this is only necessary if the sense is one that all actors should
* have by default.
*/
sight: Sense
thruProp = &seeThru
sizeProp = &sightSize
presenceProp = &sightPresence
ambienceProp = &brightness
;
sound: Sense
thruProp = &hearThru
sizeProp = &soundSize
presenceProp = &soundPresence
;
smell: Sense
thruProp = &smellThru
sizeProp = &smellSize
presenceProp = &smellPresence
;
touch: Sense
thruProp = &touchThru
sizeProp = &touchSize
presenceProp = &touchPresence
/*
* Override canObjBeSensed for touch. Unlike other senses, touch
* requires physical contact with an object, so it cannot operate at
* a distance, regardless of the size of an object.
*/
canObjBeSensed(obj, trans, ambient)
{
/* if it's distant, we can't sense the object no matter how large */
if (trans == distant)
return nil;
/* for other cases, inherit the default handling */
return inherited(obj, trans, ambient);
}
;
/* ------------------------------------------------------------------------ */
/*
* "Add" two transparency levels, yielding a new transparency level.
* This function can be used to determine the result of passing a sense
* through multiple layers of material.
*/
transparencyAdd(a, b)
{
/* transparent + x -> x for all x */
if (a == transparent)
return b;
if (b == transparent)
return a;
/* opaque + x -> opaque for all x */
if (a == opaque || b == opaque)
return opaque;
/*
* any other combinations yield opaque - we can't have two levels of
* attenuation or obscuration without losing all detail and energy
* transmission
*/
return opaque;
}
/* ------------------------------------------------------------------------ */
/*
* Compare two transparency levels to determine which one is more
* transparent. Returns 0 if the two levels are equally transparent, 1
* if the first one is more transparent, and -1 if the second one is
* more transparent. The comparison follows this rule:
*
* transparent > attenuated > distant == obscured > opaque
*/
transparencyCompare(a, b)
{
/*
* for the purposes of the comparison, consider obscured to be
* identical to distant
*/
if (a == obscured)
a = distant;
if (b == obscured)
b = distant;
/* if they're the same, return zero to so indicate */
if (a == b)
return 0;
/*
* We know they're not equal, so if one is transparent, then the
* other one isn't. Thus, if either one is transparent, it's the
* winner.
*/
if (a == transparent)
return 1;
if (b == transparent)
return -1;
/*
* We know they're not equal and we know neither is transparent, so
* if one is attenuated then the other is worse, and the attenuated
* one is the winner.
*/
if (a == attenuated)
return 1;
if (b == attenuated)
return -1;
/*
* We now know neither one is transparent or attenuated, and we've
* already transformed obscured into distant, so the only possible
* values remaining are distant and opaque. We know also they're
* not equal, because we would have already returned if that were
* the case. So, we can conclude that one must be distant and the
* other must be opaque. Hence, the one that's opaque is the less
* transparent one.
*/
if (a == opaque)
return -1;
else
return 1;
}
/* ------------------------------------------------------------------------ */
/*
* Given a brightness level and a transparency level, compute the
* brightness as modified by the transparency level.
*/
adjustBrightness(br, trans)
{
switch(trans)
{
case transparent:
case distant:
/*
* Transparent medium or distance - this doesn't modify
* brightness at all. (Technically, distance would reduce
* brightness somewhat, but the typical scale of an IF setting
* isn't usually large enough that brightness should
* significantly diminish.)
*/
return br;
case attenuated:
case obscured:
/*
* Distant, obscured, or attenuated. We reduce self-illuminating
* light (level 1) and dim light (level 2) to nothing (level 0),
* we leave nothing as nothing (obviously), and we reduce all
* other levels one step. So, everything below level 3 goes to
* 0, and everything at or above level 3 gets decremented by 1.
*/
return (br >= 3 ? br - 1 : 0);
case opaque:
/* opaque medium - nothing makes it through */
return 0;
default:
/* shouldn't get to other cases */
return nil;
}
}
/* ------------------------------------------------------------------------ */
/*
* SenseConnector: an object that can pass senses across room
* boundaries. This is a mix-in class: add it to the superclass list of
* the object before Thing (or a Thing subclass).
*
* A SenseConnector acts as a sense conduit across all of its locations,
* so to establish a connection between locations, simply place a
* SenseConnector in each location. Since a SenseConnector is useful
* only when placed placed in multiple locations, SenseConnector is
* based on MultiLoc.
*/
class SenseConnector: MultiLoc
/*
* A SenseConnector's material generally determines how senses pass
* through the connection.
*/
connectorMaterial = adventium
/*
* Determine how senses pass through this connection. By default,
* we simply use the material's transparency.
*/
transSensingThru(sense) { return connectorMaterial.senseThru(sense); }
/*
* Add the direct containment connections for this item to a lookup
* table.
*
* Since we provide a sense connection among all of our containers,
* add each of our containers to the list.
*/
addDirectConnections(tab)
{
/* add myself */
tab[self] = true;
/* add my CollectiveGroup objects */
foreach (local cur in collectiveGroups)
tab[cur] = true;
/* add my contents */
foreach (local cur in contents)
{
if (tab[cur] == nil)
cur.addDirectConnections(tab);
}
/* add my containers */
foreach (local cur in locationList)
{
if (tab[cur] == nil)
cur.addDirectConnections(tab);
}
}
/*
* Transmit energy from a container onto me.
*/
shineFromWithout(fromParent, sense, ambient, fill)
{
/* if this increases my ambient level, accept the new level */
if (ambient > tmpAmbient_)
{
local levelThru;
/* remember the new level and fill material to this point */
tmpAmbient_ = ambient;
tmpAmbientFill_ = fill;
/* transmit to my contents */
shineOnContents(sense, ambient, fill);
/*
* We must transmit this energy to each of our other
* parents, possibly reduced for traversing our connector.
* Calculate the new level after traversing our connector.
*/
levelThru = adjustBrightness(ambient, transSensingThru(sense));
/*
* if there's anything left, transmit it to the other
* containers
*/
if (levelThru >= 2)
{
/* transmit to each container except the source */
foreach (local cur in locationList)
{
/* if this isn't the sender, transmit to it */
if (cur != fromParent)
cur.shineFromWithin(self, sense, levelThru, fill);
}
}
}
}
/*
* Build a sense path from a container to me
*/
sensePathFromWithout(fromParent, sense, trans, obs, fill)
{
/*
* if there's better transparency along this path than along any
* previous path we've used to visit this item, take this path
*/
if (transparencyCompare(trans, tmpTrans_) > 0)
{
local transThru;
/* remember the new path to this point */
tmpTrans_ = trans;
tmpObstructor_ = obs;
/* we're coming to this object from outside */
tmpPathIsIn_ = true;
/* transmit to my contents */
sensePathToContents(sense, trans, obs, fill);
/*
* We must transmit this energy to each of our other
* parents, possibly reduced for traversing our connector.
* Calculate the new level after traversing our connector.
*/
transThru = transparencyAdd(trans, transSensingThru(sense));
/* if we changed the transparency, we're the obstructor */
if (transThru != trans)
obs = self;
/*
* if there's anything left, transmit it to the other
* containers
*/
if (transThru != opaque)
{
/* transmit to each container except the source */
foreach (local cur in locationList)
{
/* if this isn't the sender, transmit to it */
if (cur != fromParent)
cur.sensePathFromWithin(self, sense,
transThru, obs, fill);
}
}
}
}
/*
* Call a function on each connected container. Since we provide a
* sense path connection among our containers, we must iterate over
* each of our containers.
*/
forEachConnectedContainer(func, [args])
{
forEachContainer(func, args...);
}
/*
* Return a list of my connected containers. We connect to all of
* our containers, so simply return my location list.
*/
getConnectedContainers = (locationList)
/*
* Check moving an object through me. This is called when we try to
* move an object from one of our containers to another of our
* containers through me. By default, we don't allow it.
*/
checkMoveThrough(obj, dest)
{
/* return an error - cannot move through <self> */
return new CheckStatusFailure(&cannotMoveThroughMsg, obj, self);
}
/*
* Check touching an object through me. This is called when an
* actor tries to reach from one of my containers through me into
* another of my containers. By default, we don't allow it.
*/
checkTouchThrough(obj, dest)
{
/* return an error - cannot reach through <self> */
return new CheckStatusFailure(&cannotReachThroughMsg, dest, self);
}
/*
* Check throwing an object through me. This is called when an actor
* tries to throw a projectile 'obj' at 'dest' via a path that
* includes 'self'. By default, we don't allow it.
*/
checkThrowThrough(obj, dest)
{
return new CheckStatusFailure(&cannotThrowThroughMsg, dest, self);
}
/* check for moving via a path */
checkMoveViaPath(obj, dest, op)
{
/* if moving through us, run the separate Move check */
if (op == PathThrough)
return checkMoveThrough(obj, dest);
/* if we can inherit, do so */
if (canInherit())
return inherited(obj, dest, op);
/* return success by default */
return checkStatusSuccess;
}
/* check for touching via a path */
checkTouchViaPath(obj, dest, op)
{
/* if reaching through us, run the separate Touch check */
if (op == PathThrough)
return checkTouchThrough(obj, dest);
/* if we can inherit, do so */
if (canInherit())
return inherited(obj, dest, op);
/* return success by default */
return checkStatusSuccess;
}
/* check for throwing via a path */
checkThrowViaPath(obj, dest, op)
{
/* if throwing through us, run the separate Throw check */
if (op == PathThrough)
return checkThrowThrough(obj, dest);
/* if we can inherit, do so */
if (canInherit())
return inherited(obj, dest, op);
/* return success by default */
return checkStatusSuccess;
}
;
/*
* Occluder: this is a mix-in class that can be used with multiple
* inheritance to combine with other classes (such as SenseConnector, or
* Thing subclasses), to create an "occluded view." This lets you
* exclude certain objects from view, and you can make the exclusion vary
* according to the point of view.
*
* This class is useful for situations where the view from one location
* to another is partially obstructed. For example, suppose we have two
* rooms, connected by a window between them. The window is the sense
* connector that connects the two top-level locations, and it makes
* objects in one room visible from the point of view of the other room.
* Suppose that one room contains a bookcase, with its back to the
* window. From the point of view of the other room, we can't see
* anything inside the bookcase. This class allows for such special
* situations.
*
* Note that occlusion rules are applied "globally" within a room - that
* is, anything that an Occluder occludes will be removed from view, even
* if it's visible from another, non-occluding connector. Hence,
* occlusion always takes precedence over "inclusion" - if an object is
* occluded just once, then it won't be in view, no matter how many times
* it's added back into view by other connectors. This comes from the
* order in which the occlusion rules are considered. Occlusion rules
* are always run last, and they can't distinguish the connector that
* added an object to view. So, we first run around and collect up
* everything that can be seen, by considering all of the different paths
* to seeing those things. Then, we go through all of the occlusion
* rules that apply to the room, and we remove from view everything that
* the occluding connectors want to occlude.
*/
class Occluder: object
/*
* Do we occlude the given object, in the given sense and from the
* given point of view? This returns true if the object is occluded,
* nil if not. By default, we simply ask the object whether it's
* occluded by this occluder from the given POV.
*/
occludeObj(obj, sense, pov)
{
/* by default, simply ask the object what it thinks */
return obj.isOccludedBy(self, sense, pov);
}
/*
* When we initialize for the sense path calculation, register to
* receive notification after we've finished building the sense
* table. We'll use the notification to remove any occluded objects
* from the sense table.
*/
clearSenseInfo()
{
/* do the normal work */
inherited();
/* register for notification after we've built the table */
senseTmp.notifyList.append(self);
}
/*
* Receive notification that the sense path calculation is now
* finished. 'objs' is a LookupTable containing all of the objects
* involved in the sense path calculation (the objects are the keys
* in the table). Each object in the table now has its tmpXxx_
* properties set to the sense path data we've calculated for that
* object - tmpTrans_ is the transparency to the object, tmpAmbient_
* is the ambient light level at the object, and so on.
*
* Since our job is to occlude certain objects from view, we'll run
* through the table and test each object using our occlusion rule.
* If we find that we do occlude an object, we'll set its
* transparency to 'opaque' to indicate that it cannot be seen.
*/
finishSensePath(objs, sense)
{
/* get the point of view of the calculation */
local pov = senseTmp.pointOfView;
/* run through the table, and apply our rule to each object */
objs.forEachAssoc(function(key, val)
{
/* if this object is occluded, set its path to opaque */
if (occludeObj(key, sense, pov))
{
/* set this object to opaque */
key.tmpTrans_ = key.tmpTransWithin_ = opaque;
/* we're the obstructor for the object */
key.tmpObstructor_ = key.tmpObstructorWithin_ = self;
}
});
}
;
/*
* DistanceConnector: a special type of SenseConnector that connects its
* locations with distance. This can be used for things like divided
* rooms, where a single physical location is modeled with two or more
* Room objects - the north and south end of a large cave, for example.
* This is also useful for cases where two rooms are separate but open to
* one another, such as a balcony overlooking a courtyard.
*
* Note that this inherits from both SenseConnector and Intangible.
* Intangible is included as a base class because each instance will need
* to derive from Thing, so that it fits into the normal sense model, but
* will virtually never need any other physical presence in the game
* world; Intangible fills both of these needs.
*/
class DistanceConnector: SenseConnector, Intangible
/* all senses are connected through us, but at a distance */
transSensingThru(sense) { return distant; }
/*
* When checking for reaching through this connector, specialize the
* failure message to indicate that distance is the specific problem.
* (Without this specialization, we'd get a generic message when
* trying to reach through the connector, such as "you can't reach
* that through <self>."
*/
checkTouchThrough(obj, dest)
{
/* we can't touch through this connector due to the distance */
return new CheckStatusFailure(&tooDistantMsg, dest);
}
/*
* When checking for throwing through this container, specialize the
* failure message to indicate that distance is the specific problem.
*/
checkThrowThrough(obj, dest)
{
return new CheckStatusFailure(&tooDistanceMsg, dest);
}
/*
* Do allow moving an object through a distance connector. This
* should generally only be involved at all when we're moving an
* object programmatically, in which case we should already have
* decided that the movement is allowable. Any command that tries to
* move an object through a distance connector will almost certainly
* have a suitable set of preconditions that checks for reachability,
* which will in most cases disallow the action anyway before we get
* to the point of wanting to move anything.
*/
checkMoveThrough(obj, dest) { return checkStatusSuccess; }
/*
* Report the reason that we stopped a thrown projectile from hitting
* its intended target. This is called when we're along the path
* between the thrower and the intended target, AND 'self' objects to
* the action.
*
* The default version of this method in Thing reports that the
* projectile hits 'self', as though self were a physical obstruction
* like a fence or wall. In the case of a distance connector,
* though, the reason isn't usually obstruction, but simply that the
* connector imposes such a distance that the actor can't throw the
* projectile far enough to reach the intended target. We therefore
* override the Thing version to report that the projectile fell
* short of the target.
*
* Note that if you do want to allow throwing a projectile across the
* distance represented by this connector, you can override
* checkThrowThrough() to return checkStatusSuccess.
*/
throwTargetHitWith(projectile, path)
{
/*
* figure out where we fall to when we hit this object, then send
* the object being thrown to that location
*/
getHitFallDestination(projectile, path)
.receiveDrop(projectile, new DropTypeShortThrow(self, path));
}
;
/*
* A drop-type descriptor for a "short throw," which occurs when the
* target is too far away to reach with our throw (i.e., the thrown
* object falls short of the target).
*/
class DropTypeShortThrow: DropTypeThrow
construct(target, path)
{
/* inherit the default handling */
inherited(target, path);
/* we care about the *intended* target, not the distance connector */
target_ = path[path.length()];
}
standardReport(obj, dest)
{
/* show the short-throw report */
mainReport(&throwFallShortMsg, obj, target_,
dest.getNominalDropDestination());
}
getReportPrefix(obj, dest)
{
/* return the short-throw prefix */
return gActor.getActionMessageObj().throwShortMsg(obj, target_);
}
;
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3