cfad47cfa3/t3compiler/tads3/lib/adv3/extras.t

4b825dc642cb6eb9a060e54bf8d69288fbee4904cfad47cfa334b206c65f22086bcc5d63e6f70944
1
#charset "us-ascii"
2
3
/* 
4
 *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
5
 *   
6
 *   TADS 3 Library - extras: special-purpose object classes
7
 *   
8
 *   This module defines classes for specialized simulation objects.
9
 *   
10
 *   Portions are based on original work by Eric Eve, incorporated by
11
 *   permission.  
12
 */
13
14
/* include the library header */
15
#include "adv3.h"
16
17
18
/* ------------------------------------------------------------------------ */
19
/*
20
 *   A "complex" container is an object that can have multiple kinds of
21
 *   contents simultaneously.  For example, a complex container could act
22
 *   as both a surface, so that some objects are sitting on top of it, and
23
 *   simultaneously as a container, with objects inside.
24
 *   
25
 *   The standard containment model only allows one kind of containment per
26
 *   container, because the nature of the containment is a feature of the
27
 *   container itself.  The complex container handles multiple simultaneous
28
 *   containment types by using one or more sub-containers: for example, if
29
 *   we want to be able to act as both a surface and a regular container,
30
 *   we use two sub-containers, one of class Surface and one of class
31
 *   Container, to hold the different types of contents.  When we need to
32
 *   perform an operation specific to a certain containment type, we
33
 *   delegate the operation to the sub-container of the appropriate type.
34
 *   
35
 *   Note that the complex container itself treats its direct contents as
36
 *   components, so any component parts can be made direct contents of the
37
 *   complex container object.
38
 *   
39
 *   If you want to include objects in your source code that are initially
40
 *   located within the component sub-containers, define them as directly
41
 *   within the ComplexContainer object, but give each one a 'subLocation'
42
 *   property set to the property of the component sub-container that will
43
 *   initially contain it.  For example, here's how you'd place a blanket
44
 *   inside a washing machine, and a laundry basket on top of it:
45
 *   
46
 *.  + washingMachine: ComplexContainer 'washing machine' 'washing machine'
47
 *.    subContainer: ComplexComponent, Container { etc }
48
 *.    subSurface: ComplexComponent, Surface { etc }
49
 *.  ;
50
 *.  
51
 *.  ++ Thing 'big cotton blanket' 'blanket'
52
 *.    subLocation = &subContainer
53
 *.  ;
54
 *.  
55
 *.  ++ Container 'laundry basket' 'laundry basket'
56
 *.    subLocation = &subSurface
57
 *.  ;
58
 *   
59
 *   The subLocation setting is only used for initialization, and we
60
 *   automatically set it to nil right after we use it to set up the
61
 *   initial location.  If you want to move something into one of the
62
 *   sub-containers on the fly, simply refer to the desired component
63
 *   directly:
64
 *   
65
 *   pants.moveInto(washingMachine.subContainer); 
66
 */
67
class ComplexContainer: Thing
68
    /*
69
     *   Our inner container, if any.  This is a "secret" object (in other
70
     *   words, it doesn't appear to players as a separate named object)
71
     *   that we use to store the contents that are meant to be within the
72
     *   complex container.  If this is to be used, it should be set to a
73
     *   Container object - the most convenient way to do this is by using
74
     *   the nested object syntax to define a ComplexComponent Container
75
     *   instance, like so:
76
     *   
77
     *   washingMachine: ComplexContainer
78
     *.    subContainer: ComplexComponent, Container { etc }
79
     *.  ;
80
     *   
81
     *   Note that we use the ComplexComponent class (as well as
82
     *   Container) for the sub-container object.  This makes the
83
     *   sub-container automatically use the name of its enclosing object
84
     *   in messages (in this case, the sub-container will use the same
85
     *   name as the washing machine).
86
     *   
87
     *   Note that the sub-containers don't have to be of class
88
     *   ComplexComponent, but using that class makes your job a little
89
     *   easier because the class sets the location and naming
90
     *   automatically.  If you prefer to define your sub-containers as
91
     *   separate objects, not nested in the ComplexContainer's
92
     *   definition, there's no need to make them ComplexComponents; just
93
     *   make them ordinary Component objects.
94
     *   
95
     *   If this property is left as nil, then we don't have an inner
96
     *   container.  
97
     */
98
    subContainer = nil
99
100
    /*
101
     *   Our inner surface, if any.  This is a secret object like the
102
     *   inner container; this object acts as our surface. 
103
     */
104
    subSurface = nil
105
106
    /*
107
     *   Our underside, if any.  This is a secret object like the inner
108
     *   container; this object can act as the space underneath us, or as
109
     *   our bottom surface. 
110
     */
111
    subUnderside = nil
112
113
    /*
114
     *   Our rear surface or container, if any.  This is a secret internal
115
     *   object like the inner container; this object can act as our back
116
     *   surface, or as the space just behind us.  
117
     */
118
    subRear = nil
119
120
    /* a list of all of our component objects */
121
    allSubLocations = [subContainer, subSurface, subUnderside, subRear]
122
123
    /* 
124
     *   Show our status.  We'll show the status for each of our
125
     *   sub-objects, so that we list any contents of our sub-container or
126
     *   sub-surface along with our description. 
127
     */
128
    examineStatus()
129
    {
130
        /* if we have a sub-container, show its status */
131
        if (subContainer != nil)
132
            subContainer.examineStatus();
133
134
        /* if we have a sub-surface, show its status */
135
        if (subSurface != nil)
136
            subSurface.examineStatus();
137
138
        /* if we have a sub-rear, show its status */
139
        if (subRear != nil)
140
            subRear.examineStatus();
141
142
        /* if we have a sub-underside, show its status */
143
        if (subUnderside != nil)
144
            subUnderside.examineStatus();
145
    }
146
147
    /* 
148
     *   In most cases, the open/closed and locked/unlocked status of a
149
     *   complex container refer to the status of the sub-container.  
150
     */
151
    isOpen = (subContainer != nil ? subContainer.isOpen : inherited)
152
    isLocked = (subContainer != nil ? subContainer.isLocked : inherited)
153
154
    makeOpen(stat)
155
    {
156
        if (subContainer != nil)
157
            subContainer.makeOpen(stat);
158
        else
159
            inherited(stat);
160
    }
161
162
    makeLocked(stat)
163
    {
164
        if (subContainer != nil)
165
            subContainer.makeLocked(stat);
166
        else
167
            inherited(stat);
168
    }
169
        
170
171
    /* 
172
     *   route all commands that treat us as a container to our
173
     *   sub-container object 
174
     */
175
    dobjFor(Open) maybeRemapTo(subContainer != nil, Open, subContainer)
176
    dobjFor(Close) maybeRemapTo(subContainer != nil, Close, subContainer)
177
    dobjFor(LookIn) maybeRemapTo(subContainer != nil, LookIn, subContainer)
178
    iobjFor(PutIn) maybeRemapTo(subContainer != nil,
179
                                PutIn, DirectObject, subContainer)
180
    dobjFor(Lock) maybeRemapTo(subContainer != nil, Lock, subContainer)
181
    dobjFor(LockWith) maybeRemapTo(subContainer != nil,
182
                                   LockWith, subContainer, IndirectObject)
183
    dobjFor(Unlock) maybeRemapTo(subContainer != nil, Unlock, subContainer)
184
    dobjFor(UnlockWith) maybeRemapTo(subContainer != nil,
185
                                     UnlockWith, subContainer, IndirectObject)
186
187
    /* route commands that treat us as a surface to our sub-surface */
188
    iobjFor(PutOn) maybeRemapTo(subSurface != nil,
189
                                PutOn, DirectObject, subSurface)
190
191
    /* route commands that affect our underside to our sub-underside */
192
    iobjFor(PutUnder) maybeRemapTo(subUnderside != nil,
193
                                   PutUnder, DirectObject, subUnderside)
194
    dobjFor(LookUnder) maybeRemapTo(subUnderside != nil,
195
                                    LookUnder, subUnderside)
196
197
    /* route commands that affect our rear to our sub-rear-side */
198
    iobjFor(PutBehind) maybeRemapTo(subRear != nil,
199
                                    PutBehind, DirectObject, subRear)
200
    dobjFor(LookBehind) maybeRemapTo(subRear != nil, LookBehind, subRear)
201
202
    /* route commands relevant to nested rooms to our components */
203
    dobjFor(StandOn) maybeRemapTo(getNestedRoomDest(StandOnAction) != nil,
204
                                  StandOn, getNestedRoomDest(StandOnAction))
205
    dobjFor(SitOn) maybeRemapTo(getNestedRoomDest(SitOnAction) != nil,
206
                                SitOn, getNestedRoomDest(SitOnAction))
207
    dobjFor(LieOn) maybeRemapTo(getNestedRoomDest(LieOnAction) != nil,
208
                                LieOn, getNestedRoomDest(LieOnAction))
209
    dobjFor(Board) maybeRemapTo(getNestedRoomDest(BoardAction) != nil,
210
                                Board, getNestedRoomDest(BoardAction))
211
    dobjFor(Enter) maybeRemapTo(getNestedRoomDest(EnterAction) != nil,
212
                                Enter, getNestedRoomDest(EnterAction))
213
214
    /* map GET OUT/OFF to whichever complex component we're currently in */
215
    dobjFor(GetOutOf) maybeRemapTo(getNestedRoomSource(gActor) != nil,
216
                                   GetOutOf, getNestedRoomSource(gActor))
217
    dobjFor(GetOffOf) maybeRemapTo(getNestedRoomSource(gActor) != nil,
218
                                   GetOffOf, getNestedRoomSource(gActor))
219
220
    /*
221
     *   Get the destination for nested travel into this object.  By
222
     *   default, we'll look at the sub-container and sub-surface
223
     *   components to see if either is a nested room, and if so, we'll
224
     *   return that.  The surface takes priority if both are nested rooms.
225
     *   
226
     *   You can override this to differentiate by verb, if desired; for
227
     *   example, you could have SIT ON and LIE ON refer to the sub-surface
228
     *   component, while ENTER and BOARD refer to the sub-container
229
     *   component.
230
     *   
231
     *   Note that if you do need to override this method to distinguish
232
     *   between a sub-container ("IN") and a sub-surface ("ON") for nested
233
     *   room purposes, there's a subtlety to watch out for.  The English
234
     *   library maps "sit on" and "sit in" to the single Action SitOn;
235
     *   likewise with "lie in/on" for LieOn and "stand in/on" for StandOn.
236
     *   If you're distinguishing the sub-container from the sub-surface,
237
     *   you'll probably want to distinguish SIT IN from SIT ON (and
238
     *   likewise for LIE and STAND).  Fortunately, even though the action
239
     *   class is the same for both phrasings, you can still find out
240
     *   exactly which preposition the player typed using
241
     *   action.getEnteredVerbPhrase().  
242
     */
243
    getNestedRoomDest(action)
244
    {
245
        /* 
246
         *   check the sub-surface first to see if it's a nested room;
247
         *   failing that, check the sub-container; failing that, we don't
248
         *   have a suitable component destination 
249
         */
250
        if (subSurface != nil && subSurface.ofKind(NestedRoom))
251
            return subSurface;
252
        else if (subContainer != nil && subContainer.ofKind(NestedRoom))
253
            return subContainer;
254
        else
255
            return nil;
256
    }
257
258
    /*
259
     *   Get the source for nested travel out of this object.  This is used
260
     *   for GET OUT OF <self> - we figure out which nested room component
261
     *   the actor is in, so that we can remap the command to GET OUT OF
262
     *   <that component>. 
263
     */
264
    getNestedRoomSource(actor)
265
    {
266
        /* figure out which child the actor is in */
267
        foreach (local chi in allSubLocations)
268
        {
269
            if (chi != nil && actor.isIn(chi))
270
                return chi;
271
        }
272
273
        /* the actor doesn't appear to be in one of our component locations */
274
        return nil;
275
    }
276
277
    /*
278
     *   Get a list of objects suitable for matching ALL in TAKE ALL FROM
279
     *   <self>.  By default, if we have a sub-surface and/or
280
     *   sub-container, we return everything in scope that's inside either
281
     *   one of those.  Otherwise, if we have a sub-rear-surface and/or an
282
     *   underside, we'll return everything from those.  
283
     */
284
    getAllForTakeFrom(scopeList)
285
    {
286
        local containers;
287
288
        /* 
289
         *   Make a list of the containers in which we're going to look.
290
         *   If we have a sub-container or sub-surface, look only in those.
291
         *   Otherwise, if we have a rear surface or underside, look in
292
         *   those. 
293
         */
294
        containers = [];
295
        if (subContainer != nil)
296
            containers += subContainer;
297
        if (subSurface != nil)
298
            containers += subSurface;
299
        if (containers == [])
300
        {
301
            if (subRear != nil)
302
                containers += subRear;
303
            if (subUnderside != nil)
304
                containers += subUnderside;
305
        }
306
307
        /* 
308
         *   return the list of everything in scope that's directly in one
309
         *   of the selected containers, but isn't a component of its
310
         *   direct container 
311
         */
312
        return scopeList.subset(
313
            {x: (x != self
314
                 && containers.indexOf(x) == nil
315
                 && containers.indexWhich(
316
                     {c: x.isDirectlyIn(c) && !x.isComponentOf(c)}) != nil)});
317
    }
318
319
    /*
320
     *   Add an object to my contents.  If the object has a subLocation
321
     *   setting, take it as indicating which of my subcontainers is to
322
     *   contain the object.  
323
     */
324
    addToContents(obj)
325
    {
326
        local sub;
327
        
328
        /* 
329
         *   if the object has a subLocation, add it to my appropriate
330
         *   component object; if not, add to my own contents as usual 
331
         */
332
        if ((sub = obj.subLocation) != nil)
333
        {
334
            /* 
335
             *   It specifies a subLocation - add it to the corresponding
336
             *   component's contents.  Note that subLocation is a property
337
             *   pointer - &subContainer for my container component,
338
             *   &subSurface for my surface component, etc.  
339
             */
340
            self.(sub).addToContents(obj);
341
342
            /* 
343
             *   The object's present location is merely for set-up
344
             *   purposes, so that the '+' object definition notation can
345
             *   be used to give the object its initial location.  The
346
             *   object really wants to be in the sub-container, to whose
347
             *   contents list we've just added it.  Set its location to
348
             *   the sub-container.  
349
             */
350
            obj.location = self.(sub);
351
352
            /*
353
             *   Now that we've moved the object into its sub-location,
354
             *   forget the subLocation setting, since this property is
355
             *   only for initialization. 
356
             */
357
            obj.subLocation = nil;
358
        }
359
        else
360
        {
361
            /* there's no subLocation, so use the default handling */
362
            inherited(obj);
363
        }
364
    }
365
366
    /*
367
     *   If we have any SpaceOverlay children, abandon the contents of the
368
     *   overlaid spaces as needed. 
369
     */
370
    mainMoveInto(newCont)
371
    {
372
        /* 
373
         *   If any of our components are SpaceOverlays, notify them.  We
374
         *   only worry about the rear and underside components, since it's
375
         *   never appropriate for our container and surface components to
376
         *   act as space overlays. 
377
         */
378
        notifyComponentOfMove(subRear);
379
        notifyComponentOfMove(subUnderside);
380
381
        /* do the normal work */
382
        inherited(newCont);
383
    }
384
385
    /* 
386
     *   if we're being pushed into a new location (as a PushTraveler),
387
     *   abandon the contents of any SpaceOverlay components 
388
     */
389
    beforeMovePushable(traveler, connector, dest)
390
    {
391
        /* 
392
         *   notify our SpaceOverlay components that we're being moved, if
393
         *   we're going to end up in a new location 
394
         */
395
        if (dest != location)
396
        {
397
            /* notify our rear and underside components of the move */
398
            notifyComponentOfMove(subRear);
399
            notifyComponentOfMove(subUnderside);
400
        }
401
        
402
        /* do the normal work */
403
        inherited(traveler, connector, dest);
404
    }
405
406
    /* 
407
     *   if the given component is a SpaceOverlay, notify it that we're
408
     *   moving, so that it can abandon its contents as needed 
409
     */
410
    notifyComponentOfMove(sub)
411
    {
412
        /* if it's a SpaceOverlay, abandon its contents if necessary */
413
        if (sub != nil && sub.ofKind(SpaceOverlay))
414
            sub.abandonContents();
415
    }
416
417
    /* pass bag-of-holding operations to our sub-container */
418
    tryPuttingObjInBag(target)
419
    {
420
        /* if we have a subcontainer, let it handle the operation */
421
        return (subContainer != nil
422
                ? subContainer.tryPuttingObjInBag(target)
423
                : nil);
424
    }
425
426
    /* pass implicit PUT x IN self operations to our subcontainer */
427
    tryMovingObjInto(obj)
428
    {
429
        /* if we have a subcontainer, let it handle the operation */
430
        return (subContainer != nil
431
                ? subContainer.tryMovingObjInto(obj)
432
                : nil);
433
    }
434
;
435
436
/* 
437
 *   we don't actually define any subLocation property values anywhere, so
438
 *   declare it to make sure the compiler knows it's a property name 
439
 */
440
property subLocation;
441
442
/*
443
 *   A component object of a complex container.  This class can be used as
444
 *   a mix-in for sub-objects of a complex container (the subContainer or
445
 *   subSurface) defined as nested objects.
446
 *   
447
 *   This class is based on Component, which is suitable for complex
448
 *   container sub-objects because it makes them inseparable from the
449
 *   complex container.  It's also based on NameAsParent, which makes the
450
 *   object automatically use the same name (in messages) as the lexical
451
 *   parent object.  This is usually what one wants for a sub-object of a
452
 *   complex container, because it makes the sub-object essentially
453
 *   invisible to the user by referring to the sub-object in messages as
454
 *   though it were the complex container itself: "The washing machine
455
 *   contains...".
456
 *   
457
 *   This class also automatically initializes our location to our lexical
458
 *   parent, during the pre-initialization process.  Any of these that are
459
 *   dynamically created at run-time (using 'new') must have their
460
 *   locations set manually, because initializeLocation() won't be called
461
 *   automatically in those cases.  
462
 */
463
class ComplexComponent: Component, NameAsParent
464
    initializeLocation()
465
    {
466
        /* set our location to our lexical parent */
467
        location = lexicalParent;
468
469
        /* inherit default so we initialize our container's 'contents' list */
470
        inherited();
471
    }
472
473
    /* 
474
     *   Get our "identity" object.  We take our identity from our parent
475
     *   object, if we have one.  Note that our identity isn't simply our
476
     *   parent, but rather is our parent's identity, recursively defined.
477
     */
478
    getIdentityObject()
479
    {
480
        return (location != nil ? location.getIdentityObject() : self);
481
    }
482
483
    /* don't participate in 'all', since we're a secret internal object */
484
    hideFromAll(action) { return true; }
485
486
    /* 
487
     *   In case this component is being used to implement a nested room of
488
     *   some kind (a platform, booth, etc), use the complex container's
489
     *   location as the staging location.  Normally our staging location
490
     *   would be our direct container, but as with other aspects of
491
     *   complex containers, the container/component are meant to act as a
492
     *   single combined object, so we'd want to bypass the complex
493
     *   container and move directly between the enclosing location and
494
     *   'self'.  
495
     */
496
    stagingLocations = [lexicalParent.location]
497
;
498
499
/*
500
 *   A container door.  This is useful for cases where you want to create
501
 *   the door to a container as a separate object in its own right.  
502
 */
503
class ContainerDoor: Component
504
    /*
505
     *   In most cases, you should create a ContainerDoor as a component of
506
     *   a ComplexContainer.  It's usually necessary to use a
507
     *   ComplexContainer in order to use a door, since the door has to go
508
     *   somewhere, and it can't go inside the container it controls
509
     *   (because if it were inside, it wouldn't be accessible when the
510
     *   container is closed).
511
     *   
512
     *   By default, we assume that our immediate location is a complex
513
     *   container, and its subContainer is the actual container for which
514
     *   we're the door.  You can override this property to create a
515
     *   different relationship if necessary.  
516
     */
517
    subContainer = (location.subContainer)
518
519
    /* we're open if our associated sub-container is open */
520
    isOpen = (subContainer.isOpen)
521
522
    /* our status description mentions our open status */
523
    examineStatus()
524
    {
525
        /* add our open status */
526
        say(isOpen
527
            ? gLibMessages.currentlyOpen : gLibMessages.currentlyClosed);
528
529
        /* add the base class behavior */
530
        inherited();
531
    }
532
533
    /* looking in or behind a door is like looking inside the container */
534
    dobjFor(LookIn) remapTo(LookIn, subContainer)
535
    dobjFor(LookBehind) remapTo(LookIn, subContainer)
536
537
    /* door-like operations on the door map to the container */
538
    dobjFor(Open) remapTo(Open, subContainer)
539
    dobjFor(Close) remapTo(Close, subContainer)
540
    dobjFor(Lock) remapTo(Lock, subContainer)
541
    dobjFor(LockWith) remapTo(LockWith, subContainer, IndirectObject)
542
    dobjFor(Unlock) remapTo(Unlock, subContainer)
543
    dobjFor(UnlockWith) remapTo(UnlockWith, subContainer, IndirectObject)
544
;
545
546
547
/* ------------------------------------------------------------------------ */
548
/*
549
 *   A "space overlay" is a special type of container whose contents are
550
 *   supposed to be adjacent to the container object (i.e., self), but are
551
 *   not truly contained in the usual sense.  This is used to model spatial
552
 *   relationships such as UNDER and BEHIND, which aren't directly
553
 *   supported in the normal containment model.
554
 *   
555
 *   The special feature of a space overlay is that the contents aren't
556
 *   truly attached to the container object, so they don't move with it the
557
 *   way that the contents of an ordinary container do.  For example,
558
 *   suppose we have a space overlay representing a bookcase and the space
559
 *   behind it, so that we can hide a painting behind the bookcase: in this
560
 *   case, moving the bookcase should leave the painting where it was,
561
 *   because it was just sitting there in that space.  In the real world,
562
 *   of course, the painting was sitting on the floor all along, so moving
563
 *   the bookcase would have no effect on it; but our spatial relationship
564
 *   model isn't quite as good as reality's, so we have to resort to an
565
 *   extra fix-up step.  Specifically, when we move a space overlay, we
566
 *   always check to see if its contents need to be relocated to the place
567
 *   where they were really supposed to be all along.  
568
 */
569
class SpaceOverlay: BulkLimiter
570
    /* 
571
     *   If we move this object, the objects we contain might stay put
572
     *   rather than moving along with the container.  For example, if we
573
     *   represent the space behind a bookcase, moving the bookcase would
574
     *   leave objects that were formerly behind the bookcase just sitting
575
     *   on the floor (or attached to the wall, or whatever).  
576
     */
577
    mainMoveInto(newContainer)
578
    {
579
        /* check to see if our objects need to be left behind */
580
        abandonContents();
581
582
        /* now do the normal work */
583
        inherited(newContainer);
584
    }
585
586
    /* 
587
     *   when we're being pushed to a new location via push-travel, abandon
588
     *   our contents before we're moved 
589
     */
590
    beforeMovePushable(traveler, connector, dest)
591
    {
592
        /* check to see if our objects need to be left behind */
593
        if (dest != getIdentityObject().location)
594
            abandonContents();
595
596
        /* do the normal work */
597
        inherited(traveler, connector, dest);
598
    }
599
600
    /* 
601
     *   abandonLocation is where the things under me end up when I'm
602
     *   moved.
603
     *   
604
     *   An Underside or RearContainer represents an object that has a
605
     *   space underneath or behind it, respectively, but the space itself
606
     *   isn't truly part of the container object (i.e., self).  This
607
     *   means that when the container moves, the objects under/behind it
608
     *   shouldn't move.  For example, if there's a box under a bed,
609
     *   moving the bed out of the room should leave the box sitting on
610
     *   the floor where the bed used to be.
611
     *   
612
     *   By default, our abandonLocation is simply the location of our
613
     *   "identity object" - that is, the location of our nearest
614
     *   enclosing object that isn't a component.
615
     *   
616
     *   This can be overridden if the actual abandonment location should
617
     *   be somewhere other than our assembly location.  In addition, you
618
     *   can set this to nil to indicate that objects under/behind me will
619
     *   NOT be abandoned when I move; instead, they'll simply stay with
620
     *   me, as though they're attached to my underside/back surface.  
621
     */
622
    abandonLocation = (getIdentityObject().location)
623
    
624
    /* 
625
     *   By default we list our direct contents the first time we're
626
     *   moved, and ONLY the first time.  If alwaysListOnMove is
627
     *   overridden to true, then we'll list our contents EVERY time we're
628
     *   moved.  If neverListOnMove is set to true, then we'll NEVER list
629
     *   our contents automatically when moved; this can be used in cases
630
     *   where the game wants to produce its own listing explicitly,
631
     *   rather than using the default listing we generate.  (Obviously,
632
     *   setting both 'always' and 'never' is meaningless, but in case
633
     *   you're wondering, 'never' overrides 'always' in this case.)
634
     *   
635
     *   Setting abandonLocation to nil overrules alwaysListOnMove: if
636
     *   there's no abandonment, then we consider nothing to be revealed
637
     *   when we're moved, since my contents move along with me.  
638
     */
639
    alwaysListOnMove = nil
640
    neverListOnMove = nil
641
642
    /*
643
     *   The lister we use to describe the objects being revealed when we
644
     *   move the SpaceOverlay object and abandon the contents.  Each
645
     *   concrete kind of SpaceOverlay must provide a lister that uses
646
     *   appropriate language; the list should be roughly of the form
647
     *   "Moving the armoire reveals a rusty can underneath."  Individual
648
     *   objects can override this to customize the message further.  
649
     */
650
    abandonContentsLister = nil
651
652
    /*
653
     *   Abandon my contents when I'm moved.  This is called whenever we're
654
     *   moved to a new location, to take care of leaving behind the
655
     *   objects that were formerly under me.
656
     *   
657
     *   We'll move my direct contents into abandonLocation, unless that's
658
     *   set to nil.  We don't move any Component objects within me, since
659
     *   we assume those to be attached.  
660
     */
661
    abandonContents()
662
    {
663
        local dest;
664
        
665
        /* 
666
         *   if there's no abandonment location, our contents move with us,
667
         *   so there's nothing to do 
668
         */
669
        if ((dest = abandonLocation) == nil)
670
            return;
671
672
        /* 
673
         *   If we've never been moved before, or we always reveal my
674
         *   contents when moved, list our contents now.  In any case, if
675
         *   we *never* list on move, don't generate the listing.  
676
         */
677
        if ((alwaysListOnMove || !getIdentityObject().moved)
678
            && !neverListOnMove)
679
        {
680
            local marker1, marker2;
681
            
682
            /*
683
             *   We want to generate a listing of what is revealed by
684
             *   moving the object, which we can do by generating a
685
             *   listing of what we would normally see by looking in the
686
             *   overlay interior.  We want the listing as it stands now,
687
             *   but in most cases, we don't actually want to generate the
688
             *   list quite yet, because we want the action that's moving
689
             *   the object to complete and show all of its messages first.
690
             *   
691
             *   However, if the action leaves the actor in a new
692
             *   location, do generate the listing before the rest of the
693
             *   action output, since the listing won't make any sense
694
             *   after we've moved to (and displayed the description of)
695
             *   the new location.
696
             *   
697
             *   To accomplish all of this, generate the listing now,
698
             *   before the rest of the action output, but insert special
699
             *   report markers into the transcript before and after the
700
             *   listing.  Then, register to receive after-action
701
             *   notification; at the end of the action, we'll go back and
702
             *   move the range of transcript output between the markers
703
             *   to the end of the command's transcript entries, if we
704
             *   haven't moved to a new room.
705
             *   
706
             *   One final complication: we don't want our listing here to
707
             *   hide any default report from the main command, so run it
708
             *   as a sub-action.  A sub-action doesn't override the
709
             *   visibility of its parent's default report.  
710
             */
711
712
            /* first, add a marker to the transcript before the listing */
713
            marker1 = gTranscript.addMarker();
714
715
            /* generate the listing, using a generic sub-action context */
716
            withActionEnv(Action, gActor, {: listContentsForMove() });
717
718
            /* add another transcript marker after the listing */
719
            marker2 = gTranscript.addMarker();
720
721
            /* 
722
             *   create our special handler object to receive notification
723
             *   at the end of the command - it'll move the reports to the
724
             *   end of the command output if need be 
725
             */
726
            new SpaceOverlayAbandonFinisher(marker1, marker2);
727
        }
728
729
        /* now move my non-Component contents to the abandonment location */
730
        foreach(obj in contents)
731
        {
732
            /* if it's not a component, move it */
733
            if(!obj.ofKind(Component))
734
                obj.moveInto(dest);
735
        }
736
    }
737
738
    /*
739
     *   My weight does NOT include my "contents" if we abandon our
740
     *   contents on being moved.  Our contents are not attached to us as
741
     *   they are in a normal sort of container; instead, they're merely
742
     *   colocated, so when we're moved, that colocation relationship ends.
743
     */
744
    getWeight()
745
    {
746
        /* 
747
         *   if we abandon our contents on being moved, our weight doesn't
748
         *   include them, because they're not attached; otherwise, they do
749
         *   act like they're attached, and hence must be included in our
750
         *   weight as for any ordinary container 
751
         */
752
        if (abandonLocation != nil)
753
        {
754
            /* 
755
             *   our contents are not included in our weight, so our total
756
             *   weight is simply our own intrinsic weight 
757
             */
758
            return weight;
759
        }
760
        else
761
        {
762
            /* our contents are attached, so include their weight as normal */
763
            return inherited();
764
        }
765
    }
766
767
    /* 
768
     *   List our contents for moving the object.  By default, we examine
769
     *   our interior using our abandonContentsLister.  
770
     */
771
    listContentsForMove()
772
    {
773
        /* examine our contents with the abandonContentsLister */
774
        examineInteriorWithLister(abandonContentsLister);
775
    }
776
;
777
778
/* 
779
 *   Space Overlay Abandon Finisher - this is an internal object that we
780
 *   create in SpaceOverlay.abandonContents().  Its purpose is to receive
781
 *   an afterAction notification and clean up the report order if
782
 *   necessary. 
783
 */
784
class SpaceOverlayAbandonFinisher: object
785
    construct(m1, m2)
786
    {
787
        /* remember the markers */
788
        marker1 = m1;
789
        marker2 = m2;
790
791
        /* remember the actor's starting location */
792
        origLocation = gActor.location;
793
794
        /* register for afterAction notification */
795
        gAction.addBeforeAfterObj(self);
796
    }
797
798
    /* the transcript markers identifying the listing reports */
799
    marker1 = nil
800
    marker2 = nil
801
802
    /* the actor's location at the time we generated the listing */
803
    origLocation = nil
804
805
    /* receive our after-action notification */
806
    afterAction()
807
    {
808
        /* 
809
         *   If the actor hasn't changed locations, move the reports we
810
         *   generated for the listing to the end of the transcript. 
811
         */
812
        if (gActor.location == origLocation)
813
            gTranscript.moveRangeAppend(marker1, marker2);
814
    }
815
;
816
817
/*
818
 *   An "underside" is a special type of container that describes its
819
 *   contents as being under the object.  This is appropriate for objects
820
 *   that have a space underneath, such as a bed or a table.  
821
 */
822
class Underside: SpaceOverlay
823
    /*
824
     *   Can actors put new objects under self, using the PUT UNDER
825
     *   command?  By default, we allow it.  Override this property to nil
826
     *   if new objects cannot be added by player commands.  
827
     */
828
    allowPutUnder = true
829
830
    /* we need to LOOK UNDER this object to see its contents */
831
    nestedLookIn() { nestedAction(LookUnder, self); }
832
833
    /* use custom contents listers, for our special "under" wording */
834
    contentsLister = undersideContentsLister
835
    descContentsLister = undersideDescContentsLister
836
    lookInLister = undersideLookUnderLister
837
    inlineContentsLister = undersideInlineContentsLister
838
    abandonContentsLister = undersideAbandonContentsLister
839
    
840
    /* customize the message for taking something from that's not under me */
841
    takeFromNotInMessage = &takeFromNotUnderMsg
842
 
843
    /* customize the message indicating another object is already in me */
844
    circularlyInMessage = &circularlyUnderMsg
845
 
846
    /* message phrase for objects put under me */
847
    putDestMessage = &putDestUnder
848
 
849
    /* message when we don't have room to put another object under me */
850
    tooFullMsg = &undersideTooFull
851
852
    /* message when an object is too large (all by itself) to fit under me */
853
    tooLargeForContainerMsg = &tooLargeForUndersideMsg
854
855
    /* can't put self under self */
856
    cannotPutInSelfMsg = &cannotPutUnderSelfMsg
857
858
    /* can't put something under me when it's already under me */
859
    alreadyPutInMsg = &alreadyPutUnderMsg
860
861
    /* our implied containment verb is PUT UNDER */
862
    tryMovingObjInto(obj) { return tryImplicitAction(PutUnder, obj, self); }
863
864
    /* -------------------------------------------------------------------- */
865
    /*
866
     *   Handle putting things under me 
867
     */
868
    iobjFor(PutUnder)
869
    {
870
        verify()
871
        {
872
            /* use the standard put-in-interior verification */
873
            verifyPutInInterior();
874
        }
875
        check()
876
        {
877
            /* only allow it if PUT UNDER commands are allowed */
878
            if (!allowPutUnder)
879
            {
880
                reportFailure(&cannotPutUnderMsg);
881
                exit;
882
            }
883
        }
884
        action()
885
        {
886
            /* move the direct object onto me */
887
            gDobj.moveInto(self);
888
             
889
            /* issue our default acknowledgment */
890
            defaultReport(&okayPutUnderMsg);
891
        }
892
    }
893
    
894
    /*
895
     *   Looking "under" a surface simply shows the surface's contents. 
896
     */
897
    dobjFor(LookUnder)
898
    {
899
        verify() { }
900
        action() { examineInterior(); }
901
    }
902
;
903
904
/*
905
 *   A special kind of Underside that only accepts specific contents.
906
 */
907
class RestrictedUnderside: RestrictedHolder, Underside
908
    /* 
909
     *   A message that explains why the direct object can't be put under
910
     *   this object.  In most cases, the rather generic default message
911
     *   should be overridden to provide a specific reason that the dobj
912
     *   can't be put under me.  The rejected object is provided as a
913
     *   parameter in case the message needs to vary by object, but we
914
     *   ignore this and just use a single blanket failure message by
915
     *   default.  
916
     */
917
    cannotPutUnderMsg(obj) { return &cannotPutUnderRestrictedMsg; }
918
919
    /* override PutUnder to enforce our contents restriction */
920
    iobjFor(PutUnder) { check() { checkPutDobj(&cannotPutUnderMsg); } }
921
;
922
923
/*
924
 *   A "rear container" is similar to an underside: it models the space
925
 *   behind an object.  
926
 */
927
class RearContainer: SpaceOverlay
928
    /*
929
     *   Can actors put new objects behind self, using the PUT BEHIND
930
     *   command?  By default, we allow it.  Override this property to nil
931
     *   if new objects cannot be added by player commands.  
932
     */
933
    allowPutBehind = true
934
     
935
    /* we need to LOOK BEHIND this object to see its contents */
936
    nestedLookIn() { nestedAction(LookBehind, self); }
937
938
    /* use custom contents listers */
939
    contentsLister = rearContentsLister
940
    descContentsLister = rearDescContentsLister
941
    lookInLister = rearLookBehindLister
942
    inlineContentsLister = rearInlineContentsLister 
943
    abandonContentsLister = rearAbandonContentsLister
944
945
    /* the message for taking things from me that aren't behind me */
946
    takeFromNotInMessage = &takeFromNotBehindMsg
947
948
    /* 
949
     *   my message indicating that another object x cannot be put into me
950
     *   because I'm already in x 
951
     */
952
    circularlyInMessage = &circularlyBehindMsg
953
 
954
    /* message phrase for objects put under me */
955
    putDestMessage = &putDestBehind
956
 
957
    /* message when we're too full for another object */
958
    tooFullMsg = &rearTooFullMsg
959
960
    /* message when object is too large to fit behind me */
961
    tooLargeForContainerMsg = &tooLargeForRearMsg
962
963
    /* customize the verification messages */
964
    cannotPutInSelfMsg = &cannotPutBehindSelfMsg
965
    alreadyPutInMsg = &alreadyPutBehindMsg
966
967
    /* our implied containment verb is PUT BEHIND */
968
    tryMovingObjInto(obj) { return tryImplicitAction(PutBehind, obj, self); }
969
970
    /* -------------------------------------------------------------------- */
971
    /*
972
     *   Handle the PUT UNDER command
973
     */
974
    iobjFor(PutBehind)
975
    {
976
        verify()  { verifyPutInInterior(); }
977
        check()
978
        {
979
            /* only allow it if PUT BEHIND commands are allowed */
980
            if (!allowPutBehind)
981
            {
982
                reportFailure(&cannotPutBehindMsg);
983
                exit;
984
            }
985
        }
986
        action()
987
        {
988
            /* move the direct object behind me */
989
            gDobj.moveInto(self);
990
            
991
            /* issue our default acknowledgment */
992
            defaultReport(&okayPutBehindMsg);
993
        }
994
    }
995
    
996
    /*
997
     *   Looking "behind" a surface simply shows the surface's contents. 
998
     */
999
    dobjFor(LookBehind)
1000
    {
1001
        verify() { }
1002
        action() { examineInterior(); }
1003
    }
1004
;
1005
1006
/*
1007
 *   A special kind of RearContainer that only accepts specific contents.
1008
 */
1009
class RestrictedRearContainer: RestrictedHolder, RearContainer
1010
    /* 
1011
     *   A message that explains why the direct object can't be put behind
1012
     *   this object.  In most cases, the rather generic default message
1013
     *   should be overridden to provide a specific reason that the dobj
1014
     *   can't be put behind me.  The rejected object is provided as a
1015
     *   parameter in case the message needs to vary by object, but we
1016
     *   ignore this and just use a single blanket failure message by
1017
     *   default.  
1018
     */
1019
    cannotPutBehindMsg(obj) { return &cannotPutBehindRestrictedMsg; }
1020
1021
    /* override PutBehind to enforce our contents restriction */
1022
    iobjFor(PutBehind) { check() { checkPutDobj(&cannotPutBehindMsg); } }
1023
;
1024
1025
/*
1026
 *   A "rear surface" is essentially the same as a "rear container," but
1027
 *   models the contents as being attached to the back of the object rather
1028
 *   than merely sitting behind it.
1029
 *   
1030
 *   The only practical difference between the "container" and the
1031
 *   "surface" is that moving a surface moves its contents along with it,
1032
 *   whereas moving a container abandons the contents, leaving them behind
1033
 *   where the container used to be.  
1034
 */
1035
class RearSurface: RearContainer
1036
    /*
1037
     *   We're a surface, not a space, so our contents stay attached when
1038
     *   we move.  
1039
     */
1040
    abandonLocation = nil
1041
;
1042
1043
/*
1044
 *   A restricted-contents RearSurface
1045
 */
1046
class RestrictedRearSurface: RestrictedHolder, RearSurface
1047
    /* explain the problem */
1048
    cannotPutBehindMsg(obj) { return &cannotPutBehindRestrictedMsg; }
1049
1050
    /* override PutBehind to enforce our contents restriction */
1051
    iobjFor(PutBehind) { check() { checkPutDobj(&cannotPutBehindMsg); } }
1052
;
1053
1054
/* ------------------------------------------------------------------------ */
1055
/*
1056
 *   A "stretchy container."  This is a simple container subclass whose
1057
 *   external bulk changes according to the bulks of the contents.  
1058
 */
1059
class StretchyContainer: Container
1060
    /* 
1061
     *   Our minimum bulk.  This is the minimum bulk we'll report, even
1062
     *   when the aggregate bulks of our contents are below this limit. 
1063
     */
1064
    minBulk = 0
1065
1066
    /* get my total external bulk */
1067
    getBulk()
1068
    {
1069
        local tot;
1070
1071
        /* start with my own intrinsic bulk */
1072
        tot = bulk;
1073
1074
        /* add the bulk contribution from my contents */
1075
        tot += getBulkForContents();
1076
1077
        /* return the total, but never less than the minimum */
1078
        return tot >= minBulk ? tot : minBulk;
1079
    }
1080
1081
    /*
1082
     *   Calculate the contribution to my external bulk of my contents.
1083
     *   The default for a stretchy container is to conform exactly to the
1084
     *   contents, as though the container weren't present at all, hence
1085
     *   we simply sum the bulks of our contents.  Subclasses can override
1086
     *   this to define other aggregate bulk effects as needed.  
1087
     */
1088
    getBulkForContents()
1089
    {
1090
        local tot;
1091
        
1092
        /* sum the bulks of the items in our contents */
1093
        tot = 0;
1094
        foreach (local cur in contents)
1095
            tot += cur.getBulk();
1096
1097
        /* return the total */
1098
        return tot;
1099
    }
1100
1101
    /*
1102
     *   Check what happens when a new object is inserted into my
1103
     *   contents.  This is called with the new object already tentatively
1104
     *   added to my contents, so we can examine our current status to see
1105
     *   if everything works.
1106
     *   
1107
     *   Since we can change our own size when a new item is added to our
1108
     *   contents, we'll trigger a full bulk change check. 
1109
     */
1110
    checkBulkInserted(insertedObj)
1111
    {
1112
        /* 
1113
         *   inherit the normal handling to ensure that the new object
1114
         *   fits within this container 
1115
         */
1116
        inherited(insertedObj);
1117
1118
        /* 
1119
         *   since we can change our own shape when items are added to our
1120
         *   contents, trigger a full bulk check on myself 
1121
         */
1122
        checkBulkChange();
1123
    }
1124
1125
    /* 
1126
     *   Check a bulk change of one of my direct contents.  Since my own
1127
     *   bulk changes whenever the bulk of one of my contents changes, we
1128
     *   must propagate the bulk change of our contents as a change in our
1129
     *   own bulk. 
1130
     */
1131
    checkBulkChangeWithin(changingObj)
1132
    {
1133
        /* 
1134
         *   This might cause a change in my own bulk, since my bulk
1135
         *   depends on the bulks of my contents.  When this is called,
1136
         *   obj is already set to indicate its new bulk; since we
1137
         *   calculate our own bulk by looking at our contents' bulks,
1138
         *   this means that our own getBulk will now report the latest
1139
         *   value including obj's new bulk. 
1140
         */
1141
        checkBulkChange();
1142
    }
1143
;
1144
1145
1146
/* ------------------------------------------------------------------------ */
1147
/*
1148
 *   "Bag of Holding."  This is a mix-in that actively moves items from the
1149
 *   holding actor's direct inventory into itself when the actor's hands
1150
 *   are too full.
1151
 *   
1152
 *   The bag of holding offers a solution to the conflict between "realism"
1153
 *   and playability.  On the one hand, in real life, you can only hold so
1154
 *   many items at once, so at first glance it seems a simulation ought to
1155
 *   have such a limit in order to be more realistic.  On the other hand,
1156
 *   most players justifiably hate having to deal with a carrying limit,
1157
 *   because it forces the player to spend a lot of time doing tedious
1158
 *   inventory management.
1159
 *   
1160
 *   The Bag of Holding is a compromise solution.  The concept is borrowed
1161
 *   from live role-playing games, where it's usually a magical item that
1162
 *   can hold objects of unlimited size and weight, thereby allowing
1163
 *   characters to transport impossibly large objects.  In text IF, a bag
1164
 *   of holding isn't usually magical - it's usually just something like a
1165
 *   large backpack, or a trenchcoat with lots of pockets.  And it usually
1166
 *   isn't meant as a solution to an obvious puzzle; rather, it's meant to
1167
 *   invisibly prevent inventory management from becoming a puzzle in the
1168
 *   first place, by shuffling objects out of the PC's hands automatically
1169
 *   to free up space as needed.
1170
 *   
1171
 *   This Bag of Holding implementation works by automatically moving
1172
 *   objects from an actor's hands into the bag object, whenever the actor
1173
 *   needs space to pick up a new item.  Whenever an action has a
1174
 *   "roomToHoldObj" precondition, the precondition will automatically look
1175
 *   for a BagOfHolding object within the actor's inventory, and then move
1176
 *   as many items as necessary from the actor's hands to the bag.  
1177
 */
1178
class BagOfHolding: object
1179
    /*
1180
     *   Get my bags of holding.  Since we are a bag of holding, we'll add
1181
     *   ourselves to the vector, then we'll inherit the normal handling
1182
     *   to pick up our contents. 
1183
     */
1184
    getBagsOfHolding(vec)
1185
    {
1186
        /* we're a bag of holding */
1187
        vec.append(self);
1188
1189
        /* inherit the normal handling */
1190
        inherited(vec);
1191
    }
1192
1193
    /*
1194
     *   Get my "affinity" for the given object.  This is an indication of
1195
     *   how strongly this bag wants to contain the object.  The affinity
1196
     *   is a number in arbitrary units; higher numbers indicate stronger
1197
     *   affinities.  An affinity of zero means that the bag does not want
1198
     *   to contain the object at all.
1199
     *   
1200
     *   The purpose of the affinity is to support specialized holders
1201
     *   that are designed to hold only specific types of objects, and
1202
     *   allow these specialized holders to implicitly gather their
1203
     *   specific objects.  For example, a key ring might only hold keys,
1204
     *   so it would have a high affinity for keys and a zero affinity for
1205
     *   everything else.  A lunchbox might have a higher affinity for
1206
     *   things like sandwiches than for anything else, but might be
1207
     *   willing to serve as a general container for other small items as
1208
     *   well.
1209
     *   
1210
     *   The units of affinity are arbitrary, but the library uses the
1211
     *   following values for its own classes:
1212
     *   
1213
     *   0 - no affinity at all; the bag cannot hold the object
1214
     *   
1215
     *   50 - willing to hold the object, but not of the preferred type
1216
     *   
1217
     *   100 - default affinity; willing and able to hold the object, but
1218
     *   just as willing to hold most other things
1219
     *   
1220
     *   200 - special affinity; this object is of a type that we
1221
     *   especially want to hold
1222
     *   
1223
     *   We intentionally space these loosely so that games can use
1224
     *   intermediate levels if desired.
1225
     *   
1226
     *   When we are looking for bags of holding to consolidate an actor's
1227
     *   directly-held inventory, note that we always move the object with
1228
     *   the highest bag-to-object affinity out of all of the objects
1229
     *   under consideration.  So, if you want to give a particular kind
1230
     *   of bag priority so that the library uses that bag before any
1231
     *   other bag, make this routine return a higher affinity for the
1232
     *   bag's objects than any other bags do.
1233
     *   
1234
     *   By default, we'll return the default affinity of 100.
1235
     *   Specialized bags that don't hold all types of objects must
1236
     *   override this to return zero for objects they can't hold.  
1237
     */
1238
    affinityFor(obj)
1239
    {
1240
        /* 
1241
         *   my affinity for myself is zero, for obvious reasons; for
1242
         *   everything else, use the default affinity 
1243
         */
1244
        return (obj == self ? 0 : 100);
1245
    }
1246
;
1247
1248
1249
/* ------------------------------------------------------------------------ */
1250
/*
1251
 *   Keyring - a place to stash keys
1252
 *   
1253
 *   Keyrings have some special properties:
1254
 *   
1255
 *   - A keyring is a bag of holding with special affinity for keys.
1256
 *   
1257
 *   - A keyring can only contain keys.
1258
 *   
1259
 *   - Keys are considered to be on the outside of the ring, so a key can
1260
 *   be used even if attached to the keyring (in other words, if the ring
1261
 *   itself is held, a key attached to the ring is also considered held).
1262
 *   
1263
 *   - If an actor in possession of a keyring executes an "unlock" command
1264
 *   without specifying what key to use, we will automatically test each
1265
 *   key on the ring to find the one that works.
1266
 *   
1267
 *   - When an actor takes one of our keys, and the actor is in possession
1268
 *   of this keyring, we'll automatically attach the key to the keyring
1269
 *   immediately.  
1270
 */
1271
class Keyring: BagOfHolding, Thing
1272
    /* lister for showing our contents in-line as part of a list entry */
1273
    inlineContentsLister = keyringInlineContentsLister
1274
1275
    /* lister for showing our contents as part of "examine" */
1276
    descContentsLister = keyringExamineContentsLister
1277
1278
    /* 
1279
     *   Determine if a key fits our keyring.  By default, we will accept
1280
     *   any object of class Key.  However, subclasses might want to
1281
     *   override this to associate particular keys with particular
1282
     *   keyrings rather than having a single generic keyring.  To allow
1283
     *   only particular keys onto this keyring, override this routine to
1284
     *   return true only for the desired keys.  
1285
     */
1286
    isMyKey(key)
1287
    {
1288
        /* accept any object of class Key */
1289
        return key.ofKind(Key);
1290
    }
1291
1292
    /* we have high affinity for our keys */
1293
    affinityFor(obj)
1294
    {
1295
        /* 
1296
         *   if the object is one of my keys, we have high affinity;
1297
         *   otherwise we don't accept it at all 
1298
         */
1299
        if (isMyKey(obj))
1300
            return 200;
1301
        else
1302
            return 0;
1303
    }
1304
1305
    /* implicitly put a key on the keyring */
1306
    tryPuttingObjInBag(target)
1307
    {
1308
        /* we're a container, so use "put in" to get the object */
1309
        return tryImplicitActionMsg(&announceMoveToBag, PutOn, target, self);
1310
    }
1311
1312
    /* on taking the keyring, attach any loose keys */
1313
    dobjFor(Take)
1314
    {
1315
        action()
1316
        {
1317
            /* do the normal work */
1318
            inherited();
1319
            
1320
            /* get the list of loose keys */
1321
            local lst = getLooseKeys(gActor);
1322
1323
            /* consider only the subset that are my valid keys */
1324
            lst = lst.subset({x: isMyKey(x)});
1325
1326
            /* if there are any, move them onto the keyring */
1327
            if (lst.length() != 0)
1328
            {
1329
                /* put each loose key on the keyring */
1330
                foreach (local cur in lst)
1331
                    cur.moveInto(self);
1332
1333
                /* announce what happened */
1334
                extraReport(&movedKeysToKeyringMsg, self, lst);
1335
            }
1336
        }
1337
    }
1338
1339
    /* 
1340
     *   Get the loose keys in the given actor's possession.  On taking the
1341
     *   keyring, we'll attach these loose keys to the keyring
1342
     *   automatically.  By default, we return any keys the actor is
1343
     *   directly holding. 
1344
     */
1345
    getLooseKeys(actor) { return actor.contents; }
1346
1347
    /* allow putting a key on the keyring */
1348
    iobjFor(PutOn)
1349
    {
1350
        /* we can only put keys on keyrings */
1351
        verify()
1352
        {
1353
            /* we'll only allow our own keys to be attached */
1354
            if (gDobj == nil)
1355
            {
1356
                /* 
1357
                 *   we don't know the actual direct object yet, but we
1358
                 *   can at least check to see if any of the possible
1359
                 *   dobj's is my kind of key 
1360
                 */
1361
                if (gTentativeDobj.indexWhich({x: isMyKey(x.obj_)}) == nil)
1362
                    illogical(&objNotForKeyringMsg);
1363
            }
1364
            else if (!isMyKey(gDobj))
1365
            {
1366
                /* the dobj isn't a valid key for this keyring */
1367
                illogical(&objNotForKeyringMsg);
1368
            }
1369
        }
1370
        
1371
        /* put a key on me */
1372
        action()
1373
        {
1374
            /* move the key into me */
1375
            gDobj.moveInto(self);
1376
            
1377
            /* show the default "put on" response */
1378
            defaultReport(&okayPutOnMsg);
1379
        }
1380
    }
1381
1382
    /* treat "attach x to keyring" as "put x on keyring" */
1383
    iobjFor(AttachTo) remapTo(PutOn, DirectObject, self)
1384
1385
    /* treat "detach x from keyring" as "take x from keyring" */
1386
    iobjFor(DetachFrom) remapTo(TakeFrom, DirectObject, self)
1387
1388
    /* receive notification before an action */
1389
    beforeAction()
1390
    {
1391
        /*
1392
         *   Note whether or not we want to consider moving the direct
1393
         *   object to the keyring after a "take" command.  We will
1394
         *   consider doing so only if the direct object isn't already on
1395
         *   the keyring - if it is, we don't want to move it back right
1396
         *   after removing it, obviously.
1397
         *   
1398
         *   Skip the implicit keyring attachment if the current command
1399
         *   is implicit, because they must be doing something that
1400
         *   requires holding the object, in which case taking it is
1401
         *   incidental.  It could be actively annoying to attach the
1402
         *   object to the keyring in such cases - for example, if the
1403
         *   command is "put key on keyring," attaching it as part of the
1404
         *   implicit action would render the explicit command redundant
1405
         *   and cause it to fail.  
1406
         */
1407
        moveAfterTake = (!gAction.isImplicit
1408
                         && gDobj != nil
1409
                         && !gDobj.isDirectlyIn(self));
1410
    }
1411
1412
    /* flag: consider moving to keyring after this "take" action */
1413
    moveAfterTake = nil
1414
1415
    /* receive notification after an action */
1416
    afterAction()
1417
    {
1418
        /*
1419
         *   If the command was "take", and the direct object was a key,
1420
         *   and the actor involved is holding the keyring and can touch
1421
         *   it, and the command succeeded in moving the key to the
1422
         *   actor's direct inventory, then move the key onto the keyring.
1423
         *   Only consider this if we decided to during the "before"
1424
         *   notification.  
1425
         */
1426
        if (moveAfterTake
1427
            && gActionIs(Take)
1428
            && isMyKey(gDobj)
1429
            && isIn(gActor)
1430
            && gActor.canTouch(self)
1431
            && gDobj.isDirectlyIn(gActor))
1432
        {
1433
            /* move the key to me */
1434
            gDobj.moveInto(self);
1435
1436
            /* 
1437
             *   Mention what we did.  If the only report for this action
1438
             *   so far is the default 'take' response, then use the
1439
             *   combined taken-and-attached message.  Otherwise, append
1440
             *   our 'attached' message, which is suitable to use after
1441
             *   other messages. 
1442
             */
1443
            if (gTranscript.currentActionHasReport(
1444
                {x: (x.ofKind(CommandReportMessage)
1445
                     && x.messageProp_ != &okayTakeMsg)}))
1446
            {
1447
                /* 
1448
                 *   we have a non-default message already, so add our
1449
                 *   message indicating that we added the key to the
1450
                 *   keyring 
1451
                 */
1452
                reportAfter(&movedKeyToKeyringMsg, self);
1453
            }
1454
            else
1455
            {
1456
                /* use the combination taken-and-attached message */
1457
                mainReport(&takenAndMovedToKeyringMsg, self);
1458
            }
1459
        }
1460
    }
1461
1462
    /* find among our keys a key that works the direct object */
1463
    findWorkingKey(lock)
1464
    {
1465
        /* try each key on the keyring */
1466
        foreach (local key in contents)
1467
        {
1468
            /* 
1469
             *   if this is the key that unlocks the lock, replace the
1470
             *   command with 'unlock lock with key' 
1471
             */
1472
            if (lock.keyFitsLock(key))
1473
            {
1474
                /* note that we tried keys and found the right one */
1475
                extraReport(&foundKeyOnKeyringMsg, self, key);
1476
                
1477
                /* return the key */
1478
                return key;
1479
            }
1480
        }
1481
1482
        /* we didn't find the right key - indicate failure */
1483
        reportFailure(&foundNoKeyOnKeyringMsg, self);
1484
        return nil;
1485
    }
1486
1487
    /*
1488
     *   Append my directly-held contents to a vector when I'm directly
1489
     *   held.  We consider all of the keys on the keyring to be
1490
     *   effectively at the same containment level as the keyring, so if
1491
     *   the keyring is held, so are its attached keys.  
1492
     */
1493
    appendHeldContents(vec)
1494
    {
1495
        /* append all of our contents, since they're held when we are */
1496
        vec.appendUnique(contents);
1497
    }
1498
1499
    /*
1500
     *   Announce myself as a default object for an action.
1501
     *   
1502
     *   Do not announce a keyring as a default for "lock with" or "unlock
1503
     *   with".  Although we can use a keyring as the indirect object of a
1504
     *   lock/unlock command, we don't actually do the unlocking with the
1505
     *   keyring; so, when we're chosen as the default, suppress the
1506
     *   announcement, since it would imply that we're being used to lock
1507
     *   or unlock something.  
1508
     */
1509
    announceDefaultObject(whichObj, action, resolvedAllObjects)
1510
    {
1511
        /* if it's not a lock-with or unlock-with, use the default message */
1512
        if (!action.ofKind(LockWithAction)
1513
            && !action.ofKind(UnlockWithAction))
1514
        {
1515
            /* for anything but our special cases, use the default handling */
1516
            return inherited(whichObj, action, resolvedAllObjects);
1517
        }
1518
1519
        /* use no announcement */
1520
        return '';
1521
    }
1522
    
1523
    /* 
1524
     *   Allow locking or unlocking an object with a keyring.  This will
1525
     *   automatically try each key on the keyring to see if it fits the
1526
     *   lock. 
1527
     */
1528
    iobjFor(LockWith)
1529
    {
1530
        verify()
1531
        {
1532
            /* if we don't have any keys, we're not locking anything */
1533
            if (contents.length() == 0)
1534
                illogical(&cannotLockWithMsg);
1535
1536
            /* 
1537
             *   if we know the direct object, and we don't have any keys
1538
             *   that are plausible for the direct object, we're an
1539
             *   unlikely match 
1540
             */
1541
            if (gDobj != nil)
1542
            {
1543
                local foundPlausibleKey;
1544
                
1545
                /* 
1546
                 *   try each of my keys to see if it's plausible for the
1547
                 *   direct object 
1548
                 */
1549
                foundPlausibleKey = nil;
1550
                foreach (local cur in contents)
1551
                {
1552
                    /* 
1553
                     *   if this is a plausible key, note that we have at
1554
                     *   least one plausible key 
1555
                     */
1556
                    if (gDobj.keyIsPlausible(cur))
1557
                    {
1558
                        /* note that we found a plausible key */
1559
                        foundPlausibleKey = true;
1560
1561
                        /* no need to look any further - one is good enough */
1562
                        break;
1563
                    }
1564
                }
1565
1566
                /* 
1567
                 *   If we didn't find a plausible key, we're an unlikely
1568
                 *   match.
1569
                 *   
1570
                 *   If we did find a plausible key, increase the
1571
                 *   likelihood that this is the indirect object so that
1572
                 *   it's greater than the likelihood for any random key
1573
                 *   that's plausible for the lock (which has the default
1574
                 *   likelihood of 100), but less than the likelihood of
1575
                 *   the known good key (which is 150).  This will cause a
1576
                 *   keyring to be taken as a default over any ordinary
1577
                 *   key, but will cause the correct key to override the
1578
                 *   keyring as the default if the correct key is known to
1579
                 *   the player already.  
1580
                 */
1581
                if (foundPlausibleKey)
1582
                    logicalRank(140, 'keyring with plausible key');
1583
                else
1584
                    logicalRank(50, 'no plausible key');
1585
            }
1586
        }
1587
1588
        action()
1589
        {
1590
            local key;
1591
1592
            /* 
1593
             *   Try finding a working key.  If we find one, replace the
1594
             *   command with 'lock <lock> with <key>, so that we have the
1595
             *   full effect of the 'lock with' command using the key
1596
             *   itself. 
1597
             */
1598
            if ((key = findWorkingKey(gDobj)) != nil)
1599
                replaceAction(LockWith, gDobj, key);
1600
        }
1601
    }
1602
1603
    iobjFor(UnlockWith)
1604
    {
1605
        /* verify the same as for LockWith */
1606
        verify()
1607
        {
1608
            /* if we don't have any keys, we're not unlocking anything */
1609
            if (contents.length() == 0)
1610
                illogical(&cannotUnlockWithMsg);
1611
            else
1612
                verifyIobjLockWith();
1613
        }
1614
1615
        action()
1616
        {
1617
            local key;
1618
1619
            /* 
1620
             *   if we can find a working key, run an 'unlock with' action
1621
             *   using the key 
1622
             */
1623
            if ((key = findWorkingKey(gDobj)) != nil)
1624
                replaceAction(UnlockWith, gDobj, key);
1625
        }
1626
    }
1627
;
1628
1629
/*
1630
 *   Key - this is an object that can be used to unlock things, and which
1631
 *   can be stored on a keyring.  The key that unlocks a lock is
1632
 *   identified with a property on the lock, not on the key.
1633
 */
1634
class Key: Thing
1635
    /*
1636
     *   A key on a keyring that is being held by an actor is considered
1637
     *   to be held by the actor, since the key does not have to be
1638
     *   removed from the keyring in order to be manipulated as though it
1639
     *   were directly held.  
1640
     */
1641
    isHeldBy(actor)
1642
    {
1643
        /* 
1644
         *   if I'm on a keyring, I'm being held if the keyring is being
1645
         *   held; otherwise, use the default definition 
1646
         */
1647
        if (location != nil && location.ofKind(Keyring))
1648
            return location.isHeldBy(actor);
1649
        else
1650
            return inherited(actor);
1651
    }
1652
1653
    /*
1654
     *   Try making the current command's actor hold me.  If we're on a
1655
     *   keyring, we'll simply try to make the keyring itself held, rather
1656
     *   than taking the key off the keyring; otherwise, we'll inherit the
1657
     *   default behavior to make ourselves held.  
1658
     */
1659
    tryHolding()
1660
    {
1661
        if (location != nil && location.ofKind(Keyring))
1662
            return location.tryHolding();
1663
        else
1664
            return inherited();
1665
    }
1666
1667
    /* -------------------------------------------------------------------- */
1668
    /*
1669
     *   Action processing 
1670
     */
1671
1672
    /* treat "detach key" as "take key" if it's on a keyring */
1673
    dobjFor(Detach)
1674
    {
1675
        verify()
1676
        {
1677
            /* if I'm not on a keyring, there's nothing to detach from */
1678
            if (location == nil || !location.ofKind(Keyring))
1679
                illogical(&keyNotDetachableMsg);
1680
        }
1681
        remap()
1682
        {
1683
            /* if I'm on a keyring, remap to "take self" */
1684
            if (location != nil && location.ofKind(Keyring))
1685
                return [TakeAction, self];
1686
            else
1687
                return inherited();
1688
        }
1689
    }
1690
1691
    /* "lock with" */
1692
    iobjFor(LockWith)
1693
    {
1694
        verify()
1695
        {
1696
            /* 
1697
             *   if we know the direct object is a LockableWithKey, we can
1698
             *   perform some additional checks on the likelihood of this
1699
             *   key being the intended key for the lock 
1700
             */
1701
            if (gDobj != nil
1702
                && gDobj.ofKind(LockableWithKey))
1703
            {
1704
                /*
1705
                 *   If the player should know that we're the key for the
1706
                 *   lock, boost our likelihood so that we'll be picked
1707
                 *   out automatically from an ambiguous set of keys.  
1708
                 */
1709
                if (gDobj.isKeyKnown(self))
1710
                    logicalRank(150, 'known key');
1711
1712
                /* 
1713
                 *   if this isn't a plausible key for the lockable, it's
1714
                 *   unlikely that this is a match 
1715
                 */
1716
                if (!gDobj.keyIsPlausible(self))
1717
                    illogical(keyNotPlausibleMsg);
1718
            }
1719
        }
1720
    }
1721
1722
    /* 
1723
     *   the message to use when the key is obviously not plausible for a
1724
     *   given lock 
1725
     */
1726
    keyNotPlausibleMsg = &keyDoesNotFitLockMsg
1727
1728
    /* "unlock with" */
1729
    iobjFor(UnlockWith)
1730
    {
1731
        verify()
1732
        {
1733
            /* use the same key selection we use for "lock with" */
1734
            verifyIobjLockWith();
1735
        }
1736
    }
1737
;
1738
1739
/* ------------------------------------------------------------------------ */
1740
/*
1741
 *   A Dispenser is a container for a special type of item, such as a book
1742
 *   of matches or a box of candy.
1743
 */
1744
class Dispenser: Container
1745
    /* 
1746
     *   Can we return one of our items to the dispenser once the item is
1747
     *   dispensed?  Books of matches wouldn't generally allow this, since
1748
     *   a match must be torn out to be removed, but simple box dispensers
1749
     *   probably would.  By default, we won't allow returning an item
1750
     *   once dispensed.  
1751
     */
1752
    canReturnItem = nil
1753
1754
    /* 
1755
     *   Is the item one of the types of items we dispense?  Normally, we
1756
     *   dispense identical items, so our default implementation simply
1757
     *   determines if the item is an instance of our dispensable class.
1758
     *   If the dispenser can hand out items of multiple, unrelated
1759
     *   classes, this can be overridden to use a different means of
1760
     *   identifying the dispensed items.  
1761
     */
1762
    isMyItem(obj) { return obj.ofKind(myItemClass); }
1763
1764
    /*
1765
     *   The class of items we dispense.  This is used by the default
1766
     *   implementation of isMyItem(), so subclasses that inherit that
1767
     *   implementation should provide the appropriate base class here.  
1768
     */
1769
    myItemClass = Dispensable
1770
1771
    /* "put in" indirect object handler */
1772
    iobjFor(PutIn)
1773
    {
1774
        verify()
1775
        {
1776
            /* if we know the direct object, consider it further */
1777
            if (gDobj != nil)
1778
            {
1779
                /* if we don't allow returning our items, don't allow it */
1780
                if (!canReturnItem && isMyItem(gDobj))
1781
                    illogical(&cannotReturnToDispenserMsg);
1782
1783
                /* if it's not my dispensed item, it can't go in here */
1784
                if (!isMyItem(gDobj))
1785
                    illogical(&cannotPutInDispenserMsg);
1786
            }
1787
1788
            /* inherit default handling */
1789
            inherited();
1790
        }
1791
    }
1792
;
1793
1794
/*
1795
 *   A Dispensable is an item that comes from a Dispenser.  This is in
1796
 *   most respects an ordinary item; the only special thing about it is
1797
 *   that if we're still in our dispenser, we're an unlikely match for any
1798
 *   command except "take" and the like.  
1799
 */
1800
class Dispensable: Thing
1801
    /* 
1802
     *   My dispenser.  This is usually my initial location, so by default
1803
     *   we'll pre-initialize this to our location. 
1804
     */
1805
    myDispenser = nil
1806
1807
    /* pre-initialization */
1808
    initializeThing()
1809
    {
1810
        /* inherit the default initialization */
1811
        inherited();
1812
1813
        /* 
1814
         *   We're usually in our dispenser initially, so assume that our
1815
         *   dispenser is simply our initial location.  If myDispenser is
1816
         *   overridden in a subclass, don't overwrite the inherited
1817
         *   value.  
1818
         */
1819
        if (propType(&myDispenser) == TypeNil)
1820
            myDispenser = location;
1821
    }
1822
1823
    dobjFor(All)
1824
    {
1825
        verify()
1826
        {
1827
            /* 
1828
             *   If we're in our dispenser, and the command isn't "take"
1829
             *   or "take from", reduce our disambiguation likelihood -
1830
             *   it's more likely that the actor is referring to another
1831
             *   equivalent item that they've already removed from the
1832
             *   dispenser.  
1833
             */
1834
            if (isIn(myDispenser)
1835
                && !gActionIs(Take) && !gActionIs(TakeFrom))
1836
            {
1837
                /* we're in our dispenser - reduce the likelihood */
1838
                logicalRank(60, 'in dispenser');
1839
            }
1840
        }
1841
    }
1842
;
1843
1844
1845
/* ------------------------------------------------------------------------ */
1846
/*
1847
 *   A Matchbook is a special dispenser for matches. 
1848
 */
1849
class Matchbook: Collective, Openable, Dispenser
1850
    /* we cannot return a match to a matchbook */
1851
    canReturnItem = nil
1852
1853
    /* 
1854
     *   we dispense matches (subclasses can override this if they want to
1855
     *   dispense a specialized match subclass) 
1856
     */
1857
    myItemClass = Matchstick
1858
1859
    /*
1860
     *   Act as a collective for any items within me.  This will have no
1861
     *   effect unless we also have a plural name that matches that of the
1862
     *   contained items.
1863
     *   
1864
     *   It is usually desirable for a matchbook to act as a collective
1865
     *   for the contained items, so that a command like "take matches"
1866
     *   will be taken to apply to the matchbook rather than the
1867
     *   individual matches.  
1868
     */
1869
    isCollectiveFor(obj) { return obj.isIn(self); }
1870
1871
    /*
1872
     *   Append my directly-held contents to a vector when I'm directly
1873
     *   held.  When the matchbook is open, append our matches, because we
1874
     *   consider the matches to be effectively attached to the matchbook
1875
     *   (rather than contained within it).  
1876
     */
1877
    appendHeldContents(vec)
1878
    {
1879
        /* if we're open, append our contents */
1880
        if (isOpen)
1881
            vec.appendUnique(contents);
1882
    }
1883
;
1884
1885
/*
1886
 *   A FireSource is an object that can set another object on fire.  This
1887
 *   is a mix-in class that can be used with other classes.  
1888
 */
1889
class FireSource: object
1890
    /* 
1891
     *   We can use a fire source to light another object, provided the
1892
     *   fire source is itself burning.  We don't provide any action
1893
     *   handling - we leave that to the direct object.  
1894
     */
1895
    iobjFor(BurnWith)
1896
    {
1897
        preCond = [objHeld, objBurning]
1898
        verify()
1899
        {
1900
            /* don't allow using me to light myself */
1901
            if (gDobj == self)
1902
                illogicalNow(&cannotBurnDobjWithMsg);
1903
1904
            /* 
1905
             *   If we're already lit, make this an especially good choice
1906
             *   for lighting other objects - this will ensure that we
1907
             *   choose this over a match that isn't already lit, which is
1908
             *   what you'd normally want to do to avoid wasting a match.
1909
             *   
1910
             *   Note that our ranking is specifically coordinated with
1911
             *   that used by Matchstick.  We'll use a lit match over any
1912
             *   normal FireSource (rank 160); we'll use a lit FireSource
1913
             *   (rank 150) over an unlit match (rank 140).
1914
             *   
1915
             *   If we're not lit, make the action non-obvious so that
1916
             *   we're not taken as a default to light another object on
1917
             *   fire.  We *could* light something once we're lit, but that
1918
             *   presumes there's a way to light me in the first place,
1919
             *   which might require yet another object (a match, for
1920
             *   example) - so ignore me as a default if we're not already
1921
             *   lit, and go directly to some other object.  This should be
1922
             *   overridden for self-lighting objects such as matches.  
1923
             */
1924
            if (isLit)
1925
                logicalRank(150, 'fire source');
1926
            else
1927
                nonObvious;
1928
        }
1929
    }
1930
;
1931
1932
/*
1933
 *   A Matchstick is a self-igniting match from a matchbook.  (We use this
1934
 *   lengthy name rather than simply "Match" because the latter is too
1935
 *   generic, and could be taken by a casual reader for an object
1936
 *   representing a successful search result or the like.)  
1937
 */
1938
class Matchstick: FireSource, LightSource
1939
    /* matches have fairly feeble light */
1940
    brightnessOn = 2
1941
1942
    /* not lit initially */
1943
    isLit = nil
1944
1945
    /* amount of time we burn, in turns */
1946
    burnLength = 2
1947
1948
    /* default long description describes burning status */
1949
    desc()
1950
    {
1951
        if (isLit)
1952
            gLibMessages.litMatchDesc(self);
1953
        else
1954
            gLibMessages.unlitMatchDesc(self);
1955
    }
1956
1957
    /* get our state */
1958
    getState = (isLit ? matchStateLit : matchStateUnlit)
1959
1960
    /* get a list of all states */
1961
    allStates = [matchStateLit, matchStateUnlit]
1962
    
1963
    /* "burn" action */
1964
    dobjFor(Burn)
1965
    {
1966
        preCond = [objHeld]
1967
        verify()
1968
        {
1969
            /* can't light a match that's already burning */
1970
            if (isLit)
1971
                illogicalAlready(&alreadyBurningMsg);
1972
        }
1973
        action()
1974
        {
1975
            local t;
1976
            
1977
            /* describe it */
1978
            defaultReport(&okayBurnMatchMsg);
1979
1980
            /* make myself lit */
1981
            makeLit(true);
1982
1983
            /* get our default burn length */
1984
            t = burnLength;
1985
1986
            /* 
1987
             *   if this is an implicit command, reduce the burn length by
1988
             *   one turn - this ensures that the player can't
1989
             *   artificially extend the match's useful life by doing
1990
             *   something that implicitly lights the match 
1991
             */
1992
            if (gAction.isImplicit)
1993
                --t;
1994
1995
            /* start our burn-out timer going */
1996
            new SenseFuse(self, &matchBurnedOut, t, self, sight);
1997
        }
1998
    }
1999
2000
    iobjFor(BurnWith)
2001
    {
2002
        verify()
2003
        {
2004
            /*
2005
             *   Whether or not a match is burning, it's an especially
2006
             *   good choice to light something else on fire.  Make it
2007
             *   even more likely when it's burning already.
2008
             *   
2009
             *   Note that this is specifically coordinated with the base
2010
             *   FireSource ranking.  We'll pick a lit match (160) over an
2011
             *   ordinary lit FireSource (150), but we'll pick a lit
2012
             *   FireSource (150) over an unlit match (140).  This will
2013
             *   avoid consuming a match that's not already lit when
2014
             *   another fire source is already available.  
2015
             */
2016
            logicalRank(isLit ? 160 : 140, 'fire source');
2017
        }
2018
    }
2019
2020
    /* "extinguish" */
2021
    dobjFor(Extinguish)
2022
    {
2023
        verify()
2024
        {
2025
            /* can't extinguish a match that isn't burning */
2026
            if (!isLit)
2027
                illogicalAlready(&matchNotLitMsg);
2028
        }
2029
        action()
2030
        {
2031
            /* describe the match going out */
2032
            defaultReport(&okayExtinguishMatchMsg);
2033
2034
            /* no longer lit */
2035
            makeLit(nil);
2036
2037
            /* remove the match from the game */
2038
            moveInto(nil);
2039
        }
2040
    }
2041
2042
    /* fuse handler for burning out */
2043
    matchBurnedOut()
2044
    {
2045
        /* 
2046
         *   if I'm not still burning, I must have been extinguished
2047
         *   explicitly already, so there's nothing to do 
2048
         */
2049
        if (!isLit)
2050
            return;
2051
        
2052
        /* make sure we separate any output from other commands */
2053
        "<.p>";
2054
2055
        /* report that we're done burning */
2056
        gLibMessages.matchBurnedOut(self);
2057
2058
        /* 
2059
         *   remove myself from the game (for simplicity, a match simply
2060
         *   disappears when it's done burning) 
2061
         */
2062
        moveInto(nil);
2063
    }
2064
2065
    /* matches usually come in bunches of equivalents */
2066
    isEquivalent = true
2067
;
2068
2069
/*
2070
 *   A light source that produces light using a fuel supply.  This kind of
2071
 *   light source uses a daemon to consume fuel whenever it's lit.
2072
 */
2073
class FueledLightSource: LightSource
2074
    /* provide a bright light by default */
2075
    brightnessOn = 3
2076
2077
    /* not lit initially */
2078
    isLit = nil
2079
2080
    /*
2081
     *   Our fuel source object.  If desired, this can be set to a
2082
     *   separate object to model the fuel supply separately from the
2083
     *   light source itself; for example, you could set this to point to
2084
     *   a battery, or to a vial of oil.  By default, for simplicity, the
2085
     *   fuel supply and light source are the same object.
2086
     *   
2087
     *   The fuel supply object must expose two methods: getFuelLevel()
2088
     *   and consumeFuel().  
2089
     */
2090
    fuelSource = (self)
2091
2092
    /* 
2093
     *   Get my fuel level, and consume fuel.  We use these methods only
2094
     *   when we're our own fuelSource (which we are by default).  When
2095
     *   we're not our own fuel source, the fuel source object must
2096
     *   provide these methods instead of us.
2097
     *   
2098
     *   Our fuel level is the number of turns that we can continue to
2099
     *   burn.  Each turn we're lit, we'll reduce the fuel level by one.
2100
     *   We'll automatically extinguish ourself when the fuel level
2101
     *   reaches zero.
2102
     *   
2103
     *   If the light source can burn forever, simply return nil as the
2104
     *   fuel level.  
2105
     */
2106
    getFuelLevel() { return fuelLevel; }
2107
    consumeFuel(amount) { fuelLevel -= amount; }
2108
2109
    /* our fuel level - we use this when we're our own fuel source */
2110
    fuelLevel = 20
2111
2112
    /* light or extinguish */
2113
    makeLit(lit)
2114
    {
2115
        /* if the current fuel level is zero, we can't be lit */
2116
        if (lit && fuelSource.getFuelLevel() == 0)
2117
            return;
2118
2119
        /* inherit the default handling */
2120
        inherited(lit);
2121
2122
        /* if we're lit, activate our daemon; otherwise, stop our daemon */
2123
        if (isLit)
2124
        {
2125
            /* start our burn daemon going */
2126
            burnDaemonObj =
2127
                new SenseDaemon(self, &burnDaemon, 1, self, sight);
2128
        }
2129
        else
2130
        {
2131
            /* stop our daemon */
2132
            eventManager.removeEvent(burnDaemonObj);
2133
2134
            /* forget out daemon */
2135
            burnDaemonObj = nil;
2136
        }
2137
    }
2138
2139
    /* burn daemon - this is called on each turn while we're burning */
2140
    burnDaemon()
2141
    {
2142
        local level = fuelSource.getFuelLevel();
2143
        
2144
        /* if we use fuel, consume one increment of fuel for this turn */
2145
        if (level != nil)
2146
        {
2147
            /* 
2148
             *   If our fuel level has reached zero, stop burning.  Note
2149
             *   that the daemon is called on the first turn after we
2150
             *   start burning, so we must go through a turn with the fuel
2151
             *   level at zero before we stop burning.  
2152
             */
2153
            if (level == 0)
2154
            {
2155
                /* make sure we separate any output from other commands */
2156
                "<.p>";
2157
2158
                /* mention that the candle goes out */
2159
                sayBurnedOut();
2160
2161
                /* 
2162
                 *   Extinguish the candle.  Note that we do this *after*
2163
                 *   we've already displayed the message about the candle
2164
                 *   burning out, because that message is displayed in our
2165
                 *   own sight context.  If we're the only light source
2166
                 *   present, then we're invisible once we're not providing
2167
                 *   light, so our message about burning out would be
2168
                 *   suppressed if we displayed it after cutting off our
2169
                 *   own light.  To make sure we can see the message, wait
2170
                 *   until after the message to cut off our light.  
2171
                 */
2172
                makeLit(nil);
2173
            }
2174
            else
2175
            {
2176
                /* reduce our fuel level by one */
2177
                fuelSource.consumeFuel(1);
2178
            }
2179
        }
2180
    }
2181
2182
    /* mention that we've just burned out */
2183
    sayBurnedOut() { gLibMessages.objBurnedOut(self); }
2184
2185
    /* our daemon object, valid while we're burning */
2186
    burnDaemonObj = nil
2187
;
2188
2189
/*
2190
 *   A candle is an item that can be set on fire for a controlled burn.
2191
 *   Although we call this a candle, this class can be used for other types
2192
 *   of fuel burners, such as torches and oil lanterns.
2193
 *   
2194
 *   Ordinary candles are usually fire sources as well, in that you can
2195
 *   light one candle with another once the first one is lit.  To get this
2196
 *   effect, mix FireSource into the superclass list (but put it before
2197
 *   Candle, since FireSource is specifically designed as a mix-in class).
2198
 */
2199
class Candle: FueledLightSource
2200
    /* 
2201
     *   The message we display when we try to light the candle and we're
2202
     *   out of fuel.  This message can be overridden by subclasses that
2203
     *   don't fit the default message.  
2204
     */
2205
    outOfFuelMsg = &candleOutOfFuelMsg
2206
2207
    /* the message we display when we successfully light the candle */
2208
    okayBurnMsg = &okayBurnCandleMsg
2209
2210
    /* show a message when the candle runs out fuel while burning */
2211
    sayBurnedOut()
2212
    {
2213
        /* by default, show our standard library message */
2214
        gLibMessages.candleBurnedOut(self);
2215
    }
2216
2217
    /* 
2218
     *   Determine if I can be lit with the specific indirect object.  By
2219
     *   default, we'll allow any object to light us if the object passes
2220
     *   the normal checks applied by its own iobjFor(BurnWith) handlers.
2221
     *   This can be overridden if we can only be lit with specific
2222
     *   sources of fire; for example, a furnace with a deeply-recessed
2223
     *   burner could refuse to be lit by anything but particular long
2224
     *   matches, or a particular type of fuel could refuse to be lit
2225
     *   except by certain especially hot flames.  
2226
     */
2227
    canLightWith(obj) { return true; }
2228
2229
    /* 
2230
     *   Default long description describes burning status.  In most
2231
     *   cases, this should be overridden to provide more details, such as
2232
     *   information on our fuel level. 
2233
     */
2234
    desc()
2235
    {
2236
        if (isLit)
2237
            gLibMessages.litCandleDesc(self);
2238
        else
2239
            inherited();
2240
    }
2241
2242
    /* "burn with" action */
2243
    dobjFor(BurnWith)
2244
    {
2245
        preCond = [touchObj]
2246
        verify()
2247
        {
2248
            /* can't light it if it's already lit */
2249
            if (isLit)
2250
                illogicalAlready(&alreadyBurningMsg);
2251
        }
2252
        check()
2253
        {
2254
            /* 
2255
             *   make sure the object being used to light us is a valid
2256
             *   source of fire for us 
2257
             */
2258
            if (!canLightWith(obj))
2259
            {
2260
                reportFailure(&cannotBurnDobjWithMsg);
2261
                exit;
2262
            }
2263
2264
            /* if the fuel level is zero, we can't be lit */
2265
            if (fuelSource.getFuelLevel() == 0)
2266
            {
2267
                reportFailure(outOfFuelMsg);
2268
                exit;
2269
            }
2270
        }
2271
        action()
2272
        {
2273
            /* make myself lit */
2274
            makeLit(true);
2275
2276
            /* describe it */
2277
            defaultReport(okayBurnMsg);
2278
        }
2279
    }
2280
2281
    /* "extinguish" */
2282
    dobjFor(Extinguish)
2283
    {
2284
        verify()
2285
        {
2286
            /* can't extinguish a match that isn't burning */
2287
            if (!isLit)
2288
                illogicalAlready(&candleNotLitMsg);
2289
        }
2290
        action()
2291
        {
2292
            /* describe the match going out */
2293
            defaultReport(&okayExtinguishCandleMsg);
2294
2295
            /* no longer lit */
2296
            makeLit(nil);
2297
        }
2298
    }
2299
;
2300
2301
/* ------------------------------------------------------------------------ */
2302
/*
2303
 *   "Tour Guide" is a mix-in class for Actors.  This class can be
2304
 *   multiply inherited by objects along with Actor or a subclass of
2305
 *   Actor.  This mix-in makes the Follow action, when applied to the tour
2306
 *   guide, initiate travel according to where the tour guide wants to go
2307
 *   next.  So, if the tour guide is here and is waving us through the
2308
 *   door, FOLLOW GUIDE will initiate travel through the door.
2309
 *   
2310
 *   This class should appear in the superclass list ahead of Actor or the
2311
 *   Actor subclass.  
2312
 */
2313
class TourGuide: object
2314
    dobjFor(Follow)
2315
    {
2316
        verify()
2317
        {
2318
            /*
2319
             *   If the actor can see us, and we're in a "guided tour"
2320
             *   state, we can definitely perform the travel.  Otherwise,
2321
             *   use the standard "follow" behavior. 
2322
             */
2323
            if (gActor.canSee(self) && getTourDest() != nil)
2324
            {
2325
                /* 
2326
                 *   we're waiting to show the actor to the next stop on
2327
                 *   the tour, so we can definitely proceed with this
2328
                 *   action 
2329
                 */
2330
            }
2331
            else
2332
            {
2333
                /* we're not in a tour state, so use the standard handling */
2334
                inherited();
2335
            }
2336
        }
2337
2338
        action()
2339
        {
2340
            local dest;
2341
            
2342
            /* 
2343
             *   if we're in a guided tour state, initiate travel to our
2344
             *   escort destination; otherwise, use the standard handling 
2345
             */
2346
            if (gActor.canSee(self) && (dest = getTourDest()) != nil)
2347
            {
2348
                /* initiate travel to our destination */
2349
                replaceAction(TravelVia, dest);
2350
                return;
2351
            }
2352
            else
2353
            {
2354
                /* no tour state; use the standard handling */
2355
                inherited();
2356
            }
2357
        }
2358
    }
2359
2360
    /*
2361
     *   Get the travel connector that takes us to our next guided tour
2362
     *   destination.  By default, this returns the escortDest from our
2363
     *   current actor state if our state is a guided tour state, or nil
2364
     *   if our state is any other kind of state.  Subclasses must
2365
     *   override this if they use other kinds of states to represent
2366
     *   guided tours, since we'll only detect that we're in a guided tour
2367
     *   state if our current actor state object is of class
2368
     *   GuidedTourState (or any subclass).  
2369
     */
2370
    getTourDest()
2371
    {
2372
        return (curState.ofKind(GuidedTourState)
2373
                ? curState.escortDest
2374
                : nil);
2375
    }
2376
;
2377
2378
/*
2379
 *   Guided Tour state.  This provides a simple way of defining a "guided
2380
 *   tour," which is a series of locations to which we try to guide the
2381
 *   player character.  We don't force the player character to travel as
2382
 *   specified; we merely try to lead the player.  The actual travel is up
2383
 *   to the player.
2384
 *   
2385
 *   Here's how this works.  For each location on the guided tour, create
2386
 *   one of these state objects.  Set escortDest to the travel connector
2387
 *   to which we're attempting to guide the player character from the
2388
 *   current location.  Set stateAfterEscort to the state object for the
2389
 *   next location on the tour.  Set stateDesc to something indicating
2390
 *   that we're trying to show the player to the next stop - something
2391
 *   along the lines of "Bob waits for you by the door."  Set
2392
 *   arrivingWithDesc to a message indicating that we just showed up in
2393
 *   the current location and are ready to show the player to the next -
2394
 *   "Bob goes to the door and waits for you to follow him."  
2395
 */
2396
class GuidedTourState: AccompanyingState
2397
    /* the travel connector we're trying to show the player into */
2398
    escortDest = nil
2399
2400
    /* 
2401
     *   The next state for our actor to assume after the travel.  This
2402
     *   should be overridden and set to the state object for the next
2403
     *   stop on the tour. 
2404
     */
2405
    stateAfterEscort = nil
2406
2407
    /* the actor we're escorting - this is usually the player character */
2408
    escortActor = (gPlayerChar)
2409
2410
    /* 
2411
     *   The class we use for our actor state during the escort travel.
2412
     *   By default, we use the basic guided-tour accompanying travel
2413
     *   state class, but games will probably want to use a customized
2414
     *   subclass of this basic class in most cases.  The main reason to
2415
     *   use a custom subclass is to provide customized messages to
2416
     *   describe the departure of the escorting actor.  
2417
     */
2418
    escortStateClass = GuidedInTravelState
2419
    
2420
    /* 
2421
     *   we should accompany the travel if the actor we're guiding will be
2422
     *   traveling, and they're traveling to the next stop on our tour 
2423
     */
2424
    accompanyTravel(traveler, conn)
2425
    {
2426
        return (traveler.isActorTraveling(escortActor) && conn == escortDest);
2427
    }
2428
2429
    /* 
2430
     *   get our accompanying state object - we'll create an instance of
2431
     *   the class specified in our escortStateClass property 
2432
     */
2433
    getAccompanyingTravelState(traveler, conn)
2434
    {
2435
        return escortStateClass.createInstance(
2436
            location, gActor, stateAfterEscort);
2437
    }
2438
;
2439
2440
/*
2441
 *   A subclass of the basic accompanying travel state specifically
2442
 *   designed for guided tours.  This is almost the same as the basic
2443
 *   accompanying travel state, but provides customized messages to
2444
 *   describe the departure of our associated actor, which is the actor
2445
 *   serving as the tour guide.  
2446
 */
2447
class GuidedInTravelState: AccompanyingInTravelState
2448
    sayDeparting(conn)
2449
        { gLibMessages.sayDepartingWithGuide(location, leadActor); }
2450
;
2451
2452
/* ------------------------------------------------------------------------ */
2453
/*
2454
 *   An Attachable is an object that can be attached to another, using an
2455
 *   ATTACH X TO Y command.  This is a mix-in class that is meant to be
2456
 *   combined with a Thing-derived class to create an attachable object.
2457
 *   
2458
 *   Attachment is symmetrical: we can only attach to other Attachable
2459
 *   objects.  As a result, the verb handling for ATTACH can be performed
2460
 *   symmetrically - ATTACH X TO Y is handled the same way as ATTACH Y TO
2461
 *   X.  Sometimes reversing the roles makes the command nonsensical, but
2462
 *   when the reversal makes sense, it seems unlikely that it'll ever
2463
 *   change the meaning of the command.  This makes it program the verb
2464
 *   handling, because it means that we can designate one of X or Y as the
2465
 *   handler for the verb, and just write the code once there.  Refer to
2466
 *   the handleAttach() method to see how this works.
2467
 *   
2468
 *   There's an important detail that we leave to instances, because
2469
 *   there's no good general rule we can implement.  Specifically, there's
2470
 *   the matter of imposing appropriate constraints on the relative
2471
 *   locations of objects once they're attached to one another.  There are
2472
 *   numerous anomalies that become possible once two objects are attached.
2473
 *   Consider the example of a battery connected to a jumper cable that's
2474
 *   in turn connected to a lamp:
2475
 *   
2476
 *   - if we put the battery in a box but leave the lamp outside the box,
2477
 *   we shouldn't be able to close the lid of the box all the way without
2478
 *   breaking the cables
2479
 *   
2480
 *   - if we're carrying the battery but not the lamp, traveling to a new
2481
 *   room should drag the lamp along
2482
 *   
2483
 *   - if we drop the battery down a well, the lamp should be dragged down
2484
 *   with it
2485
 *   
2486
 *   Our world model isn't sophisticated enough to properly model an
2487
 *   attachment relationship, so it can't deal with these contingencies by
2488
 *   proper physical simulation.  Which is why we have to leave these for
2489
 *   the game to handle.
2490
 *   
2491
 *   There are two main strategies you can apply to handle these problems.
2492
 *   
2493
 *   First, you can impose limits that prevent these sorts of situations
2494
 *   from coming up in the first place, either by carefully designing the
2495
 *   scenario so they simply don't come up, or by imposing more or less
2496
 *   artificial constraints.  For example, you could solve all of the
2497
 *   problems above by eliminating the jumper cable and attaching the lamp
2498
 *   directly to the battery, or by making the jumper cable very short.
2499
 *   Anything attached to the battery would effectively become located "in"
2500
 *   the battery, so it would move everywhere along with the battery
2501
 *   automatically.  Detaching the lamp would move the lamp back outside
2502
 *   the battery, and conversely, moving the lamp out of the battery would
2503
 *   detach the objects.
2504
 *   
2505
 *   Second, you can detect the anomalous cases and handle them explicitly
2506
 *   with special-purpose code.  You could use beforeAction and afterAction
2507
 *   methods on one of the attached objects, for example, to detect the
2508
 *   various problematic actions, either blocking them or implementing
2509
 *   appropriate consequences.
2510
 *   
2511
 *   Given the number of difficult anomalies possible with rope-like
2512
 *   objects, the second approach is challenging on its own.  However, it
2513
 *   often helps to combine it with the first approach, limiting the
2514
 *   scenario.  In other words, you'd limit the scenario to some extent,
2515
 *   but not totally: rather than completely excising the difficult
2516
 *   behavior, you'd narrow it down to a manageable subset of the full
2517
 *   range of real-world possibilities; then, you'd deal with the remaining
2518
 *   anomalies on a case-by-case basis.  For example, you could make the
2519
 *   battery too heavy to carry, which would guarantee that it would never
2520
 *   be put in a box, thrown down a well, or carried out of the room.  That
2521
 *   would only leave a few issues: walking away while carrying the plugged
2522
 *   in lamp, which could be handled with an afterAction that severs the
2523
 *   attachment; putting the lamp in a box and closing the box, which could
2524
 *   be handled with a beforeAction by blocking Close actions whenever the
2525
 *   lamp is inside the object being closed.  
2526
 */
2527
class Attachable: object
2528
    /* 
2529
     *   The list of objects I'm currently attached to.  Note that each of
2530
     *   the objects in this list must usually be an Attachable, and we
2531
     *   must be included in the attachedObjects list in each of these
2532
     *   objects.  
2533
     */
2534
    attachedObjects = []
2535
2536
    /*
2537
     *   Perform programmatic attachment, without any notifications.  This
2538
     *   simply updates my attachedObjects list and the other object's list
2539
     *   to indicate that we're attached to the other object (and vice
2540
     *   versa). 
2541
     */
2542
    attachTo(obj)
2543
    {
2544
        attachedObjects += obj;
2545
        obj.attachedObjects += self;
2546
    }
2547
2548
    /* perform programmatic detachment, without any notifications */
2549
    detachFrom(obj)
2550
    {
2551
        attachedObjects -= obj;
2552
        obj.attachedObjects -= self;
2553
    }
2554
2555
    /* get the subset of my attachments that are non-permanent */
2556
    getNonPermanentAttachments()
2557
    {
2558
        /* return the subset of objects not permanently attached */
2559
        return attachedObjects.subset({x: !isPermanentlyAttachedTo(x)});
2560
    }
2561
2562
    /* am I attached to the given object? */
2563
    isAttachedTo(obj)
2564
    {
2565
        /* we are attached to the other object if it's in our list */
2566
        return (attachedObjects.indexOf(obj) != nil);
2567
    }
2568
2569
    /*
2570
     *   Am I the "major" item in my attachment relationship to the given
2571
     *   object?  This affects how our relationship is described in our
2572
     *   status message: in an asymmetrical relationship, where one object
2573
     *   is the "major" item, we will always describe the minor item as
2574
     *   being attached to the major item rather than vice versa.  This
2575
     *   allows you to ensure that the message is always "the sign is
2576
     *   attached to the wall", and never "the wall is attached to the
2577
     *   sign": the wall is the major item in this relationship, so it's
2578
     *   always the sign that's attached to it.
2579
     *   
2580
     *   By default, we always return nil here, which means that
2581
     *   attachment relationships are symmetrical by default.  In a
2582
     *   symmetrical relationship, we'll describe the other things as
2583
     *   attached to 'self' when describing self.  
2584
     */
2585
    isMajorItemFor(obj) { return nil; }
2586
2587
    /*
2588
     *   Am I *listed* as attached to the given object?  If this is true,
2589
     *   then our examineStatus() will list 'obj' among the things I'm
2590
     *   attached to: "Self is attached to obj."  If this is nil, I'm not
2591
     *   listed as attached.
2592
     *   
2593
     *   By default, we're listed if (1) we're not permanently attached to
2594
     *   'obj', AND (2) we're not the "major" item in the attachment
2595
     *   relationship.  The reason we're not listed if we're permanently
2596
     *   attached is that the attachment information is presumably better
2597
     *   handled via the fixed description of the object rather than in
2598
     *   the extra status message; this is analogous to the way immovable
2599
     *   items (such as Fixtures) aren't normally listed in the
2600
     *   description of a room.  The reason we're not listed if we're the
2601
     *   "major" item in the relationship is that the "major" status
2602
     *   reverses the relationship: when we're the major item, the other
2603
     *   item is described as attached to *us*, rather than vice versa.  
2604
     */
2605
    isListedAsAttachedTo(obj)
2606
    {
2607
        /* 
2608
         *   only list the item if it's not permanently attached, and
2609
         *   we're not the "major" item for the object 
2610
         */
2611
        return (!isPermanentlyAttachedTo(obj) && !isMajorItemFor(obj));
2612
    }
2613
2614
    /*
2615
     *   Is 'obj' listed as attached to me when I'm described?  If this is
2616
     *   true, then our examineStatus() will list 'obj' among the things
2617
     *   attached to me: "Attached to self is obj."  If this is nil, then
2618
     *   'obj' is not listed among the things attached to me when I'm
2619
     *   described.
2620
     *   
2621
     *   This routine is simply the "major" list counterpart of
2622
     *   isListedAsAttachedTo().
2623
     *   
2624
     *   By default, we list 'obj' among my attachments if (1) I'm the
2625
     *   "major" item for 'obj', AND (2) 'obj' is listed as attached to
2626
     *   me, as indicated by obj.isListedAsAttachedTo(self).  We only list
2627
     *   our minor attachments here, because we list all of our other
2628
     *   listable attachments separately, as the things I'm attached to.
2629
     *   We also only list items that are themselves listable as
2630
     *   attachments, for obvious reasons.  
2631
     */
2632
    isListedAsMajorFor(obj)
2633
    {
2634
        /* 
2635
         *   only list the item if we're the "major" item for the object,
2636
         *   and the object is itself listable as an attachment 
2637
         */
2638
        return (isMajorItemFor(obj) && obj.isListedAsAttachedTo(self));
2639
    }
2640
    
2641
    /*
2642
     *   Can I attach to the given object?  This returns true if the other
2643
     *   object is allowable as an attachment, nil if not.
2644
     *   
2645
     *   By default, we look to see if the other side is an Attachable, and
2646
     *   if so, if it overrides canAttachTo(); if so, we'll call its
2647
     *   canAttachTo to ask whether it thinks it can attach to us.  If the
2648
     *   other side doesn't override this, we'll simply return nil.  This
2649
     *   arrangement is convenient because it means that only one side of
2650
     *   an attachable pair needs to implement this; the other side will
2651
     *   automatically figure it out by calling the first side and relying
2652
     *   on the symmetry of the relationship.  
2653
     */
2654
    canAttachTo(obj)
2655
    {
2656
        /* 
2657
         *   if the other side's an Attachable, and it overrides this
2658
         *   method, call the override; if not, it's by default not one of
2659
         *   our valid attachments 
2660
         */
2661
        if (overrides(obj, Attachable, &canAttachTo))
2662
        {
2663
            /* 
2664
             *   the other side is an Attachable that defines a specific
2665
             *   attachment rule, so ask the other side if it thinks we're
2666
             *   one of its attachments; by the symmetry of the
2667
             *   relationship, if we're one of its attachments, then it's
2668
             *   one of ours 
2669
             */
2670
            return obj.canAttachTo(self);
2671
        }
2672
        else
2673
        {
2674
            /* 
2675
             *   the other side doesn't want to tell us, so we're on our
2676
             *   own; we don't recognize any attachments on our own, so
2677
             *   it's not a valid attachment 
2678
             */
2679
            return nil;
2680
        }
2681
    }
2682
2683
    /*
2684
     *   Explain why we can't attach to the given object.  This should
2685
     *   simply display an appropriate mesage.  We use reportFailure to
2686
     *   flag it as a failure report, but that's not actually required,
2687
     *   since we call this from our 'check' routine, which will mark the
2688
     *   action as having failed even if we don't here.  
2689
     */
2690
    explainCannotAttachTo(obj) { reportFailure(&wrongAttachmentMsg); }
2691
2692
    /*
2693
     *   Is it possible for me to detach from the given object?  This asks
2694
     *   whether a given attachment relationship can be dissolved with
2695
     *   DETACH FROM. 
2696
     *   
2697
     *   By default, we'll use similar logic to canAttachTo: if the other
2698
     *   object overrides canDetachFrom(), we'll let it make the
2699
     *   determination.  Otherwise, we'll return nil if one or the other
2700
     *   side is a PermanentAttachment, true if not.  This lets you prevent
2701
     *   detachment by overriding canDetachFrom() on just one side of the
2702
     *   relationship.  
2703
     */
2704
    canDetachFrom(obj)
2705
    {
2706
        /* if the other object overrides canDetachFrom, defer to it */
2707
        if (overrides(obj, Attachable, &canDetachFrom))
2708
        {
2709
            /* let the other side make the judgment */
2710
            return obj.canDetachFrom(self);
2711
        }
2712
        else
2713
        {
2714
            /* 
2715
             *   the other side doesn't override it, so assume we can
2716
             *   detach unless one or the other side is a
2717
             *   PermanentAttachment 
2718
             */
2719
            return !isPermanentlyAttachedTo(obj);
2720
        }
2721
    }
2722
2723
    /*
2724
     *   Am I permanently attached to the other object?  This returns true
2725
     *   if I'm a PermanentAttachment or the other object is.  
2726
     */
2727
    isPermanentlyAttachedTo(obj)
2728
    {
2729
        /* 
2730
         *   if either one of us is a PermanentAttachment, we're
2731
         *   permanently attached to each other 
2732
         */
2733
        return ofKind(PermanentAttachment) || obj.ofKind(PermanentAttachment);
2734
    }
2735
    
2736
2737
    /* 
2738
     *   A message explaining why we can't detach from the given object.
2739
     *   Note that 'obj' can be nil, because we could be attempting a
2740
     *   DETACH command with no indirect object.  
2741
     */
2742
    cannotDetachMsgFor(obj)
2743
    {
2744
        /* 
2745
         *   if we have an object, it must be the wrong one; otherwise, we
2746
         *   simply can't detach generically, since the object to detach
2747
         *   from wasn't specified, and there's nothing obvious we can
2748
         *   detach from 
2749
         */
2750
        return obj != nil ? &wrongDetachmentMsg : &cannotDetachMsg;
2751
    }
2752
2753
    /*
2754
     *   Process attachment to a new object.  This routine is called on
2755
     *   BOTH the direct and indirect object during the attachment process
2756
     *   - that is, it's called on the direct object with the indirect
2757
     *   object as the argument, and then it's called on the indirect
2758
     *   object with the direct object as the argument.
2759
     *   
2760
     *   This symmetrical handling makes it easy to handle the frequent
2761
     *   cases where the player might say ATTACH X TO Y or ATTACH Y TO X
2762
     *   and mean the same thing either way.  Because this method is called
2763
     *   for both X and Y in either phrasing, you can simply choose to
2764
     *   write the handler code in either X or Y - you only have to write
2765
     *   it once, because the handler will be called on each of the
2766
     *   objects, regardless of the phrasing.  So, if you choose to
2767
     *   designate X as the official ATTACH handler, write a handleAttach()
2768
     *   method on X, and leave the one on Y doing nothing: during
2769
     *   execution, the X method will do its work, and the Y method will do
2770
     *   nothing, so regardless of phrasing order, the net result will be
2771
     *   the same.
2772
     *   
2773
     *   By default we do nothing.  Each instance should override this to
2774
     *   display any extra message and take any extra action needed to
2775
     *   process the attachment status change.  Note that the override
2776
     *   doesn't need to worry about managing the attachedObjects list, as
2777
     *   the main action handler does that automatically.
2778
     *   
2779
     *   Note that handleAttach() is always called after both objects have
2780
     *   updated their attachedObjects lists.  This means that you can turn
2781
     *   right around and detach the objects here, if you don't want to
2782
     *   leave them attached.  
2783
     */
2784
    handleAttach(other)
2785
    {
2786
        /* do nothing by default */
2787
    }
2788
2789
    /*
2790
     *   Receive notification that this object or one of its attachments
2791
     *   is being moved to a new container.  When an attached object is
2792
     *   moved, we'll call this on the object being moved AND on every
2793
     *   object attached to it.  'movedObj' is the object being moved, and
2794
     *   'newCont' is the new container it's being moved into.
2795
     *   
2796
     *   By default we do nothing.  Instances can override this as needed.
2797
     *   For example, if you wish to enforce a rule that this object and
2798
     *   all of its attached objects share a common direct container, you
2799
     *   could either block the move (by displaying an error and using
2800
     *   'exit') or run a nested DetachFrom action to sever the attachment
2801
     *   with the object being moved.  
2802
     */
2803
    moveWhileAttached(movedObj, newCont)
2804
    {
2805
        /* do nothing by default */
2806
    }
2807
2808
    /*
2809
     *   Receive notification that this object or one of its attachments is
2810
     *   being moved in the course of an actor traveling to a new location.
2811
     *   Whenever anyone travels while carrying an attachable object
2812
     *   (directly or indirectly), we'll call this on the object being
2813
     *   moved AND on every object attached to it.  'movedObj' is the
2814
     *   object being carried by the traveling actor, 'traveler' is the
2815
     *   Traveler performing the travel, and 'connector' is the
2816
     *   TravelConnector that the traveler is traversing.
2817
     *   
2818
     *   By default, we do nothing.  Instances can override this as needed.
2819
     */
2820
    travelWhileAttached(movedObj, traveler, connector)
2821
    {
2822
        /* do nothing by default */
2823
    }
2824
2825
    /*
2826
     *   Handle detachment.  This works like handleAttach(), in that this
2827
     *   routine is invoked symmetrically for both sides of a DETACH X FROM
2828
     *   Y commands.
2829
     *   
2830
     *   As with handleAttach(), we do nothing by default, so instances
2831
     *   should override as needed.  Note that the override doesn't need to
2832
     *   worry about managing the attachedObjects list, as the main action
2833
     *   handler does that automatically.  As with handleAttach(), this is
2834
     *   called after the attachedObjects lists for both objects are
2835
     *   updated.  
2836
     */
2837
    handleDetach(other)
2838
    {
2839
        /* do nothing by default */
2840
    }
2841
2842
    /* the Lister we use to show our list of attached objects */
2843
    attachmentLister = perInstance(new SimpleAttachmentLister(self))
2844
2845
    /* 
2846
     *   the Lister we use to list the items attached to us (i.e., the
2847
     *   items for which we're the "major" item in the attachment
2848
     *   relationship) 
2849
     */
2850
    majorAttachmentLister = perInstance(new MajorAttachmentLister(self))
2851
2852
    /* add a list of our attachments to the desription */
2853
    examineStatus()
2854
    {
2855
        local tab;
2856
        
2857
        /* inherit the normal status description */
2858
        inherited();
2859
2860
        /* get the actor's visual sense table */
2861
        tab = gActor.visibleInfoTable();
2862
2863
        /* add our list of attachments */
2864
        attachmentLister.showList(gActor, self, attachedObjects,
2865
                                  0, 0, tab, nil);
2866
2867
        /* add our list of major attachments */
2868
        majorAttachmentLister.showList(gActor, self, attachedObjects,
2869
                                       0, 0, tab, nil);
2870
    }
2871
2872
    /* 
2873
     *   Move into a new container.  If I'm attached to anything, we'll
2874
     *   notify ourself and our attachments. 
2875
     */
2876
    mainMoveInto(newCont)
2877
    {
2878
        /* if I'm attached to anything, notify everyone */
2879
        if (attachedObjects.length() != 0)
2880
        {
2881
            /* notify myself */
2882
            moveWhileAttached(self, newCont);
2883
2884
            /* notify my attachments */
2885
            attachedObjects.forEach({x: x.moveWhileAttached(self, newCont)});
2886
        }
2887
2888
        /* inherit the base handling */
2889
        inherited(newCont);
2890
    }
2891
2892
    /*
2893
     *   Receive notification of travel.  If I'm involved in the travel,
2894
     *   and I'm attached to anything, we'll notify ourself and our
2895
     *   attachments. 
2896
     */
2897
    beforeTravel(traveler, connector)
2898
    {
2899
        /* 
2900
         *   If we're traveling with the traveler, and we're attached to
2901
         *   anything, notify everything that's attached.
2902
         */
2903
        if (attachedObjects.length() != 0
2904
            && traveler.isTravelerCarrying(self))
2905
        {
2906
            /* notify myself */
2907
            travelWhileAttached(self, traveler, connector);
2908
2909
            /* notify each of my attachments */
2910
            attachedObjects.forEach(
2911
                {x: x.travelWhileAttached(self, traveler, connector)});
2912
        }
2913
    }
2914
2915
    /* 
2916
     *   during initialization, make sure the attachedObjects list is
2917
     *   symmetrical for both sides of the attachment relationship 
2918
     */
2919
    initializeThing()
2920
    {
2921
        /* do the normal work */
2922
        inherited();
2923
2924
        /* 
2925
         *   check to make sure that each of our attached objects points
2926
         *   back at us 
2927
         */
2928
        foreach (local cur in attachedObjects)
2929
        {
2930
            /* 
2931
             *   if we're not in this one's attachedObjects list, add
2932
             *   ourselves to the list, so that everyone's consistent
2933
             */
2934
            if (cur.attachedObjects.indexOf(self) == nil)
2935
                cur.attachedObjects += self;
2936
        }
2937
    }
2938
2939
    /* handle attachment on the direct object side */
2940
    dobjFor(AttachTo)
2941
    {
2942
        /* require that the actor can touch the direct object */
2943
        preCond = [touchObj]
2944
        
2945
        verify()
2946
        {
2947
            /* 
2948
             *   it makes sense to attach to anything but myself, or things
2949
             *   we're already attached to 
2950
             */
2951
            if (gIobj != nil)
2952
            {
2953
                if (isAttachedTo(gIobj))
2954
                    illogicalAlready(&alreadyAttachedMsg);
2955
                else if (gIobj == self)
2956
                    illogicalSelf(&cannotAttachToSelfMsg);
2957
            }
2958
        }
2959
        
2960
        check()
2961
        {
2962
            /* only allow it if we can attach to the other object */
2963
            if (!canAttachTo(gIobj))
2964
            {
2965
                explainCannotAttachTo(gIobj);
2966
                exit;
2967
            }
2968
        }
2969
        
2970
        action()
2971
        {
2972
            /* add the other object to our list of attached objects */
2973
            attachedObjects += gIobj;
2974
2975
            /* add our default acknowledgment */
2976
            defaultReport(&okayAttachToMsg);
2977
2978
            /* fire the handleAttach event if we're ready */
2979
            maybeHandleAttach(gIobj);
2980
        }
2981
    }
2982
2983
    /* handle attachment on the indirect object side */
2984
    iobjFor(AttachTo)
2985
    {
2986
        /*
2987
         *   Require that the direct object can touch the indirect object.
2988
         *   This ensures that the two objects to be attached can touch
2989
         *   one another.  Note that we don't also require that the actor
2990
         *   be able to touch the indirect object directly, since it's
2991
         *   good enough that (1) the actor can touch the direct object
2992
         *   (which we enforce with the dobj precondition), and (2) the
2993
         *   direct object can touch the indirect object.  This allows for
2994
         *   odd things like plugging something into a recessed outlet,
2995
         *   where the recessed bit can't be reached directly but can be
2996
         *   reached using the plug.  
2997
         */
2998
        preCond = [dobjTouchObj]
2999
        
3000
        verify()
3001
        {
3002
            /* 
3003
             *   it makes sense to attach to anything but myself, or things
3004
             *   we're already attached to 
3005
             */
3006
            if (gDobj != nil)
3007
            {
3008
                if (isAttachedTo(gDobj))
3009
                    illogicalAlready(&alreadyAttachedMsg);
3010
                else if (gDobj == self)
3011
                    illogicalSelf(&cannotAttachToSelfMsg);
3012
            }
3013
        }
3014
        
3015
        check()
3016
        {
3017
            /* only allow it if we can attach to the other object */
3018
            if (!canAttachTo(gDobj))
3019
            {
3020
                explainCannotAttachTo(gDobj);
3021
                exit;
3022
            }
3023
        }
3024
        
3025
        action()
3026
        {
3027
            /* add the other object to our list of attached objects */
3028
            attachedObjects += gDobj;
3029
3030
            /* fire the handleAttach event if we're ready */
3031
            maybeHandleAttach(gIobj);
3032
        }
3033
    }
3034
3035
    /* 
3036
     *   Fire the handleAttach event - we'll notify both sides as soon as
3037
     *   both sides are hooked up with each other.  This ensures that both
3038
     *   lists are updated before we notify either side, so the ordering
3039
     *   doesn't depend on whether we handle the dobj or iobj first. 
3040
     */
3041
    maybeHandleAttach(other)
3042
    {
3043
        /* if both lists are hooked up, send the notifications */
3044
        if (attachedObjects.indexOf(other) != nil
3045
            && other.attachedObjects.indexOf(self) != nil)
3046
        {
3047
            /* notify our side */
3048
            handleAttach(other);
3049
3050
            /* notify the other side */
3051
            other.handleAttach(self);
3052
        }
3053
    }
3054
3055
    /* handle simple, unspecified detachment (DETACH OBJECT) */
3056
    dobjFor(Detach)
3057
    {
3058
        verify()
3059
        {
3060
            /* if I'm not attached to anything, this is illogical */
3061
            if (attachedObjects.length() == 0)
3062
                illogicalAlready(cannotDetachMsgFor(nil));
3063
        }
3064
        action()
3065
        {
3066
            local lst;
3067
3068
            /* get the non-permanent attachment subset */
3069
            lst = getNonPermanentAttachments();
3070
3071
            /* check what that leaves us */
3072
            if (lst.length() == 0)
3073
            {
3074
                /* 
3075
                 *   we're not attached to anything that we can detach
3076
                 *   from, so simply report that we can't detach
3077
                 *   generically 
3078
                 */
3079
                reportFailure(cannotDetachMsgFor(nil));
3080
            }
3081
            else if (lst.length() == 1)
3082
            {
3083
                /* 
3084
                 *   we have exactly one attached object from which we can
3085
                 *   detach, so they must want to detach from that -
3086
                 *   process this as DETACH FROM my one attached object 
3087
                 */
3088
                replaceAction(DetachFrom, self, lst[1]);
3089
            }
3090
            else
3091
            {
3092
                /* 
3093
                 *   we have more than one detachable attachment, so ask
3094
                 *   which one they mean 
3095
                 */
3096
                askForIobj(DetachFrom);
3097
            }
3098
        }
3099
    }
3100
3101
    /* handle detaching me from a specific other object */
3102
    dobjFor(DetachFrom)
3103
    {
3104
        verify()
3105
        {
3106
            /* it only makes sense to try detaching us from our attachments */
3107
            if (gIobj != nil && !isAttachedTo(gIobj))
3108
                illogicalAlready(&notAttachedToMsg);
3109
        }
3110
        check()
3111
        {
3112
            /* make sure I'm allowed to detach from the given object */
3113
            if (!canDetachFrom(gIobj))
3114
            {
3115
                reportFailure(cannotDetachMsgFor(gIobj));
3116
                exit;
3117
            }
3118
        }
3119
        action()
3120
        {
3121
            /* remove the other object from our list of attached objects */
3122
            attachedObjects -= gIobj;
3123
3124
            /* add our default acknowledgment */
3125
            defaultReport(&okayDetachFromMsg);
3126
3127
            /* fire the handleDetach event if appropriate */
3128
            maybeHandleDetach(gIobj);
3129
        }
3130
    }
3131
3132
    /* handle detachment on the indirect object side */
3133
    iobjFor(DetachFrom)
3134
    {
3135
        verify()
3136
        {
3137
            /* it only makes sense to try detaching my attachments */
3138
            if (gDobj == nil)
3139
            {
3140
                /* 
3141
                 *   we don't know the dobj yet, but we can check the
3142
                 *   tentative list for the possible set 
3143
                 */
3144
                if (gTentativeDobj
3145
                    .indexWhich({x: isAttachedTo(x.obj_)}) == nil)
3146
                    illogicalAlready(&notAttachedToMsg);
3147
            }
3148
            else if (gDobj != nil && !isAttachedTo(gDobj))
3149
                illogicalAlready(&notAttachedToMsg);
3150
        }
3151
        check()
3152
        {
3153
            /* make sure I'm allowed to detach from the given object */
3154
            if (!canDetachFrom(gDobj))
3155
            {
3156
                reportFailure(cannotDetachMsgFor(gDobj));
3157
                exit;
3158
            }
3159
        }
3160
        action()
3161
        {
3162
            /* remove the other object from our list of attached objects */
3163
            attachedObjects -= gDobj;
3164
3165
            /* fire the handleDetach event if appropriate */
3166
            maybeHandleDetach(gDobj);
3167
        }
3168
    }
3169
3170
    /* 
3171
     *   Fire the handleDetach event - we'll notify both sides as soon as
3172
     *   both sides are un-hooked up.  This ensures that both lists are
3173
     *   updated before we notify either side, so the ordering doesn't
3174
     *   depend on whether we handle the dobj or iobj first.  
3175
     */
3176
    maybeHandleDetach(other)
3177
    {
3178
        /* if both lists are un-hooked up, send the notifications */
3179
        if (attachedObjects.indexOf(other) == nil
3180
            && other.attachedObjects.indexOf(self) == nil)
3181
        {
3182
            /* notify our side */
3183
            handleDetach(other);
3184
3185
            /* notify the other side */
3186
            other.handleDetach(self);
3187
        }
3188
    }
3189
3190
    /* 
3191
     *   TAKE X FROM Y is the same as DETACH X FROM Y for things we're
3192
     *   attached to, but use the inherited handling otherwise 
3193
     */
3194
    dobjFor(TakeFrom)
3195
    {
3196
        verify()
3197
        {
3198
            /* 
3199
             *   use the inherited handling only if we're not attached -
3200
             *   if we're attached, consider it logical, overriding any
3201
             *   containment relationship check we might otherwise make 
3202
             */
3203
            if (gIobj == nil || !isAttachedTo(gIobj))
3204
                inherited();
3205
        }
3206
        check()
3207
        {
3208
            /* inherit the default check only if we're not attached */
3209
            if (!isAttachedTo(gIobj))
3210
                inherited();
3211
        }
3212
        action()
3213
        {
3214
            /* 
3215
             *   if we're attached, change this into a DETACH FROM action;
3216
             *   otherwise, use the inherited TAKE FROM handling 
3217
             */
3218
            if (isAttachedTo(gIobj))
3219
                replaceAction(DetachFrom, self, gIobj);
3220
            else
3221
                inherited();
3222
        }
3223
    }
3224
    iobjFor(TakeFrom)
3225
    {
3226
        verify()
3227
        {
3228
            /* use the inherited handling only if we're not attached */
3229
            if (gDobj == nil || !isAttachedTo(gDobj))
3230
                inherited();
3231
        }
3232
        check()
3233
        {
3234
            /* inherit the default check only if we're not attached */
3235
            if (!isAttachedTo(gDobj))
3236
                inherited();
3237
        }
3238
        action()
3239
        {
3240
            /* inherit the default action only if we're not attached */
3241
            if (!isAttachedTo(gDobj))
3242
                inherited();
3243
        }
3244
    }
3245
;
3246
3247
/*
3248
 *   An Attachable-specific precondition: the Attachable isn't already
3249
 *   attached to something else.  This can be added to the preCond list for
3250
 *   an Attachable (for iobjFor(AttachTo) and dobjFor(AttachTo)) to ensure
3251
 *   that any existing attachment is removed before a new attachment is
3252
 *   formed.  This is useful when the Attachable can connect to only one
3253
 *   thing at a time.  
3254
 */
3255
objNotAttached: PreCondition
3256
    checkPreCondition(obj, allowImplicit)
3257
    {
3258
        /* 
3259
         *   if we don't already have any non-permanent attachments, we're
3260
         *   fine (as we don't require removing permanent attachments) 
3261
         */
3262
        if (obj.attachedObjects.indexWhich(
3263
            {x: !obj.isPermanentlyAttachedTo(x)}) == nil)
3264
            return nil;
3265
3266
        /* 
3267
         *   Try an implicit Detach command.  It should be safe to use the
3268
         *   form that doesn't specify what we're detaching from, since the
3269
         *   whole point of this condition is that the object can have only
3270
         *   one non-permanent attachment, hence the vague Detach handler
3271
         *   should be able to figure out what we mean.  
3272
         */
3273
        if (allowImplicit && tryImplicitAction(Detach, obj))
3274
        {
3275
            /* if we're still attached to anything, we failed, so abort */
3276
            if (obj.attachedObjects.indexWhich(
3277
                {x: !obj.isPermanentlyAttachedTo(x)}) != nil)
3278
                exit;
3279
3280
            /* tell the caller we executed an implied action */
3281
            return true;
3282
        }
3283
3284
        /* we must detach first */
3285
        reportFailure(&mustDetachMsg, obj);
3286
        exit;
3287
    }
3288
;
3289
3290
/*
3291
 *   A "nearby" attachable is a subclass of Attachable that adds a
3292
 *   requirement that the attached objects be in a given location.  By
3293
 *   default, we simply require that they have a common immediate
3294
 *   container, but this can be overridden so that each object's location
3295
 *   is negotiated separately.  This is a simple and effective pattern that
3296
 *   avoids many of the potential anomalies with attachment (see the
3297
 *   Attachable comments for examples).
3298
 *   
3299
 *   In AttachTo actions, we enforce the nearby requirement with a
3300
 *   precondition requiring the direct object to be in the same immediate
3301
 *   container as the indirect object, and vice versa.  In
3302
 *   moveWhileAttached(), we enforce the rule by detaching the objects if
3303
 *   one is being moved away from the other's immediate container.  
3304
 */
3305
class NearbyAttachable: Attachable
3306
    dobjFor(AttachTo)
3307
    {
3308
        /* require that the objects be in the negotiated locations */
3309
        preCond = (inherited() + nearbyAttachableCond)
3310
    }
3311
    iobjFor(AttachTo)
3312
    {
3313
        /* require that the objects be in the negotiated locations */
3314
        preCond = (inherited() + nearbyAttachableCond)
3315
    }
3316
3317
    /*
3318
     *   Get the target locations for attaching to the given other object.
3319
     *   The "target locations" are the locations where the objects are
3320
     *   required to be in order to carry out the ATTACH command to attach
3321
     *   this object to the other object (or vice versa). 
3322
     *   
3323
     *   This method returns a list with three elements.  The first
3324
     *   element is the target location for 'self', and the second is the
3325
     *   target location for 'other', the object we're attaching to.  The
3326
     *   third element is an integer giving the priority; a higher number
3327
     *   means higher priority.
3328
     *   
3329
     *   The priority is an arbitrary value that we use to determine which
3330
     *   of the two objects involved in the attach gets to decide on the
3331
     *   target locations.  We call this method on both of the two objects
3332
     *   being attached to one another, then we use the target locations
3333
     *   returned by the object that claims the higher priority.  If the
3334
     *   two priorities are equal, we pick one arbitrarily.
3335
     *   
3336
     *   The default implementation chooses my own immediate container as
3337
     *   the target location for both objects.  However, if the other
3338
     *   object is non-portable, we'll choose its immediate location
3339
     *   instead, since we obviously can't move it to our container.  
3340
     */
3341
    getNearbyAttachmentLocs(other)
3342
    {
3343
        /* 
3344
         *   If the other object is portable, use our immediate container
3345
         *   as the proposed location for both objects; otherwise, use the
3346
         *   other object's immediate container.  In any case, use a low
3347
         *   priority, since we're just the default base class
3348
         *   implementation; any override will generally have higher
3349
         *   priority. 
3350
         */
3351
        if (other.ofKind(NonPortable))
3352
        {
3353
            /* the other can't be moved, so use its location */
3354
            return [other.location, other.location, 0];
3355
        }
3356
        else
3357
        {
3358
            /* 
3359
             *   the other can be moved, so use our own location, in a
3360
             *   paraphrase of the realty agent's favorite mantra 
3361
             */
3362
            return [location, location, 0];
3363
        }
3364
    }
3365
3366
    /* when an attached object is being moved, detach the objects */
3367
    moveWhileAttached(movedObj, newCont)
3368
    {
3369
        /* 
3370
         *   If I'm the one being moved, detach me from all of my
3371
         *   non-permanent attachments; otherwise, just detach me from the
3372
         *   other object, since it's the only one of my attachments being
3373
         *   moved.  
3374
         */
3375
        if (movedObj == self)
3376
        {
3377
            /* I'm being moved - detach from everything */
3378
            foreach (local cur in attachedObjects)
3379
            {
3380
                /* 
3381
                 *   If we're not permanently attached to this one, and
3382
                 *   it's not inside me, detach from it.  We don't need to
3383
                 *   detach from objects inside this one, because they'll
3384
                 *   be moved along with us automatically.  
3385
                 */
3386
                if (!cur.isIn(self) && !isPermanentlyAttachedTo(cur))
3387
                    nestedDetachFrom(cur);
3388
            }
3389
        }
3390
        else
3391
        {
3392
            /* just detach from the one object */
3393
            nestedDetachFrom(movedObj);
3394
        }
3395
    }
3396
3397
    /* perform a nested DetachFrom action on the given object */
3398
    nestedDetachFrom(obj)
3399
    {
3400
        /* run the nested DetachFrom as an implied action */
3401
        tryImplicitAction(DetachFrom, self, obj);
3402
3403
        /* 
3404
         *   if we're still attached to this object, the implied command
3405
         *   must have failed, so abort the entire action 
3406
         */
3407
        if (attachedObjects.indexOf(obj) != nil)
3408
            exit;
3409
    }
3410
;
3411
3412
/*
3413
 *   Precondition for nearby-attachables.  This ensures that the two
3414
 *   objects being attached are in their negotiated locations.  
3415
 */
3416
nearbyAttachableCond: PreCondition
3417
    /* carry out the precondition */
3418
    checkPreCondition(obj, allowImplicit)
3419
    {
3420
        local dobjProposal, iobjProposal;
3421
        local dobjTargetLoc, iobjTargetLoc;
3422
        local iobjRet, dobjRet;
3423
3424
        /*
3425
         *   Ask each of the NearbyAttachable objects (the direct and
3426
         *   indirect objects) what it thinks.  If an object isn't a
3427
         *   NearbyAttachable, it won't have any opinion, so use a
3428
         *   placeholder result with an extremely negative priority
3429
         *   (ensuring that it won't be chosen).  In order for this
3430
         *   precondition to have been triggered, one or the other of the
3431
         *   objects must have been a nearby-attachable.  
3432
         */
3433
        dobjProposal = (gDobj.ofKind(NearbyAttachable)
3434
                        ? gDobj.getNearbyAttachmentLocs(gIobj)
3435
                        : [nil, nil, -2147483648]);
3436
        iobjProposal = (gIobj.ofKind(NearbyAttachable)
3437
                        ? gIobj.getNearbyAttachmentLocs(gDobj)
3438
                        : [nil, nil, -2147483648]);
3439
3440
        /* 
3441
         *   If the direct object claims higher priority, use its
3442
         *   attachment locations; otherwise, use the direct object's
3443
         *   locations.  (This means that we take the indirect object's
3444
         *   proposed locations if the priorites are equal.)  
3445
         */
3446
        if (dobjProposal[3] > iobjProposal[3])
3447
        {
3448
            /* the direct object claims higher priority, so use its results */
3449
            dobjTargetLoc = dobjProposal[1];
3450
            iobjTargetLoc = dobjProposal[2];
3451
        }
3452
        else
3453
        {
3454
            /* 
3455
             *   The direct object doesn't have a higher priority, so use
3456
             *   the indirect object's results.  Note that the iobj
3457
             *   results list has the iobj in the first position, since it
3458
             *   was the 'self' when we asked it for its proposal.  
3459
             */
3460
            dobjTargetLoc = iobjProposal[2];
3461
            iobjTargetLoc = iobjProposal[1];
3462
        }
3463
        
3464
        /* carry out the pair of moves as needed */
3465
        dobjRet = moveObject(gDobj, dobjTargetLoc, allowImplicit);
3466
        iobjRet = moveObject(gIobj, iobjTargetLoc, allowImplicit);
3467
3468
        /* 
3469
         *   Return the indication of whether or not we carried out an
3470
         *   implied command.  (Note that we can't call moveObject in this
3471
         *   'return' expression directly, because of the short-circuit
3472
         *   behavior of the '||' operator.  We must call both, even if
3473
         *   both carry out an action.)  
3474
         */
3475
        return (dobjRet || iobjRet);
3476
    }
3477
3478
    /* carry out an implied action to move an object to a location */
3479
    moveObject(obj, loc, allowImplicit)
3480
    {
3481
        /* if the object is already there, we have nothing to do */
3482
        if (obj.location == loc)
3483
            return nil;
3484
3485
        /* try the implied move */
3486
        if (allowImplicit && loc.tryMovingObjInto(obj))
3487
        {
3488
            /* make sure it worked */
3489
            if (obj.location != loc)
3490
                exit;
3491
3492
            /* we performed an implied action */
3493
            return true;
3494
        }
3495
3496
        /* we can't move it - report failure and abort */
3497
        loc.mustMoveObjInto(obj);
3498
        exit;
3499
    }
3500
;
3501
3502
/*
3503
 *   A PlugAttachable is a mix-in class that turns PLUG INTO into ATTACH TO
3504
 *   and UNPLUG FROM into DETACH FROM.  This can be combined with
3505
 *   Attachable or an Attachable subclass for objects that can be attached
3506
 *   with PLUG INTO commands.  
3507
 */
3508
class PlugAttachable: object
3509
    /* PLUG IN - to what? */
3510
    dobjFor(PlugIn)
3511
    {
3512
        verify() { }
3513
        action() { askForIobj(PlugInto); }
3514
    }
3515
3516
    /* PLUG INTO is the same as ATTACH TO for us */
3517
    dobjFor(PlugInto) remapTo(AttachTo, self, IndirectObject)
3518
    iobjFor(PlugInto) remapTo(AttachTo, DirectObject, self)
3519
3520
    /* UNPLUG FROM is the same as DETACH FROM */
3521
    dobjFor(Unplug) remapTo(Detach, self)
3522
    dobjFor(UnplugFrom) remapTo(DetachFrom, self, IndirectObject)
3523
    iobjFor(UnplugFrom) remapTo(DetachFrom, DirectObject, self)
3524
;
3525
3526
/* ------------------------------------------------------------------------ */
3527
/*
3528
 *   Permanent attachments.  This class is for things that are described
3529
 *   in the story text as attached to one another, but which can never be
3530
 *   separated.  This is a mix-in class that can be combined with a Thing
3531
 *   subclass.
3532
 *   
3533
 *   Descriptions of attachment tend to invite the player to try detaching
3534
 *   the parts; the purpose of this class is to provide responses that are
3535
 *   better than the defaults.  A good custom message for this class
3536
 *   should usually acknowledge the attachment relationship, and explain
3537
 *   why the parts can't be separated.
3538
 *   
3539
 *   There are two ways to express the attachment relationship.
3540
 *   
3541
 *   First, the more flexible way: in each PermanentAttachment object,
3542
 *   define the 'attachedObjects' property to contain a list of the
3543
 *   attached objects.  All of those other attached objects should usually
3544
 *   be PermanentAttachment objects themselves, because the real-world
3545
 *   relationship we're modeling is obviously symmetrical.  Because of the
3546
 *   symmetrical relationship, it's only necessary to include the list
3547
 *   entry on one side of a pair of attached objects - each side will
3548
 *   automatically link itself to the other at start-up if it appears in
3549
 *   the other's attachedObjects list.
3550
 *   
3551
 *   Second, the really easy way: if one of the attached objects is
3552
 *   directly inside the other (which often happens for permanent
3553
 *   attachments, because one is a component of the other), make the
3554
 *   parent a PermanentAttachment, make the inner one a
3555
 *   PermanentAttachmentChild, and you're done.  The two will
3556
 *   automatically link up their attachment lists at start-up.
3557
 *   
3558
 *   Note that this is a subclass of Attachable.  Note also that a
3559
 *   PermanentAttachment can be freely combined with a regular Attachable;
3560
 *   for example, you could create a rope with a hook permanently
3561
 *   attached, but stil allow the rope to be attached to other things as
3562
 *   well: you'd make the rope a regular Attachable, and make the hook a
3563
 *   PermanentAttachment.  The hook would be unremovable because of its
3564
 *   permanent status, and this would symmetrical prevent the rope from
3565
 *   being removed from the hook.  But the rope could still be attached to
3566
 *   and detached from other objects.  
3567
 */
3568
class PermanentAttachment: Attachable
3569
    /*
3570
     *   Get the message explaining why we can't detach from 'obj'.
3571
     *   
3572
     *   By default, if our container is also a PermanentAttachment, and
3573
     *   we're attached to it, we'll simply return its message.  This
3574
     *   makes it really easy to define symmetrical permanent attachment
3575
     *   relationships using containment, since all you have to do is make
3576
     *   the container and the child both be PermanentAttachments, and
3577
     *   then just define the cannot-detach message in the container.  If
3578
     *   the container isn't a PermanentAttachment, or we're not attached
3579
     *   to it, we'll return our default library message.  
3580
     */
3581
    cannotDetachMsgFor(obj)
3582
    {
3583
        if (location != nil
3584
            && location.ofKind(PermanentAttachment)
3585
            && isAttachedTo(location))
3586
            return location.cannotDetachMsgFor(obj);
3587
        else
3588
            return baseCannotDetachMsg;
3589
    }
3590
3591
    /* basic message to use when we try to detach something from self */
3592
    baseCannotDetachMsg = &cannotDetachPermanentMsg
3593
;
3594
3595
/*
3596
 *   A permanent attachment "child" - this is an attachment that's
3597
 *   explicitly attached to its container object.  This is a convenient
3598
 *   way of setting up an attachment relationship between container and
3599
 *   contents when the contents object isn't a Component.  
3600
 */
3601
class PermanentAttachmentChild: PermanentAttachment
3602
    /* we're attached directly to our container */
3603
    attachedObjects = perInstance([location])
3604
;
3605
3606
/* ------------------------------------------------------------------------ */
3607
/*
3608
 *   A mix-in class for objects that don't come into play until some
3609
 *   future event.  This class lets us initialize these objects with their
3610
 *   *eventual* location, using the standard '+' syntax, but they won't
3611
 *   actually appear in the given location until later in the game.
3612
 *   During pre-initialization, we'll remember the starting location, then
3613
 *   set the actual location to nil; later, the object can be easily moved
3614
 *   to its eventual location by calling makePresent().  
3615
 */
3616
class PresentLater: object
3617
    /*
3618
     *   My "key" - this is an optional property you can add to a
3619
     *   PresentLater object to associate it with a group of objects.  You
3620
     *   can then use makePresentByKey() to move every object with a given
3621
     *   key into the game world at once.  This is useful when an event
3622
     *   triggers a whole set of objects to come into the game world:
3623
     *   rather than having to write a method that calls makePresent() on
3624
     *   each of the related objects individually, you can simply give each
3625
     *   related object the same key value, then call makePresentByKey() on
3626
     *   that key.
3627
     *   
3628
     *   You don't need to define this for an object unless you want to use
3629
     *   makePresentByKey() with the object.  
3630
     */
3631
    plKey = nil
3632
3633
    /*
3634
     *   Flag: are we present initially?  By default, we're only present
3635
     *   later, as that's the whole point.  In some cases, though, we have
3636
     *   objects that come and go, but start out present.  Setting this
3637
     *   property to true makes the object present initially, but still
3638
     *   allows it to come and go using the standard PresentLater
3639
     *   mechanisms.  
3640
     */
3641
    initiallyPresent = nil
3642
3643
    initializeLocation()
3644
    {
3645
        /* 
3646
         *   Save the initial location for later, and then clear out the
3647
         *   current location.  We want to start out being out of the game,
3648
         *   but remember where we'll appear when called upon.  To
3649
         *   accommodate MultiLoc objects, check locationList first.  
3650
         */
3651
        if (locationList != nil)
3652
        {
3653
            /* save the location list */
3654
            eventualLocation = locationList;
3655
3656
            /* 
3657
             *   clear my location list if I'm not initially present; if I
3658
             *   am initially present, inherit the normal initialization 
3659
             */
3660
            if (!initiallyPresent)
3661
                locationList = [];
3662
            else
3663
                inherited();
3664
        }
3665
        else
3666
        {
3667
            /* save my eventual location */
3668
            eventualLocation = location;
3669
3670
            /* 
3671
             *   clear my location if I'm not initially present; if I am
3672
             *   present initially, inherit the normal set-up 
3673
             */
3674
            if (!initiallyPresent)
3675
                location = nil;
3676
            else
3677
                inherited();
3678
        }
3679
    }
3680
3681
    /* bring the object into the game world in its eventual location(s) */
3682
    makePresent()
3683
    {
3684
        local pc;
3685
        
3686
        /* 
3687
         *   If we have a list, add ourself to each location in the list;
3688
         *   otherwise, simply move ourself to the single location.  
3689
         */
3690
        if (eventualLocation != nil && eventualLocation.ofKind(Collection))
3691
            eventualLocation.forEach({loc: moveIntoAdd(loc)});
3692
        else
3693
            moveInto(eventualLocation);
3694
3695
        /* if the player character can now see me, mark me as seen */
3696
        pc = gPlayerChar;
3697
        if (pc.canSee(self))
3698
        {
3699
            /* mark me as seen */
3700
            pc.setHasSeen(self);
3701
3702
            /* mark my visible contents as seen */
3703
            setContentsSeenBy(pc.visibleInfoTable(), pc);
3704
        }
3705
    }
3706
3707
    /* 
3708
     *   make myself present if the given condition is true; otherwise,
3709
     *   remove me from the game world (i.e. move me into nil)
3710
     */
3711
    makePresentIf(cond)
3712
    {
3713
        if (cond)
3714
            makePresent();
3715
        else
3716
            moveInto(nil);
3717
    }
3718
3719
    /* 
3720
     *   Bring every PresentLater object with the given key into the game.
3721
     *   Note that this is a "class" method that you call on PresentLater
3722
     *   itself:
3723
     *   
3724
     *   PresentLater.makePresentByKey('foo'); 
3725
     */
3726
    makePresentByKey(key)
3727
    {
3728
        /* 
3729
         *   scan every PresentLater object, and move each one with the
3730
         *   given key into the game 
3731
         */
3732
        forEachInstance(PresentLater, new function(obj) {
3733
            if (obj.plKey == key)
3734
                obj.makePresent();
3735
        });
3736
    }
3737
3738
    /* 
3739
     *   Bring every PresentLater object with the given key into the game,
3740
     *   or move every one out of the game, according to the condition
3741
     *   'cond'.
3742
     *   
3743
     *   If 'cond' is a function pointer, we'll invoke it once per object
3744
     *   with the given key, passing the object as the parameter, and use
3745
     *   the return value as the in game/out of game setting.  For example,
3746
     *   if you wanted to show every object with key 'foo' AND with the
3747
     *   property 'showObj' set to true, you could write this:
3748
     *   
3749
     *   PresentLater.makePresentByKeyIf('foo', {x: x.showObj});
3750
     *   
3751
     *   Note that this is a "class" method that you call on PresentLater
3752
     *   itself.  
3753
     */
3754
    makePresentByKeyIf(key, cond)
3755
    {
3756
        /* 
3757
         *   scan every PresentLater object, check each one's key, and make
3758
         *   each one with the given key present 
3759
         */
3760
        forEachInstance(PresentLater, new function(obj) {
3761
            /* consider this object if its key matches */
3762
            if (obj.plKey == key)
3763
            {
3764
                local flag = cond;
3765
                
3766
                /* 
3767
                 *   evaluate the condition - if it's a function pointer,
3768
                 *   invoke it on the current object, otherwise just take
3769
                 *   it as a pre-evaluated condition value 
3770
                 */
3771
                if (dataTypeXlat(cond) == TypeFuncPtr)
3772
                    flag = (cond)(obj);
3773
3774
                /* show or hide the object according to the condition */
3775
                obj.makePresentIf(flag);
3776
            }
3777
        });
3778
    }
3779
3780
    /* our eventual location */
3781
    eventualLocation = nil
3782
;
3783