2PlayerGame/detective-game/public/index.html
UndecimoDia cb93544793 Fixes
2026-08-15 18:04:59 +03:00

289 lines
22 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>Невпізнаний — детектив на двох, розділ 3</title>
<style>
:root {
--night:#0b0d12; --panel:#12151c; --panel2:#1a1f29; --line:#262c38;
--txt:#cfd6df; --dim:#7d8794; --acc:#e8b04b; --red:#c94f4f; --green:#5aa870; --blue:#5b8dbf;
--paper:#e9ddc0; --paper2:#dcCfae; --ink:#282218; --inkdim:#6f6450; --paperline:#c3b28b;
--amber:#ffb454; --amberdim:#8a6a34; --crt:#0e0903;
/* Фіолетовий замість синього: #5b8dbf і зелений #5aa870 мали майже однакову
яскравість, і на 4px смужці лівої грані рядка різниця не читалася зовсім. */
--mk-green:#5aa870; --mk-red:#c94f4f; --mk-violet:#a25ce0; --mk-amber:#e8b04b;
--noise:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .05 0'/></filter><rect width='140' height='140' filter='url(%23n)'/></svg>");
}
* { box-sizing:border-box; margin:0; }
html { accent-color:var(--acc); }
body { background:var(--night); color:var(--txt); font:15px/1.45 Georgia, 'Times New Roman', serif; height:100vh; overflow:hidden; }
button { font:inherit; color:var(--txt); background:var(--panel2); border:1px solid var(--line); border-radius:4px; padding:6px 12px; cursor:pointer; }
button:hover { border-color:var(--acc); }
button.primary { background:var(--acc); color:#14100a; font-weight:bold; }
button.danger { background:var(--red); color:#fff; }
button:disabled { opacity:.5; cursor:default; }
input, select, textarea { font:inherit; color:var(--txt); background:#0d1116; border:1px solid var(--line); border-radius:4px; padding:6px 10px; }
input:focus, textarea:focus { outline:none; border-color:var(--acc); }
h3 { font-size:15px; color:var(--acc); margin-bottom:6px; letter-spacing:.4px; }
.dim { color:var(--dim); } .small { font-size:12.5px; }
ul { padding-left:18px; } li { margin:4px 0; }
/* ---------- Лобі: дощова ніч ---------- */
#lobby { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; z-index:50; overflow:hidden;
background:
radial-gradient(ellipse at 20% 110%, rgba(232,176,75,.10), transparent 45%),
radial-gradient(ellipse at 85% 105%, rgba(91,141,191,.09), transparent 40%),
linear-gradient(180deg, #05070c 0%, #0a0e16 55%, #11141c 100%); }
#lobby::before { content:''; position:absolute; inset:-60px; pointer-events:none; opacity:.5;
background:repeating-linear-gradient(103deg, transparent 0 16px, rgba(175,195,220,.05) 16px 17px);
animation:rainfall 0.9s linear infinite; }
@keyframes rainfall { to { transform:translate(-34px, 120px); } }
#lobby::after { content:''; position:absolute; left:0; right:0; bottom:0; height:26%; pointer-events:none; opacity:.9;
background:
linear-gradient(0deg, #04060a 30%, transparent),
repeating-linear-gradient(90deg, transparent 0 46px, rgba(20,26,36,.9) 46px 118px, transparent 118px 150px); }
#lobby .card { position:relative; z-index:2; background:rgba(16,19,27,.94); border:1px solid var(--line); border-radius:10px;
padding:36px 40px; width:460px; box-shadow:0 30px 80px rgba(0,0,0,.7), inset 0 0 60px rgba(0,0,0,.35); }
#lobby h1 { font-size:34px; color:var(--txt); letter-spacing:8px; margin-bottom:2px;
text-shadow:0 0 22px rgba(201,79,79,.5), 0 2px 0 #000; }
#lobby h1::after { content:'СПРАВУ ВІДКРИТО'; display:block; font-size:10px; letter-spacing:4px; color:var(--red);
border:1px solid var(--red); width:max-content; padding:2px 8px; margin-top:8px; transform:rotate(-2deg); opacity:.85; }
#lobby .row { margin:16px 0; display:flex; gap:8px; }
#lobby input { flex:1; text-transform:uppercase; letter-spacing:2px; }
.rolebtn { flex:1; padding:12px; text-align:left; transition:transform .12s; }
.rolebtn:hover { transform:translateY(-2px); }
.rolebtn b { display:block; color:var(--acc); }
.rolebtn[data-role="analyst"] b { color:var(--amber); font-family:'Courier New', monospace; letter-spacing:1px; }
/* ---------- Каркас ---------- */
#app { display:none; height:100vh; flex-direction:column; }
#app.on { display:flex; }
header { display:flex; align-items:center; gap:14px; padding:8px 14px; background:linear-gradient(180deg, #171b24, #121620);
border-bottom:1px solid var(--line); box-shadow:0 4px 16px rgba(0,0,0,.4); }
header .title { color:var(--txt); font-weight:bold; font-size:16px; letter-spacing:4px; text-shadow:0 0 14px rgba(201,79,79,.4); }
.badge { font-size:12px; border:1px solid var(--line); border-radius:2px; padding:2px 9px; color:var(--dim); letter-spacing:.5px; }
.badge.on { color:var(--green); border-color:var(--green); }
#main { flex:1; display:flex; min-height:0; }
#stage { flex:1; min-width:0; display:flex; flex-direction:column; padding:12px; gap:8px; overflow:auto; }
/* Дошка — недоторканний шар: модалка (z-index:40) її НЕ накриває, інакше будь-який
обшук чи допит відрізав би обох гравців від доказів саме тоді, коли вони потрібні. */
#side { width:400px; border-left:1px solid #1c130c; display:flex; flex-direction:column; min-height:0;
position:relative; z-index:45;
background:linear-gradient(160deg, #33261a, #2a1f14 60%, #241a10); background-blend-mode:multiply; }
#side::before { content:''; position:absolute; } /* резерв */
/* ---------- Дошка: корок і папір ---------- */
.tabs { display:flex; border-bottom:2px solid rgba(0,0,0,.35); background:rgba(0,0,0,.18); }
.tabs button { flex:1; border:none; border-radius:0; background:none; padding:9px 4px; color:#b7a284; border-bottom:2px solid transparent; letter-spacing:1px; }
.tabs button.on { color:var(--paper); border-bottom-color:var(--acc); text-shadow:0 1px 0 #000; }
.tabbody { flex:1; overflow-y:auto; padding:12px 12px 20px; background-image:var(--noise); }
.tabbody > .dim { color:#c9b596; text-shadow:0 1px 0 rgba(0,0,0,.5); }
#side .card { position:relative; background:var(--paper); background-image:var(--noise); color:var(--ink);
border:1px solid var(--paperline); border-radius:2px; padding:14px 11px 9px; margin:0 0 12px;
box-shadow:0 4px 10px rgba(0,0,0,.55); }
#side .card:nth-child(odd) { transform:rotate(.5deg); }
#side .card:nth-child(3n) { transform:rotate(-.6deg); }
#side .card::before { content:''; position:absolute; top:-5px; left:50%; width:11px; height:11px; margin-left:-6px;
border-radius:50%; background:radial-gradient(circle at 35% 30%, #ff9a8a, #a72a1e 65%); box-shadow:0 2px 3px rgba(0,0,0,.6); }
#side .card .dim, #side .card .small.dim { color:var(--inkdim); }
#side .card b { color:#1d1812; }
#side .card input { background:transparent; border:none; border-bottom:1px dashed #a5946e; border-radius:0; color:var(--ink); padding:3px 2px; }
#side .card input::placeholder { color:#a5946e; }
#side button { background:#d9cba6; color:var(--ink); border:1px solid #b3a077; }
#side button:hover { border-color:#7a1f1f; }
#side .tag { font-size:10.5px; background:transparent; border:1px solid #b3a077; color:var(--inkdim); border-radius:2px; padding:0 5px; }
.card .t { display:flex; gap:6px; align-items:baseline; }
.card .t b { flex:1; }
.evref { display:block; width:100%; text-align:left; margin-top:4px; font-style:italic; }
@keyframes evflash { 0%,100% { box-shadow:0 4px 10px rgba(0,0,0,.55); } 25%,60% { box-shadow:0 0 0 3px var(--acc), 0 4px 14px rgba(0,0,0,.6); } }
#side .card.flash { animation:evflash 1.3s ease; }
.mk { display:flex; gap:4px; margin-top:6px; }
.mk button { padding:1px 8px; font-size:12px; }
.mk button.on { border-color:#7a1f1f; color:#7a1f1f; font-weight:bold; }
#side .card.cross { opacity:.55; }
#side .card.cross::after { content:'✗'; position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
font-size:64px; color:rgba(167,42,30,.35); transform:rotate(-14deg); pointer-events:none; }
#side .card.cross b { text-decoration:line-through; }
#side .card.tick { border-color:#4d7a44; box-shadow:0 4px 10px rgba(0,0,0,.55), inset 0 0 0 2px rgba(77,122,68,.35); }
.pinbtn { background:none; border:none; opacity:.45; font-size:15px; padding:0 4px; }
.pinbtn.on { opacity:1; filter:drop-shadow(0 0 4px rgba(232,176,75,.8)); }
/* Закріплене 📌 — третій візуальний канал: рамку вже займає .tick (зелений ring),
накладку «✗» — .cross. Лишається бордова грань теки + світліший папір. */
#side .card.pinned { border-left:5px solid #7a1f1f; padding-left:9px; background-color:#f4ebd4; }
#side .card.pinned > .t > b { color:#5d1717; }
.divider { text-align:center; letter-spacing:2px; margin:2px 0 12px; }
#side .divider { color:#c9b596; text-shadow:0 1px 0 rgba(0,0,0,.5); }
/* Фільтр дошки: рядок пошуку + чипи-теги над списком карток */
#side input.evfilter { width:100%; margin-top:6px; background:var(--paper2);
border:1px solid #b3a077; border-radius:2px; color:var(--ink); padding:5px 9px; }
#side input.evfilter::placeholder { color:#7d7052; }
#side input.evfilter:focus { border-color:#7a1f1f; background:var(--paper); }
.chips { display:flex; flex-wrap:wrap; gap:4px; margin:6px 0 10px; }
.chip { font-size:11.5px; padding:2px 10px; border-radius:11px; }
#side .chip.on { border-color:#7a1f1f; color:#7a1f1f; font-weight:bold; background:#e3d3aa; }
#side .card .orig { font-style:italic; margin-bottom:3px; }
/* ---------- Сцена: польовий ---------- */
#cv { width:100%; border:1px solid #1c222c; border-radius:10px; background:#0a0e12; display:block;
box-shadow:0 0 0 1px #04060a, 0 14px 44px rgba(0,0,0,.6); }
#fieldhint { color:#8fa6bd; font-size:13px; min-height:18px; font-family:'Courier New', monospace; letter-spacing:.4px;
text-shadow:0 0 8px rgba(120,160,210,.35); }
#fieldhint::before { content:'▸ '; color:var(--acc); }
/* ---------- Сцена: аналітик = CRT-термінал ---------- */
.role-analyst #stage { position:relative; margin:10px; border-radius:10px; background:var(--crt);
border:1px solid #33230d; box-shadow:inset 0 0 140px rgba(255,170,60,.07), 0 12px 40px rgba(0,0,0,.6);
color:var(--amber); font-family:'Courier New', monospace; animation:crtflicker 7s infinite; }
.role-analyst #stage::after { content:''; position:fixed; pointer-events:none; z-index:5; inset:0;
background:repeating-linear-gradient(0deg, rgba(0,0,0,.14) 0 1px, transparent 1px 3px); mix-blend-mode:multiply; }
@keyframes crtflicker { 0%,94%,100% { opacity:1; } 95% { opacity:.96; } 96% { opacity:1; } 98% { opacity:.985; } }
.role-analyst #stage h3, .role-analyst #stage .syshead { color:var(--amber); text-shadow:0 0 8px rgba(255,180,84,.5); }
.role-analyst #stage .dim { color:var(--amberdim); }
.role-analyst #stage button { background:transparent; color:var(--amber); border:1px solid var(--amberdim); border-radius:2px; font-family:inherit; }
.role-analyst #stage button:hover:not(:disabled) { border-color:var(--amber); box-shadow:0 0 10px rgba(255,180,84,.25); }
.role-analyst #stage button.primary { background:rgba(255,180,84,.16); color:var(--amber); font-weight:bold; }
.role-analyst #stage input, .role-analyst #stage select, .role-analyst #stage textarea {
background:#080502; color:var(--amber); border:1px solid #3a2a10; font-family:inherit; border-radius:2px; }
.role-analyst #stage .card { background:#120b03; border:1px dashed #45330f; border-radius:2px; padding:9px 11px; margin-bottom:8px; }
.role-analyst #stage .card.tick { border-color:var(--green); border-style:solid; }
.role-analyst #stage table th { color:var(--amberdim); }
.role-analyst #stage th, .role-analyst #stage td { border-bottom:1px solid #33240c; }
.role-analyst #stage tr.rowlink:hover td { background:rgba(255,180,84,.07); }
.role-analyst #cv { filter:sepia(.35) hue-rotate(-22deg) saturate(1.5) contrast(1.05); border-color:#33230d; }
.role-analyst #stage details summary { cursor:pointer; color:var(--amberdim); }
.atabs { display:flex; gap:6px; flex-wrap:wrap; }
.atabs button.on { border-color:var(--amber) !important; color:var(--amber); box-shadow:0 0 10px rgba(255,180,84,.3); }
.apanel { display:none; } .apanel.on { display:block; }
table { width:100%; border-collapse:collapse; font-size:13.5px; }
th, td { text-align:left; padding:4px 8px; border-bottom:1px solid var(--line); }
th { color:var(--dim); font-weight:normal; }
tr.rowlink { cursor:pointer; } tr.rowlink:hover td { background:var(--panel2); }
tr.cross td { opacity:.45; text-decoration:line-through; }
tr.tick td { color:var(--green); }
tr.hl td { background:rgba(232,176,75,.1); }
/* Мітки на людях: чотири кольори замість ✓/✗ — що вони означають, вирішують
гравці, тож жодних підписів. Суцільна заливка рядка не читається ні на
бурштиновому CRT аналітика, ні на папері модалки польового, тож канал —
кольорова ліва грань рядка плюс кружечок-свотч у кнопці. */
tr[class*="mk-"] > td:first-child { border-left:4px solid transparent; padding-left:6px; }
tr.mk-green > td:first-child { border-left-color:var(--mk-green); }
tr.mk-red > td:first-child { border-left-color:var(--mk-red); }
tr.mk-violet> td:first-child { border-left-color:var(--mk-violet); }
tr.mk-amber > td:first-child { border-left-color:var(--mk-amber); }
.dot { display:inline-block; width:10px; height:10px; border-radius:50%; vertical-align:-1px;
box-shadow:0 0 0 1px rgba(0,0,0,.45); }
.mk button.swatch { padding:2px 7px; }
.mk button.swatch.on { border-color:currentColor; box-shadow:inset 0 0 0 1px currentColor; }
.filters { display:flex; gap:8px; flex-wrap:wrap; margin:8px 0; align-items:center; }
/* min-width, а не width: жорсткі 130px різали довгі назви камер («Муніципальна к…»),
хоча панель широка і праворуч лишалося порожньо. Ряд і так переноситься. */
.filters select, .filters input { flex:1 1 auto; min-width:140px; max-width:260px; }
.filters input[type=checkbox] { flex:0 0 auto; min-width:0; width:auto; }
.filters label { flex:0 0 auto; white-space:nowrap; }
#cc-cam { max-width:min(100%, 460px); }
.syshead { color:var(--dim); text-transform:uppercase; font-size:12px; letter-spacing:2px; margin:14px 0 6px; }
.syshead::before { content:'— '; opacity:.5; }
/* База мешканців: пошук за іменем і пошук за прикметами — два незалежні запити,
які перебивають один одного, а не одна форма з двома рядами полів. Риска й
підписи розводять їх; стилі спільні для обох ролей, бо екран той самий. */
.dbsec { color:var(--dim); text-transform:uppercase; font-size:11px; letter-spacing:1.6px; margin:12px 0 4px; }
.dbsplit { height:1px; background:var(--line); margin:16px 0 2px; }
.role-analyst #stage .dbsec { color:var(--amber); opacity:.7; }
/* ---------- Модалки: паперовий протокол ---------- */
.modal { position:fixed; inset:0; background:rgba(2,4,8,.72); display:flex; align-items:center; justify-content:center; z-index:40;
backdrop-filter:blur(2px); }
/* Затемнення не доходить до дошки (400px праворуч) — клік-по-тлу для закриття
лишається робочим, але докази й нотатки доступні під час обшуку чи допиту.
На вузьких екранах, де 400px — це пів-екрана, повертаємось до старої поведінки. */
@media (min-width: 1000px) { .modal { right:400px; } }
.modal > div { background:var(--paper); background-image:var(--noise); color:var(--ink);
border:1px solid #b6a67e; border-radius:3px; padding:22px 24px; width:min(600px, 94vw); max-height:88vh; overflow-y:auto;
box-shadow:0 30px 90px rgba(0,0,0,.8), inset 0 0 0 6px rgba(0,0,0,.03); font-family:Georgia, serif; }
.modal h3 { color:#7a1f1f; border-bottom:2px solid rgba(122,31,31,.5); padding-bottom:5px; margin-bottom:10px;
text-transform:uppercase; letter-spacing:1.5px; font-size:14px; }
.modal .dim, .modal .small.dim { color:var(--inkdim); }
.modal .syshead { color:#7a1f1f; opacity:.8; }
/* База польового живе в модалці — на папері, а не на панелі: свої чорнило й лінія. */
.modal .dbsec { color:#7a1f1f; opacity:.75; }
.modal .dbsplit { background:var(--paperline); }
.modal button { background:#d9cba6; color:var(--ink); border:1px solid #b3a077; border-radius:2px; }
.modal button:hover { border-color:#7a1f1f; }
.modal button.primary { background:#7a1f1f; color:var(--paper); }
.modal button.danger { background:#7a1f1f; color:var(--paper); letter-spacing:1px; }
.modal input, .modal select, .modal textarea { background:rgba(255,255,255,.5); color:var(--ink); border:1px solid #b3a077; border-radius:2px; }
.modal th, .modal td { border-bottom:1px solid #cbbd9b; }
.modal th { color:var(--inkdim); }
.modal .tag { border:1px solid #b3a077; color:var(--inkdim); background:transparent; }
.modal .card { background:rgba(255,255,255,.35); border:1px solid #c3b28b; border-radius:2px; padding:9px 11px; margin-bottom:8px; }
.modal label { color:var(--ink); }
.modalfoot { display:flex; gap:8px; flex-wrap:wrap; margin-top:14px;
border-top:1px solid rgba(122,31,31,.25); padding-top:10px; }
/* свіжа відповідь на допиті — щоб її було видно серед тем і пліток */
@keyframes logflash { 0%,100% { background:transparent; } 30%,70% { background:rgba(122,31,31,.15); } }
.logrow.flash { animation:logflash 1.8s ease; border-radius:3px; }
/* Глобальне tr.rowlink:hover заливає рядок майже чорним --panel2. Аналітику це
перекрито в #stage, але база польового живе в модалці (папір + чорнило) —
там рядок ставав нечитабельним. Той самий бордовий акцент, що й решта модалки. */
.modal tr.rowlink:hover td { background:rgba(122,31,31,.10); }
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(110px,1fr)); gap:8px; margin-top:8px; }
/* ---------- Тости: телеграма ---------- */
#toasts { position:fixed; bottom:18px; left:50%; transform:translateX(-50%); display:flex; flex-direction:column; gap:6px; z-index:60; }
.toast { background:var(--paper); background-image:var(--noise); color:var(--ink); border-top:3px double #7a1f1f; border-bottom:3px double #7a1f1f;
padding:8px 18px; animation:fade 4s forwards; box-shadow:0 8px 24px rgba(0,0,0,.6); font-size:14px; }
@keyframes fade { 0%,80% { opacity:1 } 100% { opacity:0 } }
/* ---------- Фінал ---------- */
#endscreen p { margin:10px 0; }
#endscreen h3 { display:inline-block; transform:rotate(-2deg); border:3px solid #7a1f1f; padding:6px 14px;
font-size:20px; letter-spacing:3px; box-shadow:none; }
</style>
</head>
<body>
<div id="lobby">
<div class="card">
<h1>НЕВПІЗНАНИЙ</h1>
<div class="dim" style="margin-top:10px">Детектив на двох • розділ 3 • кожна справа — нова</div>
<div class="row"><input id="code" maxlength="24" placeholder="КОД СЕСІЇ (він же — справа)"></div>
<div class="row">
<button class="rolebtn" data-role="field"><b>Польовий детектив</b><span class="small dim">район, обшуки, допити</span></button>
<button class="rolebtn" data-role="analyst"><b>&gt; АНАЛІТИК_</b><span class="small dim">бази, камери, лабораторія</span></button>
</div>
<div class="dim small">Той самий код — та сама справа і її збереження. Новий код — цілком нова справа.</div>
</div>
</div>
<div id="app">
<header>
<span class="title">НЕВПІЗНАНИЙ</span>
<span class="badge" id="b-session"></span>
<span class="badge" id="b-field">поле</span>
<span class="badge" id="b-analyst">аналітик</span>
<span style="flex:1"></span>
<button id="sndbtn" class="small" title="звук">🔊</button>
<button id="introbtn" class="small">Справа</button>
<button id="accusebtn" class="danger">ЗВИНУВАЧЕННЯ</button>
</header>
<div id="main">
<div id="stage"></div>
<div id="side">
<div class="tabs" id="sidetabs">
<button data-tab="ev" class="on">Докази</button>
<button data-tab="notes">Нотатки</button>
</div>
<div class="tabbody" id="sidebody"></div>
</div>
</div>
</div>
<div id="toasts"></div>
<script src="sound.js"></script>
<script src="client.js"></script>
<script src="field.js"></script>
<script src="analyst.js"></script>
</body>
</html>