vhyl.t
Back to reference
1 /* Adventure world policy: entities, actions, reports, actors and scheduling.
2 * Copyright (c) 2026 John Cunningham. MIT License; see LICENSE. */
3
4 /*
5 * vhyl core: things, rooms, scope, and the command cycle.
6 *
7 * What a world declares is an entity and its rows. What this file supplies is
8 * what every world would otherwise write again: what is in scope, what is
9 * visible, how a room is described, and the verbs that move things about.
10 */
11
12 /** @api author
13 * Base entity for physical and describable things in the adventure world. */
14 class Thing: object
15
16 /** @api author
17 * Properties exposed to structured hosts for entity presentation. */
18 presentation = [&name, &isOpen, &isOpenable, &isContainer, &isLit,
19 &isLightSource, &isFixed, &isDecoration, &isTransparent,
20 &isHidden, &bulk, &bulkCapacity, &objPos]
21 /** @api author
22 * Display name used by library messages and room descriptions. */
23 name = nil
24 /* A container holds things, and holds them shut when it can be closed. */
25 /** @api author
26 * Whether the thing can hold contained entities. */
27 isContainer = nil
28 /** @api author
29 * Whether open and close actions may change this thing's state. */
30 isOpenable = nil
31 /** @api author
32 * Current open state; defaults to open for non-openable things. */
33 isOpen = true
34 /* Fixed things stay where they are: scenery, fittings, rooms. */
35 /** @api author
36 * Prevent taking or moving this thing. */
37 isFixed = nil
38 /* Described in the room, or passed over as part of the furniture. */
39 /** @api author
40 * Mark scenery that is normally omitted from the room listing. */
41 isDecoration = nil
42
43 /** @api author
44 * Description emitted when the thing is examined. */
45 desc = nil
46 /* Gives light when it is lit. A candle is a light source unlit. */
47 /** @api author
48 * Whether this thing can provide light when lit. */
49 isLightSource = nil
50 /** @api author
51 * Current light state. */
52 isLit = nil
53
54 /** @api author
55 * Return a candidate rank for this thing in the given action and role. */
56 rank(a, role) { return isDecoration ? rankUnlikely : rankLogical; }
57
58 cannotTakeMsg = nil
59 cannotOpenMsg = nil
60
61 /** @api author
62 * Space this thing occupies in a container. */
63 bulk = 1
64 /** @api author
65 * Total bulk this thing can hold when used as a container. */
66 bulkCapacity = 0
67 /* Whether what is inside is mentioned when the room is described. */
68 /** @api author
69 * Whether contents appear in a room description. */
70 contentsListed = true
71 /* The largest single thing that fits, whatever room is left. */
72 /** @api author
73 * Maximum bulk of one item accepted by this container. */
74 maxSingleBulk = 0
75 /* Seen into while shut. */
76 /** @api author
77 * Allow sight into this thing while closed. */
78 isTransparent = nil
79
80 isAttachable = nil
81 /** @api author
82 * Whether locking actions apply to this thing. */
83 isLockable = nil
84 /** @api author
85 * Current locked state. */
86 isLocked = nil
87 /*
88 * Being worn. On Thing rather than on Wearable for the same reason locking
89 * is: a precondition has to be able to ask it of anything it is given.
90 */
91 /** @api author
92 * Current worn state used by clothing actions and preconditions. */
93 isWorn = nil
94 /* What unlocks it. nil on a lockable thing means nothing does. */
95 /** @api author
96 * Item that unlocks this thing, or nil when no key does. */
97 keyItem = nil
98
99 isDistant = nil
100 /* Whether subjects may be looked up in it; see Consultable. */
101 isConsultable = nil
102 /* Seen and never touched; see Intangible. */
103 isIntangible = nil
104
105 isOutOfReach = nil
106 canObjReachSelf(who) { return nil; }
107
108 specialDesc = nil
109 hasSpecialDesc = nil
110
111 allowedPostures = [standing]
112 defaultPosture = standing
113
114 roomBeforeAction(c) { return nil; }
115
116 seen = nil
117
118 isFamiliar = nil
119
120 /** @api author
121 * Test whether this knower knows an entity or fact tag. */
122 knowsAbout(obj)
123 {
124 if (obj == nil)
125 return nil;
126
127 if (dataType(obj) == TypeSString)
128 return holds(informedOf, obj);
129 return obj.isFamiliar || knows.contains(self, obj);
130 }
131
132 /** @api author
133 * Record knowledge of an entity or fact tag for this knower. */
134 setKnowsAbout(obj)
135 {
136 if (obj == nil)
137 return nil;
138 if (dataType(obj) == TypeSString)
139 {
140 if (!holds(informedOf, obj))
141 informedOf = informedOf + [obj];
142 }
143 else if (!knows.contains(self, obj))
144 knows.set(self, obj);
145 return nil;
146 }
147 /* Forget a fact tag. Things are not forgotten; a fact can be superseded. */
148 /** @api author
149 * Remove a fact tag from this knower; entity knowledge remains. */
150 forget(tag)
151 {
152 local kept = [];
153 foreach (local t in informedOf)
154 {
155 if (t != tag)
156 kept = kept + [t];
157 }
158 informedOf = kept;
159 return nil;
160 }
161
162 informedOf = []
163
164 /** @api author
165 * Subjects taught to this thing during vhylStart. */
166 initiallyKnowsAbout = nil
167
168 known = (player.knowsAbout(self))
169 /*
170 * A name that takes no article: a person, a place, a ship. vhyl had no such
171 * notion, so a library message about an actor said *the Zoé*, and one about
172 * the player said *the you*. What an article is doing there is the language
173 * module's business; **whether the name takes one** is the world's.
174 */
175 /** @api author
176 * Use a proper name without an article in English presentation. */
177 isProper = nil
178
179 isPlural = nil
180 isMassNoun = nil
181
182 isHim = nil
183 isHer = nil
184 isGenderNeutral = nil
185
186 isIt = (!(isHim || isHer || isGenderNeutral))
187
188 pcReferralPerson = SecondPerson
189
190 bulkLimit = 0
191
192 isEdible = nil
193 isDrinkable = nil
194 fluidName = nil
195 /* Whether this will take something poured into it. */
196 allowPourIntoMe = nil
197 /*
198 * Where a thing put here comes to rest. An enumerator, not a preposition:
199 * the word for it belongs to the language module.
200 */
201 /** @api author
202 * Position assigned to an item inserted here: in, on, under or behind. */
203 objPos = posIn
204
205 remapIn = nil
206 remapOn = nil
207 remapBehind = nil
208
209 remapUnder = nil
210 /* Out of sight until someone finds it. */
211 /** @api author
212 * Omit this thing from sight until revealed by an action. */
213 isHidden = nil
214
215 /** @api author
216 * Observe an item being inserted after placement changes. */
217 notifyInsert(item) { return nil; }
218
219 /** @api author
220 * Return nil to allow an action or text explaining a refusal. */
221 refuses(action, c) { return nil; }
222
223 /** @api author
224 * Return preconditions for this object in the action and role. */
225 preCond(a, role) { return []; }
226
227 /** @api author
228 * React before the traveller uses a connector. */
229 beforeTravel(traveller, conn) { return nil; }
230 /** @api author
231 * React after a traveller uses a connector. */
232 afterTravel(traveller, conn) { return nil; }
233 ;
234
235 /** @api author
236 * A lit-by-default location whose exits and contents are relation rows. */
237 class Room: Thing
238 isFixed = true
239 /* Lit by itself. A DarkRoom is not, and needs something carried. */
240 isLit = true
241 ;
242
243 /* Somewhere the player cannot see without a light. */
244 /** @api author
245 * A room that requires another light source to be visible. */
246 class DarkRoom: Room
247 isLit = nil
248 ;
249
250 /* Outdoors: lit by the sky rather than by anything carried. */
251 /** @api author
252 * A room lit by the sky under the default world policy. */
253 class OutdoorRoom: Room
254 isLit = true
255 ;
256
257 /* Templates, so a declaration reads as a sentence. */
258 Thing template'name';
259 Room template'name';
260 DarkRoom template'name';
261 OutdoorRoom template'name';
262
263 /*
264 * The player. `vocab` so they can be referred to at all: without it `X ME`
265 * answered *I don't understand that* in every game ever written against vhyl,
266 * and looking at yourself is the first thing a good many players do.
267 */
268 /** @api author
269 * The library player entity; place it by setting gameMain.initialRoom. */
270 player: Thing'you'
271 travelCycle = -1
272 vocab ='me; my; myself self you'
273 ;
274
275 /** @api author
276 * Per-game startup settings supplied by a modify gameMain declaration. */
277 gameMain: object
278 /** @api author
279 * Room where the player is placed by vhylStart. */
280 initialRoom = nil
281 /* Displayed once at the start. An emitting string, like desc. */
282 /** @api author
283 * Text emitted once before the first room description. */
284 intro =""
285
286 /** @api author
287 * Select past tense for English presentation. */
288 usePastTense = nil
289 ;
290
291 /** @api host
292 * Initialise the world once, place the player and describe the initial room. */
293 vhylStart()
294 {
295 /* An emitting property displays when it is read; interpolating it would
296 * print it and then interpolate the nil it answers. */
297 gameMain.intro;
298 "\n\n";
299 if (gameMain.initialRoom == nil)
300 "vhyl: no initial room; set `modify gameMain initialRoom = ...`.\n";
301 else
302 {
303 contains.set(gameMain.initialRoom, player);
304
305 for (local a = firstObj(Actor); a != nil; a = nextObj(a, Actor))
306 {
307 if (a.curState != nil)
308 continue;
309 local first = nil;
310 for (local st = firstObj(ActorState); st != nil; st = nextObj(st, ActorState))
311 {
312 if (st.actor != a)
313 continue;
314 if (st.isInitState)
315 {
316 first = st;
317 break;
318 }
319 if (first == nil)
320 first = st;
321 }
322 if (first != nil)
323 a.setCurState(first);
324 }
325
326 for (local t = firstObj(Thing); t != nil; t = nextObj(t, Thing))
327 {
328 if (t.initiallyKnowsAbout == nil)
329 continue;
330 foreach (local subject in t.initiallyKnowsAbout)
331 t.setKnowsAbout(subject);
332 }
333
334 for (local i = firstObj(InitObject); i != nil; i = nextObj(i, InitObject))
335 i.execute();
336
337 syncStates();
338 describeRoom();
339 }
340 return nil;
341 }
342
343 /* ------------------------------------------------------------- directions */
344
345 allDirections: object
346 ways = [north, south, east, west,
347 northeast, northwest, southeast, southwest,
348 up, down, inward, outward]
349 ;
350
351 oppositeOf(way)
352 {
353 if (way == north) return south;
354 if (way == south) return north;
355 if (way == east) return west;
356 if (way == west) return east;
357 if (way == northeast) return southwest;
358 if (way == southwest) return northeast;
359 if (way == northwest) return southeast;
360 if (way == southeast) return northwest;
361 if (way == up) return down;
362 if (way == down) return up;
363 if (way == inward) return outward;
364 if (way == outward) return inward;
365 return nil;
366 }
367
368 /* Where this exit ends up, following a connector if there is one. */
369 destinationOf(from, way)
370 {
371 local target = exits.get(from, way);
372 if (target == nil || target.isHidden)
373 return nil;
374 if (!target.ofKind(TravelConnector))
375 return target;
376 if (target.ofKind(Door))
377 return doorDestination(target, from);
378 return target.destination;
379 }
380
381 wayFrom(from, to)
382 {
383 if (from == nil || to == nil)
384 return nil;
385 foreach (local way in allDirections.ways)
386 {
387 if (destinationOf(from, way) == to)
388 return way;
389 }
390 return nil;
391 }
392
393 /* ---------------------------------------------------------------- queries */
394
395 /** @api author
396 * Return the room containing the player. */
397 here()
398 {
399 return contains.outermost(player);
400 }
401
402 litBy(room)
403 {
404 foreach (local item in contains.all(room))
405 {
406 if (item.isLightSource && item.isLit)
407 return true;
408 /*
409 * Light gets out of anything that is not shut: an open box, a pocket,
410 * and the player, who is not a container but is carrying the torch.
411 * Containment is one table, so the walk does not care which it is.
412 */
413 if (item.isContainer && item.isOpenable && !item.isOpen)
414 continue;
415 if (litBy(item))
416 return true;
417 }
418 return nil;
419 }
420
421 /* A room you can see in: lit itself, or by something in it or carried. */
422 isDark()
423 {
424 local room = here();
425 if (room.isLit)
426 return nil;
427 return !litBy(room);
428 }
429
430 /*
431 * Visible means reachable without opening anything: every container between the
432 * item and the room has to be open. One walk up the containment table.
433 */
434 visible(item)
435 {
436 local room = here();
437 foreach (local step in contains.ancestors(item))
438 {
439 if (step == room || step == player)
440 return true;
441
442 if (step.isContainer && step.isOpenable && !step.isOpen
443 && !step.isTransparent)
444 return nil;
445 }
446 return nil;
447 }
448
449 inScope(item)
450 {
451 /*
452 * The player is in scope, and used not to be — which meant `X ME` answered
453 * *I don't see that* in every game ever written against vhyl, and looking
454 * at yourself is among the first things a good many players do.
455 *
456 * Being in scope is not the same as being a likely candidate: the player
457 * ranks itself down for everything but examining (below), so a word that
458 * names both you and something else still means the something else.
459 */
460 if (item == player)
461 return true;
462
463 if (item.isHidden)
464 return nil;
465 /* In the dark you can still reach what you are carrying, and nothing else. */
466 if (isDark())
467 return carried(item);
468 /*
469 * A nested space may confine you. Sitting in a booth whose reachOut is nil,
470 * you can touch what is in the booth and what you are carrying and nothing
471 * else — which is what nested rooms are for.
472 */
473 local nest = nestedIn();
474 if (nest != nil && !nest.reachOut && item != nest)
475 return carried(item) || holds(contains.all(nest), item);
476 /* A door stands in both the rooms it joins, so it is in scope from either. */
477 if (item.ofKind(TravelConnector))
478 return connectorHere(item) || visible(item);
479 /* A MultiLoc is in scope in any room it is present in. */
480 if (item.isMultiLoc)
481 return presentIn.contains(item, here());
482
483 if (item.isDistant)
484 return nil;
485 return visible(item);
486 }
487
488 carried(item)
489 {
490 return location.get(item) == player;
491 }
492
493 /* ------------------------------------------------------------ description */
494
495 /*
496 * Whether what is inside can be seen. Open, or not shuttable, or shut but
497 * transparent — a glass jar with the lid on shows what is in it.
498 */
499 seeInto(item)
500 {
501 if (!item.isOpenable)
502 return true;
503 return item.isOpen || item.isTransparent;
504 }
505
506 listContents(room)
507 {
508 foreach (local item in contains.all(room))
509 {
510 /*
511 * Fixtures belong to the room's own description, not to the list of
512 * what is lying about in it. A desk is part of the study; a trolley is
513 * something someone left there.
514 */
515
516 if (item.isHidden)
517 continue;
518
519 item.seen = true;
520 player.setKnowsAbout(item);
521 if (item == player || item.isDecoration || item.isFixed)
522 continue;
523 sayFor('room.thereis', item, nil);
524 if (item.isContainer && item.isOpenable && !item.isOpen && !item.isTransparent)
525 sayFor('room.andclosed', item, nil);
526 else if (item.isContainer && item.contentsListed && seeInto(item))
527 {
528 local inside = [for held in contains.all(item) : held if !held.isHidden];
529 if (inside.length() > 0)
530 {
531 noteSeen(inside);
532 sayAbout('room.andholding', item, inside, nil);
533 }
534 }
535 sayMsg('room.endofitem', nil);
536 }
537
538 foreach (local item in roomSpecials(room))
539 {
540 local state = item.isActor ? item.curState : nil;
541 if (state != nil && state.hasSpecialDesc)
542 {
543 "\n";
544 state.specialDesc;
545 }
546 else if (item.hasSpecialDesc)
547 {
548 "\n";
549 item.specialDesc;
550 }
551 else if (item.isActor)
552 {
553 "\n";
554 sayFor('room.actorhere', item, nil);
555 }
556 }
557 return nil;
558 }
559
560 roomSpecials(room)
561 {
562 local found = [];
563 foreach (local item in contains.all(room))
564 {
565 if (item.isHidden || item == player)
566 continue;
567 if (item.isActor || item.hasSpecialDesc)
568 found = found + [item];
569 /*
570 * A glass booth, an open crate, a chain hanging off a gatepost: what
571 * is inside is plainly there. `seeInto` is the whole test — a thing
572 * that does not shut never hides what it holds — and descending only
573 * matters for something that asked for a line, so nothing is listed
574 * that did not opt in.
575 */
576 if (!seeInto(item))
577 continue;
578 foreach (local inside in contains.all(item))
579 {
580 if (inside.isHidden || inside == player)
581 continue;
582 if (inside.isActor || inside.hasSpecialDesc)
583 found = found + [inside];
584 }
585 }
586 return found;
587 }
588
589 describeRoom()
590 {
591 local room = here();
592
593 sayFor('room.enter', room, nil);
594 if (isDark())
595 {
596 sayMsg('room.dark', nil);
597 "\n\n";
598 return nil;
599 }
600
601 sayFor('room.name', room, nil);
602 "\n";
603 room.seen = true;
604 player.setKnowsAbout(room);
605 room.desc;
606 "\n";
607 listContents(room);
608 listSensed(room);
609 "\n";
610 return nil;
611 }
612
613 /* --------------------------------------------------------------- messages */
614
615 /** @api author
616 * A message keyed by stable id, with text or a computed say method. */
617 class Message: object
618 /** @api author
619 * Stable key used by actions and host presentation. */
620 id = nil
621
622 /** @api author
623 * Literal presentation text for this message. */
624 text = nil
625 /** @api author
626 * Emit a computed message for the supplied command. */
627 say(c) { return nil; }
628 /* Higher wins. A game's overrides sit above vhyl's, which are all 0. */
629 /** @api author
630 * Higher-priority active messages replace lower-priority messages with the same id. */
631 priority = 0
632 /* Answered nil to stand aside: a message set that only applies sometimes. */
633 /** @api author
634 * Observational condition controlling whether this message can be selected. */
635 isActive = true
636 ;
637
638 msgArgs: Command;
639
640 /* Say an id about one or two objects, with no command in hand. */
641 sayFor(id, a, b)
642 {
643 msgArgs.dobj = a;
644 msgArgs.iobj = b;
645 msgArgs.list_ = nil;
646 msgArgs.way_ = nil;
647 return sayMsg(id, msgArgs);
648 }
649
650 account(id, c)
651 {
652 return accountAbout(id, c, nil);
653 }
654
655 accountAbout(id, c, word)
656 {
657 return accountAs(2, id, c, word);
658 }
659
660 accountDefault(id, c)
661 {
662 return accountAs(4, id, c, nil);
663 }
664
665 /* One account, at a given place in the report order. */
666 accountAs(where, id, c, word)
667 {
668 local done = c.done_;
669 if (done == nil || done.length() == 0)
670 return nil;
671
672 if (done.length() > 1 && messageFor(id +'.several') != nil)
673 id = id +'.several';
674 local r = addReport(c, where, id, done[1], c.iobj);
675 if (r != nil)
676 {
677 r.list = done;
678 r.word = word;
679 }
680 return nil;
681 }
682
683 noteDone(c, item)
684 {
685 c.done_ = c.done_ == nil ? [item] : c.done_ + [item];
686 return nil;
687 }
688
689 /*
690 * The text of a message about one or two objects, **read** rather than said.
691 *
692 * `refuses` answers a sentence for the turn to print, so it needs the words
693 * rather than the side effect. A message with no text — one whose body emits —
694 * has nothing to read, and answers nil so the caller falls back to whatever it
695 * would have said anyway.
696 */
697 msgFor(id, a, b)
698 {
699 local m = messageFor(id);
700 if (m == nil || m.text == nil)
701 return nil;
702 msgArgs.dobj = a;
703 msgArgs.iobj = b;
704 msgArgs.list_ = nil;
705 msgArgs.way_ = nil;
706 return language.expand(m.text, msgArgs);
707 }
708
709 /* Say an id about an object and a list of things, or a word. */
710 sayAbout(id, a, list, word)
711 {
712 msgArgs.dobj = a;
713 msgArgs.iobj = nil;
714 msgArgs.list_ = list;
715 msgArgs.way_ = word;
716 return sayMsg(id, msgArgs);
717 }
718
719 /* The message that answers to this id, or nil. */
720 messageFor(id)
721 {
722 local best = nil;
723 for (local m = firstObj(Message); m != nil; m = nextObj(m, Message))
724 {
725 if (m.id != id || !m.isActive)
726 continue;
727 if (best == nil || m.priority > best.priority)
728 best = m;
729 }
730 return best;
731 }
732
733 /*
734 * Say what this id means. `c` is the command, so a message that wants the
735 * objects has them; one that does not ignores it.
736 *
737 * An unknown id prints the id itself rather than nothing, because a message
738 * that silently says nothing is the hardest kind to find.
739 */
740 sayMsg(id, c)
741 {
742
743 event(id);
744 if (c != nil)
745 {
746
747 local named = [];
748 if (c.dobj != nil)
749 named = named + [c.dobj];
750 if (c.iobj != nil && !holds(named, c.iobj))
751 named = named + [c.iobj];
752 if (c.list_ != nil)
753 {
754 foreach (local item in c.list_)
755 {
756 if (!holds(named, item))
757 named = named + [item];
758 }
759 }
760 foreach (local item in named)
761 eventSubject(item);
762 }
763 local m = messageFor(id);
764 if (m == nil)
765 {
766 "[no message: <<id>>]\n";
767 return nil;
768 }
769 if (m.text != nil)
770 "<<language.expand(m.text, c)>>";
771 else
772 m.say(c);
773 return nil;
774 }
775
776 language: object
777 expand(text, c) { return text; }
778 ;
779
780 /*
781 * The player is the last thing a command means when it could mean anything
782 * else, and is not something to be picked up. `isFixed` is what refuses TAKE ME
783 * with a sentence rather than with a silence.
784 */
785 modify player
786 isFixed = true
787 rank(a, role) { return a == examineAction ? rankLogical : rankSelf; }
788 ;
789
790 /* ------------------------------------------------------------ verb codes */
791
792 /** @api host
793 * Maps a stable structured action code to an Action and optional direction. */
794 class Verb: object
795 /* What the host sends. Stable; a code is never reused for something else. */
796 /** @api host
797 * Stable numeric code submitted by a structured-action host. */
798 code = 0
799 /* The action it runs. */
800 /** @api author
801 * Action executed for this verb code. */
802 action = nil
803 /* A direction, for the travel verbs. */
804 /** @api author
805 * Direction supplied to the action for travel verb codes. */
806 dir = nil
807 /* For the export. Not shown to a player: it is a key, not a word. */
808 /** @api host
809 * Stable key exported for host use; it is not player-visible prose. */
810 label = nil
811
812 verbPhrase = nil
813
814 preposition()
815 {
816 local phrase = verbPhrase;
817 if (phrase == nil)
818 return nil;
819 local open = phrase.find('(');
820 if (open == nil)
821 return nil;
822 local rest = phrase.substr(open + 1);
823 /* The **first** group only, and not past its closing bracket: an
824 * `unlock/unlocking (what) (with what)` has a preposition in its
825 * second group and none in its first, and reading past the bracket
826 * printed *(first unlocking what) the locker)*. */
827 local close = rest.find(')');
828 if (close != nil)
829 rest = rest.substr(1, close - 1);
830 local space = rest.find(' ');
831 if (space == nil)
832 return nil;
833 local word = rest.substr(1, space - 1);
834 return word is in ('what','whom') ? nil : word;
835 }
836 /* The gerund alone: 'taking' from 'take/taking (what)'. */
837 gerund()
838 {
839 local phrase = verbPhrase;
840 if (phrase == nil)
841 return label == nil ?'' : label +'ing';
842 local slash = phrase.find('/');
843 if (slash == nil)
844 return phrase;
845 local rest = phrase.substr(slash + 1);
846 local space = rest.find(' ');
847 return space == nil ? rest : rest.substr(1, space - 1);
848 }
849 ;
850
851 /* The verb this code names, or nil. */
852 verbFor(code)
853 {
854 for (local v = firstObj(Verb); v != nil; v = nextObj(v, Verb))
855 {
856 if (v.code == code)
857 return v;
858 }
859 return nil;
860 }
861
862 /* Nothing to say (1-9). */
863 vLook: Verb code = 1 action = lookAction label ='look' verbPhrase ='look/looking around';
864 vInventory: Verb code = 2 action = inventoryAction label ='inventory' verbPhrase ='take/taking inventory';
865 vUndo: Verb code = 3 action = undoAction label ='undo' verbPhrase ='undo/undoing';
866 vScore: Verb code = 4 action = scoreAction label ='score' verbPhrase ='check/checking (the score)';
867 vGetOff: Verb code = 5 action = getOffAction label ='get-off' verbPhrase ='get/getting off';
868 vWait: Verb code = 6 action = waitAction label ='wait' verbPhrase ='wait/waiting';
869 vStand: Verb code = 7 action = standAction label ='stand' verbPhrase ='stand/standing up';
870 vSit: Verb code = 8 action = sitAction label ='sit' verbPhrase ='sit/sitting down';
871 vLie: Verb code = 9 action = lieAction label ='lie' verbPhrase ='lie/lying down';
872 vStandOn: Verb code = 43 action = standOnAction label ='stand-on' verbPhrase ='stand/standing (on what)';
873 vSitOn: Verb code = 44 action = sitOnAction label ='sit-on' verbPhrase ='sit/sitting (on what)';
874 vLieOn: Verb code = 45 action = lieOnAction label ='lie-on' verbPhrase ='lie/lying (on what)';
875 vSay: Verb code = 46 action = sayAction label ='say' verbPhrase ='say/saying (what)';
876 vWriteOn: Verb code = 47 action = writeOnAction label ='write-on' verbPhrase ='write/writing (what) (on what)';
877 vThinkAbout: Verb code = 48 action = thinkAboutAction label ='think-about' verbPhrase ='think/thinking (about what)';
878 vConsult: Verb code = 49 action = consultAction label ='consult' verbPhrase ='look/looking up (what) (in what)';
879 vGive: Verb code = 58 action = giveAction label ='give-to' verbPhrase ='give/giving (what) (to whom)';
880 vShow: Verb code = 59 action = showAction label ='show-to' verbPhrase ='show/showing (what) (to whom)';
881 vUnlock: Verb code = 41 action = unlockAction label ='unlock' verbPhrase ='unlock/unlocking (what) (with what)';
882 vLock: Verb code = 42 action = lockAction label ='lock' verbPhrase ='lock/locking (what) (with what)';
883
884 vNorth: Verb code = 10 action = goAction dir = north label ='go-north' verbPhrase ='go/going north';
885 vSouth: Verb code = 11 action = goAction dir = south label ='go-south' verbPhrase ='go/going south';
886 vEast: Verb code = 12 action = goAction dir = east label ='go-east' verbPhrase ='go/going east';
887 vWest: Verb code = 13 action = goAction dir = west label ='go-west' verbPhrase ='go/going west';
888 vUp: Verb code = 14 action = goAction dir = up label ='go-up' verbPhrase ='go/going up';
889 vDown: Verb code = 15 action = goAction dir = down label ='go-down' verbPhrase ='go/going down';
890 vIn: Verb code = 16 action = goAction dir = inward label ='go-in' verbPhrase ='go/going in';
891 vOut: Verb code = 17 action = goAction dir = outward label ='go-out' verbPhrase ='go/going out';
892
893 /* One subject (20-49). */
894 vExamine: Verb code = 20 action = examineAction label ='examine' verbPhrase ='examine/examining (what)';
895 vTake: Verb code = 21 action = takeAction label ='take' verbPhrase ='take/taking (what)';
896 vDrop: Verb code = 22 action = dropAction label ='drop' verbPhrase ='drop/dropping (what)';
897 vOpen: Verb code = 23 action = openAction label ='open' verbPhrase ='open/opening (what)';
898 vClose: Verb code = 24 action = closeAction label ='close' verbPhrase ='close/closing (what)';
899 vTurn: Verb code = 25 action = turnAction label ='turn' verbPhrase ='turn/turning (what)';
900 vTurnOn: Verb code = 26 action = turnOnAction label ='turn-on' verbPhrase ='turn on (what)';
901 vTurnOff: Verb code = 27 action = turnOffAction label ='turn-off' verbPhrase ='turn off (what)';
902 vFlip: Verb code = 28 action = flipAction label ='flip' verbPhrase ='flip/flipping (what)';
903 vPress: Verb code = 29 action = pressAction label ='press' verbPhrase ='press/pressing (what)';
904 vBoard: Verb code = 30 action = boardAction label ='board' verbPhrase ='board/boarding (what)';
905 vEnter: Verb code = 31 action = enterAction label ='enter' verbPhrase ='enter/entering (what)';
906 vWear: Verb code = 32 action = wearAction label ='wear' verbPhrase ='wear/wearing (what)';
907 vDoff: Verb code = 33 action = doffAction label ='doff' verbPhrase ='take off (what)';
908 vLookUnder: Verb code = 34 action = lookUnderAction label ='look-under' verbPhrase ='look/looking (under what)';
909 vLookBehind: Verb code = 35 action = lookBehindAction label ='look-behind' verbPhrase ='look/looking (behind what)';
910 vListen: Verb code = 36 action = listenAction label ='listen' verbPhrase ='listen/listening (to what)';
911 vSmell: Verb code = 37 action = smellAction label ='smell' verbPhrase ='smell/smelling (what)';
912
913 /* Two subjects (50-59). */
914 vPutOn: Verb code = 50 action = putOnAction label ='put-on' verbPhrase ='put/putting (what) (on what)';
915 vPutIn: Verb code = 51 action = putInAction label ='put-in' verbPhrase ='put/putting (what) (in what)';
916 vPutBehind: Verb code = 52 action = putBehindAction label ='put-behind' verbPhrase ='put/putting (what) (behind what)';
917 vAttach: Verb code = 53 action = attachAction label ='attach' verbPhrase ='attach/attaching (what) (to what)';
918 vDetach: Verb code = 54 action = detachAction label ='detach' verbPhrase ='detach/detaching (what) (from what)';
919 vAskAbout: Verb code = 55 action = askAboutAction label ='ask-about' verbPhrase ='ask/asking (whom) (about what)';
920 vTellAbout: Verb code = 56 action = tellAboutAction label ='tell-about' verbPhrase ='tell/telling (whom) (about what)';
921
922 /* A subject and a direction (60-69). */
923 vEat: Verb code = 38 action = eatAction label ='eat' verbPhrase ='eat/eating (what)';
924 vDrink: Verb code = 39 action = drinkAction label ='drink' verbPhrase ='drink/drinking (what)';
925 vDetachPart: Verb code = 40 action = detachPartAction label ='detach-part' verbPhrase ='detach/detaching (what)';
926 vPourInto: Verb code = 57 action = pourIntoAction label ='pour-into' verbPhrase ='pour/pouring (what) (into what)';
927
928 vPushNorth: Verb code = 60 action = pushDirAction dir = north label ='push-north' verbPhrase ='push/pushing (what) north';
929 vPushSouth: Verb code = 61 action = pushDirAction dir = south label ='push-south' verbPhrase ='push/pushing (what) south';
930 vPushEast: Verb code = 62 action = pushDirAction dir = east label ='push-east' verbPhrase ='push/pushing (what) east';
931 vPushWest: Verb code = 63 action = pushDirAction dir = west label ='push-west' verbPhrase ='push/pushing (what) west';
932
933 /* ---------------------------------------------------------------- actions */
934
935 /** @api author
936 * Defines argument requirements and the execution and reporting hooks for a command. */
937 class Action: object
938 /* Whether the turn resolves a direct and an indirect object first. */
939 /** @api author
940 * Whether the command requires a direct entity argument. */
941 needsDobj = nil
942 /** @api author
943 * Whether the command requires an indirect entity argument. */
944 needsIobj = nil
945
946 /** @api author
947 * Treat the indirect argument as a topic instead of an ordinary thing. */
948 iobjIsTopic = nil
949
950 /** @api author
951 * Treat the direct argument as a topic instead of an ordinary thing. */
952 dobjIsTopic = nil
953
954 /** @api author
955 * Message id used when the required direct argument is absent. */
956 missingDobjMsg ='parse.noref'
957 /** @api author
958 * Message id used when the required indirect argument is absent. */
959 missingIobjMsg ='parse.noref'
960 /*
961 * What the action does. `c` is the command, carrying the resolved dobj and
962 * iobj and whatever else the rule captured, so an action that wants a
963 * direction or a word reads it there.
964 */
965 /** @api author
966 * Perform the action after resolution and preconditions; c holds resolved objects. */
967 exec(c) { sayMsg('action.nothing', c);"\n"; return nil; }
968
969 /** @api author
970 * Return an action-specific candidate rank for an argument and role. */
971 rank(item, role) { return rankLogical; }
972
973 /** @api author
974 * Require the selected object to be reachable for this action. */
975 needsReach = true
976
977 /** @api author
978 * Return the preconditions needed for an argument in its resolved role. */
979 preCond(item, role) { return []; }
980
981 /** @api author
982 * Produce the action's report after successful execution. */
983 report(c) { return nil; }
984 ;
985
986 /* The command a rule produces, and what the turn resolves into it. */
987 /** @api author
988 * Carries parser captures, resolved arguments and reports through an action. */
989 class Command: object
990 action = nil
991
992 set_ = nil
993
994 dobj_ = nil
995 iobj_ = nil
996 /* What the turn resolved them to. */
997 /** @api author
998 * Resolved direct object, available in action hooks. */
999 dobj = nil
1000 /** @api author
1001 * Resolved indirect object, available in action hooks. */
1002 iobj = nil
1003
1004 way_ = nil
1005
1006 dir_ = nil
1007 /* Things a message has to name several of. */
1008 list_ = nil
1009
1010 done_ = nil
1011 /* What the command still has to say, in the order it will say it. */
1012 reports_ = nil
1013
1014 failed_ = nil
1015
1016 implied_ = nil
1017
1018 actorPhrase_ = nil
1019 inner_ = nil
1020 /* A literal phrase the rule captured, as words rather than a name. */
1021 literal_ = nil
1022
1023 topicText_ = nil
1024 ;
1025
1026 /* ------------------------------------------------------------- reporting */
1027
1028 class Report: object
1029
1030 place = 2
1031 id = nil
1032 dobj = nil
1033 iobj = nil
1034 list = nil
1035
1036 reveals = nil
1037 word = nil
1038 construct(where, what, a, b) { place = where; id = what; dobj = a; iobj = b; }
1039 ;
1040
1041 /* A displayed discovery is world knowledge, including for a host using only
1042 * semantic events. Do not infer this from arbitrary message subjects. */
1043 noteSeen(items)
1044 {
1045 foreach (local item in items)
1046 {
1047 item.seen = true;
1048 player.setKnowsAbout(item);
1049 }
1050 return nil;
1051 }
1052
1053 /* Put a report on the command's list. */
1054 addReport(c, where, id, a, b)
1055 {
1056 if (c == nil || id == nil)
1057 return nil;
1058 local r = new Report(where, id, a, b);
1059 c.reports_ = c.reports_ == nil ? [r] : c.reports_ + [r];
1060 return r;
1061 }
1062
1063 /* The main result of the action. There may be several. */
1064 mainReport(c, id, a, b) { return addReport(c, 2, id, a, b); }
1065
1066 reportBefore(c, id, a, b) { return addReport(c, 1, id, a, b); }
1067 /* Moved after every main report, however early it is added. */
1068 reportAfter(c, id, a, b) { return addReport(c, 3, id, a, b); }
1069 /*
1070 * A main report that also marks the command a failure, so a caller that ran it
1071 * as an implied action knows not to go on.
1072 */
1073 reportFailure(c, id, a, b)
1074 {
1075 if (c != nil)
1076 c.failed_ = true;
1077 return addReport(c, 2, id, a, b);
1078 }
1079
1080 defaultReport(c, id, a, b) { return addReport(c, 4, id, a, b); }
1081 /*
1082 * The same, for a description: *You see nothing unusual.* Dropped when
1083 * anything else is said, but **kept** for an implied command, because a
1084 * description is an answer rather than an acknowledgement.
1085 */
1086 defaultDescReport(c, id, a, b) { return addReport(c, 5, id, a, b); }
1087 /* An extra line that does not suppress a default. */
1088 extraReport(c, id, a, b) { return addReport(c, 6, id, a, b); }
1089
1090 sayReports(c)
1091 {
1092 local list = c.reports_;
1093 c.reports_ = nil;
1094 if (list == nil || list.length() == 0)
1095 return nil;
1096 local real = nil;
1097 foreach (local r in list)
1098 {
1099 if (r.place <= 3)
1100 real = true;
1101 }
1102 foreach (local where in [1, 2, 4, 5, 3, 6])
1103 {
1104 if (real && (where == 4 || where == 5))
1105 continue;
1106 if (c.implied_ && where == 4)
1107 continue;
1108 foreach (local r in list)
1109 {
1110 if (r.place != where)
1111 continue;
1112 msgArgs.dobj = r.dobj;
1113 msgArgs.iobj = r.iobj;
1114 msgArgs.list_ = r.list;
1115 msgArgs.way_ = r.word;
1116 if (r.reveals != nil)
1117 noteSeen(r.reveals);
1118 sayMsg(r.id, msgArgs);
1119 "\n";
1120 }
1121 }
1122 return nil;
1123 }
1124
1125 lookAction: Action
1126 exec(c) { return describeRoom(); }
1127 ;
1128
1129 inventoryAction: Action
1130 exec(c) { return doInventory(); }
1131 ;
1132
1133 undoAction: Action
1134 exec(c)
1135 {
1136 if (undo())
1137 sayMsg('undo.ok', c);
1138 else
1139 sayMsg('undo.none', c);
1140 "\n";
1141 return nil;
1142 }
1143 ;
1144
1145 goAction: Action
1146 exec(c) { return doGo(c.dir_); }
1147 ;
1148
1149 getOffAction: Action
1150 exec(c) { return doGetOff(); }
1151 ;
1152
1153 /*
1154 * WAIT. Nothing happens except a turn passing, which is the point: a game with
1155 * daemons, fuses and actors needs a way to let them act. vhyl had no such verb
1156 * at all, so every program that wanted one declared it — which is the sort of
1157 * thing a library exists to stop.
1158 */
1159 /* ------------------------------------------------------------- postures */
1160
1161 takePosture(c, item, want)
1162 {
1163 local where = item == nil ? nestedIn() : item;
1164 /*
1165 * STAND with nothing named, while already standing: say so. Without this
1166 * a player standing on the table who typed STAND climbed the table again.
1167 */
1168 if (item == nil && player.posture == want)
1169 {
1170 sayMsg('posture.already', c);
1171 "\n";
1172 return nil;
1173 }
1174 /* STAND with nothing named, and nothing to stand on: get up. */
1175 if (where == nil)
1176 {
1177 if (want == standing && player.posture == standing)
1178 {
1179 sayMsg('posture.already', c);
1180 "\n";
1181 return nil;
1182 }
1183 if (want != standing)
1184 {
1185 sayMsg('posture.nowhere', c);
1186 "\n";
1187 return nil;
1188 }
1189 player.posture = standing;
1190 noteDone(c, player);
1191 return nil;
1192 }
1193 if (!inScope(where))
1194 {
1195 sayFor('posture.cannot', where, nil);
1196 "\n";
1197 return nil;
1198 }
1199 if (!where.isEnterable && !where.isVehicle)
1200 {
1201 sayFor('posture.cannot', where, nil);
1202 "\n";
1203 return nil;
1204 }
1205 if (!holds(where.allowedPostures, want))
1206 {
1207 sayFor('posture.wontallow', where, nil);
1208 "\n";
1209 return nil;
1210 }
1211 /* Get on it first, if we are not on it already. */
1212 if (location.get(player) != where)
1213 contains.set(where, player);
1214 player.posture = want;
1215 noteDone(c, where);
1216 return nil;
1217 }
1218
1219 standAction: Action
1220 exec(c) { return takePosture(c, c.dobj, standing); }
1221 report(c) { return accountDefault('posture.stand', c); }
1222 ;
1223 sitAction: Action
1224 exec(c) { return takePosture(c, c.dobj, sitting); }
1225 report(c) { return accountDefault('posture.sit', c); }
1226 ;
1227 lieAction: Action
1228 exec(c) { return takePosture(c, c.dobj, lying); }
1229 report(c) { return accountDefault('posture.lie', c); }
1230 ;
1231 standOnAction: Action
1232 needsDobj = true
1233 exec(c) { return takePosture(c, c.dobj, standing); }
1234 report(c) { return accountDefault('posture.stand', c); }
1235 ;
1236 sitOnAction: Action
1237 needsDobj = true
1238 exec(c) { return takePosture(c, c.dobj, sitting); }
1239 report(c) { return accountDefault('posture.sit', c); }
1240 ;
1241 lieOnAction: Action
1242 needsDobj = true
1243 exec(c) { return takePosture(c, c.dobj, lying); }
1244 report(c) { return accountDefault('posture.lie', c); }
1245 ;
1246
1247 /* -------------------------------------------- verbs with words for objects */
1248
1249 sayAction: Action
1250 exec(c)
1251 {
1252 local said = getLiteral(c);
1253 if (said == nil)
1254 {
1255 sayMsg('say.nothing', c);
1256 "\n";
1257 return nil;
1258 }
1259 sayAbout('say.ok', nil, nil, said);
1260 "\n";
1261 return nil;
1262 }
1263 ;
1264
1265 writeOnAction: Action
1266 needsDobj = true
1267 preCond(item, role) { return role == dobjRole ? [touchObj] : []; }
1268 exec(c)
1269 {
1270 sayFor('writeon.cannot', c.dobj, nil);
1271 "\n";
1272 return nil;
1273 }
1274 ;
1275
1276 /* THINK ABOUT X: a mere reference to something, which need not be present. */
1277 thinkAboutAction: Action
1278 needsDobj = true
1279 dobjIsTopic = true
1280 needsReach = nil
1281 exec(c)
1282 {
1283 sayFor('think.about', c.dobj, nil);
1284 "\n";
1285 return nil;
1286 }
1287 ;
1288
1289 /*
1290 * LOOK UP X IN Y and CONSULT Y ABOUT X. The thing is the direct object and the
1291 * topic the indirect one, so a Consultable answers for itself.
1292 */
1293 consultAction: Action
1294 needsDobj = true
1295 needsIobj = true
1296 iobjIsTopic = true
1297 preCond(item, role) { return role == dobjRole ? [touchObj] : []; }
1298 exec(c)
1299 {
1300 local book = c.dobj;
1301 if (!book.isConsultable)
1302 {
1303 sayFor('consult.cannot', book, nil);
1304 "\n";
1305 return nil;
1306 }
1307 local entry = book.consult(c.iobj, getTopicText(c));
1308 if (entry == nil)
1309 {
1310 sayAbout('consult.nothing', book, nil, getTopicText(c));
1311 "\n";
1312 return nil;
1313 }
1314 "<<entry>>\n";
1315 return nil;
1316 }
1317 ;
1318
1319 waitAction: Action
1320 exec(c) { sayMsg('wait.ok', c);"\n"; return nil; }
1321 ;
1322
1323 examineAction: Action
1324 needsDobj = true
1325 /* Looking at something needs no more than seeing it. */
1326 needsReach = nil
1327 exec(c)
1328 {
1329 local item = c.dobj;
1330 item.seen = true;
1331 player.setKnowsAbout(item);
1332 /* Reading an emitting property displays it; there is nothing to test. */
1333 item.desc;
1334 "\n";
1335 /*
1336 * What is inside, when you can see inside. A glass jar shows its
1337 * contents with the lid on; a closed tin does not; and a RearContainer
1338 * says nothing, because what is behind it is out of sight until
1339 * someone looks there.
1340 */
1341 if (item.isContainer && item.contentsListed && seeInto(item))
1342 {
1343 local inside = [for held in contains.all(item) : held if !held.isHidden];
1344 if (inside.length() == 0)
1345 {
1346 sayFor('examine.empty', item, nil);
1347 "\n";
1348 }
1349 else
1350 {
1351 noteSeen(inside);
1352 sayAbout(item.objPos == posOn ?'examine.onit' :'examine.init',
1353 item, inside, nil);
1354 "\n";
1355 }
1356 }
1357 return nil;
1358 }
1359 ;
1360
1361 takeAction: Action
1362 preCond(item, role) { return role == dobjRole ? [touchObj] : []; }
1363 needsDobj = true
1364 /* What you are already holding is the one you did not mean, and what is
1365 * bolted down is not a candidate for taking at all. */
1366 rank(item, role)
1367 {
1368 if (item.isFixed)
1369 return rankNever;
1370 if (carried(item))
1371 return rankAlready;
1372 return rankLogical;
1373 }
1374 exec(c)
1375 {
1376 local item = c.dobj;
1377 if (item == player)
1378 {
1379 sayMsg('take.self', c);
1380 "\n";
1381 }
1382 else if (carried(item))
1383 {
1384 sayMsg('take.already', c);
1385 "\n";
1386 }
1387 else if (item.isFixed)
1388 {
1389 /* A thing may say why in its own words; otherwise the library's. */
1390 if (item.cannotTakeMsg != nil)
1391 "<<item.cannotTakeMsg>>\n";
1392 else
1393 {
1394 sayMsg('take.fixed', c);
1395 "\n";
1396 }
1397 }
1398 else
1399 {
1400 contains.set(player, item);
1401 noteDone(c, item);
1402 }
1403 return nil;
1404 }
1405 report(c) { return accountDefault('take.ok', c); }
1406 ;
1407
1408 dropAction: Action
1409 preCond(item, role) { return role == dobjRole ? [objHeld] : []; }
1410 needsDobj = true
1411 /* The mirror of take: only what you are holding can be dropped. */
1412 rank(item, role) { return carried(item) ? rankLogical : rankNotNow; }
1413 exec(c)
1414 {
1415 if (!carried(c.dobj))
1416 {
1417 sayMsg('drop.notheld', c);
1418 "\n";
1419 }
1420 else
1421 {
1422 contains.set(here(), c.dobj);
1423 noteDone(c, c.dobj);
1424 }
1425 return nil;
1426 }
1427 report(c) { return accountDefault('drop.ok', c); }
1428 ;
1429
1430 openAction: Action
1431 preCond(item, role) { return role == dobjRole ? [touchObj, objUnlocked] : []; }
1432 needsDobj = true
1433 rank(item, role)
1434 {
1435 if (!item.isOpenable)
1436 return rankNever;
1437 return item.isOpen ? rankAlready : rankLogical;
1438 }
1439 exec(c)
1440 {
1441 local item = c.dobj;
1442 if (!item.isOpenable)
1443 {
1444 if (item.cannotOpenMsg != nil)
1445 "<<item.cannotOpenMsg>>\n";
1446 else
1447 {
1448 sayMsg('open.cannot', c);
1449 "\n";
1450 }
1451 }
1452 else if (item.isLocked)
1453 {
1454 sayMsg('open.locked', c);
1455 "\n";
1456 }
1457 else if (item.isOpen)
1458 {
1459 sayMsg('open.already', c);
1460 "\n";
1461 }
1462 else
1463 {
1464 item.isOpen = true;
1465 noteDone(c, item);
1466
1467 if (item.isContainer && item.contentsListed)
1468 {
1469 local inside = [for held in contains.all(item) : held if !held.isHidden];
1470 if (inside.length() > 0)
1471 {
1472 local r = mainReport(c,'open.reveals', item, nil);
1473 if (r != nil)
1474 {
1475 r.list = inside;
1476 r.reveals = inside;
1477 }
1478 }
1479 }
1480 }
1481 return nil;
1482 }
1483 report(c) { return accountDefault('open.ok', c); }
1484 ;
1485
1486 closeAction: Action
1487 preCond(item, role) { return role == dobjRole ? [touchObj] : []; }
1488 needsDobj = true
1489 rank(item, role)
1490 {
1491 if (!item.isOpenable)
1492 return rankNever;
1493 return item.isOpen ? rankLogical : rankAlready;
1494 }
1495 exec(c)
1496 {
1497 local item = c.dobj;
1498 if (!item.isOpenable)
1499 {
1500 sayMsg('close.cannot', c);
1501 "\n";
1502 }
1503 else if (!item.isOpen)
1504 {
1505 sayMsg('close.already', c);
1506 "\n";
1507 }
1508 else
1509 {
1510 item.isOpen = nil;
1511 noteDone(c, item);
1512 }
1513 return nil;
1514 }
1515 report(c) { return accountDefault('close.ok', c); }
1516 ;
1517
1518 /* TURN X, which the pencil sharpener needs. */
1519 turnAction: Action
1520 needsDobj = true
1521 exec(c) { sayMsg('turn.nothing', c);"\n"; return nil; }
1522 ;
1523
1524 turnOnAction: Action
1525 needsDobj = true
1526 rank(item, role)
1527 {
1528 if (!item.isLightSource && !item.ofKind(Switch))
1529 return rankNever;
1530 return item.isLit || item.isOn ? rankAlready : rankLogical;
1531 }
1532 exec(c)
1533 {
1534 local item = c.dobj;
1535 if (!item.isLightSource)
1536 {
1537 sayMsg('turnon.cannot', c);
1538 "\n";
1539 }
1540 else if (item.isLit)
1541 {
1542 sayMsg('turnon.already', c);
1543 "\n";
1544 }
1545 else
1546 {
1547 item.isLit = true;
1548 noteDone(c, item);
1549 }
1550 return nil;
1551 }
1552 /* Seeing the room again is part of the account, not a second command. */
1553 report(c)
1554 {
1555 account('turnon.ok', c);
1556 if (!isDark())
1557 describeRoom();
1558 return nil;
1559 }
1560 ;
1561
1562 turnOffAction: Action
1563 needsDobj = true
1564 rank(item, role)
1565 {
1566 if (!item.isLightSource && !item.ofKind(Switch))
1567 return rankNever;
1568 return item.isLit || item.isOn ? rankLogical : rankAlready;
1569 }
1570 exec(c)
1571 {
1572 local item = c.dobj;
1573 if (!item.isLightSource)
1574 {
1575 sayMsg('turnoff.cannot', c);
1576 "\n";
1577 }
1578 else if (!item.isLit)
1579 {
1580 sayMsg('turnoff.already', c);
1581 "\n";
1582 }
1583 else
1584 {
1585 item.isLit = nil;
1586 noteDone(c, item);
1587 }
1588 return nil;
1589 }
1590 report(c)
1591 {
1592 local done = c.done_;
1593 sayAbout('turnoff.ok', done[1], done, nil);
1594 if (isDark())
1595 sayMsg('light.out', c);
1596 "\n";
1597 return nil;
1598 }
1599 ;
1600
1601 /* ASK X ABOUT Y and TELL X ABOUT Y. */
1602 converse(actor, about, asking)
1603 {
1604 if (!actor.isActor)
1605 {
1606 sayFor('talk.notactor', actor, nil);"\n";
1607 return nil;
1608 }
1609
1610 if (actor.curState != nil && actor.curState.ofKind(HermitActorState))
1611 {
1612 actor.talkedThisTurn = true;
1613 if (actor.curState.hasNoResponse)
1614 {
1615 actor.curState.noResponse;
1616 "\n";
1617 }
1618 else
1619 {
1620 sayFor('talk.hermit', actor, nil);
1621 "\n";
1622 }
1623 return nil;
1624 }
1625 actor.talkedThisTurn = true;
1626 local t = topicFor(actor, about, asking);
1627 if (t == nil)
1628 {
1629 sayFor('talk.notopic', actor, nil);
1630 "\n";
1631 }
1632 else
1633 {
1634 deliverTopic(t, actor, about);
1635 }
1636 return nil;
1637 }
1638
1639 /* Execute the exact selected entry; both ASK/TELL and menu adapters use it. */
1640 deliverTopic(t, actor, about)
1641 {
1642 if (t.id != nil) sayFor(t.id, actor, about);
1643 else t.reply;
1644 "\n";
1645 if (t.waits) hostAction(0);
1646 t.used = true;
1647 return nil;
1648 }
1649
1650 askAboutAction: Action
1651 needsDobj = true
1652 needsIobj = true
1653 iobjIsTopic = true
1654 missingIobjMsg ='talk.vaguetopic'
1655 exec(c) { return converse(c.dobj, c.iobj, true); }
1656 ;
1657
1658 tellAboutAction: Action
1659 needsDobj = true
1660 needsIobj = true
1661 iobjIsTopic = true
1662 missingIobjMsg ='talk.vaguetopic'
1663 exec(c) { return converse(c.dobj, c.iobj, nil); }
1664 ;
1665
1666 eatAction: Action
1667 preCond(item, role) { return role == dobjRole ? [objHeld] : []; }
1668 needsDobj = true
1669 rank(item, role) { return item.isEdible ? rankLogical : rankIllogical; }
1670 exec(c)
1671 {
1672 if (!c.dobj.isEdible)
1673 {
1674 sayFor('eat.cannot', c.dobj, nil);
1675 "\n";
1676 return nil;
1677 }
1678
1679 contains.unset(location.get(c.dobj), c.dobj);
1680 noteDone(c, c.dobj);
1681 return nil;
1682 }
1683 report(c) { return account('eat.ok', c); }
1684 ;
1685
1686 drinkAction: Action
1687 preCond(item, role) { return role == dobjRole ? [touchObj] : []; }
1688 needsDobj = true
1689 rank(item, role)
1690 {
1691 return item.isDrinkable || item.fluidName != nil ? rankLogical : rankIllogical;
1692 }
1693 exec(c)
1694 {
1695 if (!c.dobj.isDrinkable)
1696 {
1697 sayFor('drink.cannot', c.dobj, nil);
1698 "\n";
1699 return nil;
1700 }
1701 noteDone(c, c.dobj);
1702 return nil;
1703 }
1704 report(c) { return account('drink.ok', c); }
1705 ;
1706
1707 pourIntoAction: Action
1708 preCond(item, role) { return role == dobjRole ? [objHeld] : [touchObj, objOpen]; }
1709 needsDobj = true
1710 needsIobj = true
1711 rank(item, role)
1712 {
1713 if (role == dobjRole)
1714 return item.fluidName != nil ? rankLogical : rankIllogical;
1715 return item.allowPourIntoMe ? rankLogical : rankUnlikely;
1716 }
1717 exec(c)
1718 {
1719 local from = c.dobj;
1720 if (from.fluidName == nil)
1721 {
1722 sayFor('pour.nothing', from, nil);
1723 "\n";
1724 return nil;
1725 }
1726 if (!c.iobj.allowPourIntoMe)
1727 {
1728 sayFor('pour.wontake', c.iobj, nil);
1729 "\n";
1730 return nil;
1731 }
1732 /* The fluid moves by being named somewhere else. */
1733 c.iobj.fluidName = from.fluidName;
1734 from.fluidName = nil;
1735 c.way_ = c.iobj.fluidName;
1736 noteDone(c, from);
1737 return nil;
1738 }
1739 /* The fluid's word and the thing it went into, so the account can name both. */
1740 report(c) { return accountAbout('pour.ok', c, c.way_); }
1741 ;
1742
1743 detachPartAction: Action
1744 needsDobj = true
1745 rank(item, role) { return item.ofKind(Component) ? rankLogical : rankIllogical; }
1746 exec(c)
1747 {
1748 local part = c.dobj;
1749 if (!part.ofKind(Component) || !part.isDetachable)
1750 {
1751 sayFor('detach.fixed', part, nil);
1752 "\n";
1753 return nil;
1754 }
1755 /*
1756 * Detached, it is an ordinary thing: it stops being fixed and stops
1757 * being part of its owner's description, and it goes where the owner is
1758 * rather than staying inside it.
1759 */
1760 local owner = location.get(part);
1761 part.isFixed = nil;
1762 part.isDecoration = nil;
1763 part.isDetachable = nil;
1764 contains.set(owner == nil ? here() : location.get(owner), part);
1765 noteDone(c, part);
1766 return nil;
1767 }
1768 report(c) { return account('detach.part', c); }
1769 ;
1770
1771 /*
1772 * GIVE X TO Y, and SHOW X TO Y.
1773 *
1774 * A library with actors and no way to hand one of them anything is a library
1775 * that has not met a game: the author game's whole ending is Becky giving Zoé
1776 * the tape. The default is a refusal, because what an actor does with a thing
1777 * is the game's business and not vhyl's — but the **action exists**, so a Doer
1778 * can catch it and a topic can answer it.
1779 */
1780 giveAction: Action
1781 preCond(item, role) { return role == dobjRole ? [objHeld] : []; }
1782 needsDobj = true
1783 needsIobj = true
1784 rank(item, role) { return role == iobjRole && !item.isActor ? rankIllogical : rankLogical; }
1785 exec(c)
1786 {
1787 if (!c.iobj.isActor)
1788 {
1789 sayFor('give.notactor', c.iobj, nil);"\n";
1790 return nil;
1791 }
1792 if (!carried(c.dobj))
1793 {
1794 sayFor('drop.notheld', c.dobj, nil);"\n";
1795 return nil;
1796 }
1797 sayFor('give.refused', c.iobj, c.dobj);"\n";
1798 return nil;
1799 }
1800 ;
1801
1802 showAction: Action
1803 preCond(item, role) { return role == dobjRole ? [objHeld] : []; }
1804 needsDobj = true
1805 needsIobj = true
1806 rank(item, role) { return role == iobjRole && !item.isActor ? rankIllogical : rankLogical; }
1807 exec(c)
1808 {
1809 if (!c.iobj.isActor)
1810 {
1811 sayFor('give.notactor', c.iobj, nil);"\n";
1812 return nil;
1813 }
1814 sayFor('show.nothing', c.iobj, c.dobj);"\n";
1815 return nil;
1816 }
1817 ;
1818
1819 unlockAction: Action
1820 preCond(item, role) { return role == dobjRole ? [touchObj] : [objHeld]; }
1821 needsDobj = true
1822 rank(item, role) { return item.isLockable ? rankLogical : rankIllogical; }
1823 exec(c)
1824 {
1825 local it = c.dobj;
1826 if (!it.isLockable)
1827 {
1828 sayFor('lock.cannot', it, nil);"\n";
1829 return nil;
1830 }
1831 if (!it.isLocked)
1832 {
1833 sayFor('unlock.already', it, nil);"\n";
1834 return nil;
1835 }
1836 local key = c.iobj != nil ? c.iobj : it.keyItem;
1837 if (it.keyItem == nil || key != it.keyItem || !carried(key))
1838 {
1839 sayFor('unlock.wrongkey', it, c.iobj);"\n";
1840 return nil;
1841 }
1842 it.isLocked = nil;
1843 c.iobj = key;
1844 noteDone(c, it);
1845 return nil;
1846 }
1847 report(c) { return accountDefault('unlock.ok', c); }
1848 ;
1849
1850 lockAction: Action
1851 preCond(item, role) { return role == dobjRole ? [touchObj, objClosed] : [objHeld]; }
1852 needsDobj = true
1853 rank(item, role) { return item.isLockable ? rankLogical : rankIllogical; }
1854 exec(c)
1855 {
1856 local it = c.dobj;
1857 if (!it.isLockable)
1858 {
1859 sayFor('lock.cannot', it, nil);"\n";
1860 return nil;
1861 }
1862 if (it.isLocked)
1863 {
1864 sayFor('lock.already', it, nil);"\n";
1865 return nil;
1866 }
1867 local key = c.iobj != nil ? c.iobj : it.keyItem;
1868 if (it.keyItem == nil || key != it.keyItem || !carried(key))
1869 {
1870 sayFor('unlock.wrongkey', it, c.iobj);"\n";
1871 return nil;
1872 }
1873 it.isOpen = nil;
1874 it.isLocked = true;
1875 c.iobj = key;
1876 noteDone(c, it);
1877 return nil;
1878 }
1879 report(c) { return accountDefault('lock.ok', c); }
1880 ;
1881
1882 attachAction: Action
1883 preCond(item, role) { return role == dobjRole ? [objHeld] : [touchObj]; }
1884 needsDobj = true
1885 needsIobj = true
1886 exec(c)
1887 {
1888 local a = c.dobj;
1889 local b = c.iobj;
1890 if (!a.isAttachable || !b.isAttachable)
1891 {
1892 sayMsg('attach.cannot', c);
1893 "\n";
1894 }
1895 else if (a == b)
1896 {
1897 sayMsg('put.initself', nil);
1898 "\n";
1899 }
1900 else if (a.attachesTo != nil && a.attachesTo != b)
1901 {
1902 sayFor('attach.wrongpair', a, b);
1903 "\n";
1904 }
1905 else if (attachedTo.contains(a, b))
1906 {
1907 sayMsg('attach.already', c);
1908 "\n";
1909 }
1910 else
1911 {
1912 attachedTo.set(a, b);
1913 noteDone(c, a);
1914 }
1915 return nil;
1916 }
1917 report(c) { return accountDefault('attach.ok', c); }
1918 ;
1919
1920 detachAction: Action
1921 needsDobj = true
1922 needsIobj = true
1923 exec(c)
1924 {
1925 if (!attachedTo.contains(c.dobj, c.iobj))
1926 {
1927 sayMsg('detach.notjoined', c);
1928 "\n";
1929 }
1930 else
1931 {
1932 attachedTo.unset(c.dobj, c.iobj);
1933 noteDone(c, c.dobj);
1934 }
1935 return nil;
1936 }
1937 report(c) { return accountDefault('detach.ok', c); }
1938 ;
1939
1940 scoreAction: Action
1941 exec(c)
1942 {
1943 sayAbout('score.is', nil, nil, score.value);
1944 "\n";
1945 foreach (local why in score.awards)
1946 " <<why>>\n";
1947 return nil;
1948 }
1949 ;
1950
1951 senseAction: Action
1952 needsDobj = true
1953 /* Nor does listening to it, or smelling it. */
1954 needsReach = nil
1955 which = nil
1956 exec(c)
1957 {
1958 local it = c.dobj;
1959 if (it.ofKind(Sensed) && it.sense == which)
1960 it.desc;
1961 else if (which =='hear')
1962 sayMsg('listen.nothing', c);
1963 else
1964 sayMsg('smell.nothing', c);
1965 "\n";
1966 return nil;
1967 }
1968 ;
1969
1970 listenAction: senseAction which ='hear';
1971 smellAction: senseAction which ='smell';
1972
1973 flipAction: Action
1974 needsDobj = true
1975 exec(c)
1976 {
1977 local g = c.dobj;
1978 if (g.ofKind(Switch))
1979 {
1980 g.isOn = !g.isOn;
1981 c.way_ = g.isOn ?'flip.on' :'flip.off';
1982 noteDone(c, g);
1983 }
1984 else if (g.ofKind(Lever))
1985 {
1986 g.isUp = !g.isUp;
1987 c.way_ = g.isUp ?'pull.up' :'pull.down';
1988 noteDone(c, g);
1989 }
1990 else
1991 {
1992 sayFor('flip.cannot', g, nil);
1993 "\n";
1994 }
1995 return nil;
1996 }
1997 report(c)
1998 {
1999 account(c.way_, c);
2000 c.dobj.activate();
2001 return nil;
2002 }
2003 ;
2004
2005 pressAction: Action
2006 needsDobj = true
2007 exec(c)
2008 {
2009 if (!c.dobj.ofKind(Button))
2010 {
2011 sayMsg('press.nothing', c);
2012 "\n";
2013 }
2014 else
2015 noteDone(c, c.dobj);
2016 return nil;
2017 }
2018 /*
2019 * The click, then whatever it set off. `activate` belongs here rather than
2020 * in `exec` because that is the order a player experiences it, and because
2021 * the account has to come first whatever the button does.
2022 */
2023 report(c)
2024 {
2025 account('press.ok', c);
2026 c.dobj.activate();
2027 return nil;
2028 }
2029 ;
2030
2031 /* SET DIAL TO <word>. The word is a token, not an entity, so the rule binds it
2032 * as a part of speech and the action reads the text. */
2033 setToAction: Action
2034 needsDobj = true
2035 exec(c)
2036 {
2037 local d = c.dobj;
2038 /* The rule may have named the setting outright or captured whatever the
2039 * player typed. A captured token binds the word itself. */
2040 if (c.way_ == nil && c.set_ != nil)
2041 c.way_ = c.set_;
2042 if (!d.ofKind(Dial))
2043 {
2044 sayFor('set.cannot', d, nil);
2045 "\n";
2046 }
2047 else if (!holds(d.validSettings, c.way_))
2048 {
2049 sayFor('set.badvalue', d, nil);
2050 "\n";
2051 }
2052 else
2053 {
2054 d.setting = c.way_;
2055 noteDone(c, d);
2056 }
2057 return nil;
2058 }
2059 /*
2060 * The setting, then what setting it did. `way_` is the value here rather
2061 * than a message id, so this does not go through `account`.
2062 */
2063 report(c)
2064 {
2065 sayAbout('set.ok', c.dobj, nil, c.way_);
2066 "\n";
2067 c.dobj.activate();
2068 return nil;
2069 }
2070 ;
2071
2072 boardAction: Action
2073 needsDobj = true
2074 exec(c) { return doBoard(c, c.dobj); }
2075 /*
2076 * A vehicle may describe boarding it in its own words. Read, not tested: an
2077 * emitting property prints when it is read, so `hasBoardDesc` is the flag.
2078 */
2079 report(c)
2080 {
2081 local item = c.done_[1];
2082 if (item.hasBoardDesc)
2083 {
2084 item.boardDesc;
2085 "\n";
2086 return nil;
2087 }
2088 return account('board.ok', c);
2089 }
2090 ;
2091
2092 enterAction: Action
2093 needsDobj = true
2094 exec(c) { return doEnter(c, c.dobj); }
2095 /*
2096 * ENTER reaches BOARD when the thing is a vehicle, so it needs the same
2097 * account. Everything else about entering says its words as it happens: a
2098 * booth's `enterDesc` is the game's own prose and there is nothing to
2099 * summarise.
2100 */
2101 report(c) { return boardAction.report(c); }
2102 ;
2103
2104 wearAction: Action
2105 preCond(item, role) { return role == dobjRole ? [objHeld] : []; }
2106 needsDobj = true
2107 rank(item, role)
2108 {
2109 if (!item.isWearable)
2110 return rankNever;
2111 return item.isWorn ? rankAlready : rankLogical;
2112 }
2113 exec(c)
2114 {
2115 local item = c.dobj;
2116 if (!item.isWearable)
2117 {
2118 sayMsg('wear.cannot', c);
2119 "\n";
2120 }
2121 else if (item.isWorn)
2122 {
2123 sayMsg('wear.already', c);
2124 "\n";
2125 }
2126 else
2127 {
2128 contains.set(player, item);
2129 item.isWorn = true;
2130 noteDone(c, item);
2131 }
2132 return nil;
2133 }
2134 report(c) { return accountDefault('wear.ok', c); }
2135 ;
2136
2137 doffAction: Action
2138 preCond(item, role) { return role == dobjRole ? [objHeld] : []; }
2139 needsDobj = true
2140 rank(item, role) { return item.isWorn ? rankLogical : rankNotNow; }
2141 exec(c)
2142 {
2143 if (!c.dobj.isWorn)
2144 {
2145 sayMsg('doff.notworn', c);
2146 "\n";
2147 }
2148 else
2149 {
2150 c.dobj.isWorn = nil;
2151 noteDone(c, c.dobj);
2152 }
2153 return nil;
2154 }
2155 report(c) { return accountDefault('doff.ok', c); }
2156 ;
2157
2158 pushDirAction: Action
2159 needsDobj = true
2160 exec(c) { return doPush(c.dobj, c.dir_); }
2161 ;
2162
2163 putOnAction: Action
2164 preCond(item, role) { return role == dobjRole ? [objHeld] : [touchObj]; }
2165 needsDobj = true
2166 needsIobj = true
2167 exec(c) { return doPutOn(c, c.dobj, c.iobj); }
2168
2169 report(c) { return accountDefault(c.way_, c); }
2170 ;
2171
2172 putInAction: Action
2173 preCond(item, role) { return role == dobjRole ? [objHeld] : [touchObj, objOpen]; }
2174 needsDobj = true
2175 needsIobj = true
2176 exec(c) { return doPutIn(c, c.dobj, c.iobj); }
2177 report(c) { return accountDefault(c.way_, c); }
2178 ;
2179
2180 putBehindAction: Action
2181 needsDobj = true
2182 needsIobj = true
2183 exec(c)
2184 {
2185 local behind = c.iobj;
2186 if (behind.remapBehind == nil)
2187 {
2188 sayFor('putbehind.cannot', behind, nil);"\n";
2189 return nil;
2190 }
2191 contains.set(behind.remapBehind, c.dobj);
2192 noteDone(c, c.dobj);
2193 return nil;
2194 }
2195 report(c) { return accountDefault('putbehind.ok', c); }
2196 ;
2197
2198 /*
2199 * LOOK BEHIND and LOOK UNDER. What is behind a thing is out of sight, so this
2200 * is how you find it — the leaflet behind the box and the note under the book.
2201 */
2202 lookUnderAction: Action
2203 needsDobj = true
2204 exec(c)
2205 {
2206 local it = c.dobj;
2207 local space = it.remapUnder;
2208 if (space == nil)
2209 {
2210 sayFor('lookunder.nothing', it, nil);"\n";
2211 return nil;
2212 }
2213 local found = contains.all(space);
2214 if (found.length() == 0)
2215 {
2216 sayFor('lookunder.nothing', it, nil);
2217 "\n";
2218 }
2219 else
2220 {
2221 noteSeen(found);
2222 sayAbout('lookunder.found', it, found, nil);
2223 "\n";
2224 foreach (local held in contains.all(space))
2225 held.isHidden = nil;
2226 }
2227 return nil;
2228 }
2229 ;
2230
2231 lookBehindAction: Action
2232 needsDobj = true
2233 exec(c)
2234 {
2235 local it = c.dobj;
2236 local space = it.remapBehind != nil ? it.remapBehind : it;
2237 local found = contains.all(space);
2238 if (space == it && !it.isContainer)
2239 {
2240 sayFor('lookbehind.nothing', it, nil);
2241 "\n";
2242 }
2243 else if (found.length() == 0)
2244 {
2245 sayFor('lookbehind.nothing', it, nil);
2246 "\n";
2247 }
2248 else
2249 {
2250 noteSeen(found);
2251 sayAbout('lookbehind.found', it, found, nil);
2252 "\n";
2253 /* Found things stop being hidden. */
2254 foreach (local held in contains.all(space))
2255 held.isHidden = nil;
2256 }
2257 return nil;
2258 }
2259 ;
2260
2261 /* ------------------------------------------------------------- connectors */
2262
2263 /** @api author
2264 * A traversable entity that can control staging, passage and travel reports. */
2265 class TravelConnector: Thing
2266 isFixed = true
2267 /* Where it leads. A Door works out its own, from the side you are on. */
2268 destination = nil
2269 /*
2270 * Said as you go through, if anything, and said as it is refused. These are
2271 * emitting properties: reading one prints it. So a game that supplies one
2272 * also sets the matching `has...` flag, because there is no way to ask
2273 * whether an emitting property is empty without printing it.
2274 */
2275 travelDesc = nil
2276 hasTravelDesc = nil
2277
2278 canPass = true
2279 barrierDesc = nil
2280 hasBarrierDesc = nil
2281
2282 onTravel = nil
2283
2284 connectorStagingLocation = nil
2285 /* And the posture they have to be in once they are there. */
2286 connectorStagingPosture = standing
2287 ;
2288
2289 /*
2290 * A way through that is not a door: an arch, a gap, a hole in a hedge. It is
2291 * `TravelConnector` with nothing added, which is the honest answer — a passage
2292 * differs from a door by not shutting, and a Door is the one that adds.
2293 */
2294 /** @api author
2295 * A travel connector without a door's open and closed state. */
2296 class Passage: TravelConnector;
2297
2298 /** @api author
2299 * A passage whose bulk policy permits a path-like route. */
2300 class PathPassage: Passage
2301 bulkLimit = 0
2302 ;
2303 /** @api author
2304 * A connector representing travel upward. */
2305 class StairwayUp: TravelConnector;
2306 /** @api author
2307 * A connector representing travel downward. */
2308 class StairwayDown: TravelConnector;
2309
2310 /** @api author
2311 * A travel connector shared by rooms through connects relation rows. */
2312 class Door: TravelConnector
2313 isOpenable = true
2314 isOpen = nil
2315 ;
2316
2317 /** @api author
2318 * A door initially hidden from ordinary observation. */
2319 class SecretDoor: Door
2320 isHidden = true
2321 ;
2322
2323 /*
2324 * Something you ride. Being in a vehicle is an ordinary containment row, so
2325 * travel carries whatever contains the player rather than the player alone —
2326 * no separate notion of a traveller is needed.
2327 */
2328 /** @api author
2329 * A boardable thing that can carry an actor through travel. */
2330 class Vehicle: Thing
2331 isVehicle = true
2332 /* You get **on** a bicycle, not in one. */
2333 objPos = posOn
2334 boardDesc = nil
2335 hasBoardDesc = nil
2336 /*
2337 * Whether it may go to this room. A method rather than a field, because the
2338 * answer is about where you are heading: asking about where you are would
2339 * let a bicycle into the hall as long as it started outdoors.
2340 */
2341 canEnter(room) { return true; }
2342 ;
2343
2344 /* Too heavy to carry, but it can be shoved from room to room. */
2345 /** @api author
2346 * A thing too heavy for ordinary taking. */
2347 class Heavy: Thing
2348 isFixed = true
2349 canPushTravel = nil
2350 ;
2351
2352 /*
2353 * Touchable, and not going anywhere. `Fixture` is scenery and belongs to the
2354 * room's description; an `Immovable` is a thing in its own right that simply
2355 * cannot be moved — a safe, an anvil, a fallen beam.
2356 */
2357 /** @api author
2358 * A thing that cannot be moved by ordinary actions. */
2359 class Immovable: Thing
2360 isFixed = true
2361 canPushTravel = nil
2362 refuses(action, c)
2363 {
2364 if (action == takeAction || action == pushDirAction)
2365 return msgFor('immovable.cannot', self, nil);
2366 return inherited(action, c);
2367 }
2368 ;
2369
2370 /** @api author
2371 * A perceivable thing that cannot be touched. */
2372 class Intangible: Thing
2373 isFixed = true
2374 isDecoration = true
2375 isIntangible = true
2376 refuses(action, c)
2377 {
2378 if (action == examineAction || action == listenAction
2379 || action == smellAction || action == thinkAboutAction)
2380 return nil;
2381 return msgFor('intangible.cannot', self, nil);
2382 }
2383 ;
2384
2385 /** @api author
2386 * A referable absence with a custom explanation. */
2387 class Unthing: Thing
2388 isFixed = true
2389 isDecoration = true
2390 refuses(action, c) { return msgFor('unthing.gone', self, nil); }
2391 rank(a, role) { return rankNever; }
2392 ;
2393
2394 /* Part of the furniture: mentioned in the room description, not listed. */
2395 /** @api author
2396 * Fixed furniture or scenery within a location. */
2397 class Fixture: Thing
2398 isFixed = true
2399 ;
2400
2401 /* Scenery you can look at but need not be told about. */
2402 /** @api author
2403 * Scenery normally omitted from room listings. */
2404 class Decoration: Fixture
2405 isDecoration = true
2406
2407 isImportant = nil
2408 refuses(action, c)
2409 {
2410 if (isImportant)
2411 return inherited(action, c);
2412 if (action == examineAction || action == listenAction
2413 || action == smellAction || action == thinkAboutAction
2414 || action == askAboutAction || action == tellAboutAction
2415 || action == consultAction)
2416 return inherited(action, c);
2417 return msgFor('decoration.notimportant', self, nil);
2418 }
2419 ;
2420
2421 /** @api author
2422 * A thing whose identity and placement are tied to another object. */
2423 class Component: Thing
2424 isFixed = true
2425 /* Part of its owner's description, not a separate line in the room. */
2426 isDecoration = true
2427 /* Whether it may be prised off. An AttachableComponent says yes. */
2428 isDetachable = nil
2429 /*
2430 * Taking a part says **what it is part of**, rather than the room's stock
2431 * refusal, because *the handle is part of the lantern* is the answer a
2432 * player actually wanted. `refuses` answers text, so this reads the message
2433 * rather than saying it.
2434 */
2435 refuses(action, c)
2436 {
2437 /* A detached part is an ordinary thing and refuses nothing. */
2438 if (action != takeAction || !isFixed)
2439 return nil;
2440 return msgFor('take.part', self, location.get(self));
2441 }
2442 /* And it is the one you did not mean when something else could be taken. */
2443 rank(a, role) { return a == takeAction ? rankSelf : rankLogical; }
2444 ;
2445
2446 Component template'name';
2447
2448 /** @api author
2449 * A component that can be attached to another entity. */
2450 class AttachableComponent: Component
2451 isDetachable = true
2452 isAttachable = true
2453 ;
2454
2455 AttachableComponent template'name';
2456
2457 /** @api author
2458 * Scenery visible but outside ordinary reach. */
2459 class Distant: Decoration
2460 isDistant = true
2461 ;
2462
2463 Distant template'name';
2464
2465 /* Things rest on top of it rather than inside. */
2466 /** @api author
2467 * A fixed surface on which things can rest. */
2468 class Surface: Fixture
2469 isContainer = true
2470 isOpenable = nil
2471 /* What a thing is said to be, relative to this. */
2472 objPos = posOn
2473 ;
2474
2475 /* Holds things inside, and cannot be shut. */
2476 /** @api author
2477 * A Thing whose contents participate in containment and capacity rules. */
2478 class Container: Thing
2479 isContainer = true
2480 isOpenable = nil
2481 objPos = posIn
2482 ;
2483
2484 /* A container with a lid. */
2485 /** @api author
2486 * A container that can be opened and closed. */
2487 class OpenableContainer: Container
2488 isOpenable = true
2489 isOpen = nil
2490 ;
2491
2492 /* One that can be locked shut, and needs the right key. */
2493 /** @api author
2494 * An openable container with lock state. */
2495 class LockableContainer: OpenableContainer
2496 isLockable = true
2497 isLocked = true
2498 ;
2499
2500 /* Things go behind it rather than in it. */
2501 /** @api author
2502 * A thing that receives objects placed behind it. */
2503 class RearContainer: Thing
2504 isContainer = true
2505 isOpenable = nil
2506 objPos = posBehind
2507 /* Behind is out of sight until you look there. */
2508 contentsListed = nil
2509 ;
2510
2511 /** @api author
2512 * An openable container with a distinct bulk policy. */
2513 class BagOfHolding: OpenableContainer
2514 isOpen = true
2515 bulkCapacity = 100
2516 ;
2517
2518 /** @api author
2519 * A subordinate physical part of another thing. */
2520 class SubComponent: Thing
2521 isFixed = true
2522 isContainer = true
2523 /* Named as part of the thing it belongs to, not on its own. */
2524 isDecoration = true
2525 ;
2526
2527 SubComponent template'name';
2528
2529 /** @api author
2530 * A surface on which an actor may stand. */
2531 class Platform: Surface
2532 isEnterable = true
2533 objPos = posOn
2534 /* What you can reach from up here. nil means everything in the room. */
2535 reachOut = true
2536 /* Said when you get on. An emitting string; a game overrides it. */
2537 enterDesc = nil
2538 ;
2539
2540 /** @api author
2541 * A thing that can answer consult actions. */
2542 class Consultable: Thing
2543 isConsultable = true
2544 consult(about, words) { return nil; }
2545 ;
2546
2547 /* A space you sit inside rather than on: a booth, a cupboard, a cart. */
2548 /** @api author
2549 * A container that an actor may enter. */
2550 class Booth: Container
2551 isEnterable = true
2552 isFixed = true
2553 objPos = posIn
2554 reachOut = true
2555 enterDesc = nil
2556 allowedPostures = [sitting, standing]
2557 defaultPosture = sitting
2558 ;
2559
2560 /** @api author
2561 * A platform supporting sitting posture. */
2562 class Chair: Platform
2563 allowedPostures = [sitting, standing]
2564 defaultPosture = sitting
2565 ;
2566
2567 /** @api author
2568 * A platform supporting lying posture. */
2569 class Bed: Platform
2570 allowedPostures = [lying, sitting, standing]
2571 defaultPosture = lying
2572 ;
2573
2574 /* ------------------------------------------------------- attachment, lists */
2575
2576 /** @api author
2577 * A thing that can attach to another entity. */
2578 class Attachable: Thing
2579 isAttachable = true
2580 /* What it will join to. nil means anything attachable. */
2581 attachesTo = nil
2582 ;
2583
2584 Attachable template'name';
2585
2586 /** @api author
2587 * Cycles, stops or exhausts an authored list of values. */
2588 class EventList: object
2589 items = []
2590 at = 0
2591 /* 'cycle' starts again, 'stop' stays on the last, 'once' then silence. */
2592 kind ='cycle'
2593 next()
2594 {
2595 local count = items.length();
2596 if (count == 0)
2597 return nil;
2598 at = at + 1;
2599 if (at > count)
2600 {
2601 if (kind =='cycle')
2602 at = 1;
2603 else if (kind =='stop')
2604 at = count;
2605 else
2606 return nil;
2607 }
2608 return items[at];
2609 }
2610 ;
2611
2612 /** @api author
2613 * Tracks a condition-driven interval in the world. */
2614 class Scene: object
2615 isHappening = nil
2616 hasHappened = nil
2617 /* Answered true when it should start. */
2618 startsWhen = nil
2619 /* Answered true when it should stop. */
2620 endsWhen = nil
2621 whenStarting() { return nil; }
2622 whenEnding() { return nil; }
2623 ;
2624
2625 score: object
2626 value = 0
2627 awards = []
2628 ;
2629
2630 /* Award points once for a named achievement. */
2631 awardPoints(points, why)
2632 {
2633 if (holds(score.awards, why))
2634 return nil;
2635 score.awards = score.awards + [why];
2636 score.value = score.value + points;
2637 sayAbout(points == 1 ?'score.gained1' :'score.gained', nil, nil, points);
2638 "\n";
2639 return nil;
2640 }
2641
2642 /* Run every turn: start and stop whatever scenes are due. */
2643 runScenes()
2644 {
2645 for (local sc = firstObj(Scene); sc != nil; sc = nextObj(sc, Scene))
2646 {
2647 if (!sc.isHappening && !sc.hasHappened && sc.startsWhen)
2648 {
2649 sc.isHappening = true;
2650 sc.whenStarting();
2651 }
2652 else if (sc.isHappening && sc.endsWhen)
2653 {
2654 sc.isHappening = nil;
2655 sc.hasHappened = true;
2656 sc.whenEnding();
2657 }
2658 }
2659 return nil;
2660 }
2661
2662 /* --------------------------------------------------------------- sensory */
2663
2664 /** @api author
2665 * A sensed phenomenon that can be present in multiple rooms. */
2666 class Sensed: MultiLoc
2667 /* Mentioned when the room is described, if it is worth mentioning. */
2668 isAmbient = true
2669 ambientDesc =""
2670 ;
2671
2672 /** @api author
2673 * A sound present in one or more rooms. */
2674 class Noise: Sensed
2675 sense ='hear'
2676 ;
2677
2678 /** @api author
2679 * A smell present in one or more rooms. */
2680 class Odour: Sensed
2681 sense ='smell'
2682 ;
2683
2684 Sensed template'name';
2685 Noise template'name';
2686 Odour template'name';
2687
2688 /* What can be sensed here, listed after the room. */
2689 listSensed(room)
2690 {
2691 for (local n = firstObj(Sensed); n != nil; n = nextObj(n, Sensed))
2692 {
2693 if (n.isAmbient && presentIn.contains(n, room))
2694 {
2695 n.ambientDesc;
2696 " ";
2697 }
2698 }
2699 return nil;
2700 }
2701
2702 /* ----------------------------------------------------------------- actors */
2703
2704 /* --------------------------------------------------------- actor travel */
2705
2706 travelActions: object
2707 /* One command epoch; scalar stamps avoid retaining a fresh world list
2708 * every turn. Undo/rollback journal the epoch and actor stamps together. */
2709 cycle = 0
2710 backgroundActor = nil
2711 /* The actor a `sayDeparting` is currently being asked about. */
2712 conn = nil
2713 ;
2714
2715 /** @api author
2716 * A character that participates in travel, conversation and actor states. */
2717 class Actor: Thing
2718 travelCycle = -1
2719
2720 /** @api author
2721 * Decide whether this actor may accompany a leader along this journey. */
2722 canAccompanyTravel(leader, conn, dest) { return true; }
2723
2724 presentation = [&name, &isOpen, &isOpenable, &isContainer, &isLit,
2725 &isLightSource, &isFixed, &isDecoration, &isTransparent,
2726 &isHidden, &bulk, &bulkCapacity, &objPos,
2727 &curState]
2728 isFixed = true
2729 isActor = true
2730 /* The state it is in, which decides what it says and does. */
2731 /** @api author
2732 * Current ActorState, selected by the library or setCurState. */
2733 curState = nil
2734 /* Said instead of desc while in a state that has its own. */
2735 desc = nil
2736 /* Whether this actor has spoken this turn, for a ConvAgendaItem. */
2737 talkedThisTurn = nil
2738
2739 posture = standing
2740
2741 /** @api author
2742 * Change actor state, invoking deactivation and activation hooks. */
2743 setCurState(state)
2744 {
2745 if (state == curState)
2746 return nil;
2747 local old = curState;
2748 if (old != nil)
2749 old.deactivateState(self, state);
2750 curState = state;
2751 if (state != nil)
2752 {
2753 state.actor = self;
2754 state.activateState(self, old);
2755 }
2756 return nil;
2757 }
2758
2759 /* Background activity for an actor with no state of its own to run one. */
2760 /** @api author
2761 * Run background behaviour when the actor has no state turn hook. */
2762 idleTurn() { return nil; }
2763
2764 /* Whether this actor will do as it is told. The state decides. */
2765 obeyCommand(issuer, a)
2766 {
2767 return curState == nil ? nil : curState.obeyCommand(issuer, a);
2768 }
2769
2770 moveIntoForTravel(room)
2771 {
2772 if (room != nil)
2773 relocateActor(self, room);
2774 return nil;
2775 }
2776
2777 travelTo(dest, conn)
2778 {
2779 if (dest == nil || contains.outermost(self) == dest
2780 || !canStartJourney())
2781 return nil;
2782 local from = contains.outermost(self);
2783 local escort = accompanists(self, from, conn, dest);
2784 if (escort == nil)
2785 return nil;
2786 return travelParty(dest, conn, escort);
2787 }
2788
2789 /* A companion's own background agenda can still speak or do other work.
2790 * Its travel waits for its leader, independent of declaration order. */
2791 canStartJourney()
2792 {
2793 if (travelCycle == travelActions.cycle)
2794 return nil;
2795 if (travelActions.backgroundActor == self && curState != nil
2796 && (curState.accompanyingActor != nil || curState.escortActor != nil))
2797 return nil;
2798 return true;
2799 }
2800
2801 /* Internal: eligibility has been checked exactly once for this journey. */
2802 travelParty(dest, conn, escort)
2803 {
2804
2805 travelCycle = travelActions.cycle;
2806 foreach (local a in escort)
2807 a.travelCycle = travelActions.cycle;
2808 foreach (local a in escort)
2809 {
2810 wearTravelState(a, conn);
2811 a.travelAlone(dest, conn);
2812 }
2813 travelAlone(dest, conn);
2814 finishAccompaniment(escort, true);
2815 return true;
2816 }
2817
2818 /* Internal: the party has already been gathered and admitted. */
2819 travelAlone(dest, conn)
2820 {
2821 local from = contains.outermost(self);
2822 local way = wayFrom(from, dest);
2823 foreach (local item in contains.all(from))
2824 item.beforeTravel(self, conn);
2825 if (curState != nil)
2826 curState.beforeTravel(self, conn);
2827
2828 if (from == here() && from != dest)
2829 sayDeparting(way, conn);
2830 relocateActor(self, dest);
2831 if (conn != nil && conn.onTravel != nil)
2832 conn.onTravel;
2833 if (dest == here() && from != dest)
2834 sayArriving(way == nil ? nil : oppositeOf(way), conn);
2835 foreach (local item in contains.all(dest))
2836 item.afterTravel(self, conn);
2837 if (curState != nil)
2838 curState.afterTravel(self, conn);
2839 return nil;
2840 }
2841
2842 scriptedTravelTo(dest)
2843 {
2844 if (dest == nil || !canStartJourney())
2845 return nil;
2846 local from = contains.outermost(self);
2847 local way = wayFrom(from, dest);
2848 if (way == nil)
2849 return nil;
2850 local conn = exits.get(from, way);
2851 if (conn != nil && conn.ofKind(TravelConnector) && !conn.canPass)
2852 return nil;
2853 local escort = accompanists(self, from, conn, dest);
2854 if (escort == nil)
2855 return nil;
2856 if (conn != nil && conn.ofKind(Door) && !conn.isOpen)
2857 {
2858 if (conn.isLocked)
2859 return nil;
2860 conn.isOpen = true;
2861 /*
2862 * A door stands in both the rooms it joins, so the player sees it
2863 * open from either side — which is what `connectorHere` answers and
2864 * what plain visibility, which is about containment, does not.
2865 */
2866 if (connectorHere(conn) || visible(conn))
2867 {
2868 sayFor('travel.opens', self, conn);
2869 "\n";
2870 }
2871 }
2872 if (conn != nil && conn.ofKind(TravelConnector) && !conn.canPass)
2873 return nil;
2874 return travelParty(dest, conn, escort);
2875 }
2876
2877 /* What the player sees when this actor leaves, and when one arrives. */
2878 sayDeparting(way, conn)
2879 {
2880 /* A state may take the message over, which is how accompanying travel
2881 * says *the lead guard prods you on* instead of *the guard leaves*. */
2882 if (curState != nil && curState.saysDeparting)
2883 return curState.sayDeparting(self, way, conn);
2884 sayAbout(way == nil ?'travel.leaves' :'travel.leavesway',
2885 self, nil, way);
2886 "\n";
2887 return nil;
2888 }
2889
2890 sayArriving(fromWay, conn)
2891 {
2892 sayAbout(fromWay == nil ?'travel.arrives' :'travel.arrivesway',
2893 self, nil, fromWay);
2894 "\n";
2895 return nil;
2896 }
2897 ;
2898
2899 /** @api author
2900 * Behaviour and presentation state associated with an Actor. */
2901 class ActorState: object
2902 presentation = [&actor, &accompanyingActor, &escortActor, &escortDest]
2903 /** @api author
2904 * Actor to which this state belongs. */
2905 actor = nil
2906 accompanyingActor = nil
2907 escortActor = nil
2908 escortDest = nil
2909 /* Mentioned when the actor is described. */
2910 /** @api author
2911 * Description emitted while the actor is in this state. */
2912 stateDesc =""
2913 /* Only these topics answer while in this state; nil means all of them. */
2914 /** @api author
2915 * Topics available in this state, or nil for all matching topics. */
2916 only = nil
2917
2918 /** @api author
2919 * Prefer this state during initial state selection. */
2920 isInitState = nil
2921
2922 specialDesc = nil
2923 /* Whether `specialDesc` says anything; an emitting property cannot be asked. */
2924 hasSpecialDesc = nil
2925
2926 /** @api author
2927 * Decide whether this actor accepts an issued command. */
2928 obeyCommand(issuer, a) { return nil; }
2929 /* Reacting to what happens, with the state rather than with the actor. */
2930 /** @api author
2931 * React before a resolved action is performed. */
2932 beforeAction(c) { return nil; }
2933 /** @api author
2934 * React after a resolved action is performed. */
2935 afterAction(c) { return nil; }
2936 /** @api author
2937 * React before the actor travels. */
2938 beforeTravel(traveller, conn) { return nil; }
2939 /** @api author
2940 * React after the actor travels. */
2941 afterTravel(traveller, conn) { return nil; }
2942 /* Called as this state becomes, and stops being, the actor's. */
2943 /** @api author
2944 * Run when this state becomes active for the actor. */
2945 activateState(a, oldState) { return nil; }
2946 /** @api author
2947 * Run when this state stops being active for the actor. */
2948 deactivateState(a, newState) { return nil; }
2949
2950 /** @api author
2951 * Run this state's background activity on a completed turn. */
2952 takeTurn() { return nil; }
2953 /* Whether this state takes the departure message over; see below. */
2954 saysDeparting = nil
2955 sayDeparting(a, way, conn) { return nil; }
2956 /* Whether this state travels with somebody; see `AccompanyingState`. */
2957 accompanyTravel(leadActor, conn) { return nil; }
2958 getAccompanyingTravelState(leadActor, conn) { return nil; }
2959 ;
2960
2961 /** @api author
2962 * An actor state that declines ordinary conversation. */
2963 class HermitActorState: ActorState
2964 noResponse = nil
2965
2966 hasNoResponse = nil
2967 /* Nothing gets through, so no topic does either. */
2968 only = []
2969 ;
2970
2971 /** @api author
2972 * An ActorState that follows an explicitly stored leader during travel. */
2973 class AccompanyingState: ActorState
2974 /* The stored leader is authoritative and inspectable. The hook may
2975 * decline a particular journey, but cannot choose a different leader. */
2976 /** @api author
2977 * Actor followed by this state's actor. */
2978 accompanyingActor = player
2979 accompanyTravel(leadActor, conn) { return true; }
2980 /* The state to wear for the journey itself. */
2981 getAccompanyingTravelState(leadActor, conn) { return nil; }
2982 ;
2983
2984 /** @api author
2985 * Temporary state used while an actor follows a leader through travel. */
2986 class AccompanyingInTravelState: ActorState
2987 saysDeparting = true
2988 /* Where to go back to once the journey is over. */
2989 stateAfter = nil
2990 sayDeparting(a, way, conn)
2991 {
2992 sayFor('travel.comeswith', a, nil);
2993 "\n";
2994 return nil;
2995 }
2996 ;
2997
2998 /** @api author
2999 * An accompanying state that leads another actor toward a tour destination. */
3000 class GuidedTourState: AccompanyingState
3001 accompanyingActor = nil
3002 escortActor = player
3003 /* The room this leg of the tour leads to. */
3004 escortDest = nil
3005 /* The state for the next stop. */
3006 stateAfterEscort = nil
3007 /* A tour leads rather than follows, so it does not wait to be taken. */
3008 accompanyTravel(leadActor, conn) { return nil; }
3009 /* And it goes first, which is what the guide is for. */
3010 saysDeparting = true
3011 sayDeparting(a, way, conn)
3012 {
3013 sayFor('travel.leadsway', a, nil);
3014 "\n";
3015 return nil;
3016 }
3017 ;
3018
3019 /*
3020 * One thing an actor will talk about. Matched by the entity the player named,
3021 * so ASK SAILOR ABOUT LIGHTHOUSE finds the entry whose matchObj is the
3022 * lighthouse — topics are entities, which the vocabulary already resolves.
3023 */
3024
3025 /** @api author
3026 * A vocabulary-resolved subject used by ask and tell dialogue. */
3027 class Topic: Thing
3028 isFamiliar = true
3029 isDecoration = true
3030 isFixed = true
3031 ;
3032
3033 /* And a Distant says *too far off*, which the reach check answers first. */
3034
3035 /** @api author
3036 * An ask or tell response matched to a topic and optionally an actor state. */
3037 class TopicEntry: object
3038 /** @api author
3039 * Actor who offers this topic response. */
3040 actor = nil
3041 /** @api author
3042 * Topic or object matched by this entry. */
3043 matchObj = nil
3044 /* Only offered while the actor is in this state; nil means always. */
3045 /** @api author
3046 * Restrict the entry to this actor state, or nil for any state. */
3047 inState = nil
3048 /** @api author
3049 * Match ask exchanges when true and tell exchanges otherwise. */
3050 isAsk = true
3051
3052 /** @api author
3053 * Message id used for the response. */
3054 id = nil
3055 /* Or the words themselves, for a game that only ever runs in a terminal. */
3056 /** @api author
3057 * Literal response text for a terminal-only game. */
3058 reply = nil
3059
3060 /** @api author
3061 * Advance a turn after delivering the topic response. */
3062 waits = nil
3063
3064 /** @api author
3065 * Whether this entry currently participates in topic matching. */
3066 isActive = true
3067 /* Said once, then this entry steps aside for the next. */
3068 /** @api author
3069 * Retire this topic entry after its first accepted exchange. */
3070 once = nil
3071 /** @api author
3072 * Library-maintained record of whether this entry was accepted. */
3073 used = nil
3074 ;
3075
3076 /* ----------------------------------------------------------------- states */
3077
3078 /** @api author
3079 * Synchronises vocabulary with an observed property condition. */
3080 class State: object
3081 /* Whose condition this is. */
3082 item = nil
3083
3084 appliesTo = nil
3085 /* And a further condition, for a class only some of whose instances have it. */
3086 applies(it) { return true; }
3087 /* The property it watches, as an address: `&isSharpened`. */
3088 watches = nil
3089 /* The adjective it answers to when that property is true, and when not.
3090 * Either may be nil, for a condition only one side of which has a word. */
3091 whenTrue = nil
3092 whenFalse = nil
3093 /* What it last wrote, so a change takes the old word away again. */
3094 said = nil
3095 /* The same, per thing, when the state covers a whole class. */
3096 saidFor = []
3097 saidItems = []
3098 /* What this state last wrote for one thing. */
3099 wordFor(it)
3100 {
3101 local at = saidItems.indexOf(it);
3102 return at == nil ? nil : saidFor[at];
3103 }
3104 noteWord(it, word)
3105 {
3106 local at = saidItems.indexOf(it);
3107 if (at == nil)
3108 {
3109 saidItems = saidItems + [it];
3110 saidFor = saidFor + [word];
3111 return nil;
3112 }
3113 local rebuilt = [];
3114 for (local i = 1; i <= saidFor.length(); ++i)
3115 rebuilt = rebuilt + [i == at ? word : saidFor[i]];
3116 saidFor = rebuilt;
3117 return nil;
3118 }
3119 /* The adjective this state gives that thing as it stands, or nil. */
3120 wordNow(it)
3121 {
3122 if (watches == nil)
3123 return nil;
3124 return it.(watches) ? whenTrue : whenFalse;
3125 }
3126 ;
3127
3128 /*
3129 * Bring every state's vocabulary into line with the world.
3130 *
3131 * Idempotent: a state whose word has not changed writes nothing, so this costs
3132 * one property read per state on a turn where nothing happened.
3133 */
3134 syncStates()
3135 {
3136 for (local s = firstObj(State); s != nil; s = nextObj(s, State))
3137 {
3138 if (s.watches == nil)
3139 continue;
3140 /* One thing's own condition. */
3141 if (s.item != nil)
3142 {
3143 local wanted = s.wordNow(s.item);
3144 if (wanted == s.said)
3145 continue;
3146 if (s.said != nil)
3147 vocab.unset(s.item, s.said, &adjective);
3148 if (wanted != nil)
3149 vocab.set(s.item, wanted, &adjective);
3150 s.said = wanted;
3151 continue;
3152 }
3153 /* Or every instance of a class that the state applies to. */
3154 if (s.appliesTo == nil)
3155 continue;
3156 for (local it = firstObj(s.appliesTo); it != nil;
3157 it = nextObj(it, s.appliesTo))
3158 {
3159 if (!s.applies(it))
3160 continue;
3161 local wanted = s.wordNow(it);
3162 local was = s.wordFor(it);
3163 if (wanted == was)
3164 continue;
3165 if (was != nil)
3166 vocab.unset(it, was, &adjective);
3167 if (wanted != nil)
3168 vocab.set(it, wanted, &adjective);
3169 s.noteWord(it, wanted);
3170 }
3171 }
3172 return nil;
3173 }
3174
3175 /** @api author
3176 * A scheduled item of actor behaviour. */
3177 class AgendaItem: Daemon
3178 /* Only runs while the actor is in this state. */
3179 inState = nil
3180 actor = nil
3181
3182 isReady = true
3183 isDone = nil
3184
3185 invokeItem() { return nil; }
3186
3187 offstage = nil
3188 isDue
3189 {
3190 if (isDone)
3191 return nil;
3192 if (!isReady)
3193 return nil;
3194 if (inState != nil && (actor == nil || actor.curState != inState))
3195 return nil;
3196 if (!offstage && (actor == nil || !visible(actor)))
3197 return nil;
3198 return true;
3199 }
3200 ;
3201
3202 /** @api author
3203 * An agenda item that accounts for conversation activity. */
3204 class ConvAgendaItem: AgendaItem
3205 isReady = (actor != nil && !actor.talkedThisTurn)
3206 ;
3207
3208 addToAgenda(a, item)
3209 {
3210 if (item == nil)
3211 return nil;
3212 item.actor = a;
3213 item.isActive = true;
3214 item.isDone = nil;
3215 return nil;
3216 }
3217
3218 removeFromAgenda(a, item)
3219 {
3220 if (item != nil)
3221 item.isActive = nil;
3222 return nil;
3223 }
3224
3225 topicAvailableTo(t, asker)
3226 {
3227 return t.isActive && asker.knowsAbout(t.matchObj)
3228 && (t.inState == nil || t.actor.curState == t.inState)
3229 && !(t.once && t.used);
3230 }
3231
3232 topicFor(actor, about, asking)
3233 {
3234 for (local t = firstObj(TopicEntry); t != nil; t = nextObj(t, TopicEntry))
3235 {
3236 if (t.actor != actor || t.matchObj != about)
3237 continue;
3238 if (!topicAvailableTo(t, player))
3239 continue;
3240 if (t.isAsk != asking)
3241 continue;
3242 if (t.inState != nil && actor.curState != t.inState)
3243 continue;
3244 if (t.once && t.used)
3245 continue;
3246 return t;
3247 }
3248 return nil;
3249 }
3250
3251 /* ---------------------------------------------------------------- gadgets */
3252
3253 /** @api author
3254 * Base for manipulable controls such as switches and buttons. */
3255 class Gadget: Thing
3256 isFixed = true
3257 /* What turning, pushing or flipping it does. */
3258 activate() { return nil; }
3259 ;
3260
3261 /* On or off. */
3262 /** @api author
3263 * A binary control operated by turn-on and turn-off actions. */
3264 class Switch: Gadget
3265
3266 presentation = [&name, &isOpen, &isOpenable, &isContainer, &isLit,
3267 &isLightSource, &isFixed, &isDecoration, &isTransparent,
3268 &isHidden, &bulk, &bulkCapacity, &objPos,
3269 &isOn]
3270 isOn = nil
3271 desc = nil
3272 ;
3273
3274 /* Up or down. */
3275 /** @api author
3276 * A control with lever-specific action reporting. */
3277 class Lever: Gadget
3278
3279 presentation = [&name, &isOpen, &isOpenable, &isContainer, &isLit,
3280 &isLightSource, &isFixed, &isDecoration, &isTransparent,
3281 &isHidden, &bulk, &bulkCapacity, &objPos,
3282 &isUp]
3283 isUp = true
3284 desc = nil
3285 ;
3286
3287 /* Set to one of several positions. */
3288 /** @api author
3289 * A control whose setting can be changed by the player. */
3290 class Dial: Gadget
3291
3292 presentation = [&name, &isOpen, &isOpenable, &isContainer, &isLit,
3293 &isLightSource, &isFixed, &isDecoration, &isTransparent,
3294 &isHidden, &bulk, &bulkCapacity, &objPos,
3295 &setting]
3296 setting = nil
3297 validSettings = []
3298 desc = nil
3299 ;
3300
3301 /* Pressed, and does whatever it does. */
3302 /** @api author
3303 * A control operated by pressing. */
3304 class Button: Gadget
3305 desc = nil
3306 ;
3307
3308 Actor template'name';
3309 Gadget template'name';
3310 Switch template'name';
3311 Lever template'name';
3312 Dial template'name';
3313 Button template'name';
3314
3315 /** @api author
3316 * A thing that can be eaten. */
3317 class Food: Thing
3318 isEdible = true
3319 ;
3320
3321 Food template'name';
3322
3323 /* Worn rather than merely carried. */
3324 /** @api author
3325 * A thing that can be worn and removed. */
3326 class Wearable: Thing
3327
3328 presentation = [&name, &isOpen, &isOpenable, &isContainer, &isLit,
3329 &isLightSource, &isFixed, &isDecoration, &isTransparent,
3330 &isHidden, &bulk, &bulkCapacity, &objPos,
3331 &isWorn]
3332 isWearable = true
3333 isWorn = nil
3334 ;
3335
3336 /*
3337 * Something you go into, which takes you somewhere. The oak you climb and the
3338 * chute you slide down are both this.
3339 */
3340 /** @api author
3341 * A fixed object an actor can enter. */
3342 class Enterable: Fixture
3343 isEnterable = true
3344 destination = nil
3345 /* An emitting string, like desc. */
3346 enterDesc = nil
3347 ;
3348
3349 /*
3350 * In several rooms at once. Its rows are in presentIn rather than contains,
3351 * because containment answers one container and this thing has no one room.
3352 */
3353 /** @api author
3354 * A fixture present in multiple rooms through presentIn relation rows. */
3355 class MultiLoc: Fixture
3356 isMultiLoc = true
3357
3358 locationList = []
3359 ;
3360
3361 TravelConnector template'name';
3362 Passage template'name';
3363 PathPassage template'name';
3364 StairwayUp template'name';
3365 StairwayDown template'name';
3366 Door template'name';
3367 SecretDoor template'name';
3368 Vehicle template'name';
3369 Heavy template'name';
3370 Fixture template'name';
3371 Decoration template'name';
3372 Surface template'name';
3373 Container template'name';
3374 OpenableContainer template'name';
3375 LockableContainer template'name';
3376 RearContainer template'name';
3377 BagOfHolding template'name';
3378 Platform template'name';
3379 Booth template'name';
3380 Wearable template'name';
3381 Enterable template'name';
3382 MultiLoc template'name';
3383
3384 class PreinitObject: object execBeforeMe = [];
3385
3386 /*
3387 * A MultiLoc names the rooms it stands in and vhyl writes the rows, for the
3388 * same reason a door does: where a thing is belongs to the declaration, and
3389 * turning that into table rows is the library's job.
3390 */
3391 multiLocPreinit: PreinitObject
3392 execute()
3393 {
3394 for (local m = firstObj(MultiLoc); m != nil; m = nextObj(m, MultiLoc))
3395 {
3396 foreach (local room in m.locationList)
3397 presentIn.set(m, room);
3398 }
3399 return nil;
3400 }
3401 ;
3402
3403 /* The room on a door's far side, from the one you are standing in. */
3404 doorDestination(door, from)
3405 {
3406 foreach (local side in connects.all(door))
3407 {
3408 if (side != from)
3409 return side;
3410 }
3411 return nil;
3412 }
3413
3414 /* A connector is in scope from either of the rooms it joins. */
3415 connectorHere(item)
3416 {
3417 if (!connects.contains(item, here()))
3418 return nil;
3419 return !item.isHidden;
3420 }
3421
3422 /* ---------------------------------------------------------- action helpers */
3423
3424 doInventory()
3425 {
3426 local held = [for item in contains.all(player) : item.name];
3427 if (held.length() == 0)
3428 {
3429 sayMsg('inventory.none', nil);
3430 "\n";
3431 }
3432 else
3433 {
3434 sayMsg('inventory.some', nil);
3435 foreach (local item in contains.all(player))
3436 {
3437 "\n ";
3438 sayFor(item.isWorn ?'inventory.itemworn' :'inventory.item',
3439 item, nil);
3440 }
3441 "\n";
3442 }
3443 return nil;
3444 }
3445
3446 doGo(way)
3447 {
3448
3449 if (way == outward && nestedIn() != nil)
3450 return doGetOff();
3451 local target = exits.get(here(), way);
3452
3453 if (target != nil && target.isHidden)
3454 target = nil;
3455 if (target == nil)
3456 {
3457 sayMsg('go.nowhere', nil);
3458 "\n";
3459 return nil;
3460 }
3461 local to = target;
3462 if (target.ofKind(TravelConnector))
3463 {
3464
3465 local stage = target.connectorStagingLocation;
3466 if (stage != nil)
3467 {
3468 if (location.get(player) != stage)
3469 {
3470 if (!tryImplied(verbForPosture(target.connectorStagingPosture),
3471 stage, nil, nil))
3472 {
3473 sayFor('stage.notthere', stage, nil);
3474 "\n";
3475 return nil;
3476 }
3477 }
3478 else if (player.posture != target.connectorStagingPosture)
3479 {
3480 player.posture = target.connectorStagingPosture;
3481 }
3482 }
3483
3484 if (target.isOutOfReach && !target.canObjReachSelf(player))
3485 {
3486 sayFor('reach.toohigh', target, nil);"\n";
3487 return nil;
3488 }
3489 if (target.ofKind(Door) && !target.isOpen)
3490 {
3491
3492 if (!tryImplied(23, target, nil, nil))
3493 {
3494 sayFor('go.shut', target, nil);"\n";
3495 return nil;
3496 }
3497 }
3498 if (!target.canPass)
3499 {
3500 if (target.barrierDesc != nil)
3501 "<<target.barrierDesc>>\n";
3502 else
3503 {
3504 sayFor('go.barred', target, nil);
3505 "\n";
3506 }
3507 return nil;
3508 }
3509
3510 if (target.bulkLimit > 0 && carriedBulk() > target.bulkLimit)
3511 {
3512 sayFor('go.toobulky', target, nil);
3513 "\n";
3514 return nil;
3515 }
3516 if (target.hasTravelDesc)
3517 {
3518 target.travelDesc;
3519 "\n";
3520 }
3521 to = target.ofKind(Door) ? doorDestination(target, here()) : target.destination;
3522 if (to == nil)
3523 {
3524 sayFor('go.leadsnowhere', target, nil);"\n";
3525 return nil;
3526 }
3527 }
3528
3529 local from = here();
3530 local riding = location.get(player);
3531 if (riding != nil && riding.isVehicle && !riding.canEnter(to))
3532 {
3533 sayFor('ride.wontgo', riding, nil);"\n";
3534 return nil;
3535 }
3536 if (to == from || player.travelCycle == travelActions.cycle)
3537 return nil;
3538 local escort = accompanists(player, from, target, to);
3539 if (escort == nil)
3540 return nil;
3541 player.travelCycle = travelActions.cycle;
3542 foreach (local a in escort)
3543 a.travelCycle = travelActions.cycle;
3544 foreach (local a in escort)
3545 {
3546 wearTravelState(a, target);
3547 a.sayDeparting(way, target);
3548 relocateActor(a, to);
3549 }
3550
3551 foreach (local a in escort)
3552 {
3553 local worn = a.curState;
3554 if (worn != nil && worn.ofKind(GuidedTourState)
3555 && worn.stateAfterEscort != nil)
3556 a.setCurState(worn.stateAfterEscort);
3557 }
3558 /*
3559 * Ride what you are in. The player's own container is a containment row
3560 * like any other, so a vehicle travels by moving the thing the player is
3561 * inside and leaving the player inside it.
3562 */
3563 if (riding != nil && riding.isVehicle)
3564 relocateActor(riding, to);
3565 else
3566 relocateActor(player, to);
3567
3568 if (target.ofKind(TravelConnector) && target.onTravel != nil)
3569 target.onTravel;
3570 describeRoom();
3571 finishAccompaniment(escort, nil);
3572 return nil;
3573 }
3574
3575 /*
3576 * The actors in this room travelling with this leader, breadth first.
3577 *
3578 * A state says whether it comes along, and a guided tour says so only for the
3579 * leg of the tour it is on — which is what stops a guide following the player
3580 * off the route.
3581 */
3582 accompanists(leader, from, conn, dest)
3583 {
3584 local going = [];
3585 local leaders = [leader];
3586 local candidates = contains.all(from);
3587 local at = 1;
3588 while (at <= leaders.length())
3589 {
3590 local lead = leaders[at];
3591 foreach (local a in candidates)
3592 {
3593 if (!a.isActor || a == leader || a.curState == nil
3594 || holds(leaders, a))
3595 continue;
3596 local state = a.curState;
3597 if (state.ofKind(GuidedTourState))
3598 {
3599 if (state.escortActor != lead
3600 || (state.escortDest != conn && state.escortDest != dest))
3601 continue;
3602 }
3603 else if (state.accompanyingActor != lead
3604 || !state.accompanyTravel(lead, conn))
3605 continue;
3606 if (leaders.length() >= 64
3607 || a.travelCycle == travelActions.cycle
3608 || !a.canAccompanyTravel(lead, conn, dest))
3609 return nil;
3610 going = going + [a];
3611 leaders = leaders + [a];
3612 }
3613 at = at + 1;
3614 }
3615 return going;
3616 }
3617
3618 /* A movement fact is independent of prose and visibility. Fiat relocation
3619 * uses this too, but deliberately gathers nobody and consumes no action. */
3620 relocateActor(a, dest)
3621 {
3622 local from = location.get(a);
3623 if (from == dest)
3624 return nil;
3625 contains.set(dest, a);
3626 event('actor.move');
3627 eventSubject(a);
3628 eventSubject(dest);
3629 if (from != nil)
3630 eventSubject(from);
3631 return true;
3632 }
3633
3634 wearTravelState(a, conn)
3635 {
3636 local state = a.curState;
3637 local lead = state.ofKind(GuidedTourState) ? state.escortActor : state.accompanyingActor;
3638 local worn = state.getAccompanyingTravelState(lead, conn);
3639 if (worn != nil)
3640 {
3641 worn.stateAfter = state;
3642 worn.accompanyingActor = state.accompanyingActor;
3643 worn.escortActor = state.escortActor;
3644 a.setCurState(worn);
3645 }
3646 return nil;
3647 }
3648
3649 finishAccompaniment(escort, advanceTours)
3650 {
3651 foreach (local a in escort)
3652 {
3653 local worn = a.curState;
3654 if (worn != nil && worn.ofKind(AccompanyingInTravelState))
3655 {
3656 local back = worn.stateAfter;
3657 if (back != nil && back.ofKind(GuidedTourState)
3658 && back.stateAfterEscort != nil)
3659 back = back.stateAfterEscort;
3660 a.setCurState(back);
3661 }
3662 else if (advanceTours && worn != nil && worn.ofKind(GuidedTourState)
3663 && worn.stateAfterEscort != nil)
3664 a.setCurState(worn.stateAfterEscort);
3665 }
3666 return nil;
3667 }
3668
3669 doBoard(c, item)
3670 {
3671 if (!item.isVehicle)
3672 {
3673 sayFor('board.cannot', item, nil);
3674 "\n";
3675 }
3676 else if (location.get(player) == item)
3677 {
3678 sayFor('board.already', item, nil);
3679 "\n";
3680 }
3681 else
3682 {
3683 contains.set(item, player);
3684 noteDone(c, item);
3685 }
3686 return nil;
3687 }
3688
3689 doGetOff()
3690 {
3691 local on = location.get(player);
3692 if (on == nil || !(on.isVehicle || on.isEnterable))
3693 {
3694 sayFor('getoff.nothing', nil, nil);
3695 "\n";
3696 }
3697 else
3698 {
3699 /* Out to whatever held it, which may itself be a nested space. */
3700 local out = location.get(on);
3701 contains.set(out == nil ? here() : out, player);
3702 sayFor(on.objPos == posIn ?'getoff.outof' :'getoff.off', on, nil);"\n";
3703 }
3704 return nil;
3705 }
3706
3707 /* The verb that puts somebody into a posture, for an implied command. */
3708 verbForPosture(want)
3709 {
3710 if (want == sitting)
3711 return 44;
3712 if (want == lying)
3713 return 45;
3714 return 43;
3715 }
3716
3717 /* What the player is in or on, if anything short of the room. */
3718 nestedIn()
3719 {
3720 local where = location.get(player);
3721 if (where == nil || where.ofKind(Room))
3722 return nil;
3723 return where;
3724 }
3725
3726 doEnter(c, item)
3727 {
3728 if (item.isVehicle)
3729 return doBoard(c, item);
3730 if (!item.isEnterable)
3731 {
3732 sayFor('enter.cannot', item, nil);"\n";
3733 return nil;
3734 }
3735
3736 if (item.isLockable && item.isLocked)
3737 {
3738 sayFor('enter.locked', item, nil);"\n";
3739 return nil;
3740 }
3741 if (item.isOpenable && !item.isOpen)
3742 {
3743 sayFor('enter.shut', item, nil);"\n";
3744 return nil;
3745 }
3746 if (item.destination != nil)
3747 {
3748 item.enterDesc;
3749 contains.set(item.destination, player);
3750 describeRoom();
3751 return nil;
3752 }
3753 contains.set(item, player);
3754 /*
3755 * Just read it. An emitting property displays when read and answers nil, so
3756 * testing it for nil would print it and then print the default as well —
3757 * which is why the default lives on the class instead of here.
3758 */
3759 item.enterDesc;
3760 "\n";
3761 return nil;
3762 }
3763
3764 doPush(item, way)
3765 {
3766 if (!item.canPushTravel)
3767 {
3768 sayFor('push.cannot', item, nil);
3769 "\n";
3770 }
3771 else
3772 {
3773 local target = exits.get(here(), way);
3774 if (target == nil)
3775 {
3776 sayMsg('go.nowhere', nil);
3777 "\n";
3778 }
3779 else if (target.ofKind(TravelConnector))
3780 {
3781 sayFor('push.throughconnector', item, target);
3782 "\n";
3783 }
3784 else
3785 {
3786 contains.set(target, item);
3787 contains.set(target, player);
3788 sayFor('push.ok', item, nil);"\n";
3789 describeRoom();
3790 }
3791 }
3792 return nil;
3793 }
3794
3795 doPutOn(c, item, onto)
3796 {
3797 /* PUT X ON COOKER means the hob, if the cooker says so. */
3798 local named = onto;
3799 if (onto.remapOn != nil)
3800 onto = onto.remapOn;
3801 if (!onto.isContainer)
3802 {
3803 sayFor('puton.cannot', onto, nil);
3804 "\n";
3805 }
3806 else if (item.isFixed)
3807 {
3808 sayFor('put.wontmove', item, nil);
3809 "\n";
3810 }
3811 else if (item == onto)
3812 {
3813 sayMsg('put.initself', nil);
3814 "\n";
3815 }
3816 else if (!roomFor(onto, item))
3817 {
3818 sayFor('puton.noroom', onto, nil);
3819 "\n";
3820 }
3821 else if (onto.notifyInsert(item) != nil)
3822 "<<onto.notifyInsert(item)>>\n";
3823 else
3824 {
3825 contains.set(onto, item);
3826 /* Where it came to rest decides which account is given, so the report
3827 * phase is told rather than left to work it out again. */
3828 c.way_ = onto.objPos == posOn ?'puton.ok' :'putin.ok';
3829 c.iobj = named;
3830 noteDone(c, item);
3831 }
3832 return nil;
3833 }
3834
3835 /* How much the player has in their arms, for a passage with a limit. */
3836 carriedBulk()
3837 {
3838 local used = 0;
3839 foreach (local item in contains.all(player))
3840 {
3841 if (!item.isWorn)
3842 used = used + item.bulk;
3843 }
3844 return used;
3845 }
3846
3847 /* How much is already inside something. */
3848 bulkIn(container)
3849 {
3850 local used = 0;
3851 foreach (local item in contains.all(container))
3852 used = used + item.bulk;
3853 return used;
3854 }
3855
3856 /* Whether one more thing fits. A capacity of 0 means no limit is stated. */
3857 roomFor(container, item)
3858 {
3859 /* A narrow neck refuses a big thing however empty it is. */
3860 if (container.maxSingleBulk != 0 && item.bulk > container.maxSingleBulk)
3861 return nil;
3862 if (container.bulkCapacity == 0)
3863 return true;
3864 return bulkIn(container) + item.bulk <= container.bulkCapacity;
3865 }
3866
3867 doPutIn(c, item, into)
3868 {
3869 /*
3870 * PUT X IN COOKER means the oven, if the cooker says so. The message names
3871 * what the player named: "in the cooker", not "in the oven space of the
3872 * cooker", which is what leaks if the part answers for itself.
3873 */
3874 local named = into;
3875 if (into.remapIn != nil)
3876 into = into.remapIn;
3877 if (!into.isContainer)
3878 {
3879 sayFor('putin.cannot', into, nil);
3880 "\n";
3881 }
3882 else if (into.isOpenable && !into.isOpen)
3883 {
3884 sayFor('putin.shut', into, nil);
3885 "\n";
3886 }
3887 else if (item.isFixed)
3888 {
3889 sayFor('put.wontmove', item, nil);
3890 "\n";
3891 }
3892 else if (item == into)
3893 {
3894 sayMsg('put.initself', nil);
3895 "\n";
3896 }
3897 else if (!roomFor(into, item))
3898 {
3899 sayFor('putin.noroom', into, nil);
3900 "\n";
3901 }
3902 else if (into.notifyInsert(item) != nil)
3903 "<<into.notifyInsert(item)>>\n";
3904 else
3905 {
3906 contains.set(into, item);
3907 c.way_ = into.objPos == posOn ?'puton.ok'
3908 : into.objPos == posBehind ?'putbehind.ok' :'putin.ok';
3909 c.iobj = named;
3910 noteDone(c, item);
3911 }
3912 return nil;
3913 }
3914
3915 /* ----------------------------------------------------------------- events */
3916
3917 /** @api author
3918 * Schedules a property call on an owner after completed turns. */
3919 class Event: object
3920 /* Whose property is called when it fires, and which. */
3921 /** @api author
3922 * Object on which the event's property is called. */
3923 owner = nil
3924 /** @api author
3925 * Property reference called when the event fires. */
3926 prop = nil
3927 /* Turns until it fires. */
3928 /** @api author
3929 * Remaining completed turns before the next firing. */
3930 turnsLeft = 1
3931 /* 0 fires once and stops; n fires every n turns. */
3932 /** @api author
3933 * Number of turns between firings, or zero for a one-shot event. */
3934 interval = 0
3935 /** @api author
3936 * Whether this event participates in scheduling. */
3937 isActive = true
3938 /* Answered nil to skip a firing without stopping the event. */
3939 /** @api author
3940 * Observational condition that can skip a firing without stopping the event. */
3941 isDue = true
3942 ;
3943
3944 /* Fires once. */
3945 /** @api author
3946 * A one-shot scheduled event. */
3947 class Fuse: Event;
3948
3949 /* Fires again and again. */
3950 /** @api author
3951 * A scheduled event that repeats at its interval. */
3952 class Daemon: Event
3953 interval = 1
3954 ;
3955
3956 /** @api author
3957 * A recurring event conditioned on the player's location. */
3958 class SenseEvent: Daemon
3959 where = nil
3960 isDue { return where == nil || where == here(); }
3961 ;
3962
3963 /** @api author
3964 * An object whose execute hook runs during vhylStart. */
3965 class InitObject: object
3966 execute() { return nil; }
3967 ;
3968
3969 /* The turn the game is on, counted from the first command. */
3970 turnCount: object value = 0;
3971
3972 /* Set when the game is over, with what to say. */
3973 gameOver: object done = nil;
3974
3975 /*
3976 * End the game. The turn that calls this finishes; nothing after it runs.
3977 */
3978
3979 finishGame(why)
3980 {
3981 gameOver.done = true;
3982 "\n";
3983 if (messageFor(why) != nil)
3984 sayMsg(why, nil);
3985 else
3986 "<<why>>";
3987 "\n";
3988 return nil;
3989 }
3990
3991 /* Every event that is due, in declaration order. */
3992
3993 runActors()
3994 {
3995 for (local a = firstObj(Actor); a != nil; a = nextObj(a, Actor))
3996 {
3997 if (a == player)
3998 continue;
3999
4000 travelActions.backgroundActor = a;
4001 local acted = nil;
4002 for (local item = firstObj(AgendaItem); item != nil;
4003 item = nextObj(item, AgendaItem))
4004 {
4005 if (item.actor != a || !item.isActive)
4006 continue;
4007
4008 if (item.isDone)
4009 {
4010 item.isActive = nil;
4011 continue;
4012 }
4013 if (!item.isDue)
4014 continue;
4015 item.turnsLeft = item.turnsLeft - 1;
4016 if (item.turnsLeft > 0)
4017 continue;
4018 if (item.interval > 0)
4019 item.turnsLeft = item.interval;
4020 else
4021 item.isActive = nil;
4022 /* Due, and the actor has already spoken this turn: it has had its
4023 * countdown, and waits its turn rather than losing it. */
4024 if (acted)
4025 {
4026 item.turnsLeft = 1;
4027 item.isActive = true;
4028 continue;
4029 }
4030 if (item.owner != nil && item.prop != nil)
4031 {
4032 item.owner.(item.prop);
4033 acted = true;
4034 }
4035 else
4036 {
4037 item.invokeItem();
4038 acted = true;
4039 }
4040 if (gameOver.done)
4041 {
4042 travelActions.backgroundActor = nil;
4043 return nil;
4044 }
4045 }
4046
4047 if (!acted)
4048 {
4049 if (a.curState != nil)
4050 a.curState.takeTurn();
4051 else
4052 a.idleTurn();
4053 }
4054 a.talkedThisTurn = nil;
4055 travelActions.backgroundActor = nil;
4056 }
4057 return nil;
4058 }
4059
4060 runEvents()
4061 {
4062 for (local e = firstObj(Event); e != nil; e = nextObj(e, Event))
4063 {
4064 if (!e.isActive)
4065 continue;
4066
4067 if (e.ofKind(AgendaItem))
4068 continue;
4069 e.turnsLeft = e.turnsLeft - 1;
4070 if (e.turnsLeft > 0)
4071 continue;
4072 if (e.interval > 0)
4073 e.turnsLeft = e.interval;
4074 else
4075 e.isActive = nil;
4076 /* A sense event fires only where it can be noticed. */
4077 if (e.isDue && e.owner != nil && e.prop != nil)
4078 e.owner.(e.prop);
4079 if (gameOver.done)
4080 return nil;
4081 }
4082 return nil;
4083 }
4084
4085 /* ------------------------------------------------------------------ doers */
4086
4087 /** @api author
4088 * Redirects or customises a resolved action for selected objects. */
4089 class Doer: object
4090 /* The action this catches, and optionally the object it must be about. */
4091 /** @api author
4092 * Action this Doer intercepts, or nil for any action. */
4093 forAction = nil
4094 /** @api author
4095 * Indirect object this Doer requires, or nil for any object. */
4096 forIobj = nil
4097 /** @api author
4098 * Direct object this Doer requires, or nil for any object. */
4099 forDobj = nil
4100 /* The action to run instead. Answer nil to leave the command alone. */
4101 /** @api author
4102 * Replacement action, or nil to leave the command unchanged. */
4103 instead = nil
4104 /* Whether this Doer applies to the command in hand. */
4105 catches(c)
4106 {
4107 if (forAction != nil && c.action != forAction)
4108 return nil;
4109 if (forIobj != nil && c.iobj != forIobj)
4110 return nil;
4111 if (forDobj != nil && c.dobj != forDobj)
4112 return nil;
4113 return true;
4114 }
4115 ;
4116
4117 /* The Doer that catches this command, if any. */
4118 findDoer(c)
4119 {
4120 for (local d = firstObj(Doer); d != nil; d = nextObj(d, Doer))
4121 {
4122 if (d.catches(c))
4123 return d;
4124 }
4125 return nil;
4126 }
4127
4128 /* ---------------------------------------------------------------- parsing */
4129
4130 grammar command(ordered):
4131 nounPhrase->actorPhrase_',' command->inner_ : Command
4132 ;
4133
4134 grammar command(look):'look' |'l' : Command action = lookAction;
4135 grammar command(waitCmd):'wait' |'z' : Command action = waitAction;
4136 grammar command(inventory):'inventory' |'i' : Command action = inventoryAction;
4137 grammar command(undoCmd):'undo' : Command action = undoAction;
4138 grammar command(getOff):'get''off' : Command action = getOffAction;
4139 grammar command(standUp):'stand' |'stand''up' |'get''up' : Command action = standAction;
4140 grammar command(sitDown):'sit' |'sit''down' : Command action = sitAction;
4141 grammar command(lieDown):'lie' |'lie''down' : Command action = lieAction;
4142 grammar command(standOn):
4143 'stand' ('on' |'onto') nounPhrase->dobj_ : Command action = standOnAction;
4144 grammar command(sitOn):
4145 'sit' ('on' |'in' |'onto') nounPhrase->dobj_ : Command action = sitOnAction;
4146 grammar command(lieOn):
4147 'lie' ('on' |'in' |'onto') nounPhrase->dobj_ : Command action = lieOnAction;
4148
4149 grammar command(examine):'examine' nounPhrase->dobj_ : Command action = examineAction;
4150 grammar command(x):'x' nounPhrase->dobj_ : Command action = examineAction;
4151 /* LOOK AT X is what a good many players type, and vhyl had only X and
4152 * EXAMINE. Badness, because LOOK on its own is the room. */
4153 grammar command(lookAt): [badness 5]'look''at' nounPhrase->dobj_ : Command action = examineAction;
4154
4155 grammar command(take): [badness 10]'take' nounPhrase->dobj_ : Command action = takeAction;
4156 grammar command(get):'get' nounPhrase->dobj_ : Command action = takeAction;
4157 grammar command(drop):'drop' nounPhrase->dobj_ : Command action = dropAction;
4158 grammar command(openIt):'open' nounPhrase->dobj_ : Command action = openAction;
4159 grammar command(closeIt):'close' nounPhrase->dobj_ : Command action = closeAction;
4160 grammar command(turnOn):'turn''on' nounPhrase->dobj_ : Command action = turnOnAction;
4161 grammar command(turnIt):'turn' nounPhrase->dobj_ : Command action = turnAction;
4162 grammar command(lightIt):'light' nounPhrase->dobj_ : Command action = turnOnAction;
4163 grammar command(turnOff):'turn''off' nounPhrase->dobj_ : Command action = turnOffAction;
4164 grammar command(board):'board' nounPhrase->dobj_ : Command action = boardAction;
4165 grammar command(rideIt):'ride' nounPhrase->dobj_ : Command action = boardAction;
4166 grammar command(enterIt):'enter' nounPhrase->dobj_ : Command action = enterAction;
4167 grammar command(climbIt):'climb' nounPhrase->dobj_ : Command action = enterAction;
4168 grammar command(wearIt):'wear' nounPhrase->dobj_ : Command action = wearAction;
4169 grammar command(removeIt):'remove' nounPhrase->dobj_ : Command action = doffAction;
4170 grammar command(takeOff):'take''off' nounPhrase->dobj_ : Command action = doffAction;
4171 grammar command(flipIt):'flip' nounPhrase->dobj_ : Command action = flipAction;
4172 grammar command(pullIt):'pull' nounPhrase->dobj_ : Command action = flipAction;
4173 grammar command(pressIt):'press' nounPhrase->dobj_ : Command action = pressAction;
4174 grammar command(pushBtn):'push' nounPhrase->dobj_ : Command action = pressAction;
4175
4176 grammar command(eatIt):'eat' nounPhrase->dobj_ : Command action = eatAction;
4177 grammar command(drinkIt):'drink' nounPhrase->dobj_ : Command action = drinkAction;
4178 grammar command(pourInto):'pour' nounPhrase->dobj_'into' nounPhrase->iobj_ : Command action = pourIntoAction;
4179 grammar command(pourIn):'pour' nounPhrase->dobj_'in' nounPhrase->iobj_ : Command action = pourIntoAction;
4180 grammar command(detachPart):'detach' nounPhrase->dobj_ : Command action = detachPartAction;
4181
4182 grammar command(giveTo):'give' nounPhrase->dobj_'to' nounPhrase->iobj_ : Command action = giveAction;
4183 grammar command(showTo):'show' nounPhrase->dobj_'to' nounPhrase->iobj_ : Command action = showAction;
4184 grammar command(unlockWith):'unlock' nounPhrase->dobj_'with' nounPhrase->iobj_ : Command action = unlockAction;
4185 grammar command(unlockIt):'unlock' nounPhrase->dobj_ : Command action = unlockAction;
4186 grammar command(lockWith):'lock' nounPhrase->dobj_'with' nounPhrase->iobj_ : Command action = lockAction;
4187 grammar command(lockIt):'lock' nounPhrase->dobj_ : Command action = lockAction;
4188
4189 grammar command(sayCmd):
4190 'say' literalPhrase->literal_ : Command action = sayAction;
4191 grammar command(writeOn):
4192 'write' literalPhrase->literal_'on' nounPhrase->dobj_
4193 : Command action = writeOnAction;
4194 grammar command(typeOn):
4195 'type' literalPhrase->literal_'on' nounPhrase->dobj_
4196 : Command action = writeOnAction;
4197 grammar command(thinkAbout):
4198 ('think''about' |'ponder') nounPhrase->dobj_
4199 : Command action = thinkAboutAction;
4200 grammar command(lookUpIn):
4201 'look''up' nounPhrase->iobj_'in' nounPhrase->dobj_
4202 : Command action = consultAction;
4203 grammar command(consultCmd):
4204 'consult' nounPhrase->dobj_'about' nounPhrase->iobj_
4205 : Command action = consultAction;
4206
4207 grammar command(setTo):'set' nounPhrase->dobj_'to' tokWord->set_ : Command action = setToAction;
4208 grammar command(turnToo):'turn' nounPhrase->dobj_'to' tokWord->set_ : Command action = setToAction;
4209 grammar command(attachTo):'attach' nounPhrase->dobj_'to' nounPhrase->iobj_ : Command action = attachAction;
4210 grammar command(detachFrom):'detach' nounPhrase->dobj_'from' nounPhrase->iobj_ : Command action = detachAction;
4211 grammar command(scoreCmd):'score' : Command action = scoreAction;
4212 grammar command(listenTo):'listen''to' nounPhrase->dobj_ : Command action = listenAction;
4213 grammar command(smellIt):'smell' nounPhrase->dobj_ : Command action = smellAction;
4214 grammar command(askAbout):'ask' nounPhrase->dobj_'about' nounPhrase->iobj_ : Command action = askAboutAction;
4215 grammar command(tellAbout):'tell' nounPhrase->dobj_'about' nounPhrase->iobj_ : Command action = tellAboutAction;
4216 grammar command(putOn):'put' nounPhrase->dobj_'on' nounPhrase->iobj_ : Command action = putOnAction;
4217 grammar command(putIn):'put' nounPhrase->dobj_'in' nounPhrase->iobj_ : Command action = putInAction;
4218 grammar command(putBehind):'put' nounPhrase->dobj_'behind' nounPhrase->iobj_ : Command action = putBehindAction;
4219 grammar command(lookBehind):'look''behind' nounPhrase->dobj_ : Command action = lookBehindAction;
4220 grammar command(lookUnder):'look''under' nounPhrase->dobj_ : Command action = lookUnderAction;
4221
4222 grammar command(gNorth): ('go'|) ('north' |'n') : Command action = goAction dir_ = north;
4223 grammar command(gSouth): ('go'|) ('south' |'s') : Command action = goAction dir_ = south;
4224 grammar command(gEast): ('go'|) ('east' |'e') : Command action = goAction dir_ = east;
4225 grammar command(gWest): ('go'|) ('west' |'w') : Command action = goAction dir_ = west;
4226 grammar command(gNE): ('go'|) ('northeast' |'ne') : Command action = goAction dir_ = northeast;
4227 grammar command(gNW): ('go'|) ('northwest' |'nw') : Command action = goAction dir_ = northwest;
4228 grammar command(gSE): ('go'|) ('southeast' |'se') : Command action = goAction dir_ = southeast;
4229 grammar command(gSW): ('go'|) ('southwest' |'sw') : Command action = goAction dir_ = southwest;
4230 grammar command(gUp): ('go'|) ('up' |'u') : Command action = goAction dir_ = up;
4231 grammar command(gDown): ('go'|) ('down' |'d') : Command action = goAction dir_ = down;
4232 grammar command(gIn): ('go'|) ('in' |'inside') : Command action = goAction dir_ = inward;
4233 grammar command(gOut): ('go'|) ('out' |'outside') : Command action = goAction dir_ = outward;
4234
4235 grammar command(pushN):'push' nounPhrase->dobj_'north' : Command action = pushDirAction dir_ = north;
4236 grammar command(pushS):'push' nounPhrase->dobj_'south' : Command action = pushDirAction dir_ = south;
4237 grammar command(pushE):'push' nounPhrase->dobj_'east' : Command action = pushDirAction dir_ = east;
4238 grammar command(pushW):'push' nounPhrase->dobj_'west' : Command action = pushDirAction dir_ = west;
4239
4240 class NounPhrase: object
4241 n_ = nil
4242 a1_ = nil
4243 a2_ = nil
4244
4245 adjs_ = nil
4246 of_ = nil
4247 left_ = nil
4248 trailing_ = nil
4249 also_ = nil
4250 /*
4251 * What an article is in front of. A field of its own rather than `left_`,
4252 * because *the lead and the chart* is an article in front of a **list**,
4253 * and a wrapper that shared a field with the list's left half hid it.
4254 */
4255 under_ = nil
4256
4257 pronoun_ = nil
4258 ;
4259
4260 grammar nounPhrase(itOne):
4261 [badness 5]'it' : NounPhrase pronoun_ ='it';
4262 grammar nounPhrase(themOne):
4263 [badness 5]'them' : NounPhrase pronoun_ ='them';
4264
4265 grammar nounPhrase(himOne):
4266 [badness 5]'him' : NounPhrase pronoun_ ='him';
4267 grammar nounPhrase(herOne):
4268 [badness 5]'her' : NounPhrase pronoun_ ='her';
4269 grammar nounPhrase(allOf):
4270 [badness 5]'all' : NounPhrase pronoun_ ='all';
4271 grammar nounPhrase(everything):
4272 [badness 5]'everything' : NounPhrase pronoun_ ='all';
4273
4274 /*
4275 * An article, which a player types without thinking about it and every parser
4276 * in the genre accepts. vhyl refused `TAKE THE TOKEN` and `ASK ZOÉ ABOUT THE
4277 * TAPE` until an author game tried to write a walkthrough in English.
4278 *
4279 * A production of its own rather than a word in each rule, so the three noun
4280 * phrase shapes stay three. Spelled `anArticle` because `article` is already
4281 * the language module's a/an chooser, and a production shares a namespace with
4282 * a function.
4283 */
4284
4285 class AdjWord: object w_ = nil;
4286
4287 grammar adjWord(fromVocab): vocab.adjective->w_ : AdjWord;
4288
4289 /* A run of them, so a phrase may carry as many as it likes. */
4290 class AdjRun: object one_ = nil rest_ = nil;
4291
4292 grammar adjRun(last): adjWord->one_ : AdjRun;
4293 grammar adjRun(more): adjWord->one_ adjRun->rest_ : AdjRun;
4294
4295 grammar anArticle(theOne):'the' : object;
4296 grammar anArticle(aOne):'a' : object;
4297 grammar anArticle(anOne):'an' : object;
4298
4299 grammar simpleNoun(bare):
4300 vocab.noun->n_ : NounPhrase;
4301 grammar simpleNoun(adjs):
4302 adjRun->adjs_ vocab.noun->n_ : NounPhrase;
4303
4304 grammar simpleNoun(nounThen):
4305 vocab.noun->n_ adjWord->trailing_ : NounPhrase;
4306 grammar simpleNoun(nounQuoted):
4307 vocab.noun->n_'"' adjWord->trailing_'"' : NounPhrase;
4308 grammar simpleNoun(quotedNoun):
4309 '"' adjWord->trailing_'"' vocab.noun->n_ : NounPhrase;
4310
4311 /*
4312 * `of` joins two simple names: *piece of paper*, *small pile of dry straw*.
4313 * The thing named is the one both halves name, which is what a game states by
4314 * giving it both words.
4315 */
4316 grammar simpleNoun(ofLink):
4317 simpleNoun->left_'of' simpleNoun->of_ : NounPhrase;
4318
4319 /* One thing, with or without an article. */
4320 grammar nounPhrase(one):
4321 simpleNoun->under_ : NounPhrase;
4322 grammar nounPhrase(artOne):
4323 anArticle simpleNoun->under_ : NounPhrase;
4324
4325 grammar nounPhrase(andMore):
4326 simpleNoun->left_ ('and' |',') nounPhrase->also_ : NounPhrase;
4327 grammar nounPhrase(artAndMore):
4328 anArticle simpleNoun->left_ ('and' |',') nounPhrase->also_ : NounPhrase;
4329
4330 /* ----------------------------------------------------- literals and topics */
4331
4332 class LiteralPhrase: object
4333 word_ = nil
4334 rest_ = nil
4335 ;
4336
4337 grammar literalPhrase(oneWord): tokWord->word_ : LiteralPhrase;
4338 grammar literalPhrase(moreWords): tokWord->word_ literalPhrase->rest_ : LiteralPhrase;
4339 grammar literalPhrase(quoted):'"' literalPhrase->rest_'"' : LiteralPhrase;
4340
4341 /* The words of a literal phrase, joined back into a line. */
4342 literalText(phrase)
4343 {
4344 if (phrase == nil)
4345 return'';
4346
4347 local head = phrase.word_ == nil || phrase.word_ =='"' ?'' : phrase.word_;
4348 local tail = literalText(phrase.rest_);
4349 if (head =='')
4350 return tail;
4351 return tail =='' ? head : head +' ' + tail;
4352 }
4353
4354 getLiteral(c)
4355 {
4356 if (c == nil)
4357 return nil;
4358 if (c.literal_ != nil)
4359 return literalText(c.literal_);
4360 return c.set_;
4361 }
4362
4363 getTopic(c)
4364 {
4365 return c == nil ? nil : c.iobj;
4366 }
4367
4368 getTopicText(c)
4369 {
4370 if (c == nil)
4371 return nil;
4372 if (c.topicText_ != nil)
4373 return literalText(c.topicText_);
4374 local about = c.iobj;
4375 return about == nil ? nil : about.name;
4376 }
4377
4378 holds(list, wanted)
4379 {
4380 if (list == nil)
4381 return nil;
4382 return list.indexOf(wanted) != nil;
4383 }
4384
4385 /* What is in both lists. */
4386 bothOf(left, right)
4387 {
4388 if (left == nil)
4389 return right;
4390 if (right == nil)
4391 return left;
4392 return [for item in left : item if holds(right, item)];
4393 }
4394
4395 /* The entities a phrase names: every word in it has to name them. */
4396 phraseCandidates(np)
4397 {
4398 if (np == nil)
4399 return [];
4400
4401 if (np.pronoun_ != nil)
4402 {
4403 if (np.pronoun_ =='all')
4404 {
4405 /*
4406 * Everything to hand — what is in the room and what you are
4407 * carrying. Which of those a verb means is the verb's business:
4408 * TAKE ALL means the room's things and DROP ALL means yours, and
4409 * the ranking says so already, so ALL does not have to.
4410 */
4411 local about = [for item in contains.all(here())
4412 : item if item != player && !item.isFixed && !item.isDecoration];
4413 foreach (local held in contains.all(player))
4414 {
4415 if (!held.isFixed && !held.isDecoration)
4416 about = about + [held];
4417 }
4418 return about;
4419 }
4420 local named = lastNamed.items == nil ? [] : lastNamed.items;
4421
4422 if (np.pronoun_ =='him' || np.pronoun_ =='her')
4423 {
4424 local wantHim = np.pronoun_ =='him';
4425 local fits = [for x in named : x if wantHim ? x.isHim : x.isHer];
4426 if (fits.length() > 0)
4427 return fits;
4428 local here_ = contains.all(here()) + contains.all(player);
4429 return [for x in here_ : x if (wantHim ? x.isHim : x.isHer)];
4430 }
4431 return named;
4432 }
4433 /*
4434 * `piece of paper`: the things both halves name. A game says so by giving
4435 * the object both words, which is what its vocabulary is for.
4436 */
4437 if (np.of_ != nil)
4438 return bothOf(phraseCandidates(np.left_), phraseCandidates(np.of_));
4439 /* A wrapper — an article, or the one-thing case — names what it wraps. */
4440 if (np.under_ != nil)
4441 return phraseCandidates(np.under_);
4442
4443 if (np.also_ != nil)
4444 {
4445 local all = [];
4446 foreach (local part in listedPhrases(np))
4447 {
4448 foreach (local item in phraseCandidates(part))
4449 {
4450 if (!holds(all, item))
4451 all = all + [item];
4452 }
4453 }
4454 return all;
4455 }
4456 local list = np.n_;
4457 /* The old pair, kept because a game may still set them directly. */
4458 if (np.a1_ != nil)
4459 list = bothOf(list, np.a1_);
4460 if (np.a2_ != nil)
4461 list = bothOf(list, np.a2_);
4462 /* However many adjectives the phrase carried, in the order written. */
4463 for (local run = np.adjs_; run != nil; run = run.rest_)
4464 {
4465 if (run.one_ != nil)
4466 list = bothOf(list, run.one_.w_);
4467 }
4468 if (np.trailing_ != nil)
4469 list = bothOf(list, np.trailing_.w_);
4470 return list == nil ? [] : list;
4471 }
4472
4473 lastNamed: object items = nil;
4474
4475 listedPhrases(np)
4476 {
4477
4478 if (np == nil)
4479 return [];
4480 /* An article in front of a list is still a list. */
4481 if (np.under_ != nil)
4482 return listedPhrases(np.under_);
4483 if (np.also_ == nil)
4484 return [np];
4485 return listedPhrases(np.left_) + listedPhrases(np.also_);
4486 }
4487
4488 firstNamed(candidates)
4489 {
4490 if (candidates == nil || candidates.length() == 0)
4491 return nil;
4492 foreach (local c in candidates)
4493 {
4494 if (player.knowsAbout(c))
4495 return c;
4496 }
4497 return candidates[1];
4498 }
4499
4500 rankOf(item, a, role)
4501 {
4502 local byAction = a.rank(item, role);
4503 local byThing = item.rank(a, role);
4504 return byAction < byThing ? byAction : byThing;
4505 }
4506
4507 chooseOne(candidates, a, role)
4508 {
4509
4510 local reachable = [for c in candidates : c if inScope(c) || (c.isDistant && visible(c))];
4511 if (reachable.length() == 0)
4512 return nil;
4513 if (reachable.length() == 1)
4514 return reachable[1];
4515 local best = nil;
4516 foreach (local option in reachable)
4517 {
4518 local score = rankOf(option, a, role);
4519 if (best == nil || score > best)
4520 best = score;
4521 }
4522 local likely = [for c in reachable : c if rankOf(c, a, role) == best];
4523 if (likely.length() == 1)
4524 return likely[1];
4525 sayMsg('parse.which', nil);
4526 foreach (local option in likely)
4527 sayAbout('parse.whichone', option, nil, tellApart(option, likely));
4528 "? ";
4529 local answer = inputLine();
4530 if (answer == nil)
4531 return nil;
4532 foreach (local option in likely)
4533 {
4534 local mark = tellApart(option, likely);
4535 if (option.name == answer
4536 || (mark != nil && (answer == mark
4537 || answer == mark +' ' + option.name)))
4538 return option;
4539 }
4540 sayMsg('parse.notone', nil);"\n";
4541 return nil;
4542 }
4543
4544 tellApart(item, others)
4545 {
4546 for (local st = firstObj(State); st != nil; st = nextObj(st, State))
4547 {
4548 if (st.watches == nil)
4549 continue;
4550 if (st.item != nil && st.item != item)
4551 continue;
4552 if (st.item == nil
4553 && (st.appliesTo == nil || !item.ofKind(st.appliesTo)
4554 || !st.applies(item)))
4555 continue;
4556 local mine = st.wordNow(item);
4557 if (mine == nil)
4558 continue;
4559 /* Only worth saying when another candidate it would be confused with
4560 * shares the name and differs in this condition. */
4561 foreach (local other in others)
4562 {
4563 if (other == item || other.name != item.name)
4564 continue;
4565 if (st.wordNow(other) != mine)
4566 return mine;
4567 }
4568 }
4569 return nil;
4570 }
4571
4572 readingRank(m)
4573 {
4574 local a = m.action;
4575 if (a == nil)
4576 return rankNever;
4577 if (!a.needsDobj)
4578 return rankLogical;
4579 local best = rankNever;
4580 foreach (local candidate in phraseCandidates(m.dobj_))
4581 {
4582 if (!inScope(candidate) && !candidate.isDistant)
4583 continue;
4584 local score = rankOf(candidate, a, dobjRole);
4585 if (score > best)
4586 best = score;
4587 }
4588 return best;
4589 }
4590
4591 bestReading(matches)
4592 {
4593 local best = matches[1];
4594 local bestSense = readingRank(best);
4595 local bestBad = best.badness;
4596 foreach (local m in matches)
4597 {
4598 local sense = readingRank(m);
4599 local bad = m.badness;
4600 if (sense > bestSense || (sense == bestSense && bad < bestBad))
4601 {
4602 best = m;
4603 bestSense = sense;
4604 bestBad = bad;
4605 }
4606 }
4607 return best;
4608 }
4609
4610 endedCommand()
4611 {
4612 sayMsg('game.ended', nil);
4613 "\n";
4614 return nil;
4615 }
4616
4617 /** @api host
4618 * Submit parser tokens to the library's text command cycle. */
4619 vhylTurn(toks)
4620 {
4621 if (!gameOver.done)
4622 travelActions.cycle = travelActions.cycle + 1;
4623 local m = command.parseTokens(toks, vhylDict);
4624 if (gameOver.done)
4625 {
4626 /* Recognise history navigation without ranking, resolving, asking an
4627 * actor, or running any action hooks in an ended world. */
4628 foreach (local reading in m)
4629 {
4630 if (reading.action == undoAction && reading.actorPhrase_ == nil)
4631 return undoAction.exec(reading);
4632 }
4633 return endedCommand();
4634 }
4635 if (m.length() == 0)
4636 {
4637 sayMsg('parse.nomatch', nil);
4638 "\n";
4639 return nil;
4640 }
4641 local c = bestReading(m);
4642 /* Undo is history navigation, not another actor/event turn. */
4643 if (c.action == undoAction)
4644 return undoAction.exec(c);
4645
4646 if (c.actorPhrase_ != nil)
4647 {
4648 local who = firstNamed(phraseCandidates(c.actorPhrase_));
4649 if (who == nil || !who.isActor || !inScope(who))
4650 {
4651 sayMsg('talk.notactor', nil);
4652 "\n";
4653 return nil;
4654 }
4655 local inner = c.inner_;
4656 if (inner == nil || !who.obeyCommand(player, inner.action))
4657 {
4658 sayFor('order.refused', who, nil);
4659 "\n";
4660 return nil;
4661 }
4662 c = inner;
4663 c.actorPhrase_ = nil;
4664 }
4665 local a = c.action;
4666 if (a == nil)
4667 {
4668 sayMsg('parse.nomatch', nil);
4669 "\n";
4670 return nil;
4671 }
4672 if (a.needsDobj)
4673 {
4674
4675 local listed = listedPhrases(c.dobj_);
4676 if (listed.length() > 1)
4677 {
4678 local named = [];
4679 foreach (local phrase in listed)
4680 {
4681 local one = a.dobjIsTopic ? firstNamed(phraseCandidates(phrase))
4682 : chooseOne(phraseCandidates(phrase), a, dobjRole);
4683 if (one == nil)
4684 {
4685 sayMsg(a.missingDobjMsg, c);
4686 "\n";
4687 return nil;
4688 }
4689 named = named + [one];
4690 }
4691 if (a.needsIobj)
4692 {
4693 c.iobj = a.iobjIsTopic ? firstNamed(phraseCandidates(c.iobj_))
4694 : chooseOne(phraseCandidates(c.iobj_), a, iobjRole);
4695 if (c.iobj == nil)
4696 {
4697 sayMsg(a.missingIobjMsg, c);
4698 "\n";
4699 return nil;
4700 }
4701 }
4702 return runCommandOver(c, named);
4703 }
4704
4705 local several = c.dobj_ != nil && c.dobj_.pronoun_ =='all';
4706 local candidates = phraseCandidates(c.dobj_);
4707 if (several)
4708 {
4709
4710 local best = rankNever;
4711 foreach (local item in candidates)
4712 {
4713 local score = inScope(item) ? rankOf(item, a, dobjRole) : rankNever;
4714 if (score > best)
4715 best = score;
4716 }
4717 local wanted = best <= rankNever ? []
4718 : [for item in candidates
4719 : item if inScope(item) && rankOf(item, a, dobjRole) == best];
4720 if (wanted.length() == 0)
4721 {
4722 sayMsg(a.missingDobjMsg, c);
4723 "\n";
4724 return nil;
4725 }
4726 return runCommandOver(c, wanted);
4727 }
4728 c.dobj = a.dobjIsTopic ? firstNamed(candidates)
4729 : chooseOne(candidates, a, dobjRole);
4730 if (c.dobj == nil)
4731 {
4732 sayMsg(a.missingDobjMsg, c);
4733 "\n";
4734 return nil;
4735 }
4736 }
4737 if (a.needsIobj)
4738 {
4739 c.iobj = a.iobjIsTopic
4740 ? firstNamed(phraseCandidates(c.iobj_))
4741 : chooseOne(phraseCandidates(c.iobj_), a, iobjRole);
4742 if (c.iobj == nil)
4743 {
4744 sayMsg(a.missingIobjMsg, c);
4745 "\n";
4746 return nil;
4747 }
4748 }
4749 return runCommand(c);
4750 }
4751
4752 runCommand(c)
4753 {
4754 return runCommandOver(c, c.dobj == nil ? nil : [c.dobj]);
4755 }
4756
4757 runCommandOver(c, items)
4758 {
4759 local a = c.action;
4760 c.done_ = nil;
4761 c.reports_ = nil;
4762 c.failed_ = nil;
4763 if (items == nil)
4764 return finishCommand(c, actOnce(c, a, nil));
4765 foreach (local item in items)
4766 {
4767 c.dobj = item;
4768 actOnce(c, a, item);
4769 if (gameOver.done)
4770 break;
4771 }
4772 return finishCommand(c, a);
4773 }
4774
4775 /* --------------------------------------------------------- preconditions */
4776
4777 /** @api author
4778 * Checks or establishes a requirement before an action executes. */
4779 class PreCondition: object
4780 met(item, a, c) { return true; }
4781 impliedVerb(item, a, c) { return nil; }
4782 /* What the implied command acts on, when it is not the thing itself. */
4783 impliedDobj(item, a, c) { return item; }
4784 impliedIobj(item, a, c) { return nil; }
4785 failMsg ='precond.cannot'
4786 ;
4787
4788 /* The command an implied action runs in, so the outer one is not disturbed. */
4789 impliedArgs: Command;
4790
4791 tryImplied(verb, item, into, outer)
4792 {
4793 local v = verbFor(verb);
4794 if (v == nil)
4795 return 0;
4796
4797 if (rankOf(item, v.action, dobjRole) < rankImplicit)
4798 return 0;
4799 local c = impliedArgs;
4800 c.action = v.action;
4801 c.dobj = item;
4802 c.iobj = into;
4803 c.dobj_ = nil;
4804 c.iobj_ = nil;
4805 c.set_ = nil;
4806 c.way_ = v.gerund();
4807 c.list_ = nil;
4808 c.dir_ = v.dir;
4809 c.done_ = nil;
4810 c.reports_ = nil;
4811 c.failed_ = nil;
4812
4813 c.implied_ = true;
4814 /*
4815 * `sayAbout` rather than `sayFor`, because the gerund is a **word** the
4816 * message needs and `sayFor` carries only entities.
4817 */
4818 local prep = v.preposition();
4819 sayAbout('precond.first', item, into == nil ? nil : [into],
4820 prep == nil ? v.gerund() : v.gerund() +' ' + prep);
4821 "\n";
4822 actOnce(c, c.action, item);
4823 if (c.done_ != nil && c.done_.length() > 0)
4824 c.action.report(c);
4825 sayReports(c);
4826 /* true when it worked, nil when it ran and refused, 0 when it never ran. */
4827 return !c.failed_ && c.done_ != nil && c.done_.length() > 0 ? true : nil;
4828 }
4829
4830 meetPreConds(c, a, item, role)
4831 {
4832 if (item == nil)
4833 return true;
4834 local conds = a.preCond(item, role) + item.preCond(a, role);
4835 if (conds.length() == 0)
4836 return true;
4837 for (local pass = 0; pass < 8; ++pass)
4838 {
4839 local unmet = nil;
4840 foreach (local p in conds)
4841 {
4842 if (!p.met(item, a, c))
4843 {
4844 unmet = p;
4845 break;
4846 }
4847 }
4848 if (unmet == nil)
4849 return true;
4850 local verb = unmet.impliedVerb(item, a, c);
4851 local tried = verb == nil ? 0
4852 : tryImplied(verb, unmet.impliedDobj(item, a, c),
4853 unmet.impliedIobj(item, a, c), c);
4854 if (tried != true)
4855 {
4856
4857 if (tried == 0)
4858 {
4859 sayFor(unmet.failMsg, item, c.iobj);
4860 "\n";
4861 }
4862 return nil;
4863 }
4864 }
4865 sayFor('precond.cannot', item, nil);
4866 "\n";
4867 return nil;
4868 }
4869
4870 /* Held directly by the player. Implies TAKE. */
4871 objHeld: PreCondition
4872 met(item, a, c) { return carried(item); }
4873 impliedVerb(item, a, c) { return 21; }
4874 failMsg ='precond.held'
4875 ;
4876
4877 /* Visible. Nothing implies being able to see. */
4878 objVisible: PreCondition
4879 met(item, a, c) { return visible(item); }
4880 failMsg ='precond.visible'
4881 ;
4882
4883 /* Open, if it is the kind of thing that shuts. Implies OPEN. */
4884 objOpen: PreCondition
4885 met(item, a, c) { return !item.isOpenable || item.isOpen; }
4886 impliedVerb(item, a, c) { return 23; }
4887 failMsg ='precond.open'
4888 ;
4889
4890 /* Shut. Implies CLOSE. */
4891 objClosed: PreCondition
4892 met(item, a, c) { return !item.isOpenable || !item.isOpen; }
4893 impliedVerb(item, a, c) { return 24; }
4894 failMsg ='precond.closed'
4895 ;
4896
4897 /* Unlocked. Implies UNLOCK, with no key named — the action finds one. */
4898 objUnlocked: PreCondition
4899 met(item, a, c) { return !item.isLockable || !item.isLocked; }
4900 impliedVerb(item, a, c) { return 41; }
4901 failMsg ='precond.unlocked'
4902 ;
4903
4904 /* Not being worn. Implies TAKE OFF. */
4905 objNotWorn: PreCondition
4906 met(item, a, c) { return !item.isWorn; }
4907 impliedVerb(item, a, c) { return 33; }
4908 failMsg ='precond.worn'
4909 ;
4910
4911 /* Nothing inside. Implies taking one thing out, and the loop does the rest. */
4912 objEmpty: PreCondition
4913 met(item, a, c) { return contains.all(item).length() == 0; }
4914 impliedVerb(item, a, c) { return 21; }
4915 impliedDobj(item, a, c)
4916 {
4917 local inside = contains.all(item);
4918 return inside.length() == 0 ? item : inside[1];
4919 }
4920 failMsg ='precond.empty'
4921 ;
4922
4923 touchObj: PreCondition
4924 met(item, a, c) { return blockingFor(item) == nil; }
4925 impliedVerb(item, a, c) { return 23; }
4926 impliedDobj(item, a, c)
4927 {
4928 local shut = blockingFor(item);
4929 return shut == nil ? item : shut;
4930 }
4931 failMsg ='precond.reach'
4932 ;
4933
4934 /*
4935 * The nearest shut container between the player and this thing, or nil.
4936 *
4937 * Nearest first, because opening the outer one is what lets you reach the
4938 * inner one, and the loop comes back for the rest.
4939 */
4940 blockingFor(item)
4941 {
4942 local shut = nil;
4943 foreach (local step in contains.ancestors(item))
4944 {
4945
4946 if (step.isOpenable && !step.isOpen && !step.isTransparent)
4947 shut = step;
4948 }
4949 return shut;
4950 }
4951
4952 replaceAction(verb, item, other)
4953 {
4954 local v = verbFor(verb);
4955 if (v == nil)
4956 return nil;
4957 local c = impliedArgs;
4958 c.action = v.action;
4959 c.dobj = item;
4960 c.iobj = other;
4961 c.dobj_ = nil;
4962 c.iobj_ = nil;
4963 c.set_ = nil;
4964 c.way_ = nil;
4965 c.list_ = nil;
4966 c.dir_ = v.dir;
4967 c.done_ = nil;
4968 c.reports_ = nil;
4969 c.failed_ = nil;
4970 c.implied_ = nil;
4971 actOnce(c, c.action, item);
4972 if (c.done_ != nil && c.done_.length() > 0)
4973 c.action.report(c);
4974 sayReports(c);
4975 return true;
4976 }
4977
4978 /* One object's worth of a command: redirect, refuse, act. */
4979 actOnce(c, a, item)
4980 {
4981
4982 local doer = findDoer(c);
4983 if (doer != nil && doer.instead != nil)
4984 {
4985 a = doer.instead;
4986 c.action = a;
4987 }
4988
4989 if (a.needsReach)
4990 {
4991
4992 local far = c.dobj != nil && !a.dobjIsTopic && c.dobj.isDistant ? c.dobj
4993 : c.iobj != nil && !a.iobjIsTopic && c.iobj.isDistant ? c.iobj : nil;
4994 if (far != nil)
4995 {
4996 sayFor('reach.toofar', far, nil);
4997 "\n";
4998 return a;
4999 }
5000
5001 local high = c.dobj != nil && !a.dobjIsTopic && c.dobj.isOutOfReach
5002 && !c.dobj.canObjReachSelf(player) ? c.dobj
5003 : c.iobj != nil && !a.iobjIsTopic && c.iobj.isOutOfReach
5004 && !c.iobj.canObjReachSelf(player) ? c.iobj : nil;
5005 if (high != nil)
5006 {
5007 sayFor('reach.toohigh', high, nil);
5008 "\n";
5009 return a;
5010 }
5011 }
5012
5013 if (!meetPreConds(c, a, c.dobj, dobjRole))
5014 return a;
5015 if (!meetPreConds(c, a, a.iobjIsTopic ? nil : c.iobj, iobjRole))
5016 return a;
5017 if (c.dobj != nil)
5018 {
5019 local no = c.dobj.refuses(a, c);
5020 if (no != nil)
5021 {
5022 "<<no>>\n";
5023 return a;
5024 }
5025 }
5026 if (c.iobj != nil)
5027 {
5028 local no = c.iobj.refuses(a, c);
5029 if (no != nil)
5030 {
5031 "<<no>>\n";
5032 return a;
5033 }
5034 }
5035
5036 local nest = nestedIn();
5037 if (nest != nil && nest.roomBeforeAction(c))
5038 return a;
5039 if (here().roomBeforeAction(c))
5040 return a;
5041
5042 foreach (local watcher in contains.all(here()))
5043 {
5044 if (watcher.isActor && watcher.curState != nil)
5045 watcher.curState.beforeAction(c);
5046 }
5047 a.exec(c);
5048 foreach (local watcher in contains.all(here()))
5049 {
5050 if (watcher.isActor && watcher.curState != nil)
5051 watcher.curState.afterAction(c);
5052 }
5053 return a;
5054 }
5055
5056 /*
5057 * The account and the turn, once, however many objects the command named.
5058 */
5059 finishCommand(c, a)
5060 {
5061 if (a == nil)
5062 return nil;
5063
5064 if (c.done_ != nil && c.done_.length() > 0)
5065 {
5066 a.report(c);
5067
5068 lastNamed.items = c.done_;
5069 }
5070
5071 sayReports(c);
5072
5073 if (!gameOver.done)
5074 {
5075 turnCount.value = turnCount.value + 1;
5076
5077 runScenes();
5078 runActors();
5079 runEvents();
5080 syncStates();
5081 }
5082 dialogueUI.afterTurn();
5083 return nil;
5084 }
5085
5086 /*
5087 * The command an action intent fills in. One command runs at a time, so this is
5088 * reused rather than allocated, for the same reason `msgArgs` is.
5089 */
5090 actArgs: Command;
5091
5092 /* An optional library module may handle structured UI intents. */
5093 dialogueUI: object
5094 accepts(verb) { return nil; }
5095 act(verb, args) { return nil; }
5096 afterTurn() { return nil; }
5097 ;
5098
5099 /** @api host
5100 * Submit a structured verb code and entity arguments to the normal action cycle. */
5101 vhylAct(verb, subjects)
5102 {
5103 if (dialogueUI.accepts(verb))
5104 return dialogueUI.act(verb, subjects);
5105 if (!gameOver.done)
5106 travelActions.cycle = travelActions.cycle + 1;
5107 local v = verbFor(verb);
5108 if (gameOver.done)
5109 {
5110 if (v != nil && v.action == undoAction)
5111 return undoAction.exec(nil);
5112 return endedCommand();
5113 }
5114 if (v == nil)
5115 {
5116 sayAbout('act.noverb', nil, nil, verb);
5117 "\n";
5118 return nil;
5119 }
5120 local c = actArgs;
5121 local a = v.action;
5122 if (a == undoAction)
5123 return undoAction.exec(nil);
5124 c.action = a;
5125 c.dobj_ = nil;
5126 c.iobj_ = nil;
5127 c.dobj = nil;
5128 c.iobj = nil;
5129 c.way_ = nil;
5130 c.list_ = nil;
5131 c.dir_ = v.dir;
5132 local count = subjects == nil ? 0 : subjects.length();
5133 if (a.needsDobj)
5134 {
5135 if (count < 1)
5136 {
5137 sayMsg(a.missingDobjMsg, c);
5138 "\n";
5139 return nil;
5140 }
5141 c.dobj = subjects[1];
5142 }
5143 if (a.needsIobj)
5144 {
5145 if (count < 2)
5146 {
5147 sayMsg(a.missingIobjMsg, c);
5148 "\n";
5149 return nil;
5150 }
5151 c.iobj = subjects[2];
5152 }
5153 /*
5154 * Scope is a world rule, so it is checked here too. A topic is deliberately
5155 * exempt: you can ask about a wreck on the shore below without it being to
5156 * hand, which is the same exemption the parser makes.
5157 */
5158 if (c.dobj != nil && !inScope(c.dobj))
5159 {
5160 sayFor('act.outofreach', c.dobj, nil);
5161 "\n";
5162 return nil;
5163 }
5164 if (c.iobj != nil && !a.iobjIsTopic && !inScope(c.iobj))
5165 {
5166 sayFor('act.outofreach', c.iobj, nil);
5167 "\n";
5168 return nil;
5169 }
5170 return runCommand(c);
5171 }
5172
5173 recover(code)
5174 {
5175 sayMsg('turn.failed', nil);
5176 "\n> ";
5177 return nil;
5178 }