1179 lines
47 KiB
HTML
1179 lines
47 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>.puSave Editor</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,400&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--bg: #0e0f11;
|
|
--surface: #151618;
|
|
--surface2: #1c1e21;
|
|
--surface3: #232629;
|
|
--border: #2a2d32;
|
|
--text: #e8eaed;
|
|
--muted: #6b7280;
|
|
--accent: #ff6b35;
|
|
--accent2: #ffd166;
|
|
--accent3: #06d6a0;
|
|
--danger: #ef4444;
|
|
--radius: 6px;
|
|
}
|
|
|
|
html, body { height: 100%; }
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: 'DM Sans', sans-serif;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* NOISE OVERLAY */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
inset: 0;
|
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E");
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
}
|
|
|
|
/* HEADER */
|
|
header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 14px 28px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--surface);
|
|
flex-shrink: 0;
|
|
gap: 16px;
|
|
}
|
|
.logo {
|
|
font-family: 'Bebas Neue', sans-serif;
|
|
font-size: 1.5rem;
|
|
letter-spacing: 0.08em;
|
|
color: var(--text);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.logo span { color: var(--accent); }
|
|
.logo-sub {
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.6rem;
|
|
color: var(--muted);
|
|
letter-spacing: 0.2em;
|
|
text-transform: uppercase;
|
|
border: 1px solid var(--border);
|
|
padding: 2px 7px;
|
|
border-radius: 3px;
|
|
}
|
|
.header-right { display: flex; align-items: center; gap: 10px; }
|
|
#filename-display {
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.68rem;
|
|
color: var(--muted);
|
|
max-width: 220px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* BUTTONS */
|
|
.btn {
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.72rem;
|
|
letter-spacing: 0.05em;
|
|
padding: 8px 16px;
|
|
border-radius: var(--radius);
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
white-space: nowrap;
|
|
}
|
|
.btn-primary { background: var(--accent); color: #fff; font-weight: 500; }
|
|
.btn-primary:hover { background: #ff8255; }
|
|
.btn-ghost { background: transparent; color: var(--muted); border: 1px solid var(--border); }
|
|
.btn-ghost:hover { background: var(--surface2); color: var(--text); }
|
|
.btn-danger { background: transparent; color: var(--danger); border: 1px solid rgba(239,68,68,0.3); font-size: 0.65rem; padding: 5px 10px; }
|
|
.btn-danger:hover { background: rgba(239,68,68,0.1); }
|
|
.btn-add { background: transparent; color: var(--accent3); border: 1px dashed rgba(6,214,160,0.35); font-size: 0.68rem; padding: 6px 12px; width: 100%; margin-top: 8px; }
|
|
.btn-add:hover { background: rgba(6,214,160,0.07); }
|
|
.btn-sm { padding: 4px 10px; font-size: 0.65rem; }
|
|
|
|
/* DROP ZONE */
|
|
#drop-zone {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
padding: 40px;
|
|
text-align: center;
|
|
}
|
|
.drop-ring {
|
|
width: 120px; height: 120px;
|
|
border-radius: 50%;
|
|
border: 2px solid var(--border);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 2.8rem;
|
|
transition: all 0.2s;
|
|
background: var(--surface);
|
|
position: relative;
|
|
}
|
|
.drop-ring::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: -8px;
|
|
border-radius: 50%;
|
|
border: 1px dashed var(--border);
|
|
animation: spin 12s linear infinite;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
#drop-zone.dragover .drop-ring { border-color: var(--accent); background: rgba(255,107,53,0.05); }
|
|
#drop-zone h2 { font-family: 'Bebas Neue', sans-serif; font-size: 2rem; letter-spacing: 0.1em; color: var(--text); }
|
|
#drop-zone p { font-family: 'DM Mono', monospace; font-size: 0.78rem; color: var(--muted); }
|
|
#file-input { display: none; }
|
|
|
|
/* EDITOR */
|
|
#editor { display: none; flex: 1; flex-direction: column; min-height: 0; }
|
|
.editor-layout { display: flex; flex: 1; min-height: 0; overflow: hidden; }
|
|
|
|
/* SIDEBAR */
|
|
.sidebar {
|
|
width: 220px;
|
|
min-width: 140px;
|
|
max-width: 400px;
|
|
border-right: 1px solid var(--border);
|
|
background: var(--surface);
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
padding: 12px 0 24px;
|
|
flex-shrink: 0;
|
|
height: 100%;
|
|
}
|
|
.resize-handle {
|
|
width: 5px;
|
|
cursor: col-resize;
|
|
background: transparent;
|
|
flex-shrink: 0;
|
|
transition: background 0.15s;
|
|
}
|
|
.resize-handle:hover, .resize-handle.dragging { background: var(--accent); opacity: 0.4; }
|
|
|
|
.sidebar-section { margin-bottom: 2px; }
|
|
.sidebar-group-label {
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.58rem;
|
|
font-weight: 500;
|
|
letter-spacing: 0.18em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
padding: 10px 16px 4px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
user-select: none;
|
|
transition: color 0.15s;
|
|
}
|
|
.sidebar-group-label:hover { color: var(--text); }
|
|
.sidebar-group-label .schev {
|
|
font-size: 0.5rem;
|
|
transition: transform 0.2s;
|
|
opacity: 0.4;
|
|
}
|
|
.sidebar-group-label .schev.closed { transform: rotate(-90deg); }
|
|
.sidebar-children-wrap { overflow: hidden; max-height: 2000px; transition: max-height 0.22s ease; }
|
|
.sidebar-children-wrap.closed { max-height: 0; }
|
|
|
|
.nav-item {
|
|
padding: 8px 16px;
|
|
font-size: 0.78rem;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
color: var(--muted);
|
|
font-family: 'DM Mono', monospace;
|
|
border-left: 2px solid transparent;
|
|
transition: all 0.1s;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
}
|
|
.nav-item span.label { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
.nav-item:hover { background: var(--surface2); color: var(--text); }
|
|
.nav-item.active { background: rgba(255,107,53,0.08); border-left-color: var(--accent); color: var(--accent); }
|
|
.nav-badge {
|
|
background: var(--surface3);
|
|
color: var(--muted);
|
|
font-size: 0.58rem;
|
|
padding: 1px 5px;
|
|
border-radius: 10px;
|
|
flex-shrink: 0;
|
|
}
|
|
.nav-item.active .nav-badge { background: rgba(255,107,53,0.15); color: var(--accent); }
|
|
|
|
/* CONTENT */
|
|
.content { padding: 28px 32px; overflow-y: auto; flex: 1; height: 100%; min-width: 0; }
|
|
|
|
.page-title {
|
|
font-family: 'Bebas Neue', sans-serif;
|
|
font-size: 1.8rem;
|
|
letter-spacing: 0.06em;
|
|
color: var(--text);
|
|
margin-bottom: 2px;
|
|
}
|
|
.page-sub {
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.68rem;
|
|
color: var(--muted);
|
|
margin-bottom: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
/* CARDS */
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
margin-bottom: 12px;
|
|
overflow: hidden;
|
|
}
|
|
.card-header {
|
|
padding: 12px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
background: var(--surface2);
|
|
gap: 10px;
|
|
}
|
|
.card-header:hover { background: var(--surface3); }
|
|
.card-title {
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.75rem;
|
|
color: var(--text);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.card-body { padding: 16px; }
|
|
.card-body.collapsed { display: none; }
|
|
.chevron { font-size: 0.6rem; color: var(--muted); transition: transform 0.2s; }
|
|
.chevron.open { transform: rotate(180deg); }
|
|
|
|
/* TAGS */
|
|
.tag {
|
|
display: inline-block;
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.6rem;
|
|
padding: 2px 7px;
|
|
border-radius: 3px;
|
|
background: var(--surface3);
|
|
color: var(--muted);
|
|
}
|
|
.tag-orange { background: rgba(255,107,53,0.12); color: var(--accent); }
|
|
.tag-green { background: rgba(6,214,160,0.1); color: var(--accent3); }
|
|
.tag-yellow { background: rgba(255,209,102,0.1); color: var(--accent2); }
|
|
.tag-red { background: rgba(239,68,68,0.1); color: var(--danger); }
|
|
|
|
/* FORMS */
|
|
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
.form-grid.cols-3 { grid-template-columns: 1fr 1fr 1fr; }
|
|
.form-group { display: flex; flex-direction: column; gap: 5px; }
|
|
.form-group.full { grid-column: 1/-1; }
|
|
label {
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.62rem;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
}
|
|
input[type="text"], input[type="number"], select, textarea {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
color: var(--text);
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.78rem;
|
|
padding: 8px 10px;
|
|
width: 100%;
|
|
transition: border-color 0.15s;
|
|
outline: none;
|
|
}
|
|
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
|
input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; }
|
|
textarea { resize: vertical; min-height: 70px; }
|
|
select option { background: var(--surface2); }
|
|
hr.divider { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
|
|
|
|
/* ID CHIPS */
|
|
.chips-wrap {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
padding: 10px;
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
min-height: 44px;
|
|
align-items: flex-start;
|
|
}
|
|
.chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.65rem;
|
|
padding: 3px 8px;
|
|
border-radius: 4px;
|
|
background: var(--surface3);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.chip-remove {
|
|
background: none;
|
|
border: none;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
font-size: 0.7rem;
|
|
padding: 0;
|
|
line-height: 1;
|
|
transition: color 0.1s;
|
|
}
|
|
.chip-remove:hover { color: var(--danger); }
|
|
.chip-input-row { display: flex; gap: 8px; margin-top: 8px; }
|
|
.chip-input-row input { flex: 1; }
|
|
|
|
/* ASSIGNED ITEM CARD */
|
|
.assigned-card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
margin-bottom: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.assigned-card-header {
|
|
padding: 10px 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
background: var(--surface2);
|
|
cursor: pointer;
|
|
}
|
|
.assigned-card-header:hover { background: var(--surface3); }
|
|
.assigned-card-body { padding: 14px; }
|
|
.assigned-card-body.collapsed { display: none; }
|
|
|
|
/* PERK TOGGLE ROW */
|
|
.perk-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 10px;
|
|
border-radius: var(--radius);
|
|
background: var(--surface2);
|
|
margin-bottom: 6px;
|
|
gap: 10px;
|
|
}
|
|
.perk-row-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
|
|
.perk-id {
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.65rem;
|
|
color: var(--accent2);
|
|
flex-shrink: 0;
|
|
width: 40px;
|
|
}
|
|
.perk-date {
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.6rem;
|
|
color: var(--muted);
|
|
flex-shrink: 0;
|
|
}
|
|
.perk-controls { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
|
|
/* ROULETTE */
|
|
.roulette-block {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 16px;
|
|
display: flex;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.roulette-block .form-group { flex: 1; min-width: 160px; }
|
|
|
|
/* TIMESTAMP DISPLAY */
|
|
.ts-display {
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.6rem;
|
|
color: var(--accent3);
|
|
margin-top: 3px;
|
|
}
|
|
|
|
/* TOAST */
|
|
#toast {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
background: var(--accent);
|
|
color: #fff;
|
|
font-family: 'DM Mono', monospace;
|
|
font-size: 0.75rem;
|
|
padding: 10px 18px;
|
|
border-radius: var(--radius);
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
transition: all 0.2s;
|
|
pointer-events: none;
|
|
z-index: 1000;
|
|
}
|
|
#toast.show { opacity: 1; transform: translateY(0); }
|
|
|
|
@media (max-width: 700px) {
|
|
.editor-layout { flex-direction: column; }
|
|
.sidebar { width: 100% !important; max-width: 100%; height: auto; border-right: none; border-bottom: 1px solid var(--border); }
|
|
.resize-handle { display: none; }
|
|
.form-grid { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<div class="logo">.pu<span>Save</span> <span class="logo-sub">editor</span></div>
|
|
<div class="header-right">
|
|
<span id="filename-display"></span>
|
|
<button class="btn btn-ghost" id="load-new-btn">Load new</button>
|
|
<button class="btn btn-primary" id="save-btn" style="display:none">↓ Save file</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div id="drop-zone">
|
|
<div class="drop-ring">💾</div>
|
|
<h2>Drop your .puSave file</h2>
|
|
<p>or click below to browse</p>
|
|
<button class="btn btn-primary" onclick="document.getElementById('file-input').click()">Browse file</button>
|
|
<input type="file" id="file-input" accept=".puSave,.json">
|
|
</div>
|
|
|
|
<div id="editor">
|
|
<div class="editor-layout">
|
|
<nav class="sidebar" id="sidebar"></nav>
|
|
<div class="resize-handle" id="resize-handle"></div>
|
|
<main class="content" id="content"></main>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="toast"></div>
|
|
|
|
<script>
|
|
let save = null;
|
|
let currentPage = 'overview';
|
|
let currentFileName = 'output.puSave';
|
|
const collapsedGroups = new Set();
|
|
|
|
// ─── SIDEBAR RESIZE ────────────────────────────────────────────────────────────
|
|
(function() {
|
|
const handle = document.getElementById('resize-handle');
|
|
const sidebar = document.getElementById('sidebar');
|
|
let dragging = false, startX = 0, startW = 0;
|
|
handle.addEventListener('mousedown', e => {
|
|
dragging = true; startX = e.clientX; startW = sidebar.offsetWidth;
|
|
handle.classList.add('dragging');
|
|
document.body.style.cursor = 'col-resize';
|
|
document.body.style.userSelect = 'none';
|
|
});
|
|
document.addEventListener('mousemove', e => {
|
|
if (!dragging) return;
|
|
sidebar.style.width = Math.min(400, Math.max(140, startW + (e.clientX - startX))) + 'px';
|
|
});
|
|
document.addEventListener('mouseup', () => {
|
|
if (!dragging) return;
|
|
dragging = false; handle.classList.remove('dragging');
|
|
document.body.style.cursor = ''; document.body.style.userSelect = '';
|
|
});
|
|
})();
|
|
|
|
// ─── FILE LOADING ──────────────────────────────────────────────────────────────
|
|
document.getElementById('file-input').addEventListener('change', e => {
|
|
const file = e.target.files[0]; if (!file) return;
|
|
loadFile(file);
|
|
});
|
|
document.getElementById('load-new-btn').addEventListener('click', () => {
|
|
document.getElementById('file-input').click();
|
|
});
|
|
|
|
const dz = document.getElementById('drop-zone');
|
|
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('dragover'); });
|
|
dz.addEventListener('dragleave', () => dz.classList.remove('dragover'));
|
|
dz.addEventListener('drop', e => {
|
|
e.preventDefault(); dz.classList.remove('dragover');
|
|
const file = e.dataTransfer.files[0]; if (file) loadFile(file);
|
|
});
|
|
|
|
function loadFile(file) {
|
|
currentFileName = file.name;
|
|
document.getElementById('filename-display').textContent = file.name;
|
|
const reader = new FileReader();
|
|
reader.onload = e => {
|
|
try {
|
|
save = JSON.parse(e.target.result);
|
|
document.getElementById('drop-zone').style.display = 'none';
|
|
document.getElementById('editor').style.display = 'flex';
|
|
document.getElementById('save-btn').style.display = '';
|
|
currentPage = 'overview';
|
|
renderSidebar();
|
|
renderContent();
|
|
} catch(err) {
|
|
alert('Failed to parse file: ' + err.message);
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
// ─── SAVE FILE ─────────────────────────────────────────────────────────────────
|
|
document.getElementById('save-btn').addEventListener('click', () => {
|
|
const blob = new Blob([JSON.stringify(save)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url; a.download = currentFileName; a.click();
|
|
URL.revokeObjectURL(url);
|
|
toast('File downloaded!');
|
|
});
|
|
|
|
// ─── SIDEBAR ───────────────────────────────────────────────────────────────────
|
|
function renderSidebar() {
|
|
const sg = save.saveGame;
|
|
const sidebar = document.getElementById('sidebar');
|
|
|
|
const groups = [
|
|
{ key: 'overview', label: 'Overview', items: null },
|
|
{ key: 'meta', label: 'Meta', items: null },
|
|
{ key: 'punishments', label: 'Punishments', items: [
|
|
{ id: 'active-punishments', label: 'Active', badge: (sg.activePunishments||[]).length },
|
|
{ id: 'finished-punishments', label: 'Finished', badge: (sg.finishedPunishments||[]).length },
|
|
{ id: 'blocked-punishments', label: 'Blocked', badge: (sg.blockedPunishments||[]).length },
|
|
]},
|
|
{ key: 'classes', label: 'Classes', items: [
|
|
{ id: 'classes-overview', label: 'All Classes', badge: (sg.assignedClasses||[]).length },
|
|
...(sg.assignedClasses||[]).map(c => ({ id: 'class-'+c.id, label: 'Class '+c.id }))
|
|
]},
|
|
{ key: 'clubs', label: 'Clubs', items: (sg.assignedClubs||[]).map(c => ({ id: 'club-'+c.id, label: 'Club '+c.id })) },
|
|
{ key: 'partners', label: 'Partners', items: (sg.assignedPartners||[]).map(p => ({ id: 'partner-'+p.id, label: 'Partner '+p.id })) },
|
|
{ key: 'tasks', label: 'Active Tasks', items: null },
|
|
{ key: 'roulette', label: 'Roulette', items: null },
|
|
];
|
|
|
|
sidebar.innerHTML = groups.map(g => {
|
|
const collapsed = collapsedGroups.has(g.key);
|
|
if (!g.items) {
|
|
// single nav item
|
|
return `<div class="nav-item ${currentPage===g.key?'active':''}" onclick="navigate('${g.key}')">
|
|
<span class="label">${g.label}</span>
|
|
</div>`;
|
|
}
|
|
return `<div class="sidebar-section">
|
|
<div class="sidebar-group-label" onclick="toggleGroup('${g.key}')">
|
|
<span>${g.label}</span>
|
|
<span class="schev ${collapsed?'closed':''}">▼</span>
|
|
</div>
|
|
<div class="sidebar-children-wrap ${collapsed?'closed':''}">
|
|
${g.items.map(item => `
|
|
<div class="nav-item ${currentPage===item.id?'active':''}" onclick="navigate('${item.id}')" style="padding-left:24px">
|
|
<span class="label">${item.label}</span>
|
|
${item.badge !== undefined ? `<span class="nav-badge">${item.badge}</span>` : ''}
|
|
</div>
|
|
`).join('')}
|
|
${g.items.length === 0 ? `<div style="padding:6px 24px;font-family:'DM Mono',monospace;font-size:0.65rem;color:var(--muted)">none</div>` : ''}
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
function toggleGroup(key) {
|
|
if (collapsedGroups.has(key)) collapsedGroups.delete(key);
|
|
else collapsedGroups.add(key);
|
|
renderSidebar();
|
|
}
|
|
|
|
function navigate(page) {
|
|
currentPage = page;
|
|
renderSidebar();
|
|
renderContent();
|
|
}
|
|
|
|
// ─── CONTENT ROUTER ────────────────────────────────────────────────────────────
|
|
function renderContent() {
|
|
const el = document.getElementById('content');
|
|
if (currentPage === 'overview') renderOverview(el);
|
|
else if (currentPage === 'meta') renderMeta(el);
|
|
else if (currentPage === 'active-punishments') renderPunishmentList(el, 'activePunishments', 'Active Punishments', 'tag-orange');
|
|
else if (currentPage === 'finished-punishments') renderPunishmentList(el, 'finishedPunishments', 'Finished Punishments', 'tag-green');
|
|
else if (currentPage === 'blocked-punishments') renderPunishmentList(el, 'blockedPunishments', 'Blocked Punishments', 'tag-red');
|
|
else if (currentPage === 'classes-overview') renderClassesOverview(el);
|
|
else if (currentPage.startsWith('class-')) renderClass(el, currentPage.replace('class-',''));
|
|
else if (currentPage.startsWith('club-')) renderClub(el, currentPage.replace('club-',''));
|
|
else if (currentPage.startsWith('partner-')) renderPartner(el, currentPage.replace('partner-',''));
|
|
else if (currentPage === 'tasks') renderActiveTasks(el);
|
|
else if (currentPage === 'roulette') renderRoulette(el);
|
|
}
|
|
|
|
// ─── OVERVIEW ──────────────────────────────────────────────────────────────────
|
|
function renderOverview(el) {
|
|
const sg = save.saveGame;
|
|
el.innerHTML = `
|
|
<div class="page-title">Save Overview</div>
|
|
<div class="page-sub">
|
|
<span class="tag">${esc(save.mapId||'no mapId')}</span>
|
|
</div>
|
|
<div class="form-grid cols-3" style="margin-bottom:20px">
|
|
${stat('Active Major', sg.activeMajor||'—')}
|
|
${stat('Assigned Classes', (sg.assignedClasses||[]).length)}
|
|
${stat('Assigned Clubs', (sg.assignedClubs||[]).length)}
|
|
${stat('Assigned Partners', (sg.assignedPartners||[]).length)}
|
|
${stat('Active Punishments', (sg.activePunishments||[]).length)}
|
|
${stat('Finished Punishments', (sg.finishedPunishments||[]).length)}
|
|
${stat('Blocked Punishments', (sg.blockedPunishments||[]).length)}
|
|
${stat('Active Tasks', (sg.activeTasks||[]).length)}
|
|
${stat('Roulette ID', sg.roulette?.id||'—')}
|
|
</div>
|
|
`;
|
|
}
|
|
function stat(label, value) {
|
|
return `<div class="card">
|
|
<div class="card-body" style="padding:14px">
|
|
<div style="font-family:'DM Mono',monospace;font-size:0.6rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:6px">${label}</div>
|
|
<div style="font-family:'Bebas Neue',sans-serif;font-size:1.6rem;color:var(--accent);letter-spacing:0.05em">${value}</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// ─── META ──────────────────────────────────────────────────────────────────────
|
|
function renderMeta(el) {
|
|
el.innerHTML = `
|
|
<div class="page-title">Meta</div>
|
|
<div class="page-sub">map ID and major</div>
|
|
<div class="card"><div class="card-body">
|
|
<div class="form-grid">
|
|
<div class="form-group full">
|
|
<label>Map ID</label>
|
|
<input type="text" id="meta-mapId" value="${esc(save.mapId||'')}">
|
|
</div>
|
|
<div class="form-group full">
|
|
<label>Active Major ID</label>
|
|
<input type="text" id="meta-activeMajor" value="${esc(save.saveGame.activeMajor||'')}">
|
|
</div>
|
|
</div>
|
|
</div></div>
|
|
<button class="btn btn-primary" onclick="saveMeta()">Save</button>
|
|
`;
|
|
}
|
|
function saveMeta() {
|
|
save.mapId = document.getElementById('meta-mapId').value;
|
|
save.saveGame.activeMajor = document.getElementById('meta-activeMajor').value;
|
|
toast('Saved!');
|
|
}
|
|
|
|
// ─── PUNISHMENT LISTS ──────────────────────────────────────────────────────────
|
|
function renderPunishmentList(el, key, title, tagClass) {
|
|
const list = save.saveGame[key] || [];
|
|
el.innerHTML = `
|
|
<div class="page-title">${title}</div>
|
|
<div class="page-sub">${list.length} punishment ID${list.length!==1?'s':''}</div>
|
|
<div class="chips-wrap" id="pun-chips-${key}">
|
|
${list.map((id,i) => `
|
|
<span class="chip">
|
|
<span class="${tagClass||''}">${esc(id)}</span>
|
|
<button class="chip-remove" onclick="removePunId('${key}',${i})">✕</button>
|
|
</span>`).join('')}
|
|
</div>
|
|
<div class="chip-input-row">
|
|
<input type="text" id="pun-new-id-${key}" placeholder="Add punishment ID…">
|
|
<button class="btn btn-ghost btn-sm" onclick="addPunId('${key}')">Add</button>
|
|
</div>
|
|
<button class="btn btn-primary" style="margin-top:16px" onclick="toast('List auto-saved on each add/remove!')">✓ Changes are live</button>
|
|
`;
|
|
}
|
|
function addPunId(key) {
|
|
const input = document.getElementById(`pun-new-id-${key}`);
|
|
const val = input.value.trim(); if (!val) return;
|
|
save.saveGame[key].push(val);
|
|
input.value = '';
|
|
renderContent();
|
|
renderSidebar();
|
|
toast('Added!');
|
|
}
|
|
function removePunId(key, idx) {
|
|
save.saveGame[key].splice(idx, 1);
|
|
renderContent();
|
|
renderSidebar();
|
|
toast('Removed!');
|
|
}
|
|
|
|
// ─── CLASSES OVERVIEW ─────────────────────────────────────────────────────────
|
|
function renderClassesOverview(el) {
|
|
const classes = save.saveGame.assignedClasses || [];
|
|
el.innerHTML = `
|
|
<div class="page-title">Assigned Classes</div>
|
|
<div class="page-sub">${classes.length} class${classes.length!==1?'es':''} assigned</div>
|
|
|
|
<div style="overflow-x:auto;margin-bottom:20px">
|
|
<table id="classes-table" style="width:100%;border-collapse:collapse;font-family:'DM Mono',monospace;font-size:0.72rem">
|
|
<thead>
|
|
<tr style="border-bottom:1px solid var(--border)">
|
|
<th style="text-align:left;padding:8px 10px;color:var(--muted);font-weight:400;letter-spacing:0.1em;text-transform:uppercase;font-size:0.6rem">Class ID</th>
|
|
<th style="text-align:left;padding:8px 10px;color:var(--muted);font-weight:400;letter-spacing:0.1em;text-transform:uppercase;font-size:0.6rem">Status</th>
|
|
<th style="text-align:left;padding:8px 10px;color:var(--muted);font-weight:400;letter-spacing:0.1em;text-transform:uppercase;font-size:0.6rem">Attendances</th>
|
|
<th style="text-align:left;padding:8px 10px;color:var(--muted);font-weight:400;letter-spacing:0.1em;text-transform:uppercase;font-size:0.6rem">Last Attended</th>
|
|
<th style="padding:8px 10px"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${classes.map((c, i) => `
|
|
<tr style="border-bottom:1px solid var(--border);transition:background 0.1s" onmouseenter="this.style.background='var(--surface2)'" onmouseleave="this.style.background=''">
|
|
<td style="padding:6px 10px">
|
|
<input type="text" value="${esc(c.id)}" data-i="${i}" data-field="id"
|
|
style="background:transparent;border:none;border-bottom:1px solid transparent;border-radius:0;padding:3px 0;width:100%;min-width:80px;color:var(--accent2)"
|
|
onfocus="this.style.borderBottomColor='var(--accent)'"
|
|
onblur="this.style.borderBottomColor='transparent';updateClassField(${i},'id',this.value)">
|
|
</td>
|
|
<td style="padding:6px 10px">
|
|
<select data-i="${i}" data-field="status"
|
|
style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 6px;color:var(--text);font-family:'DM Mono',monospace;font-size:0.7rem"
|
|
onchange="updateClassField(${i},'status',this.value)">
|
|
${['active','inactive','completed'].map(s => `<option value="${s}" ${c.status===s?'selected':''}>${s}</option>`).join('')}
|
|
</select>
|
|
</td>
|
|
<td style="padding:6px 10px">
|
|
<input type="number" value="${c.attendances||0}" data-i="${i}" data-field="attendances" min="0"
|
|
style="background:transparent;border:none;border-bottom:1px solid transparent;border-radius:0;padding:3px 0;width:60px;color:var(--text)"
|
|
onfocus="this.style.borderBottomColor='var(--accent)'"
|
|
onblur="this.style.borderBottomColor='transparent';updateClassField(${i},'attendances',+this.value)">
|
|
</td>
|
|
<td style="padding:6px 10px">
|
|
<input type="number" value="${c.lastAttended||0}" data-i="${i}" data-field="lastAttended"
|
|
style="background:transparent;border:none;border-bottom:1px solid transparent;border-radius:0;padding:3px 0;width:110px;color:var(--text)"
|
|
onfocus="this.style.borderBottomColor='var(--accent)'"
|
|
onblur="this.style.borderBottomColor='transparent';updateClassField(${i},'lastAttended',+this.value)"
|
|
title="${c.lastAttended ? new Date(c.lastAttended*1000).toLocaleString() : '—'}">
|
|
<div style="font-size:0.58rem;color:var(--accent3)">${c.lastAttended ? new Date(c.lastAttended*1000).toLocaleDateString() : '—'}</div>
|
|
</td>
|
|
<td style="padding:6px 10px;white-space:nowrap;display:flex;gap:6px;align-items:center">
|
|
<button class="btn btn-ghost btn-sm" onclick="navigate('class-${c.id}')" title="Open full editor">✎</button>
|
|
<button class="btn btn-danger btn-sm" onclick="removeClassAt(${i})">✕</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<hr class="divider">
|
|
<div style="font-family:'Bebas Neue',sans-serif;font-size:1.1rem;letter-spacing:0.06em;margin-bottom:12px;color:var(--text)">Add Class</div>
|
|
<div class="card"><div class="card-body">
|
|
<div class="form-grid cols-3">
|
|
<div class="form-group">
|
|
<label>Class ID</label>
|
|
<input type="text" id="add-cls-id" placeholder="e.g. 1234">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Status</label>
|
|
<select id="add-cls-status">
|
|
<option value="active">active</option>
|
|
<option value="inactive">inactive</option>
|
|
<option value="completed">completed</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Attendances</label>
|
|
<input type="number" id="add-cls-attendances" value="0" min="0">
|
|
</div>
|
|
<div class="form-group full">
|
|
<label>Last Attended</label>
|
|
<div style="display:flex;gap:10px;align-items:center">
|
|
<input type="number" id="add-cls-lastAttended" value="${Math.floor(Date.now()/1000) - 86400}" style="flex:1">
|
|
<button class="btn btn-ghost btn-sm" onclick="document.getElementById('add-cls-lastAttended').value=Math.floor(Date.now()/1000)">Now</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-primary" style="margin-top:14px;width:100%" onclick="addClassFromForm()">+ Add Class</button>
|
|
</div></div>
|
|
`;
|
|
}
|
|
|
|
function updateClassField(idx, field, value) {
|
|
if (!save.saveGame.assignedClasses[idx]) return;
|
|
save.saveGame.assignedClasses[idx][field] = value;
|
|
// refresh sidebar badge
|
|
renderSidebar();
|
|
}
|
|
|
|
function removeClassAt(idx) {
|
|
if (!confirm('Remove this class from the save?')) return;
|
|
save.saveGame.assignedClasses.splice(idx, 1);
|
|
renderSidebar(); renderContent();
|
|
}
|
|
|
|
function addClassFromForm() {
|
|
const id = document.getElementById('add-cls-id').value.trim();
|
|
if (!id) { toast('Class ID required!'); return; }
|
|
if (save.saveGame.assignedClasses.find(c => c.id === id)) { toast('ID already exists!'); return; }
|
|
if (!save.saveGame.assignedClasses) save.saveGame.assignedClasses = [];
|
|
save.saveGame.assignedClasses.push({
|
|
id,
|
|
status: document.getElementById('add-cls-status').value,
|
|
attendances: parseInt(document.getElementById('add-cls-attendances').value) || 0,
|
|
lastAttended: parseInt(document.getElementById('add-cls-lastAttended').value) || 0
|
|
});
|
|
document.getElementById('add-cls-id').value = '';
|
|
document.getElementById('add-cls-attendances').value = '0';
|
|
renderSidebar(); renderContent();
|
|
toast('Class added!');
|
|
}
|
|
|
|
|
|
function renderClass(el, id) {
|
|
const sg = save.saveGame;
|
|
const cls = (sg.assignedClasses||[]).find(c => c.id === id);
|
|
if (!cls) { el.innerHTML = `<div class="page-title">Not found</div>`; return; }
|
|
const lastDate = cls.lastAttended ? new Date(cls.lastAttended * 1000).toLocaleString() : '—';
|
|
|
|
el.innerHTML = `
|
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:4px">
|
|
<div>
|
|
<div class="page-title">Class ${esc(id)}</div>
|
|
<div class="page-sub"><span class="tag">${esc(cls.status||'active')}</span></div>
|
|
</div>
|
|
<button class="btn btn-danger" onclick="removeAssignedClass('${id}')">Remove class</button>
|
|
</div>
|
|
<div class="card"><div class="card-body">
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label>Class ID</label>
|
|
<input type="text" id="cls-id" value="${esc(cls.id)}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Status</label>
|
|
<select id="cls-status">
|
|
${['active','inactive','completed'].map(s => `<option value="${s}" ${cls.status===s?'selected':''}>${s}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Attendances</label>
|
|
<input type="number" id="cls-attendances" value="${cls.attendances||0}" min="0">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Last Attended (unix timestamp)</label>
|
|
<input type="number" id="cls-lastAttended" value="${cls.lastAttended||0}">
|
|
<div class="ts-display">${lastDate}</div>
|
|
</div>
|
|
</div>
|
|
</div></div>
|
|
<button class="btn btn-primary" onclick="saveClass('${id}')">Save class</button>
|
|
`;
|
|
|
|
// live timestamp preview
|
|
document.getElementById('cls-lastAttended').addEventListener('input', e => {
|
|
const ts = parseInt(e.target.value);
|
|
e.target.nextElementSibling.textContent = ts ? new Date(ts * 1000).toLocaleString() : '—';
|
|
});
|
|
}
|
|
function saveClass(id) {
|
|
const sg = save.saveGame;
|
|
const cls = sg.assignedClasses.find(c => c.id === id);
|
|
const newId = document.getElementById('cls-id').value;
|
|
cls.id = newId;
|
|
cls.status = document.getElementById('cls-status').value;
|
|
cls.attendances = parseInt(document.getElementById('cls-attendances').value) || 0;
|
|
cls.lastAttended = parseInt(document.getElementById('cls-lastAttended').value) || 0;
|
|
// update page key if ID changed
|
|
if (newId !== id) { currentPage = 'class-' + newId; renderSidebar(); }
|
|
toast('Class saved!');
|
|
}
|
|
function removeAssignedClass(id) {
|
|
if (!confirm('Remove this class from the save?')) return;
|
|
save.saveGame.assignedClasses = save.saveGame.assignedClasses.filter(c => c.id !== id);
|
|
currentPage = 'overview';
|
|
renderSidebar(); renderContent();
|
|
}
|
|
|
|
// ─── ASSIGNED CLUB ─────────────────────────────────────────────────────────────
|
|
function renderClub(el, id) {
|
|
const club = (save.saveGame.assignedClubs||[]).find(c => c.id === id);
|
|
if (!club) { el.innerHTML = `<div class="page-title">Not found</div>`; return; }
|
|
const perks = club.activePerks || {};
|
|
const perkKeys = Object.keys(perks);
|
|
|
|
el.innerHTML = `
|
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:4px">
|
|
<div>
|
|
<div class="page-title">Club ${esc(id)}</div>
|
|
<div class="page-sub">${perkKeys.length} active perk${perkKeys.length!==1?'s':''}</div>
|
|
</div>
|
|
<button class="btn btn-danger" onclick="removeAssigned('clubs','${id}')">Remove club</button>
|
|
</div>
|
|
<div class="card"><div class="card-body">
|
|
<div class="form-group">
|
|
<label>Club ID</label>
|
|
<input type="text" id="club-id" value="${esc(club.id)}">
|
|
</div>
|
|
</div></div>
|
|
|
|
<hr class="divider">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
|
|
<div style="font-family:'Bebas Neue',sans-serif;font-size:1.1rem;letter-spacing:0.06em">Active Perks</div>
|
|
<button class="btn btn-ghost btn-sm" onclick="addActivePerk('clubs','${id}')">+ Add perk</button>
|
|
</div>
|
|
${renderPerkRows('clubs', id, perks)}
|
|
|
|
<button class="btn btn-primary" style="margin-top:16px" onclick="saveClubAssignment('${id}')">Save club</button>
|
|
`;
|
|
}
|
|
function saveClubAssignment(id) {
|
|
const club = save.saveGame.assignedClubs.find(c => c.id === id);
|
|
const newId = document.getElementById('club-id').value;
|
|
club.id = newId;
|
|
if (newId !== id) { currentPage = 'club-' + newId; renderSidebar(); }
|
|
toast('Club saved!');
|
|
}
|
|
|
|
// ─── ASSIGNED PARTNER ──────────────────────────────────────────────────────────
|
|
function renderPartner(el, id) {
|
|
const partner = (save.saveGame.assignedPartners||[]).find(p => p.id === id);
|
|
if (!partner) { el.innerHTML = `<div class="page-title">Not found</div>`; return; }
|
|
const perks = partner.activePerks || {};
|
|
const perkKeys = Object.keys(perks);
|
|
|
|
el.innerHTML = `
|
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:4px">
|
|
<div>
|
|
<div class="page-title">Partner ${esc(id)}</div>
|
|
<div class="page-sub">${perkKeys.length} active perk${perkKeys.length!==1?'s':''}</div>
|
|
</div>
|
|
<button class="btn btn-danger" onclick="removeAssigned('partners','${id}')">Remove partner</button>
|
|
</div>
|
|
<div class="card"><div class="card-body">
|
|
<div class="form-group">
|
|
<label>Partner ID</label>
|
|
<input type="text" id="partner-id" value="${esc(partner.id)}">
|
|
</div>
|
|
</div></div>
|
|
|
|
<hr class="divider">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
|
|
<div style="font-family:'Bebas Neue',sans-serif;font-size:1.1rem;letter-spacing:0.06em">Active Perks</div>
|
|
<button class="btn btn-ghost btn-sm" onclick="addActivePerk('partners','${id}')">+ Add perk</button>
|
|
</div>
|
|
${renderPerkRows('partners', id, perks)}
|
|
|
|
<button class="btn btn-primary" style="margin-top:16px" onclick="savePartnerAssignment('${id}')">Save partner</button>
|
|
`;
|
|
}
|
|
function savePartnerAssignment(id) {
|
|
const partner = save.saveGame.assignedPartners.find(p => p.id === id);
|
|
const newId = document.getElementById('partner-id').value;
|
|
partner.id = newId;
|
|
if (newId !== id) { currentPage = 'partner-' + newId; renderSidebar(); }
|
|
toast('Partner saved!');
|
|
}
|
|
|
|
// ─── SHARED PERK ROWS ──────────────────────────────────────────────────────────
|
|
function renderPerkRows(type, entityId, perks) {
|
|
const keys = Object.keys(perks);
|
|
if (keys.length === 0) return `<div style="font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted);padding:12px 0">No active perks</div>`;
|
|
return keys.map(pk => {
|
|
const perk = perks[pk];
|
|
const date = perk.activatedOn ? new Date(perk.activatedOn * 1000).toLocaleString() : '—';
|
|
return `
|
|
<div class="perk-row" id="perk-row-${type}-${entityId}-${pk}">
|
|
<div class="perk-row-left">
|
|
<span class="perk-id">#${esc(pk)}</span>
|
|
<div style="flex:1;min-width:0">
|
|
<div style="font-family:'DM Mono',monospace;font-size:0.68rem;color:var(--text)">Perk ID: ${esc(pk)}</div>
|
|
<div class="ts-display">${date}</div>
|
|
</div>
|
|
</div>
|
|
<div class="perk-controls">
|
|
<label style="display:flex;align-items:center;gap:5px;font-size:0.65rem;color:var(--muted);text-transform:none;letter-spacing:0">
|
|
<input type="checkbox" ${perk.active?'checked':''} onchange="togglePerkActive('${type}','${entityId}','${pk}',this.checked)">
|
|
active
|
|
</label>
|
|
<input type="number" value="${perk.activatedOn||0}" style="width:110px;font-size:0.65rem;padding:4px 6px"
|
|
onchange="setPerkTimestamp('${type}','${entityId}','${pk}',this.value)"
|
|
title="activatedOn timestamp">
|
|
<button class="btn btn-danger btn-sm" onclick="removeActivePerk('${type}','${entityId}','${pk}')">✕</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function togglePerkActive(type, entityId, perkId, val) {
|
|
const list = type === 'clubs' ? save.saveGame.assignedClubs : save.saveGame.assignedPartners;
|
|
const entity = list.find(e => e.id === entityId);
|
|
if (entity?.activePerks?.[perkId]) entity.activePerks[perkId].active = val;
|
|
}
|
|
function setPerkTimestamp(type, entityId, perkId, val) {
|
|
const list = type === 'clubs' ? save.saveGame.assignedClubs : save.saveGame.assignedPartners;
|
|
const entity = list.find(e => e.id === entityId);
|
|
if (entity?.activePerks?.[perkId]) entity.activePerks[perkId].activatedOn = parseInt(val) || 0;
|
|
}
|
|
function removeActivePerk(type, entityId, perkId) {
|
|
const list = type === 'clubs' ? save.saveGame.assignedClubs : save.saveGame.assignedPartners;
|
|
const entity = list.find(e => e.id === entityId);
|
|
if (entity?.activePerks) { delete entity.activePerks[perkId]; renderContent(); }
|
|
}
|
|
function addActivePerk(type, entityId) {
|
|
const id = prompt('Enter perk ID:'); if (!id) return;
|
|
const list = type === 'clubs' ? save.saveGame.assignedClubs : save.saveGame.assignedPartners;
|
|
const entity = list.find(e => e.id === entityId);
|
|
if (entity) {
|
|
if (!entity.activePerks) entity.activePerks = {};
|
|
entity.activePerks[id] = { active: true, activatedOn: Math.floor(Date.now()/1000) };
|
|
renderContent();
|
|
toast('Perk added!');
|
|
}
|
|
}
|
|
|
|
// ─── REMOVE ASSIGNED ──────────────────────────────────────────────────────────
|
|
function removeAssigned(type, id) {
|
|
if (!confirm(`Remove this ${type.slice(0,-1)} from the save?`)) return;
|
|
if (type === 'clubs') save.saveGame.assignedClubs = save.saveGame.assignedClubs.filter(c => c.id !== id);
|
|
if (type === 'partners') save.saveGame.assignedPartners = save.saveGame.assignedPartners.filter(p => p.id !== id);
|
|
currentPage = 'overview';
|
|
renderSidebar(); renderContent();
|
|
}
|
|
|
|
// ─── ADD ASSIGNED ─────────────────────────────────────────────────────────────
|
|
function addAssignedClass() {
|
|
const id = prompt('Enter class ID:'); if (!id) return;
|
|
if (!save.saveGame.assignedClasses) save.saveGame.assignedClasses = [];
|
|
save.saveGame.assignedClasses.push({ id, status: 'active', attendances: 0, lastAttended: Math.floor(Date.now()/1000) });
|
|
currentPage = 'class-' + id;
|
|
renderSidebar(); renderContent(); toast('Class added!');
|
|
}
|
|
function addAssignedClub() {
|
|
const id = prompt('Enter club ID:'); if (!id) return;
|
|
if (!save.saveGame.assignedClubs) save.saveGame.assignedClubs = [];
|
|
save.saveGame.assignedClubs.push({ id, activePerks: {} });
|
|
currentPage = 'club-' + id;
|
|
renderSidebar(); renderContent(); toast('Club added!');
|
|
}
|
|
function addAssignedPartner() {
|
|
const id = prompt('Enter partner ID:'); if (!id) return;
|
|
if (!save.saveGame.assignedPartners) save.saveGame.assignedPartners = [];
|
|
save.saveGame.assignedPartners.push({ id, activePerks: {} });
|
|
currentPage = 'partner-' + id;
|
|
renderSidebar(); renderContent(); toast('Partner added!');
|
|
}
|
|
|
|
// ─── ACTIVE TASKS ──────────────────────────────────────────────────────────────
|
|
function renderActiveTasks(el) {
|
|
const tasks = save.saveGame.activeTasks || [];
|
|
el.innerHTML = `
|
|
<div class="page-title">Active Tasks</div>
|
|
<div class="page-sub">${tasks.length} task${tasks.length!==1?'s':''}</div>
|
|
<div class="chips-wrap" id="tasks-chips">
|
|
${tasks.map((t,i) => `
|
|
<span class="chip">
|
|
${typeof t === 'object' ? esc(JSON.stringify(t)) : esc(String(t))}
|
|
<button class="chip-remove" onclick="removeTask(${i})">✕</button>
|
|
</span>`).join('')}
|
|
${tasks.length === 0 ? `<span style="font-family:'DM Mono',monospace;font-size:0.65rem;color:var(--muted)">No active tasks</span>` : ''}
|
|
</div>
|
|
<div class="chip-input-row">
|
|
<input type="text" id="task-new" placeholder="Add task ID…">
|
|
<button class="btn btn-ghost btn-sm" onclick="addTask()">Add</button>
|
|
</div>
|
|
`;
|
|
}
|
|
function addTask() {
|
|
const val = document.getElementById('task-new').value.trim(); if (!val) return;
|
|
if (!save.saveGame.activeTasks) save.saveGame.activeTasks = [];
|
|
save.saveGame.activeTasks.push(val);
|
|
renderContent(); toast('Added!');
|
|
}
|
|
function removeTask(idx) {
|
|
save.saveGame.activeTasks.splice(idx, 1);
|
|
renderContent();
|
|
}
|
|
|
|
// ─── ROULETTE ──────────────────────────────────────────────────────────────────
|
|
function renderRoulette(el) {
|
|
const r = save.saveGame.roulette || {};
|
|
const date = r.rolledOn ? new Date(r.rolledOn * 1000).toLocaleString() : '—';
|
|
el.innerHTML = `
|
|
<div class="page-title">Roulette</div>
|
|
<div class="page-sub">last roll state</div>
|
|
<div class="card"><div class="card-body">
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label>Roulette ID</label>
|
|
<input type="text" id="rou-id" value="${esc(r.id||'')}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Rolled On (unix timestamp)</label>
|
|
<input type="number" id="rou-rolledOn" value="${r.rolledOn||0}" oninput="this.nextElementSibling.textContent=parseInt(this.value)?new Date(parseInt(this.value)*1000).toLocaleString():'—'">
|
|
<div class="ts-display">${date}</div>
|
|
</div>
|
|
</div>
|
|
</div></div>
|
|
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:4px">
|
|
<button class="btn btn-primary" onclick="saveRoulette()">Save roulette</button>
|
|
<button class="btn btn-ghost" onclick="setRouletteNow()">Set rolled-on to now</button>
|
|
</div>
|
|
`;
|
|
}
|
|
function saveRoulette() {
|
|
if (!save.saveGame.roulette) save.saveGame.roulette = {};
|
|
save.saveGame.roulette.id = document.getElementById('rou-id').value;
|
|
save.saveGame.roulette.rolledOn = parseInt(document.getElementById('rou-rolledOn').value) || 0;
|
|
toast('Roulette saved!');
|
|
}
|
|
function setRouletteNow() {
|
|
const ts = Math.floor(Date.now()/1000);
|
|
document.getElementById('rou-rolledOn').value = ts;
|
|
document.getElementById('rou-rolledOn').nextElementSibling.textContent = new Date(ts*1000).toLocaleString();
|
|
}
|
|
|
|
// ─── HELPERS ───────────────────────────────────────────────────────────────────
|
|
function esc(str) {
|
|
return String(str)
|
|
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
.replace(/"/g,'"').replace(/'/g,''');
|
|
}
|
|
|
|
function toggleCard(header) {
|
|
const body = header.nextElementSibling;
|
|
const chev = header.querySelector('.chevron');
|
|
body.classList.toggle('collapsed');
|
|
if (chev) chev.classList.toggle('open', !body.classList.contains('collapsed'));
|
|
}
|
|
|
|
function toast(msg) {
|
|
const el = document.getElementById('toast');
|
|
el.textContent = msg;
|
|
el.classList.add('show');
|
|
setTimeout(() => el.classList.remove('show'), 2200);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|