| | 1 | #charset "us-ascii" |
| | 2 | |
| | 3 | /* |
| | 4 | * Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved. |
| | 5 | * |
| | 6 | * TADS 3 Library - actors |
| | 7 | * |
| | 8 | * This module provides definitions related to actors, which represent |
| | 9 | * characters in the game. |
| | 10 | */ |
| | 11 | |
| | 12 | /* include the library header */ |
| | 13 | #include "adv3.h" |
| | 14 | |
| | 15 | |
| | 16 | /* ------------------------------------------------------------------------ */ |
| | 17 | /* |
| | 18 | * Implied command modes |
| | 19 | */ |
| | 20 | enum ModePlayer, ModeNPC; |
| | 21 | |
| | 22 | |
| | 23 | /* ------------------------------------------------------------------------ */ |
| | 24 | /* |
| | 25 | * A Topic is an object representing some piece of knowledge in the |
| | 26 | * story. Actors can use Topic objects in commands such as "ask" and |
| | 27 | * "tell". |
| | 28 | * |
| | 29 | * A physical simulation object can be a Topic through multiple |
| | 30 | * inheritance. In addition, a game can define Topic objects for |
| | 31 | * abstract conversation topics that don't correspond to simulation |
| | 32 | * objects; for example, a topic could be created for "the meaning of |
| | 33 | * life" to allow a command such as "ask guru about meaning of life." |
| | 34 | * |
| | 35 | * The key distinction between Topic objects and regular objects is that |
| | 36 | * a Topic can represent an abstract, non-physical concept that isn't |
| | 37 | * connected to any "physical" object in the simulation. |
| | 38 | */ |
| | 39 | class Topic: VocabObject |
| | 40 | /* |
| | 41 | * Is the topic known? If this is true, the topic is in scope for |
| | 42 | * actions that operate on topics, such as "ask about" and "tell |
| | 43 | * about." If this is nil, the topic isn't known. |
| | 44 | * |
| | 45 | * By default, we mark all topics as known to begin with, which |
| | 46 | * allows discussion of any topic at any time. Some authors prefer |
| | 47 | * to keep track of which topics the player character actually has |
| | 48 | * reason to know about within the context of the game, making topics |
| | 49 | * available for conversation only after they become known for some |
| | 50 | * good reason, such as another character mentioning them in |
| | 51 | * conversation. |
| | 52 | * |
| | 53 | * Note that, as with Thing.isKnown, this is only the DEFAULT 'known' |
| | 54 | * property. Each actor can have its own separate 'known' property |
| | 55 | * by defining the actor's 'knownProp' to a different property name. |
| | 56 | */ |
| | 57 | isKnown = true |
| | 58 | |
| | 59 | /* |
| | 60 | * Topics are abstract objects, so they can't be sensed with any of |
| | 61 | * the physical senses, even if they're ever included as part of a |
| | 62 | * containment hierarchy (which might be convenient in some cases |
| | 63 | * for purposes of associating a topic with a physical object, for |
| | 64 | * example). |
| | 65 | */ |
| | 66 | canBeSensed(sense, trans, ambient) { return nil; } |
| | 67 | |
| | 68 | /* a topic cannot by default be used to resolve a possessive phrase */ |
| | 69 | canResolvePossessive = nil |
| | 70 | ; |
| | 71 | |
| | 72 | |
| | 73 | /* ------------------------------------------------------------------------ */ |
| | 74 | /* |
| | 75 | * FollowInfo - this is an object that tracks an actor's knowledge of |
| | 76 | * the objects that the actor can follow, which are objects that actor |
| | 77 | * has witnessed leaving the current location. We keep track of each |
| | 78 | * followable object and the direction we saw it depart. |
| | 79 | */ |
| | 80 | class FollowInfo: object |
| | 81 | /* the object we can follow */ |
| | 82 | obj = nil |
| | 83 | |
| | 84 | /* the TravelConnector the object traversed to leave */ |
| | 85 | connector = nil |
| | 86 | |
| | 87 | /* |
| | 88 | * The source location - this is the location we saw the object |
| | 89 | * depart. We keep track of this because an actor can follow an |
| | 90 | * object only if the actor is starting from the same location where |
| | 91 | * the actor saw the object depart. |
| | 92 | */ |
| | 93 | sourceLocation = nil |
| | 94 | ; |
| | 95 | |
| | 96 | |
| | 97 | /* ------------------------------------------------------------------------ */ |
| | 98 | /* |
| | 99 | * Postures. A posture describes how an actor is internally positioned: |
| | 100 | * standing, lying, sitting. We represent postures with objects of |
| | 101 | * class Posture to make it easier to add new game-specific postures. |
| | 102 | */ |
| | 103 | class Posture: object |
| | 104 | /* |
| | 105 | * Try getting the current actor into this posture within the given |
| | 106 | * location, by running an appropriate implied command. |
| | 107 | */ |
| | 108 | tryMakingPosture(loc) { } |
| | 109 | |
| | 110 | /* put the actor into our posture via a nested action */ |
| | 111 | setActorToPosture(actor, loc) { } |
| | 112 | ; |
| | 113 | |
| | 114 | /* |
| | 115 | * Standing posture - this is the default posture, which an actor |
| | 116 | * normally uses for travel. Actors are generally in this posture any |
| | 117 | * time they are not sitting on something, lying on something, or |
| | 118 | * similar. |
| | 119 | */ |
| | 120 | standing: Posture |
| | 121 | tryMakingPosture(loc) { return tryImplicitAction(StandOn, loc); } |
| | 122 | setActorToPosture(actor, loc) { nestedActorAction(actor, StandOn, loc); } |
| | 123 | ; |
| | 124 | |
| | 125 | /* |
| | 126 | * Sitting posture. |
| | 127 | */ |
| | 128 | sitting: Posture |
| | 129 | tryMakingPosture(loc) { return tryImplicitAction(SitOn, loc); } |
| | 130 | setActorToPosture(actor, loc) { nestedActorAction(actor, SitOn, loc); } |
| | 131 | ; |
| | 132 | |
| | 133 | /* |
| | 134 | * Lying posture. |
| | 135 | */ |
| | 136 | lying: Posture |
| | 137 | tryMakingPosture(loc) { return tryImplicitAction(LieOn, loc); } |
| | 138 | setActorToPosture(actor, loc) { nestedActorAction(actor, LieOn, loc); } |
| | 139 | ; |
| | 140 | |
| | 141 | |
| | 142 | /* ------------------------------------------------------------------------ */ |
| | 143 | /* |
| | 144 | * Conversation manager output filter. We look for special tags in the |
| | 145 | * output stream: |
| | 146 | * |
| | 147 | * <.reveal key> - add 'key' to the knowledge token lookup table. The |
| | 148 | * 'key' is an arbitrary string, which we can look up in the table to |
| | 149 | * determine if the key has even been revealed. This can be used to make |
| | 150 | * a response conditional on another response having been displayed, |
| | 151 | * because the key will only be added to the table when the text |
| | 152 | * containing the <.reveal key> sequence is displayed. |
| | 153 | * |
| | 154 | * <.convnode name> - switch the current responding actor to conversation |
| | 155 | * node 'name'. |
| | 156 | * |
| | 157 | * <.convstay> - keep the responding actor in the same conversation node |
| | 158 | * as it was in at the start of the current response |
| | 159 | * |
| | 160 | * <.topics> - schedule a topic inventory for the end of the turn (just |
| | 161 | * before the next command prompt) |
| | 162 | */ |
| | 163 | conversationManager: OutputFilter, PreinitObject |
| | 164 | /* |
| | 165 | * Custom extended tags. Games and library extensions can add their |
| | 166 | * own tag processing as needed, by using 'modify' to extend this |
| | 167 | * object. There are two things you have to do to add your own tags: |
| | 168 | * |
| | 169 | * First, add a 'customTags' property that defines a regular |
| | 170 | * expression for your added tags. This will be incorporated into |
| | 171 | * the main pattern we use to look for tags. Simply specify a |
| | 172 | * string that lists your tags separated by "|" characters, like |
| | 173 | * this: |
| | 174 | * |
| | 175 | * customTags = 'foo|bar' |
| | 176 | * |
| | 177 | * Second, define a doCustomTag() method to process the tags. The |
| | 178 | * filter routine will call your doCustomTag() method whenever it |
| | 179 | * finds one of your custom tags in the output stream. |
| | 180 | */ |
| | 181 | customTags = nil |
| | 182 | doCustomTag(tag, arg) { /* do nothing by default */ } |
| | 183 | |
| | 184 | /* filter text written to the output stream */ |
| | 185 | filterText(ostr, txt) |
| | 186 | { |
| | 187 | local start; |
| | 188 | |
| | 189 | /* scan for our special tags */ |
| | 190 | for (start = 1 ; ; ) |
| | 191 | { |
| | 192 | local match; |
| | 193 | local arg; |
| | 194 | local actor; |
| | 195 | local sp; |
| | 196 | local tag; |
| | 197 | local nxtOfs; |
| | 198 | |
| | 199 | /* scan for the next tag */ |
| | 200 | match = rexSearch(tagPat, txt, start); |
| | 201 | |
| | 202 | /* if we didn't find it, we're done */ |
| | 203 | if (match == nil) |
| | 204 | break; |
| | 205 | |
| | 206 | /* note the next offset */ |
| | 207 | nxtOfs = match[1] + match[2]; |
| | 208 | |
| | 209 | /* get the argument (the third group from the match) */ |
| | 210 | arg = rexGroup(3); |
| | 211 | if (arg != nil) |
| | 212 | arg = arg[3]; |
| | 213 | |
| | 214 | /* pick out the tag */ |
| | 215 | tag = rexGroup(1)[3].toLower(); |
| | 216 | |
| | 217 | /* check which tag we have */ |
| | 218 | switch (tag) |
| | 219 | { |
| | 220 | case 'reveal': |
| | 221 | /* reveal the key by adding it to our database */ |
| | 222 | setRevealed(arg); |
| | 223 | break; |
| | 224 | |
| | 225 | case 'convbegin': |
| | 226 | /* |
| | 227 | * Internal tag - starting a conversational response for |
| | 228 | * an actor, identified by an index in our idToActor |
| | 229 | * vector. Get the actor. |
| | 230 | */ |
| | 231 | actor = idToActor[toInteger(arg)]; |
| | 232 | |
| | 233 | /* |
| | 234 | * since we're just starting a response, clear the flag |
| | 235 | * in the actor indicating that a ConvNode has been set |
| | 236 | * in the course of this response |
| | 237 | */ |
| | 238 | actor.responseSetConvNode = nil; |
| | 239 | |
| | 240 | /* remember the new responding actor */ |
| | 241 | respondingActor = actor; |
| | 242 | |
| | 243 | /* done */ |
| | 244 | break; |
| | 245 | |
| | 246 | case 'convend': |
| | 247 | /* |
| | 248 | * Ending a conversational response for a given actor, |
| | 249 | * identified by the first argument, which is an index in |
| | 250 | * our idToActor vector. |
| | 251 | */ |
| | 252 | sp = arg.find(' '); |
| | 253 | actor = idToActor[toInteger(arg.substr(1, sp - 1))]; |
| | 254 | |
| | 255 | /* the rest of the argument is the default new ConvNode */ |
| | 256 | arg = arg.substr(sp + 1); |
| | 257 | |
| | 258 | /* if the new ConvNode is empty, it means no ConvNode */ |
| | 259 | if (arg == '') |
| | 260 | arg = nil; |
| | 261 | |
| | 262 | /* |
| | 263 | * if we didn't explicitly set a new ConvNode in the |
| | 264 | * course of this response, apply the default |
| | 265 | */ |
| | 266 | if (!actor.responseSetConvNode) |
| | 267 | actor.setConvNodeReason(arg, 'convend'); |
| | 268 | |
| | 269 | /* |
| | 270 | * Since we've just finished showing a message that |
| | 271 | * specifically refers to this actor, the player should |
| | 272 | * be able to refer to this actor using a pronoun on the |
| | 273 | * next command. Set the responding actor as the |
| | 274 | * antecedent for the appropriate singular pronouns for |
| | 275 | * the player character. Note that we do this at the end |
| | 276 | * of the response, so that the antecedent is the last |
| | 277 | * one if we have more than one. |
| | 278 | */ |
| | 279 | gPlayerChar.setPronounObj(actor); |
| | 280 | |
| | 281 | /* done */ |
| | 282 | break; |
| | 283 | |
| | 284 | case 'convnode': |
| | 285 | /* |
| | 286 | * If there's a current responding actor, set its current |
| | 287 | * conversation node. |
| | 288 | */ |
| | 289 | if (respondingActor != nil) |
| | 290 | { |
| | 291 | /* |
| | 292 | * Set the new node. While we're working, capture |
| | 293 | * any output that occurs so that we can insert it |
| | 294 | * into the output stream just after the <.convnode> |
| | 295 | * tag, so that any text displayed within the |
| | 296 | * ConvNode's activation method (noteActive) is |
| | 297 | * displayed in the proper order. |
| | 298 | */ |
| | 299 | local ctxt = mainOutputStream.captureOutput( |
| | 300 | {: respondingActor.setConvNodeReason(arg, 'convnode') }); |
| | 301 | |
| | 302 | /* re-insert any text we captured */ |
| | 303 | txt = txt.substr(1, nxtOfs - 1) |
| | 304 | + ctxt |
| | 305 | + txt.substr(nxtOfs); |
| | 306 | } |
| | 307 | break; |
| | 308 | |
| | 309 | case 'convstay': |
| | 310 | /* |
| | 311 | * leave the responding actor in the old conversation |
| | 312 | * node - we don't need to change the ConvNode, but we do |
| | 313 | * need to note that we've explicitly set it |
| | 314 | */ |
| | 315 | if (respondingActor != nil) |
| | 316 | respondingActor.responseSetConvNode = true; |
| | 317 | break; |
| | 318 | |
| | 319 | case 'topics': |
| | 320 | /* schedule a topic inventory listing */ |
| | 321 | scheduleTopicInventory(); |
| | 322 | break; |
| | 323 | |
| | 324 | default: |
| | 325 | /* check for an extended tag */ |
| | 326 | doCustomTag(tag, arg); |
| | 327 | break; |
| | 328 | } |
| | 329 | |
| | 330 | /* continue the search after this match */ |
| | 331 | start = nxtOfs; |
| | 332 | } |
| | 333 | |
| | 334 | /* |
| | 335 | * remove the tags from the text by replacing every occurrence |
| | 336 | * with an empty string, and return the result |
| | 337 | */ |
| | 338 | return rexReplace(tagPat, txt, '', ReplaceAll); |
| | 339 | } |
| | 340 | |
| | 341 | /* regular expression pattern for our tags */ |
| | 342 | tagPat = static new RexPattern( |
| | 343 | '<nocase><langle><dot>' |
| | 344 | + '(reveal|convbegin|convend|convnode|convstay|topics' |
| | 345 | + (customTags != nil ? '|' + customTags : '') |
| | 346 | + ')' |
| | 347 | + '(<space>+(<^rangle>+))?' |
| | 348 | + '<rangle>') |
| | 349 | |
| | 350 | /* |
| | 351 | * Schedule a topic inventory request. Game code can call this at |
| | 352 | * any time to request that the player character's topic inventory |
| | 353 | * be shown automatically just before the next command prompt. In |
| | 354 | * most cases, game code won't call this directly, but will request |
| | 355 | * the same effect using the <.topics> tag in topic response text. |
| | 356 | */ |
| | 357 | scheduleTopicInventory() |
| | 358 | { |
| | 359 | /* note that we have a request for a prompt-time topic inventory */ |
| | 360 | pendingTopicInventory = true; |
| | 361 | } |
| | 362 | |
| | 363 | /* |
| | 364 | * Show or schedule a topic inventory request. If the current |
| | 365 | * action has a non-default command report, schedule it; otherwise, |
| | 366 | * show it now. |
| | 367 | * |
| | 368 | * If there's a non-default report, don't suggest the topics now; |
| | 369 | * instead, schedule a topic inventory for the end of the turn. |
| | 370 | * When we have a non-default report, the report could change the |
| | 371 | * ConvNode for the actor, so we don't want to show the topic |
| | 372 | * inventory until we've had a chance to process all of the reports. |
| | 373 | */ |
| | 374 | showOrScheduleTopicInventory(actor, otherActor) |
| | 375 | { |
| | 376 | /* check for a non-default command report in the current action */ |
| | 377 | if (gTranscript.currentActionHasReport( |
| | 378 | {x: x.ofKind(MainCommandReport)})) |
| | 379 | { |
| | 380 | /* we have a non-default report - defer the topic inventory */ |
| | 381 | scheduleTopicInventory(); |
| | 382 | } |
| | 383 | else |
| | 384 | { |
| | 385 | /* we have only a default report, so show the inventory now */ |
| | 386 | actor.suggestTopicsFor(otherActor, nil); |
| | 387 | } |
| | 388 | } |
| | 389 | |
| | 390 | /* |
| | 391 | * Note that an actor is about to give a response through a |
| | 392 | * TopicEntry object. We'll remember the actor so that we'll know |
| | 393 | * which actor is involved in a <.convnode> operation. |
| | 394 | */ |
| | 395 | beginResponse(actor) |
| | 396 | { |
| | 397 | /* if the actor doesn't have an ID yet, assign one */ |
| | 398 | if (actor.convMgrID == nil) |
| | 399 | { |
| | 400 | /* add the actor to our vector of actors */ |
| | 401 | idToActor.append(actor); |
| | 402 | |
| | 403 | /* the ID is simply the index in this vector */ |
| | 404 | actor.convMgrID = idToActor.length(); |
| | 405 | } |
| | 406 | |
| | 407 | /* output a <.convbegin> for the actor */ |
| | 408 | gTranscript.addReport(new ConvBeginReport(actor.convMgrID)); |
| | 409 | } |
| | 410 | |
| | 411 | /* |
| | 412 | * Finish the response - call this after we finish handling the |
| | 413 | * response. There must be a subsequent matching call to this |
| | 414 | * routine whenever beginResponse() is called. |
| | 415 | * |
| | 416 | * 'node' is the default new ConvNode the actor for the responding |
| | 417 | * actor. If another ConvNode was explicitly set in the course of |
| | 418 | * handling the response, this is ignored, since the explicit |
| | 419 | * setting overrides this default. |
| | 420 | */ |
| | 421 | finishResponse(actor, node) |
| | 422 | { |
| | 423 | local prv; |
| | 424 | local oldNode; |
| | 425 | |
| | 426 | /* if the node is a ConvNode object, use its name */ |
| | 427 | if (node != nil && node.ofKind(ConvNode)) |
| | 428 | node = node.name; |
| | 429 | |
| | 430 | /* |
| | 431 | * if the previous report was our ConvBeginReport, the |
| | 432 | * conversation display was empty, so ignore the whole thing |
| | 433 | */ |
| | 434 | if ((prv = gTranscript.getLastReport()) != nil |
| | 435 | && prv.ofKind(ConvBeginReport) |
| | 436 | && prv.actorID == actor.convMgrID) |
| | 437 | { |
| | 438 | /* remove the <.convbegin> report - we're canceling it out */ |
| | 439 | gTranscript.deleteLastReport(); |
| | 440 | |
| | 441 | /* we're done - do not generate the <.convend> */ |
| | 442 | return; |
| | 443 | } |
| | 444 | |
| | 445 | /* |
| | 446 | * if the actor has a current ConvNode, and our default next |
| | 447 | * node is nil, and the current node is marked as "sticky," stay |
| | 448 | * in the current node rather than switching to a nil default |
| | 449 | */ |
| | 450 | if (node == nil |
| | 451 | && (oldNode = actor.curConvNode) != nil |
| | 452 | && oldNode.isSticky) |
| | 453 | { |
| | 454 | /* it's sticky, so stay at this node */ |
| | 455 | node = oldNode.name; |
| | 456 | } |
| | 457 | |
| | 458 | /* output a <.convend> for the actor */ |
| | 459 | gTranscript.addReport(new ConvEndReport(actor.convMgrID, node)); |
| | 460 | } |
| | 461 | |
| | 462 | /* |
| | 463 | * The current responding actor. Actors should set this when they're |
| | 464 | * about to show a response to an ASK, TELL, etc. |
| | 465 | */ |
| | 466 | respondingActor = nil |
| | 467 | |
| | 468 | /* |
| | 469 | * Mark a tag as revealed. This adds an entry for the tag to the |
| | 470 | * revealedNameTab table. We simply set the table entry to 'true'; |
| | 471 | * the presence of the tag in the table constitutes the indication |
| | 472 | * that the tag has been revealed. |
| | 473 | * |
| | 474 | * (Games and library extensions can use 'modify' to override this |
| | 475 | * and store more information in the table entry. For example, you |
| | 476 | * could store the time when the information was first revealed, or |
| | 477 | * the location where it was learned. If you do override this, just |
| | 478 | * be sure to set the revealedNameTab entry for the tag to a non-nil |
| | 479 | * and non-zero value, so that any code testing the presence of the |
| | 480 | * table entry will see that the slot is indeed set.) |
| | 481 | */ |
| | 482 | setRevealed(tag) |
| | 483 | { |
| | 484 | revealedNameTab[tag] = true; |
| | 485 | } |
| | 486 | |
| | 487 | /* |
| | 488 | * The global lookup table of all revealed keys. This table is keyed |
| | 489 | * by the string naming the revelation; the value associated with |
| | 490 | * each key is not used (we always just set it to true). |
| | 491 | */ |
| | 492 | revealedNameTab = static new LookupTable(32, 32) |
| | 493 | |
| | 494 | /* a vector of actors, indexed by their convMgrID values */ |
| | 495 | idToActor = static new Vector(32) |
| | 496 | |
| | 497 | /* preinitialize */ |
| | 498 | execute() |
| | 499 | { |
| | 500 | /* add every ConvNode object to our master table */ |
| | 501 | forEachInstance(ConvNode, |
| | 502 | { obj: obj.getActor().convNodeTab[obj.name] = obj }); |
| | 503 | |
| | 504 | /* |
| | 505 | * set up the prompt daemon that makes automatic topic inventory |
| | 506 | * suggestions when appropriate |
| | 507 | */ |
| | 508 | new PromptDaemon(self, &topicInventoryDaemon); |
| | 509 | } |
| | 510 | |
| | 511 | /* |
| | 512 | * Prompt daemon: show topic inventory when appropriate. When a |
| | 513 | * response explicitly asks us to show a topic inventory using the |
| | 514 | * <.topics> tag, or when other game code asks us to show topic |
| | 515 | * inventory by calling scheduleTopicInventory(), we'll show the |
| | 516 | * inventory just before the command input prompt. |
| | 517 | */ |
| | 518 | topicInventoryDaemon() |
| | 519 | { |
| | 520 | /* if we have a topic inventory scheduled, show it now */ |
| | 521 | if (pendingTopicInventory) |
| | 522 | { |
| | 523 | /* |
| | 524 | * Show the player character's topic inventory. This is not |
| | 525 | * an explicit inventory request, since the player didn't ask |
| | 526 | * for it. |
| | 527 | */ |
| | 528 | gPlayerChar.suggestTopics(nil); |
| | 529 | |
| | 530 | /* we no longer have a pending inventory request */ |
| | 531 | pendingTopicInventory = nil; |
| | 532 | } |
| | 533 | } |
| | 534 | |
| | 535 | /* flag: we have a pending prompt-time topic inventory request */ |
| | 536 | pendingTopicInventory = nil |
| | 537 | ; |
| | 538 | |
| | 539 | /* ------------------------------------------------------------------------ */ |
| | 540 | /* |
| | 541 | * A plug-in topic database. The topic database is a set of TopicEntry |
| | 542 | * objects that specify the responses to queries on particular topics. |
| | 543 | * The exact nature of the queries that a particular topic database |
| | 544 | * handles is up to the database subclass to define; we just provide the |
| | 545 | * abstract mechanism for finding and displaying responses. |
| | 546 | * |
| | 547 | * This is a "plug-in" database in that it's meant to be added into other |
| | 548 | * classes using multiple inheritance. This isn't meant to be used as a |
| | 549 | * stand-alone abstract topic entry container. |
| | 550 | */ |
| | 551 | class TopicDatabase: object |
| | 552 | /* |
| | 553 | * Is the topic group active? A TopicEntry always checks with its |
| | 554 | * container to see if the children of the container are active. By |
| | 555 | * default, everything in the database is active. |
| | 556 | */ |
| | 557 | topicGroupActive = true |
| | 558 | |
| | 559 | /* |
| | 560 | * Get the score adjustment for all topic entries contained within. |
| | 561 | * The default adjustment is zero; TopicGroup objects can use this to |
| | 562 | * adjust the score for their nested entries. |
| | 563 | */ |
| | 564 | topicGroupScoreAdjustment = 0 |
| | 565 | |
| | 566 | /* |
| | 567 | * Handle a topic. Look up the topic in our topic list for the |
| | 568 | * given conversational action type. If we find a match, we'll |
| | 569 | * invoke the matching topic list entry to handle it. We'll return |
| | 570 | * true if we find a match, nil if not. |
| | 571 | */ |
| | 572 | handleTopic(fromActor, topic, convType, path) |
| | 573 | { |
| | 574 | local resp; |
| | 575 | |
| | 576 | /* find the best response */ |
| | 577 | resp = findTopicResponse(fromActor, topic, convType, path); |
| | 578 | |
| | 579 | /* if we found a match, let it handle the topic */ |
| | 580 | if (resp != nil) |
| | 581 | { |
| | 582 | /* show the response */ |
| | 583 | showTopicResponse(fromActor, topic, resp); |
| | 584 | |
| | 585 | /* tell the caller we handled it */ |
| | 586 | return true; |
| | 587 | } |
| | 588 | else |
| | 589 | { |
| | 590 | /* tell the caller we didn't handle it */ |
| | 591 | return nil; |
| | 592 | } |
| | 593 | } |
| | 594 | |
| | 595 | /* show the response we found for a topic */ |
| | 596 | showTopicResponse(fromActor, topic, resp) |
| | 597 | { |
| | 598 | /* let the response object handle it */ |
| | 599 | resp.handleTopic(fromActor, topic); |
| | 600 | } |
| | 601 | |
| | 602 | /* find the best response (a TopicEntry object) for the given topic */ |
| | 603 | findTopicResponse(fromActor, topic, convType, path) |
| | 604 | { |
| | 605 | local topicList; |
| | 606 | local best, bestScore; |
| | 607 | |
| | 608 | /* |
| | 609 | * Get the list of possible topics for this conversation type. |
| | 610 | * The topic list is contained in one of our properties; exactly |
| | 611 | * which property is determined by the conversation type. |
| | 612 | */ |
| | 613 | topicList = self.(convType.topicListProp); |
| | 614 | |
| | 615 | /* if the topic list is nil, we obviously won't find the topic */ |
| | 616 | if (topicList == nil) |
| | 617 | return nil; |
| | 618 | |
| | 619 | /* scan our topic list for the best match */ |
| | 620 | best = nil; |
| | 621 | foreach (local cur in topicList) |
| | 622 | { |
| | 623 | /* get this item's score */ |
| | 624 | local score = cur.adjustScore(cur.matchTopic(fromActor, topic)); |
| | 625 | |
| | 626 | /* |
| | 627 | * If this item has a score at all, and the topic entry is |
| | 628 | * marked as active, and it's best (or only) score so far, |
| | 629 | * note it. Ignore topics marked as not active, since |
| | 630 | * they're in the topic database only provisionally. |
| | 631 | */ |
| | 632 | if (score != nil |
| | 633 | && cur.checkIsActive() |
| | 634 | && (best == nil || score > bestScore)) |
| | 635 | { |
| | 636 | best = cur; |
| | 637 | bestScore = score; |
| | 638 | } |
| | 639 | } |
| | 640 | |
| | 641 | /* |
| | 642 | * If there's a hierarchical search path, AND this topic entry |
| | 643 | * defines a deferToEntry() method, look for matches in the |
| | 644 | * inferior databases on the path and check to see if we want to |
| | 645 | * defer to one of them. |
| | 646 | */ |
| | 647 | if (best != nil && path != nil && best.propDefined(&deferToEntry)) |
| | 648 | { |
| | 649 | /* look for a match in each inferior database */ |
| | 650 | for (local i = 1, local len = path.length() ; i <= len ; ++i) |
| | 651 | { |
| | 652 | local inf; |
| | 653 | |
| | 654 | /* |
| | 655 | * Look up an entry in this inferior database. Pass in |
| | 656 | * the remainder of the path, so that the inferior |
| | 657 | * database can consider further deferral to its own |
| | 658 | * inferior databases. |
| | 659 | */ |
| | 660 | inf = path[i].findTopicResponse(fromActor, topic, convType, |
| | 661 | path.sublist(i + 1)); |
| | 662 | |
| | 663 | /* |
| | 664 | * if we found an entry in this inferior database, and |
| | 665 | * our entry defers to the inferior entry, then ignore |
| | 666 | * the match in our own database |
| | 667 | */ |
| | 668 | if (inf != nil && best.deferToEntry(inf)) |
| | 669 | return nil; |
| | 670 | } |
| | 671 | } |
| | 672 | |
| | 673 | /* return the best matching response object, if any */ |
| | 674 | return best; |
| | 675 | } |
| | 676 | |
| | 677 | /* show our suggested topic list */ |
| | 678 | showSuggestedTopicList(lst, asker, askee, explicit) |
| | 679 | { |
| | 680 | /* get the asking actor's scope list for use later */ |
| | 681 | scopeList = asker.scopeList(); |
| | 682 | |
| | 683 | /* remove items that have redundant list groups and full names */ |
| | 684 | for (local i = 1, local len = lst.length() ; i <= len ; ++i) |
| | 685 | { |
| | 686 | local a = lst[i]; |
| | 687 | |
| | 688 | /* check for redundant elements */ |
| | 689 | for (local j = i + 1 ; j <= len ; ++j) |
| | 690 | { |
| | 691 | local b = lst[j]; |
| | 692 | |
| | 693 | /* |
| | 694 | * If item 'a' matches item 'b', and both are active, |
| | 695 | * remove item 'b'. We only need to remove redundant |
| | 696 | items if they're both active, since inactive items |
| | 697 | */ |
| | 698 | if (a.suggestionGroup == b.suggestionGroup |
| | 699 | && a.fullName == b.fullName |
| | 700 | && a.isSuggestionActive(asker, scopeList) |
| | 701 | && b.isSuggestionActive(asker, scopeList)) |
| | 702 | { |
| | 703 | /* delete item 'b' from the list */ |
| | 704 | lst.removeElementAt(j); |
| | 705 | |
| | 706 | /* adjust our indices for the deletion */ |
| | 707 | --j; |
| | 708 | --len; |
| | 709 | } |
| | 710 | } |
| | 711 | } |
| | 712 | |
| | 713 | /* show our list */ |
| | 714 | new SuggestedTopicLister(asker, askee, explicit) |
| | 715 | .showList(asker, nil, lst, 0, 0, nil, nil); |
| | 716 | } |
| | 717 | |
| | 718 | /* |
| | 719 | * Flag: this database level should limit topic suggestions (for the |
| | 720 | * TOPICS and TALK TO commands) to its own topics, excluding any |
| | 721 | * topics inherited from the "broader" context. If this property is |
| | 722 | * set to true, then we won't include suggestions from any lower |
| | 723 | * level of the database hierarchy. If this property is nil, we'll |
| | 724 | * also include any topic suggestions from the broader context. |
| | 725 | * |
| | 726 | * Topic databases are arranged into a fixed hierarchy for an actor. |
| | 727 | * At the top level is the current ConvNode object; at the next level |
| | 728 | * is the ActorState; and at the bottom level is the Actor itself. |
| | 729 | * So, if the ConvNode's limitSuggestions property is set to true, |
| | 730 | * then the suggestions for the actor will include ONLY the ConvNode. |
| | 731 | * If the ConvNode has the property set to nil, but the ActorState |
| | 732 | * has it set to true, then we'll include the ConvNode and the |
| | 733 | * ActorState suggestions. |
| | 734 | * |
| | 735 | * By default, we set this to nil. This should usually be set to |
| | 736 | * true for any ConvNode or ActorState where the NPC won't allow the |
| | 737 | * player to stray from the subject. For example, if a ConvNode only |
| | 738 | * accepts a YES or NO response to a question, then this property |
| | 739 | * should probably be set to true in the ConvNode, since other |
| | 740 | * suggested topics won't be accepted as conversation topics as long |
| | 741 | * as the ConvNode is active. |
| | 742 | */ |
| | 743 | limitSuggestions = nil |
| | 744 | |
| | 745 | /* |
| | 746 | * Add a topic to our topic database. We'll add it to the |
| | 747 | * appropriate list or lists as indicated in the topic itself. |
| | 748 | * 'topic' is a TopicEntry object. |
| | 749 | */ |
| | 750 | addTopic(topic) |
| | 751 | { |
| | 752 | /* add the topic to each list indicated in the topic */ |
| | 753 | foreach (local cur in topic.includeInList) |
| | 754 | addTopicToList(topic, cur); |
| | 755 | } |
| | 756 | |
| | 757 | /* remove a topic from our topic database */ |
| | 758 | removeTopic(topic) |
| | 759 | { |
| | 760 | /* remove the topic from each of its lists */ |
| | 761 | foreach (local cur in topic.includeInList) |
| | 762 | removeTopicFromList(topic, cur); |
| | 763 | } |
| | 764 | |
| | 765 | /* add a suggested topic */ |
| | 766 | addSuggestedTopic(topic) |
| | 767 | { |
| | 768 | /* add the topic to our suggestion list */ |
| | 769 | addTopicToList(topic, &suggestedTopics); |
| | 770 | } |
| | 771 | |
| | 772 | /* remove a suggested topic */ |
| | 773 | removeSuggestedTopic(topic) |
| | 774 | { |
| | 775 | /* add the topic to our suggestion list */ |
| | 776 | removeTopicFromList(topic, &suggestedTopics); |
| | 777 | } |
| | 778 | |
| | 779 | /* |
| | 780 | * Add a topic to the given topic list. The topic list is given as a |
| | 781 | * property point; for example, we'd specify &askTopics to add the |
| | 782 | * topic to our ASK list. |
| | 783 | */ |
| | 784 | addTopicToList(topic, listProp) |
| | 785 | { |
| | 786 | /* if we haven't created this topic list vector yet, create it now */ |
| | 787 | if (self.(listProp) == nil) |
| | 788 | self.(listProp) = new Vector(8); |
| | 789 | |
| | 790 | /* add the topic */ |
| | 791 | self.(listProp).append(topic); |
| | 792 | } |
| | 793 | |
| | 794 | /* remove a topic from the given topic list */ |
| | 795 | removeTopicFromList(topic, listProp) |
| | 796 | { |
| | 797 | /* if the list exists, remove the topic from it */ |
| | 798 | if (self.(listProp) != nil) |
| | 799 | self.(listProp).removeElement(topic); |
| | 800 | } |
| | 801 | |
| | 802 | /* |
| | 803 | * Our list of suggested topics. These are SuggestedTopic objects |
| | 804 | * that describe things that another actor wants to ask or tell this |
| | 805 | * actor about. |
| | 806 | */ |
| | 807 | suggestedTopics = nil |
| | 808 | |
| | 809 | /* |
| | 810 | * Get the "owner" of the topics in this database. The meaning of |
| | 811 | * "owner" varies according to the topic database type; for actor |
| | 812 | * topic databases, for example, this is the actor. Generally, the |
| | 813 | * owner is the object being queried about the topic, from the |
| | 814 | * player's perspective. Each type of database should define this |
| | 815 | * method to return the appropriate object. |
| | 816 | */ |
| | 817 | getTopicOwner() { return nil; } |
| | 818 | ; |
| | 819 | |
| | 820 | /* |
| | 821 | * A TopicDatabase for an Actor. This is used not only directly for an |
| | 822 | * Actor but also for an actor's sub-databases, in ActorState and |
| | 823 | * ConvNode. |
| | 824 | * |
| | 825 | * Actor topic databases field queries for the various types of |
| | 826 | * topic-based interactions an actor can participate in: ASK, TELL, SHOW, |
| | 827 | * GIVE, and so on. |
| | 828 | * |
| | 829 | * Each actor has its own topic database, which means each actor can have |
| | 830 | * its own set of responses. Actor states can also have their own |
| | 831 | * separate topic databases; this makes it easy to make an actor's |
| | 832 | * response to a particular question vary according to the actor's state. |
| | 833 | * Conversation nodes can also have their own separate databases, which |
| | 834 | * allows for things like threaded conversations. |
| | 835 | */ |
| | 836 | class ActorTopicDatabase: TopicDatabase |
| | 837 | /* |
| | 838 | * Initiate conversation on the given simulation object. If we can |
| | 839 | * find an InitiateTopic matching the given object, we'll show its |
| | 840 | * topic response and return true; if we can't find a topic to |
| | 841 | * initiate, we'll simply return nil. |
| | 842 | */ |
| | 843 | initiateTopic(obj) |
| | 844 | { |
| | 845 | /* find an initiate topic for the given object */ |
| | 846 | if (handleTopic(gPlayerChar, obj, initiateConvType, nil)) |
| | 847 | { |
| | 848 | /* |
| | 849 | * we handled the topic, so note that we're in conversation |
| | 850 | * with the player character now |
| | 851 | */ |
| | 852 | getTopicOwner().noteConversation(gPlayerChar); |
| | 853 | |
| | 854 | /* indicate that we found a topic to initiate */ |
| | 855 | return true; |
| | 856 | } |
| | 857 | |
| | 858 | /* we didn't find a topic to initiate */ |
| | 859 | return nil; |
| | 860 | } |
| | 861 | |
| | 862 | /* show a topic response */ |
| | 863 | showTopicResponse(fromActor, topic, resp) |
| | 864 | { |
| | 865 | local actor = getTopicOwner(); |
| | 866 | local newNode; |
| | 867 | |
| | 868 | /* |
| | 869 | * note whether the response is conversational - we need to do |
| | 870 | * this ahead of time, since invoking the response can sometimes |
| | 871 | * have the side effect of changing the response's status |
| | 872 | */ |
| | 873 | local isConv = resp.isConversational; |
| | 874 | |
| | 875 | /* tell the conversation manager we're starting a response */ |
| | 876 | conversationManager.beginResponse(actor); |
| | 877 | |
| | 878 | /* let the response object handle it */ |
| | 879 | resp.handleTopic(fromActor, topic); |
| | 880 | |
| | 881 | /* |
| | 882 | * By default, after showing a response, we want to leave the |
| | 883 | * conversation node tree entirely if we didn't explicitly set |
| | 884 | * the next node in the course of the response. So, set the |
| | 885 | * default new node to 'nil'. However, if the topic is |
| | 886 | * non-conversational, it shouldn't affect the conversation |
| | 887 | * thread at all, so leave the current node unchanged. |
| | 888 | */ |
| | 889 | if (isConv) |
| | 890 | newNode = nil; |
| | 891 | else |
| | 892 | newNode = actor.curConvNode; |
| | 893 | |
| | 894 | /* tell the conversation manager we're done with the response */ |
| | 895 | conversationManager.finishResponse(actor, newNode); |
| | 896 | } |
| | 897 | |
| | 898 | /* |
| | 899 | * Our 'ask about', 'ask for', 'tell about', 'give', 'show', |
| | 900 | * miscellaneous, command, and self-initiated topic databases - these |
| | 901 | * are vectors we initialize as needed. Since every actor and every |
| | 902 | * actor state has its own separate topic database, it's likely that |
| | 903 | * the bulk of these databases will be empty, so we don't bother even |
| | 904 | * creating a vector for a topic list until the first topic is added. |
| | 905 | * This means we have to be able to cope with these being nil |
| | 906 | * anywhere we use them. |
| | 907 | */ |
| | 908 | askTopics = nil |
| | 909 | askForTopics = nil |
| | 910 | tellTopics = nil |
| | 911 | showTopics = nil |
| | 912 | giveTopics = nil |
| | 913 | miscTopics = nil |
| | 914 | commandTopics = nil |
| | 915 | initiateTopics = nil |
| | 916 | |
| | 917 | /* our special command database */ |
| | 918 | specialTopics = nil |
| | 919 | ; |
| | 920 | |
| | 921 | /* ------------------------------------------------------------------------ */ |
| | 922 | /* |
| | 923 | * A "suggested" topic. These provide suggestions for things the player |
| | 924 | * might want to ASK or TELL another actor about. At certain times |
| | 925 | * (specifically, when starting a conversation with HELLO or TALK TO, or |
| | 926 | * when the player enters a TOPICS command to explicitly ask for a list |
| | 927 | * of topic suggestions), we'll look for these objects in the actor or |
| | 928 | * actor state for the actor to whom we're talking. We'll show a list |
| | 929 | * of each currently active suggestion we find. This gives the player |
| | 930 | * some guidance of what to talk about. For example: |
| | 931 | * |
| | 932 | * >talk to bob |
| | 933 | *. "Excuse me," you say. |
| | 934 | * |
| | 935 | * Bob looks up from his newspaper. "Yes? Oh, you again." |
| | 936 | * |
| | 937 | * (You'd like to ask him about the black book, the candle, and the |
| | 938 | * bell, and tell him about the crypt.) |
| | 939 | * |
| | 940 | * Topic suggestions are entirely optional. Some authors don't like the |
| | 941 | * idea, since they think it's too much like a menu system, and just |
| | 942 | * gives away the solution to the game. If you don't want to have |
| | 943 | * anything to do with topic suggestions, we won't force you - simply |
| | 944 | * don't define any SuggestedTopic objects, and the library will never |
| | 945 | * offer suggestions and will even disable the TOPICS command. |
| | 946 | * |
| | 947 | * If you do want to use topic suggestions, the easiest way to use this |
| | 948 | * class is to combine it using multiple inheritance with a TopicEntry |
| | 949 | * object. You just have to add SuggestedTopic to the superclass list |
| | 950 | * for your topic entry object, and give the suggested topic a name |
| | 951 | * string (using a property and format defined by the language-specific |
| | 952 | * library) to display in suggestions lists. Doing this, the suggestion |
| | 953 | * will automatically be enabled whenever the topic entry is available, |
| | 954 | * and will automatically be removed from the suggestions when the topic |
| | 955 | * is invoked in conversation (in other words, we'll only suggest asking |
| | 956 | * about the topic until it's been asked about once). |
| | 957 | * |
| | 958 | * Topic suggestions can be associated with an actor or an actor state; |
| | 959 | * these are topics that a given character would like to talk to the |
| | 960 | * associated actor about. The association is a bit tricky: suggested |
| | 961 | * topic objects are stored with the actor being *talked to*. For |
| | 962 | * example, if we want to suggest topics that the player character might |
| | 963 | * want to ASK BILL ABOUT, we store these suggestions with *Bill*. We |
| | 964 | * do NOT store the suggestions with the player character. This might |
| | 965 | * seem backwards at first glance, since fundamentally the suggestions |
| | 966 | * belong in the player character's "brain" - they are, after all, |
| | 967 | * things the player character wants to talk about. In practice, |
| | 968 | * though, there are two things that make it easier to keep the |
| | 969 | * information with the character being asked. First, in most games, |
| | 970 | * there's just one player character, so one of the two actors in each |
| | 971 | * association will always be the player character; by storing the |
| | 972 | * objects with the NPC, we can just let the PC be assumed as the other |
| | 973 | * actor as a default, saving us some typing that would be necessary if |
| | 974 | * we had to specify each object in the other direction. Second, we |
| | 975 | * keep the *response* objects associated with the character being asked |
| | 976 | * - that association is intuitive, at least. The thing is, we can |
| | 977 | * usually combine the suggestion and response into a single object, |
| | 978 | * saving another bunch of typing; if we didn't keep the suggestion with |
| | 979 | * the character being asked, we couldn't combine the suggestions and |
| | 980 | * responses this way, since they'd have to be associated with different |
| | 981 | * actors. |
| | 982 | */ |
| | 983 | class SuggestedTopic: object |
| | 984 | /* |
| | 985 | * The name of the suggestion. The rules for setting this vary by |
| | 986 | * language; in the English version, we'll display the fullName when |
| | 987 | * we show a stand-alone item, and the groupName when we appear in a |
| | 988 | * list group (such as a group of ASK ABOUT or TELL ABOUT |
| | 989 | * suggestions). |
| | 990 | * |
| | 991 | * In English, the fullName should be suitable for use after |
| | 992 | * 'could': "You could <fullName>, <fullName>, or <fullName>". |
| | 993 | * |
| | 994 | * In English, the phrasing where the 'name' property is used |
| | 995 | * depends on the specific subclass, but it should usually be a |
| | 996 | * qualified noun phrase (that is, it should include a qualifier |
| | 997 | * such as "a" or "the" or a possessive). For ASK and TELL, for |
| | 998 | * example, the 'name' should be suitable for use after ABOUT: "You |
| | 999 | * could ask him about <the lighthouse>, <Bob's black book>, or <the |
| | 1000 | * weather>." |
| | 1001 | * |
| | 1002 | * By default, we'll walk up our 'location' tree looking for another |
| | 1003 | * suggested topic; if we find one, we'll use its corresponding name |
| | 1004 | * values. |
| | 1005 | */ |
| | 1006 | fullName = (fromEnclosingSuggestedTopic(&fullName, '')) |
| | 1007 | name = (fromEnclosingSuggestedTopic(&name, '')) |
| | 1008 | |
| | 1009 | /* |
| | 1010 | * Our associated topic. In most cases, this will be initialized |
| | 1011 | * automatically: if this suggested topic object is also a |
| | 1012 | * TopicEntry object (using multiple inheritance), we'll set this |
| | 1013 | * during start-up to 'self', or if our location is a TopicEntry, |
| | 1014 | * we'll set this to our location. This only needs to be |
| | 1015 | * initialized manually if neither of those conditions is true. |
| | 1016 | */ |
| | 1017 | associatedTopic = nil |
| | 1018 | |
| | 1019 | /* |
| | 1020 | * Set the location to the actor to ask or tell about this topic. |
| | 1021 | * This is the target of the ASK ABOUT or TELL ABOUT command, NOT |
| | 1022 | * the actor who's doing the asking. This can also be set to a |
| | 1023 | * TopicEntry object, in which case we'll be associated with the |
| | 1024 | * actor with which the topic entry is associated, and we'll also |
| | 1025 | * automatically tie the topic entry to this suggestion. |
| | 1026 | * |
| | 1027 | * Because we're using the location property, you can use the '+' |
| | 1028 | * notation to add a suggested topic to the target actor, state |
| | 1029 | * objects, or topic entry. |
| | 1030 | */ |
| | 1031 | location = nil |
| | 1032 | |
| | 1033 | /* |
| | 1034 | * The actor who *wants* to ask or tell about this topic. Our |
| | 1035 | * location property gives the actor to be asked or told, because |
| | 1036 | * we're associated with the target actor - the same actor who has |
| | 1037 | * the TopicEntry information for the topic. This property, in |
| | 1038 | * contrast, gives the actor who's doing the asking. |
| | 1039 | * |
| | 1040 | * By default, we return the player character; in most cases, you |
| | 1041 | * won't have to override this. In most games, only the player |
| | 1042 | * character uses the suggested topic mechanism, because there's no |
| | 1043 | * reason to suggest topics for NPC's - they're just automata, after |
| | 1044 | * all, so if we want them to ask something, we can just program |
| | 1045 | * them to ask it directly. Also, most games have only one player |
| | 1046 | * character. Games that meet these criteria won't ever have to |
| | 1047 | * override this. If you do have multiple player characters, you'll |
| | 1048 | * probably want to override this for each suggested topic to |
| | 1049 | * indicate which character wants to ask about the topic, as the |
| | 1050 | * different player characters might have different things they'd |
| | 1051 | * want to talk about. |
| | 1052 | */ |
| | 1053 | suggestTo = (gPlayerChar) |
| | 1054 | |
| | 1055 | /* the ListGroup with which we're to list this suggestion */ |
| | 1056 | suggestionGroup = [] |
| | 1057 | |
| | 1058 | /* find the nearest enclosing SuggestedTopic parent */ |
| | 1059 | findEnclosingSuggestedTopic() |
| | 1060 | { |
| | 1061 | /* walk up our location list */ |
| | 1062 | for (local loc = location ; loc != nil ; loc = loc.location) |
| | 1063 | { |
| | 1064 | /* if this is a suggested topic, it's what we're looking for */ |
| | 1065 | if (loc.ofKind(SuggestedTopic)) |
| | 1066 | return loc; |
| | 1067 | } |
| | 1068 | |
| | 1069 | /* didn't find anything */ |
| | 1070 | return nil; |
| | 1071 | } |
| | 1072 | |
| | 1073 | /* find the outermost enclosing SuggestedTopic parent */ |
| | 1074 | findOuterSuggestedTopic() |
| | 1075 | { |
| | 1076 | local outer; |
| | 1077 | |
| | 1078 | /* walk up our location list */ |
| | 1079 | for (local loc = self, outer = nil ; loc != nil ; loc = loc.location) |
| | 1080 | { |
| | 1081 | /* if this is a suggested topic, it's the outermost so far */ |
| | 1082 | if (loc.ofKind(SuggestedTopic)) |
| | 1083 | outer = loc; |
| | 1084 | } |
| | 1085 | |
| | 1086 | /* return the outermost suggested topic we found */ |
| | 1087 | return outer; |
| | 1088 | } |
| | 1089 | |
| | 1090 | /* |
| | 1091 | * get a property from the nearest enclosing SuggestedTopic, or |
| | 1092 | * return the given default value if there is no enclosing |
| | 1093 | * SuggestedTopic |
| | 1094 | */ |
| | 1095 | fromEnclosingSuggestedTopic(prop, defaultVal) |
| | 1096 | { |
| | 1097 | /* look for the nearest enclosing suggested topic */ |
| | 1098 | local enc = findEnclosingSuggestedTopic(); |
| | 1099 | |
| | 1100 | /* |
| | 1101 | * return the desired property from the enclosing suggested |
| | 1102 | * topic object if we found one, or the default if there is no |
| | 1103 | * enclosing object |
| | 1104 | */ |
| | 1105 | return (enc != nil ? enc.(prop) : defaultVal); |
| | 1106 | } |
| | 1107 | |
| | 1108 | /* |
| | 1109 | * Should we suggest this topic to the given actor? We'll return |
| | 1110 | * true if the actor is the same actor for which this suggestion is |
| | 1111 | * intended, and the associated topic entry is currently active, and |
| | 1112 | * we haven't already satisfied our curiosity about the topic. |
| | 1113 | */ |
| | 1114 | isSuggestionActive(actor, scopeList) |
| | 1115 | { |
| | 1116 | /* |
| | 1117 | * Check to see if this is our target actor; that the associated |
| | 1118 | * topic itself is active; that our curiosity hasn't already been |
| | 1119 | * satisfied; and that it's at least possible to match the |
| | 1120 | * associated topic right now. If all of these conditions are |
| | 1121 | * met, we can make this suggestion. |
| | 1122 | */ |
| | 1123 | return (actor == suggestTo |
| | 1124 | && associatedTopicIsActive() |
| | 1125 | && associatedTopicCanMatch(actor, scopeList) |
| | 1126 | && !curiositySatisfied); |
| | 1127 | } |
| | 1128 | |
| | 1129 | /* |
| | 1130 | * The number of times to suggest asking about our topic. When |
| | 1131 | * we've asked about our associated topic this many times, we'll |
| | 1132 | * have satisfied our curiosity. In most cases, we'll only want to |
| | 1133 | * suggest a topic until it's asked about once, since most topics |
| | 1134 | * only have a single meaningful response, so we'll use 1 as the |
| | 1135 | * default. This should be overridden in cases where a topic will |
| | 1136 | * reveal more information when asked several times. If this is |
| | 1137 | * nil, it means that there's no limit to the number of times to |
| | 1138 | * suggest asking about this. |
| | 1139 | */ |
| | 1140 | timesToSuggest = 1 |
| | 1141 | |
| | 1142 | /* |
| | 1143 | * Have we satisfied our curiosity about this topic? Returns true |
| | 1144 | * if so, nil if not. We'll never suggest a topic when this returns |
| | 1145 | * true, because this means that the player no longer feels the need |
| | 1146 | * to ask about the topic. |
| | 1147 | */ |
| | 1148 | curiositySatisfied = (timesToSuggest != nil |
| | 1149 | && associatedTopicTalkCount() >= timesToSuggest) |
| | 1150 | |
| | 1151 | /* initialize - this is called automatically during pre-initialization */ |
| | 1152 | initializeSuggestedTopic() |
| | 1153 | { |
| | 1154 | /* if we have a location, link up with our location */ |
| | 1155 | if (location != nil) |
| | 1156 | location.addSuggestedTopic(self); |
| | 1157 | |
| | 1158 | /* |
| | 1159 | * if we're also a TopicEntry (using multiple inheritance), then |
| | 1160 | * we are our own associated topic object |
| | 1161 | */ |
| | 1162 | if (ofKind(TopicEntry)) |
| | 1163 | associatedTopic = self; |
| | 1164 | } |
| | 1165 | |
| | 1166 | /* |
| | 1167 | * Methods that rely on the associated topic. We isolate these in a |
| | 1168 | * few methods here so that the rest of class doesn't depend on the |
| | 1169 | * exact nature of our topic association. In particular, this allows |
| | 1170 | * for subclasses that don't have an associated topic at all, or that |
| | 1171 | * have multiple associated topics. Subclasses with specialized |
| | 1172 | * topic relationships can simply override these methods to define |
| | 1173 | * these methods appropriately. |
| | 1174 | */ |
| | 1175 | |
| | 1176 | /* is the associated topic active? */ |
| | 1177 | associatedTopicIsActive() { return associatedTopic.checkIsActive(); } |
| | 1178 | |
| | 1179 | /* get the number of previous invocations of the associated topic */ |
| | 1180 | associatedTopicTalkCount() { return associatedTopic.talkCount; } |
| | 1181 | |
| | 1182 | /* is it possible to match the associated topic? */ |
| | 1183 | associatedTopicCanMatch(actor, scopeList) |
| | 1184 | { return associatedTopic.isMatchPossible(actor, scopeList); } |
| | 1185 | |
| | 1186 | /* |
| | 1187 | * Note that we're being shown in a topic inventory listing. By |
| | 1188 | * default, we don't do anything here, but subclasses can use this to |
| | 1189 | * do any extra work they want to do on being listed. |
| | 1190 | */ |
| | 1191 | noteSuggestion() { } |
| | 1192 | ; |
| | 1193 | |
| | 1194 | /* |
| | 1195 | * A suggested topic that applies to an entire AltTopic group. |
| | 1196 | * |
| | 1197 | * Normally, a suggestion is tied to an individual TopicEntry. This |
| | 1198 | * means that when a topic has several AltTopic alternatives, each |
| | 1199 | * AltTopic can be its own separate, independent suggestion. A |
| | 1200 | * particular alternative can be a suggestion or not, independently of |
| | 1201 | * the other alternatives for the same TopicEntry. Since each AltTopic |
| | 1202 | * is a separate suggestion, asking about one of the alternatives won't |
| | 1203 | * have any effect on the "curiosity" about the other alternatives - in |
| | 1204 | * other words, the other alternatives will be separately suggested when |
| | 1205 | * they become active. |
| | 1206 | * |
| | 1207 | * In many cases, it's better for an entire set of alternatives to be |
| | 1208 | * treated as a single suggested topic. That is, we want to suggest the |
| | 1209 | * topic when ANY of the alternatives is active, and asking about any one |
| | 1210 | * of the alternatives will satisfy the PC's curiosity for ALL of the |
| | 1211 | * alternatives. This sort of arrangement is usually better for cases |
| | 1212 | * where the conditions that trigger the different alternatives aren't |
| | 1213 | * things that ought to make the PC think to ask the same question again. |
| | 1214 | * |
| | 1215 | * Use this class by associating it with the *root* TopicEntry of the |
| | 1216 | * group of alternatives. You can do this most simply by mixing this |
| | 1217 | * class into the superclass list of the root TopicEntry: |
| | 1218 | * |
| | 1219 | *. + AskTellTopic, SuggestedTopicTree, SuggestedAskTopic |
| | 1220 | *. // ... |
| | 1221 | *. ; |
| | 1222 | * ++ AltTopic ... ; |
| | 1223 | * ++ AltTopic ... ; |
| | 1224 | * |
| | 1225 | * This makes the entire group of AltTopics part of the same suggestion. |
| | 1226 | * Note that you must *also* include SuggestedAsk, SuggestedTellTopic, or |
| | 1227 | * one of the other specialized types among the superclass, to indicate |
| | 1228 | * which kind of suggestion this is. |
| | 1229 | */ |
| | 1230 | class SuggestedTopicTree: SuggestedTopic |
| | 1231 | /* is the associated topic active? */ |
| | 1232 | associatedTopicIsActive() |
| | 1233 | { |
| | 1234 | /* the topic is active if anything in the AltTopic group is active */ |
| | 1235 | return associatedTopic.anyAltIsActive; |
| | 1236 | } |
| | 1237 | |
| | 1238 | /* get the number of previous invocations of the associated topic */ |
| | 1239 | associatedTopicTalkCount() |
| | 1240 | { |
| | 1241 | /* return the number of invocations of any alternative */ |
| | 1242 | return associatedTopic.altTalkCount; |
| | 1243 | } |
| | 1244 | ; |
| | 1245 | |
| | 1246 | /* |
| | 1247 | * A suggested ASK ABOUT topic. We'll list ASK ABOUT topics together in |
| | 1248 | * a subgroup ("you'd like to ask him about the book, the candle, and |
| | 1249 | * the bell..."). |
| | 1250 | */ |
| | 1251 | class SuggestedAskTopic: SuggestedTopic |
| | 1252 | suggestionGroup = [suggestionAskGroup] |
| | 1253 | ; |
| | 1254 | |
| | 1255 | /* |
| | 1256 | * A suggested TELL ABOUT topic. We'll list TELL ABOUT topics together |
| | 1257 | * in a subgroup. |
| | 1258 | */ |
| | 1259 | class SuggestedTellTopic: SuggestedTopic |
| | 1260 | suggestionGroup = [suggestionTellGroup] |
| | 1261 | ; |
| | 1262 | |
| | 1263 | /* |
| | 1264 | * A suggested ASK FOR topic. We'll list ASK FOR topics together as a |
| | 1265 | * group. |
| | 1266 | */ |
| | 1267 | class SuggestedAskForTopic: SuggestedTopic |
| | 1268 | suggestionGroup = [suggestionAskForGroup] |
| | 1269 | ; |
| | 1270 | |
| | 1271 | /* |
| | 1272 | * A suggested GIVE TO topic. |
| | 1273 | */ |
| | 1274 | class SuggestedGiveTopic: SuggestedTopic |
| | 1275 | suggestionGroup = [suggestionGiveGroup] |
| | 1276 | ; |
| | 1277 | |
| | 1278 | /* |
| | 1279 | * A suggested SHOW TO topic. |
| | 1280 | */ |
| | 1281 | class SuggestedShowTopic: SuggestedTopic |
| | 1282 | suggestionGroup = [suggestionShowGroup] |
| | 1283 | ; |
| | 1284 | |
| | 1285 | /* |
| | 1286 | * A suggested YES/NO topic |
| | 1287 | */ |
| | 1288 | class SuggestedYesTopic: SuggestedTopic |
| | 1289 | suggestionGroup = [suggestionYesNoGroup] |
| | 1290 | ; |
| | 1291 | class SuggestedNoTopic: SuggestedTopic |
| | 1292 | suggestionGroup = [suggestionYesNoGroup] |
| | 1293 | ; |
| | 1294 | |
| | 1295 | /* ------------------------------------------------------------------------ */ |
| | 1296 | /* |
| | 1297 | * A conversation node. Conversation nodes are supplemental topic |
| | 1298 | * databases that represent a point in time in a conversation - a |
| | 1299 | * particular context that arises from what came immediately before in |
| | 1300 | * the conversation. A conversation node is used to set up a group of |
| | 1301 | * special responses that make sense only in a momentary context within a |
| | 1302 | * conversation. |
| | 1303 | * |
| | 1304 | * A ConvNode object must be nested (via the 'location' property) within |
| | 1305 | * an actor or an ActorState. This is how we associate the ConvNode with |
| | 1306 | * its actor. Note that putting a ConvNode inside an ActorState doesn't |
| | 1307 | * do anything different from putting the node directly inside the |
| | 1308 | * ActorState's actor - we allow it only for convenience, to allow |
| | 1309 | * greater flexibility arranging source code. |
| | 1310 | */ |
| | 1311 | class ConvNode: ActorTopicDatabase |
| | 1312 | /* |
| | 1313 | * Every ConvNode must have a name property. This is a string |
| | 1314 | * identifying the object. Use this name string instead of a regular |
| | 1315 | * object name (so ConvNode instances can essentially always be |
| | 1316 | * anonymous, as far as the compiler is concerned). This string is |
| | 1317 | * used to find the ConvNode in the master ConvNode database |
| | 1318 | * maintained in the conversationManager object. |
| | 1319 | * |
| | 1320 | * A ConvNode name should be unique with respect to all other |
| | 1321 | * ConvNode objects - no two ConvNode objects should have the same |
| | 1322 | * name string. Other than this, the name strings are arbitrary. |
| | 1323 | * (However, they shouldn't contain any '>' characters, because this |
| | 1324 | * would prevent them from being used in <.convnode> tags, which is |
| | 1325 | * the main place ConvNode's are usually used.) |
| | 1326 | */ |
| | 1327 | name = '' |
| | 1328 | |
| | 1329 | /* |
| | 1330 | * Is this node "sticky"? If so, we'll stick to this node if we |
| | 1331 | * show a response that doesn't set a new node. By default, we're |
| | 1332 | * not sticky, so if we show a response that doesn't set a new node |
| | 1333 | * and doesn't use a <.convstay> tag, we'll simply forget the node |
| | 1334 | * and set the actor to no current ConvNode. |
| | 1335 | * |
| | 1336 | * Sticky nodes are useful when you want the actor to stay |
| | 1337 | * on-subject even when the player digresses to talk about other |
| | 1338 | * things. This is useful when the actor has a particular thread |
| | 1339 | * they want to drive the conversation along. |
| | 1340 | */ |
| | 1341 | isSticky = nil |
| | 1342 | |
| | 1343 | /* |
| | 1344 | * Show our NPC-initiated greeting. This is invoked when our actor's |
| | 1345 | * initiateConversation() method is called to cause our actor to |
| | 1346 | * initiate a conversation with the player character. This method |
| | 1347 | * should show what our actor says to initiate the conversation. By |
| | 1348 | * default, we'll invoke our npcGreetingList's script, if the |
| | 1349 | * property is non-nil. |
| | 1350 | * |
| | 1351 | * A greeting should always be defined for any ConvNode that's used |
| | 1352 | * in an initiateConversation() call. |
| | 1353 | * |
| | 1354 | * To define a greeting when defining a ConvNode, you can override |
| | 1355 | * this method with a simple double-quoted string message, or you can |
| | 1356 | * define an npcGreetingList property as an EventList of some kind. |
| | 1357 | */ |
| | 1358 | npcGreetingMsg() |
| | 1359 | { |
| | 1360 | /* if we have an npcGreetingList property, invoke the script */ |
| | 1361 | if (npcGreetingList != nil) |
| | 1362 | npcGreetingList.doScript(); |
| | 1363 | } |
| | 1364 | |
| | 1365 | /* an optional EventList containing our NPC-initiated greetings */ |
| | 1366 | npcGreetingList = nil |
| | 1367 | |
| | 1368 | /* |
| | 1369 | * Our NPC-initiated conversation continuation message. This is |
| | 1370 | * invoked on each turn (during the NPC's takeTurn() daemon |
| | 1371 | * processing) that we're in this conversation node and the player |
| | 1372 | * character doesn't do anything conversational. This allows the NPC |
| | 1373 | * to carry on the conversation of its own volition. Define this as |
| | 1374 | * a double-quoted string if you want the NPC to say something to |
| | 1375 | * continue the conversation. |
| | 1376 | */ |
| | 1377 | npcContinueMsg = nil |
| | 1378 | |
| | 1379 | /* |
| | 1380 | * An optional EventList containing NPC-initiated continuation |
| | 1381 | * messages. You can define an EventList here instead of defining |
| | 1382 | * npcContinueMsg, if you want more than one continuation message. |
| | 1383 | */ |
| | 1384 | npcContinueList = nil |
| | 1385 | |
| | 1386 | /* |
| | 1387 | * Flag: automatically show a topic inventory on activating this |
| | 1388 | * conversation node. Some conversation nodes have sufficiently |
| | 1389 | * obscure entries that it's desirable to show a topic inventory |
| | 1390 | * automatically when the node becomes active. |
| | 1391 | * |
| | 1392 | * By default, we automatically show a topic inventory if the node |
| | 1393 | * contains an active SpecialTopic entry. Since special topics are |
| | 1394 | * inherently obscure, in that they use non-standard commands, we |
| | 1395 | * always want to show topics when one of these becomes active. |
| | 1396 | */ |
| | 1397 | autoShowTopics() |
| | 1398 | { |
| | 1399 | /* if we have an active special topic, show the topic inventory */ |
| | 1400 | return (specialTopics != nil |
| | 1401 | && specialTopics.indexWhich({x: x.checkIsActive()}) != nil); |
| | 1402 | } |
| | 1403 | |
| | 1404 | /* our NPC is initiating a conversation starting with this node */ |
| | 1405 | npcInitiateConversation() |
| | 1406 | { |
| | 1407 | local actor = getActor(); |
| | 1408 | |
| | 1409 | /* tell the conversation manager we're the actor who's talking */ |
| | 1410 | conversationManager.beginResponse(actor); |
| | 1411 | |
| | 1412 | /* note that we're in conversation with the player character now */ |
| | 1413 | getActor().noteConversation(gPlayerChar); |
| | 1414 | |
| | 1415 | /* show our NPC greeting */ |
| | 1416 | npcGreetingMsg(); |
| | 1417 | |
| | 1418 | /* look for an ActorHelloTopic within the node */ |
| | 1419 | handleTopic(gPlayerChar, actorHelloTopicObj, helloConvType, nil); |
| | 1420 | |
| | 1421 | /* end the response, staying in the current ConvNode by default */ |
| | 1422 | conversationManager.finishResponse(actor, self); |
| | 1423 | } |
| | 1424 | |
| | 1425 | /* |
| | 1426 | * Continue the conversation of the NPC's own volition. Returns |
| | 1427 | * true if we displayed anything, nil if not. |
| | 1428 | */ |
| | 1429 | npcContinueConversation() |
| | 1430 | { |
| | 1431 | local actor = getActor(); |
| | 1432 | local disp; |
| | 1433 | |
| | 1434 | /* tell the conversation manager we're starting a response */ |
| | 1435 | conversationManager.beginResponse(actor); |
| | 1436 | |
| | 1437 | /* show our text, watching to see if we generate any output */ |
| | 1438 | disp = outputManager.curOutputStream.watchForOutput(new function() |
| | 1439 | { |
| | 1440 | /* |
| | 1441 | * if we have a continuation list, invoke it; otherwise if we |
| | 1442 | * have a continuation message, show it; otherwise, just |
| | 1443 | * return nil to let the caller know we have nothing to add |
| | 1444 | */ |
| | 1445 | if (npcContinueList != nil) |
| | 1446 | npcContinueList.doScript(); |
| | 1447 | else |
| | 1448 | npcContinueMsg; |
| | 1449 | }); |
| | 1450 | |
| | 1451 | /* end the response, staying in the current ConvNode by default */ |
| | 1452 | conversationManager.finishResponse(actor, self); |
| | 1453 | |
| | 1454 | /* |
| | 1455 | * if we actually said anything, note that we're in conversation |
| | 1456 | * with the player character |
| | 1457 | */ |
| | 1458 | if (disp) |
| | 1459 | getActor().noteConversation(gPlayerChar); |
| | 1460 | |
| | 1461 | /* return the display indication */ |
| | 1462 | return disp; |
| | 1463 | } |
| | 1464 | |
| | 1465 | /* our actor is our location, or our location's actor */ |
| | 1466 | getActor() |
| | 1467 | { |
| | 1468 | /* if our location is an actor state, return the state's actor */ |
| | 1469 | if (location.ofKind(ActorState)) |
| | 1470 | return location.getActor(); |
| | 1471 | |
| | 1472 | /* otherwise, our location must be our actor */ |
| | 1473 | return location; |
| | 1474 | } |
| | 1475 | |
| | 1476 | /* our actor is the "owner" of our topics */ |
| | 1477 | getTopicOwner() { return getActor(); } |
| | 1478 | |
| | 1479 | /* |
| | 1480 | * Handle a conversation topic. The actor state object will call |
| | 1481 | * this to give the ConvNode the first crack at handling a |
| | 1482 | * conversation command. We'll return true if we handle the command, |
| | 1483 | * nil if not. Our default handling is to look up the topic in the |
| | 1484 | * given database list property, and handle it through the TopicEntry |
| | 1485 | * we find there, if any. |
| | 1486 | */ |
| | 1487 | handleConversation(otherActor, topic, convType, path) |
| | 1488 | { |
| | 1489 | /* try handling it, returning the handled/not-handled result */ |
| | 1490 | return handleTopic(otherActor, topic, convType, path); |
| | 1491 | } |
| | 1492 | |
| | 1493 | /* |
| | 1494 | * Can we end the conversation? If so, return true; our caller will |
| | 1495 | * invoke our endConversation() to let us know that the conversation |
| | 1496 | * is over. |
| | 1497 | * |
| | 1498 | * To prevent the conversation from ending, simply return nil. |
| | 1499 | * |
| | 1500 | * In most cases, you won't want to force the conversation to keep |
| | 1501 | * going without any comment. Instead, you'll want to display some |
| | 1502 | * message to let the player know what's going on - something like |
| | 1503 | * "Hey! We're not through here!" If you do display a message, then |
| | 1504 | * rather than returning nil, return the special value blockEndConv - |
| | 1505 | * this tells the caller that the actor said something, so the caller |
| | 1506 | * will call noteConvAction() to prevent further generated |
| | 1507 | * conversation output on this same turn. |
| | 1508 | * |
| | 1509 | * 'reason' gives the reason the conversation is ending, as an |
| | 1510 | * endConvXxx enum code. |
| | 1511 | */ |
| | 1512 | canEndConversation(actor, reason) { return true; } |
| | 1513 | |
| | 1514 | /* |
| | 1515 | * Receive notification that our actor is ending a stateful |
| | 1516 | * conversation. This is called before the normal |
| | 1517 | * InConversationState disengagement operations. 'reason' is one of |
| | 1518 | * the endConvXxx enums, indicating why the conversation is ending. |
| | 1519 | * |
| | 1520 | * Instances can override this for special behavior on terminating a |
| | 1521 | * conversation. For example, an actor who just asked a question |
| | 1522 | * could say something to indicate that the other actor is being |
| | 1523 | * rude. By default, we do nothing. |
| | 1524 | * |
| | 1525 | * Note that there's no way to block the ending of the conversation |
| | 1526 | * here. If you want to prevent the conversation from ending, use |
| | 1527 | * canEndConversation() instead. |
| | 1528 | */ |
| | 1529 | endConversation(actor, reason) { } |
| | 1530 | |
| | 1531 | /* |
| | 1532 | * Process a special command. Check the given command line string |
| | 1533 | * against all of our topics, and see if we have a match to any topic |
| | 1534 | * that takes a special command syntax. If we find a matching |
| | 1535 | * special topic, we'll note the match, and turn the command into our |
| | 1536 | * secret internal pseudo-command "XSPCLTOPIC". That command will |
| | 1537 | * then go through the parser, which will recognize it and process it |
| | 1538 | * using the normal conversational mechanisms, which will find the |
| | 1539 | * SpecialTopic we noted earlier (in this method) and display its |
| | 1540 | * response. |
| | 1541 | * |
| | 1542 | * 'str' is the original input string, exactly as entered by the |
| | 1543 | * player, and 'procStr' is the "processed" version of the input |
| | 1544 | * string. The nature of the processing varies by language, but |
| | 1545 | * generally this involves things like removing punctuation marks and |
| | 1546 | * any "noise words" that don't usually change the meaning of the |
| | 1547 | * input, at least for the purposes of matching a special topic. |
| | 1548 | */ |
| | 1549 | processSpecialCmd(str, procStr) |
| | 1550 | { |
| | 1551 | local match; |
| | 1552 | local cnt; |
| | 1553 | |
| | 1554 | /* we don't have an active special topic yet */ |
| | 1555 | activeSpecialTopic = nil; |
| | 1556 | |
| | 1557 | /* |
| | 1558 | * if we have no special topics, there's definitely no special |
| | 1559 | * processing we need to do |
| | 1560 | */ |
| | 1561 | if (specialTopics == nil) |
| | 1562 | return str; |
| | 1563 | |
| | 1564 | /* scan our special topics for a match */ |
| | 1565 | cnt = 0; |
| | 1566 | foreach (local cur in specialTopics) |
| | 1567 | { |
| | 1568 | /* if this one is active, and it matches the string, note it */ |
| | 1569 | if (cur.checkIsActive() && cur.matchPreParse(str, procStr)) |
| | 1570 | { |
| | 1571 | /* remember it as the last match */ |
| | 1572 | match = cur; |
| | 1573 | |
| | 1574 | /* count the match */ |
| | 1575 | ++cnt; |
| | 1576 | } |
| | 1577 | } |
| | 1578 | |
| | 1579 | /* |
| | 1580 | * If we found exactly one match, then activate it. If we found |
| | 1581 | * zero or more than one, ignore any special topics and proceed |
| | 1582 | * on the assumption that this is a normal command. (We ignore |
| | 1583 | * ambiguous matches because this probably means that the entire |
| | 1584 | * command is some very common word that happens to be acceptable |
| | 1585 | * as a keyword in one or more of our matches. In these cases, |
| | 1586 | * the common word was probably meant as an ordinary command, |
| | 1587 | * since the player would likely have been more specific if a |
| | 1588 | * special topic were really desired.) |
| | 1589 | */ |
| | 1590 | if (cnt == 1) |
| | 1591 | { |
| | 1592 | /* |
| | 1593 | * remember the active SpecialTopic - we'll use this memory |
| | 1594 | * to find it again when we get through the full command |
| | 1595 | * processing |
| | 1596 | */ |
| | 1597 | activeSpecialTopic = match; |
| | 1598 | |
| | 1599 | /* |
| | 1600 | * Change the command to our special internal pseudo-command |
| | 1601 | * that triggers the active special topic. Include the |
| | 1602 | * original string as a literal phrase, enclosed in double |
| | 1603 | * quotes and specially coded to ensure that the tokenizer |
| | 1604 | * doesn't become confused by any embedded quotes. |
| | 1605 | */ |
| | 1606 | return 'xspcltopic "' + SpecialTopicAction.encodeOrig(str) + '"'; |
| | 1607 | } |
| | 1608 | else |
| | 1609 | { |
| | 1610 | /* proceed, treating the original input as an ordinary command */ |
| | 1611 | return str; |
| | 1612 | } |
| | 1613 | } |
| | 1614 | |
| | 1615 | patWhitespace = static new RexPattern('<space>+') |
| | 1616 | patDelim = static new RexPattern('<punct|space>') |
| | 1617 | |
| | 1618 | /* |
| | 1619 | * Handle an XSPCLTOPIC command from the given actor. This is part |
| | 1620 | * two of the two-phase processing of SpecialTopic matches. Our |
| | 1621 | * pre-parser checks each SpecialTopic's custom syntax for a match |
| | 1622 | * to the player's text input, and if it finds a match, it sets our |
| | 1623 | * activeSpecialTopic property to the matching SpecialTopic, and |
| | 1624 | * changes the user's command to XSPCLTOPIC for processing by the |
| | 1625 | * regular parser. The regular parser sees the XSPCLTOPIC command, |
| | 1626 | * which is a valid verb that calls the issuing actor's |
| | 1627 | * saySpecialTopic() routine, which in turn forwards the request to |
| | 1628 | * the issuing actor's interlocutor's current conversation node - |
| | 1629 | * which is to say, 'self'. We complete the two-step procedure by |
| | 1630 | * going back to the active special topic object that we previously |
| | 1631 | * noted and showing its response. |
| | 1632 | */ |
| | 1633 | saySpecialTopic(fromActor) |
| | 1634 | { |
| | 1635 | /* make sure we have an active special topic object */ |
| | 1636 | if (activeSpecialTopic != nil) |
| | 1637 | { |
| | 1638 | local actor = getTopicOwner(); |
| | 1639 | |
| | 1640 | /* tell the conversation manager we're starting a response */ |
| | 1641 | conversationManager.beginResponse(actor); |
| | 1642 | |
| | 1643 | /* let the SpecialTopic handle the response */ |
| | 1644 | activeSpecialTopic.handleTopic(fromActor, nil); |
| | 1645 | |
| | 1646 | /* |
| | 1647 | * Tell the conversation manager we're done. By default, we |
| | 1648 | * want to leave the conversation tree entirely, so set the |
| | 1649 | * new default node to 'nil'. |
| | 1650 | */ |
| | 1651 | conversationManager.finishResponse(actor, nil); |
| | 1652 | |
| | 1653 | /* that wraps things up for the active special topic */ |
| | 1654 | activeSpecialTopic = nil; |
| | 1655 | } |
| | 1656 | else |
| | 1657 | { |
| | 1658 | /* |
| | 1659 | * There is no active special topic, so the player must have |
| | 1660 | * typed in the XSPCLTOPIC command explicitly - if we got |
| | 1661 | * here through the normal two-step procedure then this |
| | 1662 | * property would not be nil. Politely decline the command, |
| | 1663 | * since it's not for the player's direct use. |
| | 1664 | */ |
| | 1665 | gLibMessages.commandNotPresent; |
| | 1666 | } |
| | 1667 | } |
| | 1668 | |
| | 1669 | /* |
| | 1670 | * The active special topic. This is the SpecialTopic object that |
| | 1671 | * we matched during pre-parsing, so it's the one whose response we |
| | 1672 | * wish to show while processing the command we pre-parsed. |
| | 1673 | */ |
| | 1674 | activeSpecialTopic = nil |
| | 1675 | |
| | 1676 | /* |
| | 1677 | * Note that we're becoming active, with a reason code. Our actor |
| | 1678 | * will call this method when we're becoming active, as long as we |
| | 1679 | * weren't already active. |
| | 1680 | * |
| | 1681 | * 'reason' is a string giving a reason code for why we're being |
| | 1682 | * called. For calls from the library, this will be one of these |
| | 1683 | * codes: |
| | 1684 | * |
| | 1685 | * 'convnode' - processing a <.convnode> tag |
| | 1686 | * |
| | 1687 | * 'convend' - processing a <.convend> tag |
| | 1688 | * |
| | 1689 | * 'initiateConversation' - a call to Actor.initiateConversation() |
| | 1690 | * |
| | 1691 | * 'endConversation' - a call to Actor.endConversation() |
| | 1692 | * |
| | 1693 | * The reason code is provided so that the node can adapt its action |
| | 1694 | * for different trigger conditions, if desired. By default, we |
| | 1695 | * ignore the reason code and just call the basic noteActive() |
| | 1696 | * method. |
| | 1697 | */ |
| | 1698 | noteActiveReason(reason) |
| | 1699 | { |
| | 1700 | noteActive(); |
| | 1701 | } |
| | 1702 | |
| | 1703 | /* |
| | 1704 | * Note that we're becoming active, with a reason code. Our actor |
| | 1705 | * will call this method when we're becoming active, as long as we |
| | 1706 | * weren't already active. |
| | 1707 | * |
| | 1708 | * Note that if you want to adapt the method's behavior according to |
| | 1709 | * why the node was activated, you can override noteActiveReason() |
| | 1710 | * instead of this method. |
| | 1711 | */ |
| | 1712 | noteActive() |
| | 1713 | { |
| | 1714 | /* if desired, schedule a topic inventory whenever we're activated */ |
| | 1715 | if (autoShowTopics()) |
| | 1716 | conversationManager.scheduleTopicInventory(); |
| | 1717 | } |
| | 1718 | |
| | 1719 | /* |
| | 1720 | * Note that we're leaving this conversation node. This doesn't do |
| | 1721 | * anything by default, but individual instances might find the |
| | 1722 | * notification useful for triggering side effects. |
| | 1723 | */ |
| | 1724 | noteLeaving() { } |
| | 1725 | ; |
| | 1726 | |
| | 1727 | |
| | 1728 | /* ------------------------------------------------------------------------ */ |
| | 1729 | /* |
| | 1730 | * Pre-parser for special ConvNode-specific commands. When the player |
| | 1731 | * character is talking to another character, and the NPC's current |
| | 1732 | * ConvNode includes topics with their own commands, we'll check the |
| | 1733 | * player's input to see if it matches any of these topics. |
| | 1734 | */ |
| | 1735 | specialTopicPreParser: StringPreParser |
| | 1736 | doParsing(str, which) |
| | 1737 | { |
| | 1738 | local actor; |
| | 1739 | local node; |
| | 1740 | |
| | 1741 | /* |
| | 1742 | * don't handle this on requests for missing literals - these |
| | 1743 | * responses are always interpreted as literal text, so there's |
| | 1744 | * no way this could be a special ConvNode command |
| | 1745 | */ |
| | 1746 | if (which == rmcAskLiteral) |
| | 1747 | return str; |
| | 1748 | |
| | 1749 | /* |
| | 1750 | * if the player character isn't currently in conversation, or |
| | 1751 | * the actor with whom the player character is conversing doesn't |
| | 1752 | * have a current conversation node, there's nothing to do |
| | 1753 | */ |
| | 1754 | if ((actor = gPlayerChar.getCurrentInterlocutor()) == nil |
| | 1755 | || (node = actor.curConvNode) == nil) |
| | 1756 | return str; |
| | 1757 | |
| | 1758 | /* ask the conversation node to process the string */ |
| | 1759 | return node.processSpecialCmd(str, processInputStr(str)); |
| | 1760 | } |
| | 1761 | |
| | 1762 | /* |
| | 1763 | * Process the input string, as desired, for special-topic parsing. |
| | 1764 | * This method is for the language module's use; by default, we do |
| | 1765 | * nothing. |
| | 1766 | * |
| | 1767 | * Language modules should override this to remove punctuation marks |
| | 1768 | * and to do any other language-dependent processing to make the |
| | 1769 | * string parsable. |
| | 1770 | */ |
| | 1771 | processInputStr(str) { return str; } |
| | 1772 | ; |
| | 1773 | |
| | 1774 | /* ------------------------------------------------------------------------ */ |
| | 1775 | /* |
| | 1776 | * A conversational action type descriptor. This descriptor is used in |
| | 1777 | * handleConversation() in Actor and ActorState to describe the type of |
| | 1778 | * conversational action we're performing. The type descriptor object |
| | 1779 | * encapsulates a set of information that tells us how to handle the |
| | 1780 | * action. |
| | 1781 | */ |
| | 1782 | class ConvType: object |
| | 1783 | /* |
| | 1784 | * The unknown interlocutor message property. This is used when we |
| | 1785 | * try this conversational action without knowing whom we're talking |
| | 1786 | * to. For example, if we just say HELLO, and there's no one around |
| | 1787 | * to talk to, we'll use this as the default response. This can be a |
| | 1788 | * library message property, or simply a single-quoted string to |
| | 1789 | * display. |
| | 1790 | */ |
| | 1791 | unknownMsg = nil |
| | 1792 | |
| | 1793 | /* |
| | 1794 | * The TopicDatabase topic-list property. This is the property of |
| | 1795 | * the TopicDatabase object that we evaluate to get this list of |
| | 1796 | * topic entries to search for a match to the topic. |
| | 1797 | */ |
| | 1798 | topicListProp = nil |
| | 1799 | |
| | 1800 | /* the default response property for this action */ |
| | 1801 | defaultResponseProp = nil |
| | 1802 | |
| | 1803 | /* |
| | 1804 | * Call the default response property on the given topic database. |
| | 1805 | * This invokes the property given by defaultResponseProp(). We have |
| | 1806 | * both the property and the method to call the property because this |
| | 1807 | * allows us to test for the existence of the property and to call it |
| | 1808 | * with the appropriate argument list. |
| | 1809 | */ |
| | 1810 | defaultResponse(db, otherActor, topic) { } |
| | 1811 | |
| | 1812 | /* |
| | 1813 | * Perform any special follow-up action for this type of |
| | 1814 | * conversational action. |
| | 1815 | */ |
| | 1816 | afterResponse(actor, otherActor) { } |
| | 1817 | ; |
| | 1818 | |
| | 1819 | helloConvType: ConvType |
| | 1820 | unknownMsg = &sayHelloMsg |
| | 1821 | topicListProp = &miscTopics |
| | 1822 | defaultResponseProp = &defaultGreetingResponse |
| | 1823 | defaultResponse(db, other, topic) |
| | 1824 | { db.defaultGreetingResponse(other); } |
| | 1825 | |
| | 1826 | /* after an explicit HELLO, show any suggested topics */ |
| | 1827 | afterResponse(actor, otherActor) |
| | 1828 | { |
| | 1829 | /* show or schedule a topic inventory, as appropriate */ |
| | 1830 | conversationManager.showOrScheduleTopicInventory(actor, otherActor); |
| | 1831 | } |
| | 1832 | ; |
| | 1833 | |
| | 1834 | byeConvType: ConvType |
| | 1835 | unknownMsg = &sayGoodbyeMsg |
| | 1836 | topicListProp = &miscTopics |
| | 1837 | defaultResponseProp = &defaultGoodbyeResponse |
| | 1838 | defaultResponse(db, other, topic) |
| | 1839 | { db.defaultGoodbyeResponse(other); } |
| | 1840 | ; |
| | 1841 | |
| | 1842 | yesConvType: ConvType |
| | 1843 | unknownMsg = &sayYesMsg |
| | 1844 | topicListProp = &miscTopics |
| | 1845 | defaultResponseProp = &defaultYesResponse |
| | 1846 | defaultResponse(db, other, topic) |
| | 1847 | { db.defaultYesResponse(other); } |
| | 1848 | ; |
| | 1849 | |
| | 1850 | noConvType: ConvType |
| | 1851 | unknownMsg = &sayNoMsg |
| | 1852 | topicListProp = &miscTopics |
| | 1853 | defaultResponseProp = &defaultNoResponse |
| | 1854 | defaultResponse(db, other, topic) |
| | 1855 | { db.defaultNoResponse(other); } |
| | 1856 | ; |
| | 1857 | |
| | 1858 | askAboutConvType: ConvType |
| | 1859 | topicListProp = &askTopics |
| | 1860 | defaultResponseProp = &defaultAskResponse |
| | 1861 | defaultResponse(db, other, topic) |
| | 1862 | { db.defaultAskResponse(other, topic); } |
| | 1863 | ; |
| | 1864 | |
| | 1865 | askForConvType: ConvType |
| | 1866 | topicListProp = &askForTopics |
| | 1867 | defaultResponseProp = &defaultAskForResponse |
| | 1868 | defaultResponse(db, other, topic) |
| | 1869 | { db.defaultAskForResponse(other, topic); } |
| | 1870 | ; |
| | 1871 | |
| | 1872 | tellAboutConvType: ConvType |
| | 1873 | topicListProp = &tellTopics |
| | 1874 | defaultResponseProp = &defaultTellResponse |
| | 1875 | defaultResponse(db, other, topic) |
| | 1876 | { db.defaultTellResponse(other, topic); } |
| | 1877 | ; |
| | 1878 | |
| | 1879 | giveConvType: ConvType |
| | 1880 | topicListProp = &giveTopics |
| | 1881 | defaultResponseProp = &defaultGiveResponse |
| | 1882 | defaultResponse(db, other, topic) |
| | 1883 | { db.defaultGiveResponse(other, topic); } |
| | 1884 | ; |
| | 1885 | |
| | 1886 | showConvType: ConvType |
| | 1887 | topicListProp = &showTopics |
| | 1888 | defaultResponseProp = &defaultShowResponse |
| | 1889 | defaultResponse(db, other, topic) |
| | 1890 | { db.defaultShowResponse(other, topic); } |
| | 1891 | ; |
| | 1892 | |
| | 1893 | commandConvType: ConvType |
| | 1894 | topicListProp = &commandTopics |
| | 1895 | defaultResponseProp = &defaultCommandResponse |
| | 1896 | defaultResponse(db, other, topic) |
| | 1897 | { db.defaultCommandResponse(other, topic); } |
| | 1898 | ; |
| | 1899 | |
| | 1900 | /* |
| | 1901 | * This type is for NPC-initiated conversations. It's not a normal |
| | 1902 | * conversational action, since it doesn't involve handling a player |
| | 1903 | * command, but is usually instead triggered by an agenda item, |
| | 1904 | * takeTurn(), or other background activity. |
| | 1905 | */ |
| | 1906 | initiateConvType: ConvType |
| | 1907 | topicListProp = &initiateTopics |
| | 1908 | ; |
| | 1909 | |
| | 1910 | /* |
| | 1911 | * CONSULT ABOUT isn't a true conversational action, since it's applied |
| | 1912 | * to inanimate objects (such as books); but it's handled through the |
| | 1913 | * conversation system, so it needs a conversation type object |
| | 1914 | */ |
| | 1915 | consultConvType: ConvType |
| | 1916 | topicListProp = &consultTopics |
| | 1917 | ; |
| | 1918 | |
| | 1919 | /* ------------------------------------------------------------------------ */ |
| | 1920 | /* |
| | 1921 | * A topic database entry. Actors and actor state objects store topic |
| | 1922 | * databases; a topic database is essentially a set of these entries. |
| | 1923 | * |
| | 1924 | * A TopicEntry can go directly inside an Actor, in which case it's part |
| | 1925 | * of the actor's global set of topics; or, it can go inside an |
| | 1926 | * ActorState, in which case it's part of the state's database and is |
| | 1927 | * only active when the state is active; or, it can go inside a |
| | 1928 | * TopicGroup, which is a set of topics with a common controlling |
| | 1929 | * condition; or, it can go inside a ConvNode, in which case it's in |
| | 1930 | * effect only when the conversation node is active. |
| | 1931 | * |
| | 1932 | * Each entry is a relationship between a topic, which is something that |
| | 1933 | * can come up in an ASK or TELL action, and a handling for the topic. |
| | 1934 | * In addition, each entry determines what kind or kinds of actions it |
| | 1935 | * responds to. |
| | 1936 | * |
| | 1937 | * Note that TopicEntry objects are *not* simulation objects. Rather, |
| | 1938 | * these are abstract objects; they can be associated with simulation |
| | 1939 | * objects via the matching mechanism, but these are separate from the |
| | 1940 | * actual simulation objects. The reason for this separation is that a |
| | 1941 | * given simulation object might have many different response - the |
| | 1942 | * response could vary according to who's being asked the question, who's |
| | 1943 | * asking, and what else is happening in the game. |
| | 1944 | * |
| | 1945 | * An entry decides for itself if it matches a topic. By default, an |
| | 1946 | * entry can match based on either a simulation object, which we'll match |
| | 1947 | * to anything in the topic's "in scope" or "likely" match lists, or |
| | 1948 | * based on a regular expression string, which we'll match to the actual |
| | 1949 | * topic text entered in the player's command. |
| | 1950 | * |
| | 1951 | * An entry can decide how strongly it matches a topic. The database |
| | 1952 | * will choose the strongest match when multiple entries match the same |
| | 1953 | * topic. The strength of the match is given by a numeric score; the |
| | 1954 | * higher the score, the stronger the match. The match strength makes it |
| | 1955 | * easy to specify a hierarchy of topics from specific to general, so |
| | 1956 | * that we provide general responses to general topic areas, but can |
| | 1957 | * still respond to particular topics areas more specifically. For |
| | 1958 | * example, we might want to provide a specific match to the FROBNOZ |
| | 1959 | * SPELL object, talking about that particular magic spell, but provide a |
| | 1960 | * generic '.* spell' pattern to response to questions about any old |
| | 1961 | * spell. We'd give the generic pattern a lower score, so that the |
| | 1962 | * specific FROBNOZ SPELL response would win when it matches, but we'd |
| | 1963 | * fall back on the generic pattern in other cases. |
| | 1964 | */ |
| | 1965 | class TopicEntry: object |
| | 1966 | /* |
| | 1967 | * My matching simulation object or objects. This can be either a |
| | 1968 | * single object or a list of objects. |
| | 1969 | */ |
| | 1970 | matchObj = nil |
| | 1971 | |
| | 1972 | /* |
| | 1973 | * Is this topic active? This can be used to control how an actor |
| | 1974 | * can respond without have to worry about adding and removing topics |
| | 1975 | * manually at key events, or storing the topics in state objects. |
| | 1976 | * Sometimes, it's easier to just put a topic entry in the actor's |
| | 1977 | * database from the start, and test some condition dynamically when |
| | 1978 | * the topic is actually queried. To do this, override this method |
| | 1979 | * to test the condition that determines when the topic entry should |
| | 1980 | * become active. We'll never show the topic's response when |
| | 1981 | * isActive returns nil. By default, we simply return true to |
| | 1982 | * indicate that the topic entry is active. |
| | 1983 | */ |
| | 1984 | isActive = true |
| | 1985 | |
| | 1986 | /* |
| | 1987 | * Flag: we are a "conversational" topic. This is true by default. |
| | 1988 | * When this is set to nil, a ConversationReadyState will NOT show |
| | 1989 | * its greeting and will not enter its InConversationState to show |
| | 1990 | * this topic entry's response. |
| | 1991 | * |
| | 1992 | * This should be set to nil when the topic entry's response is |
| | 1993 | * non-conversational, in which case a greeting would be |
| | 1994 | * undesirable. This is appropriate for responses like "You don't |
| | 1995 | * think he'd want to talk about that", where the response indicates |
| | 1996 | * that the player character didn't even ask a question (or |
| | 1997 | * whatever). |
| | 1998 | */ |
| | 1999 | isConversational = true |
| | 2000 | |
| | 2001 | /* |
| | 2002 | * Do we imply a greeting? By default, all conversational topics |
| | 2003 | * imply a greeting. We separate this out so that the implied |
| | 2004 | * greeting can be controlled independently of whether or not we're |
| | 2005 | * actually conversational, if desired. |
| | 2006 | */ |
| | 2007 | impliesGreeting = (isConversational) |
| | 2008 | |
| | 2009 | /* |
| | 2010 | * Get the actor associated with the topic, if any. By default, |
| | 2011 | * we'll return our enclosing database's topic owner, if it's an |
| | 2012 | * actor - in almost all cases, if there's any actor associated with |
| | 2013 | * a topic, it's simply the owner of the database containing the |
| | 2014 | * topic. |
| | 2015 | */ |
| | 2016 | getActor() |
| | 2017 | { |
| | 2018 | local owner; |
| | 2019 | |
| | 2020 | /* |
| | 2021 | * if we have an owner, and it's an actor, then it's our |
| | 2022 | * associated actor; otherwise, we don't have any associated |
| | 2023 | * actor |
| | 2024 | */ |
| | 2025 | if ((owner = location.getTopicOwner()) != nil && owner.ofKind(Actor)) |
| | 2026 | return owner; |
| | 2027 | else |
| | 2028 | return nil; |
| | 2029 | } |
| | 2030 | |
| | 2031 | /* |
| | 2032 | * Determine if this topic is active. This checks the isActive |
| | 2033 | * property, and also takes into account our relationship to |
| | 2034 | * alternative entries for the topic. Generally, you should *define* |
| | 2035 | * (override) isActive, and *call* this method. |
| | 2036 | */ |
| | 2037 | checkIsActive() |
| | 2038 | { |
| | 2039 | /* |
| | 2040 | * if our isActive property indicates we're not active, we're |
| | 2041 | * definitely not active, so there's no need to check for an |
| | 2042 | * overriding alternative |
| | 2043 | */ |
| | 2044 | if (!isActive) |
| | 2045 | return nil; |
| | 2046 | |
| | 2047 | /* if we have an active nested alternative, it overrides us */ |
| | 2048 | if (altTopicList.indexWhich({x: x.isActive}) != nil) |
| | 2049 | return nil; |
| | 2050 | |
| | 2051 | /* ask our container if its topics are active */ |
| | 2052 | return location.topicGroupActive(); |
| | 2053 | } |
| | 2054 | |
| | 2055 | /* |
| | 2056 | * Check to see if any alternative in the alternative group is |
| | 2057 | * active. This returns true if we're active or if any of our nested |
| | 2058 | * AltTopics is active. |
| | 2059 | */ |
| | 2060 | anyAltIsActive() |
| | 2061 | { |
| | 2062 | /* |
| | 2063 | * if all topics within our container are inactive, then there's |
| | 2064 | * definitely no active alternative |
| | 2065 | */ |
| | 2066 | if (!location.topicGroupActive()) |
| | 2067 | return nil; |
| | 2068 | |
| | 2069 | /* |
| | 2070 | * if we're active, or any of our nested AltTopics is active, our |
| | 2071 | * alternative group is active |
| | 2072 | */ |
| | 2073 | if (isActive || altTopicList.indexWhich({x: x.isActive}) != nil) |
| | 2074 | return true; |
| | 2075 | |
| | 2076 | /* we didn't find any active alternatives in the entire group */ |
| | 2077 | return nil; |
| | 2078 | } |
| | 2079 | |
| | 2080 | /* |
| | 2081 | * Adjust my score value for any hierarchical adjustments. We'll add |
| | 2082 | * the score adjustment for each enclosing object. |
| | 2083 | */ |
| | 2084 | adjustScore(score) |
| | 2085 | { |
| | 2086 | /* the score is nil, it means there's no match, so don't adjust it */ |
| | 2087 | if (score == nil) |
| | 2088 | return score; |
| | 2089 | |
| | 2090 | /* add in the cumulative adjustment from my containers */ |
| | 2091 | return score + location.topicGroupScoreAdjustment; |
| | 2092 | } |
| | 2093 | |
| | 2094 | /* |
| | 2095 | * Check to see if we want to defer to the given topic from an |
| | 2096 | * inferior topic database. By default, we never defer to a topic |
| | 2097 | * from an inferior database: we choose a matching topic from the top |
| | 2098 | * database in the hierarchy where we find a match. |
| | 2099 | * |
| | 2100 | * The database hierarchy, for most purposes, starts with the |
| | 2101 | * ConvNode at the highest level, then the ActorState, then the |
| | 2102 | * Actor. We search those databases, in that order, and we take the |
| | 2103 | * first match we find. By default, if there's another match in a |
| | 2104 | * lower-level database, it doesn't matter what its matchScore is: we |
| | 2105 | * always pick the one from the highest-level database where we find |
| | 2106 | * a match. You can override this method to change this behavior. |
| | 2107 | * |
| | 2108 | * We don't actually define this method here, because the presence of |
| | 2109 | * the method is significant. If the method isn't defined at all, we |
| | 2110 | * won't bother looking for a possible deferral, saving the trouble |
| | 2111 | * of searching the other databases in the hierarchy. |
| | 2112 | */ |
| | 2113 | // deferToEntry(other) { return nil; } |
| | 2114 | |
| | 2115 | /* |
| | 2116 | * Our match strength score. By default, we'll use a score of 100, |
| | 2117 | * which is just an arbitrary base score. |
| | 2118 | */ |
| | 2119 | matchScore = 100 |
| | 2120 | |
| | 2121 | /* |
| | 2122 | * The set of database lists we're part of. This is a list of |
| | 2123 | * property pointers, giving the TopicDatabase properties of the |
| | 2124 | * lists we participate in. |
| | 2125 | */ |
| | 2126 | includeInList = [] |
| | 2127 | |
| | 2128 | /* |
| | 2129 | * Our response. This is displayed when we're the topic entry |
| | 2130 | * selected to handle an ASK or TELL. Each topic entry must override |
| | 2131 | * this to show our response text (or, alternatively, an entry can |
| | 2132 | * override handleTopic so that it doesn't call this property). |
| | 2133 | */ |
| | 2134 | topicResponse = "" |
| | 2135 | |
| | 2136 | /* |
| | 2137 | * The number of times this topic has invoked by the player. Each |
| | 2138 | * time the player asks/tells/etc about this topic, we'll increment |
| | 2139 | * this count. |
| | 2140 | */ |
| | 2141 | talkCount = 0 |
| | 2142 | |
| | 2143 | /* |
| | 2144 | * The number of times this topic or any nested AltTopic has been |
| | 2145 | * invoked by the player. Each time the player asks/tells/etc about |
| | 2146 | * this topic OR any of its AltTopic children, we'll increment this |
| | 2147 | * count. |
| | 2148 | */ |
| | 2149 | altTalkCount = 0 |
| | 2150 | |
| | 2151 | /* |
| | 2152 | * the owner of any AltTopic nested within me is the same as my own |
| | 2153 | * topic owner, which we take from our location |
| | 2154 | */ |
| | 2155 | getTopicOwner() |
| | 2156 | { |
| | 2157 | if (location != nil) |
| | 2158 | return location.getTopicOwner(); |
| | 2159 | else |
| | 2160 | return nil; |
| | 2161 | } |
| | 2162 | |
| | 2163 | /* |
| | 2164 | * Initialize. If we have a location property, we'll assume that the |
| | 2165 | * location is a topic database object, and we'll add ourselves to |
| | 2166 | * that database. |
| | 2167 | */ |
| | 2168 | initializeTopicEntry() |
| | 2169 | { |
| | 2170 | /* if we have a location, add ourselves to its topic database */ |
| | 2171 | if (location != nil) |
| | 2172 | location.addTopic(self); |
| | 2173 | |
| | 2174 | /* sort our list of AltTopic children */ |
| | 2175 | altTopicList = altTopicList.sort( |
| | 2176 | SortAsc, {a, b: a.altTopicOrder - b.altTopicOrder}); |
| | 2177 | } |
| | 2178 | |
| | 2179 | /* add a topic nested within us */ |
| | 2180 | addTopic(entry) |
| | 2181 | { |
| | 2182 | /* if we have a location, add the entry to its topic database */ |
| | 2183 | if (location != nil) |
| | 2184 | location.addTopic(entry); |
| | 2185 | } |
| | 2186 | |
| | 2187 | /* |
| | 2188 | * Add an AltTopic entry. This is called by our AltTopic children |
| | 2189 | * during initialization; we'll simply add the entry to our list of |
| | 2190 | * AltTopic children. |
| | 2191 | */ |
| | 2192 | addAltTopic(entry) |
| | 2193 | { |
| | 2194 | /* add the entry to our list of alternatives */ |
| | 2195 | altTopicList += entry; |
| | 2196 | } |
| | 2197 | |
| | 2198 | /* get the topic group score adjustment (for AltTopics nested within) */ |
| | 2199 | topicGroupScoreAdjustment = (location.topicGroupScoreAdjustment) |
| | 2200 | |
| | 2201 | /* check the group isActive status (for AltTopics nested within) */ |
| | 2202 | topicGroupActive = (location.topicGroupActive) |
| | 2203 | |
| | 2204 | /* our list of AltTopic children */ |
| | 2205 | altTopicList = [] |
| | 2206 | |
| | 2207 | /* |
| | 2208 | * Match the topic. This is abstract in this base class; it must be |
| | 2209 | * defined by each concrete subclass. This returns nil if there's |
| | 2210 | * no match, or an integer value if there's a match. The higher the |
| | 2211 | * number's value, the stronger the match. |
| | 2212 | * |
| | 2213 | * This is abstract in the base class because the meaning of 'topic' |
| | 2214 | * varies by subclass, according to which type of command it's used |
| | 2215 | * with. For example, in ASK and TELL commands, 'topic' is a |
| | 2216 | * ResolvedTopic describing the topic in the player's command; for |
| | 2217 | * GIVE and SHOW commands, it's the resolved simulation object. |
| | 2218 | */ |
| | 2219 | // matchTopic(fromActor, topic) { return nil; } |
| | 2220 | |
| | 2221 | /* |
| | 2222 | * Check to see if a match to this topic entry is *possible* right |
| | 2223 | * now for the given actor. For most subclasses, this is inherently |
| | 2224 | * imprecise, because the 'match' function simply isn't reversible in |
| | 2225 | * general: to know if we can be matched, we'd have to determine if |
| | 2226 | * there's a non-empty set of possible inputs that can match us. |
| | 2227 | * This method is complementary to matchTopic(), so subclasses must |
| | 2228 | * override with a corresponding implementation. |
| | 2229 | * |
| | 2230 | * 'actor' is the actor to whom we're making the suggestion. |
| | 2231 | * 'scopeList' is the list of objects that are in scope for the |
| | 2232 | * actor. |
| | 2233 | * |
| | 2234 | * The library only uses this to determine if a suggestion should be |
| | 2235 | * offered. So, specialized topic instances with non-standard match |
| | 2236 | * rules don't have to worry about this unless they're used as |
| | 2237 | * suggestions, or unless the game itself needs this information for |
| | 2238 | * some other reason. |
| | 2239 | */ |
| | 2240 | // isMatchPossible(actor, scopeList) { return true; } |
| | 2241 | |
| | 2242 | /* |
| | 2243 | * Set pronouns for the topic, if possible. If the topic corresponds |
| | 2244 | * to a game-world object, then we should set the pronoun antecedent |
| | 2245 | * to the game object. This must be handled per subclass because of |
| | 2246 | * the range of possible meanings of 'topic'. |
| | 2247 | */ |
| | 2248 | setTopicPronouns(fromActor, topic) { } |
| | 2249 | |
| | 2250 | /* |
| | 2251 | * Handle the topic. This is called when we find that this is the |
| | 2252 | * best topic entry for the current topic. |
| | 2253 | * |
| | 2254 | * By default, we'll do one of two things: |
| | 2255 | * |
| | 2256 | * - If 'self' inherits from Script, then we'll simply invoke our |
| | 2257 | * doScript() method. This makes it especially easy to set up a |
| | 2258 | * topic entry that shows a series of responses: just add EventList |
| | 2259 | * or one of its subclasses to the base class list when defining the |
| | 2260 | * topic, and define the eventList property as a list of string |
| | 2261 | * responses. For example: |
| | 2262 | * |
| | 2263 | *. + TopicEntry, StopEventList @blackBook |
| | 2264 | *. ['<q>What makes you think I know anything about it?</q> |
| | 2265 | *. he says, his voice shaking. ', |
| | 2266 | *. '<q>No! You can\'t make me tell you!</q> he wails. ', |
| | 2267 | *. '<q>All right, fine! I\'ll tell you, but I warn you, |
| | 2268 | *. this is knowledge mortal men were never meant to know.</q> ', |
| | 2269 | *. // and so on |
| | 2270 | *. ] |
| | 2271 | *. ; |
| | 2272 | * |
| | 2273 | * - Otherwise, we'll call our topicResponse property, which should |
| | 2274 | * simply be a double-quoted string to display. This is the simplest |
| | 2275 | * way to define a topic with just one response. |
| | 2276 | * |
| | 2277 | * Note that 'topic' will vary by subclass, depending on the type of |
| | 2278 | * command used with the topic type. For example, for ASK and TELL |
| | 2279 | * commands, 'topic' is a ResolvedTopic object; for GIVE and SHOW, |
| | 2280 | * it's a simulation object (i.e., generally a Thing subclass). |
| | 2281 | */ |
| | 2282 | handleTopic(fromActor, topic) |
| | 2283 | { |
| | 2284 | /* note the invocation */ |
| | 2285 | noteInvocation(fromActor); |
| | 2286 | |
| | 2287 | /* set pronoun antecedents if possible */ |
| | 2288 | setTopicPronouns(fromActor, topic); |
| | 2289 | |
| | 2290 | /* check to see if we're a Script */ |
| | 2291 | if (ofKind(Script)) |
| | 2292 | { |
| | 2293 | /* we're a Script - invoke our script */ |
| | 2294 | doScript(); |
| | 2295 | } |
| | 2296 | else |
| | 2297 | { |
| | 2298 | /* show our simple response string */ |
| | 2299 | topicResponse; |
| | 2300 | } |
| | 2301 | } |
| | 2302 | |
| | 2303 | /* note that we've been invoked */ |
| | 2304 | noteInvocation(fromActor) |
| | 2305 | { |
| | 2306 | /* |
| | 2307 | * we count as one of the alternatives in our alternative group, |
| | 2308 | * so note the invocation of the group |
| | 2309 | */ |
| | 2310 | noteAltInvocation(fromActor, self); |
| | 2311 | |
| | 2312 | /* count the invocation */ |
| | 2313 | ++talkCount; |
| | 2314 | } |
| | 2315 | |
| | 2316 | /* |
| | 2317 | * Note that something in our entire alternative group has been |
| | 2318 | * invoked. We count as a member of our own group, so this is |
| | 2319 | * invoked when we're invoked; this is also invoked when any AltTopic |
| | 2320 | * child of ours is invoked. |
| | 2321 | */ |
| | 2322 | noteAltInvocation(fromActor, alt) |
| | 2323 | { |
| | 2324 | local owner; |
| | 2325 | |
| | 2326 | /* notify our owner of the topic invocation */ |
| | 2327 | if ((owner = location.getTopicOwner()) != nil) |
| | 2328 | owner.notifyTopicResponse(fromActor, alt); |
| | 2329 | |
| | 2330 | /* count the alternative invocation */ |
| | 2331 | ++altTalkCount; |
| | 2332 | } |
| | 2333 | |
| | 2334 | /* |
| | 2335 | * Add a suggested topic. A suggested topic can be nested within a |
| | 2336 | * topic entry; doing this associates the suggested topic with the |
| | 2337 | * topic entry, and automatically associates the suggested topic |
| | 2338 | * with the entry's actor or actor state. |
| | 2339 | */ |
| | 2340 | addSuggestedTopic(t) |
| | 2341 | { |
| | 2342 | /* |
| | 2343 | * If the SuggestedTopic is *directly* within us, we're the |
| | 2344 | * SuggestedTopic object's associated TopicEntry. The nesting |
| | 2345 | * could be deeper, if we have alternative topics nested within |
| | 2346 | * us; in these cases, we're not directly associated with the |
| | 2347 | * suggested topic. |
| | 2348 | */ |
| | 2349 | if (t.location == self) |
| | 2350 | t.associatedTopic = self; |
| | 2351 | |
| | 2352 | /* add the suggestion to our location's topic database */ |
| | 2353 | if (location != nil) |
| | 2354 | location.addSuggestedTopic(t); |
| | 2355 | } |
| | 2356 | ; |
| | 2357 | |
| | 2358 | /* |
| | 2359 | * A TopicGroup is an abstract container for a set of TopicEntry objects. |
| | 2360 | * The purpose of the group object is to apply a common "is active" |
| | 2361 | * condition to all of the topics within the group. |
| | 2362 | * |
| | 2363 | * The isActive condition of the TopicGroup is effectively AND'ed with |
| | 2364 | * any other conditions on the nested TopicEntry's. In other words, a |
| | 2365 | * TopicEntry within the TopicGroup is active if the TopicEntry would |
| | 2366 | * otherwise be acive AND the TopicGroup is active. |
| | 2367 | * |
| | 2368 | * TopicEntry objects are associated with the group via the 'location' |
| | 2369 | * property - set the location of the TopicEntry to point to the |
| | 2370 | * containing TopicGroup. |
| | 2371 | * |
| | 2372 | * You can put a TopicGroup anywhere a TopicEntry could go - directly |
| | 2373 | * inside an Actor, inside an ActorState, or within another TopicGroup. |
| | 2374 | * The topic entries within a topic group act as though they were |
| | 2375 | * directly in the topic group's container. |
| | 2376 | */ |
| | 2377 | class TopicGroup: object |
| | 2378 | /* |
| | 2379 | * The group "active" condition - each instance should override this |
| | 2380 | * to specify the condition that applies to all of the TopicEntry |
| | 2381 | * objects within the group. |
| | 2382 | */ |
| | 2383 | isActive = true |
| | 2384 | |
| | 2385 | /* |
| | 2386 | * The *adjustment* to the match score for topic entries contained |
| | 2387 | * within this group. This is usually a positive number, so that it |
| | 2388 | * boosts the match strength of the child topics. |
| | 2389 | */ |
| | 2390 | matchScoreAdjustment = 0 |
| | 2391 | |
| | 2392 | /* |
| | 2393 | * the topic owner for any topic entries within the group is the |
| | 2394 | * topic owner taken from the group's own location |
| | 2395 | */ |
| | 2396 | getTopicOwner() { return location.getTopicOwner(); } |
| | 2397 | |
| | 2398 | /* are TopicEntry objects within the group active? */ |
| | 2399 | topicGroupActive() |
| | 2400 | { |
| | 2401 | /* |
| | 2402 | * our TopicEntry objects are active if the group condition is |
| | 2403 | * true and our container's contents are active |
| | 2404 | */ |
| | 2405 | return isActive && location.topicGroupActive(); |
| | 2406 | } |
| | 2407 | |
| | 2408 | /* |
| | 2409 | * Get my score adjustment. We'll return our own basic score |
| | 2410 | * adjustment plus the cumulative adjustment for our containers. |
| | 2411 | */ |
| | 2412 | topicGroupScoreAdjustment = (matchScoreAdjustment |
| | 2413 | + location.topicGroupScoreAdjustment) |
| | 2414 | |
| | 2415 | /* add a topic - we'll simply add the topic directly to our container */ |
| | 2416 | addTopic(topic) { location.addTopic(topic); } |
| | 2417 | |
| | 2418 | /* add a suggested topic - we'll pass this up to our container */ |
| | 2419 | addSuggestedTopic(topic) { location.addSuggestedTopic(topic); } |
| | 2420 | ; |
| | 2421 | |
| | 2422 | /* |
| | 2423 | * An alternative topic entry. This makes it easy to define different |
| | 2424 | * responses to a topic according to the game state; for example, we |
| | 2425 | * might want to provide a different response for a topic after some |
| | 2426 | * event has occurred, so that we can reflect knowledge of the event in |
| | 2427 | * the response. |
| | 2428 | * |
| | 2429 | * A set of alternative topics is sort of like an inverted if-then-else. |
| | 2430 | * You start by defining a normal TopicEntry (an AskTopic, or an |
| | 2431 | * AskTellTopic, or whatever) for the basic response. Then, you add a |
| | 2432 | * nested AltTopic located within the base topic; you can add another |
| | 2433 | * AltTopic nested within the base topic, and another after that, and so |
| | 2434 | * on. When we need to choose one of the topics, we'll choose the last |
| | 2435 | * one that indicates it's active. So, the order of appearance is |
| | 2436 | * essentially an override order: the first AltTopic overrides its parent |
| | 2437 | * TopicEntry, and each subsequent AltTopic overrides its previous |
| | 2438 | * AltTopic. |
| | 2439 | * |
| | 2440 | * + AskTellTopic @lighthouse "It's very tall."; |
| | 2441 | *. ++ AltTopic "Not really..." isActive=(...); |
| | 2442 | *. ++ AltTopic "Well, maybe..." isActive=(...); |
| | 2443 | *. ++ AltTopic "One more thing..." isActive=(...); |
| | 2444 | * |
| | 2445 | * In this example, the response we'll show for ASK ABOUT LIGHTHOUSE will |
| | 2446 | * always be the LAST entry of the group that's active. For example, if |
| | 2447 | * all of the responses are active except for the very last one, then |
| | 2448 | * we'll show the "Well, maybe" response, because it's the last active |
| | 2449 | * response. If the main AskTellTopic is active, but none of the |
| | 2450 | * AltTopics are active, we'll show the "It's very tall" main response, |
| | 2451 | * because it's the last element of the group that's active. |
| | 2452 | * |
| | 2453 | * Note that an AltTopic takes its matching information from its parent, |
| | 2454 | * so you don't need to specify a matchObj or any other matching |
| | 2455 | * information in an AltTopic. You merely need to provide the response |
| | 2456 | * text and the isActive test. |
| | 2457 | */ |
| | 2458 | class AltTopic: TopicEntry |
| | 2459 | /* we match if our parent matches, and with the same score */ |
| | 2460 | matchTopic(fromActor, topic) |
| | 2461 | { return location.matchTopic(fromActor, topic); } |
| | 2462 | |
| | 2463 | /* we can match if our parent can match */ |
| | 2464 | isMatchPossible(actor, scopeList) |
| | 2465 | { return location.isMatchPossible(actor, scopeList); } |
| | 2466 | |
| | 2467 | /* we can match a pre-parse string if our parent can */ |
| | 2468 | matchPreParse(str, pstr) { return location.matchPreParse(str, pstr); } |
| | 2469 | |
| | 2470 | /* set pronouns for the topic */ |
| | 2471 | setTopicPronouns(fromActor, topic) |
| | 2472 | { location.setTopicPronouns(fromActor, topic); } |
| | 2473 | |
| | 2474 | /* include in the same lists as our parent */ |
| | 2475 | includeInList = (location.includeInList) |
| | 2476 | |
| | 2477 | /* AltTopic initialization */ |
| | 2478 | initializeAltTopic() |
| | 2479 | { |
| | 2480 | /* add myself to our parent's child list */ |
| | 2481 | if (location != nil) |
| | 2482 | location.addAltTopic(self); |
| | 2483 | } |
| | 2484 | |
| | 2485 | /* |
| | 2486 | * Determine if this topic is active. An AltTopic is active if its |
| | 2487 | * own isActive indicates true, AND none of its subsequent siblings |
| | 2488 | * are active. |
| | 2489 | */ |
| | 2490 | checkIsActive() |
| | 2491 | { |
| | 2492 | /* we can't be active if our own isActive says we're not */ |
| | 2493 | if (!isActive) |
| | 2494 | return nil; |
| | 2495 | |
| | 2496 | /* |
| | 2497 | * Check for any active element after us in the parent's list. |
| | 2498 | * To do this, scan from the end of the parent list backwards, |
| | 2499 | * and look for an element that's active. If we reach our own |
| | 2500 | * entry, then we'll know that there are no active entries |
| | 2501 | * following us in the list. Note that we already know we're |
| | 2502 | * active, or we wouldn't have gotten this far, so we can simply |
| | 2503 | * look for the rightmost active element in the list. |
| | 2504 | */ |
| | 2505 | if (location != nil |
| | 2506 | && location.altTopicList.lastValWhich({x: x.isActive}) != self) |
| | 2507 | { |
| | 2508 | /* |
| | 2509 | * we found an active element after ourself, so it overrides |
| | 2510 | * us - we're therefore not active |
| | 2511 | */ |
| | 2512 | return nil; |
| | 2513 | } |
| | 2514 | |
| | 2515 | /* ask our container if its topics are active */ |
| | 2516 | return location.topicGroupActive(); |
| | 2517 | } |
| | 2518 | |
| | 2519 | /* take our implied-greeting status from our parent */ |
| | 2520 | impliesGreeting = (location.impliesGreeting) |
| | 2521 | |
| | 2522 | /* take our conversational status from our parent */ |
| | 2523 | isConversational = (location.isConversational) |
| | 2524 | |
| | 2525 | /* |
| | 2526 | * Our relative order within our parent's list of alternatives. By |
| | 2527 | * default, we simply return the source file ordering, which ensures |
| | 2528 | * that static AltTopic objects (i.e., those defined directly in |
| | 2529 | * source files, not dynamically created with 'new') will be ordered |
| | 2530 | * just as they're laid out in the source file. |
| | 2531 | */ |
| | 2532 | altTopicOrder = (sourceTextOrder) |
| | 2533 | |
| | 2534 | /* note invocation */ |
| | 2535 | noteInvocation(fromActor) |
| | 2536 | { |
| | 2537 | /* count our own invocation */ |
| | 2538 | ++talkCount; |
| | 2539 | |
| | 2540 | /* let our container know its AltTopic child is being invoked */ |
| | 2541 | if (location != nil) |
| | 2542 | location.noteAltInvocation(fromActor, self); |
| | 2543 | } |
| | 2544 | |
| | 2545 | /* our AltTopic counter is the AltTopic counter for the enclosing topic */ |
| | 2546 | altTalkCount = (location != nil ? location.altTalkCount : talkCount) |
| | 2547 | ; |
| | 2548 | |
| | 2549 | /* |
| | 2550 | * A "topic match" topic entry. This is a topic entry that matches topic |
| | 2551 | * phrases in the grammar. |
| | 2552 | * |
| | 2553 | * Handling topic phrases is a bit tricky, because they can't be resolved |
| | 2554 | * to definitive game-world objects the way ordinary noun phrases can. |
| | 2555 | * Topic phrases can refer to things that aren't physically present, but |
| | 2556 | * which are known to the actor performing the command; they can refer to |
| | 2557 | * abstract Topic objects, that have no physical existence in the game |
| | 2558 | * world at all; and they can ever be arbitrary text that doesn't match |
| | 2559 | * any vocabulary defined by the game. |
| | 2560 | * |
| | 2561 | * Our strategy in matching topics is to first narrow the list down to |
| | 2562 | * the physical and abstract game objects that both match the vocabulary |
| | 2563 | * used in the command and are part of the memory of the actor performing |
| | 2564 | * the command. That much is handled by the normal topic phrase |
| | 2565 | * resolution rules, and gives us a list of possible matches. Then, |
| | 2566 | * given this narrowed list of possibilities, we look through the list of |
| | 2567 | * objects that we're associated with; we effectively intersect the two |
| | 2568 | * lists, and if the result is non-empty, we consider it a match. |
| | 2569 | * Finally, we also consider any regular expression that we're associated |
| | 2570 | * with; if we have one, and the topic phrase text in the command matches |
| | 2571 | * the input, we'll consider it a match. |
| | 2572 | */ |
| | 2573 | class TopicMatchTopic: TopicEntry |
| | 2574 | /* |
| | 2575 | * A regular expression pattern that we'll match to the actual topic |
| | 2576 | * text as entered in the command. If 'matchExactCase' is true, |
| | 2577 | * we'll match the exact text in its original upper/lower case |
| | 2578 | * rendering; otherwise, we'll convert the player input to lower-case |
| | 2579 | * before matching it against the pattern. In most cases, we'll want |
| | 2580 | * to match the input no matter what combination of upper and lower |
| | 2581 | * case the player entered, so matchExactCase is nil by default. |
| | 2582 | * |
| | 2583 | * Note that both the object (or object list) and the regular |
| | 2584 | * expression pattern can be included for a single topic entry |
| | 2585 | * object. This allows a topic entry to match several different ways |
| | 2586 | * of entering the topic name, or to match several different topics |
| | 2587 | * with the same response. |
| | 2588 | */ |
| | 2589 | matchPattern = nil |
| | 2590 | matchExactCase = nil |
| | 2591 | |
| | 2592 | /* |
| | 2593 | * Match the topic. By default, we'll match to either the simulation |
| | 2594 | * object or objects in matchObj, or the pattern in matchPattern. |
| | 2595 | * Note that we always try both ways of matching, so a single |
| | 2596 | * AskTellTopic can define both a pattern and an object list. |
| | 2597 | * |
| | 2598 | * Subclasses can override this as desired to use other ways of |
| | 2599 | * matching. |
| | 2600 | */ |
| | 2601 | matchTopic(fromActor, topic) |
| | 2602 | { |
| | 2603 | /* |
| | 2604 | * if we have one or more match objects, try matching to the |
| | 2605 | * topic's best simulation object match |
| | 2606 | */ |
| | 2607 | if (matchObj != nil) |
| | 2608 | { |
| | 2609 | /* |
| | 2610 | * we have a match object or match object list - if it's a |
| | 2611 | * collection, check each element, otherwise just match the |
| | 2612 | * single object |
| | 2613 | */ |
| | 2614 | if (matchObj.ofKind(Collection)) |
| | 2615 | { |
| | 2616 | /* try matching each object in the list */ |
| | 2617 | if (matchObj.indexWhich({x: findMatchObj(x, topic)}) != nil) |
| | 2618 | return matchScore; |
| | 2619 | } |
| | 2620 | else |
| | 2621 | { |
| | 2622 | /* match the single object */ |
| | 2623 | if (findMatchObj(matchObj, topic)) |
| | 2624 | return matchScore; |
| | 2625 | } |
| | 2626 | } |
| | 2627 | |
| | 2628 | /* |
| | 2629 | * check for a match to the regular expression pattern, if we |
| | 2630 | * have a pattern AND the resolved topic allows literal matches |
| | 2631 | */ |
| | 2632 | if (matchPattern != nil && topic.canMatchLiterally()) |
| | 2633 | { |
| | 2634 | local txt; |
| | 2635 | |
| | 2636 | /* |
| | 2637 | * There's no match object; try matching our regular |
| | 2638 | * expression to the actual topic text. Get the actual text. |
| | 2639 | */ |
| | 2640 | txt = topic.getTopicText(); |
| | 2641 | |
| | 2642 | /* |
| | 2643 | * if they don't want an exact case match, convert the |
| | 2644 | * original topic text to lower case |
| | 2645 | */ |
| | 2646 | if (!matchExactCase) |
| | 2647 | txt = txt.toLower(); |
| | 2648 | |
| | 2649 | /* if the regular expression matches, we match */ |
| | 2650 | if (rexMatch(matchPattern, txt) != nil) |
| | 2651 | return matchScore; |
| | 2652 | } |
| | 2653 | |
| | 2654 | /* we didn't find a match - indicate this with a nil score */ |
| | 2655 | return nil; |
| | 2656 | } |
| | 2657 | |
| | 2658 | /* |
| | 2659 | * Match an individual item from our match list to the given |
| | 2660 | * ResolvedTopic object. We'll check each object in the resolved |
| | 2661 | * topic's "in scope" and "likely" lists. |
| | 2662 | */ |
| | 2663 | findMatchObj(obj, rt) |
| | 2664 | { |
| | 2665 | /* check the "in scope" list */ |
| | 2666 | if (rt.inScopeList.indexOf(obj) != nil) |
| | 2667 | return true; |
| | 2668 | |
| | 2669 | /* check the "likely" list */ |
| | 2670 | return (rt.likelyList.indexOf(obj) != nil); |
| | 2671 | } |
| | 2672 | |
| | 2673 | /* |
| | 2674 | * It's possible for us to match if any of our matchObj objects are |
| | 2675 | * known to the actor. If we have no matchObj objects, we must be |
| | 2676 | * matching on a regular expression or on a custom condition, so we |
| | 2677 | * can't speculate on matchability; we'll simply return true in those |
| | 2678 | * cases. |
| | 2679 | */ |
| | 2680 | isMatchPossible(actor, scopeList) |
| | 2681 | { |
| | 2682 | /* check what we have in our matchObj */ |
| | 2683 | if (matchObj == nil) |
| | 2684 | { |
| | 2685 | /* |
| | 2686 | * we have no match object, so we must match on a regular |
| | 2687 | * expression or a custom condition; we can't speculate on |
| | 2688 | * our matchability, so just return true as a default |
| | 2689 | */ |
| | 2690 | return true; |
| | 2691 | } |
| | 2692 | else if (matchObj.ofKind(Collection)) |
| | 2693 | { |
| | 2694 | /* |
| | 2695 | * we have a list of match objects - return true if any of |
| | 2696 | * them are known or are currently in scope |
| | 2697 | */ |
| | 2698 | return (matchObj.indexWhich( |
| | 2699 | {x: actor.knowsAbout(x) || scopeList.indexOf(x)}) != nil); |
| | 2700 | } |
| | 2701 | else |
| | 2702 | { |
| | 2703 | /* |
| | 2704 | * we have a single match object - return true if it's known |
| | 2705 | * or it's in scope |
| | 2706 | */ |
| | 2707 | return (actor.knowsAbout(matchObj) |
| | 2708 | || scopeList.indexOf(matchObj) != nil); |
| | 2709 | } |
| | 2710 | } |
| | 2711 | |
| | 2712 | /* set the topic pronouns */ |
| | 2713 | setTopicPronouns(fromActor, topic) |
| | 2714 | { |
| | 2715 | /* check to see what kind of match object we have */ |
| | 2716 | if (matchObj == nil) |
| | 2717 | { |
| | 2718 | /* |
| | 2719 | * no match object, so we must match a regular expression |
| | 2720 | * pattern; this gives us no clue what game object we might |
| | 2721 | * match, so there's nothing we can do here |
| | 2722 | */ |
| | 2723 | } |
| | 2724 | else if (matchObj.ofKind(Collection)) |
| | 2725 | { |
| | 2726 | local lst; |
| | 2727 | |
| | 2728 | /* |
| | 2729 | * We match a list of objects. Get the subset of the |
| | 2730 | * in-scope list from the topic that we match. Consider only |
| | 2731 | * the in-scope items for now, and consider only game-world |
| | 2732 | * objects (Things). |
| | 2733 | */ |
| | 2734 | lst = matchObj.subset( |
| | 2735 | {x: x.ofKind(Thing) && topic.inScopeList.indexOf(x) != nil}); |
| | 2736 | |
| | 2737 | /* if that didn't turn up anything, consider the likelies, too */ |
| | 2738 | if (lst.length() == 0) |
| | 2739 | lst = matchObj.subset( |
| | 2740 | {x: (x.ofKind(Thing) |
| | 2741 | && topic.likelyList.indexOf(x) != nil)}); |
| | 2742 | |
| | 2743 | /* |
| | 2744 | * if that narrows it down to one match, make it the pronoun |
| | 2745 | * antecedent |
| | 2746 | */ |
| | 2747 | if (lst.length() == 1) |
| | 2748 | fromActor.setPronounObj(lst[1]); |
| | 2749 | } |
| | 2750 | else |
| | 2751 | { |
| | 2752 | /* |
| | 2753 | * we match a single object; if it's a game-world object (a |
| | 2754 | * Thing), use it as the pronoun antecedent |
| | 2755 | */ |
| | 2756 | if (matchObj.ofKind(Thing)) |
| | 2757 | fromActor.setPronounObj(matchObj); |
| | 2758 | } |
| | 2759 | } |
| | 2760 | ; |
| | 2761 | |
| | 2762 | /* |
| | 2763 | * A dual ASK/TELL topic database entry. This type of topic is included |
| | 2764 | * in both the ASK ABOUT and TELL ABOUT lists. |
| | 2765 | * |
| | 2766 | * Many authors have chosen to treat ASK and TELL as equivalent, or at |
| | 2767 | * least, equivalent for most topics. Since these verbs only very weakly |
| | 2768 | * suggest what the player character is actually saying, it's frequently |
| | 2769 | * the case that a given topic response makes just as much sense coming |
| | 2770 | * from TELL as from ASK, or vice versa. In these cases, it's best to |
| | 2771 | * enter the topic under both ASK and TELL; which one the player tries |
| | 2772 | * might simply depend on the player's frame of mind, and they might feel |
| | 2773 | * cheated if one works and the other doesn't in cases where both are |
| | 2774 | * equally valid. |
| | 2775 | */ |
| | 2776 | class AskTellTopic: TopicMatchTopic |
| | 2777 | /* include me in both the ASK and TELL lists */ |
| | 2778 | includeInList = [&askTopics, &tellTopics] |
| | 2779 | ; |
| | 2780 | |
| | 2781 | /* |
| | 2782 | * An ASK ABOUT topic database entry. This type of topic is included in |
| | 2783 | * the ASK ABOUT list only. |
| | 2784 | */ |
| | 2785 | class AskTopic: AskTellTopic |
| | 2786 | includeInList = [&askTopics] |
| | 2787 | ; |
| | 2788 | |
| | 2789 | /* |
| | 2790 | * A TELL ABOUT topic database entry. This type of topic entry is |
| | 2791 | * included in the TELL ABOUT list only. |
| | 2792 | */ |
| | 2793 | class TellTopic: AskTellTopic |
| | 2794 | includeInList = [&tellTopics] |
| | 2795 | ; |
| | 2796 | |
| | 2797 | /* |
| | 2798 | * An ASK FOR topic database entry. This type of topic entry is |
| | 2799 | * included in the ASK FOR list only. |
| | 2800 | */ |
| | 2801 | class AskForTopic: AskTellTopic |
| | 2802 | includeInList = [&askForTopics] |
| | 2803 | ; |
| | 2804 | |
| | 2805 | /* |
| | 2806 | * A combination ASK ABOUT and ASK FOR topic. |
| | 2807 | */ |
| | 2808 | class AskAboutForTopic: AskTellTopic |
| | 2809 | includeInList = [&askTopics, &askForTopics] |
| | 2810 | ; |
| | 2811 | |
| | 2812 | /* |
| | 2813 | * A combination ASK ABOUT, TELL ABOUT, and ASK FOR topic. |
| | 2814 | */ |
| | 2815 | class AskTellAboutForTopic: AskTellTopic |
| | 2816 | includeInList = [&askTopics, &tellTopics, &askForTopics] |
| | 2817 | ; |
| | 2818 | |
| | 2819 | |
| | 2820 | /* |
| | 2821 | * A base class for topic entries that match simple simulation objects. |
| | 2822 | */ |
| | 2823 | class ThingMatchTopic: TopicEntry |
| | 2824 | /* |
| | 2825 | * Match the topic. We'll match the simulation object in 'obj' to |
| | 2826 | * our matchObj object or list. |
| | 2827 | */ |
| | 2828 | matchTopic(fromActor, obj) |
| | 2829 | { |
| | 2830 | /* |
| | 2831 | * if matchObj is a collection, check each element, otherwise |
| | 2832 | * just match the single object |
| | 2833 | */ |
| | 2834 | if (matchObj.ofKind(Collection)) |
| | 2835 | { |
| | 2836 | /* try matching each object in the list */ |
| | 2837 | if (matchObj.indexOf(obj) != nil) |
| | 2838 | return matchScore; |
| | 2839 | } |
| | 2840 | else |
| | 2841 | { |
| | 2842 | /* match the single object */ |
| | 2843 | if (matchObj == obj) |
| | 2844 | return matchScore; |
| | 2845 | } |
| | 2846 | |
| | 2847 | /* didn't find a match - indicate this by returning a nil score */ |
| | 2848 | return nil; |
| | 2849 | } |
| | 2850 | |
| | 2851 | /* |
| | 2852 | * It's possible for us to match if any of our matchObj objects are |
| | 2853 | * in scope. |
| | 2854 | */ |
| | 2855 | isMatchPossible(actor, scopeList) |
| | 2856 | { |
| | 2857 | /* check to see what kind of match object we have */ |
| | 2858 | if (matchObj.ofKind(Collection)) |
| | 2859 | { |
| | 2860 | /* we can match if any of our match objects are in scope */ |
| | 2861 | return (matchObj.indexWhich({x: scopeList.indexOf(x)}) != nil); |
| | 2862 | } |
| | 2863 | else |
| | 2864 | { |
| | 2865 | /* we can match if our single match object is in scope */ |
| | 2866 | return scopeList.indexOf(matchObj); |
| | 2867 | } |
| | 2868 | } |
| | 2869 | |
| | 2870 | /* set the topic pronouns */ |
| | 2871 | setTopicPronouns(fromActor, topic) |
| | 2872 | { |
| | 2873 | /* |
| | 2874 | * the 'topic' is just an ordinary game object; as long as it's a |
| | 2875 | * Thing, set it as the antecedent |
| | 2876 | */ |
| | 2877 | if (topic.ofKind(Thing)) |
| | 2878 | fromActor.setPronounObj(topic); |
| | 2879 | } |
| | 2880 | ; |
| | 2881 | |
| | 2882 | /* |
| | 2883 | * A GIVE/SHOW topic database entry. |
| | 2884 | * |
| | 2885 | * Note that this base class is usable for any command that refers to a |
| | 2886 | * simulation object. It's NOT suitable for ASK/TELL lists, or for other |
| | 2887 | * commands that refer to topics, since we expect our 'topic' to be a |
| | 2888 | * resolved simulation object. |
| | 2889 | */ |
| | 2890 | class GiveShowTopic: ThingMatchTopic |
| | 2891 | /* include me in both the GIVE and SHOW lists */ |
| | 2892 | includeInList = [&giveTopics, &showTopics] |
| | 2893 | ; |
| | 2894 | |
| | 2895 | /* |
| | 2896 | * A GIVE TO topic database entry. This type of topic entry is included |
| | 2897 | * in the GIVE TO list only. |
| | 2898 | */ |
| | 2899 | class GiveTopic: GiveShowTopic |
| | 2900 | includeInList = [&giveTopics] |
| | 2901 | ; |
| | 2902 | |
| | 2903 | /* |
| | 2904 | * A SHOW TO topic database entry. This type of topic entry is included |
| | 2905 | * in the SHOW TO list only. |
| | 2906 | */ |
| | 2907 | class ShowTopic: GiveShowTopic |
| | 2908 | includeInList = [&showTopics] |
| | 2909 | ; |
| | 2910 | |
| | 2911 | /* |
| | 2912 | * A TopicEntry that can match a Thing or a Topic. This can be used to |
| | 2913 | * combine ASK/TELL-type responses and GIVE/SHOW-type responses in a |
| | 2914 | * single topic entry. |
| | 2915 | * |
| | 2916 | * When this kind of topic is used as a suggested topic, note that you |
| | 2917 | * should name the suggestion according to the least restrictive verb. |
| | 2918 | * This is important because the suggestion will be active if any of the |
| | 2919 | * verbs would allow it; to ensure that we suggest a verb that will |
| | 2920 | * actually work, we should thus use the least restrictive verb. In |
| | 2921 | * practice, this means you should use ASK or TELL as the suggestion |
| | 2922 | * name, because an object merely has to be known to be used as a topic; |
| | 2923 | * it might be possible to ASK/TELL about an object but not GIVE/SHOW the |
| | 2924 | * object, because the object is known but not currently in scope. |
| | 2925 | */ |
| | 2926 | class TopicOrThingMatchTopic: ThingMatchTopic, TopicMatchTopic |
| | 2927 | matchTopic(fromActor, obj) |
| | 2928 | { |
| | 2929 | /* |
| | 2930 | * if we're being asked to match a ResolvedTopic, use the |
| | 2931 | * inherited TopicMatchTopic handling; otherwise, use the |
| | 2932 | * inherited ThingMatchTopic handling |
| | 2933 | */ |
| | 2934 | if (obj.ofKind(ResolvedTopic)) |
| | 2935 | return inherited TopicMatchTopic(fromActor, obj); |
| | 2936 | else |
| | 2937 | return inherited ThingMatchTopic(fromActor, obj); |
| | 2938 | } |
| | 2939 | |
| | 2940 | isMatchPossible(actor, scopeList) |
| | 2941 | { |
| | 2942 | /* if a match is possible from either subclass, allow it */ |
| | 2943 | return (inherited TopicMatchTopic(actor, scopeList) |
| | 2944 | || inherited ThingMatchTopic(actor, scopeList)); |
| | 2945 | } |
| | 2946 | |
| | 2947 | setTopicPronouns(fromActor, obj) |
| | 2948 | { |
| | 2949 | /* |
| | 2950 | * if the object is a ResolvedTopic, use the inherited |
| | 2951 | * TopicMatchTopic handling, otherwise use the ThingMatchTopic |
| | 2952 | * handling |
| | 2953 | */ |
| | 2954 | if (obj.ofKind(ResolvedTopic)) |
| | 2955 | return inherited TopicMatchTopic(fromActor, obj); |
| | 2956 | else |
| | 2957 | return inherited ThingMatchTopic(fromActor, obj); |
| | 2958 | } |
| | 2959 | ; |
| | 2960 | |
| | 2961 | /* |
| | 2962 | * A combined ASK/TELL/SHOW topic. Players will sometimes want to point |
| | 2963 | * something out when it's visible, rather than asking about it; this |
| | 2964 | * allows SHOW TO to be used as a synonym for ASK ABOUT for these cases. |
| | 2965 | */ |
| | 2966 | class AskTellShowTopic: TopicOrThingMatchTopic |
| | 2967 | includeInList = [&askTopics, &tellTopics, &showTopics] |
| | 2968 | ; |
| | 2969 | |
| | 2970 | /* |
| | 2971 | * A combined ASK/TELL/GIVE/SHOW topic. |
| | 2972 | */ |
| | 2973 | class AskTellGiveShowTopic: TopicOrThingMatchTopic |
| | 2974 | includeInList = [&askTopics, &tellTopics, &giveTopics, &showTopics] |
| | 2975 | ; |
| | 2976 | |
| | 2977 | /* |
| | 2978 | * A command topic. This is used to respond to orders given to an NPC, |
| | 2979 | * as in "BOB, GO EAST." The match object for this kind of topic entry |
| | 2980 | * is an Action class; for example, to create a response to "BOB, LOOK", |
| | 2981 | * we'd create a CommandTopic that matches LookAction. |
| | 2982 | * |
| | 2983 | * If you're designing a CommandTopic for a command can be accepted from |
| | 2984 | * a remote location, such as by telephone, you should be aware that the |
| | 2985 | * command will be running in the NPC's visual sense context. This means |
| | 2986 | * that if the player character can't see the NPC, the topic result |
| | 2987 | * message will be hidden - the NPC's visual sense context hides all |
| | 2988 | * messages generated while it's in effect if the PC can't see the NPC. |
| | 2989 | * This is usually desirable, since most messages relay visual |
| | 2990 | * information that wouldn't be visible to the player character if the PC |
| | 2991 | * can't see the subject of the message. However, if you've specifically |
| | 2992 | * designed your CommandTopic to work remotely, this isn't at all what |
| | 2993 | * you want, since you've already taken the remoteness into account in |
| | 2994 | * the message and thus want the message to be displayed after all. The |
| | 2995 | * way to handle this is to wrap the message in a callWithSenseContext() |
| | 2996 | * with a nil sense context. For example: |
| | 2997 | * |
| | 2998 | * topicResponse() |
| | 2999 | *. { callWithSenseContext(nil, nil, {: "Here's my message!" }); } |
| | 3000 | */ |
| | 3001 | class CommandTopic: TopicEntry |
| | 3002 | /* we go in the command topics list */ |
| | 3003 | includeInList = [&commandTopics] |
| | 3004 | |
| | 3005 | /* match the topic */ |
| | 3006 | matchTopic(fromActor, obj) |
| | 3007 | { |
| | 3008 | /* |
| | 3009 | * Check the collection or the single object, as needed. Note |
| | 3010 | * that our match object is an Action base class, so we must |
| | 3011 | * match if 'obj' is of the match object class. |
| | 3012 | */ |
| | 3013 | if (matchObj.ofKind(Collection)) |
| | 3014 | { |
| | 3015 | /* check each entry for a match */ |
| | 3016 | if (matchObj.indexWhich({x: obj.ofKind(x)}) != nil) |
| | 3017 | return matchScore; |
| | 3018 | } |
| | 3019 | else |
| | 3020 | { |
| | 3021 | /* check our single object */ |
| | 3022 | if (obj.ofKind(matchObj)) |
| | 3023 | return matchScore; |
| | 3024 | } |
| | 3025 | |
| | 3026 | /* didn't find a match */ |
| | 3027 | return nil; |
| | 3028 | } |
| | 3029 | |
| | 3030 | /* |
| | 3031 | * we can always match, since the player can always type in any |
| | 3032 | * possible action |
| | 3033 | */ |
| | 3034 | isMatchPossible(actor, scopeList) { return true; } |
| | 3035 | |
| | 3036 | /* we have no pronouns to set */ |
| | 3037 | setTopicPronouns(fromActor, topic) { } |
| | 3038 | ; |
| | 3039 | |
| | 3040 | /* |
| | 3041 | * A base class for simple miscellaneous topics. These handle things |
| | 3042 | * like YES, NO, HELLO, and GOODBYE, where the topic is entirely |
| | 3043 | * contained in the verb, and there's no separate noun phrase needed to |
| | 3044 | * indicate the topic. |
| | 3045 | */ |
| | 3046 | class MiscTopic: TopicEntry |
| | 3047 | matchTopic(fromActor, obj) |
| | 3048 | { |
| | 3049 | /* |
| | 3050 | * if it's one of our matching topics, return our match score, |
| | 3051 | * otherwise return a nil score to indicate failure |
| | 3052 | */ |
| | 3053 | return (matchList.indexOf(obj) != nil) ? matchScore : nil; |
| | 3054 | } |
| | 3055 | |
| | 3056 | /* |
| | 3057 | * a match is always possible for simple verb topics (since the |
| | 3058 | * player could always type the verb) |
| | 3059 | */ |
| | 3060 | isMatchPossible(actor, scopeList) { return true; } |
| | 3061 | ; |
| | 3062 | |
| | 3063 | /* |
| | 3064 | * A greeting topic - this handles a HELLO or TALK TO command, as well |
| | 3065 | * as implied greetings (the kind of greeting generated when we jump |
| | 3066 | * directly into a conversation with an actor that uses stateful |
| | 3067 | * conversations, by typing a command like ASK ABOUT or TELL ABOUT |
| | 3068 | * without first saying HELLO explicitly). |
| | 3069 | */ |
| | 3070 | class HelloTopic: MiscTopic |
| | 3071 | includeInList = [&miscTopics] |
| | 3072 | matchList = [helloTopicObj, impHelloTopicObj] |
| | 3073 | |
| | 3074 | /* |
| | 3075 | * this is an explicit greeting, so it obviously shouldn't trigger |
| | 3076 | * an implied greeting, regardless of how conversational we are |
| | 3077 | */ |
| | 3078 | impliesGreeting = nil |
| | 3079 | |
| | 3080 | /* |
| | 3081 | * if we use this as a greeting upon entering a ConvNode, we'll want |
| | 3082 | * to stay in the node afterward |
| | 3083 | */ |
| | 3084 | noteInvocation(fromActor) |
| | 3085 | { |
| | 3086 | inherited(fromActor); |
| | 3087 | "<.convstay>"; |
| | 3088 | } |
| | 3089 | ; |
| | 3090 | |
| | 3091 | /* |
| | 3092 | * An implied greeting topic. This handles ONLY implied greetings. |
| | 3093 | * |
| | 3094 | * Note that we have a higher-than-normal score by default. This makes |
| | 3095 | * it easy to program two common cases for conversational states. |
| | 3096 | * First, the more common case, where you want a single message for both |
| | 3097 | * implied and explicit greetings: just create a HelloTopic, since that |
| | 3098 | * responds to both kinds. Second, the less common case, where we want |
| | 3099 | * to differentiate, writing separate responses for implied and explicit |
| | 3100 | * greetings: create a HelloTopic for the explicit kind, and ALSO create |
| | 3101 | * an ImpHelloTopic for the implied kind. Since the ImpHelloTopic has a |
| | 3102 | * higher score, it'll overshadow the HelloTopic object when it matches |
| | 3103 | * an implied greeting; but since ImpHelloTopic doesn't match an |
| | 3104 | * explicit greeting, we'll fall back on the HelloTopic for that. |
| | 3105 | */ |
| | 3106 | class ImpHelloTopic: MiscTopic |
| | 3107 | includeInList = [&miscTopics] |
| | 3108 | matchList = [impHelloTopicObj] |
| | 3109 | matchScore = 200 |
| | 3110 | |
| | 3111 | /* |
| | 3112 | * this is itself a greeting, so we obviously don't want to trigger |
| | 3113 | * another greeting to greet the greeting |
| | 3114 | */ |
| | 3115 | impliesGreeting = nil |
| | 3116 | |
| | 3117 | /* |
| | 3118 | * if we use this as a greeting upon entering a ConvNode, we'll want |
| | 3119 | * to stay in the node afterward |
| | 3120 | */ |
| | 3121 | noteInvocation(fromActor) |
| | 3122 | { |
| | 3123 | inherited(fromActor); |
| | 3124 | "<.convstay>"; |
| | 3125 | } |
| | 3126 | ; |
| | 3127 | |
| | 3128 | /* |
| | 3129 | * Actor Hello topic - this handles greetings when an NPC initiates the |
| | 3130 | * conversation. |
| | 3131 | */ |
| | 3132 | class ActorHelloTopic: MiscTopic |
| | 3133 | includeInList = [&miscTopics] |
| | 3134 | matchList = [actorHelloTopicObj] |
| | 3135 | matchScore = 200 |
| | 3136 | |
| | 3137 | /* this is a greeting, so we don't want to trigger another greeting */ |
| | 3138 | impliesGreeting = nil |
| | 3139 | |
| | 3140 | /* |
| | 3141 | * if we use this as a greeting upon entering a ConvNode, we'll want |
| | 3142 | * to stay in the node afterward |
| | 3143 | */ |
| | 3144 | noteInvocation(fromActor) |
| | 3145 | { |
| | 3146 | inherited(fromActor); |
| | 3147 | "<.convstay>"; |
| | 3148 | } |
| | 3149 | ; |
| | 3150 | |
| | 3151 | /* |
| | 3152 | * A goodbye topic - this handles both explicit GOODBYE commands and |
| | 3153 | * implied goodbyes. Implied goodbyes happen when a conversation ends |
| | 3154 | * without an explicit GOODBYE command, such as when the player character |
| | 3155 | * walks away from the NPC, or the NPC gets bored and wanders off, or the |
| | 3156 | * NPC terminates the conversation of its own volition. |
| | 3157 | */ |
| | 3158 | class ByeTopic: MiscTopic |
| | 3159 | includeInList = [&miscTopics] |
| | 3160 | matchList = [byeTopicObj, |
| | 3161 | leaveByeTopicObj, boredByeTopicObj, actorByeTopicObj] |
| | 3162 | |
| | 3163 | /* |
| | 3164 | * If we're not already in a conversation when we say GOODBYE, don't |
| | 3165 | * bother saying HELLO implicitly - if the player is saying GOODBYE |
| | 3166 | * explicitly, she probably has the impression that there's some kind |
| | 3167 | * of interaction already going on with the NPC. If we didn't |
| | 3168 | * override this, you'd get an automatic HELLO followed by the |
| | 3169 | * explicit GOODBYE when not already in conversation, which is a |
| | 3170 | * little weird. |
| | 3171 | */ |
| | 3172 | impliesGreeting = nil |
| | 3173 | ; |
| | 3174 | |
| | 3175 | /* |
| | 3176 | * An implied goodbye topic. This handles ONLY automatic (implied) |
| | 3177 | * conversation endings, which happen when we walk away from an actor |
| | 3178 | * we're talking to, or the other actor ends the conversation after being |
| | 3179 | * ignored for too long, or the other actor ends the conversation of its |
| | 3180 | * own volition via npc.endConversation(). |
| | 3181 | * |
| | 3182 | * We use a higher-than-default matchScore so that any time we have both |
| | 3183 | * a ByeTopic and an ImpByeTopic that are both active, we'll choose the |
| | 3184 | * more specific ImpByeTopic. |
| | 3185 | */ |
| | 3186 | class ImpByeTopic: MiscTopic |
| | 3187 | includeInList = [&miscTopics] |
| | 3188 | matchList = [leaveByeTopicObj, boredByeTopicObj, actorByeTopicObj] |
| | 3189 | matchScore = 200 |
| | 3190 | ; |
| | 3191 | |
| | 3192 | /* |
| | 3193 | * A "bored" goodbye topic. This handles ONLY goodbyes that happen when |
| | 3194 | * the actor we're talking terminates the conversation out of boredom |
| | 3195 | * (i.e., after a period of inactivity in the conversation). |
| | 3196 | * |
| | 3197 | * Note that this is a subset of ImpByeTopic - ImpByeTopic handles |
| | 3198 | * "bored" and "leaving" goodbyes, while this one handles only the |
| | 3199 | * "bored" goodbyes. You can use this kind of topic if you want to |
| | 3200 | * differentiate the responses to "bored" and "leaving" conversation |
| | 3201 | * endings. |
| | 3202 | */ |
| | 3203 | class BoredByeTopic: MiscTopic |
| | 3204 | includeInList = [&miscTopics] |
| | 3205 | matchList = [boredByeTopicObj] |
| | 3206 | matchScore = 300 |
| | 3207 | ; |
| | 3208 | |
| | 3209 | /* |
| | 3210 | * A "leaving" goodbye topic. This handles ONLY goodbyes that happen |
| | 3211 | * when the PC walks away from the actor they're talking to. |
| | 3212 | * |
| | 3213 | * Note that this is a subset of ImpByeTopic - ImpByeTopic handles |
| | 3214 | * "bored" and "leaving" goodbyes, while this one handles only the |
| | 3215 | * "leaving" goodbyes. You can use this kind of topic if you want to |
| | 3216 | * differentiate the responses to "bored" and "leaving" conversation |
| | 3217 | * endings. |
| | 3218 | */ |
| | 3219 | class LeaveByeTopic: MiscTopic |
| | 3220 | includeInList = [&miscTopics] |
| | 3221 | matchList = [leaveByeTopicObj] |
| | 3222 | matchScore = 300 |
| | 3223 | ; |
| | 3224 | |
| | 3225 | /* |
| | 3226 | * An "actor" goodbye topic. This handles ONLY goodbyes that happen when |
| | 3227 | * the NPC terminates the conversation of its own volition via |
| | 3228 | * npc.endConversation(). |
| | 3229 | */ |
| | 3230 | class ActorByeTopic: MiscTopic |
| | 3231 | includeInList = [&miscTopics] |
| | 3232 | matchList = [actorByeTopicObj] |
| | 3233 | matchScore = 300 |
| | 3234 | ; |
| | 3235 | |
| | 3236 | /* a topic for both HELLO and GOODBYE */ |
| | 3237 | class HelloGoodbyeTopic: MiscTopic |
| | 3238 | includeInList = [&miscTopics] |
| | 3239 | matchList = [helloTopicObj, impHelloTopicObj, |
| | 3240 | byeTopicObj, boredByeTopicObj, leaveByeTopicObj, |
| | 3241 | actorByeTopicObj] |
| | 3242 | |
| | 3243 | /* |
| | 3244 | * since we handle greetings, we don't want to trigger a separate |
| | 3245 | * implied greeting |
| | 3246 | */ |
| | 3247 | impliesGreeting = nil |
| | 3248 | ; |
| | 3249 | |
| | 3250 | /* |
| | 3251 | * Topic singletons representing HELLO and GOODBYE topics. These are |
| | 3252 | * used as the parameter to matchTopic() when we're looking for the |
| | 3253 | * response to the corresponding verbs. |
| | 3254 | */ |
| | 3255 | helloTopicObj: object; |
| | 3256 | byeTopicObj: object; |
| | 3257 | |
| | 3258 | /* |
| | 3259 | * a topic singleton for implied greetings (the kind of greeting that |
| | 3260 | * happens when we jump right into a conversation with a command like |
| | 3261 | * ASK ABOUT or TELL ABOUT, rather than explicitly saying HELLO first) |
| | 3262 | */ |
| | 3263 | impHelloTopicObj: object; |
| | 3264 | |
| | 3265 | /* |
| | 3266 | * a topic singleton for an NPC-initiated hello (this is the kind of |
| | 3267 | * greeting that happens when the NPC is the one who initiates the |
| | 3268 | * conversation, via actor.initiateConversation()) |
| | 3269 | */ |
| | 3270 | actorHelloTopicObj: object; |
| | 3271 | |
| | 3272 | |
| | 3273 | /* |
| | 3274 | * topic singletons for the two kinds of automatic goodbyes (the kind of |
| | 3275 | * conversation ending that happens when we simply walk away from an |
| | 3276 | * actor we're in conversation with, or when we ignore the other actor |
| | 3277 | * for enough turns that the actor gets bored and ends the conversation |
| | 3278 | * of its own volition) |
| | 3279 | */ |
| | 3280 | boredByeTopicObj: object; |
| | 3281 | leaveByeTopicObj: object; |
| | 3282 | |
| | 3283 | /* |
| | 3284 | * a topic singleton for an NPC-initiated goodbye (this is the kind of |
| | 3285 | * goodbye that happens when the NPC is the one who breaks off the |
| | 3286 | * conversation, via npc.endConversation()) |
| | 3287 | */ |
| | 3288 | actorByeTopicObj: object; |
| | 3289 | |
| | 3290 | /* |
| | 3291 | * A YES/NO topic. These handle YES and/or NO, which are normally used |
| | 3292 | * as responses to questions posed by the NPC. YesNoTopic is the base |
| | 3293 | * class, and can be used to create a single response for both YES and |
| | 3294 | * NO; YesTopic provides a response just for YES; and NoTopic provides a |
| | 3295 | * response just for NO. The only thing an instance of these classes |
| | 3296 | * should normally need to specify is the response text (or a list of |
| | 3297 | * response strings, by multiply inheriting from an EventList subclass as |
| | 3298 | * usual). |
| | 3299 | */ |
| | 3300 | class YesNoTopic: MiscTopic |
| | 3301 | includeInList = [&miscTopics] |
| | 3302 | |
| | 3303 | /* |
| | 3304 | * our list of matching topic objects - we'll only ever be asked to |
| | 3305 | * match 'yesTopicObj' (for YES inputs) or 'noTopicObj' (for NO |
| | 3306 | * inputs) |
| | 3307 | */ |
| | 3308 | matchList = [yesTopicObj, noTopicObj] |
| | 3309 | ; |
| | 3310 | |
| | 3311 | class YesTopic: YesNoTopic |
| | 3312 | matchList = [yesTopicObj] |
| | 3313 | ; |
| | 3314 | |
| | 3315 | class NoTopic: YesNoTopic |
| | 3316 | matchList = [noTopicObj] |
| | 3317 | ; |
| | 3318 | |
| | 3319 | /* |
| | 3320 | * Topic singletons representing the "topic" of YES and NO commands. We |
| | 3321 | * use these as the parameter to matchTopic() in the TopicEntry objects |
| | 3322 | * when we're looking for a response to a YES or NO command. |
| | 3323 | */ |
| | 3324 | yesTopicObj: object; |
| | 3325 | noTopicObj: object; |
| | 3326 | |
| | 3327 | |
| | 3328 | /* |
| | 3329 | * A default topic entry. This is an easy way to create an entry that |
| | 3330 | * will be used as a last resort, if no other entry is found. This kind |
| | 3331 | * of entry will match *any* topic, but with the lowest possible score, |
| | 3332 | * so it will only be used if there's no other match for the topic. |
| | 3333 | * |
| | 3334 | * It's a good idea to provide some variety in a character's default |
| | 3335 | * responses, because it seems that in every real game session, the |
| | 3336 | * player will at some point spend a while peppering an NPC with |
| | 3337 | * questions on every topic that comes to mind. Usually, the player will |
| | 3338 | * think of many things that the author didn't anticipate. The more |
| | 3339 | * things the author covers, the better, but it's unrealistic to think |
| | 3340 | * that an author can reasonably anticipate every topic, or even most |
| | 3341 | * topics, that players will think of. So, we'll have a whole bunch of |
| | 3342 | * ASK, ASK, ASK commands all at once, and much of the time we'll get a |
| | 3343 | * bunch of default responses in a row. It gets tedious in these cases |
| | 3344 | * when the NPC repeats the same default response over and over. |
| | 3345 | * |
| | 3346 | * A simple but effective trick is to provide three or four random |
| | 3347 | * variations on "I don't know that," customized for the character. This |
| | 3348 | * makes the NPC seem less like a totally predictable robot, and it can |
| | 3349 | * also be a convenient place to flesh out the character a bit. An easy |
| | 3350 | * way to do this is to add ShuffledEventList to the superclass list of |
| | 3351 | * the default topic entry, and provide a eventList list with the various |
| | 3352 | * random responses. For example: |
| | 3353 | * |
| | 3354 | * + DefaultAskTellTopic, ShuffledEventList |
| | 3355 | *. ['Bob mutters something unintelligible and keeps fiddling with |
| | 3356 | *. the radio. ', |
| | 3357 | *. 'Bob looks up from the radio for a second, but then goes back |
| | 3358 | *. to adjusting the knobs. ', |
| | 3359 | *. 'Bob just keeps adjusting the radio, completely ignoring you. '] |
| | 3360 | *. ; |
| | 3361 | * |
| | 3362 | * It's important to be rather generic in default responses; in |
| | 3363 | * particular, it's a bad idea to suggest that the NPC doesn't know about |
| | 3364 | * the topic. From the author's perspective, it's easy to make the |
| | 3365 | * mistake of thinking "this is a default response, so it'll only be used |
| | 3366 | * for topics that are completely off in left field." Wrong! Sometimes |
| | 3367 | * the player will indeed ask about completely random stuff, but in |
| | 3368 | * *most* cases, the player is only asking because they think it's a |
| | 3369 | * reasonable thing to ask about. Defaults that say things like "I don't |
| | 3370 | * know anything about that" or "What a crazy thing to ask about" or "You |
| | 3371 | * must be stupid if you think I know about that!" can make a game look |
| | 3372 | * poorly implemented, because these will inevitably be shown in response |
| | 3373 | * to questions that the NPC really ought to know about: |
| | 3374 | * |
| | 3375 | *. >ask bob about his mother |
| | 3376 | *. "I don't know anything about that!" |
| | 3377 | *. |
| | 3378 | *. >ask bob about his father |
| | 3379 | *. "You'd have to be a moron to think I'd know about that!" |
| | 3380 | * |
| | 3381 | * It's better to use responses that suggest that the NPC is |
| | 3382 | * uninterested, or is hostile, or is preoccupied with something else, or |
| | 3383 | * doesn't understand the question, or something else appropriate to the |
| | 3384 | * character. If you can manage to make the response about the |
| | 3385 | * *character*, rather than the topic, it'll reduce the chances that the |
| | 3386 | * response is jarringly illogical. |
| | 3387 | */ |
| | 3388 | class DefaultTopic: TopicEntry |
| | 3389 | /* |
| | 3390 | * A list of objects to exclude from the default match. This can be |
| | 3391 | * used to create a default topic that matches everything EXCEPT a |
| | 3392 | * few specific topics that are handled in enclosing topic databases. |
| | 3393 | * For example, if you want to create a catch-all in a ConvNode's |
| | 3394 | * list of topics, but you want a particular topic to escape the |
| | 3395 | * catch-all and be sent instead to the Actor's topic database, you |
| | 3396 | * can put that topic in the exclude list for the catch-all, making |
| | 3397 | * it a catch-almost-all. |
| | 3398 | */ |
| | 3399 | excludeMatch = [] |
| | 3400 | |
| | 3401 | /* match anything except topics in our exclude list */ |
| | 3402 | matchTopic(fromActor, topic) |
| | 3403 | { |
| | 3404 | /* |
| | 3405 | * If the topic matches anything in the exclusion list, do NOT |
| | 3406 | * match the topic. If 'topic' is a ResolvedTopic, search its |
| | 3407 | * in-scope and 'likely' lists; otherwise search for 'topic' |
| | 3408 | * directly in the exclusion list. |
| | 3409 | */ |
| | 3410 | if (topic.ofKind(ResolvedTopic)) |
| | 3411 | { |
| | 3412 | /* it's a resolved topic, so search the in-scope/likely lists */ |
| | 3413 | if (topic.inScopeList.intersect(excludeMatch).length() != 0 |
| | 3414 | || topic.likelyList.intersect(excludeMatch).length() != 0) |
| | 3415 | return nil; |
| | 3416 | } |
| | 3417 | else if (excludeMatch.indexOf(topic) != nil) |
| | 3418 | return nil; |
| | 3419 | |
| | 3420 | /* match anything else with our score */ |
| | 3421 | return matchScore; |
| | 3422 | } |
| | 3423 | |
| | 3424 | /* use a low default matching score */ |
| | 3425 | matchScore = 1 |
| | 3426 | |
| | 3427 | /* a match is always possible for a default topic */ |
| | 3428 | isMatchPossible(actor, scopeList) { return true; } |
| | 3429 | |
| | 3430 | /* set the topic pronoun */ |
| | 3431 | setTopicPronouns(fromActor, topic) |
| | 3432 | { |
| | 3433 | /* |
| | 3434 | * We're not matching anything, so we can get no guidance from |
| | 3435 | * the match object. Instead, look at the topic itself. If it's |
| | 3436 | * a Thing, set the Thing as the antecedent. If it's a |
| | 3437 | * ResolvedTopic, and there's only one Thing match in scope, or |
| | 3438 | * only one Thing match in the likely list, set that. Otherwise, |
| | 3439 | * we have no grounds for guessing. |
| | 3440 | */ |
| | 3441 | if (topic != nil) |
| | 3442 | { |
| | 3443 | if (topic.ofKind(Thing)) |
| | 3444 | { |
| | 3445 | /* we have a Thing - use it as the antecedent */ |
| | 3446 | fromActor.setPronounObj(topic); |
| | 3447 | } |
| | 3448 | else if (topic.ofKind(ResolvedTopic)) |
| | 3449 | { |
| | 3450 | local lst; |
| | 3451 | |
| | 3452 | /* |
| | 3453 | * if there's only one Thing in scope, or only one Thing |
| | 3454 | * in the 'likely' list, use it |
| | 3455 | */ |
| | 3456 | lst = topic.inScopeList.subset({x: x.ofKind(Thing)}); |
| | 3457 | if (lst.length() == 0) |
| | 3458 | lst = topic.likelyList.subset({x: x.ofKind(Thing)}); |
| | 3459 | |
| | 3460 | /* if we got exactly one object, it's the antecedent */ |
| | 3461 | if (lst.length() == 1) |
| | 3462 | fromActor.setPronounObj(lst[1]); |
| | 3463 | } |
| | 3464 | } |
| | 3465 | } |
| | 3466 | ; |
| | 3467 | |
| | 3468 | /* |
| | 3469 | * Default topic entries for different uses. We'll use a hierarchy of |
| | 3470 | * low match scores, in descending order of specificity: 3 for |
| | 3471 | * single-type defaults (ASK only, for example), 2 for multi-type |
| | 3472 | * defaults (ASK/TELL), and 1 for the ANY default. |
| | 3473 | */ |
| | 3474 | class DefaultCommandTopic: DefaultTopic |
| | 3475 | includeInList = [&commandTopics] |
| | 3476 | matchScore = 3 |
| | 3477 | ; |
| | 3478 | class DefaultAskTopic: DefaultTopic |
| | 3479 | includeInList = [&askTopics] |
| | 3480 | matchScore = 3 |
| | 3481 | ; |
| | 3482 | class DefaultTellTopic: DefaultTopic |
| | 3483 | includeInList = [&tellTopics] |
| | 3484 | matchScore = 3 |
| | 3485 | ; |
| | 3486 | class DefaultAskTellTopic: DefaultTopic |
| | 3487 | includeInList = [&askTopics, &tellTopics] |
| | 3488 | matchScore = 2 |
| | 3489 | ; |
| | 3490 | class DefaultGiveTopic: DefaultTopic |
| | 3491 | includeInList = [&giveTopics] |
| | 3492 | matchScore = 3 |
| | 3493 | ; |
| | 3494 | class DefaultShowTopic: DefaultTopic |
| | 3495 | includeInList = [&showTopics] |
| | 3496 | matchScore = 3 |
| | 3497 | ; |
| | 3498 | class DefaultGiveShowTopic: DefaultTopic |
| | 3499 | includeInList = [&giveTopics, &showTopics] |
| | 3500 | matchScore = 2 |
| | 3501 | ; |
| | 3502 | class DefaultAskForTopic: DefaultTopic |
| | 3503 | includeInList = [&askForTopics] |
| | 3504 | matchScore = 3 |
| | 3505 | ; |
| | 3506 | class DefaultAnyTopic: DefaultTopic |
| | 3507 | includeInList = [&askTopics, &tellTopics, &showTopics, &giveTopics, |
| | 3508 | &askForTopics, &miscTopics, &commandTopics] |
| | 3509 | |
| | 3510 | /* |
| | 3511 | * exclude these from actor-initiated hellos & goodbyes - those |
| | 3512 | * should only match topics explicitly |
| | 3513 | */ |
| | 3514 | excludeMatch = [actorHelloTopicObj, actorByeTopicObj] |
| | 3515 | matchScore = 1 |
| | 3516 | ; |
| | 3517 | |
| | 3518 | |
| | 3519 | /* |
| | 3520 | * A "special" topic. This is a topic that responds to its own unique, |
| | 3521 | * custom command input. In other words, rather than responding to a |
| | 3522 | * normal command like ASK ABOUT or SHOW TO, we'll respond to a command |
| | 3523 | * for which we define our own syntax. Our special syntax doesn't have |
| | 3524 | * to follow any of the ordinary parsing conventions, because whenever |
| | 3525 | * our ConvNode is active, we get a shot at parsing player input before |
| | 3526 | * the regular parser gets to see it. |
| | 3527 | * |
| | 3528 | * A special topic MUST be part of a ConvNode, because these are |
| | 3529 | * inherently meaningful only in context. A special topic is active |
| | 3530 | * only when its conversation node is active. |
| | 3531 | * |
| | 3532 | * Special topics are automatically Suggested Topics as well as Topic |
| | 3533 | * Entries. Because special topics use their own custom grammar, it's |
| | 3534 | * unreasonable to expect a player to guess at the custom grammar, so we |
| | 3535 | * should always provide a topic inventory suggestion for every special |
| | 3536 | * topic. |
| | 3537 | */ |
| | 3538 | class SpecialTopic: TopicEntry, SuggestedTopicTree |
| | 3539 | /* |
| | 3540 | * Our keyword list. Each special topic instance must define a list |
| | 3541 | * of strings giving the keywords we match. The special topic will |
| | 3542 | * match user input if the user input consists exclusively of words |
| | 3543 | * from this keyword list. The user input doesn't have to include |
| | 3544 | * all of the words defined here, but all of the words in the user's |
| | 3545 | * input have to appear here to match. |
| | 3546 | * |
| | 3547 | * Alternatively, an instance can specifically define its own custom |
| | 3548 | * regular expression pattern instead of using the keyword list; the |
| | 3549 | * regular expression allows the instance to include punctuation in |
| | 3550 | * the syntax, or apply more restrictive criteria than simply |
| | 3551 | * matching the keywords. |
| | 3552 | */ |
| | 3553 | keywordList = [] |
| | 3554 | |
| | 3555 | /* |
| | 3556 | * Initialize the special topic. This runs during |
| | 3557 | * pre-initialization, to give us a chance to do pre-game set-up. |
| | 3558 | * |
| | 3559 | * This routine adds the topic's keywords to the global dictionary, |
| | 3560 | * under the 'special' token type. Since a special topic's keywords |
| | 3561 | * are accepted when the special topic is active, it would be wrong |
| | 3562 | * for the parser to claim that the words are unknown when the |
| | 3563 | * special topic isn't active. By adding the keywords to the |
| | 3564 | * dictionary, we let the parser know that they're valid words, so |
| | 3565 | * that it won't claim that they're unknown. |
| | 3566 | */ |
| | 3567 | initializeSpecialTopic() |
| | 3568 | { |
| | 3569 | /* add each keyword */ |
| | 3570 | foreach (local cur in keywordList) |
| | 3571 | { |
| | 3572 | /* |
| | 3573 | * Add the keyword. Since we don't actually need the |
| | 3574 | * word-to-object association that the dictionary stores, |
| | 3575 | * simply associate the word with the SpecialTopic class |
| | 3576 | * rather than with this particular special topic instance. |
| | 3577 | * The dictionary only stores a given word-obj-prop |
| | 3578 | * association once, even if it's entered repeatedly, so |
| | 3579 | * tying all of the special topic keywords to the |
| | 3580 | * SpecialTopic class ensures that we won't store redundant |
| | 3581 | * entries if the same keyword is used in multiple special |
| | 3582 | * topics. |
| | 3583 | */ |
| | 3584 | cmdDict.addWord(SpecialTopic, cur, &specialTopicWord); |
| | 3585 | } |
| | 3586 | } |
| | 3587 | |
| | 3588 | /* |
| | 3589 | * our regular expression pattern - we'll build this automatically |
| | 3590 | * from the keyword list if this isn't otherwise defined |
| | 3591 | */ |
| | 3592 | matchPat = nil |
| | 3593 | |
| | 3594 | /* our suggestion (topic inventory) base name */ |
| | 3595 | name = '' |
| | 3596 | |
| | 3597 | /* |
| | 3598 | * our suggestion (topic inventory) full name is usually the same as |
| | 3599 | * the base name; special topics usually aren't grouped in topic |
| | 3600 | * suggestion listings, since each topic usually has its own unique, |
| | 3601 | * custom syntax |
| | 3602 | */ |
| | 3603 | fullName = (name) |
| | 3604 | |
| | 3605 | /* on being suggested, update the special topic history */ |
| | 3606 | noteSuggestion() { specialTopicHistory.noteListing(self); } |
| | 3607 | |
| | 3608 | /* include in the specialTopics list of our parent topic database */ |
| | 3609 | includeInList = [&specialTopics] |
| | 3610 | |
| | 3611 | /* |
| | 3612 | * By default, don't limit the number of times we'll suggest this |
| | 3613 | * topic. Since a special topic is valid only in a particular |
| | 3614 | * ConvNode context, we normally want all of the topics in that |
| | 3615 | * context to be available, even if they've been used before. |
| | 3616 | */ |
| | 3617 | timesToSuggest = nil |
| | 3618 | |
| | 3619 | /* check for a match */ |
| | 3620 | matchTopic(fromActor, topic) |
| | 3621 | { |
| | 3622 | /* |
| | 3623 | * We match if and only if we're the current active topic for |
| | 3624 | * our conversation node, as designated during our pre-parsing. |
| | 3625 | * Because we're activated exclusively by our special syntax, |
| | 3626 | * the only way we can ever match is by matching our special |
| | 3627 | * syntax in pre-parsing; when that happens, the pre-parser |
| | 3628 | * notes the matching SpecialTopic and sends a pseudo-command to |
| | 3629 | * the parser to let it know to invoke the special topic's |
| | 3630 | * response. We take this circuitous route to showing the |
| | 3631 | * response because we do our actual matching in the pre-parse |
| | 3632 | * step, but we want to do the actual command processing |
| | 3633 | * normally; we can only accomplish both needs using this |
| | 3634 | * two-step process, with the two steps tied together via our |
| | 3635 | * memory of the topic selected in pre-parse. |
| | 3636 | */ |
| | 3637 | if (getConvNode().activeSpecialTopic == self) |
| | 3638 | return matchScore; |
| | 3639 | else |
| | 3640 | return nil; |
| | 3641 | } |
| | 3642 | |
| | 3643 | /* |
| | 3644 | * a special topic is always matchable, since we match on literal |
| | 3645 | * text |
| | 3646 | */ |
| | 3647 | isMatchPossible(actor, scopeList) { return true; } |
| | 3648 | |
| | 3649 | /* |
| | 3650 | * Match a string during pre-parsing. By default, we'll match the |
| | 3651 | * string if all of its words (as defined by the regular expression |
| | 3652 | * parser) match our keywords. |
| | 3653 | */ |
| | 3654 | matchPreParse(str, procStr) |
| | 3655 | { |
| | 3656 | /* build the regular expression pattern if there isn't one */ |
| | 3657 | if (matchPat == nil) |
| | 3658 | { |
| | 3659 | local pat; |
| | 3660 | |
| | 3661 | /* start with the base pattern string */ |
| | 3662 | pat = '<nocase><space>*(%<'; |
| | 3663 | |
| | 3664 | /* add the keywords */ |
| | 3665 | for (local i = 1, local len = keywordList.length() ; |
| | 3666 | i <= len ; ++i) |
| | 3667 | { |
| | 3668 | /* add this keyword to the pattern */ |
| | 3669 | pat += keywordList[i]; |
| | 3670 | |
| | 3671 | /* add the separator or terminator, as appropriate */ |
| | 3672 | if (i == len) |
| | 3673 | pat += '%><space>*)+'; |
| | 3674 | else |
| | 3675 | pat += '%><space>*|%<'; |
| | 3676 | } |
| | 3677 | |
| | 3678 | /* create the pattern object */ |
| | 3679 | matchPat = new RexPattern(pat); |
| | 3680 | } |
| | 3681 | |
| | 3682 | /* we have a match if the pattern matches the processed input */ |
| | 3683 | return rexMatch(matchPat, procStr) == procStr.length(); |
| | 3684 | } |
| | 3685 | |
| | 3686 | /* find our enclosing ConvNode object */ |
| | 3687 | getConvNode() |
| | 3688 | { |
| | 3689 | /* scan up the containment tree for a ConvNode */ |
| | 3690 | for (local loc = location ; loc != nil ; loc = loc.location) |
| | 3691 | { |
| | 3692 | /* if this is a ConvNode, it's what we're looking for */ |
| | 3693 | if (loc.ofKind(ConvNode)) |
| | 3694 | return loc; |
| | 3695 | } |
| | 3696 | |
| | 3697 | /* not found */ |
| | 3698 | return nil; |
| | 3699 | } |
| | 3700 | ; |
| | 3701 | |
| | 3702 | /* |
| | 3703 | * A history of special topics listed in topic inventories. This keeps |
| | 3704 | * track of special topics that we've recently offered, so that we can |
| | 3705 | * provide better feedback if the player tries to use a recently-listed |
| | 3706 | * special topic after it's gone out of context. |
| | 3707 | * |
| | 3708 | * When the player types a command that the parser doesn't recognize, the |
| | 3709 | * parser will check the special topic history to see if the command |
| | 3710 | * matches a special topic that was suggested recently. If so, we'll |
| | 3711 | * explain that the command isn't usable right now, rather than claiming |
| | 3712 | * that the command is completely invalid. A player might justifiably |
| | 3713 | * find it confusing to have the game suggest a command one minute, and |
| | 3714 | * then claim that the very same command is invalid a minute later. |
| | 3715 | * |
| | 3716 | * Ideally, we'd search *every* special topic for a match each time the |
| | 3717 | * player enters an invalid command, but that could take a long time in a |
| | 3718 | * conversation-heavy game with a large number of special topics. As a |
| | 3719 | * compromise, we keep track of the last few special commands that were |
| | 3720 | * actually suggested, so that we can scan those. The reasoning is that |
| | 3721 | * a player is more likely to try a recently-offered special command; the |
| | 3722 | * player will probably eventually forget older suggestions, and in any |
| | 3723 | * case it's much more jarring to see a "command not understood" response |
| | 3724 | * to a suggestion that's still fresh in the player's memory. |
| | 3725 | * |
| | 3726 | * This is a transient object because we're interested in the special |
| | 3727 | * topics that have been offered in the current session, irrespective of |
| | 3728 | * things like 'undo' and 'restore'. From the player's perspective, the |
| | 3729 | * recency of a special topic suggestion is a function of the transcript, |
| | 3730 | * not of the internal story timeline. For example, if the game suggests |
| | 3731 | * a special topic, then the player types UNDO, the player might still |
| | 3732 | * think to try the special topic on the next turn simply because it's |
| | 3733 | * right there on the screen a few lines up. |
| | 3734 | */ |
| | 3735 | transient specialTopicHistory: object |
| | 3736 | /* |
| | 3737 | * Maximum number of topics to keep in our inventory. When the |
| | 3738 | * history exceeds this number, we'll throw away the oldest entry |
| | 3739 | * each time we need to add a new entry - thus, we'll always have the |
| | 3740 | * N most recent suggestions. |
| | 3741 | * |
| | 3742 | * This can be configured as desired. The default setting tries to |
| | 3743 | * strike a balance between speed and good feedback - we try to keep |
| | 3744 | * track of enough entries that most players wouldn't think to try |
| | 3745 | * anything that's aged out of the list, but not so many that it |
| | 3746 | * takes a long time to scan them all. |
| | 3747 | * |
| | 3748 | * If you set this to nil, we won't keep a history at all, but |
| | 3749 | * instead simply scan every special topic in the entire game when we |
| | 3750 | * need to look for a match to an entered command - in a game with a |
| | 3751 | * small number of special topics (on the order of, say, 30 or 40), |
| | 3752 | * there should be no problem using this approach. Note that this |
| | 3753 | * changes the behavior in one important way: when there's no history |
| | 3754 | * limit, we can topics that *haven't even been offered yet*. In |
| | 3755 | * some ways this is more desirable than only scanning past |
| | 3756 | * suggestions, since it avoids weird situations where the game |
| | 3757 | * claims that a command is unrecognized at one point, but later |
| | 3758 | * suggests and then accepts the exact same command. It's |
| | 3759 | * conceivably less desirable in that it could accidentally give away |
| | 3760 | * information to the player, by letting them know that a randomly |
| | 3761 | * typed command will be meaningful at some point in the game - but |
| | 3762 | * the odds of this even happening seem minuscule, and the |
| | 3763 | * possibility that it would give away meaningful information even if |
| | 3764 | * it did happen seems very remote. |
| | 3765 | */ |
| | 3766 | maxEntries = 20 |
| | 3767 | |
| | 3768 | /* note that a special topic 't' is being listed in a topic inventory */ |
| | 3769 | noteListing(t) |
| | 3770 | { |
| | 3771 | /* |
| | 3772 | * If t's already in the list, delete it from its current |
| | 3773 | * position, so that we can add it back at the end of the list, |
| | 3774 | * reflecting its status as the most recent entry. |
| | 3775 | */ |
| | 3776 | historyList.removeElement(t); |
| | 3777 | |
| | 3778 | /* |
| | 3779 | * if the list is already at capacity, remove the oldest entry, |
| | 3780 | * which is the first entry in the list |
| | 3781 | */ |
| | 3782 | if (maxEntries != nil && historyList.length() >= maxEntries) |
| | 3783 | historyList.removeElementAt(1); |
| | 3784 | |
| | 3785 | /* add the new entry at the end of the list */ |
| | 3786 | historyList.append(t); |
| | 3787 | } |
| | 3788 | |
| | 3789 | /* |
| | 3790 | * Scan the history list (or, if there's no limit to the history, |
| | 3791 | * scan all of the special topics in the entire game) for a match to |
| | 3792 | * an unrecognized command. Returns true if we find a match, nil if |
| | 3793 | * not. |
| | 3794 | */ |
| | 3795 | checkHistory(toks) |
| | 3796 | { |
| | 3797 | local str, procStr; |
| | 3798 | |
| | 3799 | /* get the original and processed version of the input string */ |
| | 3800 | str = cmdTokenizer.buildOrigText(toks); |
| | 3801 | procStr = specialTopicPreParser.processInputStr(str); |
| | 3802 | |
| | 3803 | /* |
| | 3804 | * scan each special topic in the history - or, if the history is |
| | 3805 | * unlimited, scan every special topic |
| | 3806 | */ |
| | 3807 | if (maxEntries != nil) |
| | 3808 | { |
| | 3809 | /* scan each entry in our history list */ |
| | 3810 | for (local l = historyList, local i = 1, local len = l.length() ; |
| | 3811 | i <= len ; ++i) |
| | 3812 | { |
| | 3813 | /* check this entry */ |
| | 3814 | if (l[i].matchPreParse(str, procStr)) |
| | 3815 | return true; |
| | 3816 | } |
| | 3817 | } |
| | 3818 | else |
| | 3819 | { |
| | 3820 | /* no history limit - scan every special topic in the game */ |
| | 3821 | for (local o = firstObj(SpecialTopic) ; o != nil ; |
| | 3822 | o = nextObj(o, SpecialTopic)) |
| | 3823 | { |
| | 3824 | /* check this entry */ |
| | 3825 | if (o.matchPreParse(str, procStr)) |
| | 3826 | return true; |
| | 3827 | } |
| | 3828 | } |
| | 3829 | |
| | 3830 | /* we didn't find a match */ |
| | 3831 | return nil; |
| | 3832 | } |
| | 3833 | |
| | 3834 | /* |
| | 3835 | * The list of entries. Create it when we first need it, which |
| | 3836 | * perInstance does for us. |
| | 3837 | */ |
| | 3838 | historyList = perInstance(new transient Vector(maxEntries)) |
| | 3839 | ; |
| | 3840 | |
| | 3841 | /* |
| | 3842 | * An "initiate" topic entry. This is a rather different kind of topic |
| | 3843 | * entry from the ones we've defined so far; an initiate topic is for |
| | 3844 | * cases where the NPC itself wants to initiate a conversation in |
| | 3845 | * response to something in the environment. |
| | 3846 | * |
| | 3847 | * One way to use initiate topics is to use the current location as the |
| | 3848 | * topic key. This lets the NPC say something appropriate to the current |
| | 3849 | * room, and can be coded simply as |
| | 3850 | * |
| | 3851 | *. actor.initiateTopic(location); |
| | 3852 | */ |
| | 3853 | class InitiateTopic: ThingMatchTopic |
| | 3854 | /* include in the initiateTopics list */ |
| | 3855 | includeInList = [&initiateTopics] |
| | 3856 | |
| | 3857 | /* |
| | 3858 | * since this kind of topic is triggered by internal calculations in |
| | 3859 | * the game, and not on anything the player is doing, there's no |
| | 3860 | * reason that our match object should be a pronoun antecedent |
| | 3861 | */ |
| | 3862 | setTopicPronouns(fromActor, topic) { } |
| | 3863 | ; |
| | 3864 | |
| | 3865 | /* a catch-all default initiate topic */ |
| | 3866 | class DefaultInitiateTopic: DefaultTopic |
| | 3867 | includeInList = [&initiateTopics] |
| | 3868 | ; |
| | 3869 | |
| | 3870 | |
| | 3871 | /* ------------------------------------------------------------------------ */ |
| | 3872 | /* |
| | 3873 | * An ActorState represents the current state of an Actor. |
| | 3874 | * |
| | 3875 | * The main thing that makes actors special is that they're supposed to |
| | 3876 | * be living, breathing people or creatures. That substantially |
| | 3877 | * complicates the programming of one of these objects, because in order |
| | 3878 | * to create the appearance of animation, many things about an actor have |
| | 3879 | * to change over time. |
| | 3880 | * |
| | 3881 | * The ActorState is designed to make it easier to program this |
| | 3882 | * variability that's needed to make an actor seem life-like. The idea |
| | 3883 | * is to separate the parts of an actor that tend to change according to |
| | 3884 | * what the actor is doing, moving all of those out of the Actor object |
| | 3885 | * and into an ActorState object instead. Each ActorState object |
| | 3886 | * represents one state of an actor (i.e., one thing the actor can be |
| | 3887 | * doing). The Actor object becomes easier to program, because we've |
| | 3888 | * reduced the Actor object to the character's constant, unchanging |
| | 3889 | * features. The stateful part is also easier to program, because we |
| | 3890 | * don't have to make it conditional on anything; we simply define all of |
| | 3891 | * the stateful parts in an ActorState, and we define separate ActorState |
| | 3892 | * objects for the different states. |
| | 3893 | * |
| | 3894 | * For example, suppose we want a shopkeeper actor, whose activities |
| | 3895 | * include waiting behind the counter, sweeping the floor, and stacking |
| | 3896 | * cans. We'd define one ActorState object for each of these activities. |
| | 3897 | * When the shopkeeper switches from standing behind the counter to |
| | 3898 | * sweeping, for example, we simply set the "curState" property in the |
| | 3899 | * shopkeeper object so that it points to the "sweeping" state object. |
| | 3900 | * When it's time to stack cans, we change "curState" to it points to the |
| | 3901 | * "stacking cans" state object. |
| | 3902 | */ |
| | 3903 | class ActorState: TravelMessageHandler, ActorTopicDatabase |
| | 3904 | construct(actor) { location = actor; } |
| | 3905 | |
| | 3906 | /* |
| | 3907 | * Activate the state - this is called when we're about to become |
| | 3908 | * the active state for an actor. We do nothing by default. |
| | 3909 | */ |
| | 3910 | activateState(actor, oldState) { } |
| | 3911 | |
| | 3912 | /* |
| | 3913 | * Deactivate the state - this is called when we're the active state |
| | 3914 | * for an actor, and the actor is about to switch to a new state. |
| | 3915 | * We do nothing by default. |
| | 3916 | */ |
| | 3917 | deactivateState(actor, newState) { } |
| | 3918 | |
| | 3919 | /* |
| | 3920 | * Is this the actor's initial state? If so, we'll automatically |
| | 3921 | * set the actor's curState to point to 'self' during |
| | 3922 | * pre-initialization. For obvious reasons, this should be set to |
| | 3923 | * true for only one state for each actor; if multiple states are |
| | 3924 | * all flagged as initial for the same actor, we'll pick on |
| | 3925 | * arbitrarily as the actual initial state. |
| | 3926 | */ |
| | 3927 | isInitState = nil |
| | 3928 | |
| | 3929 | /* |
| | 3930 | * Should we automatically suggest topics when the player greets our |
| | 3931 | * actor? By default, we show our "topic inventory" (the list of |
| | 3932 | * currently active topics marked as "suggested"). This can be set |
| | 3933 | * to nil to suppress this automatic suggestion list. |
| | 3934 | * |
| | 3935 | * Some authors might not like the idea of automatically suggesting |
| | 3936 | * topics every time we greet a character, but nonetheless wish to |
| | 3937 | * keep the TOPICS command as a sort of hint mechanism. This flag |
| | 3938 | * can be used for this purpose. Authors who don't like suggested |
| | 3939 | * topics at all can simply skip defining any SuggestedTopic entries, |
| | 3940 | * in which case there will never be anything to suggest, rendering |
| | 3941 | * this flag moot. |
| | 3942 | */ |
| | 3943 | autoSuggest = true |
| | 3944 | |
| | 3945 | /* |
| | 3946 | * The 'location' is the actor that we're associated with. |
| | 3947 | * |
| | 3948 | * ActorState objects aren't actual simulation objects, so the |
| | 3949 | * 'location' property isn't used for containment. For convenience, |
| | 3950 | * though, use it to indicate which actor we're associated with; this |
| | 3951 | * lets us use the '+' notation to define the state objects |
| | 3952 | * associated with an actor. |
| | 3953 | */ |
| | 3954 | location = nil |
| | 3955 | |
| | 3956 | /* |
| | 3957 | * Get the actor associated with the state - this is simply the |
| | 3958 | * 'location' property. If we're nested inside another ActorState, |
| | 3959 | * then our actor is our enclosing ActorState's actor. |
| | 3960 | */ |
| | 3961 | getActor() |
| | 3962 | { |
| | 3963 | if (location.ofKind(ActorState)) |
| | 3964 | return location.getActor(); |
| | 3965 | else |
| | 3966 | return location; |
| | 3967 | } |
| | 3968 | |
| | 3969 | /* the owner of any topic entries within the state is just my actor */ |
| | 3970 | getTopicOwner() { return getActor(); } |
| | 3971 | |
| | 3972 | /* initialize the actor state */ |
| | 3973 | initializeActorState() |
| | 3974 | { |
| | 3975 | /* |
| | 3976 | * if we're the initial state for our actor, set the actor's |
| | 3977 | * current state property to point to me |
| | 3978 | */ |
| | 3979 | if (isInitState) |
| | 3980 | getActor().setCurState(self); |
| | 3981 | } |
| | 3982 | |
| | 3983 | /* |
| | 3984 | * Show the special description for the actor when the actor is |
| | 3985 | * associated with this state. By default, we use the actor's |
| | 3986 | * actorHereDesc message, which usually shows a generic message |
| | 3987 | * (something like "Bob is here" or "Bob is sitting on the chair") to |
| | 3988 | * indicate that the actor is present. |
| | 3989 | * |
| | 3990 | * States representing scripted activities should override these to |
| | 3991 | * indicate what the actor is doing: "Bob is sweeping the floor," for |
| | 3992 | * example. |
| | 3993 | */ |
| | 3994 | specialDesc() { getActor().actorHereDesc; } |
| | 3995 | |
| | 3996 | /* show the special description for the actor at a distance */ |
| | 3997 | distantSpecialDesc() { getActor().actorThereDesc; } |
| | 3998 | |
| | 3999 | /* show the special description for the actor in a remote location */ |
| | 4000 | remoteSpecialDesc(actor) { getActor().actorThereDesc; } |
| | 4001 | |
| | 4002 | /* |
| | 4003 | * The list group(s) for the special description. By default, if |
| | 4004 | * our specialDesc isn't overridden, we'll keep this in sync with |
| | 4005 | * the specialDesc by returning our actor's actorListWith. And if |
| | 4006 | * specialDesc *is* overridden, we'll just return an empty list to |
| | 4007 | * indicate that we're not part of any list group. If you want to |
| | 4008 | * provide your own listing group special to the state, simply |
| | 4009 | * override this and speicfy the custom list group. |
| | 4010 | */ |
| | 4011 | specialDescListWith() |
| | 4012 | { |
| | 4013 | /* |
| | 4014 | * if specialDesc is inherited from ActorState, then use the |
| | 4015 | * default handling from the actor; otherwise, use no grouping at |
| | 4016 | * all by default |
| | 4017 | */ |
| | 4018 | if (!overrides(self, ActorState, &specialDesc)) |
| | 4019 | return getActor().actorListWith; |
| | 4020 | else |
| | 4021 | return []; |
| | 4022 | } |
| | 4023 | |
| | 4024 | /* show the special description when we appear in a contents listing */ |
| | 4025 | showSpecialDescInContents(actor, cont) |
| | 4026 | { |
| | 4027 | /* by default, just show our posture in our container */ |
| | 4028 | getActor().listActorPosture(actor); |
| | 4029 | } |
| | 4030 | |
| | 4031 | /* |
| | 4032 | * Our "state" description. This shows information on what the actor |
| | 4033 | * is *currently* doing; we display this after the static part of the |
| | 4034 | * actor's description on EXAMINE <ACTOR>. By default, we add |
| | 4035 | * nothing here, but state objects that represent scripted activies |
| | 4036 | * should override this to describe their scripted activities. |
| | 4037 | */ |
| | 4038 | stateDesc = "" |
| | 4039 | |
| | 4040 | /* |
| | 4041 | * Should we obey an action? If so, returns true; if not, displays |
| | 4042 | * an appropriate response and returns nil. This will only be |
| | 4043 | * called when the issuing actor is different from our actor, since |
| | 4044 | * a command to oneself is implicitly always obeyed. |
| | 4045 | */ |
| | 4046 | obeyCommand(issuingActor, action) |
| | 4047 | { |
| | 4048 | /* |
| | 4049 | * By default, we ignore all orders. We do need to generate a |
| | 4050 | * response, though, so for this purpose, treat the order as a |
| | 4051 | * conversational action, with the 'action' object as the topic. |
| | 4052 | */ |
| | 4053 | handleConversation(issuingActor, action, commandConvType); |
| | 4054 | |
| | 4055 | /* indicate that the order is refused */ |
| | 4056 | return nil; |
| | 4057 | } |
| | 4058 | |
| | 4059 | /* |
| | 4060 | * Suggest topics for the given actor to talk to us about. This is |
| | 4061 | * called when the given actor enters a TOPICS command (in which |
| | 4062 | * case 'explicit' will be true) or enters a conversation with us |
| | 4063 | * via TALK TO or the like (in which case 'explicit' will be nil). |
| | 4064 | */ |
| | 4065 | suggestTopicsFor(actor, explicit) |
| | 4066 | { |
| | 4067 | /* |
| | 4068 | * if this is not an explicit TOPICS request, and we're not in |
| | 4069 | * "auto suggest" mode, don't show anything - we don't want any |
| | 4070 | * automatic suggestions in this mode |
| | 4071 | */ |
| | 4072 | if (!explicit && !autoSuggest) |
| | 4073 | return; |
| | 4074 | |
| | 4075 | /* |
| | 4076 | * show a paragraph break, in case we're being tacked on to |
| | 4077 | * another report; but make it cosmetic, so that this by itself |
| | 4078 | * doesn't suppress a default report, in case we don't end up |
| | 4079 | * displaying any topics |
| | 4080 | */ |
| | 4081 | cosmeticSpacingReport('<.p>'); |
| | 4082 | |
| | 4083 | /* show our suggestion list */ |
| | 4084 | showSuggestedTopicList(getSuggestedTopicList(), |
| | 4085 | actor, getActor(), explicit); |
| | 4086 | } |
| | 4087 | |
| | 4088 | /* |
| | 4089 | * Get our suggested topic list. The suggested topic list consists |
| | 4090 | * of the union of the current ConvNode's suggestion list, the |
| | 4091 | * ActorState list, and the Actor's suggestion list. In each case, |
| | 4092 | * the suggestion list is the list of all SuggestedTopic objects at |
| | 4093 | * each database level. |
| | 4094 | * |
| | 4095 | * The suggestions are arranged in a hierarchy, and each hierarchy |
| | 4096 | * level can prevent suggestions from a lower level from being |
| | 4097 | * included. The top level of the hierarchy is the ConvNode; the |
| | 4098 | * next level is the ActorState; and the last level is the Actor. |
| | 4099 | * Suggestions are limited at each level with the 'limitSuggestions' |
| | 4100 | * property: if true, suggestions from lower levels are not included. |
| | 4101 | */ |
| | 4102 | getSuggestedTopicList() |
| | 4103 | { |
| | 4104 | local v = new Vector(16); |
| | 4105 | local node; |
| | 4106 | local lst; |
| | 4107 | |
| | 4108 | /* add the actor's current conversation node topics */ |
| | 4109 | if ((node = getActor().curConvNode) != nil) |
| | 4110 | { |
| | 4111 | /* if there are any suggested topics in the node, include them */ |
| | 4112 | if ((lst = node.suggestedTopics) != nil) |
| | 4113 | v.appendAll(lst); |
| | 4114 | |
| | 4115 | /* |
| | 4116 | * if this ConvNode is marked as limiting suggestions to |
| | 4117 | * those defined within the node, return what we have |
| | 4118 | * without adding anything from the broader context |
| | 4119 | */ |
| | 4120 | if (node.limitSuggestions) |
| | 4121 | return v; |
| | 4122 | } |
| | 4123 | |
| | 4124 | /* add our own topics */ |
| | 4125 | if ((lst = stateSuggestedTopics) != nil) |
| | 4126 | v.appendAll(lst); |
| | 4127 | |
| | 4128 | /* |
| | 4129 | * if the ActorState is limiting suggestions, don't include any |
| | 4130 | * suggestions from the broader context (i.e., from the Actor |
| | 4131 | * itself) |
| | 4132 | */ |
| | 4133 | if (limitSuggestions) |
| | 4134 | return v; |
| | 4135 | |
| | 4136 | /* if our actor has its own list, add those as well */ |
| | 4137 | if ((lst = getActor().suggestedTopics) != nil) |
| | 4138 | v.appendAll(lst); |
| | 4139 | |
| | 4140 | /* return the combined list */ |
| | 4141 | return v; |
| | 4142 | } |
| | 4143 | |
| | 4144 | /* |
| | 4145 | * get the topic suggestions for this state - by default, we just |
| | 4146 | * return our own suggestedTopics list |
| | 4147 | */ |
| | 4148 | stateSuggestedTopics = (suggestedTopics) |
| | 4149 | |
| | 4150 | /* |
| | 4151 | * Get my implied in-conversation state. This is used when our actor |
| | 4152 | * initiates a conversation without specifying a particular |
| | 4153 | * conversation state to enter (i.e., actor.initiateConversation() is |
| | 4154 | * called with 'state' set to nil). By default, we don't have an |
| | 4155 | * implied conversation state, so we just return 'self' to indicate |
| | 4156 | * that we want to stay in the current state. States that are |
| | 4157 | * coupled with separate in-conversation states, such as |
| | 4158 | * ConversationReadyState, should return their associated |
| | 4159 | * conversation states here. |
| | 4160 | */ |
| | 4161 | getImpliedConvState = (self) |
| | 4162 | |
| | 4163 | /* |
| | 4164 | * General conversation handler. This can be used to process most |
| | 4165 | * conversational commands - ASK, TELL, GIVE, SHOW, etc. The |
| | 4166 | * standard sequence of processing is as follows: |
| | 4167 | * |
| | 4168 | * - If our actor has a non-nil current conversation node (ConvNode) |
| | 4169 | * object, and the ConvNode wants to handle the event, let the |
| | 4170 | * ConvNode handle it. |
| | 4171 | * |
| | 4172 | * - Otherwise, check our own topic database to see if we can find a |
| | 4173 | * TopicEntry that matches the topic; if we can find one, let the |
| | 4174 | * TopicEntry handle it. |
| | 4175 | * |
| | 4176 | * - Otherwise, let the actor handle it. |
| | 4177 | * |
| | 4178 | * 'otherActor' is the actor who originated the conversation command |
| | 4179 | * (usually the player character). 'topic' is the subject being |
| | 4180 | * discussed (the indirect object of ASK ABOUT, for example). |
| | 4181 | * convType' is a ConvType describing the type of conversational |
| | 4182 | * action we're performing. |
| | 4183 | */ |
| | 4184 | handleConversation(otherActor, topic, convType) |
| | 4185 | { |
| | 4186 | local actor = getActor(); |
| | 4187 | local hasDefault; |
| | 4188 | local node; |
| | 4189 | local path; |
| | 4190 | |
| | 4191 | /* determine if I have a default response handler */ |
| | 4192 | hasDefault = propDefined(convType.defaultResponseProp); |
| | 4193 | |
| | 4194 | /* |
| | 4195 | * Figure the database search path for looking up the topics. |
| | 4196 | * We'll start in the ConvNode database, then continue to the |
| | 4197 | * ActorState database, then finally to the Actor database. |
| | 4198 | * However, we won't reach the Actor database if there's a |
| | 4199 | * default response handler in the state, because if we fail to |
| | 4200 | * find it at the state, we'll take the default. |
| | 4201 | * |
| | 4202 | * Since the path we need to provide at each point is the |
| | 4203 | * *remaining* path, don't bother including the ConvNode, since |
| | 4204 | * we'd just have to take it right back out to get the remaining |
| | 4205 | * path after the ConvNode. |
| | 4206 | */ |
| | 4207 | path = [self]; |
| | 4208 | if (!hasDefault) |
| | 4209 | path += actor; |
| | 4210 | |
| | 4211 | /* |
| | 4212 | * If our actor has a current conversation node, check to see if |
| | 4213 | * the conversation node wants to handle it. If not, check our |
| | 4214 | * own topic database, then the actor's. |
| | 4215 | */ |
| | 4216 | if ((node = actor.curConvNode) == nil |
| | 4217 | || !node.handleConversation(otherActor, topic, convType, path)) |
| | 4218 | { |
| | 4219 | /* get the remaining database search path */ |
| | 4220 | path = path.sublist(2); |
| | 4221 | |
| | 4222 | /* |
| | 4223 | * Either we don't have a ConvNode, or the ConvNode isn't |
| | 4224 | * interested in handling the operation. Check to see if we |
| | 4225 | * can handle it through our own topic database. |
| | 4226 | */ |
| | 4227 | if (!handleTopic(otherActor, topic, convType, path)) |
| | 4228 | { |
| | 4229 | /* |
| | 4230 | * We couldn't find anything in our topic database that's |
| | 4231 | * interested in handling it. Check to see if the state |
| | 4232 | * object defines the default response handler method, |
| | 4233 | * and use that as the response if so. |
| | 4234 | */ |
| | 4235 | if (hasDefault) |
| | 4236 | { |
| | 4237 | /* |
| | 4238 | * the state object (i.e., self) does define the |
| | 4239 | * default response method, so invoke that |
| | 4240 | */ |
| | 4241 | convType.defaultResponse(self, otherActor, topic); |
| | 4242 | } |
| | 4243 | else |
| | 4244 | { |
| | 4245 | /* |
| | 4246 | * We don't have a topic database entry and we don't |
| | 4247 | * have our own definition of the default response |
| | 4248 | * handler. All that remains is to let our actor |
| | 4249 | * handle it. |
| | 4250 | */ |
| | 4251 | actor.handleConversation(otherActor, topic, convType); |
| | 4252 | } |
| | 4253 | } |
| | 4254 | } |
| | 4255 | |
| | 4256 | /* whatever happened, run the appropriate after-response handling */ |
| | 4257 | convType.afterResponse(actor, otherActor); |
| | 4258 | } |
| | 4259 | |
| | 4260 | /* |
| | 4261 | * Receive notification that a TopicEntry is being used (via its |
| | 4262 | * handleTopic method) to respond to a command. The TopicEntry will |
| | 4263 | * call this before it shows its message or takes any other action. |
| | 4264 | * By default, we do nothing. |
| | 4265 | */ |
| | 4266 | notifyTopicResponse(fromActor, entry) { } |
| | 4267 | |
| | 4268 | /* |
| | 4269 | * Handle a before-action notification for our actor. By default, |
| | 4270 | * we do nothing. |
| | 4271 | */ |
| | 4272 | beforeAction() |
| | 4273 | { |
| | 4274 | /* do nothing by default */ |
| | 4275 | } |
| | 4276 | |
| | 4277 | /* handle an after-action notification for our actor */ |
| | 4278 | afterAction() |
| | 4279 | { |
| | 4280 | } |
| | 4281 | |
| | 4282 | /* handle a before-travel notification */ |
| | 4283 | beforeTravel(traveler, connector) |
| | 4284 | { |
| | 4285 | local other = getActor().getCurrentInterlocutor(); |
| | 4286 | |
| | 4287 | /* |
| | 4288 | * if our conversational partner is departing, break off the |
| | 4289 | * conversation |
| | 4290 | */ |
| | 4291 | if (connector != nil |
| | 4292 | && other != nil |
| | 4293 | && traveler.isActorTraveling(other)) |
| | 4294 | { |
| | 4295 | /* end the conversation */ |
| | 4296 | if (!endConversation(gActor, endConvTravel)) |
| | 4297 | { |
| | 4298 | /* |
| | 4299 | * they don't want to allow the conversation to end, so |
| | 4300 | * abort the travel action |
| | 4301 | */ |
| | 4302 | exit; |
| | 4303 | } |
| | 4304 | } |
| | 4305 | } |
| | 4306 | |
| | 4307 | /* handle an after-travel notification */ |
| | 4308 | afterTravel(traveler, connector) |
| | 4309 | { |
| | 4310 | } |
| | 4311 | |
| | 4312 | /* |
| | 4313 | * End the current conversation. 'reason' indicates why we're |
| | 4314 | * leaving the conversation - this is one of the endConvXxx enums |
| | 4315 | * defined in adv3.h. beforeTravel() calls this automatically when |
| | 4316 | * the other party is trying to depart, and they're talking to us. |
| | 4317 | * |
| | 4318 | * This returns true if we wish to allow the conversation to end, |
| | 4319 | * nil if not. |
| | 4320 | */ |
| | 4321 | endConversation(actor, reason) |
| | 4322 | { |
| | 4323 | local ourActor = getActor(); |
| | 4324 | local node; |
| | 4325 | |
| | 4326 | /* tell the current ConvNode about it */ |
| | 4327 | if ((node = ourActor.curConvNode) != nil) |
| | 4328 | { |
| | 4329 | local ret; |
| | 4330 | |
| | 4331 | /* the can-end call might show a response, so set our actor */ |
| | 4332 | conversationManager.beginResponse(ourActor); |
| | 4333 | |
| | 4334 | /* ask the node if it's okay to end the conversation */ |
| | 4335 | ret = node.canEndConversation(actor, reason); |
| | 4336 | |
| | 4337 | /* |
| | 4338 | * If the result is blockEndConv, it means that the actor |
| | 4339 | * said something to force the conversation to keep going. |
| | 4340 | * Make a note that the other actor already said something on |
| | 4341 | * this turn so that we don't generate another scripted |
| | 4342 | * message later, and flag this as preventing the |
| | 4343 | * conversation ending. |
| | 4344 | */ |
| | 4345 | if (ret == blockEndConv) |
| | 4346 | { |
| | 4347 | /* flag that the other actor said something this turn */ |
| | 4348 | ourActor.noteConvAction(actor); |
| | 4349 | |
| | 4350 | /* we're unable to end the conversation now */ |
| | 4351 | ret = nil; |
| | 4352 | } |
| | 4353 | |
| | 4354 | /* end the response, leaving the node unchanged by default */ |
| | 4355 | conversationManager.finishResponse( |
| | 4356 | ourActor, ourActor.curConvNode); |
| | 4357 | |
| | 4358 | /* |
| | 4359 | * if the node said no, tell the caller we can't end the |
| | 4360 | * conversation right now |
| | 4361 | */ |
| | 4362 | if (!ret) |
| | 4363 | return nil; |
| | 4364 | |
| | 4365 | /* tell the node we are indeed ending the conversation */ |
| | 4366 | node.endConversation(actor, reason); |
| | 4367 | } |
| | 4368 | |
| | 4369 | /* forget any conversation tree position */ |
| | 4370 | ourActor.setConvNodeReason(nil, 'endConversation'); |
| | 4371 | |
| | 4372 | /* indicate that we are allowing the conversation to end */ |
| | 4373 | return true; |
| | 4374 | } |
| | 4375 | |
| | 4376 | /* |
| | 4377 | * Take a turn. This is called when it's the actor's turn and |
| | 4378 | * there's not something else the actor needs to be doing (such as |
| | 4379 | * following another actor, or carrying out a command in the actor's |
| | 4380 | * pending command queue). |
| | 4381 | * |
| | 4382 | * By default, we perform several steps automatically. |
| | 4383 | * |
| | 4384 | * First, we check to see if the actor is in a ConvNode. If so, the |
| | 4385 | * ConvNode takes precedence. If we haven't been addressed already |
| | 4386 | * in conversation on this turn, we'll let the ConvNode perform its |
| | 4387 | * "continuation," which lets the NPC advance the conversation of its |
| | 4388 | * own volition. In any case, if we have a current ConvNode, we're |
| | 4389 | * done with the turn, since we assume the actor will want to proceed |
| | 4390 | * with the conversation before pursuing its agenda or performing a |
| | 4391 | * background action. |
| | 4392 | * |
| | 4393 | * Second, assuming there's no active ConvNode, we check for an |
| | 4394 | * "agenda" item that's ready to execute. If we find one, we execute |
| | 4395 | * it, and we're done. The agenda item takes precedence over any |
| | 4396 | * other scripting we might have. |
| | 4397 | * |
| | 4398 | * Finally, if we also inherit from Script, and we didn't find an |
| | 4399 | * active ConvNode or an agenda item that was ready to execute, we |
| | 4400 | * invoke our doScript() method. This makes it especially easy to |
| | 4401 | * define random background messages for the actor - just add an |
| | 4402 | * EventList class (ShuffledEventList is usually the right one) to |
| | 4403 | * the state's superclass list, and define a list of background |
| | 4404 | * message strings. |
| | 4405 | */ |
| | 4406 | takeTurn() |
| | 4407 | { |
| | 4408 | local actor = getActor(); |
| | 4409 | |
| | 4410 | /* |
| | 4411 | * Check to see if we want to continue a conversation. If so, |
| | 4412 | * and we haven't already conversed this turn, try the |
| | 4413 | * continuing conversation. If that displays anything, consider |
| | 4414 | * the turn done. |
| | 4415 | * |
| | 4416 | * Otherwise, try executing an agenda item. If we do, consider |
| | 4417 | * the turn done. |
| | 4418 | * |
| | 4419 | * Otherwise, if we're of class Script, execute our scripted |
| | 4420 | * action. |
| | 4421 | */ |
| | 4422 | if (actor.curConvNode != nil |
| | 4423 | && !actor.conversedThisTurn() |
| | 4424 | && actor.curConvNode.npcContinueConversation()) |
| | 4425 | { |
| | 4426 | /* |
| | 4427 | * we displayed an NPC-motivated conversation continuation, |
| | 4428 | * so we're done with this turn |
| | 4429 | */ |
| | 4430 | } |
| | 4431 | else if (actor.executeAgenda()) |
| | 4432 | { |
| | 4433 | /* we executed an agenda item, so we need do nothing more */ |
| | 4434 | } |
| | 4435 | else if (ofKind(Script)) |
| | 4436 | { |
| | 4437 | /* we're a Script, so invoke our scripted action */ |
| | 4438 | doScript(); |
| | 4439 | } |
| | 4440 | } |
| | 4441 | |
| | 4442 | /* |
| | 4443 | * Receive notification that we just followed another actor as part |
| | 4444 | * of our programmed following behavior (in other words, due to our |
| | 4445 | * 'followingActor' property, not due to an explicit FOLLOW command |
| | 4446 | * directed to us). 'success' is true if we ended up in the actor's |
| | 4447 | * location, nil if not. |
| | 4448 | * |
| | 4449 | * This can be used to update the actor's state after a 'follow' |
| | 4450 | * operation occurs; for example, if the actor's state depends on |
| | 4451 | * the actor's location, this can update the state accordingly. We |
| | 4452 | * don't do anything by default. |
| | 4453 | */ |
| | 4454 | justFollowed(success) |
| | 4455 | { |
| | 4456 | /* do nothing by default */ |
| | 4457 | } |
| | 4458 | |
| | 4459 | /* |
| | 4460 | * Our group-travel arrival description. By default, when we perform |
| | 4461 | * an accompanying travel with another actor as the lead actor, the |
| | 4462 | * accompanying travel state will display this message instead of our |
| | 4463 | * specialDesc when the lead actor first arrives in the new location. |
| | 4464 | * We'll just display our own specialDesc by default, but this should |
| | 4465 | * usually be overridden to say something specific to the group |
| | 4466 | * travel arrival. The actual message is entirely dependent on the |
| | 4467 | * nature of the group travel, which is why we don't provide a |
| | 4468 | * special message by default. |
| | 4469 | * |
| | 4470 | * For scripted behavior, it's sometimes better to use arrivingTurn() |
| | 4471 | * rather than this method to describe the behavior. |
| | 4472 | * arrivingWithDesc() is called as part of the room description, so |
| | 4473 | * it's best for any message shown here to fit well into the usual |
| | 4474 | * room description format. For more complex transitions into the |
| | 4475 | * new room state, arrivingTurn() is sometimes more appropriate, |
| | 4476 | * since it runs like a daemon, after the arrival (and thus the new |
| | 4477 | * room description) is completed. |
| | 4478 | */ |
| | 4479 | arrivingWithDesc() { specialDesc(); } |
| | 4480 | |
| | 4481 | /* |
| | 4482 | * Perform any special action on a group-travel arrival. When group |
| | 4483 | * travel is performed using the AccompanyingInTravelState class, |
| | 4484 | * this is essentially called in lieu of the regular takeTurn() |
| | 4485 | * method on the state that is coming into effect after the group |
| | 4486 | * travel. (Not really, but effectively: the accompanying travel |
| | 4487 | * state will still be in effect, so its takeTurn() method is what's |
| | 4488 | * really called, but that method will call this method explicitly.) |
| | 4489 | * By default, we do nothing. Since this runs on our turn, it's a |
| | 4490 | * good place to put any scripted behavior we perform on arriving at |
| | 4491 | * our new destination after the group travel. |
| | 4492 | */ |
| | 4493 | arrivingTurn() { } |
| | 4494 | |
| | 4495 | /* |
| | 4496 | * For our TravelMessageHandler implementation, the nominal traveler |
| | 4497 | * is our actor. Note that this is all we need to implement for |
| | 4498 | * travel message handling, since we simply inherit the default |
| | 4499 | * handling for all of the arrival/departure messages. |
| | 4500 | */ |
| | 4501 | getNominalTraveler() { return getActor(); } |
| | 4502 | ; |
| | 4503 | |
| | 4504 | /* |
| | 4505 | * A "ready for conversation" state. This can be used as the base class |
| | 4506 | * for actor states when the actor is receptive to conversation, and we |
| | 4507 | * want to have the sense of a conversational context. The key feature |
| | 4508 | * that this class provides is the ability to provide messages when |
| | 4509 | * engaging and disengaging the conversation. |
| | 4510 | * |
| | 4511 | * Note that this state is NOT required for conversation, since the basic |
| | 4512 | * ActorState object accepts conversational commands like ASK, TELL, |
| | 4513 | * GIVE, and TAKE. The special feature of the "conversation ready" state |
| | 4514 | * is that we explicitly move the actor to a separate state when |
| | 4515 | * conversation begins. This is especially appropriate for states in |
| | 4516 | * which the NPC is actively carrying on some other activity; the |
| | 4517 | * conversation should interrupt those states, so that the actor stops |
| | 4518 | * the other activity and gives us its full attention. |
| | 4519 | * |
| | 4520 | * This type of state can be associated with its in-conversation state |
| | 4521 | * object in one of two ways. First, the inConvState property can be |
| | 4522 | * explicitly set to point to the in-conversation state object. Second, |
| | 4523 | * this object can be nested inside its in-conversation state object via |
| | 4524 | * the 'location' property (so you can use the '+' syntax to put this |
| | 4525 | * object inside its in-conversation state object). The 'ready' object |
| | 4526 | * goes inside the 'conversing' object because a single 'conversing' |
| | 4527 | * object can frequently be shared among several 'ready' states. |
| | 4528 | */ |
| | 4529 | class ConversationReadyState: ActorState |
| | 4530 | /* |
| | 4531 | * The associated in-conversation state. This should be set to an |
| | 4532 | * InConversationState object that controls the actor's behavior |
| | 4533 | * while carrying on a conversation. Note that the library will |
| | 4534 | * automatically set this if the instance is nested (via its |
| | 4535 | * 'location' property) inside an InConversationState object. |
| | 4536 | */ |
| | 4537 | inConvState = nil |
| | 4538 | |
| | 4539 | /* my implied conversational state is my in-conversation state */ |
| | 4540 | getImpliedConvState = (inConvState) |
| | 4541 | |
| | 4542 | /* |
| | 4543 | * Show our greeting message. If 'explicit' is true, it means that |
| | 4544 | * the player character is greeting us through an explicit greeting |
| | 4545 | * command, such as HELLO or TALK TO. Otherwise, the greeting is |
| | 4546 | * implied by some other conversational action, such a ASK ABOUT or |
| | 4547 | * SHOW TO. We do nothing by default; this should be overridden in |
| | 4548 | * most cases to show some sort of exchange of pleasantries - |
| | 4549 | * something like this: |
| | 4550 | * |
| | 4551 | *. >bob, hello |
| | 4552 | *. "Hi, there," you say. |
| | 4553 | * |
| | 4554 | * Bob looks up over his newspaper. "Oh, hello," he says, putting |
| | 4555 | * down the paper. "What can I do for you?" |
| | 4556 | * |
| | 4557 | * Note that games shouldn't usually override this method. Instead, |
| | 4558 | * you should simply create a HelloTopic entry and put it inside the |
| | 4559 | * state object; we'll find the HelloTopic and show its message as |
| | 4560 | * our greeting. |
| | 4561 | * |
| | 4562 | * If you want to distinguish between explicit and implicit |
| | 4563 | * greetings, you can create an ImpHelloTopic entry for implied |
| | 4564 | * greetings (i.e., the kind of greeting that occurs automatically |
| | 4565 | * when the player jumps right into a conversation with our actor |
| | 4566 | * using ASK ABOUT or the like, without explicitly saying HELLO |
| | 4567 | * first). The regular HelloTopic will handle explicit greetings, |
| | 4568 | * and the ImpHelloTopic will handle the implied kind. |
| | 4569 | */ |
| | 4570 | showGreetingMsg(actor, explicit) |
| | 4571 | { |
| | 4572 | /* look for a HelloTopic in our topic database */ |
| | 4573 | if (handleTopic(actor, explicit ? helloTopicObj : impHelloTopicObj, |
| | 4574 | helloConvType, nil)) |
| | 4575 | "<.p>"; |
| | 4576 | } |
| | 4577 | |
| | 4578 | /* |
| | 4579 | * Enter this state from a conversation. This should show any |
| | 4580 | * message we want to display when we're ending a conversation and |
| | 4581 | * switching from the conversation to this state. 'reason' is the |
| | 4582 | * endConvXxx enum indicating what triggered the termination of the |
| | 4583 | * conversation. 'oldNode' is the ConvNode we were in just before we |
| | 4584 | * initiated the termination - we need this information because we |
| | 4585 | * want to look in the ConvNode for a Bye topic message to display, |
| | 4586 | * but we can't just look in the actor for the node because it will |
| | 4587 | * already have been cleared out by the time we get here. |
| | 4588 | * |
| | 4589 | * Games shouldn't normally override this method. Instead, simply |
| | 4590 | * create a ByeTopic entry and put it inside the state object; we'll |
| | 4591 | * find the ByeTopic and show its message for the goodbye. |
| | 4592 | * |
| | 4593 | * If you want to distinguish between different types of goodbyes, |
| | 4594 | * you can create an ImpByeTopic for any implied goodbye (i.e., the |
| | 4595 | * kind where the other actor just walks away, or where we get bored |
| | 4596 | * of the other actor ignoring us). You can also further |
| | 4597 | * differentiate by creating BoredByeTopic and/or LeaveByeTopic |
| | 4598 | * objects to handle just those cases. The regular ByeTopic will |
| | 4599 | * handle explicit GOODBYE commands, and the others (ImpByeTopic, |
| | 4600 | * BoredByeTopic, LeaveByeTopic) will handle the implied kinds. |
| | 4601 | */ |
| | 4602 | enterFromConversation(actor, reason, oldNode) |
| | 4603 | { |
| | 4604 | local topic; |
| | 4605 | local reasonMap = [endConvBye, byeTopicObj, |
| | 4606 | endConvTravel, leaveByeTopicObj, |
| | 4607 | endConvBoredom, boredByeTopicObj, |
| | 4608 | endConvActor, actorByeTopicObj]; |
| | 4609 | |
| | 4610 | /* figure out which topic object we need, based on the reason code */ |
| | 4611 | topic = reasonMap[reasonMap.indexOf(reason) + 1]; |
| | 4612 | |
| | 4613 | /* |
| | 4614 | * Look for a ByeTopic in the ConvNode; failing that, try our own |
| | 4615 | * database. |
| | 4616 | */ |
| | 4617 | if (oldNode == nil |
| | 4618 | || !oldNode.handleConversation(actor, topic, byeConvType, nil)) |
| | 4619 | { |
| | 4620 | /* there's no node handler; try our own database */ |
| | 4621 | handleTopic(actor, topic, byeConvType, nil); |
| | 4622 | } |
| | 4623 | } |
| | 4624 | |
| | 4625 | /* handle a conversational action directed to our actor */ |
| | 4626 | handleConversation(otherActor, topic, convType) |
| | 4627 | { |
| | 4628 | /* |
| | 4629 | * If this is a greeting, handle it ourselves. Otherwise, pass |
| | 4630 | * it along to our associated in-conversation state. |
| | 4631 | */ |
| | 4632 | if (convType == helloConvType) |
| | 4633 | { |
| | 4634 | /* |
| | 4635 | * Switch to our associated in-conversation state and show a |
| | 4636 | * greeting. Since we're explicitly entering the |
| | 4637 | * conversation, we have no topic entry. |
| | 4638 | */ |
| | 4639 | enterConversation(otherActor, nil); |
| | 4640 | |
| | 4641 | /* show or schedule a topic inventory, as appropriate */ |
| | 4642 | conversationManager.showOrScheduleTopicInventory( |
| | 4643 | getActor(), otherActor); |
| | 4644 | } |
| | 4645 | else |
| | 4646 | { |
| | 4647 | /* |
| | 4648 | * it's not a greeting, so pass it to our in-conversation |
| | 4649 | * state for handling |
| | 4650 | */ |
| | 4651 | inConvState.handleConversation(otherActor, topic, convType); |
| | 4652 | } |
| | 4653 | } |
| | 4654 | |
| | 4655 | /* |
| | 4656 | * Initiate conversation based on the given simulation object. This |
| | 4657 | * is an internal method that isn't usually called directly from game |
| | 4658 | * code; game code usually calls the Actor's initiateTopic(), which |
| | 4659 | * calls this routine to check for a topic that's part of the state |
| | 4660 | * object. |
| | 4661 | */ |
| | 4662 | initiateTopic(obj) |
| | 4663 | { |
| | 4664 | /* defer to our in-conversation state */ |
| | 4665 | return inConvState.initiateTopic(obj); |
| | 4666 | } |
| | 4667 | |
| | 4668 | /* |
| | 4669 | * Receive notification that a TopicEntry is being used (via its |
| | 4670 | * handleTopic method) to respond to a command. If the TopicEntry is |
| | 4671 | * conversational, automatically enter our in-conversation state. |
| | 4672 | */ |
| | 4673 | notifyTopicResponse(fromActor, entry) |
| | 4674 | { |
| | 4675 | if (entry.isConversational) |
| | 4676 | enterConversation(fromActor, entry); |
| | 4677 | } |
| | 4678 | |
| | 4679 | /* |
| | 4680 | * Enter a conversation with the given actor, either explicitly (via |
| | 4681 | * HELLO or TALK TO) or implicitly (by directly asking a question, |
| | 4682 | * etc). 'entry' gives the TopicEntry that's triggering the implicit |
| | 4683 | * conversation entry; if this is nil, it means that we're being |
| | 4684 | * triggered explicitly. |
| | 4685 | */ |
| | 4686 | enterConversation(actor, entry) |
| | 4687 | { |
| | 4688 | local myActor = getActor(); |
| | 4689 | local explicit = (entry == nil); |
| | 4690 | |
| | 4691 | /* if the actor can't talk to us, we can't enter the conversation */ |
| | 4692 | if (!actor.canTalkTo(myActor)) |
| | 4693 | { |
| | 4694 | /* tell them we can't talk now */ |
| | 4695 | reportFailure(&objCannotHearActorMsg, myActor); |
| | 4696 | |
| | 4697 | /* terminate the command */ |
| | 4698 | exit; |
| | 4699 | } |
| | 4700 | |
| | 4701 | /* |
| | 4702 | * Show our greeting, if desired. We show a greeting if we're |
| | 4703 | * being invoked explicitly (that is, there's no TopicEntry), or |
| | 4704 | * if we're being invoked explicitly and the TopicEntry implies a |
| | 4705 | * greeting. |
| | 4706 | */ |
| | 4707 | if (explicit || entry.impliesGreeting) |
| | 4708 | showGreetingMsg(actor, explicit); |
| | 4709 | |
| | 4710 | /* activate the in-conversation state */ |
| | 4711 | myActor.setCurState(inConvState); |
| | 4712 | } |
| | 4713 | |
| | 4714 | /* |
| | 4715 | * Get this state's suggested topic list. ConversationReady states |
| | 4716 | * shouldn't normally have topic entries of their own, since a |
| | 4717 | * ConvversationReady state usually forwards conversation handling |
| | 4718 | * to its corresponding in-conversation state. So, simply return |
| | 4719 | * the suggestion list from our in-conversation state object. |
| | 4720 | */ |
| | 4721 | stateSuggestedTopics = (inConvState.suggestedTopics) |
| | 4722 | |
| | 4723 | /* initialize the actor state object */ |
| | 4724 | initializeActorState() |
| | 4725 | { |
| | 4726 | /* inherit the default handling */ |
| | 4727 | inherited(); |
| | 4728 | |
| | 4729 | /* |
| | 4730 | * if we're nested inside an in-conversation state object, the |
| | 4731 | * containing in-conversation state is the one we'll use for |
| | 4732 | * conversations |
| | 4733 | */ |
| | 4734 | if (location.ofKind(InConversationState)) |
| | 4735 | inConvState = location; |
| | 4736 | } |
| | 4737 | ; |
| | 4738 | |
| | 4739 | /* |
| | 4740 | * The "in-conversation" state. This works with ConversationReadyState |
| | 4741 | * to handle transitions in and out of conversations. In this state, we |
| | 4742 | * are actively engaged in a conversation. |
| | 4743 | * |
| | 4744 | * Throughout this implementation, we assume that we only care about |
| | 4745 | * conversations with a single character, specifically the player |
| | 4746 | * character. There's generally no good reason to fully model |
| | 4747 | * conversations between NPC's, since that kind of NPC activity is in |
| | 4748 | * most cases purely pre-scripted and thus requires no special state |
| | 4749 | * tracking. Since we generally only need to worry about tracking a |
| | 4750 | * conversation with the player character, we don't bother with the |
| | 4751 | * possibility that we're simultaneously in conversation with more than |
| | 4752 | * one other character. |
| | 4753 | */ |
| | 4754 | class InConversationState: ActorState |
| | 4755 | /* |
| | 4756 | * Our attention span, in turns. This is the number of turns that |
| | 4757 | * we'll be willing to stay in the conversation while the other |
| | 4758 | * character is ignoring us. After the conversation has been idle |
| | 4759 | * this long, we'll assume the other actor is no longer talking to |
| | 4760 | * us, so we'll terminate the conversation ourselves. |
| | 4761 | * |
| | 4762 | * If the NPC's doesn't have a limited attention span, set this |
| | 4763 | * property to nil. This will prevent the NPC from ever disengaging |
| | 4764 | * of its own volition. |
| | 4765 | */ |
| | 4766 | attentionSpan = 4 |
| | 4767 | |
| | 4768 | /* |
| | 4769 | * The state to switch to when the conversation ends. Instances can |
| | 4770 | * override this to select the next state. By default, we'll return |
| | 4771 | * to the state that we were in immediately before the conversation |
| | 4772 | * started. |
| | 4773 | */ |
| | 4774 | nextState = (previousState) |
| | 4775 | |
| | 4776 | /* |
| | 4777 | * End the current conversation. 'reason' indicates why we're |
| | 4778 | * leaving the conversation - this is one of the endConvXxx enums |
| | 4779 | * defined in adv3.h. |
| | 4780 | * |
| | 4781 | * This method is a convenience only; you aren't required to call |
| | 4782 | * this method to end the conversation, since you can simply switch |
| | 4783 | * to another actor state directly if you prefer. This method's |
| | 4784 | * main purpose is to display an appropriate message terminating the |
| | 4785 | * conversation while switching to the new state. If you want to |
| | 4786 | * display your own message directly from the code that's changing |
| | 4787 | * the state, there's no reason to call this. |
| | 4788 | * |
| | 4789 | * This returns true if we wish to allow the conversation to end, |
| | 4790 | * nil if not. |
| | 4791 | */ |
| | 4792 | endConversation(actor, reason) |
| | 4793 | { |
| | 4794 | local nxt; |
| | 4795 | local myActor = getActor(); |
| | 4796 | |
| | 4797 | /* |
| | 4798 | * note the current ConvNode for our actor - when we check with |
| | 4799 | * the ConvNode to see about ending the conversation, this will |
| | 4800 | * automatically exit the ConvNode, so we need to save this first |
| | 4801 | * so that we can refer to it later to check for a Bye topic |
| | 4802 | */ |
| | 4803 | local oldNode = myActor.curConvNode; |
| | 4804 | |
| | 4805 | /* |
| | 4806 | * Inherit the base behavior first - if it disallows the action, |
| | 4807 | * return failure. The inherited version will check with the |
| | 4808 | * current ConvNode to see if has any objection. |
| | 4809 | */ |
| | 4810 | if (!inherited(actor, reason)) |
| | 4811 | return nil; |
| | 4812 | |
| | 4813 | /* get the next state */ |
| | 4814 | nxt = nextState; |
| | 4815 | |
| | 4816 | /* if there isn't one, stay in the actor's current state */ |
| | 4817 | if (nxt == nil) |
| | 4818 | nxt = myActor.curState; |
| | 4819 | |
| | 4820 | /* |
| | 4821 | * If the next state is a 'conversation ready' state, tell it |
| | 4822 | * we're entering from a conversation. We're ending the |
| | 4823 | * conversation explicitly only if 'reason' is endConvBye. Pass |
| | 4824 | * along the ConvNode we just exited (if any), so that we can |
| | 4825 | * look for a response in the node. |
| | 4826 | */ |
| | 4827 | if (nxt.ofKind(ConversationReadyState)) |
| | 4828 | nxt.enterFromConversation(actor, reason, oldNode); |
| | 4829 | |
| | 4830 | /* switch our actor to the next state */ |
| | 4831 | myActor.setCurState(nxt); |
| | 4832 | |
| | 4833 | /* indicate that we are allowing the conversation to end */ |
| | 4834 | return true; |
| | 4835 | } |
| | 4836 | |
| | 4837 | /* handle a conversational command */ |
| | 4838 | handleConversation(otherActor, topic, convType) |
| | 4839 | { |
| | 4840 | /* handle goodbyes specially */ |
| | 4841 | if (convType == byeConvType) |
| | 4842 | { |
| | 4843 | /* |
| | 4844 | * If this is an implicit goodbye, run the normal |
| | 4845 | * conversation handling in order to display any implied |
| | 4846 | * ByeTopic message - but capture the output in case we |
| | 4847 | * decide not to end the conversation after all. Only do |
| | 4848 | * this in the case of an implicit goodbye, though - for an |
| | 4849 | * explicit goodbye, there's no need for this as the explicit |
| | 4850 | * BYE will do the same thing on its own. |
| | 4851 | */ |
| | 4852 | local txt = nil; |
| | 4853 | if (topic != byeTopicObj) |
| | 4854 | { |
| | 4855 | txt = mainOutputStream.captureOutput( |
| | 4856 | {: inherited(otherActor, topic, convType) }); |
| | 4857 | } |
| | 4858 | |
| | 4859 | /* |
| | 4860 | * try to end the conversation; if we won't allow it, |
| | 4861 | * terminate the action here |
| | 4862 | */ |
| | 4863 | if (!endConversation(otherActor, endConvBye)) |
| | 4864 | exit; |
| | 4865 | |
| | 4866 | /* show the captured ByeTopic output */ |
| | 4867 | if (txt != nil) |
| | 4868 | say(txt); |
| | 4869 | } |
| | 4870 | else |
| | 4871 | { |
| | 4872 | /* use the inherited handling */ |
| | 4873 | inherited(otherActor, topic, convType); |
| | 4874 | } |
| | 4875 | } |
| | 4876 | |
| | 4877 | /* |
| | 4878 | * provide a default HELLO response, if we don't have a special |
| | 4879 | * TopicEntry for it |
| | 4880 | */ |
| | 4881 | defaultGreetingResponse(actor) |
| | 4882 | { |
| | 4883 | /* |
| | 4884 | * As our default response, point out that we're already at the |
| | 4885 | * actor's service. (This isn't an error, because the other |
| | 4886 | * actor might not have been talking to us, even though we |
| | 4887 | * thought we were talking to them.) |
| | 4888 | */ |
| | 4889 | gLibMessages.alreadyTalkingTo(getActor(), actor); |
| | 4890 | } |
| | 4891 | |
| | 4892 | takeTurn() |
| | 4893 | { |
| | 4894 | local actor = getActor(); |
| | 4895 | |
| | 4896 | /* if we didn't interact this turn, increment our boredom counter */ |
| | 4897 | if (!actor.conversedThisTurn()) |
| | 4898 | actor.boredomCount++; |
| | 4899 | |
| | 4900 | /* run the inherited handling */ |
| | 4901 | inherited(); |
| | 4902 | } |
| | 4903 | |
| | 4904 | /* activate this state */ |
| | 4905 | activateState(actor, oldState) |
| | 4906 | { |
| | 4907 | /* |
| | 4908 | * If the previous state was a ConversationReadyState, or we |
| | 4909 | * have no other state remembered, remember the previous state - |
| | 4910 | * this is the default we'll return to at the end of the |
| | 4911 | * conversation, if the instance doesn't specify another state. |
| | 4912 | * |
| | 4913 | * We don't remember prior states that aren't conv-ready states |
| | 4914 | * to make it easier to temporarily interrupt a conversation |
| | 4915 | * with some other state, and later return to the conversation. |
| | 4916 | * If we remembered every prior state, then we'd return to the |
| | 4917 | * interrupting state when the conversation ended, which is |
| | 4918 | * usually not what's wanted. Usually, we want to return to the |
| | 4919 | * last conv-ready state when a conversation ends, ignoring any |
| | 4920 | * other intermediate states that have been active since the |
| | 4921 | * conv-ready state was last in effect. |
| | 4922 | */ |
| | 4923 | if (previousState == nil || oldState.ofKind(ConversationReadyState)) |
| | 4924 | previousState = oldState; |
| | 4925 | |
| | 4926 | /* |
| | 4927 | * reset the actor's boredom counter, since we're just starting a |
| | 4928 | * new conversation, and add our boredom agenda item to the |
| | 4929 | * active list to monitor our boredom level |
| | 4930 | */ |
| | 4931 | actor.boredomCount = 0; |
| | 4932 | actor.addToAgenda(actor.boredomAgendaItem); |
| | 4933 | |
| | 4934 | /* remember the time of the last conversation command */ |
| | 4935 | actor.lastConvTime = Schedulable.gameClockTime; |
| | 4936 | } |
| | 4937 | |
| | 4938 | /* deactivate this state */ |
| | 4939 | deactivateState(actor, newState) |
| | 4940 | { |
| | 4941 | /* |
| | 4942 | * we're leaving the conversation state, so there's no need to |
| | 4943 | * monitor our boredom level any longer |
| | 4944 | */ |
| | 4945 | actor.removeFromAgenda(actor.boredomAgendaItem); |
| | 4946 | |
| | 4947 | /* do the normal work */ |
| | 4948 | inherited(actor, newState); |
| | 4949 | } |
| | 4950 | |
| | 4951 | /* |
| | 4952 | * The previous state - this is the state we were in before the |
| | 4953 | * conversation began, and the one we'll return to by default when |
| | 4954 | * the conversation ends. We'll set this automatically on |
| | 4955 | * activation. |
| | 4956 | */ |
| | 4957 | previousState = nil |
| | 4958 | ; |
| | 4959 | |
| | 4960 | /* |
| | 4961 | * A special kind of agenda item for monitoring "boredom" during a |
| | 4962 | * conversation. We check to see if our actor is in a conversation, and |
| | 4963 | * the PC has been ignoring the conversation for too long; if so, our |
| | 4964 | * actor initiates the end of the conversation, since the PC apparently |
| | 4965 | * isn't paying any attention to us. |
| | 4966 | */ |
| | 4967 | class BoredomAgendaItem: AgendaItem |
| | 4968 | /* we construct these dynamically during actor initialization */ |
| | 4969 | construct(actor) |
| | 4970 | { |
| | 4971 | /* remember our actor as our location */ |
| | 4972 | location = actor; |
| | 4973 | } |
| | 4974 | |
| | 4975 | /* |
| | 4976 | * we're ready to run if our actor is in an InConversationState and |
| | 4977 | * its boredom count has reached the limit for the state |
| | 4978 | */ |
| | 4979 | isReady() |
| | 4980 | { |
| | 4981 | local actor = getActor(); |
| | 4982 | local state = actor.curState; |
| | 4983 | |
| | 4984 | return (inherited() |
| | 4985 | && state.ofKind(InConversationState) |
| | 4986 | && state.attentionSpan != nil |
| | 4987 | && actor.boredomCount >= state.attentionSpan); |
| | 4988 | } |
| | 4989 | |
| | 4990 | /* on invocation, end the conversation */ |
| | 4991 | invokeItem() |
| | 4992 | { |
| | 4993 | local actor = getActor(); |
| | 4994 | local state = actor.curState; |
| | 4995 | |
| | 4996 | /* tell the state to end the conversation */ |
| | 4997 | state.endConversation(actor.getCurrentInterlocutor(), endConvBoredom); |
| | 4998 | } |
| | 4999 | |
| | 5000 | /* |
| | 5001 | * by default, handle boredom before other agenda items - we do this |
| | 5002 | * because an ongoing conversation will be the first thing on the |
| | 5003 | * NPC's mind |
| | 5004 | */ |
| | 5005 | agendaOrder = 50 |
| | 5006 | ; |
| | 5007 | |
| | 5008 | |
| | 5009 | /* |
| | 5010 | * A "hermit" actor state is a state where the actor is unresponsive to |
| | 5011 | * conversational overtures (ASK ABOUT, TELL ABOUT, HELLO, GOODBYE, YES, |
| | 5012 | * NO, SHOW TO, GIVE TO, and any orders directed to the actor). Any |
| | 5013 | * attempt at conversation will be met with the 'noResponse' message. |
| | 5014 | */ |
| | 5015 | class HermitActorState: ActorState |
| | 5016 | /* |
| | 5017 | * Show our response to any conversational command. We'll simply |
| | 5018 | * show the standard "there's no response" message by default, but |
| | 5019 | * subclasses can (and usually should) override this to explain |
| | 5020 | * what's really going on. Note that this routine will be invoked |
| | 5021 | * for any sort of conversation command, so any override needs to be |
| | 5022 | * generic enough that it's equally good for ASK, TELL, and |
| | 5023 | * everything else. |
| | 5024 | * |
| | 5025 | * Note that it's fairly easy to create a shuffled list of random |
| | 5026 | * messages, if you want to add some variety to the actor's |
| | 5027 | * responses. To do this, use an embedded ShuffledEventList: |
| | 5028 | * |
| | 5029 | * myState: HermitActorState |
| | 5030 | *. noResponse() { myList.doScript(); } |
| | 5031 | *. myList: ShuffledEventList { |
| | 5032 | *. ['message1', 'message2', 'message3'] } |
| | 5033 | *. ; |
| | 5034 | */ |
| | 5035 | noResponse() { mainReport(&noResponseFromMsg, getActor()); } |
| | 5036 | |
| | 5037 | /* all conversation actions get the same default response */ |
| | 5038 | handleConversation(otherActor, topic, convType) |
| | 5039 | { |
| | 5040 | /* just show our standard default response */ |
| | 5041 | noResponse(); |
| | 5042 | } |
| | 5043 | |
| | 5044 | /* |
| | 5045 | * Since the hermit state blocks topics from outside the state, don't |
| | 5046 | * offer suggestions for other topics while in this state. |
| | 5047 | * |
| | 5048 | * Note that you might sometimes want to override this to allow the |
| | 5049 | * usual topic suggestions (by setting this to nil). In particular: |
| | 5050 | * |
| | 5051 | * - If it's not outwardly obvious that the actor is unresponsive, |
| | 5052 | * you'll probably want to allow suggestions. Remember, TOPICS |
| | 5053 | * suggests topics that the *PC* wants to talk about, not things the |
| | 5054 | * NPC is interested in. If the PC doesn't necessarily know that the |
| | 5055 | * NPC won't respond, the PC would still want to ask about those |
| | 5056 | * topics. |
| | 5057 | * |
| | 5058 | * - If the hermit state is to be short-lived, you might want to show |
| | 5059 | * the topic suggestions even in the hermit state, so that the player |
| | 5060 | * is aware that there are still useful topics to explore with the |
| | 5061 | * NPC. The player might otherwise assume that the NPC is out of |
| | 5062 | * useful topics, and not bother trying again later when the NPC |
| | 5063 | * becomes more responsive. |
| | 5064 | */ |
| | 5065 | limitSuggestions = true |
| | 5066 | ; |
| | 5067 | |
| | 5068 | /* |
| | 5069 | * The basic "accompanying" state. In this state, whenever the actor |
| | 5070 | * we're accompanying travels to a location we want to follow, we'll |
| | 5071 | * travel at the same time with the other actor. |
| | 5072 | */ |
| | 5073 | class AccompanyingState: ActorState |
| | 5074 | /* |
| | 5075 | * Check to see if we are to accompany the given traveler on the |
| | 5076 | * given travel. 'traveler' is the Traveler performing the travel, |
| | 5077 | * and 'conn' is the connector that the traveler is about to take. |
| | 5078 | * |
| | 5079 | * Note that 'traveler' is a Traveler object. This will simply be an |
| | 5080 | * Actor (which is a kind of Traveler) when the actor is performing |
| | 5081 | * the travel directly, but it could also be another kind of |
| | 5082 | * Traveler, such as a Vehicle. This routine must determine whether |
| | 5083 | * to accompany other kinds of actors. |
| | 5084 | * |
| | 5085 | * By default, we'll return true to indicate that we want to |
| | 5086 | * accompany any traveler anywhere they go. This should almost |
| | 5087 | * always be overridden in practice to be more specific. |
| | 5088 | */ |
| | 5089 | accompanyTravel(traveler, conn) { return true; } |
| | 5090 | |
| | 5091 | /* |
| | 5092 | * Get our accompanying state object. We'll create a basic |
| | 5093 | * accompanying in-travel state object, returning to the current |
| | 5094 | * state when we're done. 'traveler' is the Traveler object that's |
| | 5095 | * performing the travel; this might be an Actor, but could also be a |
| | 5096 | * Vehicle or other Traveler subclass. |
| | 5097 | */ |
| | 5098 | getAccompanyingTravelState(traveler, connector) |
| | 5099 | { |
| | 5100 | /* |
| | 5101 | * Create the default intermediate state for the travel. Note |
| | 5102 | * that the lead actor is the actor performing the command - this |
| | 5103 | * won't necessarily be the traveler, since the actor could be |
| | 5104 | * steering a vehicle. |
| | 5105 | */ |
| | 5106 | return new AccompanyingInTravelState( |
| | 5107 | getActor(), gActor, getActor().curState); |
| | 5108 | } |
| | 5109 | |
| | 5110 | /* |
| | 5111 | * handle a before-travel notification for my actor |
| | 5112 | */ |
| | 5113 | beforeTravel(traveler, connector) |
| | 5114 | { |
| | 5115 | /* |
| | 5116 | * If we want to accompany the given traveler on this travel, add |
| | 5117 | * ourselves to the initiating actor's list of accompanying |
| | 5118 | * actors. Never set an actor to accompany itself, since doing |
| | 5119 | * so would lead to infinite recursion. |
| | 5120 | */ |
| | 5121 | if (accompanyTravel(traveler, connector) && getActor() != gActor) |
| | 5122 | { |
| | 5123 | /* |
| | 5124 | * Add me to the list of actors accompanying the actor |
| | 5125 | * initiating the travel - that actor will run a nested |
| | 5126 | * travel action on us before doing its own travel. Note |
| | 5127 | * that the initiating actor is gActor, since that's the |
| | 5128 | * actor performing the action that led to the travel. |
| | 5129 | */ |
| | 5130 | gActor.addAccompanyingActor(getActor()); |
| | 5131 | |
| | 5132 | /* put my actor into the appropriate new group travel state */ |
| | 5133 | getActor().setCurState( |
| | 5134 | getAccompanyingTravelState(traveler, connector)); |
| | 5135 | } |
| | 5136 | |
| | 5137 | /* inherit the default handling */ |
| | 5138 | inherited(traveler, connector); |
| | 5139 | } |
| | 5140 | ; |
| | 5141 | |
| | 5142 | /* |
| | 5143 | * "Accompanying in-travel" state - this is an actor state used when an |
| | 5144 | * actor is taking part in a group travel operation. This state lasts |
| | 5145 | * only as long as the single turn - which belongs to the lead actor - |
| | 5146 | * that it takes to carry out the group travel. Once our turn comes |
| | 5147 | * around, we'll restore the actor to the previous state - or, we can set |
| | 5148 | * the actor to a different state, if desired. Setting the actor to a |
| | 5149 | * different state is useful when the group travel triggers a new |
| | 5150 | * scripted activity in the new room. |
| | 5151 | */ |
| | 5152 | class AccompanyingInTravelState: ActorState |
| | 5153 | construct(actor, lead, next) |
| | 5154 | { |
| | 5155 | /* do the normal initialization */ |
| | 5156 | inherited(actor); |
| | 5157 | |
| | 5158 | /* remember the lead actor and the next state */ |
| | 5159 | leadActor = lead; |
| | 5160 | nextState = next; |
| | 5161 | } |
| | 5162 | |
| | 5163 | /* the lead actor of the group travel */ |
| | 5164 | leadActor = nil |
| | 5165 | |
| | 5166 | /* |
| | 5167 | * the next state - we'll switch our actor to this state after the |
| | 5168 | * travel has been completed |
| | 5169 | */ |
| | 5170 | nextState = nil |
| | 5171 | |
| | 5172 | /* |
| | 5173 | * Show our "I am here" description. By default, we'll use the |
| | 5174 | * arrivingWithDesc of the *next* state object. |
| | 5175 | */ |
| | 5176 | specialDesc() { nextState.arrivingWithDesc; } |
| | 5177 | |
| | 5178 | /* take our turn */ |
| | 5179 | takeTurn() |
| | 5180 | { |
| | 5181 | /* |
| | 5182 | * The group travel only takes the single turn in which the |
| | 5183 | * travel is initiated, so by the time our turn comes around, the |
| | 5184 | * group travel is done. Clear out the lead actor's linkage to |
| | 5185 | * us as an accompanying actor. |
| | 5186 | */ |
| | 5187 | leadActor.accompanyingActors.removeElement(getActor()); |
| | 5188 | |
| | 5189 | /* switch our actor to the next state */ |
| | 5190 | getActor().setCurState(nextState); |
| | 5191 | |
| | 5192 | /* |
| | 5193 | * call our next state's on-arrival turn-taking method, so that |
| | 5194 | * it can carry out any desired scripted behavior for our arrival |
| | 5195 | */ |
| | 5196 | nextState.arrivingTurn(); |
| | 5197 | } |
| | 5198 | |
| | 5199 | /* initiate a topic - defer to the next state */ |
| | 5200 | initiateTopic(obj) { return nextState.initiateTopic(obj); } |
| | 5201 | |
| | 5202 | /* |
| | 5203 | * Override our departure messages. When we're accompanying another |
| | 5204 | * actor on a group travel, the lead actor will, as part of its turn, |
| | 5205 | * send each accompanying actor (including us) on ahead. This means |
| | 5206 | * that the lead actor will see us departing from the starting |
| | 5207 | * location, because we'll leave before the lead actor has itself |
| | 5208 | * departed. Rather than using the normal "Bob leaves to the west" |
| | 5209 | * departure report, customize the departure reports to indicate |
| | 5210 | * specifically that we're going with the lead actor. (Note that we |
| | 5211 | * only have to handle the departing messages, since group travel |
| | 5212 | * always sends accompanying actors on ahead of the main actor, hence |
| | 5213 | * the accompanying actors will always be seen departing, not |
| | 5214 | * arriving.) |
| | 5215 | * |
| | 5216 | * Note that all of these call our generic sayDeparting() method by |
| | 5217 | * default, so a subclass can catch all of the departure types at |
| | 5218 | * once just by overriding sayDeparting(). Overriding the individual |
| | 5219 | * methods is still desirable, of course, if you want separate |
| | 5220 | * messages for the different departure types. |
| | 5221 | */ |
| | 5222 | sayDeparting(conn) |
| | 5223 | { gLibMessages.sayDepartingWith(getActor(), leadActor); } |
| | 5224 | sayDepartingDir(dir, conn) { sayDeparting(conn); } |
| | 5225 | sayDepartingThroughPassage(conn) { sayDeparting(conn); } |
| | 5226 | sayDepartingViaPath(conn) { sayDeparting(conn); } |
| | 5227 | sayDepartingUpStairs(conn) { sayDeparting(conn); } |
| | 5228 | sayDepartingDownStairs(conn) { sayDeparting(conn); } |
| | 5229 | |
| | 5230 | /* |
| | 5231 | * Describe local travel using our standard departure message as |
| | 5232 | * well. This is used to describe our travel when our origin and |
| | 5233 | * destination locations are both visible to the PC; in these cases, |
| | 5234 | * we don't describe the departure separately because the whole |
| | 5235 | * process of travel from departure to arrival is visible to the PC |
| | 5236 | * and thus is best handled with a single message, which we generate |
| | 5237 | * here. In our case, since the "accompanying" state describes even |
| | 5238 | * normal travel as though it were visible all along, we can use our |
| | 5239 | * standard "departing" message to describe local travel as well. |
| | 5240 | */ |
| | 5241 | sayArrivingLocally(dest, conn) { sayDeparting(conn); } |
| | 5242 | sayDepartingLocally(dest, conn) { sayDeparting(conn); } |
| | 5243 | ; |
| | 5244 | |
| | 5245 | /* ------------------------------------------------------------------------ */ |
| | 5246 | /* |
| | 5247 | * A pending conversation information object. An Actor keeps a list of |
| | 5248 | * these for pending conversations. |
| | 5249 | */ |
| | 5250 | class PendingConvInfo: object |
| | 5251 | construct(state, node, turns) |
| | 5252 | { |
| | 5253 | /* remember how to start the conversation */ |
| | 5254 | state_ = state; |
| | 5255 | node_ = node; |
| | 5256 | |
| | 5257 | /* compute the game clock time when we can start the conversation */ |
| | 5258 | time_ = Schedulable.gameClockTime + turns; |
| | 5259 | } |
| | 5260 | |
| | 5261 | /* |
| | 5262 | * our ActorState and ConvNode (or ConvNode name string), describing |
| | 5263 | * how we're to start the conversation |
| | 5264 | */ |
| | 5265 | state_ = nil |
| | 5266 | node_ = nil |
| | 5267 | |
| | 5268 | /* the minimum game clock time at which we can start the conversation */ |
| | 5269 | time_ = nil |
| | 5270 | ; |
| | 5271 | |
| | 5272 | /* ------------------------------------------------------------------------ */ |
| | 5273 | /* |
| | 5274 | * An "agenda item." Each actor can have its own "agenda," which is a |
| | 5275 | * list of these items. Each item represents an action that the actor |
| | 5276 | * wants to perform - this is usually a goal the actor wants to achieve, |
| | 5277 | * or a conversational topic the actor wants to pursue. |
| | 5278 | * |
| | 5279 | * On any given turn, an actor can carry out only one agenda item. |
| | 5280 | * |
| | 5281 | * Agenda items are a convenient way of controlling complex behavior. |
| | 5282 | * Each agenda item defines its own condition for when the actor can |
| | 5283 | * pursue the item, and each item defines what the actor does when |
| | 5284 | * pursuing the item. Agenda items can improve the code structure for an |
| | 5285 | * NPC's behavior, since they nicely isolate a single background action |
| | 5286 | * and group it with the conditions that trigger it. But the main |
| | 5287 | * benefit of agenda items is the one-per-turn pacing - by executing at |
| | 5288 | * most one agenda item per turn, we ensure that the NPC will carry out |
| | 5289 | * its self-initiated actions at a measured pace, rather than as a jumble |
| | 5290 | * of random actions on a single turn. |
| | 5291 | * |
| | 5292 | * Note that NPC-initiated conversation messages override agendas. If an |
| | 5293 | * actor has an active ConvNode, AND the ConvNode displays a |
| | 5294 | * "continuation message" on a given turn, then the actor will not pursue |
| | 5295 | * its agenda on that turn. In this way, ConvNode continuation messages |
| | 5296 | * act rather like high-priority agenda items. |
| | 5297 | */ |
| | 5298 | class AgendaItem: object |
| | 5299 | /* |
| | 5300 | * My actor - agenda items should be nested within the actor using |
| | 5301 | * '+' so that we can find our actor. Note that this doesn't add the |
| | 5302 | * item to the actor's agenda - that has to be done explicitly with |
| | 5303 | * actor.addToAgenda(). |
| | 5304 | */ |
| | 5305 | getActor() { return location; } |
| | 5306 | |
| | 5307 | /* |
| | 5308 | * Is this item active at the start of the game? Override this to |
| | 5309 | * true to make the item initially active; we'll add it to the |
| | 5310 | * actor's agenda during the game's initialization. |
| | 5311 | */ |
| | 5312 | initiallyActive = nil |
| | 5313 | |
| | 5314 | /* |
| | 5315 | * Is this item ready to execute? The actor will only execute an |
| | 5316 | * agenda item when this condition is met. By default, we're ready |
| | 5317 | * to execute. Items can override this to provide a declarative |
| | 5318 | * condition of readiness if desired. |
| | 5319 | */ |
| | 5320 | isReady = true |
| | 5321 | |
| | 5322 | /* |
| | 5323 | * Is this item done? On each turn, we'll remove any items marked as |
| | 5324 | * done from the actor's agenda list. We remove items marked as done |
| | 5325 | * before executing any items, so done-ness overrides readiness; in |
| | 5326 | * other words, if an item is both 'done' and 'ready', it'll simply |
| | 5327 | * be removed from the list and will not be executed. |
| | 5328 | * |
| | 5329 | * By default, we simply return nil. Items can override this to |
| | 5330 | * provide a declarative condition of done-ness, or they can simply |
| | 5331 | * set the property to true when they finish their work. For |
| | 5332 | * example, an item that only needs to execute once can simply set |
| | 5333 | * isDone to true in its invokeItem() method; an item that's to be |
| | 5334 | * repeated until some success condition obtains can override isDone |
| | 5335 | * to return the success condition. |
| | 5336 | */ |
| | 5337 | isDone = nil |
| | 5338 | |
| | 5339 | /* |
| | 5340 | * The ordering of the item relative to other agenda items. When we |
| | 5341 | * choose an agenda item to execute, we always choose the lowest |
| | 5342 | * numbered item that's ready to run. You can leave this with the |
| | 5343 | * default value if you don't care about the order. |
| | 5344 | */ |
| | 5345 | agendaOrder = 100 |
| | 5346 | |
| | 5347 | /* |
| | 5348 | * Execute this item. This is invoked during the actor's turn when |
| | 5349 | * the item is the first item that's ready to execute in the actor's |
| | 5350 | * agenda list. We do nothing by default. |
| | 5351 | */ |
| | 5352 | invokeItem() { } |
| | 5353 | |
| | 5354 | /* |
| | 5355 | * Reset the item. This is invoked whenever the item is added to an |
| | 5356 | * actor's agenda. By default, we'll set isDone to nil as long as |
| | 5357 | * isDone isn't a method; this makes it easier to reuse agenda |
| | 5358 | * items, since we don't have to worry about clearing out the isDone |
| | 5359 | * flag when reusing an item. |
| | 5360 | */ |
| | 5361 | resetItem() |
| | 5362 | { |
| | 5363 | /* if isDone isn't a method, reset it to nil */ |
| | 5364 | if (propType(&isDone) != TypeCode) |
| | 5365 | isDone = nil; |
| | 5366 | } |
| | 5367 | ; |
| | 5368 | |
| | 5369 | /* |
| | 5370 | * An AgendaItem initializer. For each agenda item that's initially |
| | 5371 | * active, we'll add the item to its actor's agenda. |
| | 5372 | */ |
| | 5373 | PreinitObject |
| | 5374 | execute() |
| | 5375 | { |
| | 5376 | forEachInstance(AgendaItem, new function(item) { |
| | 5377 | /* |
| | 5378 | * If this item is initially active, add the item to its |
| | 5379 | * actor's agenda. |
| | 5380 | */ |
| | 5381 | if (item.initiallyActive) |
| | 5382 | item.getActor().addToAgenda(item); |
| | 5383 | }); |
| | 5384 | } |
| | 5385 | ; |
| | 5386 | |
| | 5387 | /* |
| | 5388 | * A "conversational" agenda item. This type of item is ready to execute |
| | 5389 | * only when the actor hasn't engaged in conversation during the same |
| | 5390 | * turn. This type of item is ideal for situations where we want the |
| | 5391 | * actor to pursue a conversational topic, because we won't initiate the |
| | 5392 | * action until we get a turn where the player didn't directly talk to |
| | 5393 | * us. |
| | 5394 | */ |
| | 5395 | class ConvAgendaItem: AgendaItem |
| | 5396 | isReady = (!getActor().conversedThisTurn() |
| | 5397 | && getActor().canTalkTo(otherActor) |
| | 5398 | && inherited()) |
| | 5399 | |
| | 5400 | /* |
| | 5401 | * The actor we're planning to address - by default, this is the PC. |
| | 5402 | * If the conversational overture will be directed to another NPC, |
| | 5403 | * you can specify that other actor here. |
| | 5404 | */ |
| | 5405 | otherActor = (gPlayerChar) |
| | 5406 | ; |
| | 5407 | |
| | 5408 | /* |
| | 5409 | * A delayed agenda item. This type of item becomes ready to execute |
| | 5410 | * when the game clock reaches a given turn counter. |
| | 5411 | */ |
| | 5412 | class DelayedAgendaItem: AgendaItem |
| | 5413 | /* we're ready if the game clock time has reached our ready time */ |
| | 5414 | isReady = (Schedulable.gameClockTime >= readyTime && inherited()) |
| | 5415 | |
| | 5416 | /* the turn counter on the game clock when we become ready */ |
| | 5417 | readyTime = 0 |
| | 5418 | |
| | 5419 | /* |
| | 5420 | * Set our ready time based on a delay from the current time. We'll |
| | 5421 | * become ready after the given number of turns elapses. For |
| | 5422 | * convenience, we return 'self', so a delayed agenda item can be |
| | 5423 | * initialized and added to an actor's agenda in one simple |
| | 5424 | * operation, like so: |
| | 5425 | * |
| | 5426 | * actor.addToAgenda(item.setDelay(1)); |
| | 5427 | */ |
| | 5428 | setDelay(turns) |
| | 5429 | { |
| | 5430 | /* |
| | 5431 | * initialize our ready time as the given number of turns in the |
| | 5432 | * future from the current game clock time |
| | 5433 | */ |
| | 5434 | readyTime = Schedulable.gameClockTime + turns; |
| | 5435 | |
| | 5436 | /* return 'self' for the caller's convenience */ |
| | 5437 | return self; |
| | 5438 | } |
| | 5439 | ; |
| | 5440 | |
| | 5441 | |
| | 5442 | /* ------------------------------------------------------------------------ */ |
| | 5443 | /* |
| | 5444 | * An Actor is a living person, animal, or other entity with a will of |
| | 5445 | * its own. Actors can usually be addressed with targeted commands |
| | 5446 | * ("bob, go north"), and with commands like ASK ABOUT, TELL ABOUT, GIVE |
| | 5447 | * TO, and SHOW TO. |
| | 5448 | * |
| | 5449 | * Note that, by default, an Actor can be picked up and moved with |
| | 5450 | * commands like TAKE, PUT IN, and so on. This is suitable for some |
| | 5451 | * kinds of actors but not for others: it might make sense with a cat or |
| | 5452 | * a small dog, but not with a bank guard or an orc. For an actor that |
| | 5453 | * can't be taken, use the UntakeableActor or one of its subclasses. |
| | 5454 | * |
| | 5455 | * An actor's contents are the things the actor is carrying or wearing. |
| | 5456 | */ |
| | 5457 | class Actor: Thing, Schedulable, Traveler, ActorTopicDatabase |
| | 5458 | /* flag: we're an actor */ |
| | 5459 | isActor = true |
| | 5460 | |
| | 5461 | /* |
| | 5462 | * Our current state. This is an ActorState object representing what |
| | 5463 | * we're currently doing. Whenever the actor changes to a new state |
| | 5464 | * (for example, because of a scripted activity), this can be changed |
| | 5465 | * to reflect the actor's new state. The state object groups the |
| | 5466 | * parts of the actor's description and other methods that tend to |
| | 5467 | * vary according to what the actor's doing; it's easier to keep |
| | 5468 | * everything related to scripted activities together in a state |
| | 5469 | * object than it is to handle all of the variability with switch() |
| | 5470 | * statements of the like in methods directly in the actor. |
| | 5471 | * |
| | 5472 | * It's not necessary to initialize this if the actor doesn't take |
| | 5473 | * advantage of the ActorState mechanism. If this isn't initialized |
| | 5474 | * for a particular actor, we'll automatically create a default |
| | 5475 | * ActorState object during pre-initialization. |
| | 5476 | */ |
| | 5477 | curState = nil |
| | 5478 | |
| | 5479 | /* set the current state */ |
| | 5480 | setCurState(state) |
| | 5481 | { |
| | 5482 | /* if this isn't a change of state, there's nothing to do */ |
| | 5483 | if (state == curState) |
| | 5484 | return; |
| | 5485 | |
| | 5486 | /* if we have a previous state, tell it it's becoming inactive */ |
| | 5487 | if (curState != nil) |
| | 5488 | curState.deactivateState(self, state); |
| | 5489 | |
| | 5490 | /* notify the new state it's becoming active */ |
| | 5491 | if (state != nil) |
| | 5492 | state.activateState(self, curState); |
| | 5493 | |
| | 5494 | /* remember the new state */ |
| | 5495 | curState = state; |
| | 5496 | } |
| | 5497 | |
| | 5498 | /* |
| | 5499 | * Our current conversation node. This is a ConvNode object that |
| | 5500 | * keeps track of the flow of the conversation. |
| | 5501 | */ |
| | 5502 | curConvNode = nil |
| | 5503 | |
| | 5504 | /* |
| | 5505 | * Our table of conversation nodes. At initialization, the |
| | 5506 | * conversation manager scans all ConvNode instances and adds each |
| | 5507 | * one to its actor's table. This table is keyed by the name of |
| | 5508 | * node, and the value for each entry is the ConvNode object - this |
| | 5509 | * lets us look up the ConvNode object by name. Because each actor |
| | 5510 | * has its own lookup table, ConvNode names only have to be unique |
| | 5511 | * within the actor's set of ConvNodes. |
| | 5512 | */ |
| | 5513 | convNodeTab = perInstance(new LookupTable(32, 32)) |
| | 5514 | |
| | 5515 | /* set the current conversation node */ |
| | 5516 | setConvNode(node) { setConvNodeReason(node, nil); } |
| | 5517 | |
| | 5518 | /* set the current conversation node, with a reason code */ |
| | 5519 | setConvNodeReason(node, reason) |
| | 5520 | { |
| | 5521 | /* remember the old node */ |
| | 5522 | local oldNode = curConvNode; |
| | 5523 | |
| | 5524 | /* if the node was specified by name, look up the object */ |
| | 5525 | if (dataType(node) == TypeSString) |
| | 5526 | node = convNodeTab[node]; |
| | 5527 | |
| | 5528 | /* remember the new node */ |
| | 5529 | curConvNode = node; |
| | 5530 | |
| | 5531 | /* |
| | 5532 | * If we're changing to a new node, notify the new and old |
| | 5533 | * nodes. Note that these notifications occur after the new |
| | 5534 | * node has been set, which ensures that any further node change |
| | 5535 | * triggered by the node change won't redundantly issue the same |
| | 5536 | * notifications: since the old node is no longer active, it |
| | 5537 | * can't receive another departure notification, and since the |
| | 5538 | * new node is already active, it can't receive another |
| | 5539 | * activation. |
| | 5540 | */ |
| | 5541 | if (node != oldNode) |
| | 5542 | { |
| | 5543 | /* if there's an old node, note that we're leaving it */ |
| | 5544 | if (oldNode != nil) |
| | 5545 | oldNode.noteLeaving(); |
| | 5546 | |
| | 5547 | /* let the node know that it's becoming active */ |
| | 5548 | if (node != nil) |
| | 5549 | node.noteActiveReason(reason); |
| | 5550 | } |
| | 5551 | |
| | 5552 | /* |
| | 5553 | * note that we've explicitly set a ConvNode (even if it's not |
| | 5554 | * actually changing), in case the conversation manager is |
| | 5555 | * tracking what's happening during a response |
| | 5556 | */ |
| | 5557 | responseSetConvNode = true; |
| | 5558 | } |
| | 5559 | |
| | 5560 | /* |
| | 5561 | * conversation manager ID - this is assigned by the conversation |
| | 5562 | * manager to map to and from output stream references to the actor; |
| | 5563 | * this is only for internal use by the conversation manager |
| | 5564 | */ |
| | 5565 | convMgrID = nil |
| | 5566 | |
| | 5567 | /* |
| | 5568 | * Flag indicating whether or not we've set a ConvNode in the course |
| | 5569 | * of the current response. This is for use by the converstaion |
| | 5570 | * manager. |
| | 5571 | */ |
| | 5572 | responseSetConvNode = nil |
| | 5573 | |
| | 5574 | /* |
| | 5575 | * Initiate a conversation with the player character. This lets the |
| | 5576 | * NPC initiate a conversation, in response to something the player |
| | 5577 | * character does, or as part of the NPC's scripted activity. This |
| | 5578 | * is only be used for situations where the NPC initiates the |
| | 5579 | * conversation - if the player character initiates conversation with |
| | 5580 | * TALK TO, ASK, TELL, etc., we handle the conversation through our |
| | 5581 | * normal handlers for those commands. |
| | 5582 | * |
| | 5583 | * 'state' is the ActorState to switch to for the conversation. This |
| | 5584 | * will normally be an InConversationState object, but doesn't have |
| | 5585 | * to be. |
| | 5586 | * |
| | 5587 | * You can pass nil for 'state' to use the current state's implied |
| | 5588 | * conversational state. The implied conversational state of a |
| | 5589 | * ConversationReadyState is the associated InConversationState; the |
| | 5590 | * implied conversation state of any other state is simply the same |
| | 5591 | * state. |
| | 5592 | * |
| | 5593 | * 'node' is a ConvNode object, or a string naming a ConvNode object. |
| | 5594 | * We'll make this our current conversation node. A valid |
| | 5595 | * conversation node is required because we use this to generate the |
| | 5596 | * initial NPC greeting of the conversation. In most cases, when the |
| | 5597 | * NPC initiates a conversation, it's because the NPC wants to ask a |
| | 5598 | * question or otherwise say something specific, so there should |
| | 5599 | * always be a conversational context implied, thus the need for a |
| | 5600 | * ConvNode. If there's no need for a conversational context, the |
| | 5601 | * NPC script code might just as well display the conversational |
| | 5602 | * exchange as a plain old message, and not bother going to all this |
| | 5603 | * trouble. |
| | 5604 | */ |
| | 5605 | initiateConversation(state, node) |
| | 5606 | { |
| | 5607 | /* |
| | 5608 | * if there's no state provided, use the current state's implied |
| | 5609 | * conversation state |
| | 5610 | */ |
| | 5611 | if (state == nil) |
| | 5612 | state = curState.getImpliedConvState; |
| | 5613 | |
| | 5614 | /* |
| | 5615 | * if there's an ActorHelloTopic for the old state, invoke it to |
| | 5616 | * show the greeting |
| | 5617 | */ |
| | 5618 | curState.handleTopic(self, actorHelloTopicObj, helloConvType, nil); |
| | 5619 | |
| | 5620 | /* switch to the new state, if it's not the current state */ |
| | 5621 | if (state != nil && state != curState) |
| | 5622 | setCurState(state); |
| | 5623 | |
| | 5624 | /* we're now talking to the player character */ |
| | 5625 | noteConversation(gPlayerChar); |
| | 5626 | |
| | 5627 | /* switch to the conversation node */ |
| | 5628 | setConvNodeReason(node, 'initiateConversation'); |
| | 5629 | |
| | 5630 | /* tell the conversation node that the NPC is initiating it */ |
| | 5631 | if (node != nil) |
| | 5632 | curConvNode.npcInitiateConversation(); |
| | 5633 | } |
| | 5634 | |
| | 5635 | /* |
| | 5636 | * Initiate a conversation based on the given simulation object. |
| | 5637 | * We'll look for an InitiateTopic matching the given object, and if |
| | 5638 | * we can find one, we'll show its topic response. |
| | 5639 | */ |
| | 5640 | initiateTopic(obj) |
| | 5641 | { |
| | 5642 | /* try our current state first */ |
| | 5643 | if (curState.initiateTopic(obj)) |
| | 5644 | return true; |
| | 5645 | |
| | 5646 | /* we didn't find a state object; use the default handling */ |
| | 5647 | return inherited(obj); |
| | 5648 | } |
| | 5649 | |
| | 5650 | /* |
| | 5651 | * Schedule initiation of conversation. This allows the caller to |
| | 5652 | * set up a conversation to start on a future turn. The |
| | 5653 | * conversation will start after (1) the given number of turns has |
| | 5654 | * elapsed, and (2) the player didn't target this actor with a |
| | 5655 | * conversational command on the same turn. This allows us to set |
| | 5656 | * the NPC so that it *wants* to start a conversation, and will do |
| | 5657 | * so as soon as it has a chance to get a word in. |
| | 5658 | * |
| | 5659 | * If 'turns' is zero, the conversation can start the next time the |
| | 5660 | * actor takes a turn; so, if this is called during the PC's action |
| | 5661 | * processing, the conversation can start on the same turn. Note |
| | 5662 | * that if this is called during the actor's takeTurn() processing, |
| | 5663 | * it won't actually start the conversation until the next turn, |
| | 5664 | * because that's the next time we'll check the queue. If 'turns' |
| | 5665 | * is 1, then the player will get at least one more command before |
| | 5666 | * the conversation will begin, and so on with higher numbers. |
| | 5667 | */ |
| | 5668 | scheduleInitiateConversation(state, node, turns) |
| | 5669 | { |
| | 5670 | /* add a new pending conversation to our list */ |
| | 5671 | pendingConv.append(new PendingConvInfo(state, node, turns)); |
| | 5672 | } |
| | 5673 | |
| | 5674 | /* |
| | 5675 | * Break off our current conversation, of the NPC's own volition. |
| | 5676 | * This is the opposite number of initiateConversation: this causes |
| | 5677 | * the NPC to effectively say BYE on its own, rather than waiting |
| | 5678 | * for the PC to decide to end the conversation. |
| | 5679 | * |
| | 5680 | * This call is mostly useful when the actor's current state is an |
| | 5681 | * InConversationState, since the main function of this routine is |
| | 5682 | * to switch to an out-of-conversation state. |
| | 5683 | */ |
| | 5684 | endConversation() |
| | 5685 | { |
| | 5686 | /* |
| | 5687 | * tell the current state to end the conversation of the NPC's |
| | 5688 | * own volition |
| | 5689 | */ |
| | 5690 | curState.endConversation(self, endConvActor); |
| | 5691 | } |
| | 5692 | |
| | 5693 | /* |
| | 5694 | * Our list of pending conversation initiators. In our takeTurn() |
| | 5695 | * processing, we'll check this list for conversations that we can |
| | 5696 | * initiate. |
| | 5697 | */ |
| | 5698 | pendingConv = nil |
| | 5699 | |
| | 5700 | /* |
| | 5701 | * Hide actors from 'all' by default. The kinds of actions that |
| | 5702 | * normally apply to 'all' and the kinds that normally apply to |
| | 5703 | * actors have pretty low overlap. |
| | 5704 | * |
| | 5705 | * If a particular actor looks a lot like an inanimate object, it |
| | 5706 | * might want to override this to participate in 'all' for most or |
| | 5707 | * all actions. |
| | 5708 | */ |
| | 5709 | hideFromAll(action) { return true; } |
| | 5710 | |
| | 5711 | /* |
| | 5712 | * don't hide actors from defaulting, though - it's frequently |
| | 5713 | * convenient and appropriate to assume an actor by default, |
| | 5714 | * especially for commands like GIVE TO and SHOW TO |
| | 5715 | */ |
| | 5716 | hideFromDefault(action) { return nil; } |
| | 5717 | |
| | 5718 | /* |
| | 5719 | * We meet the objHeld precondition for ourself - that is, for any |
| | 5720 | * verb that requires holding an object, we can be considered to be |
| | 5721 | * holding ourself. |
| | 5722 | */ |
| | 5723 | meetsObjHeld(actor) { return actor == self || inherited(actor); } |
| | 5724 | |
| | 5725 | /* |
| | 5726 | * Actors are not listed with the ordinary objects in a room's |
| | 5727 | * description. However, an actor is listed as part of an inventory |
| | 5728 | * description. |
| | 5729 | */ |
| | 5730 | isListed = nil |
| | 5731 | isListedInContents = nil |
| | 5732 | isListedInInventory = true |
| | 5733 | |
| | 5734 | /* the contents of an actor aren't listed in a room's description */ |
| | 5735 | contentsListed = nil |
| | 5736 | |
| | 5737 | /* |
| | 5738 | * Full description. By default, we'll show either the pcDesc or |
| | 5739 | * npcDesc, depending on whether we're the current player character |
| | 5740 | * or a non-player character. |
| | 5741 | * |
| | 5742 | * Generally, individual actors should NOT override this method. |
| | 5743 | * Instead, customize pcDesc and/or npcDesc to describe the permanent |
| | 5744 | * features of the actor. |
| | 5745 | */ |
| | 5746 | desc |
| | 5747 | { |
| | 5748 | /* |
| | 5749 | * show the appropriate messages, depending on whether we're the |
| | 5750 | * player character or a non-player character |
| | 5751 | */ |
| | 5752 | if (isPlayerChar()) |
| | 5753 | { |
| | 5754 | /* show our as-player-character description */ |
| | 5755 | pcDesc; |
| | 5756 | } |
| | 5757 | else |
| | 5758 | { |
| | 5759 | /* show our as-non-player-character description */ |
| | 5760 | npcDesc; |
| | 5761 | } |
| | 5762 | } |
| | 5763 | |
| | 5764 | /* show our status */ |
| | 5765 | examineStatus() |
| | 5766 | { |
| | 5767 | /* |
| | 5768 | * If I'm an NPC, show where I'm sitting/standing/etc. (If I'm |
| | 5769 | * the PC, we don't usually want to show this explicitly to avoid |
| | 5770 | * redundancy. The player is usually sufficiently aware of the |
| | 5771 | * PC's posture by virtue of being in control of the actor, and |
| | 5772 | * the information also tends to show up often enough in other |
| | 5773 | * places, such as on the status line and in the room |
| | 5774 | * description.) |
| | 5775 | */ |
| | 5776 | if (!isPlayerChar()) |
| | 5777 | postureDesc; |
| | 5778 | |
| | 5779 | /* show the status from our state object */ |
| | 5780 | curState.stateDesc; |
| | 5781 | |
| | 5782 | /* inherit the default handling to show our contents */ |
| | 5783 | inherited(); |
| | 5784 | } |
| | 5785 | |
| | 5786 | /* |
| | 5787 | * Show my posture, as part of the full EXAMINE description of this |
| | 5788 | * actor. We'll let our nominal actor container handle it. |
| | 5789 | */ |
| | 5790 | postureDesc() { descViaActorContainer(&roomActorPostureDesc, nil); } |
| | 5791 | |
| | 5792 | /* |
| | 5793 | * The default description when we examine this actor and the actor |
| | 5794 | * is serving as the player character. This should generally not |
| | 5795 | * include any temporary status information; just show constant, |
| | 5796 | * fixed features. |
| | 5797 | */ |
| | 5798 | pcDesc { gLibMessages.pcDesc(self); } |
| | 5799 | |
| | 5800 | /* |
| | 5801 | * Show the description of this actor when this actor is a non-player |
| | 5802 | * character. |
| | 5803 | * |
| | 5804 | * This description should include only the constant, fixed |
| | 5805 | * description of the character. Do not include information on what |
| | 5806 | * the actor is doing right now, because that belongs in the |
| | 5807 | * ActorState object instead. When we display the actor's |
| | 5808 | * description, we'll show this text, and then we'll show the |
| | 5809 | * ActorState description as well; this combination approach makes it |
| | 5810 | * easier to keep the description synchronized with any scripted |
| | 5811 | * activities the actor is performing. |
| | 5812 | * |
| | 5813 | * By default, we'll show this as a "default descriptive report," |
| | 5814 | * since it simply says that there's nothing special to say. |
| | 5815 | * However, whenever this is overridden with an actual description, |
| | 5816 | * you shouldn't bother to use defaultDescReport - simply display the |
| | 5817 | * descriptive message directly: |
| | 5818 | * |
| | 5819 | * npcDesc = "He's wearing a gorilla costume. " |
| | 5820 | */ |
| | 5821 | npcDesc { defaultDescReport(&npcDescMsg, self); } |
| | 5822 | |
| | 5823 | /* examine my contents specially */ |
| | 5824 | examineListContents() |
| | 5825 | { |
| | 5826 | /* if I'm not the player character, show my inventory */ |
| | 5827 | if (!isPlayerChar()) |
| | 5828 | holdingDesc; |
| | 5829 | } |
| | 5830 | |
| | 5831 | /* |
| | 5832 | * Always list actors specially, rather than as ordinary items in |
| | 5833 | * contents listings. We'll send this to our current state object |
| | 5834 | * for processing, since our "I am here" description tends to vary by |
| | 5835 | * state. |
| | 5836 | */ |
| | 5837 | specialDesc() { curState.specialDesc(); } |
| | 5838 | distantSpecialDesc() { curState.distantSpecialDesc(); } |
| | 5839 | remoteSpecialDesc(actor) { curState.remoteSpecialDesc(actor); } |
| | 5840 | specialDescListWith() { return curState.specialDescListWith(); } |
| | 5841 | |
| | 5842 | /* |
| | 5843 | * By default, show the special description for an actor in the group |
| | 5844 | * of special descriptions that come *after* the room's portable |
| | 5845 | * contents listing. An actor's presence is usually a dynamic |
| | 5846 | * feature of a room, and so we don't want to suggest that the actor |
| | 5847 | * is a permanent feature of the room by describing the actor |
| | 5848 | * directly with the room's main description. |
| | 5849 | */ |
| | 5850 | specialDescBeforeContents = nil |
| | 5851 | |
| | 5852 | /* |
| | 5853 | * When we're asked to show a special description as part of the |
| | 5854 | * description of a containing object (which will usually be a nested |
| | 5855 | * room of some kind), just show our posture in our container, rather |
| | 5856 | * than showing our full "I am here" description. |
| | 5857 | */ |
| | 5858 | showSpecialDescInContents(actor, cont) |
| | 5859 | { |
| | 5860 | /* show our posture to indicate our container */ |
| | 5861 | listActorPosture(actor); |
| | 5862 | } |
| | 5863 | |
| | 5864 | /* |
| | 5865 | * By default, put all of the actor special descriptions after the |
| | 5866 | * special descriptions of ordinary objects, by giving actors a |
| | 5867 | * higher listing order value. |
| | 5868 | */ |
| | 5869 | specialDescOrder = 200 |
| | 5870 | |
| | 5871 | /* |
| | 5872 | * Get my listing group for my special description as part of a room |
| | 5873 | * description. By default, we'll let our immediate location decide |
| | 5874 | * how we're grouped. |
| | 5875 | */ |
| | 5876 | actorListWith() |
| | 5877 | { |
| | 5878 | local group; |
| | 5879 | |
| | 5880 | /* |
| | 5881 | * if our special desc is overridden, don't use any grouping by |
| | 5882 | * default - this make a special description defined in the |
| | 5883 | * actor override any grouping we'd otherwise do |
| | 5884 | */ |
| | 5885 | if (overrides(self, Actor, &specialDesc)) |
| | 5886 | return []; |
| | 5887 | |
| | 5888 | /* get the group for the posture */ |
| | 5889 | group = location.listWithActorIn(posture); |
| | 5890 | |
| | 5891 | /* |
| | 5892 | * if we have a group, return a list containing the group; |
| | 5893 | * otherwise return an empty list |
| | 5894 | */ |
| | 5895 | return (group == nil ? [] : [group]); |
| | 5896 | } |
| | 5897 | |
| | 5898 | /* |
| | 5899 | * Actor "I am here" description. This is displayed as part of the |
| | 5900 | * description of a room - it describes the actor as being present in |
| | 5901 | * the room. By default, we let the "nominal actor container" |
| | 5902 | * provide the description. |
| | 5903 | */ |
| | 5904 | actorHereDesc { descViaActorContainer(&roomActorHereDesc, nil); } |
| | 5905 | |
| | 5906 | /* |
| | 5907 | * Actor's "I am over there" description. This is displayed in the |
| | 5908 | * room description when the actor is visible, but is either in a |
| | 5909 | * separate top-level room or is at a distance. By default, we let |
| | 5910 | * the "nominal actor container" provide the description. |
| | 5911 | */ |
| | 5912 | actorThereDesc { descViaActorContainer(&roomActorThereDesc, nil); } |
| | 5913 | |
| | 5914 | /* |
| | 5915 | * Show our status, as an addendum to the given room's name (this is |
| | 5916 | * the room title, shown at the start of a room description and on |
| | 5917 | * the status line). By default, we'll let our nominal actor |
| | 5918 | * container provide the status, to indicate when we're |
| | 5919 | * standing/sitting/lying in a nested room. |
| | 5920 | * |
| | 5921 | * In concrete terms, this generally adds a message such as "(sitting |
| | 5922 | * on the chair)" to the name of a room if we're in a nested room |
| | 5923 | * within the room. When we're standing in the main room, this |
| | 5924 | * generally adds nothing. |
| | 5925 | * |
| | 5926 | * Note that we pass the room we're describing as the "container to |
| | 5927 | * ignore" parameter, because we don't want to say something like |
| | 5928 | * "Phone Booth (standing in the phone booth)" - that is, we don't |
| | 5929 | * want to mention the nominal container again if the nominal |
| | 5930 | * container is what we're naming in the first place. |
| | 5931 | */ |
| | 5932 | actorRoomNameStatus(room) |
| | 5933 | { descViaActorContainer(&roomActorStatus, room); } |
| | 5934 | |
| | 5935 | /* |
| | 5936 | * Describe the actor via the "nominal actor container." The nominal |
| | 5937 | * container is determined by our direct location. |
| | 5938 | * |
| | 5939 | * 'contToIgnore' is a container to ignore. If our nominal container |
| | 5940 | * is the same as this object, we'll generate a description without a |
| | 5941 | * mention of a container at all. |
| | 5942 | * |
| | 5943 | * The reason we have the 'contToIgnore' parameter is that the caller |
| | 5944 | * might already have reported our general location, and now merely |
| | 5945 | * wants to add that we're standing or standing or whatever. In |
| | 5946 | * these cases, if we were to say that we're sitting on or standing |
| | 5947 | * on that same object, it would be redundant information: "Bob is in |
| | 5948 | * the garden, sitting in the garden." The 'contToIgnore' parameter |
| | 5949 | * tells us the object that the caller has already mentioned as our |
| | 5950 | * general location so that we don't re-report the same thing. We |
| | 5951 | * need to know the actual object, rather than just the fact that the |
| | 5952 | * caller mentioned a general location, because our general location |
| | 5953 | * and the specific place we're standing or sitting or whatever might |
| | 5954 | * not be the same: "Bob is in the garden, sitting in the lawn |
| | 5955 | * chair." |
| | 5956 | * |
| | 5957 | */ |
| | 5958 | descViaActorContainer(prop, contToIgnore) |
| | 5959 | { |
| | 5960 | local pov; |
| | 5961 | local cont; |
| | 5962 | |
| | 5963 | /* get our nominal container for our current posture */ |
| | 5964 | cont = location.getNominalActorContainer(posture); |
| | 5965 | |
| | 5966 | /* get the point of view, using the player character by default */ |
| | 5967 | if ((pov = getPOV()) == nil) |
| | 5968 | pov = gPlayerChar; |
| | 5969 | |
| | 5970 | /* |
| | 5971 | * if we have a nominal container, and it's not the one to |
| | 5972 | * ignore, and the player character can see it, generate the |
| | 5973 | * description via the container; otherwise, use a generic |
| | 5974 | * library message that doesn't mention the container |
| | 5975 | */ |
| | 5976 | if (cont not in (nil, contToIgnore) && pov.canSee(cont)) |
| | 5977 | { |
| | 5978 | /* describe via the container */ |
| | 5979 | cont.(prop)(self); |
| | 5980 | } |
| | 5981 | else |
| | 5982 | { |
| | 5983 | /* use the generic library message */ |
| | 5984 | gLibMessages.(prop)(self); |
| | 5985 | } |
| | 5986 | } |
| | 5987 | |
| | 5988 | /* |
| | 5989 | * Describe my inventory as part of my description - this is only |
| | 5990 | * called when we examine an NPC. If an NPC doesn't wish to have |
| | 5991 | * its inventory listed as part of its description, it can simply |
| | 5992 | * override this to do nothing. |
| | 5993 | */ |
| | 5994 | holdingDesc |
| | 5995 | { |
| | 5996 | /* |
| | 5997 | * show our contents as for a normal "examine", but using the |
| | 5998 | * special contents lister for what an actor is holding |
| | 5999 | */ |
| | 6000 | examineListContentsWith(holdingDescInventoryLister); |
| | 6001 | } |
| | 6002 | |
| | 6003 | /* |
| | 6004 | * refer to the player character with my player character referral |
| | 6005 | * person, and refer to all other characters in the third person |
| | 6006 | */ |
| | 6007 | referralPerson { return isPlayerChar() ? pcReferralPerson : ThirdPerson; } |
| | 6008 | |
| | 6009 | /* by default, refer to the player character in the second person */ |
| | 6010 | pcReferralPerson = SecondPerson |
| | 6011 | |
| | 6012 | /* |
| | 6013 | * The referral person of the current command targeting the actor. |
| | 6014 | * This is meaningful only when a command is being directed to this |
| | 6015 | * actor, and this actor is an NPC. |
| | 6016 | * |
| | 6017 | * The referral person depends on the specifics of the language. In |
| | 6018 | * English, a command like "bob, go north" is a second-person |
| | 6019 | * command, while "tell bob to go north" is a third-person command. |
| | 6020 | * The only reason this is important is in interpreting what "you" |
| | 6021 | * means if it's used as an object in the command. "tell bob to hit |
| | 6022 | * you" probably means that Bob should hit the player character, |
| | 6023 | * while "bob, hit you" probably means that Bob should hit himself. |
| | 6024 | */ |
| | 6025 | commandReferralPerson = nil |
| | 6026 | |
| | 6027 | /* determine if I'm the player character */ |
| | 6028 | isPlayerChar() { return libGlobal.playerChar == self; } |
| | 6029 | |
| | 6030 | /* |
| | 6031 | * Implicit command handling style for this actor. There are two |
| | 6032 | * styles for handling implied commands: "player" and "NPC", |
| | 6033 | * indicated by the enum codes ModePlayer and ModeNPC, respectively. |
| | 6034 | * |
| | 6035 | * In "player" mode, each implied command is announced with a |
| | 6036 | * description of the command to be performed; DEFAULT responses are |
| | 6037 | * suppressed; and failures are shown. Furthermore, interactive |
| | 6038 | * requests for more information from the parser are allowed. |
| | 6039 | * Transcripts like this result: |
| | 6040 | * |
| | 6041 | * >open door |
| | 6042 | *. (first opening the door) |
| | 6043 | *. (first unlocking the door) |
| | 6044 | *. What do you want to unlock it with? |
| | 6045 | * |
| | 6046 | * In "NPC" mode, implied commands are treated as complete and |
| | 6047 | * separate commands. They are not announced; default responses are |
| | 6048 | * shown; failures are NOT shown; and interactive requests for more |
| | 6049 | * information are not allowed. When an implied command fails in NPC |
| | 6050 | * mode, the parser acts as though the command had never been |
| | 6051 | * attempted. |
| | 6052 | * |
| | 6053 | * By default, we return ModePlayer if we're the player character, |
| | 6054 | * ModeNPC if not (thus the respective names of the modes). Some |
| | 6055 | * authors might prefer to use "player mode" for NPC's as well as for |
| | 6056 | * the player character, which is why the various parts of the parser |
| | 6057 | * that care about this mode consult this method rather than simply |
| | 6058 | * testing the PC/NPC status of the actor. |
| | 6059 | */ |
| | 6060 | impliedCommandMode() { return isPlayerChar() ? ModePlayer : ModeNPC; } |
| | 6061 | |
| | 6062 | /* |
| | 6063 | * Try moving the given object into this object. For an actor, this |
| | 6064 | * will do one of two things. If 'self' is the actor performing the |
| | 6065 | * action that's triggering this implied command, then we can achieve |
| | 6066 | * the goal simply by taking the object. Otherwise, the way to get |
| | 6067 | * an object into my possession is to have the actor performing the |
| | 6068 | * command give me the object. |
| | 6069 | */ |
| | 6070 | tryMovingObjInto(obj) |
| | 6071 | { |
| | 6072 | if (gActor == self) |
| | 6073 | { |
| | 6074 | /* |
| | 6075 | * I'm performing the triggering action, so I merely need to |
| | 6076 | * pick up the object |
| | 6077 | */ |
| | 6078 | return tryImplicitAction(Take, obj); |
| | 6079 | } |
| | 6080 | else |
| | 6081 | { |
| | 6082 | /* |
| | 6083 | * another actor is performing the action; since that actor |
| | 6084 | * is the one who must perform the implied action, the way to |
| | 6085 | * get an object into my inventory is for that actor to give |
| | 6086 | * it to me |
| | 6087 | */ |
| | 6088 | return tryImplicitAction(GiveTo, obj, self); |
| | 6089 | } |
| | 6090 | } |
| | 6091 | |
| | 6092 | /* desribe our containment of an object as carrying the object */ |
| | 6093 | mustMoveObjInto(obj) { reportFailure(&mustBeCarryingMsg, obj, self); } |
| | 6094 | |
| | 6095 | /* |
| | 6096 | * You can limit the cumulative amount of bulk an actor can hold, and |
| | 6097 | * the maximum bulk of any one object the actor can hold, using |
| | 6098 | * bulkCapacity and maxSingleBulk. These properties are analogous to |
| | 6099 | * the same ones in Container. |
| | 6100 | * |
| | 6101 | * A word of caution on these is in order. Many authors worry that |
| | 6102 | * it's unrealistic if the player character can carry too much at one |
| | 6103 | * time, so they'll fiddle with these properties to impose a carrying |
| | 6104 | * limit that seems realistic. Be advised that authors love this |
| | 6105 | * sort of "realism" a whole lot more than players do. Players |
| | 6106 | * almost universally don't care about it, and in fact tend to hate |
| | 6107 | * the inventory juggling it inevitably leads to. Juggling inventory |
| | 6108 | * isn't any fun for the player. Don't fool yourself about this - |
| | 6109 | * the thoughts in the mind of a player who's tediously carting |
| | 6110 | * objects back and forth three at a time will not include admiration |
| | 6111 | * of your prowess at simulational realism. In contrast, if you set |
| | 6112 | * the carrying limit to infinity, it's a rare player who will even |
| | 6113 | * notice, and a much rarer player who'll complain about it. |
| | 6114 | * |
| | 6115 | * If you really must insist on inventory limits, refer to the |
| | 6116 | * BagOfHolding class for a solution that can salvage most of the |
| | 6117 | * "realism" that the accountancy-inclined author craves, without |
| | 6118 | * creating undue inconvenience for the player. BagOfHolding makes |
| | 6119 | * inventory limits palatable for the player by essentially |
| | 6120 | * automating the required inventory juggling. In fact, for most |
| | 6121 | * players, an inventory limit in conjunction with a bag of holding |
| | 6122 | * is actually better than an unlimited inventory, since it improves |
| | 6123 | * readability by keeping the direct inventory list to a manageable |
| | 6124 | * size. |
| | 6125 | */ |
| | 6126 | bulkCapacity = 10000 |
| | 6127 | maxSingleBulk = 10 |
| | 6128 | |
| | 6129 | /* |
| | 6130 | * An actor can limit the cumulative amount of weight being held, |
| | 6131 | * using weightCapacity. By default we make this so large that |
| | 6132 | * there is effectively no limit to how much weight an actor can |
| | 6133 | * carry. |
| | 6134 | */ |
| | 6135 | weightCapacity = 10000 |
| | 6136 | |
| | 6137 | /* |
| | 6138 | * Can I own the given object? By default, an actor can own |
| | 6139 | * anything. |
| | 6140 | */ |
| | 6141 | canOwn(obj) { return true; } |
| | 6142 | |
| | 6143 | /* |
| | 6144 | * Get the preconditions for travel. By default, we'll add the |
| | 6145 | * standard preconditions that the connector requires for actors. |
| | 6146 | * |
| | 6147 | * Note that these preconditions apply only when the actor is the |
| | 6148 | * traveler. If the actor is in a vehicle, so that the vehicle is |
| | 6149 | * the traveler in a given travel operation, the vehicle's |
| | 6150 | * travelerPreCond conditions are used instead of ours. |
| | 6151 | */ |
| | 6152 | travelerPreCond(conn) { return conn.actorTravelPreCond(self); } |
| | 6153 | |
| | 6154 | /* by default, actors are listed when they arrive aboard a vehicle */ |
| | 6155 | isListedAboardVehicle = true |
| | 6156 | |
| | 6157 | /* |
| | 6158 | * Get the object that's actually going to move when this actor |
| | 6159 | * travels via the given connector. In most cases this is simply the |
| | 6160 | * actor; but when the actor is in a vehicle, travel commands move |
| | 6161 | * the vehicle, not the actor: the actor stays in the vehicle while |
| | 6162 | * the vehicle moves to a new location. We determine this by asking |
| | 6163 | * our immediate location what it thinks about the situation. |
| | 6164 | * |
| | 6165 | * If we have a special traveler explicitly set, it overrides the |
| | 6166 | * traveler indicated by the location. |
| | 6167 | */ |
| | 6168 | getTraveler(conn) |
| | 6169 | { |
| | 6170 | /* |
| | 6171 | * Return our special traveler if we have one; otherwise, if we |
| | 6172 | * have a location, return the traveler indicated by our |
| | 6173 | * location; otherwise, we're the traveler. |
| | 6174 | */ |
| | 6175 | if (specialTraveler != nil) |
| | 6176 | return specialTraveler; |
| | 6177 | else if (location != nil) |
| | 6178 | return location.getLocTraveler(self, conn); |
| | 6179 | else |
| | 6180 | return self; |
| | 6181 | } |
| | 6182 | |
| | 6183 | /* |
| | 6184 | * Get the "push traveler" for the actor. This is the nominal |
| | 6185 | * traveler that we want to use when the actor enters a command like |
| | 6186 | * PUSH BOX NORTH. 'obj' is the object we're trying to push. |
| | 6187 | */ |
| | 6188 | getPushTraveler(obj) |
| | 6189 | { |
| | 6190 | /* |
| | 6191 | * If we already have a special traveler, just use the special |
| | 6192 | * traveler. Otherwise, if we have a location, ask the location |
| | 6193 | * what it thinks. Otherwise, we're the traveler. |
| | 6194 | */ |
| | 6195 | if (specialTraveler != nil) |
| | 6196 | return specialTraveler; |
| | 6197 | else if (location != nil) |
| | 6198 | return location.getLocPushTraveler(self, obj); |
| | 6199 | else |
| | 6200 | return self; |
| | 6201 | } |
| | 6202 | |
| | 6203 | /* is an actor traveling with us? */ |
| | 6204 | isActorTraveling(actor) |
| | 6205 | { |
| | 6206 | /* we're the only actor traveling when we're the traveler */ |
| | 6207 | return (actor == self); |
| | 6208 | } |
| | 6209 | |
| | 6210 | /* invoke a callback on each actor traveling with the traveler */ |
| | 6211 | forEachTravelingActor(func) |
| | 6212 | { |
| | 6213 | /* we're the only actor, so simply invoke the callback on myself */ |
| | 6214 | (func)(self); |
| | 6215 | } |
| | 6216 | |
| | 6217 | /* |
| | 6218 | * Get the actors involved in travel, when we're acting in our role |
| | 6219 | * as a Traveler. When the Traveler is simply the Actor, the only |
| | 6220 | * actor involved in the travel is 'self'. |
| | 6221 | */ |
| | 6222 | getTravelerActors = [self] |
| | 6223 | |
| | 6224 | /* we're the self-motive actor doing the travel */ |
| | 6225 | getTravelerMotiveActors = [self] |
| | 6226 | |
| | 6227 | /* |
| | 6228 | * Set the "special traveler." When this is set, we explicitly |
| | 6229 | * perform travel through this object rather than through the |
| | 6230 | * traveler indicated by our location. Returns the old value, so |
| | 6231 | * that the old value can be restored when the caller has finished |
| | 6232 | * its need for the special traveler. |
| | 6233 | */ |
| | 6234 | setSpecialTraveler(traveler) |
| | 6235 | { |
| | 6236 | local oldVal; |
| | 6237 | |
| | 6238 | /* remember the old value so that we can return it */ |
| | 6239 | oldVal = specialTraveler; |
| | 6240 | |
| | 6241 | /* remember the new value */ |
| | 6242 | specialTraveler = traveler; |
| | 6243 | |
| | 6244 | /* return the old value */ |
| | 6245 | return oldVal; |
| | 6246 | } |
| | 6247 | |
| | 6248 | /* our special traveler */ |
| | 6249 | specialTraveler = nil |
| | 6250 | |
| | 6251 | /* |
| | 6252 | * Try moving the actor into the given room in preparation for |
| | 6253 | * travel, using pre-condition rules. |
| | 6254 | */ |
| | 6255 | checkMovingTravelerInto(room, allowImplicit) |
| | 6256 | { |
| | 6257 | /* try moving the actor into the room */ |
| | 6258 | return room.checkMovingActorInto(allowImplicit); |
| | 6259 | } |
| | 6260 | |
| | 6261 | /* |
| | 6262 | * Check to ensure the actor is ready to enter the given nested |
| | 6263 | * room, using pre-condition rules. By default, we'll ask the given |
| | 6264 | * nested room to handle it. |
| | 6265 | */ |
| | 6266 | checkReadyToEnterNestedRoom(dest, allowImplicit) |
| | 6267 | { |
| | 6268 | /* ask the destination to do the work */ |
| | 6269 | return dest.checkActorReadyToEnterNestedRoom(allowImplicit); |
| | 6270 | } |
| | 6271 | |
| | 6272 | /* |
| | 6273 | * Travel within a location, as from a room to a contained nested |
| | 6274 | * room. This should generally be used in lieu of travelTo when |
| | 6275 | * traveling between locations that are related directly by |
| | 6276 | * containment rather than with TravelConnector objects. |
| | 6277 | * |
| | 6278 | * Travel within a location is not restricted by darkness; we assume |
| | 6279 | * that if the nested objects are in scope at all, travel among them |
| | 6280 | * is allowed. |
| | 6281 | * |
| | 6282 | * This type of travel does not trigger calls to travelerLeaving() |
| | 6283 | * or travelerArriving(). To mitigate this loss of notification, we |
| | 6284 | * call actorTravelingWithin() on the source and destination |
| | 6285 | * objects. |
| | 6286 | */ |
| | 6287 | travelWithin(dest) |
| | 6288 | { |
| | 6289 | /* if I'm not going anywhere, ignore the operation */ |
| | 6290 | if (dest == location) |
| | 6291 | return; |
| | 6292 | |
| | 6293 | /* |
| | 6294 | * Notify the traveler. Note that since this is local travel |
| | 6295 | * within a single top-level location, there's no connector. |
| | 6296 | */ |
| | 6297 | getTraveler(nil).travelerTravelWithin(self, dest); |
| | 6298 | } |
| | 6299 | |
| | 6300 | /* |
| | 6301 | * Traveler interface: perform local travel, between nested rooms |
| | 6302 | * within a single top-level location. |
| | 6303 | */ |
| | 6304 | travelerTravelWithin(actor, dest) |
| | 6305 | { |
| | 6306 | local origin; |
| | 6307 | |
| | 6308 | /* remember my origin */ |
| | 6309 | origin = location; |
| | 6310 | |
| | 6311 | /* notify the source that we're traveling within a room */ |
| | 6312 | if (origin != nil) |
| | 6313 | origin.actorTravelingWithin(origin, dest); |
| | 6314 | |
| | 6315 | /* |
| | 6316 | * if our origin and destination have different effective follow |
| | 6317 | * locations, track the follow |
| | 6318 | */ |
| | 6319 | if (origin != nil |
| | 6320 | && dest != nil |
| | 6321 | && origin.effectiveFollowLocation != dest.effectiveFollowLocation) |
| | 6322 | { |
| | 6323 | /* |
| | 6324 | * notify observing objects of the travel; we're not moving |
| | 6325 | * along a connector, so there is no connector associated |
| | 6326 | * with the tracking information |
| | 6327 | */ |
| | 6328 | connectionTable().forEachAssoc( |
| | 6329 | {obj, val: obj.beforeTravel(self, nil)}); |
| | 6330 | } |
| | 6331 | |
| | 6332 | /* move me to the destination */ |
| | 6333 | moveInto(dest); |
| | 6334 | |
| | 6335 | /* |
| | 6336 | * recalculate the global sense context for message generation |
| | 6337 | * purposes, since we've moved to a new location |
| | 6338 | */ |
| | 6339 | if (gAction != nil) |
| | 6340 | gAction.recalcSenseContext(); |
| | 6341 | |
| | 6342 | /* notify the destination of the interior travel */ |
| | 6343 | if (dest != nil) |
| | 6344 | dest.actorTravelingWithin(origin, dest); |
| | 6345 | } |
| | 6346 | |
| | 6347 | /* |
| | 6348 | * Check for travel in the dark. If we're in a dark room, and our |
| | 6349 | * destination is a dark room, ask the connector for guidance. |
| | 6350 | * |
| | 6351 | * Travel connectors normally call this before invoking our |
| | 6352 | * travelTo() method to carry out the travel. The darkness check |
| | 6353 | * usually must be made before any barrier checks. |
| | 6354 | */ |
| | 6355 | checkDarkTravel(dest, connector) |
| | 6356 | { |
| | 6357 | local origin; |
| | 6358 | |
| | 6359 | /* |
| | 6360 | * If we're not in the dark in the current location, there's no |
| | 6361 | * need to check for dark-to-dark travel; light-to-dark travel |
| | 6362 | * is always allowed. |
| | 6363 | */ |
| | 6364 | if (isLocationLit()) |
| | 6365 | return; |
| | 6366 | |
| | 6367 | /* get the origin - this is the traveler's location */ |
| | 6368 | origin = getTraveler(connector).location; |
| | 6369 | |
| | 6370 | /* |
| | 6371 | * Check to see if the connector itself is visible in the dark. |
| | 6372 | * If it is, then allow the travel without restriction. |
| | 6373 | */ |
| | 6374 | if (connector.isConnectorVisibleInDark(origin, self)) |
| | 6375 | return; |
| | 6376 | |
| | 6377 | /* |
| | 6378 | * We are attempting dark-to-dark travel. We allow or disallow |
| | 6379 | * this type of travel on a per-connector basis, so ask the |
| | 6380 | * connector to handle it. If the connector wishes to disallow |
| | 6381 | * the travel, it will display an appropriate failure report and |
| | 6382 | * terminate the command with 'exit'. |
| | 6383 | */ |
| | 6384 | connector.darkTravel(self, dest); |
| | 6385 | } |
| | 6386 | |
| | 6387 | /* |
| | 6388 | * Travel to a new location. |
| | 6389 | */ |
| | 6390 | travelTo(dest, connector, backConnector) |
| | 6391 | { |
| | 6392 | /* send the request to the traveler */ |
| | 6393 | getTraveler(connector) |
| | 6394 | .travelerTravelTo(dest, connector, backConnector); |
| | 6395 | } |
| | 6396 | |
| | 6397 | /* |
| | 6398 | * Perform scripted travel to the given adjacent location. This |
| | 6399 | * looks for a directional connector in our current location whose |
| | 6400 | * destination is the given location, and for a corresponding |
| | 6401 | * back-connector in the destination location. If we can find the |
| | 6402 | * connectors, we'll perform the travel using travelTo(). |
| | 6403 | * |
| | 6404 | * The purpose of this routine is to simplify scripted travel for |
| | 6405 | * simple cases where directional connectors are available for the |
| | 6406 | * desired travel. This routine is NOT suitable for intelligent |
| | 6407 | * goal-seeking NPC's who automatically try to find their own routes, |
| | 6408 | * for two reasons. First, this routine only lets an NPC move to an |
| | 6409 | * *adjacent* location; it won't try to find a path between arbitrary |
| | 6410 | * locations. Second, this routine is "omniscient": it doesn't take |
| | 6411 | * into account what the NPC knows about the connections between |
| | 6412 | * locations, but simply finds a connector that actually provides the |
| | 6413 | * desired travel. |
| | 6414 | * |
| | 6415 | * What this routine *is* suitable for are cases where we have a |
| | 6416 | * pre-scripted series of NPC travel actions, where we have a list of |
| | 6417 | * rooms we want the NPC to visit in order. This routine simplifies |
| | 6418 | * this type of scripting by automatically finding the connectors; |
| | 6419 | * the script only has to specify the next location for the NPC to |
| | 6420 | * visit. |
| | 6421 | */ |
| | 6422 | scriptedTravelTo(dest) |
| | 6423 | { |
| | 6424 | local conn; |
| | 6425 | |
| | 6426 | /* find a connector from the current location to the new location */ |
| | 6427 | conn = location.getConnectorTo(self, dest); |
| | 6428 | |
| | 6429 | /* if we found the connector, perform the travel */ |
| | 6430 | if (conn != nil) |
| | 6431 | nestedActorAction(self, TravelVia, conn); |
| | 6432 | } |
| | 6433 | |
| | 6434 | /* |
| | 6435 | * Remember the last door I traveled through. We use this |
| | 6436 | * information for disambiguation, to boost the likelihood that an |
| | 6437 | * actor that just traveled through a door is referring to the same |
| | 6438 | * door in a subsequent "close" command. |
| | 6439 | */ |
| | 6440 | rememberLastDoor(obj) { lastDoorTraversed = obj; } |
| | 6441 | |
| | 6442 | /* |
| | 6443 | * Remember our most recent travel. If we know the back connector |
| | 6444 | * (i.e., the connector that reverses the travel we're performing), |
| | 6445 | * then we'll be able to accept a GO BACK command to attempt to |
| | 6446 | * return to the previous location. |
| | 6447 | */ |
| | 6448 | rememberTravel(origin, dest, backConnector) |
| | 6449 | { |
| | 6450 | /* remember the destination of the travel, and the connector back */ |
| | 6451 | lastTravelDest = dest; |
| | 6452 | lastTravelBack = backConnector; |
| | 6453 | } |
| | 6454 | |
| | 6455 | /* |
| | 6456 | * Reverse the most recent travel. If we're still within the same |
| | 6457 | * destination we reached in the last travel, and we know the |
| | 6458 | * connector we arrived through (i.e., the "back connector" for the |
| | 6459 | * last travel, which reverses the connector we took to get here), |
| | 6460 | * then try traveling via the connector. |
| | 6461 | */ |
| | 6462 | reverseLastTravel() |
| | 6463 | { |
| | 6464 | /* |
| | 6465 | * If we don't know the connector back to our previous location, |
| | 6466 | * we obviously can't reverse the travel. If we're not still in |
| | 6467 | * the same location as the previous travel's destination, then |
| | 6468 | * we can't reverse the travel either, because the back |
| | 6469 | * connector isn't applicable to our current location. (This |
| | 6470 | * latter condition could only happen if we've been moved |
| | 6471 | * somewhere without ordinary travel occurring, but this is a |
| | 6472 | * possibility.) |
| | 6473 | */ |
| | 6474 | if (lastTravelBack == nil |
| | 6475 | || lastTravelDest == nil |
| | 6476 | || !isIn(lastTravelDest)) |
| | 6477 | { |
| | 6478 | reportFailure(&cannotGoBackMsg); |
| | 6479 | exit; |
| | 6480 | } |
| | 6481 | |
| | 6482 | /* attempt travel via our back connector */ |
| | 6483 | nestedAction(TravelVia, lastTravelBack); |
| | 6484 | } |
| | 6485 | |
| | 6486 | /* the last door I traversed */ |
| | 6487 | lastDoorTraversed = nil |
| | 6488 | |
| | 6489 | /* the destination and back connector for our last travel */ |
| | 6490 | lastTravelDest = nil |
| | 6491 | lastTravelBack = nil |
| | 6492 | |
| | 6493 | /* |
| | 6494 | * use a custom message for cases where we're holding a destination |
| | 6495 | * object for BOARD, ENTER, etc |
| | 6496 | */ |
| | 6497 | checkStagingLocation(dest) |
| | 6498 | { |
| | 6499 | /* |
| | 6500 | * if the destination is within us, explain specifically that |
| | 6501 | * this is the problem |
| | 6502 | */ |
| | 6503 | if (dest.isIn(self)) |
| | 6504 | reportFailure(&invalidStagingContainerActorMsg, self, dest); |
| | 6505 | else |
| | 6506 | inherited(dest); |
| | 6507 | |
| | 6508 | /* terminate the command */ |
| | 6509 | exit; |
| | 6510 | } |
| | 6511 | |
| | 6512 | /* |
| | 6513 | * Travel arrival/departure messages. Defer to the current state |
| | 6514 | * object on all of these. |
| | 6515 | */ |
| | 6516 | sayArriving(conn) |
| | 6517 | { curState.sayArriving(conn); } |
| | 6518 | sayDeparting(conn) |
| | 6519 | { curState.sayDeparting(conn); } |
| | 6520 | sayArrivingLocally(dest, conn) |
| | 6521 | { curState.sayArrivingLocally(dest, conn); } |
| | 6522 | sayDepartingLocally(dest, conn) |
| | 6523 | { curState.sayDepartingLocally(dest, conn); } |
| | 6524 | sayTravelingRemotely(dest, conn) |
| | 6525 | { curState.sayTravelingRemotely(dest, conn); } |
| | 6526 | sayArrivingDir(dir, conn) |
| | 6527 | { curState.sayArrivingDir(dir, conn); } |
| | 6528 | sayDepartingDir(dir, conn) |
| | 6529 | { curState.sayDepartingDir(dir, conn); } |
| | 6530 | sayArrivingThroughPassage(conn) |
| | 6531 | { curState.sayArrivingThroughPassage(conn); } |
| | 6532 | sayDepartingThroughPassage(conn) |
| | 6533 | { curState.sayDepartingThroughPassage(conn); } |
| | 6534 | sayArrivingViaPath(conn) |
| | 6535 | { curState.sayArrivingViaPath(conn); } |
| | 6536 | sayDepartingViaPath(conn) |
| | 6537 | { curState.sayDepartingViaPath(conn); } |
| | 6538 | sayArrivingUpStairs(conn) |
| | 6539 | { curState.sayArrivingUpStairs(conn); } |
| | 6540 | sayArrivingDownStairs(conn) |
| | 6541 | { curState.sayArrivingDownStairs(conn); } |
| | 6542 | sayDepartingUpStairs(conn) |
| | 6543 | { curState.sayDepartingUpStairs(conn); } |
| | 6544 | sayDepartingDownStairs(conn) |
| | 6545 | { curState.sayDepartingDownStairs(conn); } |
| | 6546 | |
| | 6547 | /* |
| | 6548 | * Get the current interlocutor. By default, we'll address new |
| | 6549 | * conversational commands (ASK ABOUT, TELL ABOUT, SHOW TO) to the |
| | 6550 | * last conversational partner, if that actor is still within range. |
| | 6551 | */ |
| | 6552 | getCurrentInterlocutor() |
| | 6553 | { |
| | 6554 | /* |
| | 6555 | * if we've talked to someone before, and we can still talk to |
| | 6556 | * them now, return that actor; otherwise we have no default |
| | 6557 | */ |
| | 6558 | if (lastInterlocutor != nil && canTalkTo(lastInterlocutor)) |
| | 6559 | return lastInterlocutor; |
| | 6560 | else |
| | 6561 | return nil; |
| | 6562 | } |
| | 6563 | |
| | 6564 | /* |
| | 6565 | * Get the default interlocutor. If there's a current interlocutor, |
| | 6566 | * and we can still talk to that actor, then that's the default |
| | 6567 | * interlocutor. If not, we'll return whatever actor is the default |
| | 6568 | * for a TALK TO command. Note that TALK TO won't necessarily have a |
| | 6569 | * default actor; if it doesn't, we'll simply return nil. |
| | 6570 | */ |
| | 6571 | getDefaultInterlocutor() |
| | 6572 | { |
| | 6573 | local actor; |
| | 6574 | |
| | 6575 | /* check for a current interlocutor */ |
| | 6576 | actor = getCurrentInterlocutor(); |
| | 6577 | |
| | 6578 | /* |
| | 6579 | * if we're not talking to anyone, or if the person we were |
| | 6580 | * talking to can no longer hear us, look for a default object |
| | 6581 | * for a TALK TO command and use it instead as the default |
| | 6582 | */ |
| | 6583 | if (actor == nil || !canTalkTo(actor)) |
| | 6584 | { |
| | 6585 | local tt; |
| | 6586 | local res; |
| | 6587 | |
| | 6588 | /* set up a TALK TO command and a resolver */ |
| | 6589 | tt = new TalkToAction(); |
| | 6590 | res = new Resolver(tt, gIssuingActor, gActor); |
| | 6591 | |
| | 6592 | /* get the default direct object */ |
| | 6593 | actor = tt.getDefaultDobj(new EmptyNounPhraseProd(), res); |
| | 6594 | |
| | 6595 | /* if that worked, get the object from the resolve info */ |
| | 6596 | if (actor != nil) |
| | 6597 | actor = actor[1].obj_; |
| | 6598 | } |
| | 6599 | |
| | 6600 | /* return what we found */ |
| | 6601 | return actor; |
| | 6602 | } |
| | 6603 | |
| | 6604 | /* |
| | 6605 | * The most recent actor that we've interacted with through a |
| | 6606 | * conversational command (ASK, TELL, GIVE, SHOW, etc). |
| | 6607 | */ |
| | 6608 | lastInterlocutor = nil |
| | 6609 | |
| | 6610 | /* |
| | 6611 | * Our conversational "boredom" counter. While we're in a |
| | 6612 | * conversation, this tracks the number of turns since the last |
| | 6613 | * conversational command from the actor we're talking to. |
| | 6614 | * |
| | 6615 | * Note that this state is part of the actor, even though it's |
| | 6616 | * usually managed by the InConversationState object. The state is |
| | 6617 | * stored with the actor rather than with the state object because |
| | 6618 | * it really describes the condition of the actor, not of the state |
| | 6619 | * object. |
| | 6620 | */ |
| | 6621 | boredomCount = 0 |
| | 6622 | |
| | 6623 | /* |
| | 6624 | * game-clock time (Schedulable.gameClockTime) of the last |
| | 6625 | * conversational command addressed to us by the player character |
| | 6626 | */ |
| | 6627 | lastConvTime = -1 |
| | 6628 | |
| | 6629 | /* |
| | 6630 | * Did we engage in any conversation on the current turn? This can |
| | 6631 | * be used as a quick check in background activity scripts when we |
| | 6632 | * want to run a step only in the absence of any conversation on the |
| | 6633 | * same turn. |
| | 6634 | */ |
| | 6635 | conversedThisTurn() { return lastConvTime == Schedulable.gameClockTime; } |
| | 6636 | |
| | 6637 | /* |
| | 6638 | * Note that we're performing a conversational command targeting the |
| | 6639 | * given actor. We'll make the actors point at each other with their |
| | 6640 | * 'lastInterlocutor' properties. This is called on the character |
| | 6641 | * performing the conversation command: if the player types ASK BOB |
| | 6642 | * ABOUT BOOK, this will be called on the player character actor, |
| | 6643 | * with 'other' set to Bob. |
| | 6644 | */ |
| | 6645 | noteConversation(other) |
| | 6646 | { |
| | 6647 | /* note that we're part of a conversational action */ |
| | 6648 | noteConvAction(other); |
| | 6649 | |
| | 6650 | /* let the other actor know we're conversing with them */ |
| | 6651 | other.noteConversationFrom(self); |
| | 6652 | } |
| | 6653 | |
| | 6654 | /* |
| | 6655 | * Note that another actor is issuing a conversational command |
| | 6656 | * targeting us. For example, if the player types ASK BOB ABOUT |
| | 6657 | * BOOK, then this will be called on Bob, with the player character |
| | 6658 | * actor as 'other'. |
| | 6659 | */ |
| | 6660 | noteConversationFrom(other) |
| | 6661 | { |
| | 6662 | /* note that we're part of a conversational action */ |
| | 6663 | noteConvAction(other); |
| | 6664 | } |
| | 6665 | |
| | 6666 | /* |
| | 6667 | * Note that we're taking part in a conversational action with |
| | 6668 | * another character. This is symmetrical - it could mean we're the |
| | 6669 | * initiator of the conversation action or the target. We'll |
| | 6670 | * remember the person we're talking to, and reset our conversation |
| | 6671 | * time counters so we know we've conversed on this turn. |
| | 6672 | */ |
| | 6673 | noteConvAction(other) |
| | 6674 | { |
| | 6675 | /* note our last conversational partner */ |
| | 6676 | lastInterlocutor = other; |
| | 6677 | |
| | 6678 | /* set the actor to be the pronoun antecedent */ |
| | 6679 | setPronounObj(other); |
| | 6680 | |
| | 6681 | /* |
| | 6682 | * reset our boredom counter, as the other actor has just spoken |
| | 6683 | * to us |
| | 6684 | */ |
| | 6685 | boredomCount = 0; |
| | 6686 | |
| | 6687 | /* remember the time of our last conversation from the PC */ |
| | 6688 | lastConvTime = Schedulable.gameClockTime; |
| | 6689 | } |
| | 6690 | |
| | 6691 | /* note that we're consulting an item */ |
| | 6692 | noteConsultation(obj) { lastConsulted = obj; } |
| | 6693 | |
| | 6694 | /* |
| | 6695 | * Receive notification that a TopicEntry response in our database is |
| | 6696 | * being invoked. We'll just pass this along to our current state. |
| | 6697 | */ |
| | 6698 | notifyTopicResponse(fromActor, entry) |
| | 6699 | { |
| | 6700 | /* let our current state handle it */ |
| | 6701 | curState.notifyTopicResponse(fromActor, entry); |
| | 6702 | } |
| | 6703 | |
| | 6704 | /* the object we most recently consulted */ |
| | 6705 | lastConsulted = nil |
| | 6706 | |
| | 6707 | /* |
| | 6708 | * The actor's "agenda." This is a list of AgendaItem objects that |
| | 6709 | * describe things the actor wants to do of its own volition on its |
| | 6710 | * own turn. |
| | 6711 | */ |
| | 6712 | agendaList = nil |
| | 6713 | |
| | 6714 | /* |
| | 6715 | * our special "boredom" agenda item - this makes us initiate an end |
| | 6716 | * to an active conversation when the PC has ignored us for a given |
| | 6717 | * number of consecutive turns |
| | 6718 | */ |
| | 6719 | boredomAgendaItem = perInstance(new BoredomAgendaItem(self)) |
| | 6720 | |
| | 6721 | /* add an agenda item */ |
| | 6722 | addToAgenda(item) |
| | 6723 | { |
| | 6724 | /* if we don't have an agenda list yet, create one */ |
| | 6725 | if (agendaList == nil) |
| | 6726 | agendaList = new Vector(10); |
| | 6727 | |
| | 6728 | /* add the item */ |
| | 6729 | agendaList.append(item); |
| | 6730 | |
| | 6731 | /* |
| | 6732 | * keep the list in ascending order of agendaOrder values - this |
| | 6733 | * will ensure that we'll always choose the earliest item that's |
| | 6734 | * ready to run |
| | 6735 | */ |
| | 6736 | agendaList.sort(SortAsc, {a, b: a.agendaOrder - b.agendaOrder}); |
| | 6737 | |
| | 6738 | /* reset the agenda item */ |
| | 6739 | item.resetItem(); |
| | 6740 | } |
| | 6741 | |
| | 6742 | /* remove an agenda item */ |
| | 6743 | removeFromAgenda(item) |
| | 6744 | { |
| | 6745 | /* if we have an agenda list, remove the item */ |
| | 6746 | if (agendaList != nil) |
| | 6747 | agendaList.removeElement(item); |
| | 6748 | } |
| | 6749 | |
| | 6750 | /* |
| | 6751 | * Execute the next item in our agenda, if there are any items in the |
| | 6752 | * agenda that are ready to execute. We'll return true if we found |
| | 6753 | * an item to execute, nil if not. |
| | 6754 | */ |
| | 6755 | executeAgenda() |
| | 6756 | { |
| | 6757 | local item; |
| | 6758 | |
| | 6759 | /* if we don't have an agenda, there are obviously no items */ |
| | 6760 | if (agendaList == nil) |
| | 6761 | return nil; |
| | 6762 | |
| | 6763 | /* remove any items that are marked as done */ |
| | 6764 | while ((item = agendaList.lastValWhich({x: x.isDone})) != nil) |
| | 6765 | agendaList.removeElement(item); |
| | 6766 | |
| | 6767 | /* |
| | 6768 | * Scan for an item that's ready to execute. Since we keep the |
| | 6769 | * list sorted in ascending order of agendaOrder values, we can |
| | 6770 | * just pick the earliest item in the list that's ready to run, |
| | 6771 | * since that will be the ready-to-run item with the lowest |
| | 6772 | * agendaOrder number. |
| | 6773 | */ |
| | 6774 | item = agendaList.valWhich({x: x.isReady}); |
| | 6775 | |
| | 6776 | /* if we found an item, execute it */ |
| | 6777 | if (item != nil) |
| | 6778 | { |
| | 6779 | try |
| | 6780 | { |
| | 6781 | /* execute the item */ |
| | 6782 | item.invokeItem(); |
| | 6783 | } |
| | 6784 | catch (RuntimeError err) |
| | 6785 | { |
| | 6786 | /* |
| | 6787 | * If an error occurs while executing the item, mark the |
| | 6788 | * item as done. This will ensure that we won't get |
| | 6789 | * stuck in a loop trying to execute the same item over |
| | 6790 | * and over, which will probably just run into the same |
| | 6791 | * error on each attempt. |
| | 6792 | */ |
| | 6793 | item.isDone = true; |
| | 6794 | |
| | 6795 | /* re-throw the exception */ |
| | 6796 | throw err; |
| | 6797 | } |
| | 6798 | |
| | 6799 | /* tell the caller we found an item to execute */ |
| | 6800 | return true; |
| | 6801 | } |
| | 6802 | else |
| | 6803 | { |
| | 6804 | /* tell the caller we found no agenda item */ |
| | 6805 | return nil; |
| | 6806 | } |
| | 6807 | } |
| | 6808 | |
| | 6809 | /* |
| | 6810 | * Calculate the amount of bulk I'm holding directly. By default, |
| | 6811 | * we'll simply add up the "actor-encumbering bulk" of each of our |
| | 6812 | * direct contents. |
| | 6813 | * |
| | 6814 | * Note that we don't differentiate here based on whether or not an |
| | 6815 | * item is being worn, or anything else - we deliberately leave such |
| | 6816 | * distinctions up to the getEncumberingBulk routine, so that only |
| | 6817 | * the objects are in the business of deciding how bulky they are |
| | 6818 | * under different circumstances. |
| | 6819 | */ |
| | 6820 | getBulkHeld() |
| | 6821 | { |
| | 6822 | local total; |
| | 6823 | |
| | 6824 | /* start with nothing */ |
| | 6825 | total = 0; |
| | 6826 | |
| | 6827 | /* add the bulks of directly-contained items */ |
| | 6828 | foreach (local cur in contents) |
| | 6829 | total += cur.getEncumberingBulk(self); |
| | 6830 | |
| | 6831 | /* return the total */ |
| | 6832 | return total; |
| | 6833 | } |
| | 6834 | |
| | 6835 | /* |
| | 6836 | * Calculate the total weight I'm holding. By default, we'll add up |
| | 6837 | * the "actor-encumbering weight" of each of our direct contents. |
| | 6838 | * |
| | 6839 | * Note that we deliberately only consider our direct contents. If |
| | 6840 | * any of the items we are directly holding contain further items, |
| | 6841 | * getEncumberingWeight will take their weights into account; this |
| | 6842 | * frees us from needing any special knowledge of the internal |
| | 6843 | * structure of any items we're holding, and puts that knowledge in |
| | 6844 | * the individual items where it belongs. |
| | 6845 | */ |
| | 6846 | getWeightHeld() |
| | 6847 | { |
| | 6848 | local total; |
| | 6849 | |
| | 6850 | /* start with nothing */ |
| | 6851 | total = 0; |
| | 6852 | |
| | 6853 | /* add the weights of directly-contained items */ |
| | 6854 | foreach (local cur in contents) |
| | 6855 | total += cur.getEncumberingWeight(self); |
| | 6856 | |
| | 6857 | /* return the total */ |
| | 6858 | return total; |
| | 6859 | } |
| | 6860 | |
| | 6861 | /* |
| | 6862 | * Try making room to hold the given object. This is called when |
| | 6863 | * checking the "room to hold object" pre-condition, such as for the |
| | 6864 | * "take" verb. |
| | 6865 | * |
| | 6866 | * If holding the new object would exceed the our maximum holding |
| | 6867 | * capacity, we'll go through our inventory looking for objects that |
| | 6868 | * can reduce our held bulk with implicit commands. Objects with |
| | 6869 | * holding affinities - "bags of holding", keyrings, and the like - |
| | 6870 | * can implicitly shuffle the actor's possessions in a manner that |
| | 6871 | * is neutral as far as the actor is concerned, thereby reducing our |
| | 6872 | * active holding load. |
| | 6873 | * |
| | 6874 | * Returns true if an implicit command was attempted, nil if not. |
| | 6875 | */ |
| | 6876 | tryMakingRoomToHold(obj, allowImplicit) |
| | 6877 | { |
| | 6878 | local objWeight; |
| | 6879 | local objBulk; |
| | 6880 | local aff; |
| | 6881 | |
| | 6882 | /* get the amount of weight this will add if taken */ |
| | 6883 | objWeight = obj.getEncumberingWeight(self); |
| | 6884 | |
| | 6885 | /* |
| | 6886 | * If this object alone is too heavy for us, give up. We |
| | 6887 | * distinguish this case from the case where the total (of |
| | 6888 | * everything held plus the new item) is too heavy: in the |
| | 6889 | * latter case we can tell the actor that they can pick this up |
| | 6890 | * by dropping something else first, whereas if this item alone |
| | 6891 | * is too heavy, no such advice is warranted. |
| | 6892 | */ |
| | 6893 | if (objWeight > weightCapacity) |
| | 6894 | { |
| | 6895 | reportFailure(&tooHeavyForActorMsg, obj); |
| | 6896 | exit; |
| | 6897 | } |
| | 6898 | |
| | 6899 | /* |
| | 6900 | * if taking the object would push our total carried weight over |
| | 6901 | * our total carrying weight limit, give up |
| | 6902 | */ |
| | 6903 | if (obj.whatIfHeldBy({: getWeightHeld()}, self) > weightCapacity) |
| | 6904 | { |
| | 6905 | reportFailure(&totalTooHeavyForMsg, obj); |
| | 6906 | exit; |
| | 6907 | } |
| | 6908 | |
| | 6909 | /* get the amount of bulk the object will add */ |
| | 6910 | objBulk = obj.getEncumberingBulk(self); |
| | 6911 | |
| | 6912 | /* |
| | 6913 | * if the object is simply too big to start with, we can't make |
| | 6914 | * room no matter what we do |
| | 6915 | */ |
| | 6916 | if (objBulk > maxSingleBulk || objBulk > bulkCapacity) |
| | 6917 | { |
| | 6918 | reportFailure(&tooLargeForActorMsg, obj); |
| | 6919 | exit; |
| | 6920 | } |
| | 6921 | |
| | 6922 | /* |
| | 6923 | * Test what would happen to our bulk if we were to move the |
| | 6924 | * object into our directly held inventory. Do this by running |
| | 6925 | * a "what if" scenario to test moving the object into our |
| | 6926 | * inventory, and check what effect it has on our held bulk. If |
| | 6927 | * it fits, we can let the caller proceed without further work. |
| | 6928 | */ |
| | 6929 | if (obj.whatIfHeldBy({: getBulkHeld()}, self) <= bulkCapacity) |
| | 6930 | return nil; |
| | 6931 | |
| | 6932 | /* |
| | 6933 | * if we're not allowed to run implicit commands, we won't be |
| | 6934 | * able to accomplish anything, so give up |
| | 6935 | */ |
| | 6936 | if (!allowImplicit) |
| | 6937 | { |
| | 6938 | reportFailure(&handsTooFullForMsg, obj); |
| | 6939 | exit; |
| | 6940 | } |
| | 6941 | |
| | 6942 | /* |
| | 6943 | * Get "bag of holding" affinity information for my immediate |
| | 6944 | * contents. Consider only objects with encumbering bulk, since |
| | 6945 | * it will do us no good to move objects without any encumbering |
| | 6946 | * bulk. Also ignore objects that aren't being held (some direct |
| | 6947 | * contents aren't considered to be held, such as clothing being |
| | 6948 | * worn). |
| | 6949 | */ |
| | 6950 | aff = getBagAffinities(contents.subset( |
| | 6951 | {x: x.getEncumberingBulk(self) != 0 && x.isHeldBy(self)})); |
| | 6952 | |
| | 6953 | /* if there are no bag affinities, we can't move anything around */ |
| | 6954 | if (aff.length() == 0) |
| | 6955 | { |
| | 6956 | reportFailure(&handsTooFullForMsg, obj); |
| | 6957 | exit; |
| | 6958 | } |
| | 6959 | |
| | 6960 | /* |
| | 6961 | * If we have at least four items, find the two that were picked |
| | 6962 | * up most recently (according to the "holding index" value) and |
| | 6963 | * move them to the end of the list. In most cases, we'll only |
| | 6964 | * have to dispose of one or two items to free up enough space |
| | 6965 | * in our hands, so we'll probably never get to the last couple |
| | 6966 | * of items in our list, so we're effectively ruling out moving |
| | 6967 | * these two most recent items; but they'll be in the list if we |
| | 6968 | * do find we need to move them after all. |
| | 6969 | * |
| | 6970 | * The point of this rearrangement is to avoid annoying cases of |
| | 6971 | * moving something we just picked up, especially if we just |
| | 6972 | * picked it up in order to carry out the command that's making |
| | 6973 | * us free up more space now. This looks especially stupid when |
| | 6974 | * we perform some command that requires picking up two items |
| | 6975 | * automatically: we pick up the first, then we put it away in |
| | 6976 | * order to pick up the second, but then we find that we need |
| | 6977 | * the first again. |
| | 6978 | */ |
| | 6979 | if (aff.length() >= 4) |
| | 6980 | { |
| | 6981 | local a, b; |
| | 6982 | |
| | 6983 | /* remove the two most recent items from the vector */ |
| | 6984 | a = BagAffinityInfo.removeMostRecent(aff); |
| | 6985 | b = BagAffinityInfo.removeMostRecent(aff); |
| | 6986 | |
| | 6987 | /* re-insert them at the end of the vector */ |
| | 6988 | aff.append(b); |
| | 6989 | aff.append(a); |
| | 6990 | } |
| | 6991 | |
| | 6992 | /* |
| | 6993 | * Move each object in the list until we have reduced the bulk |
| | 6994 | * sufficiently. |
| | 6995 | */ |
| | 6996 | foreach (local cur in aff) |
| | 6997 | { |
| | 6998 | /* |
| | 6999 | * Try moving this object to its bag. If the bag is itself |
| | 7000 | * inside this object, don't even try, since that would be an |
| | 7001 | * attempt at circular containment. |
| | 7002 | * |
| | 7003 | * If the object we're trying to hold is inside this object, |
| | 7004 | * don't move the object. That might put the object we're |
| | 7005 | * trying to hold out of reach, since moving an object into a |
| | 7006 | * bag could involve closing the object or making its |
| | 7007 | * contents not directly accessible. |
| | 7008 | */ |
| | 7009 | if (!cur.bag_.isIn(cur.obj_) |
| | 7010 | && !obj.isIn(cur.obj_) |
| | 7011 | && cur.bag_.tryPuttingObjInBag(cur.obj_)) |
| | 7012 | { |
| | 7013 | /* |
| | 7014 | * this routine tried tried to move the object into the |
| | 7015 | * bag - check our held bulk to see if we're in good |
| | 7016 | * enough shape yet |
| | 7017 | */ |
| | 7018 | if (obj.whatIfHeldBy({: getBulkHeld()}, self) <= bulkCapacity) |
| | 7019 | { |
| | 7020 | /* |
| | 7021 | * We've met our condition - there's no need to look |
| | 7022 | * any further. Return, telling the caller we've |
| | 7023 | * performed an implicit command. |
| | 7024 | */ |
| | 7025 | return true; |
| | 7026 | } |
| | 7027 | } |
| | 7028 | } |
| | 7029 | |
| | 7030 | /* |
| | 7031 | * If we get this far, it means that we tried every child object |
| | 7032 | * but failed to find anything that could help. Explain the |
| | 7033 | * problem and abort the command. |
| | 7034 | */ |
| | 7035 | reportFailure(&handsTooFullForMsg, obj); |
| | 7036 | exit; |
| | 7037 | } |
| | 7038 | |
| | 7039 | /* |
| | 7040 | * Check a bulk change of one of my direct contents. |
| | 7041 | */ |
| | 7042 | checkBulkChangeWithin(obj) |
| | 7043 | { |
| | 7044 | local objBulk; |
| | 7045 | |
| | 7046 | /* get the object's new bulk */ |
| | 7047 | objBulk = obj.getEncumberingBulk(self); |
| | 7048 | |
| | 7049 | /* |
| | 7050 | * if this change would cause the object to exceed our |
| | 7051 | * single-item bulk limit, don't allow it |
| | 7052 | */ |
| | 7053 | if (objBulk > maxSingleBulk || objBulk > bulkCapacity) |
| | 7054 | { |
| | 7055 | reportFailure(&becomingTooLargeForActorMsg, obj); |
| | 7056 | exit; |
| | 7057 | } |
| | 7058 | |
| | 7059 | /* |
| | 7060 | * If our total carrying capacity is exceeded with this change, |
| | 7061 | * don't allow it. Note that 'obj' is already among our |
| | 7062 | * contents when this routine is called, so we can simply check |
| | 7063 | * our current total bulk within. |
| | 7064 | */ |
| | 7065 | if (getBulkHeld() > bulkCapacity) |
| | 7066 | { |
| | 7067 | reportFailure(&handsBecomingTooFullForMsg, obj); |
| | 7068 | exit; |
| | 7069 | } |
| | 7070 | } |
| | 7071 | |
| | 7072 | /* |
| | 7073 | * Next available "holding index" value. Each time we pick up an |
| | 7074 | * item, we'll assign it our current holding index value and then |
| | 7075 | * increment our value. This gives us a simple way to keep track of |
| | 7076 | * the order in which we picked up items we're carrying. |
| | 7077 | * |
| | 7078 | * Note that we make the simplifying assumption that an object can |
| | 7079 | * be held by only one actor at a time (multi-location items are |
| | 7080 | * generally not portable), which means that we can use a simple |
| | 7081 | * property in each object being held to store its holding index. |
| | 7082 | */ |
| | 7083 | nextHoldingIndex = 1 |
| | 7084 | |
| | 7085 | /* add an object to my contents */ |
| | 7086 | addToContents(obj) |
| | 7087 | { |
| | 7088 | /* assign the new object our next holding index */ |
| | 7089 | obj.holdingIndex = nextHoldingIndex++; |
| | 7090 | |
| | 7091 | /* inherit default handling */ |
| | 7092 | inherited(obj); |
| | 7093 | } |
| | 7094 | |
| | 7095 | /* |
| | 7096 | * Go to sleep. This is used by the 'Sleep' action to carry out the |
| | 7097 | * command. By default, we simply say that we're not sleepy; actors |
| | 7098 | * can override this to cause other actions. |
| | 7099 | */ |
| | 7100 | goToSleep() |
| | 7101 | { |
| | 7102 | /* simply report that we can't sleep now */ |
| | 7103 | mainReport(&cannotSleepMsg); |
| | 7104 | } |
| | 7105 | |
| | 7106 | /* |
| | 7107 | * My current "posture," which specifies how we're positioned with |
| | 7108 | * respect to our container; this is one of the standard library |
| | 7109 | * posture enum values (Standing, etc.) or another posture added by |
| | 7110 | * the game. |
| | 7111 | */ |
| | 7112 | posture = standing |
| | 7113 | |
| | 7114 | /* |
| | 7115 | * Get a default acknowledgment of a change to our posture. This |
| | 7116 | * should acknowledge the posture so that it tells us the current |
| | 7117 | * posture. This is used for a command such as "stand up" from a |
| | 7118 | * chair, so that we can report the appropriate posture status in |
| | 7119 | * our acknowledgment; we might end up being inside another nested |
| | 7120 | * container after standing up from the chair, so we might not |
| | 7121 | * simply be standing when we're done. |
| | 7122 | */ |
| | 7123 | okayPostureChange() |
| | 7124 | { |
| | 7125 | /* get our nominal container for our current posture */ |
| | 7126 | local cont = location.getNominalActorContainer(posture); |
| | 7127 | |
| | 7128 | /* if the container is visible, let it handle it */ |
| | 7129 | if (cont != nil && gPlayerChar.canSee(cont)) |
| | 7130 | { |
| | 7131 | /* describe via the container */ |
| | 7132 | cont.roomOkayPostureChange(self); |
| | 7133 | } |
| | 7134 | else |
| | 7135 | { |
| | 7136 | /* use the generic library message */ |
| | 7137 | defaultReport(&okayPostureChangeMsg, posture); |
| | 7138 | } |
| | 7139 | } |
| | 7140 | |
| | 7141 | /* |
| | 7142 | * Describe the actor as part of the EXAMINE description of a nested |
| | 7143 | * room containing the actor. 'povActor' is the actor doing the |
| | 7144 | * looking. |
| | 7145 | */ |
| | 7146 | listActorPosture(povActor) |
| | 7147 | { |
| | 7148 | /* get our nominal container for our current posture */ |
| | 7149 | local cont = location.getNominalActorContainer(posture); |
| | 7150 | |
| | 7151 | /* if the container is visible, let it handle it */ |
| | 7152 | if (cont != nil && povActor.canSee(cont)) |
| | 7153 | cont.roomListActorPosture(self); |
| | 7154 | } |
| | 7155 | |
| | 7156 | /* |
| | 7157 | * Stand up. This is used by the 'Stand' action to carry out the |
| | 7158 | * command. |
| | 7159 | */ |
| | 7160 | standUp() |
| | 7161 | { |
| | 7162 | /* if we're already standing, say so */ |
| | 7163 | if (posture == standing) |
| | 7164 | { |
| | 7165 | reportFailure(&alreadyStandingMsg); |
| | 7166 | return; |
| | 7167 | } |
| | 7168 | |
| | 7169 | /* ask the location to make us stand up */ |
| | 7170 | location.makeStandingUp(); |
| | 7171 | } |
| | 7172 | |
| | 7173 | /* |
| | 7174 | * Disembark. This is used by the 'Get out' action to carry out the |
| | 7175 | * command. By default, we'll let the room handle it. |
| | 7176 | */ |
| | 7177 | disembark() |
| | 7178 | { |
| | 7179 | /* let the room handle it */ |
| | 7180 | location.disembarkRoom(); |
| | 7181 | } |
| | 7182 | |
| | 7183 | /* |
| | 7184 | * Set our posture to the given status. By default, we'll simply |
| | 7185 | * set our posture property to the new status, but actors can |
| | 7186 | * override this to handle side effects of the change. |
| | 7187 | */ |
| | 7188 | makePosture(newPosture) |
| | 7189 | { |
| | 7190 | /* remember our new posture */ |
| | 7191 | posture = newPosture; |
| | 7192 | } |
| | 7193 | |
| | 7194 | /* |
| | 7195 | * Display a description of the actor's location from the actor's |
| | 7196 | * point of view. |
| | 7197 | * |
| | 7198 | * If 'verbose' is true, then we'll show the full description in all |
| | 7199 | * cases. Otherwise, we'll show the full description if the actor |
| | 7200 | * hasn't seen the location before, or the terse description if the |
| | 7201 | * actor has previously seen the location. |
| | 7202 | */ |
| | 7203 | lookAround(verbose) |
| | 7204 | { |
| | 7205 | /* turn on the sense cache while we're looking */ |
| | 7206 | libGlobal.enableSenseCache(); |
| | 7207 | |
| | 7208 | /* show a description of my immediate location, if I have one */ |
| | 7209 | if (location != nil) |
| | 7210 | location.lookAroundPov(self, self, verbose); |
| | 7211 | |
| | 7212 | /* turn off the sense cache now that we're done */ |
| | 7213 | libGlobal.disableSenseCache(); |
| | 7214 | } |
| | 7215 | |
| | 7216 | /* |
| | 7217 | * Adjust a table of visible objects for 'look around'. By default, |
| | 7218 | * we remove any explicitly excluded objects. |
| | 7219 | */ |
| | 7220 | adjustLookAroundTable(tab, pov, actor) |
| | 7221 | { |
| | 7222 | /* remove any explicitly excluded objects */ |
| | 7223 | foreach (local cur in excludeFromLookAroundList) |
| | 7224 | tab.removeElement(cur); |
| | 7225 | |
| | 7226 | /* inherit the base handling */ |
| | 7227 | inherited(tab, pov, actor); |
| | 7228 | } |
| | 7229 | |
| | 7230 | /* |
| | 7231 | * Add an object to the 'look around' exclusion list. Returns true |
| | 7232 | * if the object was already in the list, nil if not. |
| | 7233 | */ |
| | 7234 | excludeFromLookAround(obj) |
| | 7235 | { |
| | 7236 | /* |
| | 7237 | * if the object is already in the list, don't add it again - |
| | 7238 | * just tell the caller it's already there |
| | 7239 | */ |
| | 7240 | if (excludeFromLookAroundList.indexOf(obj) != nil) |
| | 7241 | return true; |
| | 7242 | |
| | 7243 | /* add it to the list and tell the caller it wasn't already there */ |
| | 7244 | excludeFromLookAroundList.append(obj); |
| | 7245 | return nil; |
| | 7246 | } |
| | 7247 | |
| | 7248 | /* remove an object from the 'look around' exclusion list */ |
| | 7249 | unexcludeFromLookAround(obj) |
| | 7250 | { |
| | 7251 | excludeFromLookAroundList.removeElement(obj); |
| | 7252 | } |
| | 7253 | |
| | 7254 | /* |
| | 7255 | * Our list of objects explicitly excluded from 'look around'. These |
| | 7256 | * objects will be suppressed from any sort of listing (including in |
| | 7257 | * the room's contents list and in special descriptions) in 'look |
| | 7258 | * around' when this actor is doing the looking. |
| | 7259 | */ |
| | 7260 | excludeFromLookAroundList = perInstance(new Vector(5)) |
| | 7261 | |
| | 7262 | /* |
| | 7263 | * Get the location into which objects should be moved when the |
| | 7264 | * actor drops them with an explicit 'drop' command. By default, we |
| | 7265 | * return the drop destination of our current container. |
| | 7266 | */ |
| | 7267 | getDropDestination(objToDrop, path) |
| | 7268 | { |
| | 7269 | return (location != nil |
| | 7270 | ? location.getDropDestination(objToDrop, path) |
| | 7271 | : nil); |
| | 7272 | } |
| | 7273 | |
| | 7274 | /* |
| | 7275 | * The senses that determine scope for this actor. An actor might |
| | 7276 | * possess only a subset of the defined sense. |
| | 7277 | * |
| | 7278 | * By default, we give each actor all of the human senses that we |
| | 7279 | * define, except touch. In general, merely being able to touch an |
| | 7280 | * object doesn't put the object in scope, because if an object |
| | 7281 | * isn't noticed through some other sense, touch would only make an |
| | 7282 | * object accessible if it's within arm's reach, which for our |
| | 7283 | * purposes means that the object is being held directly by the |
| | 7284 | * actor. Imagine an actor in a dark room: lots of things might be |
| | 7285 | * touchable in the sense that there's no physical barrier to |
| | 7286 | * touching them, but without some other sense to locate the |
| | 7287 | * objects, the actor wouldn't have any way of knowing where to |
| | 7288 | * reach to touch things, so they're not in scope. So, touch isn't |
| | 7289 | * a scope sense. |
| | 7290 | */ |
| | 7291 | scopeSenses = [sight, sound, smell] |
| | 7292 | |
| | 7293 | /* |
| | 7294 | * "Sight-like" senses: these are the senses that operate like sight |
| | 7295 | * for the actor, and which the actor can use to determine the names |
| | 7296 | * of objects and the spatial relationships between objects. These |
| | 7297 | * senses should operate passively, in the sense that they should |
| | 7298 | * tend to collect sensory input continuously and without explicit |
| | 7299 | * action by the actor, the way sight does and the way touch, for |
| | 7300 | * example, does not. These senses should also operate instantly, |
| | 7301 | * in the sense that the sense can reasonably take in most or all of |
| | 7302 | * a location at one time. |
| | 7303 | * |
| | 7304 | * These senses are used to determine what objects should be listed |
| | 7305 | * in room descriptions, for example. |
| | 7306 | * |
| | 7307 | * By default, the only sight-like sense is sight, since other human |
| | 7308 | * senses don't normally provide a clear picture of the spatial |
| | 7309 | * relationships among objects. (Touch could with some degree of |
| | 7310 | * effort, but it can't operate passively or instantly, since |
| | 7311 | * deliberate and time-consuming action would be necessary.) |
| | 7312 | * |
| | 7313 | * An actor can have more than one sight-like sense, in which case |
| | 7314 | * the senses will act effectively as one sense that can reach the |
| | 7315 | * union of objects reachable through the individual senses. |
| | 7316 | */ |
| | 7317 | sightlikeSenses = [sight] |
| | 7318 | |
| | 7319 | /* |
| | 7320 | * Hearing-like senses. These are senses that the actor can use to |
| | 7321 | * hear objects. |
| | 7322 | */ |
| | 7323 | hearinglikeSenses = [sound] |
| | 7324 | |
| | 7325 | /* |
| | 7326 | * Smell-like senses. These are senses that the actor can use to |
| | 7327 | * smell objects. |
| | 7328 | */ |
| | 7329 | smelllikeSenses = [smell] |
| | 7330 | |
| | 7331 | /* |
| | 7332 | * Communication senses: these are the senses through which the |
| | 7333 | * actor can communicate directly with other actors through commands |
| | 7334 | * and messages. |
| | 7335 | * |
| | 7336 | * Conceptually, these senses are intended to be only those senses |
| | 7337 | * that the actors would *naturally* use to communicate, because |
| | 7338 | * senses in this list allow direct communications via the most |
| | 7339 | * ordinary game commands, such as "bob, go east". |
| | 7340 | * |
| | 7341 | * If some form of indirect communication is possible via a sense, |
| | 7342 | * but that form is not something the actor would think of as the |
| | 7343 | * most natural, default form of communication, it should *not* be |
| | 7344 | * in this list. For example, two sighted persons who can see one |
| | 7345 | * another but cannot hear one another could still communicate by |
| | 7346 | * writing messages on pieces of paper, but they would ordinarily |
| | 7347 | * communicate by talking. In such a case, sound should be in the |
| | 7348 | * list but sight should not be, because sight is not a natural, |
| | 7349 | * default form of communications for the actors. |
| | 7350 | */ |
| | 7351 | communicationSenses = [sound] |
| | 7352 | |
| | 7353 | /* |
| | 7354 | * Determine if I can communicate with the given character via a |
| | 7355 | * natural, default form of communication that we share with the |
| | 7356 | * other character. This determines if I can talk to the other |
| | 7357 | * character. We'll return true if I can talk to the other actor, |
| | 7358 | * nil if not. |
| | 7359 | * |
| | 7360 | * In order for the player character to issue a command to a |
| | 7361 | * non-player character (as in "bob, go east"), the NPC must be able |
| | 7362 | * to sense the PC via at least one communication sense that the two |
| | 7363 | * actors have in common. |
| | 7364 | * |
| | 7365 | * Likewise, in order for a non-player character to say something to |
| | 7366 | * the player, the player must be able to sense the NPC via at least |
| | 7367 | * one communication sense that the two actors have in common. |
| | 7368 | */ |
| | 7369 | canTalkTo(actor) |
| | 7370 | { |
| | 7371 | local common; |
| | 7372 | |
| | 7373 | /* |
| | 7374 | * first, get a list of the communications senses that we have |
| | 7375 | * in common with the other actor - we must have a sense channel |
| | 7376 | * via this sense |
| | 7377 | */ |
| | 7378 | common = communicationSenses.intersect(actor.communicationSenses); |
| | 7379 | |
| | 7380 | /* |
| | 7381 | * if there are no common senses, we can't communicate, |
| | 7382 | * regardless of our physical proximity |
| | 7383 | */ |
| | 7384 | if (common == []) |
| | 7385 | return nil; |
| | 7386 | |
| | 7387 | /* |
| | 7388 | * Determine how well the other actor can sense me in these |
| | 7389 | * senses. Note that all that matters it that the actor can |
| | 7390 | * hear me, because we're determine if I can talk to the other |
| | 7391 | * actor - it doesn't matter if I can hear the other actor. |
| | 7392 | */ |
| | 7393 | foreach (local curSense in common) |
| | 7394 | { |
| | 7395 | local result; |
| | 7396 | |
| | 7397 | /* |
| | 7398 | * determine how well the other actor can sense me in this |
| | 7399 | * sense |
| | 7400 | */ |
| | 7401 | result = actor.senseObj(curSense, self); |
| | 7402 | |
| | 7403 | /* check whether or not this is good enough */ |
| | 7404 | if (actor.canBeTalkedTo(self, curSense, result)) |
| | 7405 | return true; |
| | 7406 | } |
| | 7407 | |
| | 7408 | /* |
| | 7409 | * if we get this far, we didn't find any senses with a clear |
| | 7410 | * enough communications channel - we can't talk to the other |
| | 7411 | * actor |
| | 7412 | */ |
| | 7413 | return nil; |
| | 7414 | } |
| | 7415 | |
| | 7416 | /* |
| | 7417 | * Determine whether or not I can understand an attempt by another |
| | 7418 | * actor to talk to me. 'talker' is the actor doing the talking. |
| | 7419 | * 'sense' is the sense we're testing; this will always be a sense |
| | 7420 | * in our communicationSenses list, and will always be a |
| | 7421 | * communications sense we have in common with the other actor. |
| | 7422 | * 'info' is a SenseInfo object giving information on the clarity of |
| | 7423 | * the sense path to the other actor. |
| | 7424 | * |
| | 7425 | * We return true if we can understand the communication, nil if |
| | 7426 | * not. There is no middle ground where we can partially |
| | 7427 | * understand; we can either understand or not. |
| | 7428 | * |
| | 7429 | * Note that this routine is concerned only with our ability to |
| | 7430 | * sense the communication. The result here should NOT pay any |
| | 7431 | * attention to whether or not we can actually communicate given a |
| | 7432 | * clear sense path - for example, this routine should not reflect |
| | 7433 | * whether or not we have a spoken language in common with the other |
| | 7434 | * actor. |
| | 7435 | * |
| | 7436 | * This is a service method for canTalkTo. This is broken out as a |
| | 7437 | * separate method so that individual actors can override the |
| | 7438 | * necessary conditions for communications in particular senses. |
| | 7439 | */ |
| | 7440 | canBeTalkedTo(talker, sense, info) |
| | 7441 | { |
| | 7442 | /* |
| | 7443 | * By default, we allow communication if the sense path is |
| | 7444 | * transparent or distant. We don't care what the sense is, |
| | 7445 | * since we know we'll never be asked about a sense that's not |
| | 7446 | * in our communicationSenses list. |
| | 7447 | */ |
| | 7448 | return info.trans is in (transparent, distant); |
| | 7449 | } |
| | 7450 | |
| | 7451 | /* |
| | 7452 | * Flag: we wait for commands issued to other actors to complete |
| | 7453 | * before we get another turn. If this is true, then whenever we |
| | 7454 | * issue a command to another actor ("bob, go north"), we will not |
| | 7455 | * get another turn until the other actor has finished executing the |
| | 7456 | * full set of commands we issued. |
| | 7457 | * |
| | 7458 | * By default, this is true, which means that we wait for other |
| | 7459 | * actors to finish all of the commands we issue before we take |
| | 7460 | * another turn. |
| | 7461 | * |
| | 7462 | * If this is set to nil, we'll continue to take turns while the |
| | 7463 | * other actor carries out our commands. In this case, the only |
| | 7464 | * time cost to us of issuing a command is given by orderingTime(), |
| | 7465 | * which normally takes one turn for issuing a command, regardless |
| | 7466 | * of the command's complexity. Some games might wish to use this |
| | 7467 | * mode for interesting effects with NPC's carrying out commands in |
| | 7468 | * parallel with the player, but it's an unconventional style that |
| | 7469 | * some players might find confusing, so we don't use this mode by |
| | 7470 | * default. |
| | 7471 | */ |
| | 7472 | issueCommandsSynchronously = true |
| | 7473 | |
| | 7474 | /* |
| | 7475 | * Flag: the "target actor" of the command line automatically reverts |
| | 7476 | * to this actor at the end of a sentence, when this actor is the |
| | 7477 | * issuer of a command. If this flag is nil, an explicit target |
| | 7478 | * actor stays in effect until the next explicit target actor (or the |
| | 7479 | * end of the entire command line, if no other explicit target actors |
| | 7480 | * are named); if this flag is true, a target actor is in effect only |
| | 7481 | * until the end of a sentence. |
| | 7482 | * |
| | 7483 | * Consider this command line: |
| | 7484 | * |
| | 7485 | * >Bob, go north and get fuel cell. Get log tape. |
| | 7486 | * |
| | 7487 | * If this flag is nil, then the second sentence ("get log tape") is |
| | 7488 | * interpreted as a command to Bob, because Bob is explicitly |
| | 7489 | * designated as the target of the command, and this remains in |
| | 7490 | * effect until the end of the entire command line. |
| | 7491 | * |
| | 7492 | * If this flag is true, on the other hand, then the second sentence |
| | 7493 | * is interpreted as a command to the player character, because the |
| | 7494 | * target actor designation ("Bob,") lasts only until the end of the |
| | 7495 | * sentence. Once a new sentence begins, we revert to the issuing |
| | 7496 | * actor (the player character, since the command came from the |
| | 7497 | * player via the keyboard). |
| | 7498 | */ |
| | 7499 | revertTargetActorAtEndOfSentence = nil |
| | 7500 | |
| | 7501 | /* |
| | 7502 | * The amount of time, in game clock units, it takes me to issue an |
| | 7503 | * order to another actor. By default, it takes one unit (which is |
| | 7504 | * usually equal to one turn) to issue a command to another actor. |
| | 7505 | * However, if we are configured to wait for our issued commands to |
| | 7506 | * complete in full, the ordering time is zero; we don't need any |
| | 7507 | * extra wait time in this case because we'll wait the full length |
| | 7508 | * of the issued command to begin with. |
| | 7509 | */ |
| | 7510 | orderingTime(targetActor) |
| | 7511 | { |
| | 7512 | return issueCommandsSynchronously ? 0 : 1; |
| | 7513 | } |
| | 7514 | |
| | 7515 | /* |
| | 7516 | * Wait for completion of a command that we issued to another actor. |
| | 7517 | * The parser calls this routine after each time we issue a command |
| | 7518 | * to another actor. |
| | 7519 | * |
| | 7520 | * If we're configured to wait for completion of orders given to |
| | 7521 | * other actors before we get another turn, we'll set ourselves up |
| | 7522 | * in waiting mode. Otherwise, we'll do nothing. |
| | 7523 | */ |
| | 7524 | waitForIssuedCommand(targetActor) |
| | 7525 | { |
| | 7526 | /* if we can issue commands asynchronously, there's nothing to do */ |
| | 7527 | if (!issueCommandsSynchronously) |
| | 7528 | return; |
| | 7529 | |
| | 7530 | /* |
| | 7531 | * Add an empty pending command at the end of the target actor's |
| | 7532 | * queue. This command won't do anything when executed; its |
| | 7533 | * purpose is to let us track whether or not the target is still |
| | 7534 | * working on commands we have issued up to this point, which we |
| | 7535 | * can tell by looking to see whether our empty command is still |
| | 7536 | * in the actor's queue. |
| | 7537 | * |
| | 7538 | * Note that we can't simply wait until the actor's queue is |
| | 7539 | * empty, because the actor could acquire new commands while |
| | 7540 | * it's working on our pending commands, and we wouldn't want to |
| | 7541 | * wait for those to finish. Adding a dummy pending command is |
| | 7542 | * a reliable way of tracking the actor's queue, because any |
| | 7543 | * changes to the target actor's command queue will leave our |
| | 7544 | * dummy command in its proper place until the target actor gets |
| | 7545 | * around to executing it, at which point it will be removed. |
| | 7546 | * |
| | 7547 | * Remember the dummy pending command in a property of self, so |
| | 7548 | * that we can check later to determine when the command has |
| | 7549 | * finished. |
| | 7550 | */ |
| | 7551 | waitingForActor = targetActor; |
| | 7552 | waitingForInfo = new PendingCommandMarker(self); |
| | 7553 | targetActor.pendingCommand.append(waitingForInfo); |
| | 7554 | } |
| | 7555 | |
| | 7556 | /* |
| | 7557 | * Synchronous command processing: the target actor and dummy |
| | 7558 | * pending command we're waiting for. When these are non-nil, we |
| | 7559 | * won't take another turn until the given PendingCommandInfo has |
| | 7560 | * been removed from the given target actor's command queue. |
| | 7561 | */ |
| | 7562 | waitingForActor = nil |
| | 7563 | waitingForInfo = nil |
| | 7564 | |
| | 7565 | /* |
| | 7566 | * Add the given actor to the list of actors accompanying my travel |
| | 7567 | * on the current turn. This does NOT set an actor in "follow mode" |
| | 7568 | * or "accompany mode" or anything like that - don't use this to make |
| | 7569 | * an actor follow me around. Instead, this makes the given actor go |
| | 7570 | * with us for the CURRENT travel only - the travel we're already in |
| | 7571 | * the process of performing to process the current TravelVia action. |
| | 7572 | */ |
| | 7573 | addAccompanyingActor(actor) |
| | 7574 | { |
| | 7575 | /* if we don't have the accompanying actor vector yet, create it */ |
| | 7576 | if (accompanyingActors == nil) |
| | 7577 | accompanyingActors = new Vector(8); |
| | 7578 | |
| | 7579 | /* add the actor to my list */ |
| | 7580 | accompanyingActors.append(actor); |
| | 7581 | } |
| | 7582 | |
| | 7583 | /* |
| | 7584 | * My vector of actors who are accompanying me. |
| | 7585 | * |
| | 7586 | * This is for internal bookkeeping only, and it applies to the |
| | 7587 | * current travel only. This is NOT a general "follow mode" setting, |
| | 7588 | * and it shouldn't be used to get me to follow another actor or |
| | 7589 | * another actor to follow me. To make me accompany another actor, |
| | 7590 | * simply override accompanyTravel() so that it returns a suitable |
| | 7591 | * ActorState object. |
| | 7592 | */ |
| | 7593 | accompanyingActors = nil |
| | 7594 | |
| | 7595 | /* |
| | 7596 | * Get the list of objects I can follow. This is a list of all of |
| | 7597 | * the objects which I have seen departing a location - these are |
| | 7598 | * all in scope for 'follow' commands. |
| | 7599 | */ |
| | 7600 | getFollowables() |
| | 7601 | { |
| | 7602 | /* return the list of the objects we know about */ |
| | 7603 | return followables_.mapAll({x: x.obj}); |
| | 7604 | } |
| | 7605 | |
| | 7606 | /* |
| | 7607 | * Do I track departing objects for following the given object? |
| | 7608 | * |
| | 7609 | * By default, the player character tracks everyone, and NPC's track |
| | 7610 | * only the actor they're presently tasked to follow. Most NPC's |
| | 7611 | * will never accept 'follow' commands, so there's no need to track |
| | 7612 | * everyone all the time; for efficiency, we take advantage of this |
| | 7613 | * assumption so that we can avoid storing a bunch of tracking |
| | 7614 | * information that will never be used. |
| | 7615 | */ |
| | 7616 | wantsFollowInfo(obj) |
| | 7617 | { |
| | 7618 | /* |
| | 7619 | * by default, the player character tracks everyone, and NPC's |
| | 7620 | * track only the object (if any) they're currently tasked to |
| | 7621 | * follow |
| | 7622 | */ |
| | 7623 | return isPlayerChar() || followingActor == obj; |
| | 7624 | } |
| | 7625 | |
| | 7626 | /* |
| | 7627 | * Receive notification that an object is leaving its current |
| | 7628 | * location as a result of the action we're currently processing. |
| | 7629 | * Actors (and possibly other objects) will broadcast this |
| | 7630 | * notification to all Actor objects connected in any way by |
| | 7631 | * containment when they move under their own power (such as with |
| | 7632 | * Actor.travelTo) to a new location. We'll keep tracking |
| | 7633 | * information if we are configured to keep tracking information for |
| | 7634 | * the given object and we can see the given object. Note that this |
| | 7635 | * is called when the object is still at the source end of the travel |
| | 7636 | * - the important thing is that we see the object departing. |
| | 7637 | * |
| | 7638 | * 'obj' is the object that is seen to be leaving, and 'conn' is the |
| | 7639 | * TravelConnector it is taking. |
| | 7640 | * |
| | 7641 | * 'conn' is the connector being traversed. If we're simply being |
| | 7642 | * observed in this location (as in a call to setHasSeen), rather |
| | 7643 | * than being observed to leave the location, the connector will be |
| | 7644 | * nil. |
| | 7645 | * |
| | 7646 | * 'from' is the effective starting location of the travel. This |
| | 7647 | * isn't necessarily the departing object's location, since the |
| | 7648 | * departing object could be inside a vehicle or some other kind of |
| | 7649 | * traveler object. |
| | 7650 | * |
| | 7651 | * Note that this notification is sent only to actors with some sort |
| | 7652 | * of containment connection to the object that's moving, because a |
| | 7653 | * containment connection is necessary for there to be a sense |
| | 7654 | * connection. |
| | 7655 | */ |
| | 7656 | trackFollowInfo(obj, conn, from) |
| | 7657 | { |
| | 7658 | local info; |
| | 7659 | |
| | 7660 | /* |
| | 7661 | * If we're not tracking the given object, or we can't see the |
| | 7662 | * given object, ignore the notification. In addition, we |
| | 7663 | * obviously have no need to track ourselves.x |
| | 7664 | */ |
| | 7665 | if (obj == self || !wantsFollowInfo(obj) || !canSee(obj)) |
| | 7666 | return; |
| | 7667 | |
| | 7668 | /* |
| | 7669 | * If we already have a FollowInfo for the given object, re-use |
| | 7670 | * the existing one; otherwise, create a new one and add it to |
| | 7671 | * our tracking list. |
| | 7672 | */ |
| | 7673 | info = followables_.valWhich({x: x.obj == obj}); |
| | 7674 | if (info == nil) |
| | 7675 | { |
| | 7676 | /* we don't have an existing one - create a new one */ |
| | 7677 | info = new FollowInfo(); |
| | 7678 | info.obj = obj; |
| | 7679 | |
| | 7680 | /* add it to our list */ |
| | 7681 | followables_ += info; |
| | 7682 | } |
| | 7683 | |
| | 7684 | /* remember information about the travel */ |
| | 7685 | info.connector = conn; |
| | 7686 | info.sourceLocation = from; |
| | 7687 | } |
| | 7688 | |
| | 7689 | /* |
| | 7690 | * Get information on what to do to make this actor follow the given |
| | 7691 | * object. This returns a FollowInfo object that reports our last |
| | 7692 | * knowledge of the given object's location and departure, or nil if |
| | 7693 | * we don't know anything about how to follow the actor. |
| | 7694 | */ |
| | 7695 | getFollowInfo(obj) |
| | 7696 | { |
| | 7697 | return followables_.valWhich({x: x.obj == obj}); |
| | 7698 | } |
| | 7699 | |
| | 7700 | /* |
| | 7701 | * By default, all actors are followable. |
| | 7702 | */ |
| | 7703 | verifyFollowable() |
| | 7704 | { |
| | 7705 | return true; |
| | 7706 | } |
| | 7707 | |
| | 7708 | /* |
| | 7709 | * Verify a "follow" command being performed by this actor. |
| | 7710 | */ |
| | 7711 | actorVerifyFollow(obj) |
| | 7712 | { |
| | 7713 | /* |
| | 7714 | * check to see if we're in the same effective follow location |
| | 7715 | * as the target; if we are, it makes no sense to follow the |
| | 7716 | * target, since we're already effectively at the same place |
| | 7717 | */ |
| | 7718 | if (obj.location != nil |
| | 7719 | && (location.effectiveFollowLocation |
| | 7720 | == obj.location.effectiveFollowLocation)) |
| | 7721 | { |
| | 7722 | /* |
| | 7723 | * We're in the same location as the target. If we're the |
| | 7724 | * player character, this makes no sense, because the player |
| | 7725 | * character can't go into follow mode (as that would take |
| | 7726 | * away the player's ability to control the player |
| | 7727 | * character). If we're an NPC, though, this simply tells |
| | 7728 | * us to go into follow mode for the target, so there's |
| | 7729 | * nothing wrong with it. |
| | 7730 | */ |
| | 7731 | if (isPlayerChar) |
| | 7732 | { |
| | 7733 | /* |
| | 7734 | * The target is right here, but we're the player |
| | 7735 | * character, so it makes no sense for us to go into |
| | 7736 | * follow mode. If we can see the target, complain that |
| | 7737 | * it's already here; if not, we can only assume it's |
| | 7738 | * here, but we can't know for sure. |
| | 7739 | */ |
| | 7740 | if (canSee(obj)) |
| | 7741 | illogicalNow(&followAlreadyHereMsg); |
| | 7742 | else |
| | 7743 | illogicalNow(&followAlreadyHereInDarkMsg); |
| | 7744 | } |
| | 7745 | } |
| | 7746 | else if (!canSee(obj)) |
| | 7747 | { |
| | 7748 | /* |
| | 7749 | * The target isn't here, and we can't see it from here, so |
| | 7750 | * we must want to follow it to its current location. Get |
| | 7751 | * information on how we will follow the target. If there's |
| | 7752 | * no such information, we obviously can't do any following |
| | 7753 | * because we never saw the target go anywhere in the first |
| | 7754 | * place. |
| | 7755 | */ |
| | 7756 | if (getFollowInfo(obj) == nil) |
| | 7757 | { |
| | 7758 | /* we've never heard of the target */ |
| | 7759 | illogicalNow(&followUnknownMsg); |
| | 7760 | } |
| | 7761 | } |
| | 7762 | } |
| | 7763 | |
| | 7764 | /* |
| | 7765 | * Carry out a "follow" command being performed by this actor. |
| | 7766 | */ |
| | 7767 | actorActionFollow(obj) |
| | 7768 | { |
| | 7769 | local canSeeObj; |
| | 7770 | |
| | 7771 | /* note whether or not we can see the target */ |
| | 7772 | canSeeObj = canSee(obj); |
| | 7773 | |
| | 7774 | /* |
| | 7775 | * If we're not the PC, check to see if this is a follow-mode |
| | 7776 | * request; otherwise, try to go to the location of the target. |
| | 7777 | */ |
| | 7778 | if (!isPlayerChar && canSeeObj) |
| | 7779 | { |
| | 7780 | /* |
| | 7781 | * If we're not already following this actor, acknowledge the |
| | 7782 | * request and go into 'follow' mode. If we're already |
| | 7783 | * following this actor, and we didn't issue the command to |
| | 7784 | * ourself, let them know we're already in the requested |
| | 7785 | * mode. Otherwise, ignore it silently - if we issued the |
| | 7786 | * command to ourself, it's because we're just executing our |
| | 7787 | * own 'follow' mode imperative. |
| | 7788 | */ |
| | 7789 | if (followingActor != obj) |
| | 7790 | { |
| | 7791 | /* let them know we're going to follow the actor now */ |
| | 7792 | reportAfter(&okayFollowModeMsg); |
| | 7793 | |
| | 7794 | /* go into follow mode */ |
| | 7795 | followingActor = obj; |
| | 7796 | } |
| | 7797 | else if (gIssuingActor != self) |
| | 7798 | { |
| | 7799 | /* let them know we're already in follow mode */ |
| | 7800 | reportAfter(&alreadyFollowModeMsg); |
| | 7801 | } |
| | 7802 | |
| | 7803 | /* |
| | 7804 | * if we're already in the target's effective follow |
| | 7805 | * location, that's all we need to do |
| | 7806 | */ |
| | 7807 | if (location.effectiveFollowLocation |
| | 7808 | == obj.location.effectiveFollowLocation) |
| | 7809 | return; |
| | 7810 | } |
| | 7811 | |
| | 7812 | /* |
| | 7813 | * If we can see the target, AND we're in the same top-level |
| | 7814 | * location as the target, then simply use a "local travel" |
| | 7815 | * operation to move into the same location. This only works |
| | 7816 | * with targets that are within the same top-level location, |
| | 7817 | * since that's the whole point of the local-travel routines. |
| | 7818 | * For non-local travel, we need to perform a full-fledged travel |
| | 7819 | * command instead. |
| | 7820 | */ |
| | 7821 | if (canSeeObj && isIn(obj.getOutermostRoom())) |
| | 7822 | { |
| | 7823 | /* |
| | 7824 | * We have no information, so we will only have made it past |
| | 7825 | * verification if we can see the other actor from our |
| | 7826 | * current location. Try moving to the other actor's |
| | 7827 | * effective follow location. |
| | 7828 | */ |
| | 7829 | obj.location.effectiveFollowLocation.checkMovingActorInto(true); |
| | 7830 | |
| | 7831 | /* |
| | 7832 | * Since checkMovingActorInto will do its work through |
| | 7833 | * implicit actions, if we're the player character, then the |
| | 7834 | * entire action will have been performed implicitly, so we |
| | 7835 | * won't have a real report for the series of generated |
| | 7836 | * actions, just implied action announcements. If we're an |
| | 7837 | * NPC, on the other hand, we'll generate the full reports, |
| | 7838 | * since NPC implied actions simply show what the actor is |
| | 7839 | * doing. So, if we're the PC, generate an additional |
| | 7840 | * default acknowledgment of the 'follow' action. |
| | 7841 | */ |
| | 7842 | if (isPlayerChar) |
| | 7843 | defaultReport(&okayFollowInSightMsg, |
| | 7844 | location.effectiveFollowLocation); |
| | 7845 | } |
| | 7846 | else |
| | 7847 | { |
| | 7848 | local info; |
| | 7849 | local srcLoc; |
| | 7850 | |
| | 7851 | /* get the information on how to follow the target */ |
| | 7852 | info = getFollowInfo(obj); |
| | 7853 | |
| | 7854 | /* get the effective follow location we have to be in */ |
| | 7855 | srcLoc = info.sourceLocation.effectiveFollowLocation; |
| | 7856 | |
| | 7857 | /* if there's no connector, we can't go anywhere */ |
| | 7858 | if (info.connector == nil) |
| | 7859 | { |
| | 7860 | /* |
| | 7861 | * We have no departure information, so we can't follow |
| | 7862 | * the actor. If we're currently within sight of the |
| | 7863 | * location where we last saw the actor, it means that we |
| | 7864 | * saw the actor here, then went somewhere else, then |
| | 7865 | * came back, and in our absence the actor itself |
| | 7866 | * departed. In this case, report that we don't know |
| | 7867 | * where the actor went. |
| | 7868 | * |
| | 7869 | * If we're not in sight of the location where we last |
| | 7870 | * saw the actor, then instead remind the player of where |
| | 7871 | * that was. |
| | 7872 | */ |
| | 7873 | if (canSee(srcLoc)) |
| | 7874 | { |
| | 7875 | /* |
| | 7876 | * we're where we last saw the actor, but the actor |
| | 7877 | * must have departed while we were away - so we |
| | 7878 | * simply don't know where the actor went |
| | 7879 | */ |
| | 7880 | reportFailure(&followUnknownMsg); |
| | 7881 | } |
| | 7882 | else |
| | 7883 | { |
| | 7884 | /* |
| | 7885 | * we've gone somewhere else since we last saw the |
| | 7886 | * actor, so remind the player of where it was that |
| | 7887 | * we saw the actor |
| | 7888 | */ |
| | 7889 | reportFailure(&cannotFollowFromHereMsg, srcLoc); |
| | 7890 | } |
| | 7891 | |
| | 7892 | /* in any case, that's all we can do now */ |
| | 7893 | return; |
| | 7894 | } |
| | 7895 | |
| | 7896 | /* |
| | 7897 | * Before we can follow the target, we must be in the same |
| | 7898 | * effective location that the target was in when we |
| | 7899 | * observed the target leaving. |
| | 7900 | */ |
| | 7901 | if (location.effectiveFollowLocation != srcLoc) |
| | 7902 | { |
| | 7903 | /* |
| | 7904 | * If we can't even see the effective follow location, we |
| | 7905 | * must have last observed the other actor traveling from |
| | 7906 | * an unrelated location. In this case, simply say that |
| | 7907 | * we don't know where the followee went. |
| | 7908 | */ |
| | 7909 | if (!canSee(srcLoc)) |
| | 7910 | { |
| | 7911 | reportFailure(&cannotFollowFromHereMsg, srcLoc); |
| | 7912 | return; |
| | 7913 | } |
| | 7914 | |
| | 7915 | /* |
| | 7916 | * Try moving into the same location, by invoking the |
| | 7917 | * pre-condition handler for moving me into the |
| | 7918 | * effective follow location from our memory of the |
| | 7919 | * actor's travel. We *could* run this as an actual |
| | 7920 | * precondition, but it's easier to run it here now that |
| | 7921 | * we've sorted out exactly what we want to do. |
| | 7922 | */ |
| | 7923 | srcLoc.checkMovingActorInto(true); |
| | 7924 | } |
| | 7925 | |
| | 7926 | /* perform a TravelVia action on the connector */ |
| | 7927 | nestedAction(TravelVia, info.connector); |
| | 7928 | } |
| | 7929 | } |
| | 7930 | |
| | 7931 | /* |
| | 7932 | * Our list of followable information. Each entry in this list is a |
| | 7933 | * FollowInfo object that tracks a particular followable. |
| | 7934 | */ |
| | 7935 | followables_ = [] |
| | 7936 | |
| | 7937 | /* determine if I've ever seen the given object */ |
| | 7938 | hasSeen(obj) { return obj.(seenProp); } |
| | 7939 | |
| | 7940 | /* mark the object to remember that I've seen it */ |
| | 7941 | setHasSeen(obj) { obj.noteSeenBy(self, seenProp); } |
| | 7942 | |
| | 7943 | /* receive notification that another actor is observing us */ |
| | 7944 | noteSeenBy(actor, prop) |
| | 7945 | { |
| | 7946 | /* do the standard work to remember that we've been seen */ |
| | 7947 | inherited(actor, prop); |
| | 7948 | |
| | 7949 | /* |
| | 7950 | * Update the follow tracking information with the latest |
| | 7951 | * observed location. We're merely observing the fact that the |
| | 7952 | * actor is here, not that the actor is departing, so the |
| | 7953 | * connector is nil. |
| | 7954 | * |
| | 7955 | * The point of noting the actor's presence in the "follow info" |
| | 7956 | * is that we want to replace any previous memory we have of the |
| | 7957 | * actor departing from another location. Now that we know where |
| | 7958 | * the actor is, any old memory of the actor having left another |
| | 7959 | * location is now irrelevant. We only keep track of one "follow |
| | 7960 | * info" record per actor, so this new record will replace any |
| | 7961 | * older record. |
| | 7962 | */ |
| | 7963 | actor.trackFollowInfo(self, nil, location); |
| | 7964 | } |
| | 7965 | |
| | 7966 | /* |
| | 7967 | * Determine if I know about the given object. I know about an |
| | 7968 | * object if it's specifically marked as known to me; I also know |
| | 7969 | * about the object if I can see it now, or if I've ever seen it in |
| | 7970 | * the past. |
| | 7971 | */ |
| | 7972 | knowsAbout(obj) { return canSee(obj) || hasSeen(obj) || obj.(knownProp); } |
| | 7973 | |
| | 7974 | /* mark the object as known to me */ |
| | 7975 | setKnowsAbout(obj) { obj.(knownProp) = true; } |
| | 7976 | |
| | 7977 | /* |
| | 7978 | * My 'seen' property. By default, this is simply 'seen', which |
| | 7979 | * means that we don't distinguish who's seen what - in other words, |
| | 7980 | * there's a single, global 'seen' flag per object, and if anyone's |
| | 7981 | * ever seen something, then we consider that to mean everyone has |
| | 7982 | * seen it. |
| | 7983 | * |
| | 7984 | * Some games might want to track each NPC's sight memory |
| | 7985 | * separately, or at least they might want to track it individually |
| | 7986 | * for a few specific NPC's. You can do this by making up a new |
| | 7987 | * property name for each NPC whose sight memory you want to keep |
| | 7988 | * separate, and simply setting 'seenProp' to that property name for |
| | 7989 | * each such NPC. For example, for Bob, you could make the property |
| | 7990 | * bobHasSeen, so in Bob you'd define 'sightProp = &bobHasSeen'. |
| | 7991 | */ |
| | 7992 | seenProp = &seen |
| | 7993 | |
| | 7994 | /* |
| | 7995 | * My 'known' property. By default, this is simply 'known', which |
| | 7996 | * means that we don't distinguish who knows what. |
| | 7997 | * |
| | 7998 | * As with 'seenProp' above, if you want to keep track of each NPC's |
| | 7999 | * knowledge separately, you must override this property for each |
| | 8000 | * NPC who's to have its own knowledge base to use a separate |
| | 8001 | * property name. For example, if you want to keep track of what |
| | 8002 | * Bob knows individually, you could define 'knownProp = &bobKnows' |
| | 8003 | * in Bob. |
| | 8004 | */ |
| | 8005 | knownProp = &isKnown |
| | 8006 | |
| | 8007 | /* |
| | 8008 | * Determine if the actor recognizes the given object as a "topic," |
| | 8009 | * which is an object that represents some knowledge the actor can |
| | 8010 | * use in conversations, consultations, and the like. |
| | 8011 | * |
| | 8012 | * By default, we'll recognize any Topic object marked as known, and |
| | 8013 | * we'll recognize any game object for which our knowsAbout(obj) |
| | 8014 | * returns true. Games might wish to override this in some cases to |
| | 8015 | * limit or expand an actor's knowledge according to what the actor |
| | 8016 | * has experienced of the setting or story. Note that it's often |
| | 8017 | * easier to control actor knowledge using the lower-level |
| | 8018 | * knowsAbout() and setKnowsAbout() methods, though. |
| | 8019 | */ |
| | 8020 | knowsTopic(obj) |
| | 8021 | { |
| | 8022 | /* we know the object as a topic if we know about it at all */ |
| | 8023 | return knowsAbout(obj); |
| | 8024 | } |
| | 8025 | |
| | 8026 | /* |
| | 8027 | * Determine if the given object is a likely topic for a |
| | 8028 | * conversational action performed by this actor. By default, we'll |
| | 8029 | * return true if the topic is known, nil if not. |
| | 8030 | */ |
| | 8031 | isLikelyTopic(obj) |
| | 8032 | { |
| | 8033 | /* if the object is known, it's a possible topic */ |
| | 8034 | return knowsTopic(obj); |
| | 8035 | } |
| | 8036 | |
| | 8037 | /* we are the owner of any TopicEntry objects contained within us */ |
| | 8038 | getTopicOwner() { return self; } |
| | 8039 | |
| | 8040 | /* |
| | 8041 | * Suggest topics of conversation. This is called by the TOPICS |
| | 8042 | * command (in which case 'explicit' is true), and whenever we first |
| | 8043 | * engage a character in a stateful conversation (in which case |
| | 8044 | * 'explicit' is nil). |
| | 8045 | * |
| | 8046 | * We'll show the list of suggested topics associated with our |
| | 8047 | * current conversational partner. If there are no topics, we'll say |
| | 8048 | * nothing unless 'explicit' is true, in which case we'll simply say |
| | 8049 | * that there are no topics that the player character is thinking |
| | 8050 | * about. |
| | 8051 | * |
| | 8052 | * The purpose of this method is to let the game author keep an |
| | 8053 | * "inventory" of topics with this actor for a given conversational |
| | 8054 | * partner. This inventory is meant to represent the topics that on |
| | 8055 | * the player character's mind - things the player character wants to |
| | 8056 | * talk about with the other actor. Note that we're talking about |
| | 8057 | * what the player *character* is thinking about - obviously we don't |
| | 8058 | * know what's on the player's mind. |
| | 8059 | * |
| | 8060 | * When we enter conversation, or when the player asks for advice, |
| | 8061 | * we'll show this inventory. The idea is to help guide the player |
| | 8062 | * through a conversation without the more heavy-handed device of a |
| | 8063 | * formal conversation menu system, so that conversations have a more |
| | 8064 | * free-form feel without leaving the player hunting in the dark for |
| | 8065 | * the magic ASK ABOUT topic. |
| | 8066 | * |
| | 8067 | * The TOPICS system is entirely optional. If a game doesn't specify |
| | 8068 | * any SuggestedTopic objects, then this routine will simply never be |
| | 8069 | * called, and the TOPICS command won't be allowed. Some authors |
| | 8070 | * think it gives away too much to provide a list of topic |
| | 8071 | * suggestions like this, and others don't like anything that smacks |
| | 8072 | * of a menu system because they think it destroys the illusion |
| | 8073 | * created by the text-input command line that the game is boundless. |
| | 8074 | * Authors who feel this way can just ignore the TOPICS system. But |
| | 8075 | * be aware that the illusion of boundlessness isn't always a good |
| | 8076 | * thing for players; hunting around for ASK ABOUT topics can make |
| | 8077 | * the game's limits just as obvious, if not more so, by exposing the |
| | 8078 | * vast number of inputs for which the actor doesn't have a good |
| | 8079 | * response. Players aren't stupid - a string of variations on "I |
| | 8080 | * don't know about that" is just as obviously mechanistic as a |
| | 8081 | * numbered list of menu choices. Using the TOPICS system might be a |
| | 8082 | * good compromise for many authors, since the topic list can help |
| | 8083 | * guide the player to the right questions without making the player |
| | 8084 | * feel straitjacketed by a menu list. |
| | 8085 | */ |
| | 8086 | suggestTopics(explicit) |
| | 8087 | { |
| | 8088 | local actor; |
| | 8089 | |
| | 8090 | /* |
| | 8091 | * if we're talking to someone, look up their suggested topics; |
| | 8092 | * otherwise, we have nothing to suggest |
| | 8093 | */ |
| | 8094 | if ((actor = getCurrentInterlocutor()) != nil) |
| | 8095 | { |
| | 8096 | /* |
| | 8097 | * we're talking to someone - suggest topics appropriate to |
| | 8098 | * the person we're talking to |
| | 8099 | */ |
| | 8100 | actor.suggestTopicsFor(self, explicit); |
| | 8101 | } |
| | 8102 | else if (explicit) |
| | 8103 | { |
| | 8104 | /* we're not talking to anyone, so there's nothing to suggest */ |
| | 8105 | gLibMessages.noTopicsNotTalking; |
| | 8106 | } |
| | 8107 | } |
| | 8108 | |
| | 8109 | /* |
| | 8110 | * Suggest topics that the given actor might want to talk to us |
| | 8111 | * about. The given actor is almost always the player character, |
| | 8112 | * since generally NPC's don't talk to one another using |
| | 8113 | * conversation commands (there'd be no point; they're simple |
| | 8114 | * programmed automata, not full-blown AI's). |
| | 8115 | */ |
| | 8116 | suggestTopicsFor(actor, explicit) |
| | 8117 | { |
| | 8118 | /* by default, let our state suggest topics */ |
| | 8119 | curState.suggestTopicsFor(actor, explicit); |
| | 8120 | } |
| | 8121 | |
| | 8122 | /* |
| | 8123 | * Receive notification that a command is being carried out in our |
| | 8124 | * presence. |
| | 8125 | */ |
| | 8126 | beforeAction() |
| | 8127 | { |
| | 8128 | /* |
| | 8129 | * If another actor is trying to take something in my inventory, |
| | 8130 | * by default, do not allow it. |
| | 8131 | */ |
| | 8132 | if (gActor != self |
| | 8133 | && (gActionIs(Take) || gActionIs(TakeFrom)) |
| | 8134 | && gDobj.isIn(self)) |
| | 8135 | { |
| | 8136 | /* check to see if we want to allow this action */ |
| | 8137 | checkTakeFromInventory(gActor, gDobj); |
| | 8138 | } |
| | 8139 | |
| | 8140 | /* let our state object take a look at the action */ |
| | 8141 | curState.beforeAction(); |
| | 8142 | } |
| | 8143 | |
| | 8144 | /* |
| | 8145 | * Perform any actor-specific processing for an action. The main |
| | 8146 | * command processor invokes this on gActor after notifying nearby |
| | 8147 | * objects via beforeAction(), but before carrying out the main |
| | 8148 | * action of the command. |
| | 8149 | */ |
| | 8150 | actorAction() |
| | 8151 | { |
| | 8152 | /* do nothing by default */ |
| | 8153 | } |
| | 8154 | |
| | 8155 | /* |
| | 8156 | * Receive notification that a command has just been carried out in |
| | 8157 | * our presence. |
| | 8158 | */ |
| | 8159 | afterAction() |
| | 8160 | { |
| | 8161 | /* let the state object handle it */ |
| | 8162 | curState.afterAction(); |
| | 8163 | } |
| | 8164 | |
| | 8165 | /* receive a notification that someone is about to travel */ |
| | 8166 | beforeTravel(traveler, connector) |
| | 8167 | { |
| | 8168 | /* let the state object handle it */ |
| | 8169 | curState.beforeTravel(traveler, connector); |
| | 8170 | |
| | 8171 | /* |
| | 8172 | * If desired, track the departure so that we can follow the |
| | 8173 | * traveler later. First, track the departure of each actor |
| | 8174 | * traveling with the traveler. |
| | 8175 | */ |
| | 8176 | traveler.forEachTravelingActor( |
| | 8177 | {actor: trackFollowInfo(actor, connector, traveler.location)}); |
| | 8178 | |
| | 8179 | /* |
| | 8180 | * if the traveler is distinct from the actors traveling, track |
| | 8181 | * it as well |
| | 8182 | */ |
| | 8183 | if (!traveler.isActorTraveling(traveler)) |
| | 8184 | trackFollowInfo(traveler, connector, traveler.location); |
| | 8185 | } |
| | 8186 | |
| | 8187 | /* receive a notification that someone has just traveled here */ |
| | 8188 | afterTravel(traveler, connector) |
| | 8189 | { |
| | 8190 | /* let the state object handle it */ |
| | 8191 | curState.afterTravel(traveler, connector); |
| | 8192 | } |
| | 8193 | |
| | 8194 | /* |
| | 8195 | * Receive notification that I'm initiating travel. This is called |
| | 8196 | * on the actor performing the travel action before the travel is |
| | 8197 | * actually carried out. |
| | 8198 | */ |
| | 8199 | actorTravel(traveler, connector) |
| | 8200 | { |
| | 8201 | /* |
| | 8202 | * If other actors are accompanying me on this travel, run the |
| | 8203 | * same travel action on the accompanying actors, using nested |
| | 8204 | * actions. |
| | 8205 | */ |
| | 8206 | if (accompanyingActors != nil |
| | 8207 | && accompanyingActors.length() != 0) |
| | 8208 | { |
| | 8209 | /* |
| | 8210 | * Run the same travel action as a nested action on each |
| | 8211 | * accompanying actor. Skip this for any accompanying actor |
| | 8212 | * we're carrying, as they'll naturally go with us as a |
| | 8213 | * result of being carried. |
| | 8214 | */ |
| | 8215 | foreach (local cur in accompanyingActors) |
| | 8216 | { |
| | 8217 | /* if the actor's not being carried, run the same action */ |
| | 8218 | if (!cur.isIn(self)) |
| | 8219 | nestedActorAction(cur, TravelVia, gDobj); |
| | 8220 | } |
| | 8221 | |
| | 8222 | /* |
| | 8223 | * The accompanying actor list applies for this single group |
| | 8224 | * travel command, so now that we've moved everyone, we have |
| | 8225 | * no further need for the list. Clear it out. |
| | 8226 | */ |
| | 8227 | accompanyingActors.removeRange(1, accompanyingActors.length()); |
| | 8228 | } |
| | 8229 | } |
| | 8230 | |
| | 8231 | /* |
| | 8232 | * Check to see if we want to allow another actor to take something |
| | 8233 | * from my inventory. By default, we won't allow it - we'll always |
| | 8234 | * fail the command. |
| | 8235 | */ |
| | 8236 | checkTakeFromInventory(actor, obj) |
| | 8237 | { |
| | 8238 | /* don't allow it - show an error and terminate the command */ |
| | 8239 | mainReport(&willNotLetGoMsg, self, obj); |
| | 8240 | exit; |
| | 8241 | } |
| | 8242 | |
| | 8243 | /* |
| | 8244 | * Build a list of the objects that are explicitly registered to |
| | 8245 | * receive notification when I'm the actor in a command. |
| | 8246 | */ |
| | 8247 | getActorNotifyList() |
| | 8248 | { |
| | 8249 | return actorNotifyList; |
| | 8250 | } |
| | 8251 | |
| | 8252 | /* |
| | 8253 | * Add an item to our registered notification items. These items |
| | 8254 | * are to receive notifications when we're the actor performing a |
| | 8255 | * command. |
| | 8256 | * |
| | 8257 | * Items can be added here if they must be notified of actions |
| | 8258 | * performed by the actor even when the items aren't connected by |
| | 8259 | * containment with the actor at the time of the action. All items |
| | 8260 | * connected to the actor by containment are automatically notified |
| | 8261 | * of each action; only items that must receive notification even |
| | 8262 | * when not in scope need to be registered here. |
| | 8263 | */ |
| | 8264 | addActorNotifyItem(obj) |
| | 8265 | { |
| | 8266 | actorNotifyList += obj; |
| | 8267 | } |
| | 8268 | |
| | 8269 | /* remove an item from the registered notification list */ |
| | 8270 | removeActorNotifyItem(obj) |
| | 8271 | { |
| | 8272 | actorNotifyList -= obj; |
| | 8273 | } |
| | 8274 | |
| | 8275 | /* our list of registered actor notification items */ |
| | 8276 | actorNotifyList = [] |
| | 8277 | |
| | 8278 | /* |
| | 8279 | * Get the ambient light level in the visual senses at this actor. |
| | 8280 | * This is the ambient level at the actor. |
| | 8281 | */ |
| | 8282 | getVisualAmbient() |
| | 8283 | { |
| | 8284 | local ret; |
| | 8285 | local cache; |
| | 8286 | |
| | 8287 | /* check for a cached value */ |
| | 8288 | if ((cache = libGlobal.actorVisualAmbientCache) != nil |
| | 8289 | && (ret = cache[self]) != nil) |
| | 8290 | { |
| | 8291 | /* found a cached entry - use it */ |
| | 8292 | return ret; |
| | 8293 | } |
| | 8294 | |
| | 8295 | /* get the maximum ambient level at self for my sight-like senses */ |
| | 8296 | ret = senseAmbientMax(sightlikeSenses); |
| | 8297 | |
| | 8298 | /* if caching is active, cache our result for next time */ |
| | 8299 | if (cache != nil) |
| | 8300 | cache[self] = ret; |
| | 8301 | |
| | 8302 | /* return the result */ |
| | 8303 | return ret; |
| | 8304 | } |
| | 8305 | |
| | 8306 | /* |
| | 8307 | * Determine if my location is lit for my sight-like senses. |
| | 8308 | */ |
| | 8309 | isLocationLit() |
| | 8310 | { |
| | 8311 | /* |
| | 8312 | * Check for a simple, common case before doing the full |
| | 8313 | * sense-path calculation: if our location is providing its own |
| | 8314 | * light to its interior, then the location is lit. Most simple |
| | 8315 | * rooms are always lit. |
| | 8316 | */ |
| | 8317 | if (sightlikeSenses.indexOf(sight) != nil |
| | 8318 | && location != nil |
| | 8319 | && location.brightness > 1 |
| | 8320 | && location.transSensingOut(sight) == transparent) |
| | 8321 | return true; |
| | 8322 | |
| | 8323 | /* |
| | 8324 | * We don't have the simple case of light directly from our |
| | 8325 | * location, so run the full sense path check and get our |
| | 8326 | * maximum visual ambience level. If it's above the "self-lit" |
| | 8327 | * level of 1, then we can see. |
| | 8328 | */ |
| | 8329 | return (getVisualAmbient() > 1); |
| | 8330 | } |
| | 8331 | |
| | 8332 | /* |
| | 8333 | * Get the best (most transparent) sense information for one of our |
| | 8334 | * visual senses to the given object. |
| | 8335 | */ |
| | 8336 | bestVisualInfo(obj) |
| | 8337 | { |
| | 8338 | local best; |
| | 8339 | |
| | 8340 | /* we don't have a best value yet */ |
| | 8341 | best = nil; |
| | 8342 | |
| | 8343 | /* check each sight-like sense */ |
| | 8344 | foreach (local sense in sightlikeSenses) |
| | 8345 | { |
| | 8346 | /* |
| | 8347 | * get the information for the object in this sense, and keep |
| | 8348 | * the best (most transparent) info we've seen so far |
| | 8349 | */ |
| | 8350 | best = SenseInfo.selectMoreTrans(best, senseObj(sense, obj)); |
| | 8351 | } |
| | 8352 | |
| | 8353 | /* return the best one we found */ |
| | 8354 | return best; |
| | 8355 | } |
| | 8356 | |
| | 8357 | /* |
| | 8358 | * Build a list of all of the objects of which an actor is aware. |
| | 8359 | * |
| | 8360 | * An actor is aware of an object if the object is within reach of |
| | 8361 | * the actor's senses, and has some sort of presence in that sense. |
| | 8362 | * Note that both of these conditions must be true for at least one |
| | 8363 | * sense possessed by the actor; an object that is within earshot, |
| | 8364 | * but not within reach of any other sense, is in scope only if the |
| | 8365 | * object is making some kind of noise. |
| | 8366 | * |
| | 8367 | * In addition, objects that the actor is holding (i.e., those |
| | 8368 | * contained by the actor directly) are always in scope, regardless |
| | 8369 | * of their reachability through any sense. |
| | 8370 | */ |
| | 8371 | scopeList() |
| | 8372 | { |
| | 8373 | local lst; |
| | 8374 | |
| | 8375 | /* we have nothing in our master list yet */ |
| | 8376 | lst = new Vector(32); |
| | 8377 | |
| | 8378 | /* oneself is always in one's own scope list */ |
| | 8379 | lst.append(self); |
| | 8380 | |
| | 8381 | /* iterate over each sense */ |
| | 8382 | foreach (local sense in scopeSenses) |
| | 8383 | { |
| | 8384 | /* |
| | 8385 | * get the list of objects with a presence in this sense |
| | 8386 | * that can be sensed from our point of view, and and append |
| | 8387 | * it to our master list |
| | 8388 | */ |
| | 8389 | lst.appendUnique(sensePresenceList(sense)); |
| | 8390 | } |
| | 8391 | |
| | 8392 | /* add all of the items we are directly holding */ |
| | 8393 | lst.appendUnique(contents); |
| | 8394 | |
| | 8395 | /* |
| | 8396 | * ask each of our direct contents to add any contents of their |
| | 8397 | * own that are in scope by virtue of their containers being in |
| | 8398 | * scope |
| | 8399 | */ |
| | 8400 | foreach (local cur in contents) |
| | 8401 | cur.appendHeldContents(lst); |
| | 8402 | |
| | 8403 | /* add any items that are specially in scope in the location */ |
| | 8404 | if (location != nil) |
| | 8405 | { |
| | 8406 | /* get the extra scope items */ |
| | 8407 | local extra = location.getExtraScopeItems(self); |
| | 8408 | |
| | 8409 | /* if this is a non-nil list, add it to our list */ |
| | 8410 | if (extra.length() != 0) |
| | 8411 | lst.appendUnique(extra); |
| | 8412 | } |
| | 8413 | |
| | 8414 | /* |
| | 8415 | * Finally, add anything extra each item already in scope wants |
| | 8416 | * to add. Note that we keep going until we've visited each |
| | 8417 | * element of the vector at its current length on each iteration, |
| | 8418 | * so if we add any new items, we'll check them to see if they |
| | 8419 | * want to add any new items, and so on. |
| | 8420 | */ |
| | 8421 | for (local i = 1 ; i <= lst.length() ; ++i) |
| | 8422 | { |
| | 8423 | local extra; |
| | 8424 | |
| | 8425 | /* get the extra scope items for this item */ |
| | 8426 | extra = lst[i].getExtraScopeItems(self); |
| | 8427 | |
| | 8428 | /* if the extra item list is non-nil, add it to our list */ |
| | 8429 | if (extra.length() != 0) |
| | 8430 | lst.appendUnique(extra); |
| | 8431 | } |
| | 8432 | |
| | 8433 | /* return the result */ |
| | 8434 | return lst.toList(); |
| | 8435 | } |
| | 8436 | |
| | 8437 | /* |
| | 8438 | * Determine if I can see the given object. This returns true if |
| | 8439 | * the object can be sensed at all in one of my sight-like senses, |
| | 8440 | * nil if not. |
| | 8441 | */ |
| | 8442 | canSee(obj) |
| | 8443 | { |
| | 8444 | /* try each sight-like sense */ |
| | 8445 | foreach (local sense in sightlikeSenses) |
| | 8446 | { |
| | 8447 | /* |
| | 8448 | * if I can sense the object in this sense, I can sense the |
| | 8449 | * object |
| | 8450 | */ |
| | 8451 | if (senseObj(sense, obj).trans != opaque) |
| | 8452 | return true; |
| | 8453 | } |
| | 8454 | |
| | 8455 | /* we didn't find any sight-like sense where we can see the object */ |
| | 8456 | return nil; |
| | 8457 | } |
| | 8458 | |
| | 8459 | /* |
| | 8460 | * Determine if I can hear the given object. |
| | 8461 | */ |
| | 8462 | canHear(obj) |
| | 8463 | { |
| | 8464 | /* try each hearling-like sense */ |
| | 8465 | foreach (local sense in hearinglikeSenses) |
| | 8466 | { |
| | 8467 | /* |
| | 8468 | * if I can sense the object in this sense, I can sense the |
| | 8469 | * object |
| | 8470 | */ |
| | 8471 | if (senseObj(sense, obj).trans != opaque) |
| | 8472 | return true; |
| | 8473 | } |
| | 8474 | |
| | 8475 | /* we found no hearing-like sense that lets us hear the object */ |
| | 8476 | return nil; |
| | 8477 | } |
| | 8478 | |
| | 8479 | /* |
| | 8480 | * Determine if I can smell the given object. |
| | 8481 | */ |
| | 8482 | canSmell(obj) |
| | 8483 | { |
| | 8484 | /* try each hearling-like sense */ |
| | 8485 | foreach (local sense in smelllikeSenses) |
| | 8486 | { |
| | 8487 | /* |
| | 8488 | * if I can sense the object in this sense, I can sense the |
| | 8489 | * object |
| | 8490 | */ |
| | 8491 | if (senseObj(sense, obj).trans != opaque) |
| | 8492 | return true; |
| | 8493 | } |
| | 8494 | |
| | 8495 | /* we found no smell-like sense that lets us hear the object */ |
| | 8496 | return nil; |
| | 8497 | } |
| | 8498 | |
| | 8499 | /* |
| | 8500 | * Find the object that prevents us from seeing the given object. |
| | 8501 | */ |
| | 8502 | findVisualObstructor(obj) |
| | 8503 | { |
| | 8504 | /* try to find an opaque obstructor in one of our visual senses */ |
| | 8505 | foreach (local sense in sightlikeSenses) |
| | 8506 | { |
| | 8507 | local obs; |
| | 8508 | |
| | 8509 | /* cache path information for this sense */ |
| | 8510 | cacheSenseInfo(connectionTable(), sense); |
| | 8511 | |
| | 8512 | /* if we find an obstructor in this sense, return it */ |
| | 8513 | if ((obs = findOpaqueObstructor(sense, obj)) != nil) |
| | 8514 | return obs; |
| | 8515 | } |
| | 8516 | |
| | 8517 | /* we didn't find any obstructor */ |
| | 8518 | return nil; |
| | 8519 | } |
| | 8520 | |
| | 8521 | /* |
| | 8522 | * Build a table of full sensory information for all of the objects |
| | 8523 | * visible to the actor through the actor's sight-like senses. |
| | 8524 | * Returns a lookup table with the same set of information as |
| | 8525 | * senseInfoTable(). |
| | 8526 | */ |
| | 8527 | visibleInfoTable() |
| | 8528 | { |
| | 8529 | /* return objects visible from my own point of view */ |
| | 8530 | return visibleInfoTableFromPov(self); |
| | 8531 | } |
| | 8532 | |
| | 8533 | /* |
| | 8534 | * Build a table of full sensory information for all of the objects |
| | 8535 | * visible to me from a particular point of view through my |
| | 8536 | * sight-like senses. |
| | 8537 | */ |
| | 8538 | visibleInfoTableFromPov(pov) |
| | 8539 | { |
| | 8540 | local tab; |
| | 8541 | |
| | 8542 | /* we have no master table yet */ |
| | 8543 | tab = nil; |
| | 8544 | |
| | 8545 | /* iterate over each sense */ |
| | 8546 | foreach (local sense in sightlikeSenses) |
| | 8547 | { |
| | 8548 | local cur; |
| | 8549 | |
| | 8550 | /* get information for all objects for the current sense */ |
| | 8551 | cur = pov.senseInfoTable(sense); |
| | 8552 | |
| | 8553 | /* merge the table so far with the new table */ |
| | 8554 | tab = mergeSenseInfoTable(cur, tab); |
| | 8555 | } |
| | 8556 | |
| | 8557 | /* return the result */ |
| | 8558 | return tab; |
| | 8559 | } |
| | 8560 | |
| | 8561 | /* |
| | 8562 | * Build a lookup table of the objects that can be sensed for the |
| | 8563 | * purposes of taking inventory. We'll include everything in the |
| | 8564 | * normal visual sense table, plus everything directly held. |
| | 8565 | */ |
| | 8566 | inventorySenseInfoTable() |
| | 8567 | { |
| | 8568 | local visInfo; |
| | 8569 | local cont; |
| | 8570 | local ambient; |
| | 8571 | local info; |
| | 8572 | |
| | 8573 | /* |
| | 8574 | * Start with the objects visible to the actor through the |
| | 8575 | * actor's sight-like senses. |
| | 8576 | */ |
| | 8577 | visInfo = visibleInfoTable(); |
| | 8578 | |
| | 8579 | /* get the ambient light level at the actor */ |
| | 8580 | if ((info = visInfo[self]) != nil) |
| | 8581 | ambient = info.ambient; |
| | 8582 | else |
| | 8583 | ambient = 0; |
| | 8584 | |
| | 8585 | /* |
| | 8586 | * We'll assume that, for each item that the actor is directly |
| | 8587 | * holding AND knows about, the actor can still identify the item |
| | 8588 | * by touch, even if it's not visible. This way, when we're in a |
| | 8589 | * dark room, we'll still be able to refer to the objects we're |
| | 8590 | * directly holding, as long as we already know about them. |
| | 8591 | * |
| | 8592 | * Likewise, add items within our direct contents that are |
| | 8593 | * considered equally held. |
| | 8594 | */ |
| | 8595 | cont = new Vector(32); |
| | 8596 | foreach (local cur in contents) |
| | 8597 | { |
| | 8598 | /* add this item from our contents */ |
| | 8599 | cont.append(cur); |
| | 8600 | |
| | 8601 | /* add its contents that are themselves equally as held */ |
| | 8602 | cur.appendHeldContents(cont); |
| | 8603 | } |
| | 8604 | |
| | 8605 | /* |
| | 8606 | * Make a fully-sensible entry for each of our held items. We |
| | 8607 | * can simply replace any existing entry in the table that we got |
| | 8608 | * from the visual senses, since a fully transparent entry will |
| | 8609 | * be at least as good as anything we got from the normal visual |
| | 8610 | * list. Only include items that the actor knows about; we'll |
| | 8611 | * assume that we can identify by touch anything we're holding if |
| | 8612 | * we already know what it is, but not otherwise. |
| | 8613 | */ |
| | 8614 | foreach (local cur in cont) |
| | 8615 | { |
| | 8616 | /* if we know about the object, make it effectively visible */ |
| | 8617 | if (knowsAbout(cur)) |
| | 8618 | visInfo[cur] = new SenseInfo(cur, transparent, nil, ambient); |
| | 8619 | } |
| | 8620 | |
| | 8621 | /* return the table */ |
| | 8622 | return visInfo; |
| | 8623 | } |
| | 8624 | |
| | 8625 | /* |
| | 8626 | * Show what the actor is carrying. |
| | 8627 | */ |
| | 8628 | showInventory(tall) |
| | 8629 | { |
| | 8630 | /* |
| | 8631 | * show our inventory with our default listers as given by our |
| | 8632 | * inventory/wearing lister properties |
| | 8633 | */ |
| | 8634 | showInventoryWith(tall, inventoryLister); |
| | 8635 | } |
| | 8636 | |
| | 8637 | /* |
| | 8638 | * Show what the actor is carrying, using the given listers. |
| | 8639 | * |
| | 8640 | * Note that this method must be overridden if the actor does not |
| | 8641 | * use a conventional 'contents' list property to store its full set |
| | 8642 | * of contents. |
| | 8643 | */ |
| | 8644 | showInventoryWith(tall, inventoryLister) |
| | 8645 | { |
| | 8646 | local infoTab; |
| | 8647 | |
| | 8648 | /* get the table of objects sensible for inventory */ |
| | 8649 | infoTab = inventorySenseInfoTable(); |
| | 8650 | |
| | 8651 | /* list in the appropriate mode ("wide" or "tall") */ |
| | 8652 | inventoryLister.showList(self, self, contents, |
| | 8653 | ListRecurse | (tall ? ListTall : 0), |
| | 8654 | 0, infoTab, nil); |
| | 8655 | |
| | 8656 | /* mention sounds coming from inventory items */ |
| | 8657 | inventorySense(sound, inventoryListenLister); |
| | 8658 | |
| | 8659 | /* mention odors coming from inventory items */ |
| | 8660 | inventorySense(smell, inventorySmellLister); |
| | 8661 | } |
| | 8662 | |
| | 8663 | /* |
| | 8664 | * Add to an inventory description a list of things we notice |
| | 8665 | * through a specific sense. |
| | 8666 | */ |
| | 8667 | inventorySense(sense, lister) |
| | 8668 | { |
| | 8669 | local infoTab; |
| | 8670 | local presenceList; |
| | 8671 | |
| | 8672 | /* get the information table for the desired sense */ |
| | 8673 | infoTab = senseInfoTable(sense); |
| | 8674 | |
| | 8675 | /* |
| | 8676 | * get the list of everything with a presence in this sense that |
| | 8677 | * I'm carrying |
| | 8678 | */ |
| | 8679 | presenceList = senseInfoTableSubset(infoTab, |
| | 8680 | {obj, info: obj.isIn(self) && obj.(sense.presenceProp)}); |
| | 8681 | |
| | 8682 | /* add a paragraph break */ |
| | 8683 | cosmeticSpacingReport('<.p>'); |
| | 8684 | |
| | 8685 | /* list the items */ |
| | 8686 | lister.showList(self, nil, presenceList, 0, 0, infoTab, nil); |
| | 8687 | } |
| | 8688 | |
| | 8689 | /* |
| | 8690 | * The Lister object that we use for inventory listings. By |
| | 8691 | * default, we use actorInventoryLister, but this can be overridden |
| | 8692 | * if desired to use a different listing style. |
| | 8693 | */ |
| | 8694 | inventoryLister = actorInventoryLister |
| | 8695 | |
| | 8696 | /* |
| | 8697 | * The Lister for inventory listings, for use in a full description |
| | 8698 | * of the actor. By default, we use the "long form" inventory |
| | 8699 | * lister, on the assumption that most actors have relatively lengthy |
| | 8700 | * descriptive text. This can be overridden to use other formats; |
| | 8701 | * the short-form lister, for example, is useful for actors with only |
| | 8702 | * brief descriptions. |
| | 8703 | */ |
| | 8704 | holdingDescInventoryLister = actorHoldingDescInventoryListerLong |
| | 8705 | |
| | 8706 | /* |
| | 8707 | * Perform library pre-initialization on the actor |
| | 8708 | */ |
| | 8709 | initializeActor() |
| | 8710 | { |
| | 8711 | /* set up an empty pending command list */ |
| | 8712 | pendingCommand = new Vector(5); |
| | 8713 | |
| | 8714 | /* create a default inventory lister if we don't have one already */ |
| | 8715 | if (inventoryLister == nil) |
| | 8716 | inventoryLister = actorInventoryLister; |
| | 8717 | |
| | 8718 | /* create our antecedent tables */ |
| | 8719 | antecedentTable = new LookupTable(8, 8); |
| | 8720 | possAnaphorTable = new LookupTable(8, 8); |
| | 8721 | |
| | 8722 | /* if we don't have a state object, create a default */ |
| | 8723 | if (curState == nil) |
| | 8724 | setCurState(new ActorState(self)); |
| | 8725 | |
| | 8726 | /* create our pending-conversation list */ |
| | 8727 | pendingConv = new Vector(5); |
| | 8728 | } |
| | 8729 | |
| | 8730 | /* |
| | 8731 | * Note conditions before an action or other event. By default, we |
| | 8732 | * note our location and light/dark status, so that we comment on |
| | 8733 | * any change in the light/dark status after the event if we're |
| | 8734 | * still in the same location. |
| | 8735 | */ |
| | 8736 | noteConditionsBefore() |
| | 8737 | { |
| | 8738 | /* note our original location and light/dark status */ |
| | 8739 | locationBefore = location; |
| | 8740 | locationLitBefore = isLocationLit(); |
| | 8741 | } |
| | 8742 | |
| | 8743 | /* |
| | 8744 | * Note conditions after an action or other event. By default, if |
| | 8745 | * we are still in the same location we were in when |
| | 8746 | * noteConditionsBefore() was last called, and the light/dark status |
| | 8747 | * has changed, we'll mention the change in light/dark status. |
| | 8748 | */ |
| | 8749 | noteConditionsAfter() |
| | 8750 | { |
| | 8751 | /* |
| | 8752 | * If our location hasn't changed but our light/dark status has, |
| | 8753 | * note the new status. We don't make any announcement if the |
| | 8754 | * location has changed, since the travel routine will |
| | 8755 | * presumably have shown us the new location's light/dark status |
| | 8756 | * implicitly as part of the description of the new location |
| | 8757 | * after travel. |
| | 8758 | */ |
| | 8759 | if (location == locationBefore |
| | 8760 | && isLocationLit() != locationLitBefore) |
| | 8761 | { |
| | 8762 | /* consider this the start of a new turn */ |
| | 8763 | "<.commandsep>"; |
| | 8764 | |
| | 8765 | /* note the change with a new 'NoteDarkness' action */ |
| | 8766 | newActorAction(self, NoteDarkness); |
| | 8767 | |
| | 8768 | /* |
| | 8769 | * start another turn, in case this occurred during an |
| | 8770 | * implicit action or the like |
| | 8771 | */ |
| | 8772 | "<.commandsep>"; |
| | 8773 | } |
| | 8774 | } |
| | 8775 | |
| | 8776 | /* conditions we noted in noteConditionsBefore() */ |
| | 8777 | locationBefore = nil |
| | 8778 | locationLitBefore = nil |
| | 8779 | |
| | 8780 | /* let the actor have a turn as soon as the game starts */ |
| | 8781 | nextRunTime = 0 |
| | 8782 | |
| | 8783 | /* |
| | 8784 | * Scheduling order - this determines the order of execution when |
| | 8785 | * several items are schedulable at the same game clock time. |
| | 8786 | * |
| | 8787 | * We choose a scheduling order that schedules actors in this |
| | 8788 | * relative order: |
| | 8789 | * |
| | 8790 | * 100 player character, ready to execute |
| | 8791 | *. 200 NPC, ready to execute |
| | 8792 | *. 300 player character, idle |
| | 8793 | *. 400 NPC, idle |
| | 8794 | * |
| | 8795 | * An "idle" actor is one that is waiting for another character to |
| | 8796 | * complete a command, or an NPC with no pending commands to |
| | 8797 | * perform. (For the player character, it doesn't matter whether or |
| | 8798 | * not there's a pending command, because if the PC has no pending |
| | 8799 | * command, we ask the player for one.) |
| | 8800 | * |
| | 8801 | * This ordering ensures that each actor gets a chance to run each |
| | 8802 | * turn, but that actors with work to do go first, and other things |
| | 8803 | * being equal, the player character goes ahead of NPC's. |
| | 8804 | */ |
| | 8805 | scheduleOrder = 100 |
| | 8806 | |
| | 8807 | /* calculate the scheduling order */ |
| | 8808 | calcScheduleOrder() |
| | 8809 | { |
| | 8810 | /* determine if we're ready to run */ |
| | 8811 | if (readyForTurn()) |
| | 8812 | scheduleOrder = isPlayerChar() ? 100 : 200; |
| | 8813 | else |
| | 8814 | scheduleOrder = isPlayerChar() ? 300 : 400; |
| | 8815 | |
| | 8816 | /* return the scheduling order */ |
| | 8817 | return scheduleOrder; |
| | 8818 | } |
| | 8819 | |
| | 8820 | /* |
| | 8821 | * Determine if we're ready to do something on our turn. We're |
| | 8822 | * ready to do something if we're not waiting for another actor to |
| | 8823 | * finish doing something and either we're the player character or |
| | 8824 | * we already have a pending command in our command queue. |
| | 8825 | */ |
| | 8826 | readyForTurn() |
| | 8827 | { |
| | 8828 | /* |
| | 8829 | * if we're waiting for another actor, we're not ready to do |
| | 8830 | * anything |
| | 8831 | */ |
| | 8832 | if (checkWaitingForActor()) |
| | 8833 | return nil; |
| | 8834 | |
| | 8835 | /* |
| | 8836 | * if we're the player character, we're always ready to take a |
| | 8837 | * turn as long as we're not waiting for another actor (which we |
| | 8838 | * now know we're not), because we can either execute one of our |
| | 8839 | * previously queued commands, or we can ask for a new command |
| | 8840 | * to perform |
| | 8841 | */ |
| | 8842 | if (isPlayerChar()) |
| | 8843 | return true; |
| | 8844 | |
| | 8845 | /* |
| | 8846 | * if we have something other than placeholders in our command |
| | 8847 | * queue, we're ready to take a turn, because we can execute the |
| | 8848 | * next command in our queue |
| | 8849 | */ |
| | 8850 | if (pendingCommand.indexWhich({x: x.hasCommand}) != nil) |
| | 8851 | return true; |
| | 8852 | |
| | 8853 | /* |
| | 8854 | * we have no specific work to do, so we're not ready for our |
| | 8855 | * next turn |
| | 8856 | */ |
| | 8857 | return nil; |
| | 8858 | } |
| | 8859 | |
| | 8860 | /* |
| | 8861 | * Check to see if we're waiting for another actor to do something. |
| | 8862 | * Return true if so, nil if not. If we've been waiting for another |
| | 8863 | * actor, and the actor has finished the task we've been waiting for |
| | 8864 | * since the last time we checked, we'll clean up our internal state |
| | 8865 | * relating to the wait and return nil. |
| | 8866 | */ |
| | 8867 | checkWaitingForActor() |
| | 8868 | { |
| | 8869 | local cmdIdx; |
| | 8870 | local idx; |
| | 8871 | |
| | 8872 | /* if we're not waiting for an actor, simply return nil */ |
| | 8873 | if (waitingForActor == nil) |
| | 8874 | return nil; |
| | 8875 | |
| | 8876 | /* |
| | 8877 | * We're waiting for an actor to complete a command. Check to |
| | 8878 | * see if the completion marker is still in the actor's queue; if |
| | 8879 | * it's not, then the other actor has already completed our task. |
| | 8880 | * If the completion marker is in the other actor's queue, but |
| | 8881 | * there are no command entries before it, then we're also done |
| | 8882 | * waiting, because we're not actually waiting for the completion |
| | 8883 | * marker but instead for the tasks that were ahead of it in the |
| | 8884 | * main game execution loop. |
| | 8885 | * |
| | 8886 | * So, find the index of our marker in the queue, and find the |
| | 8887 | * index of the first real command in the queue. If our marker |
| | 8888 | * is still in the queue, and there's a command in the queue |
| | 8889 | * before our marker, the actor we're waiting for still has |
| | 8890 | * things to do before we're ready, so we're still waiting. |
| | 8891 | */ |
| | 8892 | idx = waitingForActor.pendingCommand.indexOf(waitingForInfo); |
| | 8893 | cmdIdx = waitingForActor.pendingCommand.indexWhich({x: x.hasCommand}); |
| | 8894 | if (idx != nil && cmdIdx != nil && idx > cmdIdx) |
| | 8895 | { |
| | 8896 | /* |
| | 8897 | * The marker is still in the queue, and there's at least |
| | 8898 | * one other command ahead of it, so the other actor hasn't |
| | 8899 | * finished the task we've been waiting for. Tell the |
| | 8900 | * caller that we are indeed still waiting for someone. |
| | 8901 | */ |
| | 8902 | return true; |
| | 8903 | } |
| | 8904 | |
| | 8905 | /* |
| | 8906 | * The other actor has disposed of our end-marker (or is about |
| | 8907 | * to, because it's the next thing left in the actor's queue), |
| | 8908 | * so it has finished with all of the commands we have been |
| | 8909 | * waiting for. However, if I haven't caught up in game clock |
| | 8910 | * time with the actor I've been waiting for, I'm still waiting. |
| | 8911 | */ |
| | 8912 | if (waitingForActor.nextRunTime > nextRunTime) |
| | 8913 | return true; |
| | 8914 | |
| | 8915 | /* we're done waiting - forget our wait status information */ |
| | 8916 | waitingForActor = nil; |
| | 8917 | waitingForInfo = nil; |
| | 8918 | |
| | 8919 | /* tell the caller we're no longer waiting for anyone */ |
| | 8920 | return nil; |
| | 8921 | } |
| | 8922 | |
| | 8923 | /* the action the actor performed most recently */ |
| | 8924 | mostRecentAction = nil |
| | 8925 | |
| | 8926 | /* |
| | 8927 | * Add busy time. An action calls this when we are the actor |
| | 8928 | * performing the action, and the action consumes game time. This |
| | 8929 | * marks us as busy for the given time units. |
| | 8930 | */ |
| | 8931 | addBusyTime(action, units) |
| | 8932 | { |
| | 8933 | /* note the action being performed */ |
| | 8934 | mostRecentAction = action; |
| | 8935 | |
| | 8936 | /* adjust the next run time by the busy time */ |
| | 8937 | nextRunTime += units; |
| | 8938 | } |
| | 8939 | |
| | 8940 | /* |
| | 8941 | * When it's our turn and we don't have any command to perform, |
| | 8942 | * we'll call this routine, which can perform a scripted operation |
| | 8943 | * if desired. |
| | 8944 | */ |
| | 8945 | idleTurn() |
| | 8946 | { |
| | 8947 | local tCur = Schedulable.gameClockTime; |
| | 8948 | local origNextRunTime = nextRunTime; |
| | 8949 | |
| | 8950 | /* |
| | 8951 | * if we haven't been targeted for conversation on this turn, |
| | 8952 | * see if we have a conversation we want to start |
| | 8953 | */ |
| | 8954 | if (lastConvTime < tCur) |
| | 8955 | { |
| | 8956 | /* check for a conversation that's ready to go */ |
| | 8957 | local info = pendingConv.valWhich({x: tCur >= x.time_}); |
| | 8958 | |
| | 8959 | /* if we found one, kick it off */ |
| | 8960 | if (info != nil) |
| | 8961 | { |
| | 8962 | /* remove it from the list */ |
| | 8963 | pendingConv.removeElement(info); |
| | 8964 | |
| | 8965 | /* start the conversation */ |
| | 8966 | initiateConversation(info.state_, info.node_); |
| | 8967 | } |
| | 8968 | } |
| | 8969 | |
| | 8970 | /* notify our state object that we're taking a turn */ |
| | 8971 | curState.takeTurn(); |
| | 8972 | |
| | 8973 | /* |
| | 8974 | * If we haven't already adjusted our next run time, consume a |
| | 8975 | * turn, so we're not ready to run again until the next game time |
| | 8976 | * increment. In some cases, we'll already have made this |
| | 8977 | * adjustment; for example, we might have run a nested command |
| | 8978 | * within our state object's takeTurn() method. |
| | 8979 | */ |
| | 8980 | if (nextRunTime == origNextRunTime) |
| | 8981 | ++nextRunTime; |
| | 8982 | } |
| | 8983 | |
| | 8984 | /* |
| | 8985 | * Receive notification that this is a non-idle turn. This is |
| | 8986 | * called whenever a command in our pending command queue is about |
| | 8987 | * to be executed. |
| | 8988 | * |
| | 8989 | * This method need not do anything at all, since the caller will |
| | 8990 | * take care of running the pending command. The purpose of this |
| | 8991 | * method is to take care of any changes an actor wants to make when |
| | 8992 | * it receives an explicit command, as opposed to running its own |
| | 8993 | * autonomous activity. |
| | 8994 | * |
| | 8995 | * By default, we cancel follow mode if it's in effect. It usually |
| | 8996 | * makes sense for an explicit command to interrupt follow mode; |
| | 8997 | * follow mode is usually started by an explicit command in the |
| | 8998 | * first place, so it is usually sensible for a new command to |
| | 8999 | * replace the one that started follow mode. |
| | 9000 | */ |
| | 9001 | nonIdleTurn() |
| | 9002 | { |
| | 9003 | /* by default, cancel follow mode */ |
| | 9004 | followingActor = nil; |
| | 9005 | } |
| | 9006 | |
| | 9007 | /* |
| | 9008 | * If we're following an actor, this keeps track of the actor we're |
| | 9009 | * following. NPC's can use this to follow around another actor |
| | 9010 | * whenever possible. |
| | 9011 | */ |
| | 9012 | followingActor = nil |
| | 9013 | |
| | 9014 | /* |
| | 9015 | * Handle a situation where we're trying to follow an actor but |
| | 9016 | * can't. By default, this simply cancels our follow mode. |
| | 9017 | * |
| | 9018 | * Actors might want to override this to be more tolerant. For |
| | 9019 | * example, an actor might want to wait until five turns elapse to |
| | 9020 | * give up on following, in case the target actor returns after a |
| | 9021 | * brief digression; or an actor could stay in follow mode until it |
| | 9022 | * received other instructions, or found something better to do. |
| | 9023 | */ |
| | 9024 | cannotFollow() |
| | 9025 | { |
| | 9026 | /* |
| | 9027 | * by default, simply cancel follow mode by forgetting about the |
| | 9028 | * actor we're following |
| | 9029 | */ |
| | 9030 | followingActor = nil; |
| | 9031 | } |
| | 9032 | |
| | 9033 | /* |
| | 9034 | * Execute one "turn" - this is a unit of time passing. The player |
| | 9035 | * character generally is allowed to execute one command in the |
| | 9036 | * course of a turn; a non-player character with a programmed task |
| | 9037 | * can perform an increment of the task. |
| | 9038 | * |
| | 9039 | * We set up an ActorTurnAction environment and invoke our |
| | 9040 | * executeActorTurn() method. In most cases, subclasses should |
| | 9041 | * override executeActorTurn() rather than this method, since |
| | 9042 | * overriding executeTurn() directly will lose the action |
| | 9043 | * environment. |
| | 9044 | */ |
| | 9045 | executeTurn() |
| | 9046 | { |
| | 9047 | /* start a new command visually when a new actor is taking over */ |
| | 9048 | "<.commandsep>"; |
| | 9049 | |
| | 9050 | /* |
| | 9051 | * Execute the turn in a daemon action context, and in the sight |
| | 9052 | * context of the actor. The sense context will ensure that we |
| | 9053 | * report the results of the action only if the actor is visible |
| | 9054 | * to the player character; in most cases, the actor's |
| | 9055 | * visibility is equivalent to the visibility of the effects, so |
| | 9056 | * this provides a simple way of ensuring that the results of |
| | 9057 | * the action are reported if and only if they're visible to the |
| | 9058 | * player character. |
| | 9059 | * |
| | 9060 | * Note that if we are the player character, don't use the sense |
| | 9061 | * context filtering -- we normally want full reports for |
| | 9062 | * everything the player character does. |
| | 9063 | */ |
| | 9064 | return withActionEnv(EventAction, self, |
| | 9065 | {: callWithSenseContext(isPlayerChar() ? nil : self, sight, |
| | 9066 | {: executeActorTurn() }) }); |
| | 9067 | } |
| | 9068 | |
| | 9069 | /* |
| | 9070 | * The main processing for an actor's turn. In most cases, |
| | 9071 | * subclasses should override this method (rather than executeTurn) |
| | 9072 | * to specialize an actor's turn processing. |
| | 9073 | */ |
| | 9074 | executeActorTurn() |
| | 9075 | { |
| | 9076 | /* |
| | 9077 | * If we have a pending response, and we're in a position to |
| | 9078 | * deliver it, our next work is to deliver the pending response. |
| | 9079 | */ |
| | 9080 | if (pendingResponse != nil && canTalkTo(pendingResponse.issuer_)) |
| | 9081 | { |
| | 9082 | /* |
| | 9083 | * We have a pending response, and the command issuer from |
| | 9084 | * the pending response can hear us now, so we can finally |
| | 9085 | * deliver the response. |
| | 9086 | * |
| | 9087 | * If the issuer is the player character, send to the player |
| | 9088 | * using our deferred message generator; otherwise, call the |
| | 9089 | * issuer's notification routine, since it's an NPC-to-NPC |
| | 9090 | * notification. |
| | 9091 | */ |
| | 9092 | if (pendingResponse.issuer_.isPlayerChar()) |
| | 9093 | { |
| | 9094 | /* |
| | 9095 | * we're notifying the player - use the deferred message |
| | 9096 | * generator |
| | 9097 | */ |
| | 9098 | getParserDeferredMessageObj().(pendingResponse.prop_)( |
| | 9099 | self, pendingResponse.args_...); |
| | 9100 | } |
| | 9101 | else |
| | 9102 | { |
| | 9103 | /* it's an NPC-to-NPC notification - notify the issuer */ |
| | 9104 | pendingResponse.issuer_.notifyIssuerParseFailure( |
| | 9105 | self, pendingResponse.prop_, pendingResponse.args_); |
| | 9106 | } |
| | 9107 | |
| | 9108 | /* |
| | 9109 | * in either case, we've gotten this out of our system now, |
| | 9110 | * so we can forget about the pending response |
| | 9111 | */ |
| | 9112 | pendingResponse = nil; |
| | 9113 | } |
| | 9114 | |
| | 9115 | /* check to see if we're waiting for another actor */ |
| | 9116 | if (checkWaitingForActor()) |
| | 9117 | { |
| | 9118 | /* |
| | 9119 | * we're still waiting, so there's nothing for us to do; take |
| | 9120 | * an idle turn and return |
| | 9121 | */ |
| | 9122 | idleTurn(); |
| | 9123 | return true; |
| | 9124 | } |
| | 9125 | |
| | 9126 | /* |
| | 9127 | * if we're the player character, and we have no pending commands |
| | 9128 | * to execute, our next task will be to read and execute a |
| | 9129 | * command |
| | 9130 | */ |
| | 9131 | if (pendingCommand.length() == 0 && isPlayerChar()) |
| | 9132 | { |
| | 9133 | local toks; |
| | 9134 | |
| | 9135 | /* read a command line and get the resulting token list */ |
| | 9136 | toks = readMainCommandTokens(rmcCommand); |
| | 9137 | |
| | 9138 | /* |
| | 9139 | * re-activate the main transcript - reading the command |
| | 9140 | * line will have deactivated the transcript, but we want it |
| | 9141 | * active again now that we're about to start executing the |
| | 9142 | * command |
| | 9143 | */ |
| | 9144 | gTranscript.activate(); |
| | 9145 | |
| | 9146 | /* |
| | 9147 | * If it came back nil, it means that the input was fully |
| | 9148 | * processed in pre-parsing; this means that we don't have |
| | 9149 | * any more work to do on this turn, so we can simply end our |
| | 9150 | * turn now. |
| | 9151 | */ |
| | 9152 | if (toks == nil) |
| | 9153 | return true; |
| | 9154 | |
| | 9155 | /* retrieve the token list from the command line */ |
| | 9156 | toks = toks[2]; |
| | 9157 | |
| | 9158 | /* |
| | 9159 | * Add it to our pending command queue. Since we read the |
| | 9160 | * command from the player, and we're the player character, |
| | 9161 | * we treat the command as coming from myself. |
| | 9162 | * |
| | 9163 | * Since this is a newly-read command line, we're starting a |
| | 9164 | * new sentence. |
| | 9165 | */ |
| | 9166 | addPendingCommand(true, self, toks); |
| | 9167 | } |
| | 9168 | |
| | 9169 | /* |
| | 9170 | * Check to see if we have any pending command to execute. If |
| | 9171 | * so, our next task is to execute the pending command. |
| | 9172 | */ |
| | 9173 | if (pendingCommand.length() != 0) |
| | 9174 | { |
| | 9175 | local cmd; |
| | 9176 | |
| | 9177 | /* remove the first pending command from our queue */ |
| | 9178 | cmd = pendingCommand[1]; |
| | 9179 | pendingCommand.removeElementAt(1); |
| | 9180 | |
| | 9181 | /* if this is a real command, note the non-idle turn */ |
| | 9182 | if (cmd.hasCommand) |
| | 9183 | nonIdleTurn(); |
| | 9184 | |
| | 9185 | /* execute the first pending command */ |
| | 9186 | cmd.executePending(self); |
| | 9187 | |
| | 9188 | /* |
| | 9189 | * We're done with this turn. If we no longer have any |
| | 9190 | * pending commands, tell the scheduler to refigure the |
| | 9191 | * execution order, since another object might now be ready |
| | 9192 | * to run ahead of our idle activity. |
| | 9193 | */ |
| | 9194 | if (pendingCommand.indexWhich({x: x.hasCommand}) == nil) |
| | 9195 | return nil; |
| | 9196 | else |
| | 9197 | return true; |
| | 9198 | } |
| | 9199 | |
| | 9200 | /* |
| | 9201 | * If we're following an actor, and the actor isn't in sight, see |
| | 9202 | * if we can catch up. |
| | 9203 | */ |
| | 9204 | if (followingActor != nil |
| | 9205 | && location != nil |
| | 9206 | && (followingActor.location.effectiveFollowLocation |
| | 9207 | != location.effectiveFollowLocation)) |
| | 9208 | { |
| | 9209 | local info; |
| | 9210 | |
| | 9211 | /* see if we have enough information to follow */ |
| | 9212 | info = getFollowInfo(followingActor); |
| | 9213 | |
| | 9214 | /* |
| | 9215 | * Check to see if we have enough information to follow the |
| | 9216 | * actor. We can only follow if we saw the actor depart at |
| | 9217 | * some point, and we're in the same location where we last |
| | 9218 | * saw the actor depart. (We have to be in the same |
| | 9219 | * location, because we follow by performing the same command |
| | 9220 | * we saw the actor perform when we last saw the actor |
| | 9221 | * depart. Repeating the command will obviously be |
| | 9222 | * ineffective unless we're in the same location as the actor |
| | 9223 | * was.) |
| | 9224 | */ |
| | 9225 | if (info != nil) |
| | 9226 | { |
| | 9227 | local success; |
| | 9228 | |
| | 9229 | /* |
| | 9230 | * we know how to follow the actor, so simply perform |
| | 9231 | * the same command we saw the actor perform. |
| | 9232 | */ |
| | 9233 | newActorAction(self, Follow, followingActor); |
| | 9234 | |
| | 9235 | /* note whether or not we succeeded */ |
| | 9236 | success = (location.effectiveFollowLocation == |
| | 9237 | followingActor.location.effectiveFollowLocation); |
| | 9238 | |
| | 9239 | /* notify the state object of our attempt */ |
| | 9240 | curState.justFollowed(success); |
| | 9241 | |
| | 9242 | /* |
| | 9243 | * if we failed to track the actor, note that we are |
| | 9244 | * unable to follow the actor |
| | 9245 | */ |
| | 9246 | if (!success) |
| | 9247 | { |
| | 9248 | /* note that we failed to follow the actor */ |
| | 9249 | cannotFollow(); |
| | 9250 | } |
| | 9251 | |
| | 9252 | /* we're done with this turn */ |
| | 9253 | return true; |
| | 9254 | } |
| | 9255 | else |
| | 9256 | { |
| | 9257 | /* |
| | 9258 | * we don't know how to follow this actor - call our |
| | 9259 | * cannot-follow handler |
| | 9260 | */ |
| | 9261 | cannotFollow(); |
| | 9262 | } |
| | 9263 | } |
| | 9264 | |
| | 9265 | /* we have no pending work to perform, so take an idle turn */ |
| | 9266 | idleTurn(); |
| | 9267 | |
| | 9268 | /* no change in scheduling priority */ |
| | 9269 | return true; |
| | 9270 | } |
| | 9271 | |
| | 9272 | /* |
| | 9273 | * By default, all actors are likely command targets. This should |
| | 9274 | * be overridden for actors who are obviously not likely to accept |
| | 9275 | * commands of any kind. |
| | 9276 | * |
| | 9277 | * This is used to disambiguate target actors in commands, so this |
| | 9278 | * should provide an indication of what should be obvious to a |
| | 9279 | * player, because the purpose of this information is to guess what |
| | 9280 | * the player is likely to take for granted in specifying a target |
| | 9281 | * actor. |
| | 9282 | */ |
| | 9283 | isLikelyCommandTarget = true |
| | 9284 | |
| | 9285 | /* |
| | 9286 | * Determine if we should accept a command. 'issuingActor' is the |
| | 9287 | * actor who issued the command: if the player typed the command on |
| | 9288 | * the command line, this will be the player character actor. |
| | 9289 | * |
| | 9290 | * This routine performs only the simplest check, since it doesn't |
| | 9291 | * have access to the specific action being performed. This is |
| | 9292 | * intended as a first check, to allow us to bypass noun resolution |
| | 9293 | * if the actor simply won't accept any command from the issuer. |
| | 9294 | * |
| | 9295 | * Returns true to accept a command, nil to reject it. If this |
| | 9296 | * routine returns nil, and the command came from the player |
| | 9297 | * character, a suitable message should be displayed. |
| | 9298 | * |
| | 9299 | * Note that most actors should not override this routine simply to |
| | 9300 | * express the will of the actor to accept a command, since this |
| | 9301 | * routine performs a number of checks for the physical ability of |
| | 9302 | * the actor to execute a command from the issuer. To determine |
| | 9303 | * whether or not the actor should obey physically valid commands |
| | 9304 | * from the issuer, override obeyCommand(). |
| | 9305 | */ |
| | 9306 | acceptCommand(issuingActor) |
| | 9307 | { |
| | 9308 | /* if we're the current player character, accept any command */ |
| | 9309 | if (isPlayerChar()) |
| | 9310 | return true; |
| | 9311 | |
| | 9312 | /* if we can't hear the issuer, we can't talk to it */ |
| | 9313 | if (issuingActor != self && !issuingActor.canTalkTo(self)) |
| | 9314 | { |
| | 9315 | /* report that the target actor can't hear the issuer */ |
| | 9316 | reportFailure(&objCannotHearActorMsg, self); |
| | 9317 | |
| | 9318 | /* tell the caller that the command cannot proceed */ |
| | 9319 | return nil; |
| | 9320 | } |
| | 9321 | |
| | 9322 | /* if I'm busy doing something else, say so */ |
| | 9323 | if (nextRunTime > Schedulable.gameClockTime) |
| | 9324 | { |
| | 9325 | /* tell the issuing actor I'm busy */ |
| | 9326 | notifyParseFailure(issuingActor, &refuseCommandBusy, |
| | 9327 | [issuingActor]); |
| | 9328 | |
| | 9329 | /* tell the caller to abandon the command */ |
| | 9330 | return nil; |
| | 9331 | } |
| | 9332 | |
| | 9333 | /* check to see if I have other work to perform first */ |
| | 9334 | if (!acceptCommandBusy(issuingActor)) |
| | 9335 | return nil; |
| | 9336 | |
| | 9337 | /* we didn't find any reason to object, so allow the command */ |
| | 9338 | return true; |
| | 9339 | } |
| | 9340 | |
| | 9341 | /* |
| | 9342 | * Check to see if I'm busy with pending commands, and if so, |
| | 9343 | * whether or not I should accept a new command. Returns true if we |
| | 9344 | * should accept a command, nil if not. If we return nil, we must |
| | 9345 | * notify the issuer of the rejection. |
| | 9346 | * |
| | 9347 | * By default, we won't accept a command if we have any work |
| | 9348 | * pending. |
| | 9349 | */ |
| | 9350 | acceptCommandBusy(issuingActor) |
| | 9351 | { |
| | 9352 | /* if we have any pending commands, don't accept a new command */ |
| | 9353 | if (pendingCommand.length() != 0) |
| | 9354 | { |
| | 9355 | /* |
| | 9356 | * if we have only commands from the same issuer pending, |
| | 9357 | * cancel all of the pending commands and accept the new |
| | 9358 | * command instead |
| | 9359 | */ |
| | 9360 | foreach (local info in pendingCommand) |
| | 9361 | { |
| | 9362 | /* |
| | 9363 | * if this is from a different issuer, don't accept a |
| | 9364 | * new command |
| | 9365 | */ |
| | 9366 | if (info.issuer_ != issuingActor) |
| | 9367 | { |
| | 9368 | /* tell the other actor that we're busy */ |
| | 9369 | notifyParseFailure(issuingActor, &refuseCommandBusy, |
| | 9370 | [issuingActor]); |
| | 9371 | |
| | 9372 | /* tell the caller to abandon the command */ |
| | 9373 | return nil; |
| | 9374 | } |
| | 9375 | } |
| | 9376 | |
| | 9377 | /* |
| | 9378 | * all of the pending commands were from the same issuer, so |
| | 9379 | * presumably the issuer wants to override those commands; |
| | 9380 | * remove the old ones from our pending queue |
| | 9381 | */ |
| | 9382 | pendingCommand.removeRange(1, pendingCommand.length()); |
| | 9383 | } |
| | 9384 | |
| | 9385 | /* we didn't find any problems */ |
| | 9386 | return true; |
| | 9387 | } |
| | 9388 | |
| | 9389 | /* |
| | 9390 | * Determine whether or not we want to obey a command from the given |
| | 9391 | * actor to perform the given action. We only get this far when we |
| | 9392 | * determine that it's possible for us to accept a command, given |
| | 9393 | * the sense connections between us and the issuing actor, and given |
| | 9394 | * our pending command queue. |
| | 9395 | * |
| | 9396 | * When this routine is called, the action has been determined, and |
| | 9397 | * the noun phrases have been resolved. However, we haven't |
| | 9398 | * actually started processing the action yet, so the globals for |
| | 9399 | * the noun slots (gDobj, gIobj, etc) are NOT available. If the |
| | 9400 | * routine needs to know which objects are involved, it must obtain |
| | 9401 | * the full list of resolved objects from the action (using, for |
| | 9402 | * example, getResolvedDobjList()). |
| | 9403 | * |
| | 9404 | * When there's a list of objects to be processed (as in GET ALL), |
| | 9405 | * we haven't started working on any one of them yet - this check is |
| | 9406 | * made once for the entire command, and applies to the entire list |
| | 9407 | * of objects. If the actor wants to respond specially to |
| | 9408 | * individual objects, you can do that by overriding actorAction() |
| | 9409 | * instead of this routine. |
| | 9410 | * |
| | 9411 | * This routine should display an appropriate message and return nil |
| | 9412 | * if the command is not to be accepted, and should simply return |
| | 9413 | * true to accept the command. |
| | 9414 | * |
| | 9415 | * By default, we'll let our state object handle this. |
| | 9416 | * |
| | 9417 | * Note that actors that override this might also need to override |
| | 9418 | * wantsFollowInfo(), since an actor that accepts "follow" commands |
| | 9419 | * will need to keep track of the movements of other actors if it is |
| | 9420 | * to carry out any following. |
| | 9421 | */ |
| | 9422 | obeyCommand(issuingActor, action) |
| | 9423 | { |
| | 9424 | /* note that the issuing actor is targeting me in conversation */ |
| | 9425 | issuingActor.noteConversation(self); |
| | 9426 | |
| | 9427 | /* let the state object handle it */ |
| | 9428 | return curState.obeyCommand(issuingActor, action); |
| | 9429 | } |
| | 9430 | |
| | 9431 | /* |
| | 9432 | * Say hello/goodbye/yes/no to the given actor. We'll greet the |
| | 9433 | * target actor is the target actor was specified (i.e., actor != |
| | 9434 | * self); otherwise, we'll greet our current default conversational |
| | 9435 | * partner, if we have one. |
| | 9436 | */ |
| | 9437 | sayHello(actor) { sayToActor(actor, helloTopicObj, helloConvType); } |
| | 9438 | sayGoodbye(actor) { sayToActor(actor, byeTopicObj, byeConvType); } |
| | 9439 | sayYes(actor) { sayToActor(actor, yesTopicObj, yesConvType); } |
| | 9440 | sayNo(actor) { sayToActor(actor, noTopicObj, noConvType); } |
| | 9441 | |
| | 9442 | /* handle one of the conversational addresses */ |
| | 9443 | sayToActor(actor, topic, convType) |
| | 9444 | { |
| | 9445 | /* |
| | 9446 | * If the target actor is the same as the issuing actor, then no |
| | 9447 | * target actor was specified in the command, so direct the |
| | 9448 | * address to our current conversational partner, if we have |
| | 9449 | * one. |
| | 9450 | */ |
| | 9451 | if (actor == self) |
| | 9452 | actor = getDefaultInterlocutor(); |
| | 9453 | |
| | 9454 | /* |
| | 9455 | * if we found an actor, send the address to the actor's state |
| | 9456 | * object; otherwise, handle it with the given default message |
| | 9457 | */ |
| | 9458 | if (actor != nil) |
| | 9459 | { |
| | 9460 | /* make sure we can talk to the other actor */ |
| | 9461 | if (!canTalkTo(actor)) |
| | 9462 | { |
| | 9463 | /* can't talk to them - say so and give up */ |
| | 9464 | reportFailure(&objCannotHearActorMsg, actor); |
| | 9465 | exit; |
| | 9466 | } |
| | 9467 | |
| | 9468 | /* remember our current conversational partner */ |
| | 9469 | noteConversation(actor); |
| | 9470 | |
| | 9471 | /* handle it as a topic */ |
| | 9472 | actor.curState.handleConversation(self, topic, convType); |
| | 9473 | } |
| | 9474 | else |
| | 9475 | { |
| | 9476 | /* |
| | 9477 | * we don't know whom we're addressing; just show the default |
| | 9478 | * message for an unknown interlocutor |
| | 9479 | */ |
| | 9480 | mainReport(convType.unknownMsg); |
| | 9481 | } |
| | 9482 | } |
| | 9483 | |
| | 9484 | /* |
| | 9485 | * Handle the XSPCLTOPIC pseudo-command. This command is generated |
| | 9486 | * by the SpecialTopic pre-parser when it recognizes the player's |
| | 9487 | * input as matching an active SpecialTopic's custom syntax. Our |
| | 9488 | * job is to route this back to our current interlocutor's active |
| | 9489 | * ConvNode, so that it can find the SpecialTopic that it matched in |
| | 9490 | * pre-parsing and show its response. |
| | 9491 | */ |
| | 9492 | saySpecialTopic() |
| | 9493 | { |
| | 9494 | local actor; |
| | 9495 | |
| | 9496 | /* send it to our interlocutor */ |
| | 9497 | if ((actor = getCurrentInterlocutor()) == nil |
| | 9498 | || actor.curConvNode == nil) |
| | 9499 | { |
| | 9500 | /* |
| | 9501 | * We don't seem to have a current interlocutor, or the |
| | 9502 | * interlocutor doesn't have a current conversation node. |
| | 9503 | * This is inconsistent; there's no way we could have |
| | 9504 | * generated XSPCLTOPIC from our pre-parser under these |
| | 9505 | * conditions. The most likely thing is that the player |
| | 9506 | * tried typing in XSPCLTOPIC manually. Politely ignore it. |
| | 9507 | */ |
| | 9508 | gLibMessages.commandNotPresent; |
| | 9509 | } |
| | 9510 | else |
| | 9511 | { |
| | 9512 | /* note the conversation directed to the other actor */ |
| | 9513 | noteConversation(actor); |
| | 9514 | |
| | 9515 | /* send the request to the ConvNode for processing */ |
| | 9516 | actor.curConvNode.saySpecialTopic(self); |
| | 9517 | } |
| | 9518 | } |
| | 9519 | |
| | 9520 | /* |
| | 9521 | * Add a command to our pending command list. The new command is |
| | 9522 | * specified as a list of tokens to be parsed, and it is added after |
| | 9523 | * any commands already in our pending list. |
| | 9524 | */ |
| | 9525 | addPendingCommand(startOfSentence, issuer, toks) |
| | 9526 | { |
| | 9527 | /* add a descriptor to the pending command list */ |
| | 9528 | pendingCommand.append( |
| | 9529 | new PendingCommandToks(startOfSentence, issuer, toks)); |
| | 9530 | } |
| | 9531 | |
| | 9532 | /* |
| | 9533 | * Insert a command at the head of our pending command list. The |
| | 9534 | * new command is specified as a list of tokens to parse, and it is |
| | 9535 | * inserted into our pending command list before any commands |
| | 9536 | * already in the list. |
| | 9537 | */ |
| | 9538 | addFirstPendingCommand(startOfSentence, issuer, toks) |
| | 9539 | { |
| | 9540 | /* add a descriptor to the start of our list */ |
| | 9541 | pendingCommand.insertAt( |
| | 9542 | 1, new PendingCommandToks(startOfSentence, issuer, toks)); |
| | 9543 | } |
| | 9544 | |
| | 9545 | /* |
| | 9546 | * Add a resolved action to our pending command list. The new |
| | 9547 | * command is specified as a resolved Action object; it is added |
| | 9548 | * after any commands already in our list. |
| | 9549 | */ |
| | 9550 | addPendingAction(startOfSentence, issuer, action, [objs]) |
| | 9551 | { |
| | 9552 | /* add a descriptor to the pending command list */ |
| | 9553 | pendingCommand.append(new PendingCommandAction( |
| | 9554 | startOfSentence, issuer, action, objs...)); |
| | 9555 | } |
| | 9556 | |
| | 9557 | /* |
| | 9558 | * Insert a resolved action at the start of our pending command |
| | 9559 | * list. The new command is specified as a resolved Action object; |
| | 9560 | * it is added before any commands already in our list. |
| | 9561 | */ |
| | 9562 | addFirstPendingAction(startOfSentence, issuer, action, [objs]) |
| | 9563 | { |
| | 9564 | /* add a descriptor to the pending command list */ |
| | 9565 | pendingCommand.insertAt(1, new PendingCommandAction( |
| | 9566 | startOfSentence, issuer, action, objs...)); |
| | 9567 | } |
| | 9568 | |
| | 9569 | |
| | 9570 | /* pending commands - this is a list of PendingCommandInfo objects */ |
| | 9571 | pendingCommand = nil |
| | 9572 | |
| | 9573 | /* |
| | 9574 | * pending response - this is a single PendingResponseInfo object, |
| | 9575 | * which we'll deliver as soon as the issuing actor is in a position |
| | 9576 | * to hear us |
| | 9577 | */ |
| | 9578 | pendingResponse = nil |
| | 9579 | |
| | 9580 | /* |
| | 9581 | * get the library message object for a parser message addressed to |
| | 9582 | * the player character |
| | 9583 | */ |
| | 9584 | getParserMessageObj() |
| | 9585 | { |
| | 9586 | /* |
| | 9587 | * If I'm the player character, use the player character message |
| | 9588 | * object; otherwise, use the default non-player character |
| | 9589 | * message object. |
| | 9590 | * |
| | 9591 | * To customize parser messages from a particular actor, create |
| | 9592 | * an object based on npcMessages, and override this routine in |
| | 9593 | * the actor so that it returns the custom object rather than |
| | 9594 | * the standard npcMessages object. To customize messages for |
| | 9595 | * ALL of the NPC's in a game, simply modify npcMessages itself, |
| | 9596 | * since it's the default for all non-player characters. |
| | 9597 | */ |
| | 9598 | return isPlayerChar() ? playerMessages : npcMessages; |
| | 9599 | } |
| | 9600 | |
| | 9601 | /* |
| | 9602 | * Get the deferred library message object for a parser message |
| | 9603 | * addressed to the player character. We only use this to generate |
| | 9604 | * messages deferred from non-player characters. |
| | 9605 | */ |
| | 9606 | getParserDeferredMessageObj() { return npcDeferredMessages; } |
| | 9607 | |
| | 9608 | /* |
| | 9609 | * Get the library message object for action responses. This is |
| | 9610 | * used to generate library responses to verbs. |
| | 9611 | */ |
| | 9612 | getActionMessageObj() |
| | 9613 | { |
| | 9614 | /* |
| | 9615 | * return the default player character or NPC message object, |
| | 9616 | * depending on whether I'm the player or not; individual actors |
| | 9617 | * can override this to supply actor-specific messages for |
| | 9618 | * library action responses |
| | 9619 | */ |
| | 9620 | return isPlayerChar() ? playerActionMessages: npcActionMessages; |
| | 9621 | } |
| | 9622 | |
| | 9623 | /* |
| | 9624 | * Notify an issuer that a command sent to us resulted in a parsing |
| | 9625 | * failure. We are meant to reply to the issuer to let the issuer |
| | 9626 | * know about the problem. messageProp is the libGlobal message |
| | 9627 | * property describing the error, and args is a list with the |
| | 9628 | * (varargs) arguments to the message property. |
| | 9629 | */ |
| | 9630 | notifyParseFailure(issuingActor, messageProp, args) |
| | 9631 | { |
| | 9632 | /* |
| | 9633 | * In case the actor is in a remote location but in scope for the |
| | 9634 | * purposes of the conversation only (such as over a phone or |
| | 9635 | * radio), run this in a neutral sense context. Since we're |
| | 9636 | * reporting a parser failure, we want the message to be |
| | 9637 | * displayed no matter what the scope situation is. |
| | 9638 | */ |
| | 9639 | callWithSenseContext(nil, nil, new function() |
| | 9640 | { |
| | 9641 | /* check who's talking to whom */ |
| | 9642 | if (issuingActor.isPlayerChar()) |
| | 9643 | { |
| | 9644 | /* |
| | 9645 | * The player issued the command. If the command was |
| | 9646 | * directed to an NPC (i.e., we're not the player), check |
| | 9647 | * to see if the player character is in scope from our |
| | 9648 | * perspective. |
| | 9649 | */ |
| | 9650 | if (issuingActor != self && !canTalkTo(issuingActor)) |
| | 9651 | { |
| | 9652 | /* |
| | 9653 | * The player issued the command to an NPC, but the |
| | 9654 | * player is not capable of hearing the NPC's |
| | 9655 | * response. |
| | 9656 | */ |
| | 9657 | cannotRespondToCommand(issuingActor, messageProp, args); |
| | 9658 | } |
| | 9659 | else |
| | 9660 | { |
| | 9661 | /* |
| | 9662 | * generate a message using the appropriate message |
| | 9663 | * generator object |
| | 9664 | */ |
| | 9665 | getParserMessageObj().(messageProp)(self, args...); |
| | 9666 | } |
| | 9667 | } |
| | 9668 | else |
| | 9669 | { |
| | 9670 | /* |
| | 9671 | * the command was issued from one NPC to another - |
| | 9672 | * notify the issuer of the problem, but don't display |
| | 9673 | * any messages, since this interaction is purely among |
| | 9674 | * the NPC's |
| | 9675 | */ |
| | 9676 | issuingActor. |
| | 9677 | notifyIssuerParseFailure(self, messageProp, args); |
| | 9678 | } |
| | 9679 | }); |
| | 9680 | } |
| | 9681 | |
| | 9682 | /* |
| | 9683 | * We have a parser error to report to the player, but we cannot |
| | 9684 | * respond at the moment because the player is not capable of |
| | 9685 | * hearing us (there is no sense path for our communications senses |
| | 9686 | * from us to the player actor). Defer reporting the message until |
| | 9687 | * later. |
| | 9688 | */ |
| | 9689 | cannotRespondToCommand(issuingActor, messageProp, args) |
| | 9690 | { |
| | 9691 | /* |
| | 9692 | * Remember the problem for later deliver. If we already have a |
| | 9693 | * deferred response, forget it - just report the latest |
| | 9694 | * problem. |
| | 9695 | */ |
| | 9696 | pendingResponse = |
| | 9697 | new PendingResponseInfo(issuingActor, messageProp, args); |
| | 9698 | |
| | 9699 | /* |
| | 9700 | * Some actors might want to override this to start searching |
| | 9701 | * for the player character. We don't have any generic |
| | 9702 | * mechanism to conduct such a search, but a game that |
| | 9703 | * implements one might want to make use of it here. |
| | 9704 | */ |
| | 9705 | } |
| | 9706 | |
| | 9707 | /* |
| | 9708 | * Receive notification that a command we sent to another NPC |
| | 9709 | * failed. This is only called when one NPC sends a command to |
| | 9710 | * another NPC; this is called on the issuer to let the issuer know |
| | 9711 | * that the target can't perform the command because of the given |
| | 9712 | * resolution failure. |
| | 9713 | * |
| | 9714 | * By default, we don't do anything here, because we don't have any |
| | 9715 | * default code to send a command from one NPC to another. Any |
| | 9716 | * custom NPC actor that sends a command to another NPC actor might |
| | 9717 | * want to use this to deal with problems in processing those |
| | 9718 | * commands. |
| | 9719 | */ |
| | 9720 | notifyIssuerParseFailure(targetActor, messageProp, args) |
| | 9721 | { |
| | 9722 | /* by default, we do nothing */ |
| | 9723 | } |
| | 9724 | |
| | 9725 | /* |
| | 9726 | * Antecedent lookup table. Each actor keeps its own table of |
| | 9727 | * antecedents indexed by pronoun type, so that we can |
| | 9728 | * simultaneously have different antecedents for different pronouns. |
| | 9729 | */ |
| | 9730 | antecedentTable = nil |
| | 9731 | |
| | 9732 | /* |
| | 9733 | * Possessive anaphor lookup table. In almost all cases, the |
| | 9734 | * possessive anaphor for a given pronoun will be the same as the |
| | 9735 | * corresponding regular pronoun: HIS indicates possession by HIM, |
| | 9736 | * for example. In a few cases, though, the anaphoric quality of |
| | 9737 | * possessives takes precedence, and these will differ. For |
| | 9738 | * example, in TELL BOB TO DROP HIS BOOK, "his" refers back to Bob, |
| | 9739 | * while in TELL BOB TO HIT HIM, "him" refers to whatever it |
| | 9740 | * referred to before the command. |
| | 9741 | */ |
| | 9742 | possAnaphorTable = nil |
| | 9743 | |
| | 9744 | /* |
| | 9745 | * set the antecedent for the neuter singular pronoun ("it" in |
| | 9746 | * English) |
| | 9747 | */ |
| | 9748 | setIt(obj) |
| | 9749 | { |
| | 9750 | setPronounAntecedent(PronounIt, obj); |
| | 9751 | } |
| | 9752 | |
| | 9753 | /* set the antecedent for the masculine singular ("him") */ |
| | 9754 | setHim(obj) |
| | 9755 | { |
| | 9756 | setPronounAntecedent(PronounHim, obj); |
| | 9757 | } |
| | 9758 | |
| | 9759 | /* set the antecedent for the feminine singular ("her") */ |
| | 9760 | setHer(obj) |
| | 9761 | { |
| | 9762 | setPronounAntecedent(PronounHer, obj); |
| | 9763 | } |
| | 9764 | |
| | 9765 | /* set the antecedent list for the ungendered plural pronoun ("them") */ |
| | 9766 | setThem(lst) |
| | 9767 | { |
| | 9768 | setPronounAntecedent(PronounThem, lst); |
| | 9769 | } |
| | 9770 | |
| | 9771 | /* look up a pronoun's value */ |
| | 9772 | getPronounAntecedent(typ) |
| | 9773 | { |
| | 9774 | /* get the stored antecedent for this pronoun */ |
| | 9775 | return antecedentTable[typ]; |
| | 9776 | } |
| | 9777 | |
| | 9778 | /* set a pronoun's antecedent value */ |
| | 9779 | setPronounAntecedent(typ, val) |
| | 9780 | { |
| | 9781 | /* remember the value in the antecedent table */ |
| | 9782 | antecedentTable[typ] = val; |
| | 9783 | |
| | 9784 | /* set the same value for the possessive anaphor */ |
| | 9785 | possAnaphorTable[typ] = val; |
| | 9786 | } |
| | 9787 | |
| | 9788 | /* set a possessive anaphor value */ |
| | 9789 | setPossAnaphor(typ, val) |
| | 9790 | { |
| | 9791 | /* set the value in the possessive anaphor table only */ |
| | 9792 | possAnaphorTable[typ] = val; |
| | 9793 | } |
| | 9794 | |
| | 9795 | /* get a possessive anaphor value */ |
| | 9796 | getPossAnaphor(typ) { return possAnaphorTable[typ]; } |
| | 9797 | |
| | 9798 | /* forget the possessive anaphors */ |
| | 9799 | forgetPossAnaphors() |
| | 9800 | { |
| | 9801 | /* copy all of the antecedents to the possessive anaphor table */ |
| | 9802 | antecedentTable.forEachAssoc( |
| | 9803 | {key, val: possAnaphorTable[key] = val}); |
| | 9804 | } |
| | 9805 | |
| | 9806 | /* |
| | 9807 | * Copy pronoun antecedents from the given actor. This should be |
| | 9808 | * called whenever an actor issues a command to us, so that pronouns |
| | 9809 | * in the command are properly resolved relative to the issuer. |
| | 9810 | */ |
| | 9811 | copyPronounAntecedentsFrom(issuer) |
| | 9812 | { |
| | 9813 | /* copy every element from the issuer's table */ |
| | 9814 | issuer.antecedentTable.forEachAssoc( |
| | 9815 | {key, val: setPronounAntecedent(key, val)}); |
| | 9816 | } |
| | 9817 | |
| | 9818 | /* -------------------------------------------------------------------- */ |
| | 9819 | /* |
| | 9820 | * Verb processing |
| | 9821 | */ |
| | 9822 | |
| | 9823 | /* show a "take from" message as indicating I don't have the dobj */ |
| | 9824 | takeFromNotInMessage = &takeFromNotInActorMsg |
| | 9825 | |
| | 9826 | /* verify() handler to check against applying an action to 'self' */ |
| | 9827 | verifyNotSelf(msg) |
| | 9828 | { |
| | 9829 | /* check to make sure we're not trying to do this to myself */ |
| | 9830 | if (self == gActor) |
| | 9831 | illogicalSelf(msg); |
| | 9832 | } |
| | 9833 | |
| | 9834 | /* macro to verify we're not self, and inherit the default behavior */ |
| | 9835 | #define verifyNotSelfInherit(msg) \ |
| | 9836 | verify() \ |
| | 9837 | { \ |
| | 9838 | verifyNotSelf(msg); \ |
| | 9839 | inherited(); \ |
| | 9840 | } |
| | 9841 | |
| | 9842 | /* |
| | 9843 | * For the basic physical manipulation verbs (TAKE, DROP, PUT ON, |
| | 9844 | * etc), it's illogical to operate on myself, so check for this in |
| | 9845 | * verify(). Otherwise, handle these as we would ordinary objects, |
| | 9846 | * since we might be able to manipulate other actors in the normal |
| | 9847 | * manner, especially actors small enough that we can pick them up. |
| | 9848 | */ |
| | 9849 | dobjFor(Take) { verifyNotSelfInherit(&takingSelfMsg) } |
| | 9850 | dobjFor(Drop) { verifyNotSelfInherit(&droppingSelfMsg) } |
| | 9851 | dobjFor(PutOn) { verifyNotSelfInherit(&puttingSelfMsg) } |
| | 9852 | dobjFor(PutUnder) { verifyNotSelfInherit(&puttingSelfMsg) } |
| | 9853 | dobjFor(Throw) { verifyNotSelfInherit(&throwingSelfMsg) } |
| | 9854 | dobjFor(ThrowAt) { verifyNotSelfInherit(&throwingSelfMsg) } |
| | 9855 | dobjFor(ThrowDir) { verifyNotSelfInherit(&throwingSelfMsg) } |
| | 9856 | dobjFor(ThrowTo) { verifyNotSelfInherit(&throwingSelfMsg) } |
| | 9857 | |
| | 9858 | /* customize the message for THROW TO <actor> */ |
| | 9859 | iobjFor(ThrowTo) |
| | 9860 | { |
| | 9861 | verify() |
| | 9862 | { |
| | 9863 | /* by default, we don't want to catch anything */ |
| | 9864 | illogical(&willNotCatchMsg, self); |
| | 9865 | } |
| | 9866 | } |
| | 9867 | |
| | 9868 | /* treat PUT SELF IN FOO as GET IN FOO */ |
| | 9869 | dobjFor(PutIn) |
| | 9870 | { |
| | 9871 | verify() |
| | 9872 | { |
| | 9873 | /* the target actor is always unsuitable as a default */ |
| | 9874 | if (gActor == self) |
| | 9875 | nonObvious; |
| | 9876 | } |
| | 9877 | |
| | 9878 | check() |
| | 9879 | { |
| | 9880 | /* if I'm putting myself somewhere, treat it as GET IN */ |
| | 9881 | if (gActor == self) |
| | 9882 | replaceAction(Enter, gIobj); |
| | 9883 | |
| | 9884 | /* do the normal work */ |
| | 9885 | inherited(); |
| | 9886 | } |
| | 9887 | } |
| | 9888 | |
| | 9889 | dobjFor(Kiss) |
| | 9890 | { |
| | 9891 | preCond = [touchObj] |
| | 9892 | verify() |
| | 9893 | { |
| | 9894 | /* cannot kiss oneself */ |
| | 9895 | verifyNotSelf(&cannotKissSelfMsg); |
| | 9896 | } |
| | 9897 | action() { mainReport(&cannotKissActorMsg); } |
| | 9898 | } |
| | 9899 | |
| | 9900 | dobjFor(AskFor) |
| | 9901 | { |
| | 9902 | preCond = [canTalkToObj] |
| | 9903 | verify() |
| | 9904 | { |
| | 9905 | /* it makes no sense to ask myself for something */ |
| | 9906 | verifyNotSelf(&cannotAskSelfForMsg); |
| | 9907 | } |
| | 9908 | action() |
| | 9909 | { |
| | 9910 | /* note that the issuer is targeting us with conversation */ |
| | 9911 | gActor.noteConversation(self); |
| | 9912 | |
| | 9913 | /* let the state object handle it */ |
| | 9914 | curState.handleConversation(gActor, gTopic, askForConvType); |
| | 9915 | } |
| | 9916 | } |
| | 9917 | |
| | 9918 | dobjFor(TalkTo) |
| | 9919 | { |
| | 9920 | preCond = [canTalkToObj] |
| | 9921 | verify() |
| | 9922 | { |
| | 9923 | /* it's generally illogical to talk to oneself */ |
| | 9924 | verifyNotSelf(&cannotTalkToSelfMsg); |
| | 9925 | } |
| | 9926 | action() |
| | 9927 | { |
| | 9928 | /* note that the issuer is targeting us in conversation */ |
| | 9929 | gActor.noteConversation(self); |
| | 9930 | |
| | 9931 | /* handle it as a 'hello' topic */ |
| | 9932 | curState.handleConversation(gActor, helloTopicObj, helloConvType); |
| | 9933 | } |
| | 9934 | } |
| | 9935 | |
| | 9936 | iobjFor(GiveTo) |
| | 9937 | { |
| | 9938 | verify() |
| | 9939 | { |
| | 9940 | /* it makes no sense to give something to myself */ |
| | 9941 | verifyNotSelf(&cannotGiveToSelfMsg); |
| | 9942 | |
| | 9943 | /* it also makes no sense to give something to itself */ |
| | 9944 | if (gDobj == gIobj) |
| | 9945 | illogicalSelf(&cannotGiveToItselfMsg); |
| | 9946 | } |
| | 9947 | action() |
| | 9948 | { |
| | 9949 | /* take note that I've seen the direct object */ |
| | 9950 | noteObjectShown(gDobj); |
| | 9951 | |
| | 9952 | /* note that the issuer is targeting us with conversation */ |
| | 9953 | gActor.noteConversation(self); |
| | 9954 | |
| | 9955 | /* let the state object handle it */ |
| | 9956 | curState.handleConversation(gActor, gDobj, giveConvType); |
| | 9957 | } |
| | 9958 | } |
| | 9959 | |
| | 9960 | iobjFor(ShowTo) |
| | 9961 | { |
| | 9962 | verify() |
| | 9963 | { |
| | 9964 | /* it makes no sense to show something to myself */ |
| | 9965 | verifyNotSelf(&cannotShowToSelfMsg); |
| | 9966 | |
| | 9967 | /* it also makes no sense to show something to itself */ |
| | 9968 | if (gDobj == gIobj) |
| | 9969 | illogicalSelf(&cannotShowToItselfMsg); |
| | 9970 | } |
| | 9971 | action() |
| | 9972 | { |
| | 9973 | /* take note that I've seen the direct object */ |
| | 9974 | noteObjectShown(gDobj); |
| | 9975 | |
| | 9976 | /* note that the issuer is targeting us with conversation */ |
| | 9977 | gActor.noteConversation(self); |
| | 9978 | |
| | 9979 | /* let the actor state object handle it */ |
| | 9980 | curState.handleConversation(gActor, gDobj, showConvType); |
| | 9981 | } |
| | 9982 | } |
| | 9983 | |
| | 9984 | /* |
| | 9985 | * Note that the given object has been explicitly shown to me. By |
| | 9986 | * default, we'll mark the object and its visible contents as having |
| | 9987 | * been seen by me. This is called whenever we're the target of a |
| | 9988 | * SHOW TO or GIVE TO, since presumably such an explicit act of |
| | 9989 | * calling our attention to an object would make us consider the |
| | 9990 | * object as having been seen in the future. |
| | 9991 | */ |
| | 9992 | noteObjectShown(obj) |
| | 9993 | { |
| | 9994 | local info; |
| | 9995 | |
| | 9996 | /* get the table of things we can see */ |
| | 9997 | info = visibleInfoTable(); |
| | 9998 | |
| | 9999 | /* if the object is in the table, mark it as seen */ |
| | 10000 | if (info[obj] != nil) |
| | 10001 | setHasSeen(obj); |
| | 10002 | |
| | 10003 | /* also mark the visible contents of the object as having been seen */ |
| | 10004 | obj.setContentsSeenBy(info, self); |
| | 10005 | } |
| | 10006 | |
| | 10007 | dobjFor(AskAbout) |
| | 10008 | { |
| | 10009 | preCond = [canTalkToObj] |
| | 10010 | verify() |
| | 10011 | { |
| | 10012 | /* it makes no sense to ask oneself about something */ |
| | 10013 | verifyNotSelf(&cannotAskSelfMsg); |
| | 10014 | } |
| | 10015 | action() |
| | 10016 | { |
| | 10017 | /* note that the issuer is targeting us with conversation */ |
| | 10018 | gActor.noteConversation(self); |
| | 10019 | |
| | 10020 | /* let our state object handle it */ |
| | 10021 | curState.handleConversation(gActor, gTopic, askAboutConvType); |
| | 10022 | } |
| | 10023 | } |
| | 10024 | |
| | 10025 | dobjFor(TellAbout) |
| | 10026 | { |
| | 10027 | preCond = [canTalkToObj] |
| | 10028 | verify() |
| | 10029 | { |
| | 10030 | /* it makes no sense to tell oneself about something */ |
| | 10031 | verifyNotSelf(&cannotTellSelfMsg); |
| | 10032 | } |
| | 10033 | check() |
| | 10034 | { |
| | 10035 | /* |
| | 10036 | * If the direct object is the issuing actor, rephrase this |
| | 10037 | * as "issuer, ask actor about iobj". |
| | 10038 | * |
| | 10039 | * Note that we do this in 'check' rather than 'action', |
| | 10040 | * because this will ensure that we'll rephrase the command |
| | 10041 | * properly even if the subclass overrides with its own |
| | 10042 | * check, AS LONG AS the overriding method inherits this base |
| | 10043 | * definition first. If we did the rephrasing in the |
| | 10044 | * 'action', then an overriding 'check' might incorrectly |
| | 10045 | * disqualify the operation on the assumption that it's an |
| | 10046 | * ordinary TELL ABOUT rather than what it really is, which |
| | 10047 | * is a rephrased ASK ABOUT. |
| | 10048 | */ |
| | 10049 | if (gDobj == gIssuingActor) |
| | 10050 | replaceActorAction(gIssuingActor, AskAbout, gActor, gTopic); |
| | 10051 | |
| | 10052 | } |
| | 10053 | action() |
| | 10054 | { |
| | 10055 | /* note that the issuer is targeting us with conversation */ |
| | 10056 | gActor.noteConversation(self); |
| | 10057 | |
| | 10058 | /* let the state object handle it */ |
| | 10059 | curState.handleConversation(gActor, gTopic, tellAboutConvType); |
| | 10060 | } |
| | 10061 | } |
| | 10062 | |
| | 10063 | /* |
| | 10064 | * Handle a conversational command. All of the conversational |
| | 10065 | * actions (HELLO, GOODBYE, YES, NO, ASK ABOUT, ASK FOR, TELL ABOUT, |
| | 10066 | * SHOW TO, GIVE TO) are routed here when we're the target of the |
| | 10067 | * action (for example, we're BOB in ASK BOB ABOUT TOPIC) AND the |
| | 10068 | * ActorState doesn't want to handle the action. |
| | 10069 | */ |
| | 10070 | handleConversation(actor, topic, convType) |
| | 10071 | { |
| | 10072 | /* try handling the topic from our topic database */ |
| | 10073 | if (!handleTopic(actor, topic, convType, nil)) |
| | 10074 | { |
| | 10075 | /* the topic database didn't handle it; use a default response */ |
| | 10076 | defaultConvResponse(actor, topic, convType); |
| | 10077 | } |
| | 10078 | } |
| | 10079 | |
| | 10080 | /* |
| | 10081 | * Show a default response to a conversational action. By default, |
| | 10082 | * we'll show the default response for our conversation type. |
| | 10083 | */ |
| | 10084 | defaultConvResponse(actor, topic, convType) |
| | 10085 | { |
| | 10086 | /* call the appropriate default response for the ConvType */ |
| | 10087 | convType.defaultResponse(self, actor, topic); |
| | 10088 | } |
| | 10089 | |
| | 10090 | /* |
| | 10091 | * Show our default greeting message - this is used when the given |
| | 10092 | * another actor greets us with HELLO or TALK TO, and we don't |
| | 10093 | * otherwise handle it (such as via a topic database entry). |
| | 10094 | * |
| | 10095 | * By default, we'll just show "there's no response" as a default |
| | 10096 | * message. We'll show this in default mode, so that if the caller |
| | 10097 | * is going to show a list of suggested conversation topics (which |
| | 10098 | * the 'hello' and 'talk to' commands will normally try to do), the |
| | 10099 | * topic list will override the "there's no response" default. In |
| | 10100 | * other words, we'll have one of these two types of exchanges: |
| | 10101 | * |
| | 10102 | *. >talk to bob |
| | 10103 | *. There's no response |
| | 10104 | * |
| | 10105 | *. >talk to bill |
| | 10106 | *. You could ask him about the candle, the book, or the bell, or |
| | 10107 | *. tell him about the crypt. |
| | 10108 | */ |
| | 10109 | defaultGreetingResponse(actor) |
| | 10110 | { defaultReport(&noResponseFromMsg, self); } |
| | 10111 | |
| | 10112 | /* show our default goodbye message */ |
| | 10113 | defaultGoodbyeResponse(actor) |
| | 10114 | { mainReport(&noResponseFromMsg, self); } |
| | 10115 | |
| | 10116 | /* |
| | 10117 | * Show the default answer to a question - this is called when we're |
| | 10118 | * the actor in ASK <actor> ABOUT <topic>, and we can't find a more |
| | 10119 | * specific response for the given topic. |
| | 10120 | * |
| | 10121 | * By default, we'll show the basic "there's no response" message. |
| | 10122 | * This isn't a very good message in most cases, because it makes an |
| | 10123 | * actor pretty frustratingly un-interactive, which gives the actor |
| | 10124 | * the appearance of a cardboard cut-out. But there's not much |
| | 10125 | * better that the library can do; the potential range of actors |
| | 10126 | * makes a more specific default response impossible. If the default |
| | 10127 | * response were "I don't know about that," it wouldn't work very |
| | 10128 | * well if the actor is someone who only speaks Italian. So, the |
| | 10129 | * best we can do is this generally rather poor default. But that |
| | 10130 | * doesn't mean that authors should resign themselves to a poor |
| | 10131 | * default answer; instead, it means that actors should take care to |
| | 10132 | * override this when defining an actor, because it's usually |
| | 10133 | * possible to find a much better default for a *specific* actor. |
| | 10134 | * |
| | 10135 | * The *usual* way of providing a default response is to define a |
| | 10136 | * DefaultAskTopic (or a DefaultAskTellTopic) and put it in the |
| | 10137 | * actor's topic database. |
| | 10138 | */ |
| | 10139 | defaultAskResponse(fromActor, topic) |
| | 10140 | { mainReport(&noResponseFromMsg, self); } |
| | 10141 | |
| | 10142 | /* |
| | 10143 | * Show the default response to being told of a topic - this is |
| | 10144 | * called when we're the actor in TELL <actor> ABOUT <topic>, and we |
| | 10145 | * can't find a more specific response for the topic. |
| | 10146 | * |
| | 10147 | * As with defaultAskResponse, this should almost always be |
| | 10148 | * overridden by each actor, since the default response ("there's no |
| | 10149 | * response") doesn't make the actor seem very dynamic. |
| | 10150 | * |
| | 10151 | * The usual way of providing a default response is to define a |
| | 10152 | * DefaultTellTopic (or a DefaultAskTellTopic) and put it in the |
| | 10153 | * actor's topic database. |
| | 10154 | */ |
| | 10155 | defaultTellResponse(fromActor, topic) |
| | 10156 | { mainReport(&noResponseFromMsg, self); } |
| | 10157 | |
| | 10158 | /* the default response for SHOW TO */ |
| | 10159 | defaultShowResponse(byActor, topic) |
| | 10160 | { mainReport(¬InterestedMsg, self); } |
| | 10161 | |
| | 10162 | /* the default response for GIVE TO */ |
| | 10163 | defaultGiveResponse(byActor, topic) |
| | 10164 | { mainReport(¬InterestedMsg, self); } |
| | 10165 | |
| | 10166 | /* the default response for ASK FOR */ |
| | 10167 | defaultAskForResponse(byActor, obj) |
| | 10168 | { mainReport(&noResponseFromMsg, self); } |
| | 10169 | |
| | 10170 | /* default response to being told YES */ |
| | 10171 | defaultYesResponse(fromActor) |
| | 10172 | { mainReport(&noResponseFromMsg, self); } |
| | 10173 | |
| | 10174 | /* default response to being told NO */ |
| | 10175 | defaultNoResponse(fromActor) |
| | 10176 | { mainReport(&noResponseFromMsg, self); } |
| | 10177 | |
| | 10178 | /* default refusal of a command */ |
| | 10179 | defaultCommandResponse(fromActor, topic) |
| | 10180 | { mainReport(&refuseCommand, self, fromActor); } |
| | 10181 | ; |
| | 10182 | |
| | 10183 | /* ------------------------------------------------------------------------ */ |
| | 10184 | /* |
| | 10185 | * An UntakeableActor is one that can't be picked up and moved. |
| | 10186 | */ |
| | 10187 | class UntakeableActor: Actor, Immovable |
| | 10188 | /* use customized messages for some 'Immovable' methods */ |
| | 10189 | cannotTakeMsg = &cannotTakeActorMsg |
| | 10190 | cannotMoveMsg = &cannotMoveActorMsg |
| | 10191 | cannotPutMsg = &cannotPutActorMsg |
| | 10192 | |
| | 10193 | /* TASTE tends to be a bit rude */ |
| | 10194 | dobjFor(Taste) |
| | 10195 | { |
| | 10196 | action() { mainReport(&cannotTasteActorMsg); } |
| | 10197 | } |
| | 10198 | |
| | 10199 | /* |
| | 10200 | * even though we act like an Immovable, we don't count as an |
| | 10201 | * Immovable for listing purposes |
| | 10202 | */ |
| | 10203 | contentsInFixedIn(loc) { return nil; } |
| | 10204 | ; |
| | 10205 | |
| | 10206 | |
| | 10207 | /* |
| | 10208 | * A Person is an actor that represents a human character. This is just |
| | 10209 | * an UntakeableActor with some custom versions of the messages for |
| | 10210 | * taking and moving the actor. |
| | 10211 | */ |
| | 10212 | class Person: UntakeableActor |
| | 10213 | /* customize the messages for trying to take or move me */ |
| | 10214 | cannotTakeMsg = &cannotTakePersonMsg |
| | 10215 | cannotMoveMsg = &cannotMovePersonMsg |
| | 10216 | cannotPutMsg = &cannotPutPersonMsg |
| | 10217 | cannotTasteActorMsg = &cannotTastePersonMsg |
| | 10218 | |
| | 10219 | /* |
| | 10220 | * use a fairly large default bulk, since people are usually fairly |
| | 10221 | * large compared with the sorts of items that one carries around |
| | 10222 | */ |
| | 10223 | bulk = 10 |
| | 10224 | ; |
| | 10225 | |
| | 10226 | /* ------------------------------------------------------------------------ */ |
| | 10227 | /* |
| | 10228 | * Pending response information structure |
| | 10229 | */ |
| | 10230 | class PendingResponseInfo: object |
| | 10231 | construct(issuer, prop, args) |
| | 10232 | { |
| | 10233 | issuer_ = issuer; |
| | 10234 | prop_ = prop; |
| | 10235 | args_ = args; |
| | 10236 | } |
| | 10237 | |
| | 10238 | /* the issuer of the command (and target of the response) */ |
| | 10239 | issuer_ = nil |
| | 10240 | |
| | 10241 | /* the message property and argument list for the message */ |
| | 10242 | prop_ = nil |
| | 10243 | args_ = [] |
| | 10244 | ; |
| | 10245 | |
| | 10246 | /* |
| | 10247 | * Pending Command Information structure. This is an abstract base class |
| | 10248 | * that we subclass for particular ways of representing the command to be |
| | 10249 | * executed. |
| | 10250 | */ |
| | 10251 | class PendingCommandInfo: object |
| | 10252 | construct(issuer) { issuer_ = issuer; } |
| | 10253 | |
| | 10254 | /* |
| | 10255 | * Check to see if this pending command item has a command to |
| | 10256 | * perform. This returns true if we have a command, nil if we're |
| | 10257 | * just a queue placeholder without any actual command to execute. |
| | 10258 | */ |
| | 10259 | hasCommand = true |
| | 10260 | |
| | 10261 | /* execute the command */ |
| | 10262 | executePending(targetActor) { } |
| | 10263 | |
| | 10264 | /* the issuer of the command */ |
| | 10265 | issuer_ = nil |
| | 10266 | |
| | 10267 | /* we're at the start of a "sentence" */ |
| | 10268 | startOfSentence_ = nil |
| | 10269 | ; |
| | 10270 | |
| | 10271 | /* a pending command based on a list of tokens from an input string */ |
| | 10272 | class PendingCommandToks: PendingCommandInfo |
| | 10273 | construct(startOfSentence, issuer, toks) |
| | 10274 | { |
| | 10275 | inherited(issuer); |
| | 10276 | |
| | 10277 | startOfSentence_ = startOfSentence; |
| | 10278 | tokens_ = toks; |
| | 10279 | } |
| | 10280 | |
| | 10281 | /* |
| | 10282 | * Execute the command. We'll parse our tokens and execute the |
| | 10283 | * parsed results. |
| | 10284 | */ |
| | 10285 | executePending(targetActor) |
| | 10286 | { |
| | 10287 | /* parse and execute the tokens */ |
| | 10288 | executeCommand(targetActor, issuer_, tokens_, startOfSentence_); |
| | 10289 | } |
| | 10290 | |
| | 10291 | /* the token list for the command */ |
| | 10292 | tokens_ = nil |
| | 10293 | ; |
| | 10294 | |
| | 10295 | /* a pending command based on a pre-resolved Action and its objects */ |
| | 10296 | class PendingCommandAction: PendingCommandInfo |
| | 10297 | construct(startOfSentence, issuer, action, [objs]) |
| | 10298 | { |
| | 10299 | inherited(issuer); |
| | 10300 | |
| | 10301 | startOfSentence_ = startOfSentence; |
| | 10302 | action_ = action; |
| | 10303 | objs_ = objs; |
| | 10304 | } |
| | 10305 | |
| | 10306 | /* execute the pending command */ |
| | 10307 | executePending(targetActor) |
| | 10308 | { |
| | 10309 | /* invoke the action's main execution method */ |
| | 10310 | try |
| | 10311 | { |
| | 10312 | /* run the action */ |
| | 10313 | newActionObj(CommandTranscript, issuer_, |
| | 10314 | targetActor, action_, objs_...); |
| | 10315 | } |
| | 10316 | catch (TerminateCommandException tcExc) |
| | 10317 | { |
| | 10318 | /* |
| | 10319 | * the command cannot proceed; simply abandon the command |
| | 10320 | * action here |
| | 10321 | */ |
| | 10322 | } |
| | 10323 | } |
| | 10324 | |
| | 10325 | /* the resolved Action to perform */ |
| | 10326 | action_ = nil |
| | 10327 | |
| | 10328 | /* the resolved objects for the action */ |
| | 10329 | objs_ = nil |
| | 10330 | ; |
| | 10331 | |
| | 10332 | /* |
| | 10333 | * A pending command marker. This is not an actual pending command; |
| | 10334 | * rather, it's just a queue marker. We sometimes want to synchronize |
| | 10335 | * some other activity with an actor's progress through its command |
| | 10336 | * queue; for example, we might want one actor to wait until another |
| | 10337 | * actor has executed a particular pending action. These markers can be |
| | 10338 | * used for this kind of synchronization; they move through the queue |
| | 10339 | * like ordinary pending commands, so we can tell if an actor has reached |
| | 10340 | * a particular command by observing the marker's progress through the |
| | 10341 | * queue. |
| | 10342 | */ |
| | 10343 | class PendingCommandMarker: PendingCommandInfo |
| | 10344 | /* I have no command to execute */ |
| | 10345 | hasCommand = nil |
| | 10346 | ; |
| | 10347 | |
| | 10348 | /* ------------------------------------------------------------------------ */ |
| | 10349 | /* |
| | 10350 | * Set the current player character |
| | 10351 | */ |
| | 10352 | setPlayer(actor) |
| | 10353 | { |
| | 10354 | /* remember the new player character */ |
| | 10355 | libGlobal.playerChar = actor; |
| | 10356 | |
| | 10357 | /* set the root global point of view to this actor */ |
| | 10358 | setRootPOV(actor, actor); |
| | 10359 | } |
| | 10360 | |