Skip to content

Instantly share code, notes, and snippets.

@jazzsequence
Last active May 17, 2019 19:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jazzsequence/7c4c4203c8ea94930928b49d90ab1e20 to your computer and use it in GitHub Desktop.
Save jazzsequence/7c4c4203c8ea94930928b49d90ab1e20 to your computer and use it in GitHub Desktop.
D&D Battle Tracker
/**
* Add a character line.
*/
function addCharacter() {
const addChar = document.querySelectorAll( '[id^="add-tmp-char-"]' );
// Loop through added character inputs.
for ( const tmpCharButton of addChar ) {
// Add a click handler for this particular input.
tmpCharButton.addEventListener( 'click', () => {
const id = getId( tmpCharButton );
const thisItem = document.getElementById( `character-${id}` )
const charInput = document.getElementById( `addChar-${id}` );
const charName = charInput ? charInput.value : false;
const dexInput = document.getElementById( `charDex-${id}` );
const dexVal = dexInput ? dexInput.value : false;
if ( charName && charName !== 'undefined' && charName !== '' ) {
// Remove the old input and button.
charInput.remove();
// Only remove the add button if we have a Dex value.
if ( dexVal !== 'undefined' && dexVal !== '' ) {
tmpCharButton.remove();
} else {
// We still need to add the dex. Change the button value to indicate that a Dex score is still needed.
const buttonTxt = `${tmpCharButton.textContent} Dex`;
tmpCharButton.textContent = buttonTxt;
}
charSpan = document.createElement('span');
charSpan.setAttribute( 'id', `addChar-${id}` );
charSpan.classList.add('character-name');
charSpan.textContent = charName;
thisItem.appendChild( charSpan );
}
if ( dexVal && dexVal !== 'undefined' && dexVal !== '' ) {
// Remove the old input and button.
dexInput.remove();
// Only remove the add button if we have a character name.
if ( charName !== 'undefined' && charName !== '' ) {
tmpCharButton.remove();
} else {
// We still need to add the character name. Change the button value to indicate that a name is still needed.
const buttonTxt = `${tmpCharButton.textContent} Name`;
tmpCharButton.textContent = buttonTxt;
}
dexSpan = document.createElement('span');
dexSpan.setAttribute( 'id', `charDex-${id}` );
dexSpan.classList.add('character-init-bonus');
dexSpan.textContent = `Initiative Bonus: ${getModifier( dexVal )}`;
thisItem.appendChild( dexSpan );
}
} );
}
}
const addNewCharacter = document.getElementById('add-new-character');
const characterList = document.getElementById('character-list');
let charCount = 0;
/**
* Builds a unique temporary input to storing a character's information.
*
* @return {node} The DOM node for the built list item.
*/
function buildTmpChar() {
const tmpChar = document.createElement('li');
const input = document.createElement('input');
const button = document.createElement('button');
const buttonTxt = document.createTextNode('Add');
input.setAttribute('id',`addChar-${charCount}`);
button.setAttribute('id',`add-tmp-char-${charCount}`);
tmpChar.setAttribute('id', `character-${charCount}`);
charCount++;
// add the input to the temp character line.
tmpChar.appendChild(input);
// add the button text to the button.
button.appendChild(buttonTxt);
// add the button to the temp character line.
tmpChar.appendChild(button);
return tmpChar;
}
// Add a click handler to the add new character button.
addNewCharacter.addEventListener( 'click', () => {
// add the temp character line to the character list.
characterList.appendChild(buildTmpChar());
} );
<!DOCTYPE html>
<html>
<head>
<title>D&D Battle Tracker</title>
</head>
<body>
<header>
<h1>Dungeons & Dragons Battle Tracker</h1>
</header>
<div class="battle-tracker">
<div class="characters">
<h2>Characters</h2>
<ul id="character-list">
</ul>
<button id="add-new-character">Add new</button>
</div>
</div>
</body>
<script src="src/js/battletracker.js"></script>
</html>
/**
* Build the initiative list. Order by initiative first, then by Dex score.
* Adds an input for recording damage and a button to save the damage input.
*/
function buildCharacterListByInit() {
const characters = getCharacterData();
// Sort characters by init score, then by dex.
characters.sort( ( a, b ) => ( a.init < b.init ) ? 1 : ( a.init === b.init ) ? ( ( a.dex < b.dex ) ? 1 : -1 ) : -1 );
for ( const character of characters ) {
const characterEl = document.createElement( 'li' );
const characterDamage = document.createElement( 'input' );
const characterDamageBtn = document.createElement( 'button' );
// Build the list item for the character.
characterEl.textContent = `${character.name} (${character.init}) [${ character.type.toUpperCase()}]`;
characterEl.setAttribute( 'id', `character-${character.id}` );
characterEl.setAttribute( 'data-name', character.name );
characterEl.setAttribute( 'data-init', character.init );
characterEl.setAttribute( 'data-max-hp', character.maxHp );
characterEl.setAttribute( 'data-current-hp', character.currentHp );
// Add an input for recording damage.
characterDamage.setAttribute( 'id', `character-${character.id}-damage` );
// Add a button to save input.
characterDamageBtn.setAttribute( 'id', `character-${character.id}-damage-btn` );
characterDamageBtn.textContent = 'Record Damage';
// Add the input and button to the list item.
characterEl.appendChild( characterDamage );
characterEl.appendChild( characterDamageBtn );
// Add the character to the list.
initList.appendChild( characterEl );
characterEl.addEventListener( 'click', () => {
recordCharacterDamage( character );
} );
}
}
/**
* Builds the initiative input and save button.
*
* @param {element} thisItem This is the <li> element to add the initiative die roll input to.
* @param {int} id The character ID in the list.
*/
function buildInitInput( thisItem, id ) {
// Build the input.
initInput = document.createElement( 'input' );
initInput.setAttribute( 'type', 'number' );
initInput.setAttribute( 'min', '1' ); // The lowest possible initiative you could roll, e.g. 1 on 1d20.
initInput.setAttribute( 'max', '20' ); // The highest possible initiative you could roll, e.g. 20 on 1d20.
initInput.setAttribute( 'id', `charInit-${id}` );
initInput.setAttribute( 'placeholder', 'd20' );
initInput.classList.add( 'character-initiative' );
thisItem.appendChild(initInput);
// Build a button to save the initiative score.
saveInit = document.createElement( 'button' );
saveInit.textContent = 'Save Initiative roll';
saveInit.setAttribute( 'id', `saveInit-${id}` );
saveInit.classList.add( 'save-initiative' );
thisItem.appendChild( saveInit );
// Add an event listener to the save init button.
saveInit.addEventListener( 'click', calculateAndSaveInitiative );
}
/**
* Take an ability score and return the modifier bonus.
*
* The ability score must be greater than 1 and up to 30.
*
* These scores are taken from the Ability Scores and Modifiers table on page 13 of the D&D 5e Player's Handbook.
*
* @param {int} score An ability score.
* @return {int} The modifier for that ability score.
*/
function calculateModifier( score ) {
console.log(score);
switch( score ) {
case 1:
return -5;
case 2:
case 3:
return -4;
case 4:
case 5:
return -3;
case 6:
case 7:
return -2;
case 8:
case 9:
return -1;
case 10:
case 11:
return 0;
case 12:
case 13:
return 1;
case 14:
case 15:
return 2;
case 16:
case 17:
return 3;
case 18:
case 19:
return 4;
case 20:
case 21:
return 5;
// At this point we get into epic levels that are less common.
case 22:
case 23:
return 6;
case 24:
case 25:
return 7;
case 26:
case 27:
return 8;
case 28:
case 29:
return 9;
case 30:
return 10;
default:
return 0;
}
}
// Add a click handler to the add new character button.
addNewCharacter.addEventListener( 'click', () => {
// add the temp character line to the character list.
characterList.appendChild(buildTmpChar());
const addChar = document.querySelectorAll( '[id^="add-tmp-char-"]' );
// Loop through added character inputs.
for ( const tmpCharButton of addChar ) {
// Add a click handler for this particular input.
tmpCharButton.addEventListener( 'click', () => {
// Get the value of the input in the line.
const charName = tmpCharButton.previousSibling.value;
const id = getId( tmpCharButton );
const thisItem = document.getElementById( `character-${id}` )
console.log(getId(tmpCharButton));
if ( charName !== 'undefined' && charName !== '' ) {
tmpCharButton.previousSibling.remove();
tmpCharButton.remove();
addedCharacter = document.createTextNode( charName );
thisItem.appendChild( addedCharacter );
}
} );
}
} );
/**
* Scrapes all the characters on the page and returns their data in an array of objects.
*
* @return {array} An array of objects of character data.
*/
function getCharacterData() {
const characterList = document.getElementsByClassName( 'character-single' );
let characters = [];
for ( const character of characterList ) {
characters.push(
{
id: parseInt( character.dataset.id ),
name: character.dataset.characterName,
init: parseInt( character.dataset.initiative ),
maxHp: parseInt( character.dataset.hpMax ),
currentHp: parseInt( character.dataset.hpCurrent ),
type: character.dataset.characterType,
}
);
}
return characters;
}
/**
* Get the numeric id from an element.
* This helps match the buttons with the parent list items and sibling inputs.
*
* @param {node} el Any element (ideally one with an id that contains a decimal).
* @return {string|false} The numeric part of the id, if one exists.
*/
function getId( el ) {
const id = el.getAttribute('id');
if ( id !== typeof undefined ) {
return id.replace(/[^0-9]+/g,'');
}
return false;
}
/**
* Calculate the initiative for a character.
*
* Pass the initiative along to the buildInitiative function which stores the value.
*/
function calculateAndSaveInitiative() {
const id = getId( this );
const initInput = document.getElementById( `charInit-${id}` );
const initRoll = initInput.value;
const initBonus = document.getElementById( `charDex-${id}` ).dataset.init_bonus;
let initiative = 0;
if ( 'undefined' !== initRoll & '' !== initRoll ) {
initiative = parseInt( initRoll ) + parseInt( initBonus );
buildInitiative( initiative, id );
buildHp( thisItem, id );
}
}
/**
* Determines character initiative.
*
* @param {int} initiative The calculated initiative for the character.
* @param {int} id The character ID.
*/
function buildInitiative( initiative, id ) {
const thisItem = document.getElementById( `character-${id}` );
const initInput = document.getElementById( `charInit-${id}` );
const saveInit = document.getElementById( `saveInit-${id}` );
// Remove the initiative roll inputs.
initInput.remove();
saveInit.remove();
// Build a new text element revealing initiative.
initDisplay = document.createElement( 'span' );
initDisplay.textContent = `Initiative: ${initiative}`;
initDisplay.setAttribute( 'id', `initiative-${id}` );
initDisplay.setAttribute( 'data-initiative', initiative );
initDisplay.classList.add( 'initiative-value' );
thisItem.appendChild( initDisplay );
}
/**
* Builds an input for tracking HP.
*
* @param {element} thisItem This is the <li> element to add the HP input to.
* @param {int} id The unique character id.
*/
function buildHp( thisItem, id ) {
// Add an input to track HP. To start, this will just ask for HP total, but we'll update this with another event listener and change it after the HP total was added.
hpInput = document.createElement( 'input' );
hpInput.setAttribute( 'type', 'number' );
hpInput.setAttribute( 'id', `hp-${id}` );
hpInput.setAttribute( 'placeholder', 'HP' );
hpInput.setAttribute( 'data-hp-max', 0 );
hpInput.setAttribute( 'data-hp-current', 0 );
hpInput.setAttribute( 'data-hp-last', 0 );
hpInput.classList.add( 'hp-input' );
thisItem.appendChild( hpInput );
// Now we need a button to add for entering/calculating HP.
hpButton = document.createElement( 'button' );
hpButton.textContent = 'Enter starting HP';
hpButton.setAttribute( 'id', `hpButton-${id}` );
hpButton.classList.add( 'update-hp' );
thisItem.appendChild( hpButton );
// Add a span to display an element saying something happened.
hpUpdateMsg = document.createElement( 'span' );
hpUpdateMsg.setAttribute( 'id', `hpUpdateMsg-${id}` );
hpUpdateMsg.classList.add( 'hp-update-msg' );
hpUpdateMsg.textContent = ''; // Reset the content of the span.
thisItem.appendChild( hpUpdateMsg );
hpButton.addEventListener( 'click', updateHp );
}
/**
* Remove the Add New and Let's go buttons, but only if we actually have some characters.
*/
function removeButtons() {
const saveAllDesc = document.getElementById( 'save-all-fields-desc' );
let error = false;
let charListIsEmpty = characterList.innerHTML.trim() === '';
let npcListIsEmpty = npcList.innerHTML.trim() === '';
// Only remove buttons if both things are empty.
if ( ! charListIsEmpty && ! npcListIsEmpty ) {
addNewCharacter.remove();
addNewNPC.remove();
saveAll.remove();
saveAllDesc.remove();
error = document.getElementById( 'not-done-error-msg' );
} else {
displayNotEmptyMessage();
}
if ( error ) {
error.remove();
}
}
/**
* Display a message if we don't actually have all the characters and/or NPCs.
*/
function displayNotEmptyMessage() {
const container = document.getElementById( 'save-fields' );
const message = document.createElement( 'p' );
message.setAttribute( 'id', 'not-done-error-msg' );
message.classList.add( 'error' );
message.textContent = 'It doesn\'t look like you\'re quite done. There is some information missing. Please make sure you have all your characters and NPCs and try again.';
container.appendChild( message );
}
// Add a similar click handler to the add new NPC button.
addNewNPC.addEventListener( 'click', () => {
npcList.appendChild( buildTmpChar() );
addCharacter();
// When at least one NPC has been added, make the button clickable and listen for a click event on the Let's go! button.
saveAll.removeAttribute( 'disabled' );
saveAll.addEventListener( 'click', () => {
// Remove the Add New character buttons.
removeButtons();
// Build the character initiative list.
buildCharacterListByInit();
} );
} );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment