View Single Post
Old August 3, 2012, 06:31   #13
Derakon
Prophet
 
Derakon's Avatar
 
Join Date: Dec 2009
Posts: 8,019
Derakon is on a distinguished road
Okay, time to talk out some more design logic. I decided I wanted to implement the 'l'ook command, and this brought me to the realization that my current "prompt the user for a selection" system is badly-designed. The way this currently works is like follows:

1) Game is waiting for the next command.
2) User presses a key.
3) This is propagated to all Listeners, which then map it to some code to execute, in a gigantic block of if/else code. Some of these bits may return Prompts. Prompts are container classes (almost no associated code) that include a prompt type (e.g. yes/no, list items, select direction), a possible Container of Things associated with the Prompt, a message, and a few other doodads.
4) Returned Prompts are handed to the code that processes input and the code that handles game display.
5) All code dealing with processing and displaying Prompts (and layering, in the event that conclusion of one Prompt requires another Prompt) is external to the Prompts.

Displaying actually is reasonably sensible -- different display layers may want to handle displaying a selection of items differently, for example. The reason I separated out the input handling logic as well was that I was thinking about alternate input modes. For example, an item list might receive a selection via the mouse -- how would the Prompt know how to handle that if it doesn't know how it's being displayed? But I think the right answer here is that Prompts can handle keyboard input on their own just fine -- and I'll deal with mouse input later. More specifically, eventually Pyrel will need a mouse handler, and that handler will of course need to know about what is currently displayed. Mouse events will have to be translated by the handler into conceptual actions, like "select item from container" and "set target to this tile", which will need appropriate functions written for them -- but they're ultimately just more kinds of input, they just have to be handled slightly differently.

In practice, what we want to be doing here is have Prompts occupy a conceptual "layer" that sits on top of the game world. As long as a Prompt is active, it receives all user input (instead of that input going to any Listeners); meanwhile, Prompts are assumed to be drawn on top of everything else, though that can be left up to the display code. So. Here's my proposed new design:

1) Make the Prompt class heirarchical -- each Prompt contains beneath it the Prompt that its answer will assist. For example, if you want to use an item, you get a SelectItemPrompt. You select a spellbook -> the SelectItemPrompt creates another SelectItemPrompt beneath it to let you choose which spell. You select a spell -> the second Prompt creates a SelectTargetPrompt to decide where the spell goes. At any point we can unwind the stack to back out from one of these Prompts.

2) Add a "curPrompt" field to the GameMap. Currently Prompts are stored in the actual GUI window instance (that is, a wxWidgets Frame class), which is frankly pretty stupid and makes it unnecessarily difficult for the inevitable switch to Qt once someone who feels strongly about it gets interested in the project. From the GameMap, they're accessible to all input-handling and display-handling code, so input can be redirected as needed and display can show them on top of the normal stuff.

3) Modify GameMap.onCommand to check for a valid Prompt; if there is one, it gets input instead of the Listeners.

4) When a new Prompt is created, the GameMap is told to set it as the current Prompt. Similarly, when a Prompt is unwound (i.e. cancelled), it sets its parent as the current Prompt -- this may be None if it was the top-level Prompt, in which case input reverts to going to the Listeners.

5) Shunt all the current Prompt-handling code from gui.mainFrame to various subclasses of Prompt, replacing the current "prompt type" field in the base Prompt class. Looks like we'll need YesNoPrompt, ItemListPrompt (select item from container), ItemMapPrompt (used for displaying equipment, where each item gets a special label), FormattedMessagePrompt (for stuff like monster memory or detailed item info), DirectionPrompt (tunneling, disarming, etc.), and TargetPrompt (looking, targeting) to cover the ones that currently exist.

6) Since I'll be vaguely in the area anyway, replace the giant if/else statement in Listener that maps commands to results of commands with a dictionary that maps commands to functions that handle those commands. Functionally similar but more elegant and probably slightly more efficient.
Derakon is offline   Reply With Quote