GUI Scripting: In-World Menu Examples

From The DarkMod Wiki
Jump to navigationJump to search

This is part of a series, GUI Scripting Language

Introduction

It is possible to offer the player a choice of some option using a menu on in-world surface, i.e., a menu that is not full-screen. Here, we look at a wall-plaque choice picker.

The behavior for this choice picker will be modeled on the standard "choiceDef", used widely in TDM's Settings main menu. However, the behavior has to be somewhat modified, due to limitations with a gui-on-surface, discussed more fully in GUI Scripting: Interactions. In particular:

  • No cursor is shown
  • Frobbing (typically RMB) is used for choice selection instead of Action (typically LMB).
  • Frob highlighting takes the place of mouse-over text glowing.
  • As with a choiceDef, cycling through available choices is done by repeated clicking. However, choiceDef offers additional selection methods, involving keystrokes, beyond what can be done here.

Three alternative implementations are shown, called "Firepit", "Spiders", and "Fumes".

Setting Up the Entity

All three examples will use the same entity, a stepped wall plaque composed from 2 rectangular brushes, combined into a func_static. See the "Adding Readables to your Map - from Scratch, Made from Brushes" section of Readables for how to do this, including texturing. For the specific entity here, use these dimensions and attributes:

  • Inner and frontmost rectangle is 40w x 6h, with "Nodraw Solid" texture, except blue "Entity GUI" applied and fitted to front surface. This surface will show the menu text. We'll use black text, but any color could be specified. (You can substitute a patch if you prefer.)
  • Outer and rearmost rectangle is 42w x 8h. Its front surface will provide the background for the control. To provide contrast with the black menu text, a light color texture works (here, textures/darkmod/fabric/shadowable/cloth_plain_creased_white_dull ). This rectangle is needed for frob highlighting to work well.

Once you've combined them into an entity, add these spawnargs:

frob_action_script  change_choice
frobable 1
gui  guis/testchooser.gui
gui_parm1 0

The "Firepit", "Spiders", and "Fumes" implementations differ in their .script and testchooser.gui contents.

Firepit – Emulating a ChoiceDef with a WindowDef

The active surface will be subdivided into left and right windowDef cells of equal size. The left will get the "Firepit" prompt, left-justified. The right will be where the various choices cycle, right-justified: "Simmer", "Bake", "Broil", and "Cremate". Those words will be provided by the global script function "change_choice", called on each frob, and passed to the testchooser.gui by GUI::Parameter choice_text. The GUI takes care of initializing this, in an onTime handler, while the script initializes the corresponding index.

The "change_choice" .script function

This is contained in any convenient .script file:

float picked = 0;  // global init.

void change_choice(entity me)
{
   //sys.println ("change_choice() called");
   picked++;
   if(picked > 3)
     picked = 0;
   // DO HERE: Change game world to match
   string s;
   if(picked == 0)
     s = "Simmer";
   else if (picked == 1)
     s = "Bake";
   else if (picked == 2)
     s = "Broil";
   else
     s = "Cremate";
   me.setGuiString(1, "choice_text", s);

};

The GUI

Our guis/testchooser.gui is:

#define ROW_START_Y 12
#define CONTENTS_WIDTH 640
#define CONTENTS_HEIGHT 96
#define MY_SCALE forceaspectwidth CONTENTS_WIDTH  forceaspectheight CONTENTS_HEIGHT
// Same ratio as blue Entity GUI surface of 40w x 6h

// Background is rendered on separate surface
windowDef Desktop
{
 rect       0, 0, 640, 480
 backcolor  0, 0, 0, 0
 nocursor   1
 MY_SCALE

 windowDef Prompt {
     rect         0,ROW_START_Y,320,480
     textscale    0.9
     textalign    0 // left justified
     forecolor    0, 0, 0, .85
     font         "fonts/carleton_condensed"
     text         "Firepit"
     visible      1
 }

 windowDef Which {
     rect         320,ROW_START_Y,320,480
     textscale    0.9
     textalign    2 // right justified
     textalignx   -10
     forecolor    0, 0, 0, .85
     font         "fonts/carleton_condensed"
     text         "gui::choice_text"
     visible      1
 }

 onTime 0 {
     set "gui::choice_text" "Simmer";  // same as picked == 0 in script
 }
}
...

Spiders – Driving a ChoiceDef from a Script

In this version, instead of emulating a choiceDef, we actually use it, although driven indirectly through the script function. That function, instead of passing the string to the GUI, passes the numeric index that a choiceDef knows about. Since the "picked_index" is initialized in the GUI, it's unnecessary to do so in the SCRIPT file. See also GUI Scripting: ChoiceDef.

For Spiders, the example has a different set of choices than Firepit, repurposing an i18n string that's already provided in the standard distribution for AIVision settings: "Nearly Blind;Forgiving;Challenging;Hardcore".

The "change_choice" .script function

void change_choice(entity me)
{
   //sys.println ("change_choice() called");
   float i = me.getGuiInt(1, "picked_index");
   i++;
   if(i > 3)
     i = 0;
   me.setGuiInt(1, "picked_index", i);
};

The GUI

In this version of testchooser.gui, the initial #defines and the Desktop and Prompt windowDefs are the same (except for Prompt, the "text" reads "Spiders" instead of "Firepit"). Here's what's changed:

...
 choiceDef Which {
     rect   320,ROW_START_Y,320,480
     MY_SCALE
     choices      "#str_07323"  // Nearly Blind;Forgiving;Challenging;Hardcore
     values       "0;1;2;3"
     textscale    0.9
     textalign    2 // right justified
     textalignx   -10
     forecolor    0, 0, 0, .85
     font         "fonts/carleton_condensed"
     gui          "picked_index" // this is driven from script, responding to frob
     choiceType   0
     visible      1
 }

 onTime 0 {
   set "gui::picked_index" 0;
 }
...

In the choiceDef, MY_SCALE had to be added to prevent font smear. (Evidently, it differs from windowDef children, where the "forceaspect…" properties are expressed in a parent.)

Fumes – Adding Choice Transitions

To allow the choice text to change with a fade in/fade out, start with the Firepit GUI example, and clone the righthand windowDef, creating a separate one for each of the 4 possible hardcoded text strings. They'll all be formally visible, but the alpha of forecolor is 0 (so hidden) unless transitioned to 0.85. On the script side, a picked_index is passed to the GUI, as in the Spiders example, but with an event added to drive the transitions.

The "change_choice" .script function

The Spiders script function is just like Firepit's but with this line at the end:

me.callGui(1,"UpdateChoice");

The GUI

In this version of testchooser.gui, more #defines are added, so that the transitions and cloned windowDefs can be succinct and uniform:

#define NO_TEXT 0,0,0,0
#define SNO_TEXT "0 0 0 0"
#define SNORMAL_TEXT "0 0 0 0.85"
#define FADE_TIME 200 // milliseconds
#define WHICH_BASE  rect 320,ROW_START_Y,320,480  textscale 0.9  textalign 2  textalignx -10  font "fonts/carleton_condensed"  visible 1  forecolor NO_TEXT

The Desktop and Prompt windowDefs are the same (except Prompt's "text" is "Fumes"). Then the latter part of the .gui becomes:

 windowDef Which0 {
     WHICH_BASE
     text        "Whiff"
 }

 windowDef Which1 {
     WHICH_BASE
     text        "Haze"
 }

 windowDef Which2 {
     WHICH_BASE
     text        "Stinging"
 }

 windowDef Which3 {
     WHICH_BASE
     text        "Fatal"
 }

 onNamedEvent UpdateChoice {
     if("gui::picked_index" == 0) {
         transition "Which3::forecolor"  SNORMAL_TEXT  SNO_TEXT  FADE_TIME;
         transition "Which0::forecolor"  SNO_TEXT  SNORMAL_TEXT  FADE_TIME;
     }
     else if ("gui::picked_index" == 1) {
         transition "Which0::forecolor"  SNORMAL_TEXT  SNO_TEXT  FADE_TIME;
         transition "Which1::forecolor"  SNO_TEXT  SNORMAL_TEXT  FADE_TIME;
     }
     else if ("gui::picked_index" == 2) {
         transition "Which1::forecolor"  SNORMAL_TEXT  SNO_TEXT  FADE_TIME;
         transition "Which2::forecolor"  SNO_TEXT  SNORMAL_TEXT  FADE_TIME;
     }
     else { // "gui::picked_index" == 3
         transition "Which2::forecolor"  SNORMAL_TEXT  SNO_TEXT  FADE_TIME;
         transition "Which3::forecolor"  SNO_TEXT  SNORMAL_TEXT  FADE_TIME;
     }
 }

 onTime 0 {
   set "gui::picked_index" 0;
   set "Which0::forecolor" SNORMAL_TEXT ;
 }
...

Other Ideas

Add Magic

You could enhance the script to trigger a puff of smoke or some sparks in front of the choice when it changes.

Multirow Menu - Separate Items

If you wanted to have a multirow menu, where each row provided choices for a different option (like the Settings menus), you could build it from separately-frobbable 1-row entities as above, stacked like wall tiles. Names of things would have to change to differentiate them, and frob-related attributes tweaked to avoid spatial conflicts; see Frobbing.

Multirow Menus - Single Item

With just one entity, it might be possible to emulate or repurpose a listDef, but where each frob click changed the row, and the selected row was indicated by an icon or color swatch.

Bomb Timer

Frob on the entity, and it starts showing seconds being counted down. Further frobbing does nothing. This involves an endless loop in the script file, something a script object is ideal for.

Price of Gold

Imagine that the price of gold nuggets varied during the game, either randomly or in response to what has happened so far. You could have a display show it, using a polling script object to invent the current value. This display might not require any frobbing. (A non-frobbable entity wouldn't require a stepped entity... a nodraw rectangle with an "GUI Entity" would be enough.)

Cycling Images Instead of Text

Each frob could cycle through background images instead of foreground text. If you needed to scale them, likely matscalex and matscaley will help.