Files

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>