The UI (side panes) - ThePix/QuestJS GitHub Wiki

You can customise the UI in two places. The first is the CSS file in your game folder (the name of which you set with settings.styleFile); that controls thee colours, the fonts and stuff like that, and is discussed here. The second is the "settings.js" file, which controls whether the various elements are displayed and the actual text that will appear there (and other things unrelated to the interface). Other options in the "settings.js" file are discussed here.

Note that the default HELP command will modify its text based on these settings, so it will be automatically customised for your game.

For options for the main pane, see here.

For how to control the display verbs see here.

Options

settings.panes = 'left'
settings.compassPane = true
settings.symbolsForCompass = true
settings.panesCollapseAt = 700
settings.mapAndImageCollapseAt = 1200

The panes value can be set to "left", "right" or "none". If set to anything else, the UI will not get drawn properly (and an error message will appear in the console telling you). Setting the value to "none" stops them getting drawn at all, so will be faster (though doubtful anyone will notice).

The compassPane value turns the compass on or off. Set symbolsForCompass to true to have arrows, etc. instead of characters in the compass rose.

The side pane will disappear when the screen width is less than settings.panesCollapseAt; this is for mobile users. However, you will want to set this to zero if you have no text input to ensure the side pane is always there. The map and/or image windows will disappear when the screen width is less than settings.mapAndImageCollapseAt.

The inventory pane

The "inventoryPane" value is an array of dictionaries. Set to an empty array to turn off inventories. Here is a basic example.

settings.inventoryPane = [
  {name:'Items Held', alt:'itemsHeld', test:settings.isHeldNotWorn, getLoc:function() { return player.name; } },
  {name:'Items Worn', alt:'itemsWorn', test:settings.isWorn, getLoc:function() { return player.name; } },
  {name:'Items Here', alt:'itemsHere', test:settings.isHere, getLoc:function() { return player.loc; } },
]

The "name" attribute is the title for the inventory, what the users sees. The "alt" attribute is the name of the HTML division; rarely going to be significant, as long as each one is unique. The "test" attribute is a function that will be passed the object - if the function returns true the object will be listed (unless it has "inventorySkip" set to true).

The "getLoc" function is used to determine the verbs to display; the "getVerbs" function of the object will be called, and passed whatever this function returns. Looking at the first entry, if an item is held, it will get included on the list. The item's "getVerbs" function will get called, and passed the player's name; it will then return the verbs applicable for when the item is held by the player (usually getVerbs can work that out for itself - an item will know it is held by the player - but some are odd).

The next example is more involved. This is for a Star Trek style game. The player can hold stuff, but everything else is done through crew members on the bridge. There is also an inventory for talking to someone on the main screen. An "onView" function is created first (and it must be done first).

settings.onView = function(item) { return w.ship.onView === item.name }
settings.inventoryPane = [
  {name:'Items Held', alt:'itemsHeld', test:settings.isHeldNotWorn, getLoc:function() { return player.name; } },
  {name:'People Here', alt:'itemsHere', test:settings.isHere, getLoc:function() { return player.loc; } },
  {name:'On Viewscreen', alt:'itemsView', test:settings.onView },
]

The "getLoc" function is required in some cases where the location is not static - for example, the room the player is in changes. It is not required in this example.

Here is another example, which will say "Nothing" if there is nothing there.

settings.inventoryPane = [
  {name:'You are holding...', alt:'itemsHeld', test:settings.isHeldNotWorn, getLoc:function() { return player.name; }, noContent:'Nothing' },
  {name:'You are wearing...', alt:'itemsWorn', test:settings.isWorn, getLoc:function() { return player.name; }, noContent:'Nothing' },
  {
    name:'You can see...', 
    alt:'itemsHere',
    test:settings.isHere, 
    getLoc:function() { return player.loc; },
    noContent:'Nothing',
    highlight:function(item) { 
      if (item.npc) return 1
      if (item.scenery) return 2
      return 0
    },
  },
]

It will also highlight some items. NPC items will additionally have the CSS class "highlight-item1", while scenery items will get "highlight-item2". The CSS classes will need to be set up in style.css, and could, for example, give NPCs a yellow background. Do check what it looks like in dark mode when you do this!

A game with attitude

You could use this to allow the user to set on-going state for the player. This could be defensive or offensive during combat for an RPG or, as in this example, set the player's attitude to other characters. The line in the settings.inventoryPane dictionary is this:

  {name:'Set Player\'s...', alt:'settings', test:function(o) { return o.setting }, },

It will therefore list any object with "setting" set to true. You also need to set the initial values, which needs to be in settings.js again, but inside settings.setup (you may have other stuff going on in this function; that is fine). This is some general code that will set the value for all these to the first in the list.

settings.setup = function() {
  for (const key in w) {
    if (w[key].setting) w[key].set()
  }
}

We then need to create an object for each aspect the player can control. I am going to do this as a general function; you can just copy the lot into your game, perhaps in code.js.

const createPlayerMoodObject = function(name, options) {
  createItem(name, {
    setting:true,
    options:options,
    verbFunction:function(list) {
      list.pop()
      for (const s of this.options) list.push(s)
    },
    set:function(s) {
      if (!s) s = this.options[0]
      this.listAlias = this.alias + ': ' + s
      player[this.name] = s
    },
  })
}

Then you need to call that function with your data. I am going to do two, so the user can select differently when interacting with men and women, mainly to show how it is done. The first attribute is the name, using the usual notation with underscores for spaces. The other attribute is the list of options.

createPlayerMoodObject("Attitude_to_men", ["Eager", "Aloof", "Jerk"])
createPlayerMoodObject("Attitude_to_women", ["Friendly", "Aloof", "Bitch"])

The attitude is a setting on the player object.

if (player.Attitude_to_men === 'Jerk') {
  kickHimWhenHeIsDown()
}

I suspect the user will get annoyed if you change it for her, but it can be done:

w.Attitude_to_men.set('Aloof')

You could add extra options as the game progresses.

w.Attitude_to_men.options.push('Jaded')

See here for how you could get this to work with dynamic conversations.

Status Attributes

Adding status attributes is quite unlike Quest 5. Rather than flag an attribute of the game or player as a status attribute, we set up an array of attributes.

settings.statusPane = "Status"
settings.statusWidthLeft = 120
settings.statusWidthRight = 40
settings.status = [
  "health",
  function() { return "<td>Health points:</td><td>" + player.hitpoints + "</td>" },
  "<td>Health points:</td><td>{show:player:hitpoints}</td>",
]

The settings.statusPane value sets the name of the status pane; if set to false the status pane will be turned off.

The status values will be set out in a table to keep values neatly aligned, use settings.statusWidthLeft and settings.statusWidthRight to adjust the column widths. Unless you have changed the width of the side pane, these should add up to 160.

The settings.status value is an array. Each element in the array will display one attribute, and you can do it using an attribute name, a function or the text processor. This example above illustrates all three.

The first is just the string "health", so Quest will show this as "health, using the player's "health" attribute. The second is a function; this should return an HTML string defining two TD elements, the name and the value (this will get inserted into a TR, so the <tr> tags should not be included). The HTML, then, might look like this:

<td>Health points:</td><td>14</td>

The third option is to use the text processor to insert the value into a string. The example above will give the same result as the function.

More complicated with functions

Although this is more complicated than the Quest 5 version, it is far more flexible, and if you want to do something that is not just a simple number it will be more straightforward. For example, you might want to report the player status as a phrase, without a label. Just return the value in a single table cell set to span two columns.

  function() { return '<td colspan="2">' + player.status + "</td>" },

This example will show both the current hits and maximum hits.

  function() { return "<td>Health points:</td><td>" + player.hitpoints + "/" + player.maxhitpoints + "</td>" },

You could even change the background colour to red when hits go below 20%.

  function() { return '<td>Health points:</td><td' + (player.hitpoint / player.maxhitpoints < 0.2 ? ' style="background:red"' : '') + '>' + player.hitpoints + "/" + player.maxhitpoints + "</td>" },

This is getting more complicated, and it might be better to move the work to the player object. Give the player a "getHitsStatus" attribute:

  getHitsStatus:function() {
    let s = '<td>Health points:</td><td'
    if (player.hitpoint / player.maxhitpoints < 0.2) s += ' style="background:red"'
    s += '>' + player.hitpoints + "/" + player.maxhitpoints + "</td>"
    return s
  }

Call that in the status array:

settings.status = [
  function() { return player.getHitsStatus() },
]

This example adds a dollar sign before and a "k" after the value.

  function() { return "<td>Bonus:</td><td>$" + player.bonus + "k</td>" },

Or better, use the displayMoney function.

  function() { return "<td>Bonus:</td><td>" + displayMoney(player.bonus) + "</td>" },

Note: The outout from the function does not use the text processor (but you could use processText in the function).

More complicated wit the text processor

While functions give ultimate control, there is a lot that can be done with the text processor/

  "<td>Weapon:</td><td>{showOrNot:player:none:getEquippedWeapon:listAlias}</td>",
  "<td>Element:</td><td>{ifExists:player:attunedElement:{cap:{show:player:attunedElement}}:none}</td>",
  "<td>Target:</td><td>{ifExists:player:target:{cap:{show:player:target:listAlias}}:none}</td>",

Further UI Options

You can add a function called settings.customUI to add additional features to the page. This example adds another div that shows the player's health graphically. This could be positioned using CSS in the styles.css file (you cannot position using JavaScript in this function as it is called before the page has been fully created). This needs to go in setting.js so it is set up before the UI is created (in io.js).

settings.customUI = function() {
  document.writeln('<div id="rightpanel" class="sidepanes sidepanesRight">');
  document.writeln('<div id="rightstatus">');
  document.writeln('<table align="center">');
  document.writeln('<tr><td><b>Health</b></td></tr>');
  document.writeln('<tr><td style="border: thin solid black;background:white;text-align:left;\"><span id="hits-indicator" style="background-color:green;padding-right:100px;"></span></td></tr>');
  document.writeln('</table>');
  document.writeln('</div>');
  document.writeln('</div>');
};

What it does is use document.writeln to add additional HTML to the page. It adds a new DIV element containing all the things we want. It uses the existing CSS classes "sidepanes" and "sidepanesRight", the latter styles it like the standard side panes, the latter positions it on the right.

You can add a function, settings.updateCustomUI, to have Quest update your UI each turn, for example, to set the value of the player's health in the indicator.

settings.updateCustomUI = function() {
  $('#hits-indicator').css('padding-right', 120 * player.health / player.maxHealth);
};

A note about how UI components are updated

An important difference in approach between Quest 5 and 6 is that Quest 6 assumes one "source of truth", rather than assuming everything gets updated.

To illustrate that, let us suppose we are tracking the user's score. This is a simple integer attribute of the player. However, we also want it be displayed on the screen. The Quest 5 approach is to maintain a second value somewhere, and this is what gets shown on the screen. The implication, then, is that when we update UI components, we send them the values they are to display. We want the score to be updated, so we send the new value to the display.

Quest 6 takes a different approach; it shows the attribute itself, not a copy if it. The reason for that is that we can be sure it is right. However, this means UI components are set up very differently. We do not send the value of score to the UI component whenever it changes or whenever the UI needs updating. Instead, we hand it a function when we create the component. When the component needs to be updated, it calls the function to find the current score.

Part of the reason for doing it this way is that change scripts are not possible, but I think it also gives us a lot more flexibility in what we display.

⚠️ **GitHub.com Fallback** ⚠️