Character Creation - ThePix/QuestJS GitHub Wiki

So you want a character creation screen...

cc_diag

This is not trivial, and the main complication is that you will need to work with HTML to display all the controls you want. If you are familiar with HTML controls, that will be a big help. That said, at the end there is some general code for setting up a text field for name and a series of customisable drop-down menus that is mostly just copy-and-pasting.

Quest 6 has a built-in dialogue that will appear if settings.startingDialogEnabled is set to true... It just will not do anything.

We will do a simple dialog just asking for name, sex and background, but really the sky is the limit here. Just be aware that players may be put off if it is too complicated. Consider whether shoe size really matters...

Everything is done via a bunch of settings, the basics of which are here:

settings.startingDialogTitle = "Who are you?"
settings.startingDialogWidth = 500
settings.startingDialogHeight = 'auto'
settings.startingDialogButton = 'OK'
settings.startingDialogHtml = '<p>Empty</p>'
settings.startingDialogOnClick = function() {
  // do stuff
}
settings.startingDialogInit = function() {
  // do stuff
}
settings.startingDialogAlt = function() {
  // do stuff
}

The first four are simple; they set the title and size of the dialogue box, plus the text on the button.

The fourth, settings.startingDialogHtml is the HTML code for all the widgets and everything else seen in the dialogue. Here is the code for our simple example. I find it easier to build up long strings in steps, but you could do this in one very long line.

settings.startingDialogHtml = '<p>Name: <input id="namefield" type="text" value="Zoxx" /></p>'
settings.startingDialogHtml += '<p>Male: <input type="radio" id="male" name="sex" value="male">&nbsp;&nbsp;&nbsp;&nbsp;'
settings.startingDialogHtml += 'Female: <input type="radio" id="female" name="sex" value="female" checked></p>'
settings.startingDialogHtml += '<p>Job: <select id="job"><option value="fighter">Fighter</option><option value="wizard">Wizard</option></select></p>'

The last three settings are functions that run at certain times. settings.startingDialogOnClick will run when the player clicks the okay button, so needs to get the data from the form, and apply it to your game. settings.startingDialogInit is optional; if present is will run when the dialogue is set up.

The settings.startingDialogAlt is also optional and is run when startingDialogEnabled is false. It is useful to disable the dialogue when you are developing your game; you can use this to set some default values.

Here is an example of settings.startingDialogOnClick that will collect the data from the above HTML.

settings.startingDialogOnClick = function() {
  player.class = document.querySelector("#job").value
  player.isFemale = document.querySelector("#female").checked
  player.alias = document.querySelector("#namefield").value
}

One useful thing to do in settings.startingDialogInit is to move the focus to the first field.

settings.startingDialogInit = function() {
  document.querySelector('#namefield').focus();
}

Add a slider

For number input, a slider can be a good way to ensure it is in a specific range. We will add one that allows the user to choose the percentage magic as opposed to combat.

These lines needs to be inserted after the other settings.startingDialogHtml += lines. The first adds the control, the second line informa the user of the values.

settings.startingDialogHtml += '<p>Magic vs combat: <input type="range" id="magic" name="magic" value="50" min="0" max="100" oninput="settings.updateMagic()"></p>'
settings.startingDialogHtml += '<p>Magic: <span id="sliderMagic">50</span> Combat: <span id="sliderCombat">50</span></p>'

Rather than change settings.startingDialogOnClick, we will have the values updated on the fly, so they can be displayed on screen. I am adding this to settings just to keep all this together; the name was determined by the "oninput" HTML attribute used above.

settings.updateMagic = function() {
  player.magic = parseInt(document.querySelector("#magic").value)
  player.combat = 100 - player.magic
  document.querySelector('#sliderMagic').innerHTML = player.magic
  document.querySelector('#sliderCombat').innerHTML = player.combat
}

Add a point-buy system

Many RPGs allow the user to distribute points among skills, or whatever, up to a limit. First we set it up, saying how many points there are to spend, and what they can be spent on. This needs to go before the settings.startingDialogHtml += lines.

settings.maxPoints = 10
settings.skills = [
  "Athletics",
  "Lore",
  "Manipulation",
  "Subterfuge",
]

Then we add the HTML, so this needs to go with yous existing settings.startingDialogHtml += lines, depending on where you want it to appear.

settings.startingDialogHtml += '<table>'
for (const s of settings.skills) {
  settings.startingDialogHtml += '<tr><td>' + s + '</td><td id="points-' + s + '">0</td><td>'
  settings.startingDialogHtml += '<input type="button" value="-" onclick="settings.pointsAdjust(\'' + s + '\', false)" />'
  settings.startingDialogHtml += '<input type="button" value="+" onclick="settings.pointsAdjust(\'' + s + '\', true)" />'
  settings.startingDialogHtml += '</td></tr>'
}
settings.startingDialogHtml += '<tr><td>Total</td><td id="points-total">0/' + settings.maxPoints + '</td><td>'
settings.startingDialogHtml += '</td></tr>'
settings.startingDialogHtml += '</table>'

Then we need a new function to respond to the button clicks.

settings.pointsAdjust = function(skill, up) {
  if (player[skill] === undefined) {
    for (const s of settings.skills) player[s] = 0
  }
  let n = 0
  for (const s of settings.skills) n += player[s]

  if (up && n < settings.maxPoints) {
    player[skill]++
    n++
  }
  if (!up && player[skill] > 0) {
    player[skill]--
    n--
  }

  document.querySelector('#points-' + skill).innerHTML = player[skill]
  document.querySelector('#points-total').innerHTML = n + '/' + settings.maxPoints
}

As with the slider, the player attributes are updated on the fly, so no need to change settings.startingDialogOnClick

All Together Now...

Here is our final code, to get the dialog seen in the image at the top.

settings.startingDialogEnabled  = true;

settings.maxPoints = 10
settings.skills = [
  "Athletics",
  "Lore",
  "Manipulation",
  "Subterfuge",
]
settings.professions = [
  {name:"Farm hand", bonus:"strength"},
  {name:"Scribe", bonus:"intelligence"},
  {name:"Exotic dancer", bonus:"agility"},
  {name:"Merchant", bonus:"charisma"},
]

settings.startingDialogTitle = "Who are you?"
settings.startingDialogWidth = 500
settings.startingDialogHeight = 'auto'
settings.startingDialogButton = 'OK'

settings.startingDialogHtml = '<p>Name: <input id="namefield" type="text" value="Zoxx" /></p>'

settings.startingDialogHtml += '<p>Male: <input type="radio" id="male" name="sex" value="male">&nbsp;&nbsp;&nbsp;&nbsp;'
settings.startingDialogHtml += 'Female<input type="radio" id="female" name="sex" value="female" checked></p>'

settings.startingDialogHtml += '<p>Background: <select id="job">'
for (const s of settings.professions) {
  settings.startingDialogHtml += '<option value="' + s.name + '">' + s.name + '</option>'
}
settings.startingDialogHtml += '</select></p>'

settings.startingDialogHtml += '<p>Magic vs combat: <input type="range" id="magic" name="magic" value="50" min="0" max="100" oninput="settings.updateMagic()"></p>'
settings.startingDialogHtml += '<p>Magic: <span id="sliderMagic">50</span> Combat: <span id="sliderCombat">50</span></p>'

settings.startingDialogHtml += '<table>'
for (const s of settings.skills) {
  settings.startingDialogHtml += '<tr><td>' + s + '</td><td id="points-' + s + '">0</td><td>'
  settings.startingDialogHtml += '<input type="button" value="-" onclick="settings.pointsAdjust(\'' + s + '\', false)" />'
  settings.startingDialogHtml += '<input type="button" value="+" onclick="settings.pointsAdjust(\'' + s + '\', true)" />'
  settings.startingDialogHtml += '</td></tr>'
}
settings.startingDialogHtml += '<tr><td>Total</td><td id="points-total">0/' + settings.maxPoints + '</td><td>'
settings.startingDialogHtml += '</td></tr>'
settings.startingDialogHtml += '</table>'


settings.pointsAdjust = function(skill, up) {
  if (player[skill] === undefined) {
    for (const s of settings.skills) player[s] = 0
  }
  let n = 0
  for (const s of settings.skills) n += player[s]

  if (up && n < settings.maxPoints) {
    player[skill]++
    n++
  }
  if (!up && player[skill] > 0) {
    player[skill]--
    n--
  }

  document.querySelector('#points-' + skill).innerHTML = player[skill]
  document.querySelector('#points-total').innerHTML = n + '/' + settings.maxPoints
}

settings.updateMagic = function() {
  player.magic = parseInt(document.querySelector("#magic").value)
  player.combat = 100 - player.magic
  document.querySelector('#sliderMagic').innerHTML = player.magic
  document.querySelector('#sliderCombat').innerHTML = player.combat
}

settings.startingDialogOnClick = function() {
  player.class = document.querySelector("#job").value
  player.isFemale = document.querySelector("#female").checked
  player.setAlias(document.querySelector("#namefield").value)
}
settings.startingDialogInit = function() {
  document.querySelector('#namefield').focus()
}
settings.startingDialogAlt = function() {
  for (const s of settings.skills) player[s] = Math.floor(settings.maxPoints / settings.skills.length)
  player.magic = 50
  player.combat = 50
  player.class = 'Merchant'
  player.setAlias('Zoxx')
}

Further Customisation

If you want to get deeper into customising the dialog, you might want to change the button. This needs to happen inside = function() {, and if you write your own button, you need to give it an event listener.

This example is for a dialogue where the content can change a lot, and the default button was moving up and down, depending on how big the content was. It therefore rebuilds the footer with an absolute position, but then has to give the new button the event listener.

settings.startingDialogInit = function() {
  ...
  document.querySelector('#name_input').focus()
  document.querySelector('#dialog-footer').style.position = 'absolute';
  document.querySelector('#dialog-footer').style.width = '460px'
  document.querySelector('#dialog-footer').style.top = settings.startingDialogHeight - 10 + 'px'
  document.querySelector('#dialog-footer').innerHTML = '<p style="text-align:center">[Click on blue text to scroll through available options]</p><button id="dialog-button" value="default">Confirm</button>'
  document.querySelector("#dialog-button").addEventListener('click', settings.setUpDialogClick)
}

Delayed Dialogue

By default, the dialogue will display immediately. You might want to delay it until you have set the scene. This is handled a little differently.

The first difference is that settings.startingDialogEnabled must be false (or not set), as we do not want the dialogue at the start. However, we do need to set it true later, specifically in settings.startingDialogOnClick, to prevent the set-up functions running a second time (it has to be in settings.startingDialogOnClick, and if you have several dialogues, it needs to be done every time, as the code the calls settings.startingDialogOnClick sets it to false just before hand, and you need to over-ride that).

You will need to call settings.setUpDialog() when you want the dialogue to appear. This example uses the trigger function so the dialogue is shown after the wait has ended.

settings.setup = function() {
  msg("So here is the situation...")
  wait()
  trigger(function() {
    settings.setUpDialog()
    return true
  })
}

Keep It Simple

If you are not that confident with HTML, you can use this basic template. Just paste this code into your settings.js file:

settings.startingDialogEnabled  = true
settings.startingDialogTitle = "Who are you?"
settings.startingDialogWidth = 320
settings.startingDialogHeight = 'auto'

settings.startingDialogHtml = '<table border="1"><tr><td>Name</td><td><input id="namefield" type="text" value="Dani"  style="width:200px"/></td></tr>';
for (const el of settings.charData) settings.addDropDown(el)
settings.startingDialogHtml += '</table>'

settings.addDropDown = function(data) {
  settings.startingDialogHtml += '<tr><td>' + data.name + '</td><td><select id="' + verbify(data.name) + '" style="width:200px">'
  const selected = random.int(data.values.length - 1)
  data.values.forEach(function(s, i) {
    settings.startingDialogHtml += '<option value="' + s + '"'
    if (i === selected) settings.startingDialogHtml += ' selected="selected"'
    settings.startingDialogHtml += '>' + s + '</option>'
  })
  settings.startingDialogHtml += '</select></td></tr>'
}

settings.startingDialogOnClick = function() {
  const p = player
  p.alias = document.querySelector("#namefield").value

  for (const el of settings.charData) {
    const value = document.querySelector('#' + verbify(el.name)).value
    p[verbify(el.name)] = el.numeric ? el.values.indexOf(value) : value
  }

  endTurnUI()
}

settings.startingDialogInit = function() {
  document.querySelector('#namefield').focus()
}

settings.startingDialogAlt = function() {
  // do stuff
}

The customise it by adding one relatively simple array. Each element in the array has a "name" which will appear on the dialogue and will converted to an attribute on the player stripping non-alphanumeric characters and by making it lower case (so 'Magic prowess' will become 'magicprocess'). Then "values" is an array of options, one of which will be picked at random. You can also have "numeric" set to true to convert the string to the index, giving an integer from zero.

settings.charData = [
  {name:'Job', values:['Student', 'Personal trainer', 'Underwear model', 'Scientist', 'Sales manager', 'Agricultural labourer', 'Software engineer', 'Career criminal']},
  {name:'Sex', values:['Male', 'Female', 'Non-binary']},
  {name:'Hair length', values:['Shaven', 'Cropped', 'Short', 'Shoulder-length', 'Long', 'Very long', 'Ponytail', 'Topknot']},
  {name:'Hair colour', values:['Black', 'Brunette', 'Fair', 'Blonde', 'Auburn', 'Ginger', 'Blue']},
  {name:'Magic prowess', values:['None', 'Meager', 'So-so', 'Good', 'Awesome'], numeric:true},
]

You can have as many elements in the any as you like as long as they fit the screen.

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