2PlayerGame/chapter2/index.html
2026-07-23 17:03:26 +03:00

532 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Тихий аукціон</title>
<style>
:root { --bg:#0c0d10; --panel:#15171c; --panel2:#1c1f26; --line:#2c313b; --txt:#cfd4dc; --dim:#7d8590; --amber:#d4a017; --cyan:#56b6c2; --red:#e06c75; --green:#7ec87e; }
* { box-sizing:border-box; margin:0; }
body { background:var(--bg); color:var(--txt); font:16px/1.5 Georgia,'Times New Roman',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; font-family:inherit; }
button:hover { border-color:var(--amber); }
button.primary { background:var(--amber); color:#0c0d10; 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); }
button.mini { padding:4px 10px; font-size:13px; }
select, textarea { background:var(--panel2); color:var(--txt); border:1px solid var(--line); border-radius:8px; padding:8px; font-size:14px; font-family:inherit; }
textarea { width:100%; min-height:60px; resize:vertical; }
.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); }
.card.starred { border-color:var(--amber); box-shadow:0 0 0 1px var(--amber); }
.stack { display:flex; flex-direction:column; gap:8px; }
.row { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
.spread { display:flex; justify-content:space-between; align-items:flex-start; gap:8px; }
.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); font-family:system-ui,sans-serif; }
.back { color:var(--cyan); background:none; border:none; padding:4px 0; font-size:14px; }
.crossed { text-decoration:line-through; opacity:.45; }
.act { color:var(--red); font-family:system-ui,sans-serif; font-size:.8em; letter-spacing:1px; }
#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; }
.overlay { position:fixed; inset:0; background:rgba(6,7,9,.94); 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); font-family:system-ui,sans-serif; }
.pill.field { color:var(--amber); }
.note { background:var(--panel2); border-radius:8px; padding:8px 10px; margin:6px 0; }
/* міні-ігри */
.mgbar { position:relative; height:46px; background:var(--panel2); border:1px solid var(--line); border-radius:8px; margin:16px 0; overflow:hidden; }
.zone { position:absolute; top:0; bottom:0; background:rgba(126,200,126,.30); border-left:1px solid var(--green); border-right:1px solid var(--green); }
.marker { position:absolute; top:0; bottom:0; width:4px; background:var(--amber); }
.symrow { display:flex; gap:8px; justify-content:center; margin-top:14px; flex-wrap:wrap; }
.symrow button { font-size:24px; padding:10px 16px; text-align:center; }
.symshow { font-size:52px; text-align:center; height:80px; line-height:80px; letter-spacing:8px; color:var(--cyan); }
</style>
</head>
<body>
<div id="app"></div>
<div id="mg"></div>
<div id="tabs" style="display:none"></div>
<script src="/case2.js"></script>
<script src="/minigames.js"></script>
<script>
const $ = s => document.getElementById(s);
const esc = t => String(t).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
let session = localStorage.getItem('np2_session') || '';
let role = localStorage.getItem('np2_role') || '';
let state = null;
let ui = { tab: 'invest', loc: null, person: null, briefed: false, accEv: [], detail: '', advSent: false };
let mg = null; // активна міні-гра: {kind:'search'|'code', ...} — живе в #mg, render() її не чіпає
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);
// авто-перехід в Акт II, коли докази вбивства на дошці
const pins = pinnedIds();
if (state.act === 1 && !ui.advSent && CASE.actGate.every(id => pins.includes(id))) {
ui.advSent = true;
send({ t: 'advance', act: 2 });
}
render();
renderInterstitial();
};
}
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 actOk = item => (item.act || 1) <= state.act;
const suspectName = id => id === 'general' ? 'Загальне' : CASE.suspects.find(s => s.id === id).name;
const crossedCls = id => state.crossed[id] ? 'crossed' : '';
const roleLabel = r => r === 'field' ? 'польовий' : 'аналітик';
// ---------- екрани ----------
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();
({ invest: role === 'field' ? renderField : renderAnalyst, notes: renderNotes, board: renderBoard, accuse: renderAccuse })[ui.tab]();
}
function renderJoin() {
$('tabs').style.display = 'none';
$('app').innerHTML = `
<div style="margin-top:12vh;text-align:center">
<h1>ТИХИЙ АУКЦІОН</h1>
<p class="dim" style="margin:8px 0 24px">Детектив на двох · Розділ 2 · Нуар</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('np2_session', session);
localStorage.setItem('np2_role', role);
connect();
}
function renderBriefing() {
$('app').innerHTML = `
<div class="overlay"><div class="inner">
<h1>СПРАВА: ТИХИЙ АУКЦІОН</h1>
<p style="margin:14px 0">${CASE.intro}</p>
<p class="act">${CASE.actNote}</p>
<h2>Твоя роль: ${CASE.roles[role].name}</h2>
<p>${CASE.roles[role].desc}</p>
<h2>Як грати</h2>
<p>${CASE.howto}</p>
<button class="primary" style="margin-top:20px;width:100%" onclick="ui.briefed=true;render()">Почати розслідування</button>
</div></div>`;
}
function renderInterstitial() {
if (mg || !state || !ui.briefed) return;
const key = 'np2_act2_' + session;
if (state.act >= 2 && !localStorage.getItem(key)) {
$('mg').innerHTML = `<div class="overlay"><div class="inner" style="text-align:center">
<h1 style="color:var(--red)">${CASE.act2title}</h1>
<p style="margin:16px 0;text-align:left">${CASE.act2text}</p>
<p class="dim" style="text-align:left">Відкрились нові локації, запити з ордером — і нові питання до старих знайомих.</p>
<button class="primary" style="margin-top:20px;width:100%" onclick="localStorage.setItem('${key}','1');document.getElementById('mg').innerHTML='';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">ТИХИЙ АУКЦІОН <span class="act">АКТ ${state.act === 1 ? 'I' : 'II'}</span></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 => actOk(l)
? `<button onclick="ui.loc='${l.id}';ui.detail='';render()"><b>${l.name}</b><br><span class="dim">${l.desc}</span></button>`
: `<button class="locked">🔒 <b>${l.name}</b><br><span class="dim">Доступ відкриється в Акті II — спершу доведіть, що це вбивство.</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 class="${crossedCls(pid)}">${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(); }
startSearchGame(sp);
}
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>
<div class="spread"><h2 class="${crossedCls(ui.person)}">${p.name}</h2>${p.suspect ? crossBtn(ui.person) : ''}</div><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 (!actOk(q)) html += `<button class="locked">🔒 ${q.label} — потрібен ордер (Акт II)</button>`;
else if (!unlocked(q.ev ? CASE.evidence[q.ev] : 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); }
startCodeGame(q);
}
function showQ(qid) {
const q = findQ(qid);
ui.detail = evCard(q.ev, CASE.evidence[q.ev], false);
render();
window.scrollTo(0, 1e6);
}
// ---------- міні-гра польового: сканер (зафіксуй маркер у зоні) ----------
function startSearchGame(sp) {
const hard = state.act >= 2;
mg = { kind: 'search', sp, period: hard ? 1300 : 1600, zoneW: hard ? 12 : 16, fails: 0 };
newZone();
$('mg').innerHTML = `<div class="overlay"><div class="inner">
<h2>Обшук: ${sp.label}</h2>
<p class="dim">Сканер ловить сигнал. Зафіксуй маркер у зеленій зоні.</p>
<div class="mgbar"><div class="zone" id="mgZone"></div><div class="marker" id="mgMarker"></div></div>
<div class="row">
<button class="primary" style="flex:1" onclick="searchFix()">Зафіксувати</button>
<button onclick="closeMg()">Відійти</button>
</div>
<p id="mgMsg" class="dim" style="margin-top:10px"></p>
</div></div>`;
drawZone();
mg.t0 = performance.now();
(function loop(now) {
if (!mg || mg.kind !== 'search') return;
$('mgMarker').style.left = triPos(now - mg.t0, mg.period) + '%';
mg.raf = requestAnimationFrame(loop);
})(performance.now());
}
function newZone() { mg.zoneStart = 8 + Math.random() * (84 - mg.zoneW); }
function drawZone() { const z = $('mgZone'); z.style.left = mg.zoneStart + '%'; z.style.width = mg.zoneW + '%'; }
function searchFix() {
const pos = triPos(performance.now() - mg.t0, mg.period);
if (inZone(pos, mg.zoneStart, mg.zoneW)) {
const sp = mg.sp;
closeMg();
ui.detail = evCard(sp.ev, CASE.evidence[sp.ev], true);
send({ t: 'find', id: sp.ev, by: role });
render();
} else {
mg.fails++;
$('mgMsg').textContent = 'Сигнал втрачено. Спокійно, ще раз.' + (mg.fails >= 3 ? ' (зона стала ширшою)' : '');
if (mg.fails === 3) mg.zoneW += 8; // ponytail: полегшення після 3 промахів, щоб гра не стала стіною
newZone(); drawZone();
}
}
// ---------- міні-гра аналітика: перехоплення коду (повтори послідовність) ----------
function startCodeGame(q) {
const len = state.act >= 2 ? 5 : 4;
mg = { kind: 'code', q, seq: seqGen(Math.floor(Math.random() * 1e9), len), input: [], showing: true, fails: 0 };
$('mg').innerHTML = `<div class="overlay"><div class="inner">
<h2>Код доступу: ${q.label}</h2>
<p class="dim">Система показує код. Запам'ятай і повтори послідовність.</p>
<div class="symshow" id="mgShow">—</div>
<div class="symrow" id="mgKeys">${MG_SYMBOLS.map((s, i) => `<button onclick="codePress(${i})">${s}</button>`).join('')}</div>
<div class="row" style="margin-top:14px">
<button onclick="codeReplay()">Показати ще раз</button>
<button onclick="closeMg()">Скасувати</button>
</div>
<p id="mgMsg" class="dim" style="margin-top:10px"></p>
</div></div>`;
codeShow();
}
function codeShow() {
mg.showing = true; mg.input = [];
$('mgMsg').textContent = '';
let i = 0;
(function step() {
if (!mg || mg.kind !== 'code') return;
if (i < mg.seq.length) {
$('mgShow').textContent = MG_SYMBOLS[mg.seq[i]];
i++;
mg.timer = setTimeout(() => { $('mgShow').textContent = '·'; mg.timer = setTimeout(step, 160); }, 520);
} else {
$('mgShow').textContent = '?';
mg.showing = false;
}
})();
}
function codeReplay() { if (mg && mg.kind === 'code' && !mg.showing) codeShow(); }
function codePress(i) {
if (!mg || mg.kind !== 'code' || mg.showing) return;
mg.input.push(i);
if (!seqPrefixOk(mg.seq, mg.input)) {
mg.fails++;
$('mgMsg').textContent = 'Невірний символ. Система повторює код.' + (mg.fails >= 3 ? ' (показ уповільнено)' : '');
codeShow();
return;
}
$('mgShow').textContent = mg.input.map(x => MG_SYMBOLS[x]).join('');
if (mg.input.length === mg.seq.length) {
const q = mg.q;
closeMg();
ui.detail = evCard(q.ev, CASE.evidence[q.ev], true);
send({ t: 'find', id: q.ev, by: role });
render();
window.scrollTo(0, 1e6);
}
}
function closeMg() {
if (mg) { if (mg.raf) cancelAnimationFrame(mg.raf); if (mg.timer) clearTimeout(mg.timer); }
mg = null;
$('mg').innerHTML = '';
renderInterstitial();
}
// ---------- записник ----------
function crossBtn(sid) {
return `<button class="mini" onclick="send({t:'cross',s:'${sid}'})">${state.crossed[sid] ? '↩ Повернути' : '✗ Відкинути'}</button>`;
}
function starBtn(id) {
return `<button class="mini" onclick="send({t:'star',id:'${id}'})">${state.stars[id] ? '★' : '☆'}</button>`;
}
function renderNotes() {
let html = window._header + `<h2>Нотатки <span class="dim">(спільні)</span></h2>`;
for (const n of state.notes) {
html += `<div class="note spread"><span>${esc(n.txt)} <span class="pill ${n.by}">${roleLabel(n.by)}</span></span>
<button class="mini" onclick="send({t:'note_del',id:${n.id}})">✕</button></div>`;
}
html += `<textarea id="noteTxt" placeholder="Версії, таймлайн, питання напарнику..."></textarea>
<button class="primary" style="margin-top:6px" onclick="addNote()">Додати нотатку</button>`;
const mine = Object.keys(state.found).filter(id => CASE.evidence[id].role === role);
html += `<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} ${state.stars[id] ? 'starred' : ''}">
<div class="spread"><h3>${ev.title} <span class="tag">${ev.tag}</span></h3>${starBtn(id)}</div>
<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 class="${crossedCls(pid)}">${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><p class="act" style="margin-top:8px">${CASE.actNote}</p></div>
<h2>Підозрювані</h2>` + CASE.suspects.map(s =>
`<div class="card"><div class="spread"><span><b class="${crossedCls(s.id)}">${s.name}</b> <span class="tag">${s.role}</span></span>${crossBtn(s.id)}</div><p class="dim ${crossedCls(s.id)}">${s.desc}</p></div>`).join('');
$('app').innerHTML = html;
}
function addNote() {
const txt = $('noteTxt').value.trim();
if (!txt) return;
send({ t: 'note', txt, by: role });
$('noteTxt').value = '';
}
// ---------- дошка ----------
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 (g === 'general') { if (!pins.length) continue; html += `<h3>Загальне</h3>`; }
else {
const s = CASE.suspects.find(x => x.id === g);
html += `<div class="spread"><h3 class="${crossedCls(g)}">${s.name} <span class="tag">${s.role}</span></h3>${crossBtn(g)}</div>`;
if (!pins.length) { html += `<p class="dim">— нічого не закріплено</p>`; continue; }
}
for (const p of pins) {
const ev = CASE.evidence[p.id];
html += `<div class="card ev ${ev.role} ${state.stars[p.id] ? 'starred' : ''}">
<div class="spread"><h3>${ev.title} <span class="tag">${ev.tag}</span> <span class="pill ${ev.role}">${roleLabel(ev.role)}</span></h3>${starBtn(p.id)}</div>
<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 (state.act === 1) html += `<div class="banner bad"><b>Зарано.</b><p>Прокурор не прийме звинувачення, поки не доведено сам факт вбивства. Завершіть Акт I.</p></div>`;
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}${state.crossed[s.id] ? ' (викреслений)' : ''}</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>${state.stars[p.id] ? ' ★' : ''}</span></label>`;
}
html += `</div><button class="primary" style="width:100%;margin-top:16px" ${state.act === 1 ? 'disabled style="width:100%;margin-top:16px;opacity:.4"' : ''} 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 (state.act === 1) return;
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" style="position:static;background:none"><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('np2_session');localStorage.removeItem('np2_act2_'+session);location.reload()">Нова гра</button>
</div></div>`;
}
// ---------- старт ----------
if (session && role) connect(); else render();
</script>
</body>
</html>