(function (root) {
'use strict';
const C = root.GameCore || (root.GameCore = {});
// Детермінована випадковість. У моделі не має бути жодного Math.random():
// без відтворюваності неможливий ані тюнінг, ані спільна «зміна дня».
function mulberry32(seed) {
let a = seed | 0;
return function () {
a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function pad2(n) {
return (n < 10 ? '0' : '') + n;
}
// Локальна календарна дата гравця: доба міняється опівночі за його годинником.
function todayKey(date) {
const d = date instanceof Date ? date : new Date();
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
}
// Сид доби — хеш ключа дати. Однаковий у всіх, у кого сьогодні той самий день.
function dailySeed(date) {
const key = typeof date === 'string' ? date : todayKey(date);
let h = 2166136261;
for (let i = 0; i < key.length; i += 1) {
h ^= key.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return (h >>> 0);
}
function kyivDay(date) {
const parts = new Intl.DateTimeFormat('en-CA', {timeZone: 'Europe/Kyiv', year: 'numeric', month: '2-digit', day: '2-digit'}).formatToParts(date || new Date());
return ['year', 'month', 'day'].map(function (k) { return parts.find(function (p) { return p.type === k; }).value; }).join('-');
}
C.rng = {
kyivDay: kyivDay,
mulberry32: mulberry32,
dailySeed: dailySeed,
todayKey: todayKey
};
})(typeof globalThis !== 'undefined' ? globalThis : this);
(function (root) {
'use strict';
const C = root.GameCore || (root.GameCore = {});
C.createStore = function (key, defaults, normalize) {
const clone = function (x) { return JSON.parse(JSON.stringify(x)); };
let memory = null;
function storage() { try { return root.localStorage || null; } catch (e) { return null; } }
function available() {
try { const s = storage(); if (!s) return false; s.setItem(key + '-probe', '1'); s.removeItem(key + '-probe'); return true; } catch (e) { return false; }
}
function load() {
if (!available()) return memory === null ? defaults() : clone(memory);
try {
const raw = storage().getItem(key);
if (!raw) return defaults();
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return defaults();
return normalize ? normalize(parsed) : parsed;
} catch (e) { return defaults(); }
}
function save(value) {
memory = clone(value);
try { const s = storage(); if (s) s.setItem(key, JSON.stringify(value)); } catch (e) { /* tab memory remains available */ }
return value;
}
return {load: load, save: save, available: available, patch: function (x) { return save(Object.assign(load(), x)); }};
};
})(globalThis);
(function (root) {
'use strict';
const C = root.GameCore || (root.GameCore = {});
// Дрібний спільний хелпер для ui/*. Власний модуль, а не побічний продукт
// схеми: інакше ui/hud мовчки залежав би від того, що ui/schema завантажився
// раніше, і порядок тримав би тільки список ORDER у build.py.
function el(tag, cls, text) {
const node = document.createElement(tag);
if (cls) node.className = cls;
if (text !== undefined && text !== null) node.textContent = String(text);
return node;
}
// Форматування числа й хвилини доби живе тут із тієї самої причини, з якої
// тут живе el(): його однаково потребують і звіт, і шер-картка. Якби воно
// лишалось у ui/report, ui/share залежав би від ui/report і від порядку
// склейки - рівно та форма, заради якої цей модуль і заводили.
//
// Число: нерозривний пробіл у розрядах і кома в дробовій частині. Картку
// людина кладе поруч зі звітом і звіряє очима, тому правило одне на обидва.
function num(value, digits) {
const v = Number(value);
if (!isFinite(v)) return String.fromCharCode(0x2014);
const d = digits === undefined ? 0 : digits;
const parts = Math.abs(v).toFixed(d).split('.');
parts[0] = parts[0].split('').map(function (ch, i, a) { return ch + (i < a.length - 1 && (a.length - i - 1) % 3 === 0 ? String.fromCharCode(0x00A0) : ''); }).join('');
const body = parts.length > 1 ? parts[0] + ',' + parts[1] : parts[0];
return (v < 0 ? '-' : '') + body;
}
function minutes(value) {
return num(value, 1) + ' хв';
}
// Хвилина доби -> ГГ:ХХ. Модель тримає свою копію всередині себе; сюди
// ходять тільки ui/*.
function clock(minute) {
const m = Math.max(0, Math.round(Number(minute) || 0));
const h = Math.floor(m / 60) % 24;
const mm = m % 60;
return (h < 10 ? '0' : '') + h + ':' + (mm < 10 ? '0' : '') + mm;
}
C.dom = { el: el, num: num, minutes: minutes, clock: clock };
})(typeof globalThis !== 'undefined' ? globalThis : this);
(function(root){
'use strict';
const T=root.Tunnel={};
T.chapters=[
{year:'1938',name:'Навчися тримати тиск',place:'Жуків острів',end:4,tag:'ПРОЛОГ',text:'Чотири безпечні кільця: спершу додай тиску, потім зменш. Кесон — робоча камера зі стисненим повітрям. Щит — машина, що прокладає тунель; кільце — одна його секція.'},
{year:'1939–1940',name:'Під руслом',place:'Південна траса',end:20,tag:'ГЛАВА I',text:'Попереду пісок і невідомі шари. Напрям змінює глибину, а з нею — потрібний тиск. Розвідка відкриває наступні п’ять кілець.'},
{year:'Лютий 1941',name:'Тонка покрівля',place:'Ділянка № 4',end:36,tag:'ГЛАВА II',text:'Популярний переказ описує прорив повітря; архівного підтвердження для гри немає. В ігровій моделі коридор звужується. Стеж за запасом до його верхньої межі.'},
{year:'1946–1949',name:'Повернення до річки',place:'Відновлення',end:56,tag:'ГЛАВА III',text:'Тунель розчищено для ігрового продовження. Паводок посилює тиск води. Є 32 робочі зміни до завершення сезону — враховуй відпочинок і ухил.'}
];
T.events=[
['Тиха вода','Річка спокійна. Прислухайся до забою.',0],['Мінливий шар','На цій ділянці звіряй тиск із новим коридором.',.08],['Вода прибуває','Вода підвищує потрібний тиск; звір його з жовтою зоною.',.14],['Щільний забій','Обраний курс змінює глибину й робочий коридор.',-.07],['Крига на руслі','Річка стишилася, але тиск під нею лишився.',0],['Річковий осад','Шари попереду покаже свердловина.',.06],['Весняний паводок','Вода сильніше тисне на забій.',.2],['Валун у забої','Бригада прибирає камінь. Утома зростає.',0],['Перевірка темпу','Стеж за втомою й обирай час шлюзування.',0],['Лікар на зміні','Повний відпочинок важливіший за поспіх.',0],['Свист у шлюзі','Шлюз оглянуто. Перед проходкою звір тиск із коридором.',.04],['Вода в породі','Насичені водою шари потребують точного тиску.',.12],['Спадає вода','Потрібний тиск нижчий; звір його з жовтою зоною.',-.13],['Нова бригада','Люди готові працювати у звичному темпі.',0],['Зношений різець','Інструмент замінено перед наступною проходкою.',0],['Течія посилилась','Над щитом проходить сильна течія.',.09],['Нічна зміна','Лампи освітлюють секції тунелю. Орієнтуйся на манометр.',0],['Спокійний забій','Робочий коридор враховує ґрунт і глибину.',-.08],['Тонка покрівля','Близько до дна запас тиску менший.',.05],['Огляд кріплень','Секції тунелю перевірено перед новим кільцем.',0],['Затримка постачання','Наступне кільце доставлено до забою.',0],['Вологий забій','Поява каламуті означатиме, що тиску замало.',.11],['Ранок над Дніпром','На поверхні починається день. Тут триває зміна.',0],['Перевірка компресора','Прилади працюють. Звір показник із коридором.',0],['Після дощу','Водонасичені породи тиснуть сильніше.',.1]
];
T.create=function(mode,day){const s={version:1,mode:mode||'campaign',day:day||GameCore.rng.kyivDay(),ring:0,chapter:0,depth:26,pressure:1.9,course:0,fatigue:0,probes:3,reveal:0,breaks:0,turns:0,finalTurns:0,history:[],checkpoint:0,phase:'playing',notice:'Навчання без аварій. Додай тиску до жовтої зони й змонтуй перше кільце.',seen:[],pending:null,lastFailure:null,decisions:Array.from({length:4},()=>({probes:0,rests:0}))};if(s.mode==='daily'){s.chapter=1;const b=T.bounds(s);s.pressure=Math.round((b.low+b.high)*10)/20;s.notice='Денна спроба почалася з безпечним тиском. Перевір прогноз і веди щит.';}return s;};
T.total=s=>s.mode==='daily'?32:56;
T.needsRest=s=>s.fatigue>=88;
T.primaryAction=s=>T.needsRest(s)?'rest':'advance';
T.saveSession=function(saved,state,practice){if(practice)return saved;if(state.mode==='daily')return Object.assign({},saved,{daily:Object.assign({},saved.daily,{[state.day]:state})});return Object.assign({},saved,{campaign:state});};
T.geology=function(s,offset){const n=s.ring+(offset||0);const rng=GameCore.rng.mulberry32(GameCore.rng.dailySeed(s.mode==='daily'?s.day:'tunel-campaign-v1')+n*7919);return {soil:['Пісок','Глина','Пливун','Мул'][Math.floor(rng()*4)],event:Math.floor(rng()*T.events.length)};};
T.nextChapter=function(s){let chapter=s.chapter;if(s.mode==='campaign')while(chapter<3&&s.ring>=T.chapters[chapter].end)chapter++;return chapter;};
function corridor(s){const geo=T.geology(s),ev=T.events[geo.event];const depth=Math.max(18,Math.min(38,s.depth+s.course*.65));const chapter=T.nextChapter(s);if(chapter===0&&s.mode==='campaign'){const lesson=[[2.1,2.5],[1.45,1.8],[2.15,2.5],[1.65,2]][s.ring];const shift=(depth-26)*.025;return {low:Math.round((lesson[0]+shift)*100)/100,high:Math.round((lesson[1]+shift)*100)/100,depth,geo,chapter};}const low=1.35+(depth-18)*.055+ev[2]+(chapter===3?.12:0);const width=(geo.soil==='Глина'?.72:geo.soil==='Пливун'?.36:.5)+(depth>29?.1:0)-(chapter===2?.08:0);return {low:Math.round(low*100)/100,high:Math.min(3.45,Math.round((low+width)*100)/100),depth,geo,chapter};}
T.bounds=function(s,offset){const future=Object.assign({},s);for(let i=0;i<(offset||0);i++){future.depth=corridor(future).depth;future.ring++;future.chapter=T.nextChapter(future);}return corridor(future);};
T.fatigueGain=function(s,b){return 11+s.pressure*2+(b.geo.event===7?6:0)+(s.course===1?3:0);};
T.forecast=function(s){return Array.from({length:Math.min(5,T.total(s)-s.ring)},(_,offset)=>{const b=T.bounds(s,offset);return {ring:s.ring+offset+1,known:s.ring+offset<s.reveal,soil:b.geo.soil,low:b.low,high:b.high,depth:b.depth,chapter:b.chapter,fatigue:T.fatigueGain(s,b)};});};
T.signal=function(s){const b=T.bounds(s);return s.pressure<b.low?'water':s.pressure>b.high?'air':'safe';};
T.act=function(s,action,value){
if(s.phase!=='playing'||s.pending)return s;
if(action==='pressure'){if(Number.isFinite(value))s.pressure=Math.max(.5,Math.min(3.5,value));return s;}
if(action==='course'){if([-1,0,1].includes(value))s.course=value;return s;}
if(action==='probe'){if(s.probes>0){s.probes--;s.reveal=Math.min(T.total(s),s.ring+5);if(s.decisions)s.decisions[s.chapter].probes++;s.notice='Пробу взято. Пронумерований прогноз збережено біля пульта; він оновлюється з ходом і курсом.';}return s;}
if(action==='rest'){s.fatigue=0;s.turns++;if(s.chapter===3)s.finalTurns++;if(s.decisions)s.decisions[s.chapter].rests++;s.checkpoint=s.ring;s.notice='Шлюзування завершено: бригада відпочила. Безпечна точка — кільце '+s.ring+'.';T.deadline(s);return s;}
if(action!=='advance')return s;
if(T.needsRest(s)){s.notice='Бригада втомилась. Проведи повне шлюзування перед наступною проходкою.';return s;}
const b=T.bounds(s),sig=T.signal(s);
if(sig!=='safe'){
if(s.chapter===0&&s.mode==='campaign'){s.notice=sig==='water'?'Каламуть у забої: додай тиску до робочої зони. Навчання — без аварії.':'Бульбашки на поверхні: зменш тиск до робочої зони. Навчання — без аварії.';return s;}
s.lastFailure={ring:s.ring+1,pressure:s.pressure,low:b.low,high:b.high,signal:sig};s.breaks++;s.notice=(sig==='water'?'Вода зайшла в забій: тиску замало.':'Повітря прорвалося крізь покрівлю: тиску забагато.')+' Бригаду виведено через шлюз.';
if(s.mode==='daily'){s.phase='finished';return s;}
s.ring=s.checkpoint;s.history=s.history.slice(0,s.ring);s.depth=s.history.length?s.history[s.history.length-1].depth:26;s.fatigue=0;s.pending='flood';return s;
}
s.history.push({depth:b.depth,pressure:s.pressure,course:s.course});s.depth=b.depth;s.ring++;s.turns++;if(s.chapter===3)s.finalTurns++;s.fatigue=Math.min(100,s.fatigue+T.fatigueGain(s,b));
s.notice='Кільце '+s.ring+' успішно змонтовано.';
if(s.ring%5===0){s.checkpoint=s.ring;}
if(s.ring>=T.total(s)){s.phase='finished';return s;}
if(s.mode==='campaign'&&s.ring===T.chapters[s.chapter].end){s.pending=s.chapter===2?'interlude':'chapter';}
T.deadline(s);return s;
};
T.deadline=s=>{if(s.chapter===3&&s.finalTurns>=32&&s.phase==='playing'){s.phase='finished';s.notice='Сезон завершився. Паводок зупинив ігрову проходку.';}};
T.continue=function(s){if(s.pending==='chapter'||s.pending==='interlude'){s.chapter++;s.probes=3;s.fatigue=0;s.checkpoint=s.ring;s.notice=T.chapters[s.chapter].text;}s.pending=null;return s;};
T.restore=function(s){
if(!s||s.version!==1||!['campaign','daily'].includes(s.mode)||typeof s.day!=='string'||!/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(s.day)||!Number.isInteger(s.ring)||s.ring<0||s.ring>T.total(s)||!Array.isArray(s.history)||s.history.length!==s.ring)return null;
const ranges={depth:[18,38],pressure:[.5,3.5],fatigue:[0,100],probes:[0,3],chapter:[0,3],checkpoint:[0,s.ring],finalTurns:[0,32],breaks:[0,100000],turns:[0,100000],reveal:[0,61]};
for(const key of Object.keys(ranges)){if(!Number.isFinite(s[key])||s[key]<ranges[key][0]||s[key]>ranges[key][1])return null;}
if(!Number.isInteger(s.chapter)||!Number.isInteger(s.probes)||![-1,0,1].includes(s.course)||!['playing','finished'].includes(s.phase)||![null,'chapter','interlude','flood'].includes(s.pending))return null;
if(s.history.some(p=>!p||!Number.isFinite(p.depth)||p.depth<18||p.depth>38||!Number.isFinite(p.pressure)||p.pressure<.5||p.pressure>3.5||![-1,0,1].includes(p.course)))return null;
if(s.lastFailure!==undefined&&s.lastFailure!==null){const f=s.lastFailure;if(!f||!Number.isInteger(f.ring)||f.ring<1||f.ring>T.total(s)||!Number.isFinite(f.pressure)||f.pressure<.5||f.pressure>3.5||!Number.isFinite(f.low)||!Number.isFinite(f.high)||f.low<.5||f.high>3.5||f.low>f.high||!['water','air'].includes(f.signal)||(f.signal==='water'?f.pressure>=f.low:f.pressure<=f.high))return null;}
if(s.decisions!==undefined&&s.decisions!==null&&(!Array.isArray(s.decisions)||s.decisions.length!==4||s.decisions.some(d=>!d||!Number.isInteger(d.probes)||d.probes<0||d.probes>3||!Number.isInteger(d.rests)||d.rests<0||d.rests>100000)))return null;
const restored=Object.assign(T.create(s.mode,s.day),s);
if(s.decisions===undefined)restored.decisions=null;
restored.notice=typeof s.notice==='string'?s.notice.slice(0,700).split('&').join('&').split('<').join('<').split('>').join('>'):'';
return restored;
};
})(globalThis);
(function(){
'use strict';
// Physical key codes also work while the player types with a Ukrainian layout.
Tunnel.shortcut=function(event){
if(event.ctrlKey||event.metaKey||event.altKey||event.isComposing)return null;
const keys={
Digit1:['course',-1],Digit2:['course',0],Digit3:['course',1],
ArrowUp:['course',-1],ArrowDown:['course',1],
ArrowLeft:['pressure',-.05],ArrowRight:['pressure',.05],
Space:['primary'],Enter:['primary'],NumpadEnter:['primary'],
KeyB:['probe'],KeyR:['rest'],Escape:['pause']
};
const command=keys[event.code];
if(!command||(event.repeat&&command[0]!=='pressure'))return null;
return {action:command[0],value:command[1]};
};
// A native activation belongs to the focused control, not the global shortcut.
Tunnel.keyboardIntent=function(event,context){
const c=context||{};
if(event.ctrlKey||event.metaKey||event.altKey||event.isComposing)return null;
const primary=['Space','Enter','NumpadEnter'].includes(event.code);
if(event.repeat&&primary&&c.primaryHeld)return {action:'block'};
if(event.repeat&&primary&&(!c.interactive||c.primary||c.range)&&!c.modal&&!c.home)return {action:'block'};
if(c.home||c.paused||c.busy||c.pending||c.finished||c.modal||c.editing)return null;
if(c.range&&event.code.startsWith('Arrow'))return null;
if(primary&&c.interactive&&!c.range)return null;
if(c.radio&&['ArrowLeft','ArrowUp','ArrowRight','ArrowDown','Home','End'].includes(event.code)){
const course=event.code==='Home'?-1:event.code==='End'?1:['ArrowLeft','ArrowUp'].includes(event.code)?(c.course===-1?1:c.course-1):(c.course===1?-1:c.course+1);
return {action:'course',value:course};
}
return Tunnel.shortcut(event);
};
})();
(function(){
'use strict';
const T=Tunnel;
T.fmt=n=>Number(n).toFixed(1).replace('.',',');
T.precise=n=>Number(n).toFixed(2).replace('.',',');
T.word=function(n,one,few,many){const v=Math.abs(Math.floor(n)),last=v%10,tail=v%100;return tail>=11&&tail<=14?many:last===1?one:last>=2&&last<=4?few:many;};
T.rings=n=>n+' '+T.word(n,'кільце','кільця','кілець');
T.shifts=n=>n+' '+T.word(n,'зміна','зміни','змін');
T.breakCount=n=>n+' '+T.word(n,'прорив','прориви','проривів');
T.courseName=course=>course<0?'угору':course>0?'униз':'рівно';
T.corridorText=b=>T.precise(b.low)+'–'+T.precise(b.high)+' атм';
T.nextStatus=function(s){if(s.phase==='finished')return 'Маршрут завершено.';const sig=T.signal(s),b=T.bounds(s);return 'Перед кільцем '+(s.ring+1)+': '+(sig==='water'?'тиску замало — додай до ':sig==='air'?'тиску забагато — зменш до ':'тиск у нормі · ')+T.corridorText(b)+'.'+(T.needsRest(s)?' Спершу шлюзування: бригаді потрібен відпочинок.':'');};
T.lesson=function(s){if(s.mode!=='campaign'||s.chapter!==0||s.ring>=4)return '';const increase=s.ring%2===0;return 'Навчання '+(s.ring+1)+'/4 · '+(increase?'додай тиску: повітря стримує воду.':'зменш тиск: надлишок повітря може прорвати покрівлю.')+' '+(T.signal(s)==='safe'?'Тиск налаштовано — монтуй кільце.':'Переведи повзунок у жовту зону.')+' Помилка тут без аварії.';};
T.chapterSummary=function(s){const start=s.chapter?T.chapters[s.chapter-1].end:0,history=s.history.slice(start),choices=[-1,0,1].map(c=>({name:T.courseName(c),count:history.filter(p=>p.course===c).length})).filter(c=>c.count);const d=s.decisions&&s.decisions[s.chapter];return 'Твої рішення: '+choices.map(c=>c.name+' — '+T.rings(c.count)).join('; ')+'. '+(d?'Проби: '+d.probes+'/3; відпочинок: '+d.rests+' '+T.word(d.rests,'раз','рази','разів')+'. ':'')+'Робочі тиски: '+(history.length?T.precise(Math.min(...history.map(p=>p.pressure)))+'–'+T.precise(Math.max(...history.map(p=>p.pressure)))+' атм.':'монтажів ще немає.');};
T.shareText=function(s){const newline=String.fromCodePoint(10),line=s.history.filter((_,i)=>i%3===0).map(p=>String.fromCodePoint(p.course<0?0x2197:p.course>0?0x2198:0x2192)).join(''),complete=s.ring===T.total(s);const title=s.mode==='daily'?'Переправа дня · '+s.day:complete?'Кампанію завершено':'Сезон завершився';let metrics=T.rings(s.ring)+' · '+T.breakCount(s.breaks);if(s.history.length)metrics+=' · середній тиск змонтованих кілець '+T.fmt(s.history.reduce((a,p)=>a+p.pressure,0)/s.history.length)+' атм';return ['Тунель під Дніпром · '+title,line,metrics,'Умовна ігрова траса. Історичний Об’єкт № 1 не добудували. Мій Київ'].filter(Boolean).join(newline);};
})();
(function(){
'use strict';
const T=Tunnel,STEP=54;
T.scenePosition=function(s){const head=564+s.ring*STEP,camera=Math.max(0,head-650);return {head,camera,x:s.ring*STEP-camera,y:360+(s.depth-26)*3};};
function motion(from,to){return 'style="--from-x:'+from+'px;--to-x:'+to+'px" transform="translate('+to+' 0)"';}
T.panorama=function(s,previous){
const p=T.scenePosition(s),before=T.scenePosition(previous||s);
return '<svg class="river-panorama" viewBox="0 48 900 138" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Київ над Дніпром. Щит проходить під руслом."><g class="motion-shift" '+motion(-before.camera*.18,-p.camera*.18)+'>'+[-900,0,900,1800].map(x=>'<image href="https://my-kiev.com/wp-content/uploads/landings/tunel-pid-dniprom/river.jpg" x="'+x+'" width="900" height="520" preserveAspectRatio="none"/>').join('')+'</g></svg>';
};
function lining(s,p){
const start=Math.max(0,Math.floor((p.camera-80)/STEP));
return s.history.slice(start).map((ring,i)=>{
const index=start+i,x=-28+index*STEP,y=360+(ring.depth-26)*3;
return '<g class="laid-ring" data-ring="'+(index+1)+'" transform="translate('+x+' '+y+')"><path d="M0 -74Q27 -80 54 -74V70Q27 77 0 70Z" fill="url(#lining-metal)" stroke="#33392f" stroke-width="2"/><path d="M5 -70V68M49 -70V68" stroke="#474d40" stroke-width="2"/><rect x="8" y="-43" width="38" height="90" fill="#243029"/><path d="M10 -37H44" stroke="#f5e6a6" stroke-width="3"/><path d="M10 43H44" stroke="#a5a28b" stroke-width="3"/>'+[-63,58].map(v=>'<circle cx="10" cy="'+v+'" r="2" fill="#33392f"/><circle cx="44" cy="'+v+'" r="2" fill="#33392f"/>').join('')+'<text x="27" y="62" text-anchor="middle" fill="#d0c6a5" font-size="8" font-family="monospace">'+(index+1)+'</text></g>';
}).join('');
}
T.scene=function(s,previous,kind,compact){
const p=T.scenePosition(s),before=T.scenePosition(previous||s),signal=T.signal(kind==='advance'&&previous?previous:s),course=s.course*3;
const base=Math.floor(Math.min(before.camera,p.camera)/900)*900;
const ground=[base-900,base,base+900,base+1800].map(x=>'<image href="https://my-kiev.com/wp-content/uploads/landings/tunel-pid-dniprom/river.jpg" x="'+x+'" width="900" height="520" preserveAspectRatio="none"/>').join('');
const head=p.head-p.camera;
const bubbles=signal==='air'?Array.from({length:10},(_,i)=>'<circle class="bubble b'+i+'" cx="'+(head-12+Math.sin(i*3)*11)+'" cy="'+(p.y-76-i*19)+'" r="'+(3+i%4)+'" fill="none" stroke="#f0f3e9" stroke-width="2"/>').join(''):'';
const unknown=Math.min(850,head+(s.reveal>s.ring?170:78));
const particles=kind==='advance'?'<g class="dig-particles" aria-hidden="true">'+Array.from({length:13},(_,i)=>'<path class="dig-chip" style="--dx:'+(14+i%4*10)+'px;--dy:'+(-25+i*6)+'px;--spin:'+(i*37)+'deg;animation-delay:'+(i%4*.05)+'s" d="m'+(head+4)+' '+(p.y-36+i*6)+' 5 -3 4 5-4 4Z" fill="'+['#584a2f','#9b7747','#d3b274'][i%3]+'"/>').join('')+'<ellipse class="dig-dust" cx="'+(head+8)+'" cy="'+p.y+'" rx="30" ry="58" fill="#c4a477"/></g>':'';
const rocks=Array.from({length:8},(_,i)=>{const x=564+(s.ring+i)*STEP+95;return '<path d="m'+x+' '+(304+i*19%110)+' 9 -6 10 8-3 9-10 1Z" fill="#806a48" stroke="#51482e" stroke-width="1.2"/>';}).join('');
const progress=kind==='advance'?'<g class="ring-reward" transform="translate('+(head-252)+' '+(p.y-84)+')"><rect width="163" height="30" rx="4" fill="#ffd400" stroke="#534b28"/><text x="81" y="20" text-anchor="middle" font-family="Arial,sans-serif" font-size="14" font-weight="bold">Кільце '+s.ring+' · +1 м</text></g>':'';
const chamber=kind==='rest'?'<g class="airlock" transform="translate('+(head-350)+' '+(p.y-60)+')"><rect width="89" height="122" rx="25" fill="#899185" stroke="#29392b" stroke-width="3"/><rect x="12" y="15" width="65" height="92" rx="21" fill="#36473b" stroke="#cbc8a8" stroke-width="2"/><g class="lock-wheel"><circle cx="44" cy="61" r="20" fill="none" stroke="#ffd400" stroke-width="4"/><path d="M24 61h40M44 41v40" stroke="#ffd400" stroke-width="3"/></g><text x="44" y="98" text-anchor="middle" fill="#ece9d5" font-size="10" font-family="Arial,sans-serif">ШЛЮЗ</text></g>':'';
return '<svg class="section-art" viewBox="'+(compact?'0 230 900 280':'0 0 900 520')+'" preserveAspectRatio="xMidYMid slice" data-world-head="'+p.head+'" data-camera="'+p.camera+'" role="img" aria-label="Розріз Дніпра. Пройдено '+s.ring+' м ігрової траси. '+(signal==='air'?'Бульбашки: витік повітря.':signal==='water'?'Каламуть біля щита: тиску замало.':'Щит працює стабільно.')+'"><defs><pattern id="unknown" width="12" height="12" patternUnits="userSpaceOnUse" patternTransform="rotate(35)"><rect width="12" height="12" fill="#4c504a" fill-opacity=".45"/><path d="M0 0v12" stroke="#303b38" stroke-opacity=".55" stroke-width="2"/></pattern><linearGradient id="lining-metal" x2="0" y2="1"><stop stop-color="#b8b4a1"/><stop offset=".4" stop-color="#6c7767"/><stop offset="1" stop-color="#a7a48c"/></linearGradient><clipPath id="ground-cut"><rect y="239" width="900" height="281"/></clipPath><clipPath id="machine-cut" clipPathUnits="objectBoundingBox"><path d="M0 .15L.69 .15L.725 .15L.77 .145L.80 .12L.816 .105L.825 .125L.835 .17L.848 .15L.86 .19L.865 .22L.879 .25L.884 .3L.889 .35L.898 .36L.902 .43L.908 .46L.914 .5L.916 .54L.902 .57L.898 .62L.891 .65L.895 .68L.888 .71L.882 .75L.87 .79L.87 .818L.85 .838L.85 .89L.835 .879L.814 .855L.77 .837L.727 .825L.69 .825L0 .825Z"/></clipPath></defs><g class="motion-shift river-scenery" '+motion(-before.camera*.18,-p.camera*.18)+'><image href="https://my-kiev.com/wp-content/uploads/landings/tunel-pid-dniprom/river.jpg" width="900" height="520" preserveAspectRatio="none"/><image href="https://my-kiev.com/wp-content/uploads/landings/tunel-pid-dniprom/river.jpg" x="900" width="900" height="520" preserveAspectRatio="none"/></g><g clip-path="url(#ground-cut)"><g class="motion-shift ground-scenery" '+motion(-before.camera,-p.camera)+'>'+ground+rocks+'</g></g><g fill="#213235" font-family="Arial,sans-serif" font-size="13"><text x="28" y="180" fill="#eef1e6" font-size="22">Дніпро</text><rect x="20" y="246" width="53" height="24" rx="4" fill="#e8d4ac" fill-opacity=".9"/><text x="28" y="263">Мул</text><rect x="20" y="287" width="59" height="24" rx="4" fill="#f6e4ba" fill-opacity=".9"/><text x="28" y="304">Пісок</text><rect x="20" y="476" width="61" height="24" rx="4" fill="#e0d2ab" fill-opacity=".9"/><text x="28" y="493">Глина</text></g><g class="motion-shift tunnel-lining" '+motion(-before.camera,-p.camera)+'>'+lining(s,p)+'</g><g class="drill-position" style="--from-x:'+before.x+'px;--to-x:'+p.x+'px;--from-y:'+before.y+'px;--to-y:'+p.y+'px" transform="translate('+p.x+' '+p.y+')"><g transform="rotate('+course+' 480 0)"><g class="drill-engine"><image href="https://my-kiev.com/wp-content/uploads/landings/tunel-pid-dniprom/shield.jpg" x="-28" y="-107" width="640" height="213.33" clip-path="url(#machine-cut)" preserveAspectRatio="none"/></g></g></g>'+(signal==='water'?'<g class="silt" fill="#657b71" opacity=".72"><ellipse cx="'+(head+1)+'" cy="'+(p.y+9)+'" rx="30" ry="27"/><ellipse cx="'+(head+16)+'" cy="'+(p.y+34)+'" rx="48" ry="20"/></g>':'')+bubbles+particles+progress+chamber+'<rect x="'+unknown+'" y="125" width="280" height="395" fill="url(#unknown)"/><path d="M'+unknown+' 125V520" stroke="#f1ead8" stroke-dasharray="7 6" stroke-width="2"/><g fill="#faf7f0" font-family="Arial,sans-serif" font-size="15"><rect x="'+(unknown+10)+'" y="149" width="102" height="87" rx="5" fill="#263934" fill-opacity=".78"/><text x="'+(unknown+20)+'" y="173">→</text><text x="'+(unknown+20)+'" y="198">'+(s.reveal>s.ring?'Проба':'Попереду')+'</text><text x="'+(unknown+20)+'" y="219">'+(s.reveal>s.ring?'взята':'невідоме')+'</text></g></svg>';
};
T.profile=function(s){const points=s.history.map((p,i)=>(12+i*476/Math.max(1,s.history.length-1))+','+(20+(p.depth-18)*3));return '<svg viewBox="0 0 500 105" role="img" aria-label="Профіль пройденого тунелю"><path d="M0 20Q100 0 180 20T350 20T500 20V105H0Z" fill="#dce3df"/><path d="M0 20Q100 0 180 20T350 20T500 20" fill="none" stroke="#6b7f8f"/><polyline points="'+(points.join(' ')||'12,44 20,44')+'" fill="none" stroke="#171b17" stroke-width="3" stroke-linecap="round"/></svg>';};
})();
(function(){
'use strict';
const T=Tunnel,C=GameCore,root=document.getElementById('tunnel');
const qa=location.search.includes('qa=1'),profile=new URLSearchParams(location.search).get('profile')||'';
const qaSuffix=qa?'-qa'+(/^[a-z0-9-]{1,32}$/.test(profile)?'-'+profile:''):'';
const store=C.createStore('mykyiv-tunel-1'+qaSuffix,()=>({campaign:null,daily:{}}),v=>({campaign:T.restore(v.campaign),daily:v.daily&&typeof v.daily==='object'?v.daily:{}}));
let saved=store.load(),s=T.restore(saved.campaign)||T.create(),home=true,practice=false,paused=false,motionState=null,motionTimer=null,returnFocus=null,primaryHeld=false;
const fmt=T.fmt,precise=T.precise;
const sources=[
['Залізниця під річкою','Дві підводні переправи мали доповнити мости. Це проєкт залізниці, а не тунель київського метро.'],
['Кесон і кільця','Стиснене повітря стримувало воду. Щит рухався вперед, а позаду монтували кільця з окремих секцій — тюбінгів.'],
['Лютий 1941 · ділянка № 4','Один опублікований переказ описує вихід стисненого повітря на поверхню і затоплення. Архівного підтвердження для цієї гри немає.'],
['Липень 1941 · зупинка','За переказом, роботи законсервували, а щити й секції тунелю затопили. У грі це тиха сторінка журналу.'],
['1946–1949 · повернення','Джерело описує післявоєнні роботи та подальше припинення будівництва. Ігровий маршрут і його метри — умовна модель.']
];
function safe(text){return String(text).split('&').join('&').split('<').join('<').split('>').join('>');}
function persist(){if(home||practice)return;saved=T.saveSession(saved,s,practice);store.save(saved);}
function btn(action,text,cls,disabled){return '<button type="button" data-action="'+action+'" class="'+(cls||'secondary')+'"'+(cls==='modal-close'?' aria-label="Закрити"':'')+(disabled?' disabled':'')+'>'+text+'</button>';}
function focusKey(element){if(!element||element===document.body)return null;if(element.id==='pressure')return '#pressure';if(element.hasAttribute('data-course'))return '[data-course="'+element.dataset.course+'"]';if(element.dataset.action)return '[data-action="'+element.dataset.action+'"]';return null;}
function focusControl(selector){if(root.querySelector('dialog[open]'))return;const desired=selector&&root.querySelector(selector),button=desired&&!desired.disabled?desired:root.querySelector('.advance:not([disabled])');if(button)button.focus({preventScroll:true});}
function primaryButton(inactive){const action=motionState?(motionState.kind==='rest'?'rest':'advance'):T.primaryAction(s),resting=action==='rest';const label=motionState?(resting?'Відпочинок…':'Монтаж…'):(resting?'Шлюзування':'Проходка');return btn(action,'<span class="action-copy"><b>'+label+'</b><small>'+(motionState?(resting?'Бригада відпочиває':'Кільце змонтовано'):(resting?'Відпочинок бригади':'Наступне кільце'))+'</small></span><span class="action-icon" aria-hidden="true">'+(resting?'↻':'→')+'</span>','primary advance '+(resting?'rest-action':''),inactive);}
function sceneStatus(){if(home)return saved.campaign?'Збережена кампанія · пульт для перегляду':'Демонстраційний розріз · почни гру';if(motionState)return motionState.kind==='advance'?'Кільце '+s.ring+' успішно змонтовано':motionState.kind==='rest'?'Шлюзування · бригада відпочиває':'Бригаду виведено через шлюз';return 'Наступне кільце · '+(T.signal(s)==='safe'?'тиск у нормі':T.signal(s)==='air'?'зменш тиск':'додай тиску');}
function nextHtml(){const b=T.bounds(s);return '<b>'+T.nextStatus(s)+'</b><span>Курс: '+T.courseName(s.course)+' · глибина зараз '+fmt(s.depth)+' м → після кільця '+fmt(b.depth)+' м · очікувана втома +'+Math.round(T.fatigueGain(s,b))+'% → '+Math.min(100,Math.round(s.fatigue+T.fatigueGain(s,b)))+'%</span>';}
function forecastHtml(){const rows=T.forecast(s).slice(0,practice?Math.max(0,4-s.ring):5),known=rows.filter(r=>r.known).length;return '<div class="forecast-title"><b>Розвідка попереду</b><span>Проби: '+s.probes+'/3</span></div>'+(known?'<p class="forecast-confirm">Пробу взято · видно '+T.rings(known)+'. Курс: '+T.courseName(s.course)+'.</p>':'<p class="forecast-confirm">Свердловина відкриє ґрунт і коридори наступних п’яти кілець.</p>')+'<ol class="forecast-list">'+rows.map(r=>'<li class="'+(r.known?'known':'unknown')+'"><b>№ '+r.ring+'</b><span>'+ (r.known?r.soil:'Невідомо')+'</span><span>'+(r.known?T.corridorText(r):'Потрібна проба')+'</span></li>').join('')+'</ol><p class="forecast-note">Прогноз за незмінного курсу; ураховано глибину й межі глав. Поточна жовта зона доступна без проби.</p>';}
function peekHtml(){const first=T.forecast(s)[0];return first&&first.known?'Проба · № '+first.ring+': '+first.soil+' · '+T.corridorText(first):'';}
function budgetHtml(){if(practice)return 'Тренування: '+s.ring+'/4 · денний шанс і кампанія збережені';if(s.mode==='daily')return 'До фінішу '+T.rings(32-s.ring)+' · сезонного ліміту немає';if(s.chapter===3)return 'До фінішу '+T.rings(56-s.ring)+' · залишилось '+T.shifts(32-s.finalTurns)+' · відпочинок коштує 1 зміну';return 'До кінця глави '+T.rings(T.chapters[s.chapter].end-s.ring)+' · сезонного ліміту немає';}
function dailySaved(){return T.restore(saved.daily[C.rng.kyivDay()]);}
function coverHtml(){const campaign=T.restore(saved.campaign),day=dailySaved(),action=campaign?(campaign.phase==='playing'?'resume':'campaign-result'):'start';return '<span class="stamp">ОБ’ЄКТ № 1 / ПІД МІСТОМ</span><h2>Два береги.<br>Одна спроба<br>їх з’єднати.</h2><p>Ти — інженер незбудованої <b>залізничної переправи</b>. Веди щит під Дніпром і відкрий історію аварії, консервації та післявоєнного повернення за популярним переказом.</p>'+btn(action,campaign?(campaign.phase==='playing'?'Продовжити кампанію →':'Фінал кампанії →'):'Почати кампанію →','primary')+(campaign?'<p class="saved-state">Збережено: '+T.chapters[campaign.chapter].tag+' · '+T.rings(campaign.ring)+'/56 · '+(campaign.phase==='finished'?'сезон завершено':'незакінчена кампанія')+'. Усі останні рішення збережено.</p>'+ (campaign.phase==='finished'?btn('restart','Нова кампанія','secondary'):''):'<p class="saved-state">Кампанія: 56 кілець. Безпечний пролог почнеться одразу.</p>')+btn('daily',day?(day.phase==='playing'?'Продовжити переправу дня · '+day.ring+'/32':'Результат дня · '+T.rings(day.ring)):'Переправа дня · 32 кільця ↗','secondary')+'<p class="saved-state">'+C.rng.kyivDay()+' · '+(day?(day.phase==='playing'?'спроба триває, можна продовжити':'денний шанс використано'):'одна спроба, перша аварія завершує маршрут')+'</p><p class="smallprint">Гра покрокова. Автозбереження після кожного рішення.</p><div class="aside-note"><h3>Три рішення на кожне кільце</h3><ol class="steps"><li>Обери напрям щита</li><li>Вистав робочий тиск</li><li>Змонтуй нову секцію тунелю</li></ol></div>';}
function asideHtml(){if(home)return coverHtml();const b=T.bounds(s),ev=T.events[b.geo.event],chapter=T.chapters[s.chapter],goal=practice?'Завершити безпечні 4 кільця':s.mode==='daily'?'32 кільця до іншого берега':s.chapter===0?'На кільці 4 відкриється глава «Під руслом»':s.chapter===1?'На кільці 20: лютий 1941, ділянка № 4':s.chapter===2?'На кільці 36: липень 1941, консервація':'На кільці 56: фінал та історична доля переправи';const tactic=s.mode==='daily'?'Використай три проби на всю трасу. Перша аварія завершить спробу.':s.chapter===0?'Попрактикуй обидва напрямки зміни тиску.':s.chapter===1?'Візьми пробу й порівняй коридори на п’ять кілець.':s.chapter===2?'Залишай запас до верхньої межі коридору; порівнюй курси.':'Плануй відпочинок: проходка і шлюзування коштують по 1 зміні.';return '<span class="stamp">'+(practice?'БЕЗПЕЧНЕ ТРЕНУВАННЯ':s.mode==='daily'?'ПЕРЕПРАВА ДНЯ':'ЖУРНАЛ ІНЖЕНЕРА')+'</span><h2>'+ (practice?'Навчися керувати':s.mode==='daily'?'Маршрут на сьогодні':chapter.name)+'</h2><div class="goal-card"><b>Найближча мета</b><p>'+goal+'</p><p><b>Твоя задача:</b> '+tactic+'</p></div>'+(s.mode==='campaign'&&s.chapter===2&&!practice?'<p class="source-note">Переказ · джерело: опис лютневої аварії не має архівної перевірки для цієї гри.</p>':'')+'<div class="event-card"><span class="eyebrow">КОНТЕКСТ НАСТУПНОЇ ДІЛЯНКИ</span><p><b>'+ev[0]+'</b></p><p>'+ev[1]+'</p><p>'+ (s.reveal>s.ring?'Ґрунт: '+b.geo.soil.toLowerCase()+'. ':'')+'Точний стан — у прогнозі біля манометра.</p></div><div class="aside-note">'+(practice?'<p>Окрема сесія без аварій і без збереження. Кампанія й денна спроба не змінюються.</p>':'<p><b>Автозбереження:</b> кільце '+s.ring+' і всі останні рішення.</p>'+ (s.mode==='campaign'?'<p><b>Безпечна точка:</b> кільце '+s.checkpoint+'. Повернення сюди тільки після аварії. Оновлюється кожні 5 кілець, після шлюзування й на початку глави.</p>':'<p>Перший прорив завершує день; шлюзування знімає втому, без відкату після аварії.</p>'))+btn('help','Як грати й читати сигнали ↗','text-button')+'</div><p class="smallprint">'+(practice?'Тренування існує лише в цій вкладці.':store.available()?'Прогрес збережено на цьому пристрої.':'Сховище недоступне: прогрес лише в цій вкладці.')+' Ігрові метри умовні.</p>';}
function render(focus){const b=T.bounds(s),sig=T.signal(s),chapter=T.chapters[s.chapter],inactive=home||paused||!!motionState||s.phase==='finished'||!!s.pending,needsRest=T.needsRest(s);root.className=(home?'home':'game')+(motionState?' is-moving motion-'+motionState.kind:'')+(needsRest?' needs-rest':'');root.setAttribute('aria-busy',String(!!motionState));root.innerHTML='<header class="topbar"><a class="brand" href="https://my-kiev.com/" aria-label="Мій Київ — головна"><img src="https://my-kiev.com/wp-content/uploads/landings/tunel-pid-dniprom/my-kiev-logo.png" width="192" height="165" alt=""><span>Мій Київ</span></a><span class="edition">Місто має більше історій, ніж здається</span><div class="header-actions">'+(!home?btn('pause','Пауза Ⅱ','text-button',!!motionState):'')+'</div></header><section class="hero"><div><div class="eyebrow"><span class="yellow-dot"></span>Київ, якого немає / інтерактивна історія № 01</div><h1>Тунель під Дніпром</h1></div><p class="intro">Під водою — ціле місто рішень.<br>Веди щит. Тримай тиск.</p></section><div class="layout"><section class="machine" aria-label="Пульт інженера"><div class="machine-header"><div class="chapter-row"><span>'+ (practice?'ТРЕНУВАННЯ · БЕЗ ДЕННОГО ШАНСУ':s.mode==='daily'?'ПЕРЕПРАВА ДНЯ · '+s.day:chapter.tag+' / '+chapter.year)+'</span><span>'+chapter.place+'</span></div><div class="ring-row"><div class="ring-number">Кільце '+s.ring+' <small>/ '+(practice?4:T.total(s))+'</small></div><div class="mini-route" aria-label="Прогрес '+T.rings(s.ring)+' із '+(practice?4:T.total(s))+'"><span class="route-fill" style="width:'+s.ring/(practice?4:T.total(s))*100+'%"></span><span class="route-shield" style="left:'+s.ring/(practice?4:T.total(s))*100+'%">◆</span>'+Array.from({length:7},(_,i)=>'<i class="'+(i<s.ring/(practice?4:T.total(s))*6?'done':'')+'"></i>').join('')+'</div></div></div><div class="scene">'+(!home?T.panorama(s,motionState&&motionState.previous):'')+'<div class="tunnel-section">'+T.scene(s,motionState&&motionState.previous,motionState&&motionState.kind,!home)+'<div class="distance">'+s.ring+' м <small>ігрової траси</small></div><div class="scene-label" id="scene-signal">'+sceneStatus()+'</div><div class="depth">Глибина зараз '+fmt(s.depth)+' м</div></div></div><div class="console"><div class="pressure-top"><div><button type="button" data-action="help-instrument" class="instrument-help" aria-label="Пояснення назв і правил">ТИСК У КЕСОНІ · що це?</button><div class="pressure-read"><span id="pressure-value">'+precise(s.pressure)+'</span> <small>атм</small></div></div><span id="pressure-status" class="status-pill '+(sig==='safe'?'':'danger')+'">'+(sig==='safe'?'● У робочій зоні':sig==='water'?'↓ Замало тиску':'↑ Забагато тиску')+'</span></div><div class="pressure-adjust">'+btn('pressure-down','−','pressure-step',inactive)+'<div class="slider-wrap"><div class="gauge-track" style="--lo:'+((b.low-.5)/3*100)+'%;--hi:'+((b.high-.5)/3*100)+'%"></div><input id="pressure" type="range" min="0.5" max="3.5" step="0.05" value="'+s.pressure+'" aria-label="Тиск у кесоні, атмосфери" aria-valuetext="'+precise(s.pressure)+' атмосфери"'+(inactive?' disabled':'')+'></div>'+btn('pressure-up','+','pressure-step',inactive)+'</div><p class="range-help" id="range-help">Робочий коридор наступного кільця: <b>'+T.corridorText(b)+'</b></p><div class="next-copy" id="next-copy">'+nextHtml()+'</div><p class="lesson" id="lesson"'+(T.lesson(s)?'':' hidden')+'>'+T.lesson(s)+'</p><div class="controls"><div class="control-box course-box"><span class="control-title">КУРС ЩИТА</span><div class="directions" role="radiogroup" aria-label="Курс щита">'+[-1,0,1].map((v,i)=>'<button type="button" role="radio" data-course="'+v+'" class="direction '+(s.course===v?'selected':'')+'" aria-label="'+['Угору','Рівно','Униз'][i]+'" aria-checked="'+(s.course===v)+'" tabindex="'+(s.course===v?0:-1)+'"'+(inactive?' disabled':'')+'><span aria-hidden="true">'+['↗','→','↘'][i]+'</span><small>'+['Угору','Рівно','Униз'][i]+'</small></button>').join('')+'</div></div><div class="control-box probe-box"><span class="control-title">ПРОБИ · '+s.probes+'/3</span>'+btn('probe','⌖ Свердловина','probe',inactive||s.probes===0)+'</div>'+primaryButton(inactive)+'</div><div class="forecast-peek" id="forecast-peek"'+(peekHtml()?'':' hidden')+'>'+peekHtml()+'</div><div class="crew-row"><span>Утома <b>'+Math.round(s.fatigue)+'%</b></span><div class="crew-meter" aria-label="Утома '+Math.round(s.fatigue)+' відсотків">'+Array.from({length:6},(_,i)=>'<i class="'+(i<s.fatigue/100*6?'filled '+(needsRest?'tired':''):'')+'"></i>').join('')+'</div>'+ (needsRest?'<b class="crew-required">Час відпочити</b>':btn('rest','Відпочити раніше ↻','early-rest',inactive||s.fatigue===0))+'</div><p class="budget" id="budget">'+budgetHtml()+'</p><div class="action-status" id="live-status" role="status" aria-live="polite">'+(home?'Пульт для перегляду':s.notice+' '+T.nextStatus(s))+'</div>'+ ((motionState&&motionState.kind==='rest')||s.notice.startsWith('Шлюзування завершено')?'<div class="rest-story"><b>Поки бригада відпочиває · популярний переказ</b><p>'+sources[Math.min(4,s.chapter+1)][1]+'</p></div>':'')+'<section class="forecast" aria-label="Прогноз розвідки наступних кілець">'+forecastHtml()+'</section><div class="keyboard-hint"><span><kbd>1</kbd><kbd>2</kbd><kbd>3</kbd> / ↑ ↓ курс</span><span><kbd>←</kbd><kbd>→</kbd> тиск</span><span><kbd>B</kbd> проба · <kbd>R</kbd> відпочинок · <kbd>Esc</kbd> пауза</span><span>Пробіл / Enter: дія поза кнопками</span></div></div></section><aside class="aside">'+asideHtml()+'</aside></div><footer class="footer"><span>Історична гра за популярним переказом. Розріз і фізика — умовна модель.</span><span>Більше, ніж гра. ↗</span></footer><dialog id="game-dialog" aria-labelledby="dialog-title"></dialog>';const down=root.querySelector('[data-action="pressure-down"]'),up=root.querySelector('[data-action="pressure-up"]');down.setAttribute('aria-label','Зменшити тиск на 0,05 атмосфери');up.setAttribute('aria-label','Додати тиску на 0,05 атмосфери');if(!motionState&&!home){if(practice&&s.ring===4)practiceResult();else if(s.pending)pending();else if(s.phase==='finished')result();else focusControl(focus);} }
function updatePressure(){const input=root.querySelector('#pressure'),sig=T.signal(s);input.value=s.pressure;input.setAttribute('aria-valuetext',precise(s.pressure)+' атмосфери. '+T.nextStatus(s));root.querySelector('#pressure-value').textContent=precise(s.pressure);const pill=root.querySelector('#pressure-status');pill.className='status-pill '+(sig==='safe'?'':'danger');pill.textContent=sig==='safe'?'● У робочій зоні':sig==='water'?'↓ Замало тиску':'↑ Забагато тиску';root.querySelector('.section-art').outerHTML=T.scene(s,null,null,true);root.querySelector('#scene-signal').textContent=sceneStatus();root.querySelector('#next-copy').innerHTML=nextHtml();root.querySelector('#live-status').textContent=s.notice+' '+T.nextStatus(s);const lesson=root.querySelector('#lesson');lesson.textContent=T.lesson(s);lesson.hidden=!lesson.textContent;root.querySelector('.forecast').innerHTML=forecastHtml();const peek=root.querySelector('#forecast-peek');peek.textContent=peekHtml();peek.hidden=!peek.textContent;}
function setPressure(value){if(home||paused||motionState||s.pending||s.phase!=='playing'||root.querySelector('dialog[open]'))return;T.act(s,'pressure',Math.round(value*100)/100);persist();updatePressure();}
function finishMotion(){if(motionTimer!==null)clearTimeout(motionTimer);motionTimer=null;motionState=null;render('.advance');}
function playAction(action){if(home||paused||motionState||s.pending||s.phase!=='playing'||root.querySelector('dialog[open]'))return;if((action==='rest'&&s.fatigue===0)||(action==='probe'&&s.probes===0))return;const previous=JSON.parse(JSON.stringify(s)),focus=focusKey(document.activeElement);T.act(s,action);persist();const advanced=s.ring>previous.ring,rested=action==='rest'&&previous.fatigue>0;if(advanced||rested||s.breaks>previous.breaks){const kind=advanced?'advance':rested?'rest':'flood';motionState={previous,kind};render();const reduced=window.matchMedia('(prefers-reduced-motion: reduce)').matches;motionTimer=setTimeout(finishMotion,reduced?0:kind==='rest'?1100:780);}else render(focus);}
function modal(title,html,closable){const d=root.querySelector('#game-dialog');if(!d.open)returnFocus=focusKey(document.activeElement);else d.close();d.innerHTML=(closable?btn('close','×','modal-close'):'')+'<div class="eyebrow">МІЙ КИЇВ / ОБ’ЄКТ № 1</div><h2 id="dialog-title">'+title+'</h2>'+html;d.oncancel=e=>{e.preventDefault();if(closable)close();};d.onkeydown=event=>{if(event.code!=='Tab')return;const all=Array.from(d.querySelectorAll('button:not([disabled]),a[href],input:not([disabled]),textarea,[tabindex="0"]'));if(!all.length)return;const first=all[0],last=all[all.length-1];if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus();}else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus();}};d.showModal();const first=d.querySelector('.primary')||d.querySelector('button,a');if(first)first.focus({preventScroll:true});}
function close(){root.querySelector('#game-dialog').close();paused=false;focusControl(returnFocus);}
function begin(mode){practice=mode==='practice';s=T.create(practice?'campaign':mode);home=false;paused=false;persist();render('#pressure');root.querySelector('.machine').scrollIntoView({block:'start'});}
function resume(state){s=T.restore(state)||T.create();home=false;practice=false;paused=false;if(s.phase==='playing'&&!s.pending)s.notice='Відновлено кільце '+s.ring+'. Перевір курс і тиск. '+(T.needsRest(s)?'Наступна дія — шлюзування.':'Наступна дія — проходка.');persist();render('.advance');root.querySelector('.machine').scrollIntoView({block:'start'});}
function failureHtml(){const f=s.lastFailure;if(!f)return '<p>Деталі останньої аварії відсутні в цьому старому збереженні.</p>';return '<div class="failure-review"><b>Остання спроба монтажу · кільце '+f.ring+'</b><p>Тиск '+precise(f.pressure)+' атм; потрібний коридор '+T.corridorText(f)+'. '+(f.signal==='water'?'Тиску замало — вода зайшла в забій.':'Тиску забагато — повітря прорвало покрівлю.')+'</p></div>';}
function pending(){if(s.pending==='flood'){modal('Ділянку затоплено','<p>'+s.notice+'</p>'+failureHtml()+'<p>Повернення до безпечної точки — кільце '+s.checkpoint+'. Автозбереження відновить уже цей стан. Перевір тиск перед новою проходкою.</p>'+btn('continue','До безпечної точки →','primary'));return;}if(s.pending==='interlude'){modal('Липень 1941. Тиша.','<span class="paper-tag">Популярний переказ · історична інтерлюдія</span><p>'+T.chapterSummary(s)+'</p><p>За опублікованим переказом, роботи законсервували, щити й секції тунелю затопили. Бригада йде з ділянки.</p><p>Переказ також описує лютневий вихід повітря на поверхню. Архівної перевірки для гри немає; ігрові прориви не пояснюють причин історичної зупинки.</p><p>До цього тунелю повернуться після війни.</p>'+btn('continue','1946 рік. Повернутися →','primary'));return;}const next=T.chapters[s.chapter+1];modal('Главу завершено','<span class="paper-tag">'+T.rings(s.ring)+' · '+T.breakCount(s.breaks)+'</span><p>'+T.chapterSummary(s)+'</p><p><b>Наступна подія:</b> '+next.year+' · '+next.name+'</p><p>'+next.text+'</p>'+btn('continue','До наступної глави →','primary'));}
function practiceResult(){modal('Тренування завершено','<p>Ти змонтував 4 кільця, додаючи й зменшуючи тиск. Кампанія збережена, денний шанс не використано.</p><p>У денній спробі ті самі сигнали, але перша аварія завершить маршрут.</p>'+btn('practice-done','До переправи дня →','primary')+btn('home','На обкладинку','secondary'));}
function result(){persist();const complete=s.ring===T.total(s),daily=s.mode==='daily',avg=s.history.length?s.history.reduce((a,p)=>a+p.pressure,0)/s.history.length:null;const title=daily?(complete?'Переправу дня завершено':'Денну спробу завершено'):(complete?'Кампанію завершено':'Сезон завершився');modal(title,'<span class="paper-tag">'+(daily?'Переправа дня · '+s.day:'Твоя ігрова кампанія')+'</span><p class="achievement">'+(complete?'Ти провів бригаду через усі '+T.rings(s.ring)+ (s.breaks?' · '+T.breakCount(s.breaks)+'.':' без проривів.'):T.rings(s.ring)+' із '+T.total(s)+'. '+(s.breaks?'Бригаду виведено через шлюз.':'Ігровий сезон зупинив проходку.'))+'</p><div class="result-profile">'+T.profile(s)+'</div><div class="stats"><div><strong>'+s.breaks+'</strong><small>'+T.word(s.breaks,'прорив','прориви','проривів')+'</small></div>'+ (avg===null?'<div><strong>—</strong><small>Монтажів ще немає</small></div>':'<div><strong>'+fmt(avg)+'</strong><small>Середній тиск змонтованих кілець, атм</small></div>')+'<div><strong>'+s.ring+'</strong><small>'+T.word(s.ring,'кільце','кільця','кілець')+' пройдено</small></div></div>'+ (s.breaks?failureHtml():'')+(daily?'<p>Денний шанс використано. Наступна траса — завтра.</p>':'<p><b>Бюджет останньої глави:</b> використано '+T.shifts(s.finalTurns)+' із 32; залишилось '+T.shifts(32-s.finalTurns)+'. Проходка й відпочинок коштували по 1 зміні.</p><div class="historical-ending"><span class="paper-tag">1949 · художній переказ телефонограми</span><p>Роботи згорнути. Подальше будівництво визнано недоцільним. Історичний Об’єкт № 1 так і не добудували.</p></div>')+'<p class="smallprint">Умовні ігрові кільця, а не реконструкція справжньої траси.</p><label class="share-label" for="share-preview">Текст результату перед копіюванням</label><textarea id="share-preview" class="share-copy" readonly>'+safe(T.shareText(s))+'</textarea>'+btn('share','Скопіювати результат ↑','primary')+btn('home','На поверхню','secondary')+'<div class="aside-note"><b>Історія лишилася в місті</b><p>Побачити кесон можна в парку «Наталка». Оглядай споруду ззовні: заходити всередину небезпечно.</p><a class="source-link" href="https://www.google.com/maps/search/?api=1&query=Кесон+парк+Наталка+Київ" target="_blank" rel="noopener">Кесон на мапі ↗</a></div>');}
function dailyIntro(){const old=dailySaved();if(old){resume(old);return;}modal('Переправа дня','<p><b>32 кільця до фінішу.</b> Траса '+C.rng.kyivDay()+', однакова для всіх. Гра покрокова: час без дій не спливає, можна перерватися й продовжити.</p><p><b>Одна спроба на цьому пристрої. Перша аварія завершує день.</b> Початковий тиск безпечний. Є три проби розвідки на всю трасу; відпочинок лише знімає втому.</p>'+btn('practice','Безпечне тренування · без витрати шансу','secondary')+btn('begin-daily','Почати єдину спробу →','primary')+btn('close','Повернутися','secondary'),true);if(home)returnFocus='[data-action="daily"]';}
function help(){const daily=s.mode==='daily',mode=practice?'Тренування без аварій. Воно не замінює кампанію й не витрачає денний шанс.':daily?'Денна траса: 32 кільця, три проби на всю спробу. Перший прорив завершує день; повернення після аварії немає. Час покроковий.':'Кампанія: 56 кілець. Помилки прологу без аварії. Після прологу аварія повертає до безпечної точки.';modal(practice?'Безпечне тренування':daily?'Як грати переправу дня':'Веди щит. Тримай тиск.','<p><b>'+mode+'</b></p><p><b>Кесон</b> — робоча камера зі стисненим повітрям. <b>Щит</b> — машина, що прокладає тунель. <b>Кільце</b> — одна секція тунелю. <b>Пливун</b> — нестійкий ґрунт, насичений водою. <b>Шлюзування</b> — безпечний вихід і відпочинок бригади.</p><p><b>Курс.</b> Глибше — вищий потрібний тиск і більше втоми, іноді ширший коридор. Глибина «зараз» змінюється тільки після монтажу; окремий прогноз показує наступне кільце.</p><p><b>Манометр.</b> Каламуть — додай тиску, бульбашки — зменш. Жовта зона відповідає наступній проходці. Успіх монтажу й прогноз нової ділянки показано окремо.</p><p><b>Свердловина.</b> Пронумерований прогноз п’яти наступних кілець лишається біля кнопок і враховує обраний курс. '+(daily?'Три проби на весь день.':'Три проби на главу.')+'</p><p><b>Відпочинок.</b> За 88% утоми велика кнопка стає «Шлюзуванням». '+(daily?'Знімає втому, сезонного ліміту немає.':practice?'У тренуванні сезонного ліміту немає.':'Безпечна точка оновлюється кожні 5 кілець, після відпочинку й на початку глави. Автозбереження пам’ятає кожне рішення. Лише у фінальній главі проходка й відпочинок коштують по 1 з 32 змін; раніше сезонного ліміту немає.')+'</p><p><b>Клавіатура.</b> 1 / 2 / 3, ↑ / ↓ — курс; ← / → — тиск; B — свердловина; R — відпочинок; Esc — пауза. Пробіл і Enter активують сфокусовану кнопку; поза елементами керування — головну дію. Фізичні B і R діють незалежно від розкладки.</p>'+btn('close','Зрозуміло','primary'),true);}
root.addEventListener('input',e=>{if(e.target.id==='pressure')setPressure(Number(e.target.value));});
document.addEventListener('keydown',e=>{const target=e.target,interactive=!!target.closest('button,a[href],input,textarea,select,summary,[contenteditable="true"],[role="button"],[role="radio"]'),radio=target.closest('[data-course]');const primaryKey=['Space','Enter','NumpadEnter'].includes(e.code);if(primaryKey&&!e.repeat&&!e.ctrlKey&&!e.metaKey&&!e.altKey&&!e.isComposing&&!home&&!paused&&!motionState&&!s.pending&&s.phase==='playing'&&!root.querySelector('dialog[open]')&&(!interactive||target.type==='range'||target.closest('.advance')))primaryHeld=true;const command=T.keyboardIntent(e,{primaryHeld,home,paused,busy:!!motionState,pending:!!s.pending,finished:s.phase==='finished',modal:!!root.querySelector('dialog[open]'),interactive,primary:!!target.closest('.advance'),range:target.type==='range',editing:!!target.closest('textarea,select,[contenteditable="true"]')||(target.tagName==='INPUT'&&target.type!=='range'),radio:!!radio,course:radio?Number(radio.dataset.course):s.course});if(!command)return;e.preventDefault();if(command.action==='block')return;if(command.action==='pressure'){setPressure(s.pressure+command.value);return;}if(command.action==='course'){T.act(s,'course',command.value);persist();render(radio?'[data-course="'+s.course+'"]':focusKey(target));return;}if(command.action==='pause'){root.querySelector('[data-action="pause"]').click();return;}playAction(command.action==='primary'?T.primaryAction(s):command.action);});
document.addEventListener('keyup',e=>{if(['Space','Enter','NumpadEnter'].includes(e.code))primaryHeld=false;});
window.addEventListener('blur',()=>{primaryHeld=false;});
root.addEventListener('click',async e=>{const target=e.target.closest('button');if(!target||target.disabled||motionState)return;if(target.hasAttribute('data-course')){T.act(s,'course',Number(target.dataset.course));persist();render('[data-course="'+s.course+'"]');return;}const a=target.dataset.action;if(a==='start'||a==='begin'){begin('campaign');return;}if(a==='resume'||a==='campaign-result'){resume(saved.campaign);return;}if(a==='daily'){dailyIntro();return;}if(a==='practice'){begin('practice');return;}if(a==='practice-done'){practice=false;home=true;s=T.restore(saved.campaign)||T.create();render();dailyIntro();return;}if(a==='begin-daily'){begin('daily');return;}if(a==='pressure-down'||a==='pressure-up'){setPressure(s.pressure+(a==='pressure-up'?.05:-.05));return;}if(['advance','probe','rest'].includes(a)){playAction(a);return;}if(a==='continue'){T.continue(s);persist();render('.advance');return;}if(a==='help'||a==='help-instrument'){help();return;}if(a==='pause'){paused=true;modal(practice?'Пауза тренування':'Пауза на поверхні','<p>Гра покрокова: час без дій не спливає. '+(practice?'Тренування лише в цій вкладці; кампанія й денний шанс не змінені.':'Усі останні рішення збережено.')+'</p>'+btn('close','Продовжити →','primary')+btn('home','До обкладинки','secondary')+(!practice&&s.mode==='campaign'?btn('restart','Почати кампанію заново','text-button'):''),true);return;}if(a==='restart'){modal('Нова кампанія?','<p>Прогрес поточної кампанії буде замінено. Денна спроба збережеться.</p>'+btn('begin','Почати заново','primary')+btn('close','Залишити прогрес','secondary'),true);return;}if(a==='home'){persist();home=true;practice=false;paused=false;s=T.restore(saved.campaign)||T.create();render();focusControl('.aside .primary');root.scrollIntoView({block:'start'});return;}if(a==='close'){close();return;}if(a==='share'){const area=root.querySelector('#share-preview');try{if(navigator.clipboard){await navigator.clipboard.writeText(T.shareText(s));target.textContent='Результат скопійовано ✓';return;}}catch(err){}area.focus();area.select();target.textContent='Текст виділено — скопіюй його';}});
document.addEventListener('visibilitychange',()=>{if(document.hidden){persist();if(motionState)finishMotion();}});
render();
})();