| | 1 | #charset "us-ascii" |
| | 2 | |
| | 3 | /* |
| | 4 | * Copyright (c) 2000, 2006 by Michael J. Roberts. All Rights Reserved. |
| | 5 | * |
| | 6 | * TADS 3 Library - Hint System |
| | 7 | * |
| | 8 | * This module provides a hint system framework. Games can use this |
| | 9 | * framework to define context-sensitive hints for players. |
| | 10 | * |
| | 11 | * This module depends on the menus module to display the user interface. |
| | 12 | */ |
| | 13 | |
| | 14 | /* include the library header */ |
| | 15 | #include "adv3.h" |
| | 16 | |
| | 17 | |
| | 18 | /* ------------------------------------------------------------------------ */ |
| | 19 | /* |
| | 20 | * We refer to some properties defined primarily in score.t - that's an |
| | 21 | * optional module, though, so make sure the compiler has heard of these. |
| | 22 | */ |
| | 23 | property scoreCount; |
| | 24 | |
| | 25 | |
| | 26 | /* ------------------------------------------------------------------------ */ |
| | 27 | /* |
| | 28 | * A basic hint menu object. This is an abstract base class that |
| | 29 | * encapsulates some behavior common to different hint menu classes. |
| | 30 | */ |
| | 31 | class HintMenuObject: object |
| | 32 | /* |
| | 33 | * The topic order. When we're about to show a list of open topics, |
| | 34 | * we'll sort the list in ascending order of this property, then in |
| | 35 | * ascending order of title. By default, we set this order value to |
| | 36 | * 1000; if individual goals don't override this, then they'll |
| | 37 | * simply be sorted lexically by topic name. This can be used if |
| | 38 | * there's some basis other than alphabetical order for sorting the |
| | 39 | * list. |
| | 40 | */ |
| | 41 | topicOrder = 1000 |
| | 42 | |
| | 43 | /* |
| | 44 | * Compare this goal to another, for the purposes of sorting a list |
| | 45 | * of topics. Returns a positive number if this goal sorts after |
| | 46 | * the other one, a negative number if this goal sorts before the |
| | 47 | * other one, 0 if the relative order is arbitrary. |
| | 48 | * |
| | 49 | * By default, we'll sort by topicOrder if the topicOrder values are |
| | 50 | * different, otherwise alphabetically by title. |
| | 51 | */ |
| | 52 | compareForTopicSort(other) |
| | 53 | { |
| | 54 | /* if the topicOrder values are different, sort by topicOrder */ |
| | 55 | if (topicOrder != other.topicOrder) |
| | 56 | return topicOrder - other.topicOrder; |
| | 57 | |
| | 58 | /* the topicOrder values are the same, so sort by title */ |
| | 59 | if (title > other.title) |
| | 60 | return 1; |
| | 61 | else if (title < other.title) |
| | 62 | return -1; |
| | 63 | else |
| | 64 | return 0; |
| | 65 | } |
| | 66 | ; |
| | 67 | |
| | 68 | /* |
| | 69 | * A Goal represents an open task: something that the player is trying |
| | 70 | * to achieve. A Goal is an abstract object, not part of the simulated |
| | 71 | * world of the game. |
| | 72 | * |
| | 73 | * Each goal is associated with a hint topic (usually shown as a |
| | 74 | * question, such as "How do I get past the guard?") and an ordered list |
| | 75 | * of hints. The hints are usually ordered from most general to most |
| | 76 | * specific. The idea is to let the player control how big a hint they |
| | 77 | * get; we start with a small nudge and work towards giving away the |
| | 78 | * puzzle completely, so the player can stop as soon as they see |
| | 79 | * something that helps. |
| | 80 | * |
| | 81 | * At any given time, a goal can be in one of three states: |
| | 82 | * |
| | 83 | * - Open: this means that the player is (or ought to be) aware of the |
| | 84 | * goal, but the goal hasn't yet been achieved. Determining this |
| | 85 | * awareness is up to the goal. In some cases, a goal is opened as soon |
| | 86 | * as the player has seen a particular object or entered a particular |
| | 87 | * area; in other cases, a goal might be opened by a scripted event, |
| | 88 | * such as a speech by an NPC telling the player they have to accomplish |
| | 89 | * something. A goal could even be opened by viewing a hint for another |
| | 90 | * goal, because that hint could explain a gating goal that the player |
| | 91 | * might not otherwise been able to know about. |
| | 92 | * |
| | 93 | * - Undiscovered: this means that the player doesn't yet have any |
| | 94 | * reason to know about the goal. |
| | 95 | * |
| | 96 | * - Closed: this means that the player has accomplished the goal, or in |
| | 97 | * some cases that the goal has become irrelevant. |
| | 98 | * |
| | 99 | * The hint system only shows goals that are Open. We don't show Closed |
| | 100 | * goals because the player presumably has no need of them any longer; |
| | 101 | * we don't show Undiscovered goals to avoid giving away developments |
| | 102 | * later in the game before they become relevant. |
| | 103 | */ |
| | 104 | enum OpenGoal, ClosedGoal, UndiscoveredGoal; |
| | 105 | class Goal: MenuTopicItem, HintMenuObject |
| | 106 | /* |
| | 107 | * The topic question associated with the goal. The hint system |
| | 108 | * shows a list of the topics for the goals that are currently open, |
| | 109 | * so that the player can decide what area they want help on. |
| | 110 | */ |
| | 111 | title = '' |
| | 112 | |
| | 113 | /* |
| | 114 | * Our parent menu - this is usually a HintMenu object. In very |
| | 115 | * simple hint systems, this could simply be a top-level hint menu |
| | 116 | * container; more typically, the hint system will be structured |
| | 117 | * into a menu tree that organizes the hint topics into several |
| | 118 | * different submenus, for easier navigatino. |
| | 119 | */ |
| | 120 | location = nil |
| | 121 | |
| | 122 | /* |
| | 123 | * The list of hints for this topic. This should be ordered from |
| | 124 | * most general to most specific; we offer the hints in the order |
| | 125 | * they appear in this list, so the earlier hints should give away |
| | 126 | * as little as possible, while the later hints should get |
| | 127 | * progressively closer to just outright giving away the answer. |
| | 128 | * |
| | 129 | * Each entry in the list can be a simple (single-quoted) string, or |
| | 130 | * it can be a Hint object. In most cases, a string will do. A |
| | 131 | * Hint object is only needed when displaying the hint has some side |
| | 132 | * effect, such as opening a new Goal. |
| | 133 | */ |
| | 134 | menuContents = [] |
| | 135 | |
| | 136 | /* |
| | 137 | * An optional object that, when seen by the player character, opens |
| | 138 | * this goal. It's often convenient to declare a goal open as soon |
| | 139 | * as the player enters a particular area or has encountered a |
| | 140 | * particular object. For such cases, simply set this property to |
| | 141 | * the room or object that opens the goal, and we'll automatically |
| | 142 | * mark the goal as Open the next time the player asks for a hint |
| | 143 | * after seeing the referenced object. |
| | 144 | */ |
| | 145 | openWhenSeen = nil |
| | 146 | |
| | 147 | /* |
| | 148 | * An option object that, when seen by the player character, closes |
| | 149 | * this goal. Many goals will be things like "how do I find the |
| | 150 | * X?", in which case it's nice to close the goal when the X is |
| | 151 | * found. |
| | 152 | */ |
| | 153 | closeWhenSeen = nil |
| | 154 | |
| | 155 | /* |
| | 156 | * this is like openWhenSeen, but opens the topic when the given |
| | 157 | * object is described (with EXAMINE) |
| | 158 | */ |
| | 159 | openWhenDescribed = nil |
| | 160 | |
| | 161 | /* close the goal when the given object is described */ |
| | 162 | closeWhenDescribed = nil |
| | 163 | |
| | 164 | /* |
| | 165 | * An optional Achievement object that opens this goal. This goal |
| | 166 | * will be opened automatically once the goal is achieved, if the |
| | 167 | * goal was previously undiscovered. This makes it easy to set up a |
| | 168 | * hint topic that becomes available after a particular puzzle is |
| | 169 | * solved, which is useful when a new puzzle only becomes known to |
| | 170 | * the player after a gating puzzle has been solved. |
| | 171 | */ |
| | 172 | openWhenAchieved = nil |
| | 173 | |
| | 174 | /* |
| | 175 | * An optional Achievement object that closes this goal. Once the |
| | 176 | * achievement is completed, this goal's state will automatically be |
| | 177 | * set to Closed. This makes it easy to associate the goal with a |
| | 178 | * puzzle: once the puzzle is solved, there's no need to show hints |
| | 179 | * for the goal any more. |
| | 180 | */ |
| | 181 | closeWhenAchieved = nil |
| | 182 | |
| | 183 | /* |
| | 184 | * An optional Topic or Thing that opens this goal when the object |
| | 185 | * becomes "known" to the player character. This will open the goal |
| | 186 | * as soon as gPlayerChar.knowsAbout(openWhenKnown) returns true. |
| | 187 | * This makes it easy to open a goal as soon as the player comes |
| | 188 | * across some information in the game. |
| | 189 | */ |
| | 190 | openWhenKnown = nil |
| | 191 | |
| | 192 | /* an optional Topic or Thing that closes this goal when known */ |
| | 193 | closeWhenKnown = nil |
| | 194 | |
| | 195 | /* |
| | 196 | * An optional <.reveal> tag name that opens this goal. If this is |
| | 197 | * set to a non-nil string, we'll automatically open this goal when |
| | 198 | * the tag has been revealed via <.reveal> (or gReveal()). |
| | 199 | */ |
| | 200 | openWhenRevealed = nil |
| | 201 | |
| | 202 | /* an optional <.reveal> tag that closes this goal when revealed */ |
| | 203 | closeWhenRevealed = nil |
| | 204 | |
| | 205 | /* |
| | 206 | * An optional arbitrary check that opens the goal. If this returns |
| | 207 | * true, we'll open the goal. This check is made in addition to the |
| | 208 | * other checks (openWhenSeen, openWhenDescribed, etc). This can be |
| | 209 | * used for any custom check that doesn't fit into one of the |
| | 210 | * standard openWhenXxx properties. |
| | 211 | */ |
| | 212 | openWhenTrue = nil |
| | 213 | |
| | 214 | /* an optional general-purpose check that closes the goal */ |
| | 215 | closeWhenTrue = nil |
| | 216 | |
| | 217 | /* |
| | 218 | * Determine if there's any condition that should open this goal. |
| | 219 | * This checks openWhenSeen, openWhenDescribed, and all of the other |
| | 220 | * openWhenXxx conditions; if any of these return true, then we'll |
| | 221 | * return true. |
| | 222 | * |
| | 223 | * Note that this should generally NOT be overridden in individual |
| | 224 | * instances; normally, instances would define openWhenTrue instead. |
| | 225 | * However, some games might find that they use the same special |
| | 226 | * condition over and over in many goals, often enough to warrant |
| | 227 | * adding a new openWhenXxx property to Goal. In these cases, you |
| | 228 | * can use 'modify Goal' to override openWhen to add the new |
| | 229 | * condition: simply define openWhen as (inherited || newCondition), |
| | 230 | * where 'newCondition' is the new special condition you want to |
| | 231 | * add. |
| | 232 | */ |
| | 233 | openWhen = ( |
| | 234 | (openWhenSeen != nil && gPlayerChar.hasSeen(openWhenSeen)) |
| | 235 | || (openWhenDescribed != nil && openWhenDescribed.described) |
| | 236 | || (openWhenAchieved != nil && openWhenAchieved.scoreCount != 0) |
| | 237 | || (openWhenKnown != nil && gPlayerChar.knowsAbout(openWhenKnown)) |
| | 238 | || (openWhenRevealed != nil && gRevealed(openWhenRevealed)) |
| | 239 | || openWhenTrue) |
| | 240 | |
| | 241 | /* |
| | 242 | * Determine if there's any condition that should close this goal. |
| | 243 | * We'll check closeWhenSeen, closeWhenDescribed, and all of the |
| | 244 | * other closeWhenXxx conditions; if any of these return true, then |
| | 245 | * we'll return true. |
| | 246 | */ |
| | 247 | closeWhen = ( |
| | 248 | (closeWhenSeen != nil && gPlayerChar.hasSeen(closeWhenSeen)) |
| | 249 | || (closeWhenDescribed != nil && closeWhenDescribed.described) |
| | 250 | || (closeWhenAchieved != nil && closeWhenAchieved.scoreCount != 0) |
| | 251 | || (closeWhenKnown != nil && gPlayerChar.knowsAbout(closeWhenKnown)) |
| | 252 | || (closeWhenRevealed != nil && gRevealed(closeWhenRevealed)) |
| | 253 | || closeWhenTrue) |
| | 254 | |
| | 255 | /* |
| | 256 | * Has this goal been fully displayed? The hint system automatically |
| | 257 | * sets this to true when the last item in our hint list is |
| | 258 | * displayed. |
| | 259 | * |
| | 260 | * You can use this, for example, to automatically remove the hint |
| | 261 | * from the hint menu after it's been fully displayed. (You might |
| | 262 | * want to do this with a hint for a red herring, for example. After |
| | 263 | * the player has learned that the red herring is a red herring, they |
| | 264 | * probably won't need to see that particular line of hints again, so |
| | 265 | * you can remove the clutter in the menu by closing the hint after |
| | 266 | * it's been fully displayed.) To do this, simply add this to the |
| | 267 | * Goal object: |
| | 268 | * |
| | 269 | *. closeWhenTrue = (goalFullyDisplayed) |
| | 270 | */ |
| | 271 | goalFullyDisplayed = nil |
| | 272 | |
| | 273 | /* |
| | 274 | * Check our menu state and update it if necessary. Each time our |
| | 275 | * parent menu is about to display, it'll call this on its sub-items |
| | 276 | * to let them update their current states. This method can promote |
| | 277 | * the state to Open or Closed if the necessary conditions for the |
| | 278 | * goal have been met. |
| | 279 | * |
| | 280 | * Sometimes it's more convenient to set a goal's state explicitly |
| | 281 | * from a scripted event; for example, if the goal is associated |
| | 282 | * with a scored achievement, awarding the goal's achievement will |
| | 283 | * set the goal's state to Closed. In these cases, there's no need |
| | 284 | * to use this method, since you're managing the goal's state |
| | 285 | * explicitly. The purpose of this method is to make it easy to |
| | 286 | * catch goal state changes that can be reached by several different |
| | 287 | * routes; in these cases, you can just write a single test for |
| | 288 | * those conditions in this method rather than trying to catch every |
| | 289 | * possible route to the new conditions and writing code in all of |
| | 290 | * those. |
| | 291 | * |
| | 292 | * The default implementation looks at our openWhenSeen property. |
| | 293 | * If this property is not nil, then we'll check the object |
| | 294 | * referenced in this property; if our current state is |
| | 295 | * Undiscovered, and the object referenced by openWhenSeen has been |
| | 296 | * seen by the player character, then we'll change our state to |
| | 297 | * Open. We'll make the corresponding check for openWhenDescribed. |
| | 298 | */ |
| | 299 | updateContents() |
| | 300 | { |
| | 301 | /* |
| | 302 | * If we're currently Undiscovered, and our openWhenSeen object |
| | 303 | * has been seen by the player charater, change our state to |
| | 304 | * Open. Likewise, if our gating achievement has been scored, |
| | 305 | * open the goal. |
| | 306 | */ |
| | 307 | if (goalState == UndiscoveredGoal && openWhen) |
| | 308 | { |
| | 309 | /* |
| | 310 | * the player has encountered our gating object, so open |
| | 311 | * this goal |
| | 312 | */ |
| | 313 | goalState = OpenGoal; |
| | 314 | } |
| | 315 | |
| | 316 | /* |
| | 317 | * if we're currently Undiscovered or Open, and our Achievement |
| | 318 | * has been scored, then change our state to Closed - once the |
| | 319 | * goal has been achieved, there's no need to offer hints on the |
| | 320 | * topic any longer |
| | 321 | */ |
| | 322 | if (goalState is in (UndiscoveredGoal, OpenGoal) && closeWhen) |
| | 323 | { |
| | 324 | /* the goal has been achieved, so close it */ |
| | 325 | goalState = ClosedGoal; |
| | 326 | } |
| | 327 | } |
| | 328 | |
| | 329 | /* display a sub-item, keeping track of when we've shown them all */ |
| | 330 | displaySubItem(idx, lastBeforeInput, eol) |
| | 331 | { |
| | 332 | /* do the inherited work */ |
| | 333 | inherited(idx, lastBeforeInput, eol); |
| | 334 | |
| | 335 | /* if we just displayed the last item, note it */ |
| | 336 | if (idx == menuContents.length()) |
| | 337 | goalFullyDisplayed = true; |
| | 338 | } |
| | 339 | |
| | 340 | /* we're active in our parent menu if our goal state is Open */ |
| | 341 | isActiveInMenu = (goalState == OpenGoal) |
| | 342 | |
| | 343 | /* |
| | 344 | * This goal's current state. We'll start off undiscovered. When a |
| | 345 | * goal should be open from the very start of the game, this should |
| | 346 | * be overridden and set to OpenGoal. |
| | 347 | */ |
| | 348 | goalState = UndiscoveredGoal |
| | 349 | ; |
| | 350 | |
| | 351 | /* |
| | 352 | * A Hint encapsulates one hint from a topic. In many cases, hints can |
| | 353 | * be listed in a topic simply as strings, rather than using Hint |
| | 354 | * objects. Hint objects provide a little more control, though; in |
| | 355 | * particular, a Hint object can specify some additional code to run |
| | 356 | * when the hint is shown, so that it can apply any side effects of |
| | 357 | * showing the hint (for example, when a hint is shown, it could mark |
| | 358 | * another Goal object as Open, which might be desirable if the hint |
| | 359 | * refers to another topic that the player might not yet have |
| | 360 | * encountered). |
| | 361 | */ |
| | 362 | class Hint: MenuTopicSubItem |
| | 363 | /* the hint text */ |
| | 364 | hintText = '' |
| | 365 | |
| | 366 | /* |
| | 367 | * A list of other Goal objects that this hint references. By |
| | 368 | * default, when we show this hint for the first time, we'll promote |
| | 369 | * each goal in this list from Undiscovered to Open. |
| | 370 | * |
| | 371 | * Sometimes, it's necessary to solve one puzzle before another can |
| | 372 | * be solved. In these cases, some hints for the first puzzle |
| | 373 | * (which depends on the second), especially the later, more |
| | 374 | * specific hints, might need to refer to the other puzzle. This |
| | 375 | * would make the player aware of the other puzzle even if they |
| | 376 | * weren't already. In such cases, it's a good idea to make sure |
| | 377 | * that we make hints for the other puzzle available immediately, |
| | 378 | * since otherwise the player might be confused by the absence of |
| | 379 | * hints about it. |
| | 380 | */ |
| | 381 | referencedGoals = [] |
| | 382 | |
| | 383 | /* |
| | 384 | * Get my hint text. By default, we mark as Open any goals listed |
| | 385 | * in our referencedGoals list, then return our hintText string. |
| | 386 | * Individual Hint objects can override this as desired to apply any |
| | 387 | * additional side effects. |
| | 388 | */ |
| | 389 | getItemText() |
| | 390 | { |
| | 391 | /* scan the referenced goals list */ |
| | 392 | foreach (local cur in referencedGoals) |
| | 393 | { |
| | 394 | /* if this goal is not yet discovered, open it */ |
| | 395 | if (cur.goalState == UndiscoveredGoal) |
| | 396 | cur.goalState = OpenGoal; |
| | 397 | } |
| | 398 | |
| | 399 | /* return our hint text */ |
| | 400 | return hintText; |
| | 401 | } |
| | 402 | ; |
| | 403 | |
| | 404 | /* |
| | 405 | * A hint menu. This same class can be used for the top-level hints |
| | 406 | * menu and for sub-menus within the hints menu. |
| | 407 | * |
| | 408 | * The typical hint menu system will be structured into a top-level hint |
| | 409 | * menu that contains a set of sub-menus for the main areas of the game; |
| | 410 | * each sub-menu will have a series of Goal items, each Goal providing a |
| | 411 | * set of answers to a particular question. Something like this: |
| | 412 | * |
| | 413 | * topHintMenu: TopHintMenu 'Hints'; |
| | 414 | *. + HintMenu 'General Questions'; |
| | 415 | *. ++ Goal 'What am I supposed to be doing?' [answer, answer, answer]; |
| | 416 | *. ++ Goal 'Amusing things to try' [thing, thing, thing]; |
| | 417 | *. + HintMenu 'First Area'; |
| | 418 | *. ++ Goal 'How do I get past the shark?' [answer, answer, answer]; |
| | 419 | *. ++ Goal 'How do I open the fish tank?' [answer, answer, answer]; |
| | 420 | *. + HintMenu 'Second Area'; |
| | 421 | *. ++ Goal 'Where is the gold key?' [answer, answer, answer]; |
| | 422 | *. ++ Goal 'How do I unlock the gold door?' [answer, answer, answer]; |
| | 423 | * |
| | 424 | * Note that there's no requirement that the hint menu tree takes |
| | 425 | * exactly this shape. A very small game could dispense with the |
| | 426 | * submenus and simply put all of the goals directly in the top hint |
| | 427 | * menu. A very large game with lots of goals could add more levels of |
| | 428 | * sub-menus to make it easier to navigate the large number of topics. |
| | 429 | */ |
| | 430 | class HintMenu: MenuItem, HintMenuObject |
| | 431 | /* the menu's title */ |
| | 432 | title = '' |
| | 433 | |
| | 434 | /* update our contents */ |
| | 435 | updateContents() |
| | 436 | { |
| | 437 | local vec = new Vector(16); |
| | 438 | |
| | 439 | /* |
| | 440 | * First, run through all of our sub-items, and update their |
| | 441 | * contents. We only want to show our active contents, so we |
| | 442 | * need to check with each item to find out which is active. |
| | 443 | */ |
| | 444 | foreach (local cur in allContents) |
| | 445 | cur.updateContents(); |
| | 446 | |
| | 447 | /* create a vector containing all of our active items */ |
| | 448 | foreach (local cur in allContents) |
| | 449 | { |
| | 450 | /* if this item is active, add it to the active vector */ |
| | 451 | if (cur.isActiveInMenu) |
| | 452 | vec.append(cur); |
| | 453 | } |
| | 454 | |
| | 455 | /* set our contents list to the list of active items */ |
| | 456 | contents = vec; |
| | 457 | } |
| | 458 | |
| | 459 | /* we're active in a menu if we have any active contents */ |
| | 460 | isActiveInMenu = (contents.length() != 0) |
| | 461 | |
| | 462 | /* add a sub-item to our contents */ |
| | 463 | addToContents(obj) |
| | 464 | { |
| | 465 | /* |
| | 466 | * add the sub-item to our allContents list rather than our |
| | 467 | * active contents |
| | 468 | */ |
| | 469 | allContents += obj; |
| | 470 | } |
| | 471 | |
| | 472 | /* initialize our contents list */ |
| | 473 | initializeContents() |
| | 474 | { |
| | 475 | /* sort our allContents list in the object-defined sorting order */ |
| | 476 | allContents = allContents.sort( |
| | 477 | SortAsc, {a, b: a.compareForTopicSort(b)}); |
| | 478 | } |
| | 479 | |
| | 480 | /* |
| | 481 | * our list of all of our sub-items (some of which may not be |
| | 482 | * active, in which case they'll appear in this list but not in our |
| | 483 | * 'contents' list, which contains only active contents) |
| | 484 | */ |
| | 485 | allContents = [] |
| | 486 | ; |
| | 487 | |
| | 488 | /* |
| | 489 | * A hint menu version of the long topic menu. |
| | 490 | */ |
| | 491 | class HintLongTopicItem: MenuLongTopicItem, HintMenuObject |
| | 492 | /* |
| | 493 | * presume these are always active - they're usually used for things |
| | 494 | * like hint system instructions that should always be available |
| | 495 | */ |
| | 496 | isActiveInMenu = true |
| | 497 | ; |
| | 498 | |
| | 499 | /* |
| | 500 | * Top-level hint menu. As a convenience, an object defined of this |
| | 501 | * class will automatically register itself as the top-level hint menu |
| | 502 | * during pre-initialization. |
| | 503 | */ |
| | 504 | class TopHintMenu: HintMenu, PreinitObject |
| | 505 | /* register as the top-level hint menu during pre-initialization */ |
| | 506 | execute() { hintManager.topHintMenuObj = self; } |
| | 507 | ; |
| | 508 | |
| | 509 | /* ------------------------------------------------------------------------ */ |
| | 510 | /* |
| | 511 | * The default hint system user interface implementation. All of the |
| | 512 | * hint-related verbs operate by calling methods in the object stored in |
| | 513 | * the global variable gHintSystem, which we'll by default initialize |
| | 514 | * with a reference to this object. Games can replace this with their |
| | 515 | * own implementations if desired. |
| | 516 | */ |
| | 517 | hintManager: PreinitObject |
| | 518 | /* during pre-initialization, register as the global hint manager */ |
| | 519 | execute() { gHintManager = self; } |
| | 520 | |
| | 521 | /* |
| | 522 | * Disable hints - this is invoked by the HINTS OFF action. |
| | 523 | * |
| | 524 | * Some users don't like on-line hint systems because they find them |
| | 525 | * to be too much of a temptation. To address this concern, we |
| | 526 | * provide this HINTS OFF command. Players who want to ensure that |
| | 527 | * their will-power won't crumble later on in the face of a |
| | 528 | * difficult puzzle can type HINTS OFF early on, before the going |
| | 529 | * gets rough; this will disable hints for the rest of the session. |
| | 530 | * It's kind of like giving your credit card to a friend before |
| | 531 | * going to the mall, making the friend promise that they won't let |
| | 532 | * you spend more than such and such an amount, no matter how much |
| | 533 | * you beg and plead. |
| | 534 | */ |
| | 535 | disableHints() |
| | 536 | { |
| | 537 | /* |
| | 538 | * Remember that hints have been disabled. Keep this |
| | 539 | * information in the transient session object, since we want |
| | 540 | * the disabled status to last for the rest of this session, |
| | 541 | * even if we restore or restart later. |
| | 542 | */ |
| | 543 | sessionHintStatus.hintsDisabled = true; |
| | 544 | |
| | 545 | /* acknowledge it */ |
| | 546 | mainReport(gLibMessages.hintsDisabled); |
| | 547 | } |
| | 548 | |
| | 549 | /* |
| | 550 | * The top-level hint menu. This must be provided by the game, and |
| | 551 | * should be set during initialization. If this is nil, hints won't |
| | 552 | * be available. |
| | 553 | * |
| | 554 | * We don't provide a default top-level hint menu because we want to |
| | 555 | * give the game maximum flexibility in defining this object exactly |
| | 556 | * as it wants. For convenience, an object of class TopHintMenu |
| | 557 | * will automatically register itself during pre-initialization - |
| | 558 | * but note that there should be only one such object in the entire |
| | 559 | * game, since if there are more than one, only one will be |
| | 560 | * arbitrarily chosen as the registered object. |
| | 561 | */ |
| | 562 | topHintMenuObj = nil |
| | 563 | |
| | 564 | /* |
| | 565 | * Show hints - invoke the hint system. |
| | 566 | */ |
| | 567 | showHints() |
| | 568 | { |
| | 569 | /* if there is no top-level hint menu, no hints are available */ |
| | 570 | if (topHintMenuObj == nil) |
| | 571 | { |
| | 572 | mainReport(gLibMessages.hintsNotPresent); |
| | 573 | return; |
| | 574 | } |
| | 575 | |
| | 576 | /* if hints are disabled, reject the request */ |
| | 577 | if (sessionHintStatus.hintsDisabled) |
| | 578 | { |
| | 579 | mainReport(gLibMessages.sorryHintsDisabled); |
| | 580 | return; |
| | 581 | } |
| | 582 | |
| | 583 | /* bring the hint menu tree up to date */ |
| | 584 | topHintMenuObj.updateContents(); |
| | 585 | |
| | 586 | /* if there are no hints available, say so and give up */ |
| | 587 | if (topHintMenuObj.contents.length() == 0) |
| | 588 | { |
| | 589 | mainReport(gLibMessages.currentlyNoHints); |
| | 590 | return; |
| | 591 | } |
| | 592 | |
| | 593 | /* if we haven't warned about hints, do so now */ |
| | 594 | if (!showHintWarning()) |
| | 595 | return; |
| | 596 | |
| | 597 | /* display the hint menu */ |
| | 598 | topHintMenuObj.display(); |
| | 599 | |
| | 600 | /* all done */ |
| | 601 | mainReport(gLibMessages.hintsDone); |
| | 602 | } |
| | 603 | |
| | 604 | /* |
| | 605 | * Show a warning before showing any hints. By default, we'll show |
| | 606 | * this at most once per session or once per saved game. Returns |
| | 607 | * true if we are to proceed to the hints, nil if not. |
| | 608 | */ |
| | 609 | showHintWarning() |
| | 610 | { |
| | 611 | /* |
| | 612 | * If we have previously warned in this session, or if we've |
| | 613 | * warned in a previous session and the same game was later |
| | 614 | * saved and restored, don't warn again. The transient session |
| | 615 | * object tells us if we've asked in this session; the normal |
| | 616 | * persistent object tells us if we've asked in a previous |
| | 617 | * session that we've since saved and restored. |
| | 618 | */ |
| | 619 | if (!sessionHintStatus.hintWarning && !gameHintStatus.hintWarning) |
| | 620 | { |
| | 621 | /* |
| | 622 | * we haven't asked yet in either the session or the game, |
| | 623 | * so show the warning now |
| | 624 | */ |
| | 625 | gLibMessages.showHintWarning(); |
| | 626 | |
| | 627 | /* note that we've shown the warning */ |
| | 628 | sessionHintStatus.hintWarning = true; |
| | 629 | gameHintStatus.hintWarning = true; |
| | 630 | |
| | 631 | /* don't proceed to hints now; let them ask again */ |
| | 632 | return nil; |
| | 633 | } |
| | 634 | |
| | 635 | /* |
| | 636 | * They've already seen the warning before. It's possible that |
| | 637 | * they've seen it in a past session with the game and not |
| | 638 | * otherwise during this session, but now that we're accessing |
| | 639 | * the hint system once, don't bother with another warning for |
| | 640 | * the rest of this session. |
| | 641 | */ |
| | 642 | sessionHintStatus.hintWarning = true; |
| | 643 | |
| | 644 | /* proceed to the hints */ |
| | 645 | return true; |
| | 646 | } |
| | 647 | ; |
| | 648 | |
| | 649 | /* |
| | 650 | * We keep several pieces of information about the status of the hint |
| | 651 | * system. Some of it pertains to the current session, independently of |
| | 652 | * any saving/restoring/restarting, so we keep this information in a |
| | 653 | * transient object. Some pertains to the present game, so we keep it |
| | 654 | * in an ordinary persistent object, so that it's saved and restored |
| | 655 | * along with the game. |
| | 656 | */ |
| | 657 | transient sessionHintStatus: object |
| | 658 | /* flag: we've warned about the hint system in this session */ |
| | 659 | hintWarning = nil |
| | 660 | |
| | 661 | /* flag: we've disabled hints for this session */ |
| | 662 | hintsDisabled = nil |
| | 663 | ; |
| | 664 | |
| | 665 | gameHintStatus: object |
| | 666 | /* flag: we've warned about the hint system in this session */ |
| | 667 | hintWarning = nil |
| | 668 | ; |
| | 669 | |