Attachment is symmetrical: we can only attach to other Attachable objects. As a result, the verb handling for ATTACH can be performed symmetrically - ATTACH X TO Y is handled the same way as ATTACH Y TO X. Sometimes reversing the roles makes the command nonsensical, but when the reversal makes sense, it seems unlikely that it'll ever change the meaning of the command. This makes it program the verb handling, because it means that we can designate one of X or Y as the handler for the verb, and just write the code once there. Refer to the handleAttach() method to see how this works.
There's an important detail that we leave to instances, because there's no good general rule we can implement. Specifically, there's the matter of imposing appropriate constraints on the relative locations of objects once they're attached to one another. There are numerous anomalies that become possible once two objects are attached. Consider the example of a battery connected to a jumper cable that's in turn connected to a lamp:
- if we put the battery in a box but leave the lamp outside the box, we shouldn't be able to close the lid of the box all the way without breaking the cables
- if we're carrying the battery but not the lamp, traveling to a new room should drag the lamp along
- if we drop the battery down a well, the lamp should be dragged down with it
Our world model isn't sophisticated enough to properly model an attachment relationship, so it can't deal with these contingencies by proper physical simulation. Which is why we have to leave these for the game to handle.
There are two main strategies you can apply to handle these problems.
First, you can impose limits that prevent these sorts of situations from coming up in the first place, either by carefully designing the scenario so they simply don't come up, or by imposing more or less artificial constraints. For example, you could solve all of the problems above by eliminating the jumper cable and attaching the lamp directly to the battery, or by making the jumper cable very short. Anything attached to the battery would effectively become located "in" the battery, so it would move everywhere along with the battery automatically. Detaching the lamp would move the lamp back outside the battery, and conversely, moving the lamp out of the battery would detach the objects.
Second, you can detect the anomalous cases and handle them explicitly with special-purpose code. You could use beforeAction and afterAction methods on one of the attached objects, for example, to detect the various problematic actions, either blocking them or implementing appropriate consequences.
Given the number of difficult anomalies possible with rope-like objects, the second approach is challenging on its own. However, it often helps to combine it with the first approach, limiting the scenario. In other words, you'd limit the scenario to some extent, but not totally: rather than completely excising the difficult behavior, you'd narrow it down to a manageable subset of the full range of real-world possibilities; then, you'd deal with the remaining anomalies on a case-by-case basis. For example, you could make the battery too heavy to carry, which would guarantee that it would never be put in a box, thrown down a well, or carried out of the room. That would only leave a few issues: walking away while carrying the plugged in lamp, which could be handled with an afterAction that severs the attachment; putting the lamp in a box and closing the box, which could be handled with a beforeAction by blocking Close actions whenever the lamp is inside the object being closed.
Attachable : object
By default, we look to see if the other side is an Attachable, and if so, if it overrides canAttachTo(); if so, we'll call its canAttachTo to ask whether it thinks it can attach to us. If the other side doesn't override this, we'll simply return nil. This arrangement is convenient because it means that only one side of an attachable pair needs to implement this; the other side will automatically figure it out by calling the first side and relying on the symmetry of the relationship.
By default, we'll use similar logic to canAttachTo: if the other object overrides canDetachFrom(), we'll let it make the determination. Otherwise, we'll return nil if one or the other side is a PermanentAttachment, true if not. This lets you prevent detachment by overriding canDetachFrom() on just one side of the relationship.
This symmetrical handling makes it easy to handle the frequent cases where the player might say ATTACH X TO Y or ATTACH Y TO X and mean the same thing either way. Because this method is called for both X and Y in either phrasing, you can simply choose to write the handler code in either X or Y - you only have to write it once, because the handler will be called on each of the objects, regardless of the phrasing. So, if you choose to designate X as the official ATTACH handler, write a handleAttach() method on X, and leave the one on Y doing nothing: during execution, the X method will do its work, and the Y method will do nothing, so regardless of phrasing order, the net result will be the same.
By default we do nothing. Each instance should override this to display any extra message and take any extra action needed to process the attachment status change. Note that the override doesn't need to worry about managing the attachedObjects list, as the main action handler does that automatically.
Note that handleAttach() is always called after both objects have updated their attachedObjects lists. This means that you can turn right around and detach the objects here, if you don't want to leave them attached.
As with handleAttach(), we do nothing by default, so instances should override as needed. Note that the override doesn't need to worry about managing the attachedObjects list, as the main action handler does that automatically. As with handleAttach(), this is called after the attachedObjects lists for both objects are updated.
By default, we're listed if (1) we're not permanently attached to 'obj', AND (2) we're not the "major" item in the attachment relationship. The reason we're not listed if we're permanently attached is that the attachment information is presumably better handled via the fixed description of the object rather than in the extra status message; this is analogous to the way immovable items (such as Fixtures) aren't normally listed in the description of a room. The reason we're not listed if we're the "major" item in the relationship is that the "major" status reverses the relationship: when we're the major item, the other item is described as attached to *us*, rather than vice versa.
This routine is simply the "major" list counterpart of isListedAsAttachedTo().
By default, we list 'obj' among my attachments if (1) I'm the "major" item for 'obj', AND (2) 'obj' is listed as attached to me, as indicated by obj.isListedAsAttachedTo(self). We only list our minor attachments here, because we list all of our other listable attachments separately, as the things I'm attached to. We also only list items that are themselves listable as attachments, for obvious reasons.
By default, we always return nil here, which means that attachment relationships are symmetrical by default. In a symmetrical relationship, we'll describe the other things as attached to 'self' when describing self.
By default we do nothing. Instances can override this as needed. For example, if you wish to enforce a rule that this object and all of its attached objects share a common direct container, you could either block the move (by displaying an error and using 'exit') or run a nested DetachFrom action to sever the attachment with the object being moved.
By default, we do nothing. Instances can override this as needed.