| | 1 | #charset "us-ascii" |
| | 2 | |
| | 3 | /* |
| | 4 | * Copyright (c) 2002, 2006 by Michael J. Roberts |
| | 5 | * |
| | 6 | * Based on exitslister.t, copyright 2002 by Steve Breslin and |
| | 7 | * incorporated by permission. |
| | 8 | * |
| | 9 | * TADS 3 Library - Exits Lister |
| | 10 | * |
| | 11 | * This module provides an automatic exit lister that shows the apparent |
| | 12 | * exits from the player character's location. The automatic exit lister |
| | 13 | * can optionally provide these main features: |
| | 14 | * |
| | 15 | * - An "exits" verb lets the player explicitly show the list of apparent |
| | 16 | * exits, along with the name of the room to which each exit connects. |
| | 17 | * |
| | 18 | * - Exits can be shown automatically as part of the room description. |
| | 19 | * This extra information can be controlled by the player through the |
| | 20 | * "exits on" and "exits off" command. |
| | 21 | * |
| | 22 | * - Exits can be shown automatically when an actor tries to go in a |
| | 23 | * direction where no exit exists, as a helpful reminder of which |
| | 24 | * directions are valid. |
| | 25 | */ |
| | 26 | |
| | 27 | /* include the library header */ |
| | 28 | #include "adv3.h" |
| | 29 | |
| | 30 | |
| | 31 | /* ------------------------------------------------------------------------ */ |
| | 32 | /* |
| | 33 | * The main exits lister. |
| | 34 | */ |
| | 35 | exitLister: PreinitObject |
| | 36 | /* preinitialization */ |
| | 37 | execute() |
| | 38 | { |
| | 39 | /* install myself as the global exit lister object */ |
| | 40 | gExitLister = self; |
| | 41 | } |
| | 42 | |
| | 43 | /* |
| | 44 | * Flag: use "verbose" listing style for exit lists in room |
| | 45 | * descriptions. When this is set to true, we'll show a |
| | 46 | * sentence-style list of exits ("Obvious exits lead east to the |
| | 47 | * living room, south, and up."). When this is set to nil, we'll use |
| | 48 | * a terse style, enclosing the message in the default system |
| | 49 | * message's brackets ("[Obvious exits: East, West]"). |
| | 50 | * |
| | 51 | * Verbose-style room descriptions tend to fit well with a room |
| | 52 | * description's prose, but at the expense of looking redundant with |
| | 53 | * the exit list that's usually built into each room's custom |
| | 54 | * descriptive text to begin with. Some authors prefer the terse |
| | 55 | * style precisely because it doesn't look like more prose |
| | 56 | * description, but looks like a separate bit of information being |
| | 57 | * offered. |
| | 58 | * |
| | 59 | * This is an author-configured setting; the library does not provide |
| | 60 | * a command to let the player control this setting. |
| | 61 | */ |
| | 62 | roomDescVerbose = nil |
| | 63 | |
| | 64 | /* |
| | 65 | * Flag: show automatic exit listings on attempts to move in |
| | 66 | * directions that don't allow travel. Enable this by default, |
| | 67 | * since most players appreciate having the exit list called out |
| | 68 | * separately from the room description (where any mention of exits |
| | 69 | * might be buried in lots of other text) in place of an unspecific |
| | 70 | * "you can't go that way". |
| | 71 | * |
| | 72 | * This is an author-configured setting; the library does not provide |
| | 73 | * a command to let the player control this setting. |
| | 74 | */ |
| | 75 | enableReminder = true |
| | 76 | |
| | 77 | /* |
| | 78 | * Flag: enable the automatic exit reminder even when the room |
| | 79 | * description exit listing is enabled. When this is nil, we will |
| | 80 | * NOT show a reminder with "can't go that way" messages when the |
| | 81 | * room description exit list is enabled - this is the default, |
| | 82 | * because it can be a little much to have the list of exits shown so |
| | 83 | * frequently. Some authors might prefer to show the reminder |
| | 84 | * unconditionally, though, so this option is offered. |
| | 85 | * |
| | 86 | * This is an author-configured setting; the library does not provide |
| | 87 | * a command to let the player control this setting. |
| | 88 | */ |
| | 89 | enableReminderAlways = nil |
| | 90 | |
| | 91 | /* |
| | 92 | * Flag: use hyperlinks in the directions mentioned in room |
| | 93 | * description exit lists, so that players can click on the direction |
| | 94 | * name in the listing to enter the direction command. |
| | 95 | */ |
| | 96 | enableHyperlinks = true |
| | 97 | |
| | 98 | /* flag: we've explained how the exits on/off command works */ |
| | 99 | exitsOnOffExplained = nil |
| | 100 | |
| | 101 | /* |
| | 102 | * Determine if the "reminder" is enabled. The reminder is the list |
| | 103 | * of exits we show along with a "can't go that way" message, to |
| | 104 | * reminder the player of the valid exits when an invalid one is |
| | 105 | * attempted. |
| | 106 | */ |
| | 107 | isReminderEnabled() |
| | 108 | { |
| | 109 | /* |
| | 110 | * The reminder is enabled if enableReminderAlways is true, OR if |
| | 111 | * enableReminder is true AND exitsMode.inRoomDesc is nil. |
| | 112 | */ |
| | 113 | return (enableReminderAlways |
| | 114 | || (enableReminder && !exitsMode.inRoomDesc)); |
| | 115 | } |
| | 116 | |
| | 117 | /* |
| | 118 | * Get the exit lister we use for room descriptions. |
| | 119 | */ |
| | 120 | getRoomDescLister() |
| | 121 | { |
| | 122 | /* use the verbose or terse lister, according to the configuration */ |
| | 123 | return roomDescVerbose |
| | 124 | ? lookAroundExitLister |
| | 125 | : lookAroundTerseExitLister; |
| | 126 | } |
| | 127 | |
| | 128 | /* perform the "exits" command to show exits on explicit request */ |
| | 129 | showExitsCommand() |
| | 130 | { |
| | 131 | /* show exits for the current actor */ |
| | 132 | showExits(gActor); |
| | 133 | |
| | 134 | /* |
| | 135 | * if we haven't explained how to turn exit listing on and off, |
| | 136 | * do so now |
| | 137 | */ |
| | 138 | if (!exitsOnOffExplained) |
| | 139 | { |
| | 140 | gLibMessages.explainExitsOnOff; |
| | 141 | exitsOnOffExplained = true; |
| | 142 | } |
| | 143 | } |
| | 144 | |
| | 145 | /* |
| | 146 | * Perform an EXITS ON/OFF/STATUS/LOOK command. 'stat' indicates |
| | 147 | * whether we're turning on (true) or off (nil) the statusline exit |
| | 148 | * listing; 'look' indicates whether we're turning the room |
| | 149 | * description listing on or off. |
| | 150 | */ |
| | 151 | exitsOnOffCommand(stat, look) |
| | 152 | { |
| | 153 | /* set the new status */ |
| | 154 | exitsMode.inStatusLine = stat; |
| | 155 | exitsMode.inRoomDesc = look; |
| | 156 | |
| | 157 | /* confirm the new status */ |
| | 158 | gLibMessages.exitsOnOffOkay(stat, look); |
| | 159 | |
| | 160 | /* |
| | 161 | * If we haven't already explained how the EXITS ON/OFF command |
| | 162 | * works, don't bother explaining it now, since they obviously |
| | 163 | * know how it works if they've actually used it. |
| | 164 | */ |
| | 165 | exitsOnOffExplained = true; |
| | 166 | } |
| | 167 | |
| | 168 | /* show the list of exits from an actor's current location */ |
| | 169 | showExits(actor) |
| | 170 | { |
| | 171 | /* show exits from the actor's location */ |
| | 172 | showExitsFrom(actor, actor.location); |
| | 173 | } |
| | 174 | |
| | 175 | /* show an exit list display in the status line, if desired */ |
| | 176 | showStatuslineExits() |
| | 177 | { |
| | 178 | /* if statusline exit displays are enabled, show the exit list */ |
| | 179 | if (exitsMode.inStatusLine) |
| | 180 | showExitsWithLister(gPlayerChar, gPlayerChar.location, |
| | 181 | statuslineExitLister, |
| | 182 | gPlayerChar.location |
| | 183 | .wouldBeLitFor(gPlayerChar)); |
| | 184 | } |
| | 185 | |
| | 186 | /* |
| | 187 | * Calculate the contribution of the exits list to the height of the |
| | 188 | * status line, in lines of text. If we're not configured to display |
| | 189 | * the exits list in the status line, then the contribution is zero; |
| | 190 | * otherwise, we'll estimate how much space we need to display the |
| | 191 | * exit list. |
| | 192 | */ |
| | 193 | getStatuslineExitsHeight() |
| | 194 | { |
| | 195 | /* |
| | 196 | * if we're enabled, our standard display takes up one line; if |
| | 197 | * we're disabled, we don't contribute anything to the status |
| | 198 | * line's vertical extent |
| | 199 | */ |
| | 200 | if (exitsMode.inStatusLine) |
| | 201 | return 1; |
| | 202 | else |
| | 203 | return 0; |
| | 204 | } |
| | 205 | |
| | 206 | /* show exits as part of a room description */ |
| | 207 | lookAroundShowExits(actor, loc, illum) |
| | 208 | { |
| | 209 | /* if room exit displays are enabled, show the exits */ |
| | 210 | if (exitsMode.inRoomDesc) |
| | 211 | showExitsWithLister(actor, loc, getRoomDescLister, illum); |
| | 212 | } |
| | 213 | |
| | 214 | /* show exits as part of a "cannot go that way" error */ |
| | 215 | cannotGoShowExits(actor, loc) |
| | 216 | { |
| | 217 | /* if we want to show the reminder, show it */ |
| | 218 | if (isReminderEnabled()) |
| | 219 | showExitsWithLister(actor, loc, explicitExitLister, |
| | 220 | loc.wouldBeLitFor(actor)); |
| | 221 | } |
| | 222 | |
| | 223 | /* show the list of exits from a given location for a given actor */ |
| | 224 | showExitsFrom(actor, loc) |
| | 225 | { |
| | 226 | /* show exits with our standard lister */ |
| | 227 | showExitsWithLister(actor, loc, explicitExitLister, |
| | 228 | loc.wouldBeLitFor(actor)); |
| | 229 | } |
| | 230 | |
| | 231 | /* |
| | 232 | * Show the list of exits using a specific lister. |
| | 233 | * |
| | 234 | * 'actor' is the actor for whom the display is being generated. |
| | 235 | * 'loc' is the location whose exit list is to be shown; this need |
| | 236 | * not be the same as the actor's current location. 'lister' is the |
| | 237 | * Lister object that will show the list of DestInfo objects that we |
| | 238 | * create to represent the exit list. |
| | 239 | * |
| | 240 | * 'locIsLit' indicates whether or not the ambient illumination, for |
| | 241 | * the actor's visual senses, is sufficient that the actor would be |
| | 242 | * able to see if the actor were in the new location. We take this |
| | 243 | * as a parameter so that we don't have to re-compute the |
| | 244 | * information if the caller has already computed it for other |
| | 245 | * reasons (such as showing a room description). If the caller |
| | 246 | * hasn't otherwise computed the value, it can be easily computed as |
| | 247 | * loc.wouldBeLitFor(actor). |
| | 248 | */ |
| | 249 | showExitsWithLister(actor, loc, lister, locIsLit) |
| | 250 | { |
| | 251 | local destList; |
| | 252 | local showDest; |
| | 253 | local options; |
| | 254 | |
| | 255 | /* |
| | 256 | * Ask the lister if it shows the destination names. We need to |
| | 257 | * know because we want to consolidate exits that go to the same |
| | 258 | * place if and only if we're going to show the destination in |
| | 259 | * the listing; if we're not showing the destination, there's no |
| | 260 | * reason to consolidate. |
| | 261 | */ |
| | 262 | showDest = lister.listerShowsDest; |
| | 263 | |
| | 264 | /* we have no option flags for the lister yet */ |
| | 265 | options = 0; |
| | 266 | |
| | 267 | /* run through all of the directions used in the game */ |
| | 268 | destList = new Vector(Direction.allDirections.length()); |
| | 269 | foreach (local dir in Direction.allDirections) |
| | 270 | { |
| | 271 | local conn; |
| | 272 | |
| | 273 | /* |
| | 274 | * If the actor's location has a connector in this |
| | 275 | * direction, and the connector is apparent, add it to the |
| | 276 | * list. |
| | 277 | * |
| | 278 | * If the actor is in the dark, we can only see the |
| | 279 | * connector if the connector is visible in the dark. If |
| | 280 | * the actor isn't in the dark, we can show all of the |
| | 281 | * connectors. |
| | 282 | */ |
| | 283 | if ((conn = loc.getTravelConnector(dir, actor)) != nil |
| | 284 | && conn.isConnectorApparent(loc, actor) |
| | 285 | && conn.isConnectorListed |
| | 286 | && (locIsLit || conn.isConnectorVisibleInDark(loc, actor))) |
| | 287 | { |
| | 288 | local dest; |
| | 289 | local destName = nil; |
| | 290 | local destIsBack; |
| | 291 | |
| | 292 | /* |
| | 293 | * We have an apparent connection in this direction, so |
| | 294 | * add it to our list. First, check to see if we know |
| | 295 | * the destination. |
| | 296 | */ |
| | 297 | dest = conn.getApparentDestination(loc, actor); |
| | 298 | |
| | 299 | /* note if this is the "back to" connector for the actor */ |
| | 300 | destIsBack = (conn == actor.lastTravelBack); |
| | 301 | |
| | 302 | /* |
| | 303 | * If we know the destination, and they want to include |
| | 304 | * destination names where possible, get the name. If |
| | 305 | * there's a name to show, include the name. |
| | 306 | */ |
| | 307 | if (dest != nil |
| | 308 | && showDest |
| | 309 | && (destName = dest.getDestName(actor, loc)) != nil) |
| | 310 | { |
| | 311 | local orig; |
| | 312 | |
| | 313 | /* |
| | 314 | * we are going to show a destination name for this |
| | 315 | * item, so set the special option flag to let the |
| | 316 | * lister know that this is the case |
| | 317 | */ |
| | 318 | options |= ExitLister.hasDestNameFlag; |
| | 319 | |
| | 320 | /* |
| | 321 | * if this is the back-to connector, note that we |
| | 322 | * know the name of the back-to location |
| | 323 | */ |
| | 324 | if (destIsBack) |
| | 325 | options |= ExitLister.hasBackNameFlag; |
| | 326 | |
| | 327 | /* |
| | 328 | * If this destination name already appears in the |
| | 329 | * list, don't include this one in the list. |
| | 330 | * Instead, add this direction to the 'others' list |
| | 331 | * for the existing entry, so that we will show each |
| | 332 | * known destination only once. |
| | 333 | */ |
| | 334 | orig = destList.valWhich({x: x.dest_ == dest}); |
| | 335 | if (orig != nil) |
| | 336 | { |
| | 337 | /* |
| | 338 | * this same destination name is already present |
| | 339 | * - add this direction to the existing entry's |
| | 340 | * list of other directions going to the same |
| | 341 | * place |
| | 342 | */ |
| | 343 | orig.others_ += dir; |
| | 344 | |
| | 345 | /* |
| | 346 | * if this is the back-to connector, note it in |
| | 347 | * the original destination item |
| | 348 | */ |
| | 349 | if (destIsBack) |
| | 350 | orig.destIsBack_ = true; |
| | 351 | |
| | 352 | /* |
| | 353 | * don't add this direction to the main list, |
| | 354 | * since we don't want to list the destination |
| | 355 | * redundantly |
| | 356 | */ |
| | 357 | continue; |
| | 358 | } |
| | 359 | } |
| | 360 | |
| | 361 | /* add it to our list */ |
| | 362 | destList.append(new DestInfo(dir, dest, destName, |
| | 363 | destIsBack)); |
| | 364 | } |
| | 365 | } |
| | 366 | |
| | 367 | /* show the list */ |
| | 368 | lister.showListAll(destList.toList(), options, 0); |
| | 369 | } |
| | 370 | ; |
| | 371 | |
| | 372 | /* |
| | 373 | * A destination tracker. This keeps track of a direction and the |
| | 374 | * apparent destination in that direction. |
| | 375 | */ |
| | 376 | class DestInfo: object |
| | 377 | construct(dir, dest, destName, destIsBack) |
| | 378 | { |
| | 379 | /* remember the direction, destination, and destination name */ |
| | 380 | dir_ = dir; |
| | 381 | dest_ = dest; |
| | 382 | destName_ = destName; |
| | 383 | destIsBack_ = destIsBack; |
| | 384 | } |
| | 385 | |
| | 386 | /* the direction of travel */ |
| | 387 | dir_ = nil |
| | 388 | |
| | 389 | /* the destination room object */ |
| | 390 | dest_ = nil |
| | 391 | |
| | 392 | /* the name of the destination */ |
| | 393 | destName_ = nil |
| | 394 | |
| | 395 | /* flag: this is the "back to" destination */ |
| | 396 | destIsBack_ = nil |
| | 397 | |
| | 398 | /* list of other directions that go to our same destination */ |
| | 399 | others_ = [] |
| | 400 | ; |
| | 401 | |
| | 402 | /* |
| | 403 | * Settings item - show defaults in status line |
| | 404 | */ |
| | 405 | exitsMode: SettingsItem |
| | 406 | /* our ID */ |
| | 407 | settingID = 'adv3.exits' |
| | 408 | |
| | 409 | /* show our description */ |
| | 410 | settingDesc = |
| | 411 | (gLibMessages.currentExitsSettings(inStatusLine, inRoomDesc)) |
| | 412 | |
| | 413 | /* convert to text */ |
| | 414 | settingToText() |
| | 415 | { |
| | 416 | /* just return the two binary variables */ |
| | 417 | return (inStatusLine ? 'on' : 'off') |
| | 418 | + ',' |
| | 419 | + (inRoomDesc ? 'on' : 'off'); |
| | 420 | } |
| | 421 | |
| | 422 | settingFromText(str) |
| | 423 | { |
| | 424 | /* parse out our format */ |
| | 425 | if (rexMatch('<space>*(<alpha>+)<space>*,<space>*(<alpha>+)', |
| | 426 | str.toLower()) != nil) |
| | 427 | { |
| | 428 | /* pull out the two variables from the regexp groups */ |
| | 429 | inStatusLine = (rexGroup(1)[3] == 'on'); |
| | 430 | inRoomDesc = (rexGroup(2)[3] == 'on'); |
| | 431 | } |
| | 432 | } |
| | 433 | |
| | 434 | /* |
| | 435 | * Our value is in two parts. inStatusLine controls whether or not |
| | 436 | * we show the exit list in the status line; inRoomDesc controls the |
| | 437 | * exit listing in room descriptions. |
| | 438 | */ |
| | 439 | inStatusLine = true |
| | 440 | inRoomDesc = nil |
| | 441 | ; |
| | 442 | |