diff --git a/assets/add.svg b/assets/add.svg
new file mode 100644
index 0000000..b7b9ead
--- /dev/null
+++ b/assets/add.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/close.svg b/assets/close.svg
new file mode 100644
index 0000000..989837c
--- /dev/null
+++ b/assets/close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/dark.svg b/assets/dark.svg
new file mode 100644
index 0000000..49e0f57
--- /dev/null
+++ b/assets/dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/dices.svg b/assets/dices.svg
new file mode 100644
index 0000000..efd2e89
--- /dev/null
+++ b/assets/dices.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/dices/10.svg b/assets/dices/10.svg
new file mode 100755
index 0000000..8a1a234
--- /dev/null
+++ b/assets/dices/10.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/assets/dices/100.svg b/assets/dices/100.svg
new file mode 100755
index 0000000..898ef0e
--- /dev/null
+++ b/assets/dices/100.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/assets/dices/12.svg b/assets/dices/12.svg
new file mode 100755
index 0000000..974d316
--- /dev/null
+++ b/assets/dices/12.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/assets/dices/20.svg b/assets/dices/20.svg
new file mode 100755
index 0000000..c1e60b2
--- /dev/null
+++ b/assets/dices/20.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/assets/dices/4.svg b/assets/dices/4.svg
new file mode 100755
index 0000000..9f58e51
--- /dev/null
+++ b/assets/dices/4.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/assets/dices/6.svg b/assets/dices/6.svg
new file mode 100755
index 0000000..f2495cc
--- /dev/null
+++ b/assets/dices/6.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/assets/dices/8.svg b/assets/dices/8.svg
new file mode 100755
index 0000000..b55084c
--- /dev/null
+++ b/assets/dices/8.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/assets/dices/custom.svg b/assets/dices/custom.svg
new file mode 100644
index 0000000..e60fd55
--- /dev/null
+++ b/assets/dices/custom.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/assets/export.svg b/assets/export.svg
new file mode 100644
index 0000000..e40eaee
--- /dev/null
+++ b/assets/export.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/history.svg b/assets/history.svg
new file mode 100644
index 0000000..3f7c900
--- /dev/null
+++ b/assets/history.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/import.svg b/assets/import.svg
new file mode 100644
index 0000000..ee50f7e
--- /dev/null
+++ b/assets/import.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/light.svg b/assets/light.svg
new file mode 100644
index 0000000..6eda6ed
--- /dev/null
+++ b/assets/light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/menu.svg b/assets/menu.svg
new file mode 100644
index 0000000..16fbd60
--- /dev/null
+++ b/assets/menu.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/roll.svg b/assets/roll.svg
new file mode 100644
index 0000000..f338e42
--- /dev/null
+++ b/assets/roll.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100755
index 0000000..f61f04f
--- /dev/null
+++ b/index.html
@@ -0,0 +1,72 @@
+
+
+
+
RPG Dices
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/js/libs/sidebar/sidebar.css b/js/libs/sidebar/sidebar.css
new file mode 100644
index 0000000..2f80227
--- /dev/null
+++ b/js/libs/sidebar/sidebar.css
@@ -0,0 +1,41 @@
+/* sidebar */
+#sidebar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100%;
+ transition: 0.4s;
+ overflow: hidden;
+ z-index: 10;
+ }
+
+ .sidebar-toggle {
+ cursor: pointer;
+ font-size: 2em;
+ padding: 5px;
+ }
+
+ #sidebar-toggle {
+ position: fixed;
+ top: 0;
+ z-index: 1;
+ }
+
+ #sidebar-toggle-input {
+ display: none;
+ }
+
+ #sidebar-toggle-input:not(:checked) ~ #sidebar {
+ width: 0px;
+ }
+
+ #sidebar-toggle-input:checked ~ #sidebar {
+ width: 250px;
+ }
+
+ @media screen and (max-width: 767px) {
+ #sidebar-toggle-input:checked ~ #sidebar {
+ width: 100%;
+ }
+ }
+
\ No newline at end of file
diff --git a/js/libs/sidebar/sidebar.js b/js/libs/sidebar/sidebar.js
new file mode 100644
index 0000000..6f06d37
--- /dev/null
+++ b/js/libs/sidebar/sidebar.js
@@ -0,0 +1,17 @@
+window.Sidebar = function () {
+ let sidebar = document.querySelector("#sidebar");
+ let close = document.querySelector("#sidebar-toggle-close");
+ let toggle = document.querySelector("#sidebar-toggle-input");
+
+ sidebar.addEventListener("click", function (event) {
+ if (document.body.clientWidth < 1280) {
+ toggle.checked = false;
+ }
+ })
+
+ close.addEventListener("click", function (event) {
+ if (document.body.clientWidth < 1280) {
+ event.preventDefault();
+ }
+ })
+ }
\ No newline at end of file
diff --git a/script.js b/script.js
new file mode 100755
index 0000000..892dfb1
--- /dev/null
+++ b/script.js
@@ -0,0 +1,318 @@
+class Dice {
+ constructor(sides, addition = 0, count = 1, color = "#000") {
+ this.sides = sides;
+ this.addition = addition;
+ this.count = count;
+ this.color = color;
+ this.selected = false;
+ }
+}
+
+class DiceHistoryEntry {
+ constructor(dice = undefined, formula = undefined, result = undefined) {
+ this.dice = dice;
+ this.formula = formula;
+ this.result = result;
+ }
+}
+
+const default_sides = [4, 6, 8, 10, 12, 20, 100];
+const default_colors = ["#de324c", "#f4895f", "#f8e16f", "#95cf92", "#369acc", "#9656a2", "#000000"];
+
+let darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+
+let dices = [];
+let history = [];
+
+let dicesContainer = document.getElementById("dices");
+let historyContainer = document.getElementById("history");
+
+function getDiceLabel(dice) {
+ return (dice.count > 1 ? dice.count : "") + "D" + dice.sides + (dice.addition ? (dice.addition < 0 ? "-" : "+") + Math.abs(dice.addition) : "");
+}
+
+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");
+ } else {
+ document.getElementById('roll-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 = getDiceLabel(dice);
+
+ 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");
+ } else {
+ document.getElementById('roll-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");
+ } else {
+ document.getElementById('history-button').classList.add("disabled");
+ }
+ history.forEach((entry) => {
+ if (entry.dice && entry.result) {
+ const entryElement = document.createElement("div");
+ entryElement.classList.add("entry");
+ const diceLabel = document.createElement("label");
+ diceLabel.innerHTML = getDiceLabel(entry.dice) + ": ";
+ diceLabel.style.color = entry.dice.color;
+ // revert b/w on dark-mode
+ if (darkMode) {
+ if (["#000", "#000000", "black", "rgb(0,0,0)", "rgb(0, 0, 0)"].indexOf(entry.dice.color) != -1) {
+ diceLabel.style.color = "#fff";
+ } else if (["#fff", "#ffffff", "white", "rgb(255,255,255)", "rgb(255, 255, 255)"].indexOf(entry.dice.color) != -1) {
+ diceLabel.style.color = "#000";
+ }
+ }
+ entryElement.appendChild(diceLabel);
+
+ const diceResult = document.createElement("span");
+ diceResult.innerText = entry.result;
+ entryElement.appendChild(diceResult);
+
+ const diceFormula = document.createElement("span");
+ const diceEqual = document.createElement("span");
+ entryElement.appendChild(diceEqual);
+ entryElement.appendChild(diceFormula);
+
+ if (entry.formula) {
+ diceFormula.innerText = entry.formula;
+ diceEqual.innerText = " = "
+ }
+
+ historyContainer.appendChild(entryElement);
+ } else {
+ historyContainer.appendChild(document.createElement("hr"));
+ }
+ })
+}
+
+function roll(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;
+ }
+ const result = eval(formula);
+ if (formula.indexOf("+") == -1) {
+ formula = "";
+ }
+ history.unshift(new DiceHistoryEntry(dice, formula, result));
+ localStorage.setItem('history', JSON.stringify(history));
+ renderHistory();
+}
+
+function rollSelected() {
+ if (dices.filter((dice) => dice.selected).length) {
+ if (history.length) {
+ history.unshift(new DiceHistoryEntry());
+ }
+ dices.forEach((dice) => {
+ if (dice.selected) {
+ roll(dice);
+ }
+ })
+ }
+}
+
+function clearHistory() {
+ history = [];
+ localStorage.removeItem('history');
+ renderHistory();
+}
+
+function exportData() {
+ const downloadButton = document.createElement('a');
+ downloadButton.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify({ dices: dices, history: history })));
+ 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;
+ renderDices();
+ }
+ if (data.history) {
+ history = data.history;
+ renderHistory();
+ }
+ });
+
+ 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();
+}
+
+if (localStorage.getItem('dices')) {
+ dices = JSON.parse(localStorage.getItem('dices'));
+} 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'));
+}
+
+document.getElementById("importFile").addEventListener("change", importData);
+
+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";
+}
\ No newline at end of file
diff --git a/style.css b/style.css
new file mode 100755
index 0000000..f477378
--- /dev/null
+++ b/style.css
@@ -0,0 +1,290 @@
+* {
+ box-sizing: border-box;
+}
+
+
+body {
+ background-color: #fff;
+ color: #000;
+}
+
+.hidden {
+ display: none;
+}
+
+#sidebar {
+ white-space: nowrap;
+ background-color: #fff;
+}
+
+.menu {
+ margin: 0;
+ padding: 0;
+ margin-top: 1.5em;
+ margin-left: 0.5em;
+ list-style: none;
+}
+
+.menu input[type="file"] {
+ width: 0;
+ height: 0;
+ margin: 0;
+ padding: 0;
+ visibility: hidden;
+}
+
+.menu li {
+ cursor: pointer;
+ display: flex;
+ justify-content: start;
+ align-items: center;
+ margin: 0.4em 0;
+}
+
+.menu li:hover {
+ opacity: 0.7;
+}
+
+.menu li label {
+ cursor: pointer;
+ display: flex;
+ justify-content: start;
+ align-items: center;
+}
+
+.menu li img {
+ height: 1.2em;
+ width: auto;
+ margin-right: 0.2em;
+}
+
+.container,
+.dices-container,
+.history-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ max-width: 100%;
+}
+
+.dices {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-wrap: wrap;
+ min-height: 12em;
+}
+
+.dice {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 10em;
+ height: 12em;
+ margin: 0 0.2em;
+ border: 0.3em solid transparent;
+}
+
+.dice .label-container {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 0.2em;
+ z-index: 2;
+ height: 2em;
+}
+
+.dice label {
+ font-size: 1em;
+ opacity: 0.7;
+}
+
+.dice.selected label {
+ font-size: 1.2em;
+ font-weight: bold;
+ opacity: 1;
+}
+
+.dice .dice-image-container {
+ position: relative;
+ width: 8em;
+ height: 8em;
+ z-index: 1;
+ filter: drop-shadow(0.1em 0.1em 0.1em transparent) drop-shadow(-0.1em -0.1em 0.1em transparent);
+}
+
+.dice .dice-image {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ mask-size: contain;
+ mask-repeat: no-repeat;
+ mask-position: center;
+}
+
+.dice.selected .dice-image-container {
+ filter: drop-shadow(0.1em 0.1em 0.1em #333) drop-shadow(-0.1em -0.1em 0.1em #333);
+}
+
+.dice .remove {
+ cursor: pointer;
+ position: absolute;
+ top: 0.2em;
+ right: 0.2em;
+ height: 1.2em;
+ width: 1.2em;
+ z-index: 5;
+}
+
+.dice .remove::before {
+ content: ' ';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #DD0000;
+ mask-size: contain;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-image: url(./assets/close.svg);
+}
+
+
+.dice .remove:hover {
+ filter: brightness(0.5);
+}
+
+.dice .dice-image.dice-image-4 {
+ mask-image: url(./assets/dices/4.svg);
+}
+
+.dice .dice-image.dice-image-6 {
+ mask-image: url(./assets/dices/6.svg);
+}
+
+.dice .dice-image.dice-image-8 {
+ mask-image: url(./assets/dices/8.svg);
+}
+
+.dice .dice-image.dice-image-10 {
+ mask-image: url(./assets/dices/10.svg);
+}
+
+.dice .dice-image.dice-image-12 {
+ mask-image: url(./assets/dices/12.svg);
+}
+
+.dice .dice-image.dice-image-20 {
+ mask-image: url(./assets/dices/20.svg);
+}
+
+.dice .dice-image.dice-image-100 {
+ mask-image: url(./assets/dices/100.svg);
+}
+
+.dice .dice-image.dice-image-custom {
+ mask-image: url(./assets/dices/custom.svg);
+}
+
+.form {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: center;
+ margin-top: 1em;
+}
+
+.form input,
+.form select {
+ width: 5em;
+}
+
+.history-container {
+ margin-top: 2em;
+ width: 400px;
+}
+
+.history-container .menu {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.history {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ margin-top: 2em;
+}
+
+.history .entry {
+ margin: 0.3em 0;
+ display: grid;
+ grid-template-columns: 1fr 1fr 0.5fr 5fr;
+}
+
+.history hr {
+ width: 100%;
+}
+
+button {
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: transparent;
+ color: #000;
+ border: none;
+ outline: none;
+ font-size: 1.3em;
+}
+
+button:hover {
+ opacity: 0.7;
+}
+
+button img {
+ height: 1.2em;
+ width: auto;
+ margin-right: 0.2em;
+}
+
+button.disabled {
+ pointer-events: none;
+ opacity: 0.5;
+}
+
+body.dark {
+ background-color: #000;
+ color: #efefef;
+}
+
+body.dark #sidebar {
+ background-color: #000;
+}
+
+body.dark #sidebar-toggle,
+body.dark #sidebar-toggle-close,
+body.dark .menu li img,
+body.dark button img {
+ filter: invert();
+}
+
+body.dark button {
+ color: #fff;
+}
+
+body.dark .dice.selected .dice-image-container {
+ filter: drop-shadow(0.1em 0.1em 0.1em #efefef) drop-shadow(-0.1em -0.1em 0.1em #efefef);
+}
+
+@media screen and (max-width: 720px) {
+ .dices {
+ font-size: 0.65em;
+ }
+}
\ No newline at end of file