574 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, height=device-height, initial-scale: 1">
<title>Cypher Player Assistant</title>
<link rel="stylesheet" type="text/css" href="static/reset.css">
<style>
:root {
/* Dark Color Scheme */
--dark-bg: #111111;
--dark-text: #eeeeee;
--dark-box-border: #333333;
--dark-hl-box-border: #cccccc;
/* Light Color Scheme */
--light-bg: #eeeeee;
--light-text: #111111;
--light-box-border: #cccccc;
--light-hl-box-border: #333333;
/* Defaults */
--bg: var(--light-bg);
--text: var(--light-text);
--box-border: var(--light-box-border);
--hl-box-border: var(--light-hl-box-border);
}
#color-scheme {
display: none;
}
#color-scheme:checked ~ .color-scheme-wrapper {
--bg: var(--dark-bg);
--text: var(--dark-text);
--box-border: var(--dark-box-border);
--hl-box-border: var(--dark-hl-box-border);
}
#color-scheme:checked ~ .color-scheme-wrapper .dark-mode-hide {
display: none;
}
#color-scheme:checked ~ .color-scheme-wrapper .light-mode-hide {
display: initial;
}
.dark-mode-hide {
display: initial;
}
.light-mode-hide {
display: none;
}
@media(prefers-color-scheme: dark) {
:root {
--bg: var(--dark-bg);
--text: var(--dark-text);
--box-border: var(--dark-box-border);
--hl-box-border: var(--dark-hl-box-border);
}
#color-scheme:checked ~ .color-scheme-wrapper {
--bg: var(--light-bg);
--text: var(--light-text);
--box-border: var(--light-box-border);
--hl-box-border: var(--light-hl-box-border);
}
#color-scheme:checked ~ .color-scheme-wrapper .dark-mode-hide {
display: initial;
}
#color-scheme:checked ~ .color-scheme-wrapper .light-mode-hide {
display: none;
}
.dark-mode-hide {
display: none;
}
.light-mode-hide {
display: initial;
}
}
/* Actual page style */
.color-scheme-wrapper {
min-height: 100vh;
background: var(--bg);
color: var(--text);
}
#color-scheme-changer {
position: fixed;
text-align: center;
top: 0;
right: 0;
font-size: 150%;
width: 1.5em;
cursor: pointer;
}
.color-scheme-toggle {
cursor: pointer;
}
.color-scheme-toggle span {
cursor: pointer;
}
.dark-mode-hide, .light-mode-hide {
cursor: pointer;
}
.dark-mode-hide {
background-color: var(--dark-bg);
color: var(--dark-text);
}
.light-mode-hide {
background-color: var(--light-bg);
color: var(--light-text);
}
body {
background-color: var(--bg);
color: var(--text);
font-family: sans-serif;
}
h1 {
width: 100%;
font-family: serif;
text-align: center;
}
#no-local-storage {
display: none;
color: red;
}
.box {
width: 80%;
border-width: 2px;
border-style: solid;
border-color: var(--box-border);
border-radius: 10px;
margin: 0 auto 1em auto;
padding: 1em;
}
.box h2 {
color: var(--box-border);
font-size: 100%;
font-weight: normal;
display: initial;
background-color: var(--bg);
position: relative;
top: -1.8em;
padding: 0 .5em;
}
.box h2::after {
display: block;
content: "";
margin-bottom: -1.5em;
}
input {
border: 1px solid var(--hl-box-border);
background-color: var(--bg);
color: var(--text);
width: 4em;
}
div.or {
font-weight: bold;
text-transform: uppercase;
margin: .5em 0;
}
#character-roster-container {
text-align: center;
}
#no-loaded-container {
text-align: center;
}
#character-container {
display: none;
}
#character-id-container {
margin: -2em 0 .5em 0;
color: var(--box-border);
font-size: 75%;
}
#character-id-container::before {
content: "ID: ";
}
.pool-container {
display: inline-block;
border-width: 2px;
border-style: solid;
border-color: var(--box-border);
border-radius: 10px;
width: 32%;
}
.pool-container:has(> h3 > input[name="pool-selector"]:checked) {
border-color: var(--hl-box-border);
}
.pool-container h4 {
color: var(--hl-box-border);
font-weight: normal;
font-size: 80%;
}
input[name="pool-selector"] {
display: none;
}
.input-unlocker {
display: none;
}
.input-unlocker + label > .input-locked {
display: initial;
cursor: pointer;
}
.input-unlocker + label > .input-unlocked {
display: none;
cursor: pointer;
}
.input-unlocker:checked + label > .input-locked {
display: none;
}
.input-unlocker:checked + label .input-unlocked {
display: initial;
}
#btn-load-character {
display: none;
}
#btn-create-character {
display: none;
}
</style>
</head>
<body>
<input id="color-scheme" type="checkbox">
<div class="color-scheme-wrapper">
<div id="color-scheme-changer">
<label for="color-scheme" id="color-scheme-toggle">
<span class="dark-mode-hide"></span>
<span class="light-mode-hide"></span>
</label>
</div>
<h1>
Cypher Player Assistant
<span id="no-local-storage" title="Local Storage is not available. You can import/export data, but the app wont save it between sessions!">!</span>
</h1>
<div id="character-roster-container" class="box">
<select id="character-roster"><option></option></select>
<button id="btn-load-character">Load Character</button>
<button id="btn-create-character">Create Character</button>
</div>
<div id="no-loaded-container" class="box">
No character is loaded. Load one from the menu above
<div class="or">— or —</div>
<button id="btn-no-char-create-character">Create one</button>
</div>
<div id="character-container">
<div id="character-base-data-container" class="box">
<h2>Character data</h2>
<div id="character-id-container"></div>
<strong>Campaign</strong> <input type="text" id="campaign-name">
<strong>Name</strong> <input type="text" id="character-name">
<strong>Max Effort</strong> <input type="number" id="max-effort" min="1" value="1">
</div>
<div id="abilities-container" class="box">
<h2>Abilities</h2>
<div class="pool-container">
<h3>
<input id="pool-selector-might" type="radio" name="pool-selector" value="might">
<label for="pool-selector-might">Might</label>
</h3>
<h4>Pool</h4>
<input type="number" id="pool-value-might" min="0" value="0">
/
<input type="number" id="pool-max-might" min="0" value="0" disabled>
<input type="checkbox" class="input-unlocker" id="pool-max-unlocker-might">
<label for="pool-max-unlocker-might">
<span class="input-locked">🔒</span>
<span class="input-unlocked">🔓</span>
</label>
<h4>Edge</h4>
<input type="number" id="pool-edge-might" min="0" value="0" disabled>
<input type="checkbox" class="input-unlocker" id="pool-edge-unlocker-might">
<label id="pool-edge-unlocker-label-might" for="pool-edge-unlocker-might">
<span class="input-locked">🔒</span>
<span class="input-unlocked">🔓</span>
</label>
</div>
<div class="pool-container">
<h3>
<input id="pool-selector-speed" type="radio" name="pool-selector" value="speed">
<label for="pool-selector-speed">Speed</label>
</h3>
<h4>Pool</h4>
<input type="number" id="pool-value-speed" min="0" value="0">
/
<input type="number" id="pool-max-speed" min="0" value="0" disabled>
<input type="checkbox" class="input-unlocker" id="pool-max-unlocker-speed">
<label for="pool-max-unlocker-speed">
<span class="input-locked">🔒</span>
<span class="input-unlocked">🔓</span>
</label>
<h4>Edge</h4>
<input type="number" id="pool-edge-speed" min="0" value="0" disabled>
<input type="checkbox" class="input-unlocker" id="pool-edge-unlocker-speed">
<label id="pool-edge-unlocker-label-speed" for="pool-edge-unlocker-speed">
<span class="input-locked">🔒</span>
<span class="input-unlocked">🔓</span>
</label>
</div>
<div class="pool-container">
<h3>
<input id="pool-selector-intellect" type="radio" name="pool-selector" value="intellect">
<label for="pool-selector-intellect">Intellect</label>
</h3>
<h4>Pool</h4>
<input type="number" id="pool-value-intellect" min="0" value="0">
/
<input type="number" id="pool-max-intellect" min="0" value="0" disabled>
<input type="checkbox" class="input-unlocker" id="pool-max-unlocker-intellect">
<label for="pool-max-unlocker-intellect">
<span class="input-locked">🔒</span>
<span class="input-unlocked">🔓</span>
</label>
<h4>Edge</h4>
<input type="number" id="pool-edge-intellect" min="0" value="0" disabled>
<input type="checkbox" class="input-unlocker" id="pool-edge-unlocker-intellect">
<label id="pool-edge-unlocker-label-intellect" for="pool-edge-unlocker-intellect">
<span class="input-locked">🔒</span>
<span class="input-unlocked">🔓</span>
</label>
</div>
</div>
</div>
</div>
<script>
const characterIDChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const btnNoCharCreateCharacter = document.getElementById("btn-no-char-create-character");
const containerNoCharacter = document.getElementById("no-loaded-container");
const containerCharacter = document.getElementById("character-container");
const containerCharacterID = document.getElementById("character-id-container");
const inputCharacterName = document.getElementById("character-name");
const inputCampaignName = document.getElementById("campaign-name");
const inputMaxEffort = document.getElementById("max-effort");
const characterSelector = document.getElementById("character-roster");
var characterRoster = {};
var currentCharacter = null;
const checkLocalStorage = () => {
let storage;
try {
storage = window.localStorage;
const x = "__storage_test__";
storage.setItem(x, x);
storage.removeItem(x);
return true;
} catch (e) {
return (
e instanceof DOMException &&
e.name === "QuotaExceededError" &&
// acknowledge QuotaExceededError only if there's something already stored
storage &&
storage.length !== 0
);
}
};
const generateID = () => {
var result = "";
for (var i = 0; i < 16; i++) {
result += characterIDChars.charAt(Math.floor(Math.random() * characterIDChars.length));
}
return result;
};
const loadCharacter = (characterID) => {
containerNoCharacter.style.display = "none";
containerCharacter.style.display = "initial";
characterData = characterRoster[characterID] || {};
containerCharacterID.textContent = characterID;
inputCharacterName.value = characterData.name || "";
inputCampaignName.value = characterData.campaign || "";
inputMaxEffort.value = characterData.max_effort || 1;
};
const createCharacter = () => {
let newID = generateID();
characterRoster[newID] = {};
currentCharacter = newID;
loadCharacter(newID);
};
const toggleInputLockedHandler = (evt) => {
let cb = evt.target;
let pool = cb.id.slice(cb.id.lastIndexOf("-") + 1);
let input = cb.previousElementSibling;
let toUnlock = cb.checked;
input.disabled = !toUnlock;
};
const saveCharacterRoster = () => {
if (!checkLocalStorage()) {
alert("Local Storage is not available, cannot save roster.");
return;
}
localStorage.setItem("character-roster", JSON.stringify())
};
const loadCharacterRoster = (noSkipLocalStorageCheck) => {
if (!noSkipLocalStorageCheck && !checkLocalStorage()) {
alert("Local storage is not available, cannot load roster.")
}
var newRoster = localStorage.getItem("character-roster");
characterRoster = (newRoster === null) ? {} : JSON.parse(newRoster);
characterRoster = {
deadBeef: {
campaign: "Campaign One",
name: "Character One",
},
otherChr: {
campaign: "Campaign Two",
name: "Character Two",
},
thirdChr: {
campaign: "Campaign One",
name: "Character Three",
}
}
while (characterSelector.firstChild) {
characterSelector.removeChild(characterSelector.lastChild);
}
let placeHolder = document.createElement("option");
placeHolder.appendChild(document.createTextNode("Choose a character to load"));
characterSelector.appendChild(placeHolder);
let characterList = new Array();
for (let [characterID, characterData] of Object.entries(characterRoster)) {
if (characterRoster.hasOwnProperty(characterID)) {
characterList.push([characterID, characterData.campaign, characterData.name]);
}
}
if (characterList.length == 0) {
return;
}
characterList.sort((a, b) => {
const campaignA = a[1].toUpperCase();
const campaignB = b[1].toUpperCase();
const nameA = a[2].toUpperCase();
const nameB = b[2].toUpperCase();
// Sort by campaign name first
if (campaignA < campaignB) {
return -1;
}
if (campaignA > campaignB) {
return 1;
}
// Sort by character name within the same campaign
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// If all else fails, sort by the character ID
if (a[0] < b[0]) {
return -1;
}
if (a[0] > b[0]) {
return 1;
}
return 0;
});
let currentGroup = null;
let previousCampaign = null;
characterList.forEach(([characterID, campaignName, characterName]) => {
if ((previousCampaign !== null) && (previousCampaign !== campaignName)) {
console.log("Found a new campaign", campaignName);
characterSelector.appendChild(currentGroup);
currentGroup = null;
}
if (currentGroup === null) {
currentGroup = document.createElement("optgroup");
currentGroup.label = campaignName;
}
let newOption = document.createElement("option");
newOption.value = characterID;
newOption.appendChild(document.createTextNode(characterName));
currentGroup.appendChild(newOption);
previousCampaign = campaignName;
});
if (currentGroup !== null) {
characterSelector.appendChild(currentGroup);
}
};
document.addEventListener(
"DOMContentLoaded",
() => {
if (!checkLocalStorage()) {
document.getElementById("no-local-storage").style.display = "initial";
alert("Local Storage is not available, saving and loading data will be unavailable.");
}
loadCharacterRoster(true);
},
);
btnNoCharCreateCharacter.addEventListener("click", createCharacter);
document
.querySelectorAll(".input-unlocker")
.forEach((elem) => {elem.addEventListener("change", toggleInputLockedHandler)});
</script>
</body>
</html>