361 lines
18 KiB
HTML
361 lines
18 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="uk">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|||
|
|
<title>Nightshade Protocol</title>
|
|||
|
|
<style>
|
|||
|
|
:root { --bg:#0b0e14; --panel:#131824; --panel2:#1a2130; --line:#2a3347; --txt:#c9d2e3; --dim:#7a8699; --amber:#e3b341; --cyan:#4dd0e1; --red:#e06c75; --green:#7ec87e; }
|
|||
|
|
* { box-sizing:border-box; margin:0; }
|
|||
|
|
body { background:var(--bg); color:var(--txt); font:16px/1.5 system-ui,sans-serif; min-height:100vh; }
|
|||
|
|
#app { max-width:760px; margin:0 auto; padding:12px; padding-bottom:80px; }
|
|||
|
|
h1,h2,h3 { font-weight:600; }
|
|||
|
|
h1 { color:var(--amber); letter-spacing:2px; font-size:1.4em; }
|
|||
|
|
h2 { font-size:1.15em; margin:14px 0 8px; color:var(--amber); }
|
|||
|
|
h3 { font-size:1em; margin:10px 0 6px; }
|
|||
|
|
button { background:var(--panel2); color:var(--txt); border:1px solid var(--line); border-radius:8px; padding:10px 14px; font-size:15px; cursor:pointer; text-align:left; }
|
|||
|
|
button:hover { border-color:var(--amber); }
|
|||
|
|
button.primary { background:var(--amber); color:#0b0e14; font-weight:600; text-align:center; }
|
|||
|
|
button.locked { color:var(--dim); border-style:dashed; cursor:default; }
|
|||
|
|
button.locked:hover { border-color:var(--line); }
|
|||
|
|
button.done { border-color:var(--green); }
|
|||
|
|
select { background:var(--panel2); color:var(--txt); border:1px solid var(--line); border-radius:8px; padding:8px; font-size:14px; }
|
|||
|
|
.card { background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:12px; margin:8px 0; }
|
|||
|
|
.card.ev { border-left:3px solid var(--cyan); }
|
|||
|
|
.card.ev.field { border-left-color:var(--amber); }
|
|||
|
|
.stack { display:flex; flex-direction:column; gap:8px; }
|
|||
|
|
.row { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
|
|||
|
|
.dim { color:var(--dim); font-size:0.88em; }
|
|||
|
|
.tag { display:inline-block; font-size:0.75em; padding:1px 8px; border-radius:10px; border:1px solid var(--line); color:var(--dim); }
|
|||
|
|
.back { color:var(--cyan); background:none; border:none; padding:4px 0; font-size:14px; }
|
|||
|
|
#tabs { position:fixed; bottom:0; left:0; right:0; display:flex; background:var(--panel); border-top:1px solid var(--line); z-index:5; }
|
|||
|
|
#tabs button { flex:1; border:none; border-radius:0; text-align:center; padding:12px 4px; font-size:13px; background:none; }
|
|||
|
|
#tabs button.active { color:var(--amber); border-top:2px solid var(--amber); }
|
|||
|
|
.banner { border-radius:10px; padding:12px; margin:10px 0; }
|
|||
|
|
.banner.bad { background:#2a1518; border:1px solid var(--red); }
|
|||
|
|
.banner.good { background:#152a18; border:1px solid var(--green); }
|
|||
|
|
.quote { border-left:2px solid var(--line); padding-left:10px; margin:8px 0; color:var(--txt); }
|
|||
|
|
.overlay { position:fixed; inset:0; background:rgba(5,7,12,.93); z-index:10; overflow-y:auto; padding:20px; }
|
|||
|
|
.overlay .inner { max-width:600px; margin:40px auto; }
|
|||
|
|
input[type=text] { background:var(--panel2); color:var(--txt); border:1px solid var(--line); border-radius:8px; padding:12px; font-size:18px; width:100%; text-transform:uppercase; letter-spacing:3px; text-align:center; }
|
|||
|
|
label.chk { display:flex; gap:10px; align-items:flex-start; padding:8px; border:1px solid var(--line); border-radius:8px; cursor:pointer; }
|
|||
|
|
label.chk input { margin-top:4px; }
|
|||
|
|
.pill { font-size:0.8em; color:var(--cyan); }
|
|||
|
|
.pill.field { color:var(--amber); }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div id="app"></div>
|
|||
|
|
<div id="tabs" style="display:none"></div>
|
|||
|
|
<script src="/case.js"></script>
|
|||
|
|
<script>
|
|||
|
|
const $ = s => document.getElementById(s);
|
|||
|
|
const esc = t => t.replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|||
|
|
|
|||
|
|
let session = localStorage.getItem('np_session') || '';
|
|||
|
|
let role = localStorage.getItem('np_role') || '';
|
|||
|
|
let state = null;
|
|||
|
|
let ui = { tab: 'invest', loc: null, person: null, briefed: false, accEv: [], detail: '' };
|
|||
|
|
|
|||
|
|
function send(a) {
|
|||
|
|
fetch('/act', { method: 'POST', body: JSON.stringify({ session, ...a }) });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function connect() {
|
|||
|
|
const es = new EventSource('/events?s=' + encodeURIComponent(session) + '&role=' + role);
|
|||
|
|
es.onmessage = e => { state = JSON.parse(e.data); render(); };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const pinnedIds = () => state.pinned.map(p => p.id);
|
|||
|
|
const isFound = id => !!state.found[id];
|
|||
|
|
const unlocked = item => !item.requires || item.requires.every(r => pinnedIds().includes(r));
|
|||
|
|
const suspectName = id => id === 'general' ? 'Загальне' : CASE.suspects.find(s => s.id === id).name;
|
|||
|
|
|
|||
|
|
// ---------- екрани ----------
|
|||
|
|
|
|||
|
|
function render() {
|
|||
|
|
if (!session || !role) return renderJoin();
|
|||
|
|
if (!state) return;
|
|||
|
|
if (!ui.briefed) return renderBriefing();
|
|||
|
|
$('tabs').style.display = 'flex';
|
|||
|
|
if (state.solved) return renderVictory();
|
|||
|
|
renderTabs();
|
|||
|
|
const t = { invest: role === 'field' ? renderField : renderAnalyst, notes: renderNotes, board: renderBoard, accuse: renderAccuse }[ui.tab];
|
|||
|
|
t();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderJoin() {
|
|||
|
|
$('tabs').style.display = 'none';
|
|||
|
|
$('app').innerHTML = `
|
|||
|
|
<div style="margin-top:12vh;text-align:center">
|
|||
|
|
<h1>NIGHTSHADE PROTOCOL</h1>
|
|||
|
|
<p class="dim" style="margin:8px 0 24px">Детектив на двох · Меридіан, 2087</p>
|
|||
|
|
<div class="stack" style="max-width:340px;margin:0 auto">
|
|||
|
|
<input type="text" id="code" maxlength="12" placeholder="КОД СЕСІЇ" value="${esc(session)}">
|
|||
|
|
<p class="dim">Придумайте будь-який код і введіть однаковий на обох пристроях.</p>
|
|||
|
|
<button class="primary" onclick="pickRole('field')">Грати як ${CASE.roles.field.name}</button>
|
|||
|
|
<button class="primary" style="background:var(--cyan)" onclick="pickRole('analyst')">Грати як ${CASE.roles.analyst.name}</button>
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function pickRole(r) {
|
|||
|
|
const code = $('code').value.trim().toUpperCase();
|
|||
|
|
if (!code) return alert('Введіть код сесії');
|
|||
|
|
session = code; role = r;
|
|||
|
|
localStorage.setItem('np_session', session);
|
|||
|
|
localStorage.setItem('np_role', role);
|
|||
|
|
send({ t: 'join', role });
|
|||
|
|
connect();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderBriefing() {
|
|||
|
|
$('app').innerHTML = `
|
|||
|
|
<div class="overlay"><div class="inner">
|
|||
|
|
<h1>СПРАВА: NIGHTSHADE PROTOCOL</h1>
|
|||
|
|
<p style="margin:14px 0">${CASE.intro}</p>
|
|||
|
|
<h2>Твоя роль: ${CASE.roles[role].name}</h2>
|
|||
|
|
<p>${CASE.roles[role].desc}</p>
|
|||
|
|
<h2>Як грати</h2>
|
|||
|
|
<p>${CASE.howto}</p>
|
|||
|
|
<p style="margin-top:10px" class="dim">Мета: разом висунути звинувачення — хто, яким способом, і які докази це доводять.</p>
|
|||
|
|
<button class="primary" style="margin-top:20px;width:100%" onclick="ui.briefed=true;render()">Почати розслідування</button>
|
|||
|
|
</div></div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderTabs() {
|
|||
|
|
const online = state.online || {};
|
|||
|
|
const other = role === 'field' ? 'analyst' : 'field';
|
|||
|
|
const tabs = [
|
|||
|
|
['invest', role === 'field' ? 'Локації' : 'Системи'],
|
|||
|
|
['notes', 'Записник'],
|
|||
|
|
['board', 'Дошка (' + state.pinned.length + ')'],
|
|||
|
|
['accuse', 'Звинувачення']
|
|||
|
|
];
|
|||
|
|
$('tabs').innerHTML = tabs.map(([id, name]) =>
|
|||
|
|
`<button class="${ui.tab === id ? 'active' : ''}" onclick="ui.tab='${id}';ui.loc=null;ui.person=null;ui.detail='';render()">${name}</button>`).join('');
|
|||
|
|
window._header = `
|
|||
|
|
<div class="row" style="justify-content:space-between">
|
|||
|
|
<h1 style="font-size:1.05em">NIGHTSHADE</h1>
|
|||
|
|
<span class="dim">${session} · ${CASE.roles[role].name} · напарник: ${online[other] ? '<span style="color:var(--green)">онлайн</span>' : 'офлайн'}</span>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- польовий ----------
|
|||
|
|
|
|||
|
|
function renderField() {
|
|||
|
|
if (ui.person) return renderPerson();
|
|||
|
|
if (ui.loc) return renderLocation();
|
|||
|
|
$('app').innerHTML = window._header + `<h2>Локації</h2><div class="stack">` +
|
|||
|
|
CASE.locations.map(l => `<button onclick="ui.loc='${l.id}';ui.detail='';render()"><b>${l.name}</b><br><span class="dim">${l.desc}</span></button>`).join('') + `</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderLocation() {
|
|||
|
|
const l = CASE.locations.find(x => x.id === ui.loc);
|
|||
|
|
let html = window._header + `<button class="back" onclick="ui.loc=null;ui.detail='';render()">← Локації</button><h2>${l.name}</h2><p class="dim">${l.desc}</p>`;
|
|||
|
|
if (l.spots.length) {
|
|||
|
|
html += `<h3>Огляд</h3><div class="stack">` + l.spots.map(sp => {
|
|||
|
|
if (sp.ev && isFound(sp.ev)) return `<button class="done" onclick="showEv('${sp.ev}')">✓ ${sp.label}</button>`;
|
|||
|
|
return `<button onclick="inspect('${l.id}','${sp.id}')">${sp.label}</button>`;
|
|||
|
|
}).join('') + `</div>`;
|
|||
|
|
}
|
|||
|
|
if (l.people.length) {
|
|||
|
|
html += `<h3>Люди</h3><div class="stack">` + l.people.map(pid => {
|
|||
|
|
const p = CASE.people[pid];
|
|||
|
|
return `<button onclick="ui.person='${pid}';render()"><b>${p.name}</b>${p.suspect ? ' <span class="tag">підозрюваний</span>' : ''}</button>`;
|
|||
|
|
}).join('') + `</div>`;
|
|||
|
|
}
|
|||
|
|
html += `<div id="detail">${ui.detail}</div>`;
|
|||
|
|
$('app').innerHTML = html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function inspect(locId, spotId) {
|
|||
|
|
const sp = CASE.locations.find(x => x.id === locId).spots.find(x => x.id === spotId);
|
|||
|
|
if (sp.flavor) { ui.detail = `<div class="card"><p>${sp.flavor}</p></div>`; return render(); }
|
|||
|
|
ui.detail = evCard(sp.ev, CASE.evidence[sp.ev], true);
|
|||
|
|
send({ t: 'find', id: sp.ev, by: role });
|
|||
|
|
render();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showEv(id) {
|
|||
|
|
ui.detail = evCard(id, CASE.evidence[id], false);
|
|||
|
|
render();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function evCard(id, ev, isNew) {
|
|||
|
|
return `<div class="card ev ${ev.role}">
|
|||
|
|
${isNew ? '<p class="dim">НОВИЙ ДОКАЗ</p>' : ''}
|
|||
|
|
<h3>${ev.title} <span class="tag">${ev.tag}</span></h3>
|
|||
|
|
<p>${ev.detail}</p>
|
|||
|
|
<p class="dim" style="margin-top:6px">Закріпи на дошці через Записник, щоб напарник побачив.</p>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderPerson() {
|
|||
|
|
const p = CASE.people[ui.person];
|
|||
|
|
const asked = state.asked[ui.person] || [];
|
|||
|
|
let html = window._header + `<button class="back" onclick="ui.person=null;render()">← Назад</button><h2>${p.name}</h2><div class="stack">`;
|
|||
|
|
for (const t of p.topics) {
|
|||
|
|
if (asked.includes(t.id)) {
|
|||
|
|
html += `<div class="card"><b>— ${t.q}</b><div class="quote">${t.a}</div></div>`;
|
|||
|
|
} else if (unlocked(t)) {
|
|||
|
|
html += `<button onclick="ask('${ui.person}','${t.id}')">— ${t.q}</button>`;
|
|||
|
|
} else {
|
|||
|
|
html += `<button class="locked">🔒 Нова тема з'явиться, коли потрібні докази будуть на дошці</button>`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
$('app').innerHTML = html + `</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ask(pid, tid) {
|
|||
|
|
const t = CASE.people[pid].topics.find(x => x.id === tid);
|
|||
|
|
send({ t: 'ask', s: pid, q: tid });
|
|||
|
|
if (t.gives) send({ t: 'find', id: t.gives, by: role });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- аналітик ----------
|
|||
|
|
|
|||
|
|
function renderAnalyst() {
|
|||
|
|
let html = window._header + `<h2>Системи Меридіана</h2>`;
|
|||
|
|
for (const sys of CASE.systems) {
|
|||
|
|
html += `<h3>${sys.name}</h3><div class="stack">`;
|
|||
|
|
for (const q of sys.queries) {
|
|||
|
|
if (q.ev && isFound(q.ev)) html += `<button class="done" onclick="showQ('${q.id}')">✓ ${q.label}</button>`;
|
|||
|
|
else if (!unlocked(q)) html += `<button class="locked">🔒 Запит стане доступним, коли потрібні матеріали будуть на дошці</button>`;
|
|||
|
|
else html += `<button onclick="runQ('${q.id}')">▸ ${q.label}</button>`;
|
|||
|
|
}
|
|||
|
|
html += `</div>`;
|
|||
|
|
}
|
|||
|
|
$('app').innerHTML = html + `<div id="detail">${ui.detail}</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function findQ(qid) { for (const s of CASE.systems) for (const q of s.queries) if (q.id === qid) return q; }
|
|||
|
|
|
|||
|
|
function runQ(qid) {
|
|||
|
|
const q = findQ(qid);
|
|||
|
|
if (q.flavor) { ui.detail = `<div class="card"><b>${q.label}</b><p>${q.flavor}</p></div>`; render(); return window.scrollTo(0, 1e6); }
|
|||
|
|
ui.detail = evCard(q.ev, CASE.evidence[q.ev], true);
|
|||
|
|
send({ t: 'find', id: q.ev, by: role });
|
|||
|
|
render();
|
|||
|
|
window.scrollTo(0, 1e6);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showQ(qid) {
|
|||
|
|
const q = findQ(qid);
|
|||
|
|
ui.detail = evCard(q.ev, CASE.evidence[q.ev], false);
|
|||
|
|
render();
|
|||
|
|
window.scrollTo(0, 1e6);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- записник ----------
|
|||
|
|
|
|||
|
|
function renderNotes() {
|
|||
|
|
const mine = Object.keys(state.found).filter(id => CASE.evidence[id].role === role);
|
|||
|
|
let html = window._header + `<h2>Мої докази (${mine.length})</h2>`;
|
|||
|
|
if (!mine.length) html += `<p class="dim">Поки нічого. ${role === 'field' ? 'Оглянь локації і поговори з людьми.' : 'Прожени запити по системах.'}</p>`;
|
|||
|
|
for (const id of mine) {
|
|||
|
|
const ev = CASE.evidence[id];
|
|||
|
|
const pin = state.pinned.find(p => p.id === id);
|
|||
|
|
html += `<div class="card ev ${ev.role}">
|
|||
|
|
<h3>${ev.title} <span class="tag">${ev.tag}</span></h3>
|
|||
|
|
<p>${ev.detail}</p>
|
|||
|
|
<div class="row" style="margin-top:8px">` +
|
|||
|
|
(pin ? `<span class="dim">📌 На дошці: ${suspectName(pin.s)}</span>`
|
|||
|
|
: `<select id="pin_${id}"><option value="general">Загальне</option>${CASE.suspects.map(s => `<option value="${s.id}">${s.name}</option>`).join('')}</select>
|
|||
|
|
<button onclick="send({t:'pin',id:'${id}',s:document.getElementById('pin_${id}').value,by:role})">📌 На дошку</button>`) +
|
|||
|
|
`</div></div>`;
|
|||
|
|
}
|
|||
|
|
// журнал допитів — спільний
|
|||
|
|
const askedAny = Object.entries(state.asked).filter(([, v]) => v.length);
|
|||
|
|
if (askedAny.length) {
|
|||
|
|
html += `<h2>Журнал допитів</h2>`;
|
|||
|
|
for (const [pid, tids] of askedAny) {
|
|||
|
|
html += `<div class="card"><h3>${CASE.people[pid].name}</h3>` +
|
|||
|
|
tids.map(tid => { const t = CASE.people[pid].topics.find(x => x.id === tid); return `<p style="margin-top:6px"><b>— ${t.q}</b></p><div class="quote">${t.a}</div>`; }).join('') + `</div>`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
html += `<h2>Фабула</h2><div class="card"><p>${CASE.intro}</p></div>
|
|||
|
|
<h2>Підозрювані</h2>` + CASE.suspects.map(s => `<div class="card"><b>${s.name}</b> <span class="tag">${s.role}</span><p class="dim">${s.desc}</p></div>`).join('');
|
|||
|
|
$('app').innerHTML = html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- дошка ----------
|
|||
|
|
|
|||
|
|
function renderBoard() {
|
|||
|
|
let html = window._header + `<h2>Дошка справи</h2><p class="dim">Спільна для обох. Тут видно все, що закріпили ви й напарник.</p>`;
|
|||
|
|
const groups = ['general', ...CASE.suspects.map(s => s.id)];
|
|||
|
|
for (const g of groups) {
|
|||
|
|
const pins = state.pinned.filter(p => p.s === g);
|
|||
|
|
if (!pins.length && g === 'general') continue;
|
|||
|
|
if (g !== 'general') {
|
|||
|
|
const s = CASE.suspects.find(x => x.id === g);
|
|||
|
|
html += `<h3>${s.name} <span class="tag">${s.role}</span></h3>`;
|
|||
|
|
if (!pins.length) { html += `<p class="dim">— нічого не закріплено</p>`; continue; }
|
|||
|
|
} else html += `<h3>Загальне</h3>`;
|
|||
|
|
for (const p of pins) {
|
|||
|
|
const ev = CASE.evidence[p.id];
|
|||
|
|
html += `<div class="card ev ${ev.role}">
|
|||
|
|
<h3>${ev.title} <span class="tag">${ev.tag}</span> <span class="pill ${ev.role}">${ev.role === 'field' ? 'польовий' : 'аналітик'}</span></h3>
|
|||
|
|
<p>${ev.detail}</p>
|
|||
|
|
<div class="row" style="margin-top:8px">
|
|||
|
|
<select onchange="send({t:'repin',id:'${p.id}',s:this.value})">${groups.map(x => `<option value="${x}" ${x === p.s ? 'selected' : ''}>${suspectName(x)}</option>`).join('')}</select>
|
|||
|
|
<button onclick="send({t:'unpin',id:'${p.id}'})">Зняти</button>
|
|||
|
|
</div></div>`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (!state.pinned.length) html += `<p class="dim" style="margin-top:20px">Дошка порожня. Закріплюйте докази із Записника — це відкриває нові питання й запити.</p>`;
|
|||
|
|
$('app').innerHTML = html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- звинувачення ----------
|
|||
|
|
|
|||
|
|
function renderAccuse() {
|
|||
|
|
const last = state.lastAccusation;
|
|||
|
|
let html = window._header + `<h2>Висунути звинувачення</h2>
|
|||
|
|
<p class="dim">Оберіть разом: хто, яким способом, і 3–${CASE.finale.maxEvidence} докази З ДОШКИ, що це доводять. Помилка — не кінець: отримаєте підказку, яка ланка не сходиться.</p>`;
|
|||
|
|
if (last && !last.result.ok)
|
|||
|
|
html += `<div class="banner bad"><b>Спроба №${last.n} — відхилено.</b><p>${last.result.hint}</p></div>`;
|
|||
|
|
html += `<h3>Вбивця</h3><select id="acc_s">${CASE.suspects.map(s => `<option value="${s.id}">${s.name} — ${s.role}</option>`).join('')}</select>
|
|||
|
|
<h3>Спосіб</h3><select id="acc_m" style="max-width:100%">${CASE.methods.map(m => `<option value="${m.id}">${m.label}</option>`).join('')}</select>
|
|||
|
|
<h3>Докази (з дошки, до ${CASE.finale.maxEvidence})</h3><div class="stack">`;
|
|||
|
|
if (!state.pinned.length) html += `<p class="dim">На дошці порожньо — спершу закріпіть докази.</p>`;
|
|||
|
|
for (const p of state.pinned) {
|
|||
|
|
const ev = CASE.evidence[p.id];
|
|||
|
|
html += `<label class="chk"><input type="checkbox" value="${p.id}" ${ui.accEv.includes(p.id) ? 'checked' : ''} onchange="toggleAccEv(this)"><span><b>${ev.title}</b> <span class="tag">${ev.tag}</span></span></label>`;
|
|||
|
|
}
|
|||
|
|
html += `</div><button class="primary" style="width:100%;margin-top:16px" onclick="submitAccusation()">⚖ Висунути звинувачення</button>`;
|
|||
|
|
$('app').innerHTML = html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toggleAccEv(cb) {
|
|||
|
|
if (cb.checked) {
|
|||
|
|
if (ui.accEv.length >= CASE.finale.maxEvidence) { cb.checked = false; return alert('Не більше ' + CASE.finale.maxEvidence + ' доказів. Оберіть найсильніші.'); }
|
|||
|
|
ui.accEv.push(cb.value);
|
|||
|
|
} else ui.accEv = ui.accEv.filter(x => x !== cb.value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function submitAccusation() {
|
|||
|
|
if (ui.accEv.length < 3) return alert('Потрібно щонайменше 3 докази.');
|
|||
|
|
const suspect = $('acc_s').value, method = $('acc_m').value;
|
|||
|
|
const result = checkAccusation(suspect, method, ui.accEv);
|
|||
|
|
send({ t: 'accuse', suspect, method, evidence: ui.accEv, result });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderVictory() {
|
|||
|
|
$('tabs').style.display = 'none';
|
|||
|
|
const a = state.lastAccusation;
|
|||
|
|
$('app').innerHTML = `<div class="overlay"><div class="inner">
|
|||
|
|
<h1 style="color:var(--green)">СПРАВУ РОЗКРИТО</h1>
|
|||
|
|
<div class="banner good"><b>${CASE.suspects.find(s => s.id === a.suspect).name}</b> — винна. Спроб: ${a.n}. Доказів знайдено: ${Object.keys(state.found).length} з ${Object.keys(CASE.evidence).length}.</div>
|
|||
|
|
<h2>Як усе було</h2>
|
|||
|
|
<p>${CASE.finale.recap}</p>
|
|||
|
|
<button class="primary" style="margin-top:20px;width:100%" onclick="localStorage.removeItem('np_session');location.reload()">Нова гра</button>
|
|||
|
|
</div></div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- старт ----------
|
|||
|
|
if (session && role) connect(); else render();
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|