Attachments is a demonstration of adv3Lite Attachable classes.
The game follows the specification of Exercise 22 in Learning TADS 3 with Adv3Lite:
The player character is the only survivor aboard a small scouting space-ship that has just been attacked, holing its hull so that all the air is evacuated, killing everyone else aboard. The player character has survived since he was suited up making repairs to the antenna on the outside of the ship when the attack occurred. The attacking vessel has departed, but now the player character must effect repairs to take his own ship to safety. The player character starts the game in the airlock. He is wearing a space suit to which is attached an air cylinder (nearly exhausted) and a helmet; a lamp is currently plugged into the helmet but can be unplugged from it and plugged in elsewhere for recharging. Just as the game begins, the lights aboard ship go out, indicating a power failure. The outer and inner airlock doors are operated by levers, with dials indicating the air pressure outside the hull, inside the airlock, and inside the ship.
Just inboard of the airlock is a storage chamber with a rack containing a spare air cylinder (full enough to last the whole game and more), a charging socket, an equipment locker, a freezer, and a winch (fixed in place). Inside the equipment locker are two connectors (for joining lengths of cable), a short length of cable, and a roll of hull repair fabric. To operate the winch while the main power supply is out, it is necessary to attach one end of the cable to the winch and the other to the charging socket. The charging socket can also be used to recharge the lamp, when the lamp is plugged into it. A hawser runs from the winch; one end of the hawser can be carried by the player character into other locations (in which case there’ll be a length of hawser running all the way back to the winch); if the free end of the hawser is attached to something (such as a pile of debris) and the winch then operated (by pressing a button on it while the winch has power), the hawser will be rewound, dragging whatever’s attached to the other end with it. Aft of the storage hold is the Engine Room. Here there’s a power switch that can be turned on to restore power to the whole ship, but only once the main fault has been repaired, and an airflow control lever than can be pulled to repressurize the ship, but only once the hole in the hull has been repaired.
Forward of the Storage Hold is the Living Quarters, which took the brunt of the blast from the attacking vessel, and now has a large hole in part of the hull. This can be repaired by attaching a square of fabric from the locker to the hull. To one side of the Living Quarters is the door into a cabin, but this won’t open until pressure has been restored to the ship. Along the floor of the Living Quarters is an electrical conduit which contains the main power cable for the ship (now exposed by the same blast that tore through the hull). A length of the cable has been burned away, and must be repaired by attaching the short length of cable from the locker to the two ends of the severed cable by means of the electrical connectors (also from the locker). Unfortunately, the mass of debris left over from the blast blocks access to the conduit to repair the cable, and can only be removed by using the winch and hawser. Once the hull has been patched the ship can be repressurized (using the lever in the Engine Room), and once the ship has been repressurized the cabin door can be opened, allowing access to the cabin. Inside the cabin is a bed and a cabinet, the latter containing a security card.
Forward of the Living Quarters is the Bridge, containing (amongst anything else you think should be on the Bridge of a scouting space-ship) a card reader and green button. If the green button is pressed once main power has been restored and the security card is attached to the card reader, the controls come back to life, and the game is won.
#charset "us-ascii" #include <tads.h> #include "advlite.h" /* * EXERCISE 22 - ATTACHMENT * * A demonstration of adv3Lite Attachable classes. * * Handling Attachables is not always straightforward, to say the least, so * this is the most complex of the demo games. If you have not already done * so, you may want to become familiar with the other demo games first. * * Attachables are complicated because there is so many different ways in * which they could behave that the library classes can only provide a general * framework which the game author must customize to suit each particular * case. For example, if I attach a rope to a heavy box lying on the floor and * then try to walk out of the room still holding the rope, a number of * different things might happen, including: * * (1) I walk into the next room, still holding the rope, dragging the box * along behind me. * * (2) I walk into the next room, still holding the rope, but it becomes * detached from the box. * * (3) I walk into the next room, still holding the rope, which is so long * that it has not yet become taut. * * (4) I am jerked to a halt by the rope, which has become taut, and cannot * leave the room while I am holding the rope. * * (5) I walk into the room, but am forced to let go of the rope in the * process. * * And those are just the possibilities involving a rope attached to a box; * many other kind of attachment relationships are possible. * * Given the vast number of possibilities, this demonstration game cannot hope * to cover them all, and will only attempt a small range by way of * illustration. * * Also, because this game is already quite complex enough, and is intended * primarily to illustrate the use of Attachables, this comments in the source * code will be largely restricted to those parts of the code relating to the * implementation of Attachable objects. * * Finally, in adv3Lite the Attachable classes all descend from * SimpleAttachable which implements simpler cases of attachment. */ versionInfo: GameID IFID = '7aa136e2-0442-4c01-9d0b-2cf9ad94a903' name = 'Exercise 22 - Attachment' byline = 'by Eric Eve' htmlByline = 'by <a href="mailto:eric.eve@hmc.ox.ac.uk">Eric Eve</a>' version = '1' authorEmail = 'Eric Eve <eric.eve@hmc.ox.ac.uk>' desc = 'A demonstration of adv3Lite Attachable classes.' htmlDesc = 'A demonstration of adv3Lite Attachable classes.' showAbout() { "This is primarily a demonstration of adv3Lite Attachables. The game can be played through to a winning conclusion (or a losing one!), but it has been designed with demonstrating Attachables in mind rather than creating something that is a particularly great game.\b If you do want to play through the game and find you get stuck, remember that this is demonstration of attachable objects. Most of the things you need to do in this game will involve attaching (or detaching) objects to one another.<.p>"; } ; gameMain: GameMainDef /* Define the initial player character; this is compulsory */ initialPlayerChar = me showIntro() { "You weren't keen on being the one ordered outside to go and fix the transmission aerial, but that space-walk probably saved your life.\b The Federation warship (at least, you assume it was a Federation warship) appeared as if from nowhere and struck without warning, holing the tiny spy-ship with a single blast of its laser canon, and throwing you from your precarious perch. You very much doubt anyone inside the ship could have survived -- no one else was suited up and the decompression would have killed them almost instantly.\b Fortunately the hostile warship vanished as suddenly as it appeared, assuming its work was done and not even bothering to check for survivors. There very nearly weren't any; it took you over an hour to work your way back to the ship and in through the airlock, by which time the oxygen in your tank was all but exhausted. Removing your helmet you gasped gratefully at the air in the airlock, remembering to switch off your helmet lamp as it, too, was starting to dim.\b But it seems your troubles are far from over yet.\b"; } ; /* * The game takes place entirely aboard a spaceship, so compass directions * will have no meaning. We therefore override Room to disallow movement in * compass directions, and provide players with an explanation of the * directions that can be used for moving around the ship. */ modify Room /* * Compass directions are not allowed (because they have no meaning) * aboard the ship. */ allowCompassDirections = nil /* On the other hand we want to allow shipboard directions everywhere */ allowShipboardDirections = true /* * A custom property representing the air pressure in the room (in * bar). In this game this will be either 0 or 1. */ pressure = 0 /* At the start of the game the power is off and all rooms are dark. */ isLit = (powerSwitch.isOn) ; /* The floor of a ship is called the deck */ modify Floor vocab = 'deck;;floor ground' ; CustomMessages messages = [ Msg(no compass directions, 'That direction has no meaning here; aboard ship you can go port (P), starboard (SB), fore (F) or aft (A). ') ] ; /* Define a message that'll be shown just before the first command prompt. */ InitObject execute() { new OneTimePromptDaemon(self, &introMessage); } introMessage = "The airlock light just went out, another casualty of the damage inflicted by the Federation warship. The artificial gravity still seems to be working, though, so some backup batteries must still have some charge left in them. In the meantime, you instinctively finger your helmet lamp. " ; /* * A Custom class for Doors that are opened and closed by an external * mechanism, not by using OPEN and CLOSE commands. */ class IndirectDoor: Door isOpenable = nil lockability = notLockable cannotOpenMsg = '{The subj dobj} {is} operated with a lever. ' cannotCloseMsg = (cannotOpenMsg) notLockableMsg = '{The subj dobj} {has} no lock. ' ; /* * DoorLever is custom class we use for the code common to the two levers * that control the doors in the airlock. */ class DoorLever: Lever, Fixture /* A custom property - the other lever (used for various purposes). */ otherLever = nil /* The door controlled by this lever. */ myDoor = nil dobjFor(Pull) { verify() { inherited; /* * This lever can't be pulled when the other one is, since we * shouldn't have both airlock doors open at once. */ local other = otherLever; gMessageParams(other); if(otherLever.isPulled) illogicalNow('{The subj dobj} {is} temporarily locked in place while {the subj other} {is} pulled down, to prevent both airlock doors being opened at once. '); } check() { inherited(); /* * Don't let the player open a door if there's a vacuum on the * other side and the player is not wearing both suit and * helmet, since that would be fatal. */ if((helmet.wornBy != gActor || spaceSuit.wornBy != gActor) && myDoor.destination.pressure == 0) "Opening <<myDoor.theName>> would be fatal to you right now. "; } } /* Pulling the lever opens the corresponding door; pushing it closes it. */ makePulled(stat) { inherited(stat); myDoor.makeOpen(stat); "\^<<myDoor.theName>> slides <<stat ? 'open' : 'closed'>>. "; if(stat && airlock.pressure != myDoor.destination.pressure) { "There's a sudden rush of air. "; airlock.pressure = myDoor.destination.pressure; } } cannotTakeMsg = '{The subj dobj} is firmly fitted to the bulkhead. ' ; //------------------------------------------------------------------------------ /* The starting location. */ airlock: Room 'Main Airlock' "This small airlock, on the port side of the ship, is just about large enough for one man to stand in -- two would be uncomfortably cosy. A pair of levers control the airlock doors, and a trio of dials indicate the air pressure inside and outside the airlock. " starboard = innerDoor port = outerDoor pressure = 1 out asExit(starboard) roomBeforeAction() { if(gActionIs(GoOut) && outerDoor.isOpen) goInstead(port); } ; + redLever: DoorLever 'red lever' "It's marked <q>Outer Door</q>. " otherLever = greenLever myDoor = outerDoor collectiveGroups = [leverGroup] ; + greenLever: DoorLever 'green lever' "It's marked <q>Inner Door</q>. " otherLever = redLever myDoor = innerDoor dobjFor(Push) { verify() { if(hawser.isIn(airlock)) illogicalNow('You can\'t close the inner door while the hawser is running through it. '); inherited; } } collectiveGroups = [leverGroup] ; /* * COLLECTIVE GROUP * * Using this CollectiveGroup allows us to provide a collective description of * the two levers as a pair, rather than two individual descriptions, in * response to EXAMINE. */ + leverGroup: CollectiveGroup, Fixture 'levers' "There's a red lever (controlling the outer door), and a green lever (controlling the inner door). " ; + portDial: Fixture 'port dial' "The port dial shows the air pressure beyond the port (outer) door; i.e. outside the ship; it currently registers 0 bar" collectiveGroups = [dialGroup] ; + centreDial: Fixture 'central dial; center centre middle' "The central dial shows the air pressure inside the airlock; it currently registers <<airlock.pressure>> bar. " collectiveGroups = [dialGroup] ; + starboardDial: Fixture 'starboard dial' "The starboard dial shows the air pressue beyond the starboard (inner) door, it currenly registers <<storageCompartment.pressure>> bar." collectiveGroups = [dialGroup] ; /* * COLLECTIVE GROUP * * This CollectiveGroup similarly allows the three dials to be described * together. */ + dialGroup: CollectiveGroup, Fixture 'dials' "The port dial indicates that air pressure beyond the port (outer) door is currenly 0 bar. The central dial shows that the air pressure inside the airlock is currently <<airlock.pressure>> bar. The starboard dial shows that the air pressure beyond the starboard (inner) door, i.e. inside the ship is currently <<storageCompartment.pressure>> bar. " ; + innerDoor: IndirectDoor -> airlockDoor 'inner door' ; + outerDoor: IndirectDoor 'outer door' destination: Room { pressure = 0 } otherSide = self canTravelerPass(traveler) { return nil; } explainTravelBarrier(traveler) { "You don't want to go back out there again; you've had quite enough space-walking for now. "; } ; /* * The player character object. This doesn't have to be called me, but me is a * convenient name. If you change it to something else, rememember to change * gameMain.initialPlayerChar accordingly. */ + me: Player 'you' ; /* * SIMPLE ATTACHABLE * * We make the spaceSuit a SimpleAttachable so that an OxygenTank can be * attached to it. * * SimpleAttachable is designed to model an asymmetric attachment, where one * of the attached objects is the major attachment and all the others are its * minor attachments (we'd consider a limpet mine to be attached to a * battleship, not the other way round - the battleship would be the major * attachment and the mine the minor attachment). If the major attachment is * moved, its minor attachments (those listed in its attachments property) * move with it. If a minor attachment is moved (e.g. by the player character * taking it) it becomes detached from the major attachment (think of a magnet * attached to a fridge). * * By default, a SimpleAttachable is designed to be used with other * SimpleAttacables: both the major attachment and its minor attachments * should be of class SimpleAttachable. */ ++ spaceSuit: SimpleAttachable, Wearable 'spacesuit; dark blue (space); suit' "It's dark blue, the colour of a naval uniform. " wornBy = me owner = me /* * The space suit is the major attachable here (any oxygen tank * attached to it will move round with it). We set it up as the major * attachments by listing the minor attachments that can be attached * to it. */ allowableAttachments = [emptyTank, fullTank] /* Prevent the player removing the space suit in a vacuum. */ dobjFor(Doff) { check() { if(gActor.getOutermostRoom.pressure == 0) "That would be fatal; there\s no air in this place. "; } } ; /* * OxygenTank is a custom class defined below; it inherits from * SimpleAttachable. By locating emptyTank in spaceSuit we ensure that the * empty tank starts out attached to the space suit at the start of the * game. */ +++ emptyTank : OxygenTank 'empty +' airLevel = 4 initiallyAttachedTo = spaceSuit ; /* * The helmet is another SimpleAttachable so we can attach a lamp to it * (and detach the lamp from it), or plug and unplug the lamp. */ ++ helmet: PlugAttachable,SimpleAttachable, Wearable 'helmet; standard (space)' "Its a standard issue space helmet. " /* The lamp is the only object that can be attached to the helmet. */ allowableAttachments = [lamp] /* * While the player character is wearing the helmet, he's dependent on * the air it contains (or can get from the oxygen cylinder) to * breathe, so we need to model the breathing and air supply. We do * this with a DAEMON. */ breathingDaemonID = nil breathingDaemon() { /* Reduce the airLevel by one each turn the helmet is worn. */ airLevel --; /* * If there's an oxygen tank attached to the space suit and the * tank contains enough air, refresh the air supply in the helmet. */ if(myTank && myTank.airLevel > 0) { local newAir = min(myTank.airLevel, maxLevel - airLevel); if(newAir > 1) "There's a sudden rush of fresh oxygen into your helmet. "; airLevel += newAir; myTank.airLevel -= newAir; } /* * If the air in the helmet is running out, display a warning * message. */ switch(airLevel) { case 4: "The air inside your helmet is starting to feel very stale. "; break; case 3: "The air in your helmet is scarcely breathable. "; break; case 2: "The air in your helmet is so stale you are beginning to faint. "; break; case 1: "You can scarcely breathe at all; you are about to pass out. "; break; /* * Once there's no air left, the player character dies of * asphyxiation. */ case 0: "For want of breathable air, you lose consciousness. "; finishGameMsg(ftDeath, [finishOptionUndo] ); } } /* * A custom property defining which oxygen tank the helmet is getting * its air supply from. This will be the tank attached to the suit. */ myTank = (spaceSuit.attachments.valWhich({x: x.ofKind(OxygenTank) })) /* The amount of air in the helmet (a custom property). */ airLevel = 5 /* The maximum amount of air the helment can hold. */ maxLevel = 5 dobjFor(Wear) { action() { inherited; /* * When the helmet is put on, it starts full of air, but we * then need to start the breathing daemon. */ airLevel = maxLevel; breathingDaemonID = new Daemon(self, &breathingDaemon, 1); } } dobjFor(Doff) { check() { /* * Don't allow the player character to remove the helmet in a * vacuum. */ if(gActor.getOutermostRoom.pressure == 0) "That would be certain death; your location is unpressurised. "; } action() { inherited; /* When the helmet is removed, stop the breathing daemon. */ if(breathingDaemonID) { breathingDaemonID.removeEvent(); breathingDaemonID = nil; } } } ; /* * PLUG ATTACHABLE * * PlugAttachable is a mix-in class for use with other Attachable classes to * make PLUG INTO and UNPLUG FROM behave like ATTACH TO and DETACH FROM. * * We make the lamp a PlugAttachable so it can be plugged into a charging * socket. * * It's also a SimpleAttachable. It can be attached either to the helmet or to * the charging socket, but that's defined on them. * * Had we wanted to, we could have saved ourselves a bit of work here by using * the FueledLightSource class from the FueledLightSource extension. */ +++ lamp: PlugAttachable, SimpleAttachable, Flashlight 'lamp; (helmet)' "It's designed to be attached to the helmet, but can be detached for charging. It can also be turned on and off. " /* * Make it visible in the dark when off so it's still in scope for the * player to turn it on. */ visibleInDark = true /* * When the lamp is attached to the socket, it is charged up again. We * control this with a charging daemon, so that the longer it's plugged * in, the more charge it receives. */ chargeDaemonID = nil attachTo(other) { inherited(other); /* Start the charging daemon when we're plugged into the socket. */ if(other == chargingSocket) { chargeDaemonID = new Daemon(self, &chargeDaemon, 1); if(fuelLevel < 10) { "The lamp starts to shine more brightly as soon as it's plugged in. "; fuelLevel = 10; } } } detachFrom(other) { inherited(other); /* Stop the charging daemon when we're removed from the socket. */ if(other == chargingSocket) { chargeDaemonID.removeEvent(); chargeDaemonID = nil; } } chargeDaemon() { /* Increase the charge in the lamp each turn it's plugged in. */ if(fuelLevel < maxCharge) fuelLevel += 20; } /* * fuelDaemon() is a custom method we use to track the amount of charge * left in the lamp. */ fuelDaemon() { switch(fuelLevel--) { case 5: "The lamp is definitely dimmer. "; break; case 4: "The lamp starts to flicker. "; break; case 3: "The lamp seems very dim now. "; break; case 2: "The lamp is about to go out. "; break; case 1: "The lamp gives its final flicker. "; break; case 0: "The lamp goes out. "; makeOn(nil); fuelLevel = 0; if(!getOutermostRoom.isIlluminated) "You are plunged into darkness. "; break; } } fuelDaemonID = nil makeOn(stat) { inherited(stat); /* * In addition to the standard (inherited) handling for turning a * Flashlight on or off we need to start or stop the Daemon that * consumes the lamp's "fuel" (or charge) each turn it's on. */ if(stat && fuelDaemonID == nil) fuelDaemonID = new SenseDaemon(self, &fuelDaemon, 1); if(!stat && fuelDaemonID != nil) fuelDaemonID.removeEvent(); } dobjFor(SwitchOn) { check() { if(fuelLevel < 1) "The lamp is fully discharged; it won't light up. "; } } maxCharge = 100000 fuelLevel = 15 initiallyAttachedTo = helmet ; //============================================================================== /* * Define the custom OxygenTank class. * * It's another SIMPLE ATTACHABLE, but it's made a bit more complicated by * the fact that only one OxygenTank can be attached to the space suit at * a time. */ class OxygenTank: SimpleAttachable 'oxygen tank; silver metal air; cylinder' /* * If there's another OxygenCylinder attached to the space suit when * the player tries to attach this one, insist that the other one is * detached first. */ dobjFor(AttachTo) { check() { if(gIobj == spaceSuit && gIobj.attachments.indexWhich({x: x.ofKind(OxygenTank) && x != self }) != nil) "You'll have to remove the other tank first. "; } } ; //------------------------------------------------------------------------------ storageCompartment: Room 'Storage Compartment' "<<first time>>This area seems to have been largely undamaged by the blast from the enemy warship, although anything not nailed down was probably swept away by the explosive decompression elsewhere in the ship, since there was no time for the bulkheads to seal. <<only>>The equipment locker looks secure, as does the food freezer. The airlock door is to port, controlled by a red button, while the engine room lies aft and the living quarters are foreward. There's a charging socket on the bulkhead, and a winch off to one side (normally used to haul supplies aboard the ship). " aft = engineRoom port = airlockDoor fore = livingQuarters ; + airlockDoor: Door ->innerDoor 'airlock door' lockability = indirectLockable ; /* * In sdv3Lite a Button is fixed in place by default, since it's usually part * of something else. */ + Button 'red button' dobjFor(Push) { action() { if(hawser.isIn(airlock) && airlockDoor.isOpen) "You can't close the airlock door while the cable is running through it. "; airlockDoor.makeOpen(!airlockDoor.isOpen); "The airlock door slides <<airlockDoor.isOpen ? 'open' : 'closed'>>. "; } } ; + Container, Fixture 'rack' ; /* OxygenTank is a custom class defined below. */ ++ fullTank: OxygenTank 'full +' initSpecialDesc = "A single oxygen tank remains in the rack by the airlock; you hope it's still full. " airLevel = 5000 ; /* * PLUG ATTACHABLE, SIMPLE ATTACHABLE * * The charging socket is both a PlugAttachable (so we can plug things into * it) and a SimpleAttachable (which means anything attached to it will * be moved into it, as we'll make it the major attachment). */ + chargingSocket: PlugAttachable, SimpleAttachable, Fixture 'charging socket' "<< powerSwitch.isOn ? 'With the power back on, there should be no difficulty getting a charge from the socket' : 'Although the main power is off, the charging socket has a backup battery which should hopefully have retained enough charge for your purposes' >>." /* The list of items that can be attached to the charging socket. */ allowableAttachments = [lamp, blackCable] ; + equipmentLocker: LockableContainer, Fixture 'equipment locker' ; ++ Decoration 'pieces of equipment;;;them' "<<notImportantMsg>>" notImportantMsg = (livingQuarters.seen ? 'There\'s nothing else you need here right now. ' : 'Once you\'ve assessed the damage you\'ll know what you need to repair it. ') isListed = true isListedInContents = true aName = ('various ' + name) ; /* * CableConnector (NEARBY ATTACHABLE) * * A CableConnector is another custom class (defined below). As can be seen * below, CableConnector subclasses from NearbyAttachable. The purpose of * CableConnectors is to join two lengths of cable together. * */ ++ redConnector: CableConnector 'red +' isHidden = true ; ++ yellowConnector: CableConnector 'yellow +' isHidden = true ; /* * PLUG ATTACHABLE / ATTACHABLE * * The black cable is a PlugAttachable so it can be plugged into things. It's * also of class Cable, which is defined below. Cable derives from Attachable * so that it can be connected to two things at once to establish an * electrical connection between them. */ ++ blackCable: PlugAttachable, Attachable, Cable 'length of black cable[n]' "It's a standard electrical cable, about a couple of metres long. " isHidden = true /* * After every ATTACH TO action involving an ElectricalConnector (a custom * class defined below), check to see whether the action has completed an * electrical connection between the two sections of cable that need to be * re-connected. */ afterAction() { if(gActionIs(AttachTo) && gDobj.ofKind(ElectricalConnector) && aftCable.isElectricallyConnectedTo(foreCable)) "You complete the connection between the fore and aft sections of the severed cable. "; } ; ++ roll: Thing 'roll of hull repair fabric; grey gray' "The fabric is grey with a faintly metallic appearance. It can be used to make temporary repairs to breaches in the hull. It won't protect against impact from large objects or weapons fire, but it's good enough to keep out light dust to shield against harmful cosmic radiation. It's also good enough to make an airtight seal so that the ship can be repressurized. " isHidden = true dobjFor(Take) { check() { if(fabric.moved) "You don\'t need any more of the fabric right now. "; } action() { /* * The fabric object, representing a square of fabric cut from * this roll, is defined below. */ fabric.actionMoveInto(gActor); "You unroll the fabric, cut of a square of the size you need, and return the roll to the locker. "; } } ; + freezer: LockableContainer, Fixture 'freezer; large' "It's a large freezer; it needs to be in order to supply provisions to the crew for several weeks. " ; ++ Decoration 'some food' "There's plenty of food, at any rate; whatever else kills you, it won't be starvation. " isListedInContents = true isListed = true notImportantMsg = ( helmet.wornBy == me ? 'You can\'t eat while you\'re wearing your helmet, so you may as well leave the food alone for now. ' : 'You can worry about eating once you\'ve got the ship away from here. ') ; /* * PLUG ATTACHABLE SIMPLE ATTACHABLE * * We make the winch a PlugAttachable and the SimpleAttachable so the black * cable can be plugged into it. */ + winch: PlugAttachable, SimpleAttachable, Fixture 'winch;;casing' "The winch, fixed firmly to the floor, is used for moving heavy loads around the ship. It is controlled by the blue button on its casing. " allowableAttachments = [blackCable] socketCapacity = 2 ; ++ Button, Component 'blue button' dobjFor(Push) { action() { /* * If power hasn't been restored, the only way to get the * winch to work is to connect it to the charging socket with * the black cable. For this connection to be made the black * cable must be attached both to the winch and to the socket. */ if(!powerSwitch.isOn && !(blackCable.isAttachedTo(winch) && blackCable.isAttachedTo(chargingSocket))) { "Nothing happens, presumably because the winch has no power. "; return; } if(hawser.isIn(storageCompartment)) "The winch emits a short whine and the hawser twitches a couple of times, but since the hawser is just about fully rewound, no more happens. "; else if(hawser.isAttachedTo(debris)) { "The winch whines and the hawser goes taut. The pitch of the whine rises as the winch strains to move the hawser. For a moment or two nothing more happens, but then there's a loud scraping noise up forward, and the hawser slowly drags a mass of debris back into the storage chamber. "; debris.actionMoveInto(storageCompartment); } else { "The winch springs into life, rewinding the hawser all the way back into the storage compartment. "; hawser.moveInto(storageCompartment); } } } ; /* * SIMPLE ATTACHABLE * * We make the hawser a SimpleAttachable so that (a) we can attach it to * things (in this game, only the debris) and (b) so it moves with whatever * its attached to). */ + hawser: SimpleAttachable 'hawser; winch loose free of[prep]; length cable end' "<<specialDesc>>" /* Vary the description of the hawser depending on where it is. */ specialDesc() { switch(getOutermostRoom) { case storageCompartment: "A short length of hawser dangles from the winch. "; break; case bridge: case livingQuarters: "The hawser runs aft. "; break; case engineRoom: "The hawser runs off for'ard. "; break; case airlock: case cabin: "The hawser runs out through the door to port. "; break; } } specialDescBeforeContents = nil specialDescListingOrder = 100 getFacets = [proxyHawser1, proxyHawser2] aName = (theName) ; /* * If the hawser object is not in the storage compartment, there must be a * length of hawser running from the the winch to wherever the other end * of the hawser is. In that case we need a proxy object to describe the * length of hawser that's visible inside the storage compartment. * ProxyHawser is a custom class defined below. */ + proxyHawser1: ProxyHawser 'hawser; winch of[prep];length cable' "The hawser from the winch runs <<cableDir()>>. " /* * We want this length of hawser to be visible only when the real * hawser object is elsewhere. */ isHidden = (hawser.isIn(storageCompartment)) /* * Describe which way the hawser runs depending on where the other end * of the hawser is. */ cableDir() { switch(hawser.getOutermostRoom) { case engineRoom: "aft"; break; case airlock: "port, into the airlock"; break; default: "foreward"; break; } } /* * The other objects that can represents sections of the hawser are * facets of this object. */ getFacets = [hawser, proxyHawser2] ; //------------------------------------------------------------------------------ /* * Define our custom CABLE CONNECTOR class. * * This descends from our custom ElecticalConnector class (defined below), * which in turn descends from NearbyAttachable. */ class CableConnector: ElectricalConnector 'cable connector; plastic; ring' "In appearance, it lools like a plastic ring. Its function is to join one length of cable to another. " allowableAttachments = [blackCable] ; /* * Definition of the custom CABLE class. * * Cable derives from our custom ElectricalConnector class (defined * immediately below). The only customization required on this class is to * define what a Cable can connect to: Cables can connect to * CableConnectors. */ class Cable: ElectricalConnector allowAttach(obj) { return obj.ofKind(CableConnector); } ; /* * ELECTRICAL CONNECTOR NEARBY ATTACHABLE * * Our custom ElectricalConnector class derives from the library's * NearbyAttachable class. A NearbyAttachable is an Attachable that * enforces the condition that the attached objects must be in a * particular location. By default this is the location that one of the * objects is already in, but this can be customised by overriding * attachedLocation. */ class ElectricalConnector: NearbyAttachable /* * isElectricallyConnectedTo() is a custom method to test whether an * electrical connection exists between two ElectricalConnectors. An * electrical connection exists if the two ElectricalConnectors are * directly or indirectly attached; they're indirectly attached if * there's a chain of attached objects between them. */ isElectricallyConnectedTo(obj) { local vec = new Vector(10, [self]); local i = 0, cur; while(i < vec.length) { cur = vec[++i]; vec.appendUnique(cur.attachments); vec.appendUnique(cur.attachedToList); if(vec.indexOf(obj)) return true; } return nil; } ; //------------------------------------------------------------------------------ engineRoom: Room 'Engine Room' "The engine room also looks undamaged. So far as you can tell from a quick scan of the instruments, the main engine is undamaged. <<controls.desc>> " fore = storageCompartment out asExit(fore) ; + controls: Decoration 'instruments; of[prep]; mass controls; them' "There's a mass of instruments and controls here, but \v<<controls.notImportantMsg>>" notImportantMsg = 'The only ones that concern you right now are the large red switch controlling the ship\'s power, the yellow lever controlling its air supply, and the pressure gauge showing the air pressure inside the ship.' ; + powerSwitch: Switch, Fixture 'large red switch' "The switch is currently <<if isOn>> on<<else>> off<<end>>. " makeOn(stat) { if(stat) { if(!aftCable.isElectricallyConnectedTo(foreCable)) { "'The switch snaps back into the off position; as a safety measure it won't stay on when there's a major fault somewhere in the system. "; exit; } "The lights come on all over the ship. "; } else "The ship's lighting goes off again. "; inherited(stat); } ; + airLever: Lever, Fixture 'yellow lever' dobjFor(Pull) { check() { if(!lqWall.repaired) "If you turn on the air supply without first repairing the damage to the hull, you'll simply waste all the air; it\'ll rush out though the breach in the hull as fast as it tries to fill the ship. "; } } makePulled(stat) { inherited(stat); if(stat && location.pressure == 0) { "There's a hiss of air rushing from vents all over the ship, and the needle in the pressure gauge starts to rise. "; forEachInstance(Room, { loc: loc.pressure = 1 } ); } } ; + Fixture 'pressure gauge;;needle' "The needle on the gauge indicates that the pressure inside the ship is currently <<location.pressure>> bar. " ; //------------------------------------------------------------------------------ livingQuarters: Room 'Living Quarters' "<<if lqWall.repaired>>The hole in the starboard hull has been patched up with a piece of fabric, but it<<else>>It<<end>> was obviously this area that took the brunt of the laser blast<<unless lqWall.repaired>>. If that wasn't immediately apparent from the gaping hole in the hull where the starboard cabins should be, it's <<else>>, as is <<end>> apparent from the wreckage of what was once the crew lounge, although it looks as if one of the sleeping cabins to port might still be usable. The storage compartment lies aft, while the way foreward leads to the bridge. <<first time>> \bThere's no sign of any other members of the crew. They were almost certainly all sucked out through the hole in the hull by the rapid decompression. <<equipmentLocker.contents.forEach({o: o.discover})>><<only>>" aft = storageCompartment port = cabinDoor fore = bridge ; /* * SIMPLE ATTACHABLE * * SimpleAttachable is the base class for all the other Attachable classes we * have seen. * * Here we use it to define a wall to which something (namely, a piece of * fabric) can be attached. */ + lqWall: SimpleAttachable, Fixture 'hull; starboard; wall' desc = "<<repaired ? 'The starboard hull now looks airtight' : 'There\'s a gaping hole in the hull'>>. " /* * The starboard hull is always 'the starboard hull', never 'a * starboard hull' */ aName = (theName) allowableAttachments = [fabric] /* * repaired is a custom property to indicate when the wall has been * repaired by attaching the piece of fabric. */ repaired = (isAttachedTo(fabric)) ; + gapingHole: Component 'gaping hole' "It's roughly circular, and about a metre in diameter. " /* * Make ATTACH FABRIC TO HOLE equivalent to ATTACH FABRIC TO STARBOARD * WALL. */ iobjFor(AttachTo) { remap = lqWall } /* Once the hull is repaired, the hole is no longer visible. */ isHidden = lqWall.repaired ; /* * DOOR */ + cabinDoor: SimpleAttachable, Door ->cabinDoorInside 'cabin door; pale; patch' "<<unless sign.isIn(self)>>A slightly paler patch on the door indicates where something may have dropped off.<<end>> " lockability = lockableWithoutKey /* * Normally making both sides of a Door a Lockable (as opposed to * LockableWithKey or IndirectLockable) doesn't achieve much, since the * door csn simply be unlocked an UNLOCK command. In this case, however, * we can achieve a significant effect by using a check condition to * restrict unlocking the door - the door won't unlock until the ship has * been pressurized. */ dobjFor(Unlock) { check() { if(location.pressure == 0) { "The cabin door won't unlock; it must be the only pressure seal that's holding, in which case you won't get the door open until you restore pressure to the ship. That may be just as well, of course; if there's someone still in the cabin that pressure seal may be the only thing keeping them alive. "; } } } /* The door can't be closed if there's a hawser running through it. */ dobjFor(Close) { verify() { if(hawser.isIn(cabin)) illogicalNow('You can\'t close the door while the cable\'s running through it. '); inherited; } } allowableAttachments = [sign] ; /* * ATTACHABLE COMPONENT * * An AttachableComponent is something that would normally be part of * something else, but which may either start out detached from it or may * later become detached (such as a handle that can be unscrewed, perhaps). * For this example we use a sign that would normally be fixed to a door. */ + sign: AttachableComponent 'sign' "The sign says <q>CAPTAIN</q>. " initSpecialDesc = "A sign lies on the floor, seemingly dislodged from its normal place by the blast. " initiallyAttached = nil ; /* * The conduit running along the floor of the living quarters starts out * covered with debris which needs to be moved before it can be accessed. */ + conduit: Container, Fixture 'cable conduit' useInitSpecialDesc = (!aftCable.isElectricallyConnectedTo(foreCable)) initSpecialDesc = "Amongst other things, the blast has exposed the main power conduit running along the floor, showing that part of the main power cable has been burned away completely. <<unless debris.moved>> Unfortunately, it looks as if the debris from the blast might make it difficult to get at the conduit. <<end>>" /* * Customise the way our contents are listed, so that when the cables * are all joined up our listing says so. */ examineLister: descContentsLister { showListSuffix(lst, pl, paraCnt) { /* * Note the use of lexicalParent here: we want to refer to the * isInInitState property of the conduit, not the examineLister. */ "<< lexicalParent.useInitSpecialDesc ? '' : ', with all the cables now joined together in a continuous run'>>. "; } } /* * The conduit starts off covered with debris that makes it difficult to * get at, although we can see what's inside. To simulate that we use the * checkReachIn method to display a message prohibiting access until the * debris is moved. */ checkReachIn(actor, target?) { if(!debris.moved) "The debris covering the conduit blocks your access to it. "; } ; /* * FixedCable is a custom class defined below (inheriting from * NearbyAttachable). Since the fore and aft sections of the cable are meant * to be a couple of metres apart, the same CableConnector can't be * simultaneously attached to both the foreCable and the aftCable. But since a * CableConnector is a SimpleAttachable, this constraint is enforced in any * case: a SimpleAttachable can only be attached to one thing at at time. */ ++ aftCable: FixedCable 'aft +' "It's a short length of cable running from the aft end of the conduit until it ends about two metres short of the fore end, the central part of the cable having been burned away. " /* * We use the initSpecialDesc of the aftCable to describe the foreCable as * well, so we include <<exclude foreCable>> in this description to ensure * that foreCable doesn't get a separate listing. */ initSpecialDesc = "At either end of the conduit you see the severed ends of the cable running fore and aft. <<exclude foreCable>>" useInitSpecialDesc = (location.useInitSpecialDesc) specialDescBeforeContents = true ; ++ foreCable: FixedCable 'fore +' "It's a short length of cable running from the forward end of the conduit until it ends about two metres short of the aft end, the central part of the cable having been burned away. " ; /* * SIMPLE ATTACHABLE * * The debris is a SimpleAttachable so we can attach the hawser to it to * drag it out of the way using the winch. We also make it of class Heavy * so we can't move it by hand. */ + debris: SimpleAttachable, Heavy 'pile of debris; metal fused; mass wreckage' "It's a mass of metal fused together by the laser blast that holed the ship; at a rough guess it's what's left of the wardroom table plus parts of the starboard side cabins. " /* Allow the hawser to be attached to the debris. */ allowableAttachments = [hawser] specialDesc = "A mass of fused metal debris is strewn over the deck. " ; /* * As with proxyHawser1 above, we need an object to represent the section * of hawser running through the living quarters if the end of the hawser * has been taken beyond the living quarters to either the bridge or the * cabin. ProxyHawser is a custom class defined below. */ + proxyHawser2: ProxyHawser desc = "The hawser runs aft back to the storage compartment and <<cableDir()>>. " isHidden = !(hawser.isIn(bridge) || hawser.isIn(cabin)) cableDir() { switch(hawser.getOutermostRoom) { case bridge: "foreward to the bridge"; break; case cabin: "port, into the cabin"; break; default: "foreward"; break; } } getFacets = [hawser, proxyHawser1] ; /* * Define the custom ProxyHawser class to represent lengths of hawser * passing through a location when the free end of the hawser is elsewhere. */ class ProxyHawser: Fixture 'hawser; winch of[prep]; length cable' specialDescBeforeContents = nil specialDesc = (desc) cannotTakeMsg = 'There\'s not much point picking up the middle of the hawser. ' dobjFor(Pull) { verify() {} check() { if(hawser.isAttachedTo(debris)) "You can't pull the hawser by hand; the load at its far end is too heavy. "; } action() { hawser.moveInto(gActor.location); "You keep pulling the hawser until its loose end appears. "; } } ; /* * Define the custom FixedCable class, used to define the two ends of the * cable left in the conduit. */ class FixedCable: Cable, Fixture 'cable end; of[prep] severed; section' isListedInContents = true isListed = true aName = theName ; //------------------------------------------------------------------------------ cabin: Room 'Sleeping Cabin' "This cabin seems to have escaped any serious damage, and there's a bunk you can sleep in if you ever get time for sleep, with a bedside cabinet placed conveniently next to it. " starboard = cabinDoorInside out asExit(starboard) ; + cabinDoorInside: Door -> cabinDoor 'cabin door' /* We can\'t close the door if the hawser is running through it. */ dobjFor(Close) { verify() { if(hawser.isIn(cabin)) illogicalNow('You can\'t close the door while the cable\'s running through it. '); inherited; } } ; + Platform, Fixture 'bunk;;bed' ; + Fixture 'bedside cabinet; small metal' "It's a small metal cabinet with a door. " remapOn: SubComponent {} remapIn: SubComponent, LockableContainer { dobjFor(Open) { preCond = [touchObj, objUnlocked]} } ; ++ ContainerDoor 'cabinet door' ; /* * SIMPLE ATTACHMENT * * This one really is simple. */ ++ securityCard: SimpleAttachable, Thing 'security card; white purple; markings' "It's a plain white card, about 8cm by 4cm, with purple markings. " subLocation = &remapIn ; //------------------------------------------------------------------------------ bridge: Room 'Bridge' "<q>Bridge</q> is perhaps a grandiose title for this small control cabin, but it's functionally the bridge, since this is where the ship is flown from. A single chair, firmly attached to the floor, faces a bank of instruments; there used to be another chair for the person watching the spy scans, but it must have been sucked out by the decompression, since the way out aft lies open. " aft = livingQuarters out asExit(aft) ; + bridgeChair: Platform, Fixture 'pilot\'s chair; large' "It's a large chair, arranged to face the bank of instruments used to fly the ship. " cannotTakeMsg = 'The chair is securely attached to the floor; that\'s why it\'s still there despite the decompression. ' canLieOnMe = nil ; + Decoration 'bank of instruments; multicoloured; control dispays screens buttons/switches knobs dials readouts panel; it them' "There are multicoloured displays, screens, buttons, switches, knobs, dials and readouts aplenty, none of them active. The whole lot can be turned on by pressing the green button right in the middle of the control panel<<conditions()>>." conditions() { local cardOK = (securityCard.isAttachedTo(cardReader)); if(powerSwitch.isOn && cardOK) return; ", but nothing will happen until "; if(!powerSwitch.isOn) "the main power supply is switched on <<cardOK ? '' : 'and '>>"; if(!cardOK) "a security card is attached to the card reader"; } notImportantMsg = 'At this point, only the green button and the card reader need concern you. ' ; + greenButton: Button 'green button' dobjFor(Push) { action() { if(!powerSwitch.isOn) "Nothing happens; there's no power. "; else if(!securityCard.isAttachedTo(cardReader)) "Nothing happens; this model of ship won't respond to the controls unless a security card is attached to the card reader. "; else { "The instruments spring to life, indicating that the ship is ready to fly. It's unlikely that the Federation warship that attacked before will come back for a second look, but there's no point hanging around, so you set course for the nearest imperial world and head back for safety.\b"; finishGameMsg(ftVictory, [finishOptionUndo]); } } } ; /* * SIMPLE ATTACHMENT * * Another SimpleAttachment that's actuall simple. We just define the * allowableAttachments property to contain the list of things that can be * attached to it: in this case, just the securityCard. */ + cardReader: SimpleAttachable, Fixture 'card reader' "It's about 8cm by 4cm. " allowableAttachments = [securityCard] ; //============================================================================== /* * SIMPLE ATTACHABLE * * The piece of fabric used to repair the ship's hull can be handled quite * simply with a SimpleAttachable. */ fabric: SimpleAttachable 'square of fabric; dull grey gray metallic; patch' "It's just over a metre square, and of a dull metallic grey colour. <<isAttachedTo(lqWall) ? 'Now that' : 'When'>> it\'s attached to the starboard hull, covering the hole, it should provide an airtight seal. " /* * attachTo() is a standard library method of SimpleAttachable that * handles the effects of attaching one object to another. Here we carry * out the inherited handling and then explain what happened. */ attachTo(other) { inherited(other); if(other == lqWall) { "You place the fabric over the hole, covering it completely. The outer edges of the fabric cling to the inner hull, making a seal that should be air-tight enough for you to repressurize the ship. "; } } /* * Once the patch has been fixed to the wall, we don't want it to be * detached again. */ isDetachable = nil /* Explain why we can't detach the fabric from the wall. */ cannotDetachMsg = 'Now that you\'ve covered the hole you don\'t want to expose it again. ' ;