116 lines
9.8 KiB
HTML
116 lines
9.8 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Bingo Overlay Editor</title>
|
|
<style>
|
|
:root { --bg:#0b1020; --panel:#151d34; --line:#2d3857; --text:#f7f0dc; --muted:#9faccc; --gold:#ffe66d; --blue:#2f8cff; --green:#35d06f; --bad:#ff6b6b; --good:#62e884; }
|
|
* { box-sizing:border-box; }
|
|
body { margin:0; background: radial-gradient(circle at top, #202b50, #0b1020 48%); color:var(--text); font-family:"Segoe UI", system-ui, sans-serif; }
|
|
.wrap { max-width:1220px; margin:0 auto; padding:28px 18px 60px; }
|
|
header { display:flex; justify-content:space-between; gap:18px; align-items:flex-end; margin-bottom:18px; }
|
|
h1 { margin:0; font-size:38px; letter-spacing:.06em; text-transform:uppercase; }
|
|
.hint { color:var(--muted); max-width:760px; line-height:1.45; }
|
|
.panel { background:rgba(21,29,52,.9); border:1px solid var(--line); border-radius:18px; padding:16px; box-shadow:0 18px 50px rgba(0,0,0,.3); margin-bottom:16px; }
|
|
.top { display:grid; grid-template-columns:1fr 1fr auto; gap:12px; align-items:end; }
|
|
label { display:block; color:var(--muted); font-size:13px; text-transform:uppercase; letter-spacing:.08em; margin-bottom:6px; }
|
|
input, textarea { width:100%; background:#090d1a; color:var(--text); border:1px solid var(--line); border-radius:12px; padding:10px 12px; font:inherit; }
|
|
textarea { min-height:300px; resize:vertical; line-height:1.35; }
|
|
button { border:0; border-radius:12px; padding:11px 14px; font-weight:800; cursor:pointer; color:#08101d; background:var(--gold); box-shadow:0 8px 20px rgba(0,0,0,.22); }
|
|
button.secondary { background:#253151; color:var(--text); border:1px solid var(--line); }
|
|
button.blue { background:var(--blue); }
|
|
button.green { background:var(--green); color:#041409; }
|
|
button.bad { background:var(--bad); color:white; }
|
|
button:disabled { opacity:.45; cursor:not-allowed; }
|
|
.actions { display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
|
|
.status { color:var(--muted); min-height:24px; margin-top:10px; }
|
|
.status.good { color:var(--good); } .status.bad { color:var(--bad); }
|
|
.columns { display:grid; grid-template-columns: 420px 1fr; gap:16px; }
|
|
.board { display:grid; grid-template-columns:repeat(5, 1fr); gap:8px; }
|
|
.cell { min-height:100px; border:1px solid var(--line); border-radius:13px; background:#0c1223; padding:8px; display:flex; flex-direction:column; justify-content:space-between; gap:8px; }
|
|
.task { font-weight:750; line-height:1.15; font-size:14px; }
|
|
.marks { display:grid; grid-template-columns:1fr 1fr; gap:5px; }
|
|
.marks button { padding:7px 5px; font-size:12px; background:#253151; color:var(--text); border:1px solid var(--line); }
|
|
.marks button.on.p1 { background:var(--blue); color:#03101a; }
|
|
.marks button.on.p2 { background:var(--green); color:#041409; }
|
|
.count { font-weight:800; color:var(--gold); }
|
|
.small { color:var(--muted); font-size:13px; line-height:1.35; }
|
|
@media (max-width: 920px) { .columns, .top { grid-template-columns:1fr; } header { display:block; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<header>
|
|
<div>
|
|
<h1>Bingo Editor</h1>
|
|
<div class="hint">Add up to 100 possible tasks, one per line. Generate a 5x5 blackout card from 25 random unique tasks. Use the board controls to mark squares completed by either player.</div>
|
|
</div>
|
|
<div class="actions"><a href="/overlay.html" target="_blank"><button class="secondary">Open Overlay</button></a></div>
|
|
</header>
|
|
|
|
<section class="panel top">
|
|
<div><label>Player 1 name</label><input id="player1" value="Player 1"></div>
|
|
<div><label>Player 2 name</label><input id="player2" value="Player 2"></div>
|
|
<div class="actions"><button id="saveBtn">Save Tasks</button><button id="generateBtn" class="blue">Generate Board</button></div>
|
|
</section>
|
|
|
|
<div class="columns">
|
|
<section class="panel">
|
|
<label>Task pool <span class="count" id="taskCount">0 / 100</span></label>
|
|
<textarea id="tasks" placeholder="Enter one task per line..."></textarea>
|
|
<div class="actions" style="margin-top:12px">
|
|
<button id="saveBtn2">Save Tasks</button>
|
|
<button id="sampleBtn" class="secondary">Load Sample 30</button>
|
|
<button id="clearTasksBtn" class="bad">Clear Tasks</button>
|
|
</div>
|
|
<div id="status" class="status"></div>
|
|
<p class="small">Tip: Generate Board resets all completion marks. Save Tasks keeps the current board if the board cells still exist.</p>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<div class="actions" style="justify-content:space-between;margin-bottom:12px">
|
|
<div><label style="margin:0">Current board</label><div class="small">Click player buttons under each square to toggle completion.</div></div>
|
|
<button id="clearMarksBtn" class="secondary">Clear Marks</button>
|
|
</div>
|
|
<div id="board" class="board"></div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let state = { players: ['Player 1', 'Player 2'], tasks: [], board: [] };
|
|
const sampleTasks = Array.from({length: 30}, (_, i) => [
|
|
'Win a boss fight','Find a hidden item','Get a critical hit','Survive with 1 HP','Defeat a rival','Catch a new teammate','Lose a teammate','Win without healing','Find a rare item','Complete a side objective','Beat a dungeon','Unlock a shortcut','Get poisoned','Learn a new move','Buy something useful','Win first try','Win after a reset','Use a status move','Defeat an optional enemy','Reach a new town','Trigger a cutscene','Get ambushed','Upgrade equipment','Finish a challenge room','Chat picks a choice','Complete a level','Find a secret door','Use only starter gear','Beat a timer','Make a risky choice'
|
|
][i]);
|
|
function getTasksFromBox() { return document.getElementById('tasks').value.split(/\r?\n/).map(t => t.trim()).filter(Boolean).slice(0, 100); }
|
|
function updateCount() { const n = document.getElementById('tasks').value.split(/\r?\n/).map(t => t.trim()).filter(Boolean).length; document.getElementById('taskCount').textContent = `${n} / 100`; document.getElementById('generateBtn').disabled = n < 25 || n > 100; }
|
|
function setStatus(msg, kind='') { const el = document.getElementById('status'); el.textContent = msg; el.className = 'status ' + kind; }
|
|
async function api(path, payload) { const res = await fetch(path, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload || {}) }); const json = await res.json(); if (!json.ok) throw new Error(json.error || 'Request failed'); return json.data; }
|
|
async function load() { const res = await fetch('/api/bingo?ts=' + Date.now(), {cache:'no-store'}); const json = await res.json(); state = json.data; document.getElementById('player1').value = state.players?.[0] || 'Player 1'; document.getElementById('player2').value = state.players?.[1] || 'Player 2'; document.getElementById('tasks').value = (state.tasks || []).join('\n'); updateCount(); renderBoard(); }
|
|
async function save() { state.players = [document.getElementById('player1').value.trim() || 'Player 1', document.getElementById('player2').value.trim() || 'Player 2']; state.tasks = getTasksFromBox(); state = await api('/api/bingo', state); setStatus('Saved bingo.json directly.', 'good'); renderBoard(); updateCount(); }
|
|
async function generate() { await save(); state = await api('/api/generate', {}); setStatus('Generated a new 5x5 board from 25 unique tasks.', 'good'); renderBoard(); }
|
|
async function mark(id, player) { state = await api('/api/mark', { cellId:id, player }); renderBoard(); }
|
|
async function clearMarks() { state.board = (state.board || []).map(c => ({...c, completedBy: []})); state = await api('/api/bingo', state); renderBoard(); setStatus('Cleared completion marks.', 'good'); }
|
|
function renderBoard() {
|
|
const board = document.getElementById('board');
|
|
const players = state.players || ['Player 1','Player 2'];
|
|
if (!state.board || state.board.length === 0) { board.innerHTML = '<div class="small">No board yet. Add at least 25 tasks and click Generate Board.</div>'; return; }
|
|
board.innerHTML = state.board.map(cell => {
|
|
const p1 = cell.completedBy?.includes(players[0]); const p2 = cell.completedBy?.includes(players[1]);
|
|
return `<div class="cell"><div class="task">${cell.task}</div><div class="marks"><button class="p1 ${p1?'on':''}" data-id="${cell.id}" data-player="${players[0]}">${players[0]}</button><button class="p2 ${p2?'on':''}" data-id="${cell.id}" data-player="${players[1]}">${players[1]}</button></div></div>`;
|
|
}).join('');
|
|
board.querySelectorAll('button[data-id]').forEach(btn => btn.addEventListener('click', () => mark(Number(btn.dataset.id), btn.dataset.player)));
|
|
}
|
|
document.getElementById('tasks').addEventListener('input', updateCount);
|
|
document.getElementById('saveBtn').addEventListener('click', () => save().catch(e => setStatus(e.message, 'bad')));
|
|
document.getElementById('saveBtn2').addEventListener('click', () => save().catch(e => setStatus(e.message, 'bad')));
|
|
document.getElementById('generateBtn').addEventListener('click', () => generate().catch(e => setStatus(e.message, 'bad')));
|
|
document.getElementById('clearMarksBtn').addEventListener('click', () => clearMarks().catch(e => setStatus(e.message, 'bad')));
|
|
document.getElementById('sampleBtn').addEventListener('click', () => { document.getElementById('tasks').value = sampleTasks.join('\n'); updateCount(); setStatus('Loaded sample tasks. Click Save or Generate Board.', ''); });
|
|
document.getElementById('clearTasksBtn').addEventListener('click', () => { document.getElementById('tasks').value = ''; updateCount(); setStatus('Task box cleared. Click Save to commit.', ''); });
|
|
load().catch(e => setStatus(e.message, 'bad'));
|
|
</script>
|
|
</body>
|
|
</html>
|