class Dice { constructor(sides, addition = 0, count = 1, color = undefined) { this.sides = sides; this.addition = addition; this.count = count; this.color = color; this.selected = false; } toText(color = false) { let result = ""; if (this.count > 1) { result += this.count; } result += "D" + this.sides; if (this.addition) { result += (this.addition < 0 ? "-" : "+") + Math.abs(this.addition); } if (color && this.color) { result += "[" + this.color + "]"; } return result; } fromText(value) { const result = value.match(dice_regex); if (result) { if (result[1]) { this.count = +result[1]; } this.sides = +result[2]; if (result[3]) { this.addition = +result[3]; } if (result[5]) { this.color = result[5]; } } } } class DiceHistoryEntry { constructor(dices = undefined, formula = undefined, result = undefined, time = undefined) { this.dices = dices; this.formula = formula; this.result = result; this.time = time; } } const default_sides = [4, 6, 8, 10, 12, 20, 100]; const default_colors = ["#de324c", "#f4895f", "#f8e16f", "#95cf92", "#369acc", "#9656a2", "#6c584c"]; const dice_regex = /(\d+)?[D|d](\d+)([\+|\-]\d+)?(\[(.+)\])?/; let darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; let dices = []; let history = []; let redoHistory = []; let dicesContainer = document.getElementById("dices"); let historyContainer = document.getElementById("history"); function renderDice(dice, index) { const diceElement = document.createElement("div"); diceElement.classList.add("dice"); const diceImage = document.createElement("div"); diceImage.classList.add("dice-image"); if (default_sides.indexOf(dice.sides) != -1) { diceImage.classList.add("dice-image-" + dice.sides); } else { diceImage.classList.add("dice-image-custom"); } if (dice.color) { diceImage.style.backgroundColor = dice.color; // revert b/w on dark-mode if (darkMode) { if (["#000", "#000000", "black", "rgb(0,0,0)", "rgb(0, 0, 0)"].indexOf(dice.color) != -1) { diceImage.style.backgroundColor = "#fff"; } else if (["#fff", "#ffffff", "white", "rgb(255,255,255)", "rgb(255, 255, 255)"].indexOf(dice.color) != -1) { diceImage.style.backgroundColor = "#000"; } } } const diceImageContainer = document.createElement("div"); diceImageContainer.classList.add("dice-image-container"); diceImageContainer.onclick = function () { if (dices[index].selected) { dices[index].selected = false; diceElement.classList.remove("selected"); } else { dices[index].selected = true; diceElement.classList.add("selected"); } if (dices.filter((dice) => dice.selected).length) { document.getElementById('roll-button').classList.remove("disabled"); document.getElementById('roll-add-button').classList.add("disabled"); if (dices.filter((dice) => dice.selected).length > 1) { document.getElementById('roll-add-button').classList.remove("disabled"); } } else { document.getElementById('roll-button').classList.add("disabled"); document.getElementById('roll-add-button').classList.add("disabled"); } localStorage.setItem('dices', JSON.stringify(dices)); }; diceImageContainer.appendChild(diceImage); diceElement.appendChild(diceImageContainer); const diceLabelContainer = document.createElement("div"); diceLabelContainer.classList.add("label-container"); const diceLabel = document.createElement("label"); diceLabel.innerText = dice.toText(); diceLabelContainer.appendChild(diceLabel); diceElement.appendChild(diceLabelContainer); const diceRemove = document.createElement("div"); diceRemove.classList.add("remove"); diceRemove.onclick = function () { removeDice(index); }; diceElement.appendChild(diceRemove); if (dice.selected) { diceElement.classList.add("selected"); } else { diceElement.classList.remove("selected"); } dicesContainer.appendChild(diceElement); } function renderDices() { dicesContainer.innerHTML = ""; dices.forEach((dice, index) => { renderDice(dice, index); }) if (dices.filter((dice) => dice.selected).length) { document.getElementById('roll-button').classList.remove("disabled"); document.getElementById('roll-add-button').classList.add("disabled"); if (dices.filter((dice) => dice.selected).length > 1) { document.getElementById('roll-add-button').classList.remove("disabled"); } } else { document.getElementById('roll-button').classList.add("disabled"); document.getElementById('roll-add-button').classList.add("disabled"); } } function addDice(sides, addition = 0, count = 1, color = "#000") { dices.push(new Dice(sides, addition, count, color)); localStorage.setItem('dices', JSON.stringify(dices)); renderDices(); } function removeDice(index) { dices.splice(index, 1); localStorage.setItem('dices', JSON.stringify(dices)); renderDices(); } function resetDices() { dices = []; default_sides.forEach((side, i) => { dices.push(new Dice(side, 0, 1, default_colors[i])); }); localStorage.setItem('dices', JSON.stringify(dices)); renderDices(); } function addDiceForm() { let inputSides = document.getElementById("inputSides"); if (!inputSides.value) { inputSides = document.getElementById("inputCustom") } const inputAddition = document.getElementById("inputAddition"); const inputCount = document.getElementById("inputCount"); const inputColor = document.getElementById("inputColor"); addDice(+inputSides.value, +inputAddition.value, +inputCount.value, inputColor.value); } function setDicesContainer(container) { dicesContainer = container; } function renderHistory() { historyContainer.innerHTML = ""; if (history.length) { document.getElementById('history-button').classList.remove("disabled"); document.getElementById('history-undo-button').classList.remove("disabled"); } else { document.getElementById('history-button').classList.add("disabled"); document.getElementById('history-undo-button').classList.add("disabled"); } if (redoHistory.length) { console.log(redoHistory); document.getElementById('history-redo-button').classList.remove("disabled"); } else { document.getElementById('history-redo-button').classList.add("disabled"); } history.forEach((entry) => { if (entry.result) { const diceLabelContainer = document.createElement("div"); diceLabelContainer.classList.add("label"); if (entry.dices) { entry.dices.forEach((diceData, i) => { const dice = new Dice(diceData.sides, diceData.addition, diceData.count, diceData.color); const diceLabel = document.createElement("label"); diceLabel.innerHTML = dice.toText() + (i == entry.dices.length - 1 ? ": " : ""); diceLabel.style.color = dice.color; // revert b/w on dark-mode if (darkMode) { if (["#000", "#000000", "black", "rgb(0,0,0)", "rgb(0, 0, 0)"].indexOf(dice.color) != -1) { diceLabel.style.color = "#fff"; } else if (["#fff", "#ffffff", "white", "rgb(255,255,255)", "rgb(255, 255, 255)"].indexOf(dice.color) != -1) { diceLabel.style.color = "#000"; } } diceLabelContainer.appendChild(diceLabel); if (i < entry.dices.length - 1) { const diceLabelAdd = document.createElement("span"); diceLabelAdd.innerHTML = " + "; diceLabelContainer.appendChild(diceLabelAdd); } }) } historyContainer.appendChild(diceLabelContainer); const diceResult = document.createElement("span"); diceResult.classList.add("result"); diceResult.innerText = entry.result; historyContainer.appendChild(diceResult); const diceFormulaContainer = document.createElement("span"); diceFormulaContainer.classList.add("formula-container"); historyContainer.appendChild(diceFormulaContainer); if (entry.formula) { const diceFormulaEqual = document.createElement("span"); diceFormulaEqual.innerText = " = "; diceFormulaContainer.appendChild(diceFormulaEqual); const diceFormula = document.createElement("span"); diceFormula.classList.add("formula"); diceFormula.innerText = entry.formula; diceFormulaContainer.appendChild(diceFormula); } const diceTime = document.createElement("span"); diceTime.classList.add("time"); historyContainer.appendChild(diceTime); if (entry.time) { diceTime.innerText = entry.time.fromNow(); diceTime.title = entry.time.format("LLLL"); } } else { historyContainer.appendChild(document.createElement("hr")); } }) } function formula(dice) { let formula = ""; for (let index = 0; index < dice.count; index++) { formula += Math.floor(Math.random() * dice.sides + 1); if (index < dice.count - 1) { formula += " + "; } } if (dice.addition) { formula += " + " + dice.addition; } return formula; } function roll(dice, time = true) { let f = formula(dice); const result = eval(f); if (f.indexOf("+") == -1) { f = ""; } history.unshift(new DiceHistoryEntry([dice], f, result, time ? moment() : undefined)); localStorage.setItem('history', JSON.stringify(history)); redoHistory = []; localStorage.removeItem('redoHistory'); renderHistory(); } function rollSelected() { const selected = dices.filter((dice) => dice.selected); if (selected.length) { if (history.length) { history.unshift(new DiceHistoryEntry()); } selected.forEach((dice, i) => { roll(dice, i == selected.length - 1); }) } } function rollForm() { let inputSides = document.getElementById("inputSides"); if (!inputSides.value) { inputSides = document.getElementById("inputCustom") } const inputAddition = document.getElementById("inputAddition"); const inputCount = document.getElementById("inputCount"); const inputColor = document.getElementById("inputColor"); if (history.length) { history.unshift(new DiceHistoryEntry()); } roll(new Dice(+inputSides.value, +inputAddition.value, +inputCount.value, inputColor.value)); } function rollText() { let inputText = document.getElementById("inputText"); const dicesTexts = inputText.value.split(" + "); let textDices = []; dicesTexts.forEach((diceText, i) => { let dice = new Dice(0); dice.fromText(diceText); if (!dice.color) { dice.color = default_colors[(i + dices.length) % 7]; } if (dice.sides > 1) { textDices.push(dice); } }) if (textDices.length) { if (history.length) { history.unshift(new DiceHistoryEntry()); } let f = ""; textDices.forEach((dice, i) => { f += formula(dice); if (i < textDices.length - 1) { f += " + "; } }) const result = eval(f); if (f.indexOf("+") == -1) { f = ""; } history.unshift(new DiceHistoryEntry(textDices, f, result, moment())); localStorage.setItem('history', JSON.stringify(history)); renderHistory(); } } function addSelected() { const selected = dices.filter((dice) => dice.selected); if (selected.length) { if (history.length) { history.unshift(new DiceHistoryEntry()); } let f = ""; selected.forEach((dice, i) => { f += formula(dice); if (i < selected.length - 1) { f += " + "; } }) const result = eval(f); if (f.indexOf("+") == -1) { f = ""; } history.unshift(new DiceHistoryEntry(selected, f, result, moment())); localStorage.setItem('history', JSON.stringify(history)); renderHistory(); } } function addDicesText() { let inputText = document.getElementById("inputText"); const dicesTexts = inputText.value.split(" + "); let textDices = []; dicesTexts.forEach((diceText, i) => { let dice = new Dice(0); dice.fromText(diceText); if (!dice.color) { dice.color = default_colors[(i + dices.length) % 7]; } if (dice.sides > 1) { textDices.push(dice); } }) if (textDices.length) { textDices.forEach((dice) => { dices.push(dice); }) localStorage.setItem('dices', JSON.stringify(dices)); renderDices(); } } function clearHistory() { history = []; localStorage.removeItem('history'); localStorage.removeItem('redoHistory'); renderHistory(); } function exportData() { const downloadButton = document.createElement('a'); downloadButton.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify({ dices: dices, history: history, redoHistory: redoHistory }))); downloadButton.setAttribute('download', 'rgp-dices-' + new Date().toISOString() + '.json'); document.body.appendChild(downloadButton); downloadButton.click(); document.body.removeChild(downloadButton); } function importData(event) { event.target.parentElement.classList.remove("error"); try { const reader = new FileReader(); reader.addEventListener('load', async (event) => { const data = JSON.parse(event.target.result); if (data.dices) { dices = data.dices.map((dice) => new Dice(dice.sides, dice.addition, dice.count, dice.color)); localStorage.setItem('dices', JSON.stringify(dices)); renderDices(); } if (data.history) { history = data.history.map((entry) => new DiceHistoryEntry(entry.dices && entry.dices.map((dice) => new Dice(dice.sides, dice.addition, dice.count, dice.color)) || undefined, entry.formula, entry.result, entry.time && moment(entry.time) || undefined)); localStorage.setItem('history', JSON.stringify(history)); renderHistory(); } if (data.redoHistory) { redoHistory = data.redoHistory.map((entry) => new DiceHistoryEntry(entry.dices && entry.dices.map((dice) => new Dice(dice.sides, dice.addition, dice.count, dice.color)) || undefined, entry.formula, entry.result, entry.time && moment(entry.time) || undefined)); localStorage.setItem('redoHistory', JSON.stringify(redoHistory)); } }); reader.readAsText(event.target.files[0]); } catch (e) { console.warn(e); event.target.parentElement.classList.add("error"); } } function updateCustom() { if (!document.getElementById("inputSides").value) { document.getElementById("inputCustom").classList.remove("hidden"); } else { document.getElementById("inputCustom").classList.add("hidden"); } } function toggleDarkMode() { if (darkMode) { darkMode = false; document.body.classList.remove("dark"); document.getElementById("dark-mode-icon").src = "./assets/dark.svg"; document.getElementById("dark-mode-text").innerText = "Dark Mode"; } else { darkMode = true; document.body.classList.add("dark"); document.getElementById("dark-mode-icon").src = "./assets/light.svg"; document.getElementById("dark-mode-text").innerText = "Light Mode"; } renderDices(); renderHistory(); } function undo() { while (history.length && history[0].result) { redoHistory.push(history.shift()); } if (history.length && !history[0].result) { redoHistory.push(history.shift()); } localStorage.setItem('history', JSON.stringify(history)); localStorage.setItem('redoHistory', JSON.stringify(redoHistory)); renderHistory(); } function redo() { if (redoHistory.length && !redoHistory[redoHistory.length - 1].result) { history.unshift(redoHistory.pop()); } else if (redoHistory.length) { history.unshift(new DiceHistoryEntry()); } while (redoHistory.length && redoHistory[redoHistory.length - 1].result) { history.unshift(redoHistory.pop()); } localStorage.setItem('history', JSON.stringify(history)); localStorage.setItem('redoHistory', JSON.stringify(redoHistory)); renderHistory(); } async function clearAndRefresh() { if ('caches' in window) { const keyList = await caches.keys(); await Promise.all(keyList.map(async (key) => await caches.delete(key))); } window.location.reload() } if (localStorage.getItem('dices')) { dices = JSON.parse(localStorage.getItem('dices')).map((dice) => new Dice(dice.sides, dice.addition, dice.count, dice.color)); } else { default_sides.forEach((side, i) => { dices.push(new Dice(side, 0, 1, default_colors[i])); }) } if (localStorage.getItem('history')) { history = JSON.parse(localStorage.getItem('history')).map((entry) => new DiceHistoryEntry(entry.dices && entry.dices.map((dice) => new Dice(dice.sides, dice.addition, dice.count, dice.color)) || undefined, entry.formula, entry.result, entry.time && moment(entry.time) || undefined)); } if (localStorage.getItem('redoHistory')) { redoHistory = JSON.parse(localStorage.getItem('history')).map((entry) => new DiceHistoryEntry(entry.dices && entry.dices.map((dice) => new Dice(dice.sides, dice.addition, dice.count, dice.color)) || undefined, entry.formula, entry.result, entry.time && moment(entry.time) || undefined)); } document.getElementById("importFile").addEventListener("change", importData); document.getElementById("inputText").addEventListener("keyup", (event) => { if (event.key.toUpperCase() === 'ENTER') { this.rollText(); } }); document.addEventListener("keyup", (event) => { if (!isNaN(+event.key)) { let number = +event.key; if (number == 0) { number = 10; } number--; if (number < dices.length) { if (history.length) { history.unshift(new DiceHistoryEntry()); } roll(dices[number]); } } else if (event.ctrlKey && event.key.toUpperCase() === 'Z') { undo(); } else if (event.ctrlKey && (event.shiftKey && event.key.toUpperCase() === 'Z' || event.key.toUpperCase() === 'Y')) { redo(); } }); renderDices(); renderHistory(); updateCustom(); if (darkMode) { document.body.classList.add("dark"); document.getElementById("dark-mode-icon").src = "./assets/light.svg"; document.getElementById("dark-mode-text").innerText = "Light Mode"; } if ("serviceWorker" in navigator) { window.addEventListener("load", function () { navigator.serviceWorker .register("/sw.js") .then(res => console.trace("service worker registered")) .catch(err => console.error("service worker not registered", err)) }) }