320 lines
9.7 KiB
HTML
320 lines
9.7 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>AnA Character Sheet</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<link rel="stylesheet" href="styles.css">
|
|
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=IM+Fell+English&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<header>
|
|
<div class="title">
|
|
<i class="fa-solid fa-dragon"></i>
|
|
<span>AnA Character Sheet</span>
|
|
<span id="lastSaved" class="last-saved">Last saved: —</span>
|
|
</div>
|
|
|
|
<div>
|
|
<select id="characterSelect"></select>
|
|
<button onclick="newCharacter()" title="New"><i class="fa-solid fa-user-plus"></i></button>
|
|
<button onclick="deleteCharacter()" title="Delete"><i class="fa-solid fa-skull-crossbones"></i></button>
|
|
<button onclick="exportCharacter()" title="Export"><i class="fa-solid fa-scroll"></i></button>
|
|
<button onclick="importCharacter()" title="Import"><i class="fa-solid fa-file-import"></i></button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="container">
|
|
|
|
<!-- CHARACTER INFO -->
|
|
<div class="section">
|
|
<h2 onclick="toggleSection(this)">
|
|
<i class="fa-solid fa-id-card section-icon"></i>
|
|
Character Info
|
|
<i class="fa-solid fa-chevron-down toggle-icon"></i>
|
|
</h2>
|
|
<div class="collapsible-content">
|
|
<div class="grid">
|
|
<div class="field"><label>Name</label><input id="name"></div>
|
|
<div class="field"><label>Race</label><input id="race"></div>
|
|
<div class="field"><label>Class</label><input id="class"></div>
|
|
<div class="field"><label>Background</label><input id="background"></div>
|
|
<div class="field"><label>Alignment</label><input id="alignment"></div>
|
|
<div class="field"><label>Proficiency Bonus</label><input id="proficiency" type="number" value="2"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ABILITIES -->
|
|
<div class="section">
|
|
<h2 onclick="toggleSection(this)">
|
|
<i class="fa-solid fa-dumbbell section-icon"></i>
|
|
Ability Scores
|
|
<i class="fa-solid fa-chevron-down toggle-icon"></i>
|
|
</h2>
|
|
<div id="abilities" class="collapsible-content ability-grid"></div>
|
|
</div>
|
|
|
|
<!-- COMBAT -->
|
|
<div class="section">
|
|
<h2 onclick="toggleSection(this)">
|
|
<i class="fa-solid fa-swords section-icon"></i>
|
|
Combat
|
|
<i class="fa-solid fa-chevron-down toggle-icon"></i>
|
|
</h2>
|
|
<div class="collapsible-content grid">
|
|
<div class="field"><label>Max HP</label><input id="hp_max" type="number"></div>
|
|
<div class="field"><label>Current HP</label><input id="hp_current" type="number"></div>
|
|
<div class="field"><label>Temp HP</label><input id="hp_temp" type="number"></div>
|
|
<div class="field"><label>Hit Dice</label><input id="hit_dice"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ACTIONS -->
|
|
<div class="section">
|
|
<h2 onclick="toggleSection(this)">
|
|
<i class="fa-solid fa-bolt section-icon"></i>
|
|
Actions & Abilities
|
|
<i class="fa-solid fa-chevron-down toggle-icon"></i>
|
|
</h2>
|
|
<div class="collapsible-content">
|
|
<div id="actionsList"></div>
|
|
<button onclick="addAction()">Add Ability</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- INVENTORY -->
|
|
<div class="section">
|
|
<h2 onclick="toggleSection(this)">
|
|
<i class="fa-solid fa-backpack section-icon"></i>
|
|
Inventory & Currency
|
|
<i class="fa-solid fa-chevron-down toggle-icon"></i>
|
|
</h2>
|
|
<div class="collapsible-content">
|
|
<label>Inventory</label>
|
|
<textarea id="inventory"></textarea>
|
|
<div class="grid">
|
|
<div class="field"><label>Gold</label><input id="gp" type="number"></div>
|
|
<div class="field"><label>Silver</label><input id="sp" type="number"></div>
|
|
<div class="field"><label>Copper</label><input id="cp" type="number"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SPELLS -->
|
|
<div class="section">
|
|
<h2 onclick="toggleSection(this)">
|
|
<i class="fa-solid fa-hat-wizard section-icon"></i>
|
|
Spells
|
|
<i class="fa-solid fa-chevron-down toggle-icon"></i>
|
|
</h2>
|
|
<div class="collapsible-content">
|
|
<label>Spell List</label>
|
|
<textarea id="spells"></textarea>
|
|
<div class="field"><label>Spell Slots</label><input id="spell_slots"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NOTES -->
|
|
<div class="section">
|
|
<h2 onclick="toggleSection(this)">
|
|
<i class="fa-solid fa-scroll section-icon"></i>
|
|
Notes
|
|
<i class="fa-solid fa-chevron-down toggle-icon"></i>
|
|
</h2>
|
|
<div class="collapsible-content">
|
|
<textarea id="notes"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
const abilities=[
|
|
{short:"STR",full:"Strength"},
|
|
{short:"DEX",full:"Dexterity"},
|
|
{short:"CON",full:"Constitution"},
|
|
{short:"INT",full:"Intelligence"},
|
|
{short:"WIS",full:"Wisdom"},
|
|
{short:"CHA",full:"Charisma"}
|
|
];
|
|
|
|
let currentCharacter=null;
|
|
let autosaveTimer=null;
|
|
|
|
/* SECTION TOGGLE (REAL) */
|
|
function toggleSection(header){
|
|
const content = header.nextElementSibling;
|
|
const icon = header.querySelector(".toggle-icon");
|
|
|
|
const isHidden = content.style.display === "none";
|
|
content.style.display = isHidden ? "block" : "none";
|
|
icon.style.transform = isHidden ? "rotate(0deg)" : "rotate(-90deg)";
|
|
}
|
|
|
|
/* ABILITIES */
|
|
function initAbilities(){
|
|
const c=document.getElementById("abilities");
|
|
c.innerHTML="";
|
|
abilities.forEach(s=>{
|
|
c.innerHTML+=`
|
|
<div class="ability-card">
|
|
<div class="ability-name">${s.full}</div>
|
|
<div class="ability-short">${s.short}</div>
|
|
<input type="number" id="${s.short}" value="10">
|
|
<div class="modifier" id="${s.short}_mod">+0</div>
|
|
</div>`;
|
|
});
|
|
}
|
|
|
|
function calculate(){
|
|
abilities.forEach(s=>{
|
|
const v=parseInt(document.getElementById(s.short).value)||10;
|
|
const m=Math.floor((v-10)/2);
|
|
document.getElementById(s.short+"_mod").innerText=m>=0?`+${m}`:m;
|
|
});
|
|
}
|
|
|
|
/* ACTIONS */
|
|
function addAction(data=null){
|
|
const c=document.getElementById("actionsList");
|
|
const d=document.createElement("div");
|
|
d.className="action-item";
|
|
d.innerHTML=`
|
|
<label>Name</label><input class="action-name" value="${data?.name||""}">
|
|
<label>Type</label>
|
|
<select class="action-type">
|
|
<option>Action</option><option>Bonus Action</option>
|
|
<option>Reaction</option><option>Passive</option>
|
|
</select>
|
|
<label>Attack Bonus</label><input class="action-bonus" value="${data?.bonus||""}">
|
|
<label>Damage</label><input class="action-damage" value="${data?.damage||""}">
|
|
<label>Description</label><textarea class="action-desc">${data?.desc||""}</textarea>
|
|
<button onclick="confirmRemoveAction(this)">
|
|
<i class="fa-solid fa-trash-can"></i>
|
|
</button>`;
|
|
c.appendChild(d);
|
|
}
|
|
|
|
function confirmRemoveAction(btn){
|
|
if(confirm("Remove this ability?")){
|
|
btn.parentElement.remove();
|
|
saveCharacter();
|
|
}
|
|
}
|
|
|
|
/* SAVE / LOAD */
|
|
function saveCharacter(){
|
|
if(!currentCharacter) return;
|
|
const data={};
|
|
document.querySelectorAll("input[id], textarea[id]").forEach(e=>data[e.id]=e.value);
|
|
data.actions=[];
|
|
document.querySelectorAll(".action-item").forEach(i=>{
|
|
data.actions.push({
|
|
name:i.querySelector(".action-name").value,
|
|
type:i.querySelector(".action-type").value,
|
|
bonus:i.querySelector(".action-bonus").value,
|
|
damage:i.querySelector(".action-damage").value,
|
|
desc:i.querySelector(".action-desc").value
|
|
});
|
|
});
|
|
const now=new Date().toLocaleString();
|
|
data._lastSaved=now;
|
|
localStorage.setItem("char_"+currentCharacter,JSON.stringify(data));
|
|
document.getElementById("lastSaved").innerText="Last saved: "+now;
|
|
}
|
|
|
|
function loadCharacter(n){
|
|
const d=JSON.parse(localStorage.getItem("char_"+n));
|
|
if(!d) return;
|
|
currentCharacter=n;
|
|
document.querySelectorAll("input[id], textarea[id]").forEach(e=>{
|
|
if(d[e.id]!=null) e.value=d[e.id];
|
|
});
|
|
document.getElementById("actionsList").innerHTML="";
|
|
d.actions?.forEach(a=>addAction(a));
|
|
document.getElementById("lastSaved").innerText="Last saved: "+(d._lastSaved||"—");
|
|
startAutosave();
|
|
calculate();
|
|
}
|
|
|
|
/* AUTOSAVE */
|
|
function startAutosave(){
|
|
clearInterval(autosaveTimer);
|
|
autosaveTimer=setInterval(()=>currentCharacter&&saveCharacter(),5000);
|
|
}
|
|
|
|
/* CHARACTER CRUD */
|
|
function newCharacter(){
|
|
const n=prompt("Character name?");
|
|
if(!n) return;
|
|
localStorage.setItem("char_"+n,"{}");
|
|
updateDropdown();
|
|
loadCharacter(n);
|
|
}
|
|
|
|
function deleteCharacter(){
|
|
if(!currentCharacter||!confirm("Delete this character permanently?")) return;
|
|
localStorage.removeItem("char_"+currentCharacter);
|
|
currentCharacter=null;
|
|
clearInterval(autosaveTimer);
|
|
document.getElementById("lastSaved").innerText="Last saved: —";
|
|
updateDropdown();
|
|
}
|
|
|
|
/* EXPORT / IMPORT */
|
|
function exportCharacter(){
|
|
if(!currentCharacter) return;
|
|
const data=localStorage.getItem("char_"+currentCharacter);
|
|
const blob=new Blob([data],{type:"application/json"});
|
|
const a=document.createElement("a");
|
|
a.href=URL.createObjectURL(blob);
|
|
a.download=currentCharacter+".json";
|
|
a.click();
|
|
}
|
|
|
|
function importCharacter(){
|
|
const input=document.createElement("input");
|
|
input.type="file";
|
|
input.accept=".json";
|
|
input.onchange=e=>{
|
|
const reader=new FileReader();
|
|
reader.onload=()=>{
|
|
const n=prompt("Name for imported character?");
|
|
if(!n) return;
|
|
localStorage.setItem("char_"+n,reader.result);
|
|
updateDropdown();
|
|
};
|
|
reader.readAsText(e.target.files[0]);
|
|
};
|
|
input.click();
|
|
}
|
|
|
|
/* DROPDOWN */
|
|
function updateDropdown(){
|
|
const s=document.getElementById("characterSelect");
|
|
s.innerHTML="";
|
|
for(let i=0;i<localStorage.length;i++){
|
|
const k=localStorage.key(i);
|
|
if(k.startsWith("char_")){
|
|
const o=document.createElement("option");
|
|
o.value=o.textContent=k.replace("char_","");
|
|
s.appendChild(o);
|
|
}
|
|
}
|
|
}
|
|
document.getElementById("characterSelect").onchange=e=>loadCharacter(e.target.value);
|
|
|
|
/* INIT */
|
|
initAbilities();
|
|
updateDropdown();
|
|
document.addEventListener("input",calculate);
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|