ПОДІЛ
240 м82°
ПІКСЕЛЬНИЙ КИЇВ · 1 / 5Кава
Кава
з собою♥
Донеси гарячою. Не розлий.
Як підемо?
Твоя кава. Твій ритм.
АСФАЛЬТ16 / 16 КОВТКІВ
/ 16
гарячих ковтків
гарячих ковтків
Донеси гарячою. Не розлий.
Прогулянку призупинено.
(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 K = root.Kava = {}, C = root.GameCore;
K.routes = [
['Поділ', 'Контрактова → Андріївський', 'cobbles'], ['Хрещатик', 'Майдан → офіс', 'stairs'],
['Золоті ворота', 'Метро → Ярославів Вал', 'roots'], ['Арсенальна', 'Метро → Лавра', 'asphalt'],
['Лівобережна', 'Метро → бізнес-центр', 'tiles'], ['Позняки', 'Зупинка → двір', 'curb'],
['Лук’янівка', 'Метро → офіс', 'rails'], ['Пейзажна алея', 'Алея → кавовий дворик', 'slope']
];
K.levels = [
{route: 0, scene: 'podil', length: 240, subtitle: 'Бруківка Подолу'},
{route: 1, scene: 'khreshchatyk', length: 252, subtitle: 'Сходи біля Майдану'},
{route: 2, scene: 'zoloti-vorota', length: 248, subtitle: 'Плитка й коріння'},
{route: 3, scene: 'arsenalna', length: 260, subtitle: 'Підйом до Лаври'},
{route: 4, scene: 'livoberezhna', length: 268, subtitle: 'Вітер із Дніпра'}
];
K.surfaces = {asphalt: ['Асфальт', 0.9], cobbles: ['Бруківка', 1.8], stairs: ['Сходи', 2.2], roots: ['Коріння каштана', 1.7], tiles: ['Хитка плитка', 1.8], curb: ['Бордюр', 2], rails: ['Трамвайні колії', 1.8], slope: ['Спуск', 1.5]};
K.events = {phone: 'Людина з телефоном', scooter: 'Самокат поруч', pigeons: 'Голуби на тротуарі', promoter: 'Промоутер із листівкою', wind: 'Порив вітру', puddle: 'Мокра плитка'};
const DT = 1 / 60;
function create(seed, index) {
const rng = C.rng.mulberry32(seed), route = index === undefined ? seed % K.routes.length : index;
const length = K.levels[route] ? K.levels[route].length : 240;
return {seed: seed, route: route, t: 0, distance: 0, length: length, coffee: 16, spilled: 0, cold: 0, temp: 82, x: 0, v: 0, steps: 0, lastTap: -10, ended: false, completed: false, fork: null, choice: null, branchUntil: 0, eventIndex: 0, feedback: '', feedbackUntil: 0, worst: '', worstLoss: 0,
events: [65, 115, 185].map(function (base) { const types = Object.keys(K.events); return {at: base + (route < K.levels.length ? route * 3 : 0), type: types[Math.floor(rng() * types.length)], wait: 0, passed: false}; })};
}
function surface(s) { if (s.distance < 30) return 'asphalt'; if (s.choice === 'stairs' && s.distance < s.branchUntil) return 'stairs'; if (s.choice === 'crossing' && s.distance < s.branchUntil) return 'asphalt'; return Math.floor(s.distance / 30) % 3 === 2 ? 'asphalt' : K.routes[s.route][2]; }
function say(s, text) { s.feedback = text; s.feedbackUntil = s.t + 2.2; }
function warning(s) { const e = s.events[s.eventIndex]; return e && !e.passed && e.at - s.distance <= 20 ? e : null; }
function choose(s, choice) { if (!s.fork || !['stairs', 'crossing'].includes(choice)) return; s.choice = choice; s.fork = null; s.branchUntil = s.distance + 36; if (choice === 'crossing') s.length += 24; say(s, choice === 'stairs' ? 'Коротше, але кожна сходинка хитає каву' : 'Рівний обхід: +24 м'); }
function tick(s, tap) {
if (s.ended) return s;
s.t += DT;
s.temp = 20 + 62 * Math.exp(-s.t / 105);
s.cold = Math.min(Math.floor(s.coffee), Math.max(0, Math.floor((s.t - 42) / 4)));
const e = warning(s);
if (e && e.at - s.distance <= 8) { e.wait += DT; if (e.wait >= 1.1) { e.passed = true; s.eventIndex++; say(s, 'Пропустив. Кава в безпеці'); } }
if (tap && !s.fork && s.t - s.lastTap >= 0.095) {
const interval = s.t - s.lastTap, ground = surface(s), fast = Math.max(0, 0.36 - interval) * 13;
s.lastTap = s.t; s.steps++;
s.v += (2.9 + fast) * K.surfaces[ground][1];
s.distance = Math.min(s.length, s.distance + 4);
if (e && !e.passed && s.distance >= e.at) { s.v += 5; e.passed = true; s.eventIndex++; say(s, K.events[e.type] + ': поштовх!'); }
if (!s.choice && s.distance >= Math.round(s.length * 0.56)) s.fork = true;
if (s.distance >= s.length) { s.ended = true; s.completed = true; }
}
s.v += (-Math.pow(2 * Math.PI / 0.72, 2) * s.x - 1.65 * s.v) * DT;
s.x += s.v * DT;
const rim = 0.43 + (16 - s.coffee) * 0.033;
if (Math.abs(s.x) > rim && s.coffee > 0) {
const loss = Math.min(s.coffee, (Math.abs(s.x) - rim) * 16 * DT);
s.coffee -= loss; s.spilled = 16 - s.coffee;
if (loss > s.worstLoss) { s.worstLoss = loss; s.worst = K.surfaces[surface(s)][0]; }
say(s, 'Хлюп! Зміни ритм кроків');
}
if (s.t >= 108 || s.coffee < 0.1) { s.ended = true; }
return s;
}
function score(s) { const left = Math.max(0, Math.floor(s.coffee + 0.000001)); const cold = Math.min(left, s.cold); return {hot: s.completed ? left - cold : 0, cold: s.completed ? cold : left, spilled: 16 - left}; }
function rank(s) { const n = score(s).hot; return !s.completed ? 'Кава не дійшла' : n === 16 ? 'Жодної краплі' : n >= 10 ? 'Гаряча й майже повна' : n >= 5 ? 'Пів стакана' : 'Кава на рукаві'; }
K.model = {DT: DT, create: create, tick: tick, choose: choose, surface: surface, warning: warning, score: score, rank: rank};
})(globalThis);
globalThis.Kava.assetURL = 'https://my-kiev.com/wp-content/uploads/2026/09/kava-pixel-kyiv.webp';
globalThis.Kava.heroURL = 'https://my-kiev.com/wp-content/uploads/2026/09/kava-coffee-walker.png';
globalThis.Kava.npcURLs = {"phone": "https://my-kiev.com/wp-content/uploads/2026/09/kava-npc-phone.png", "scooter": "https://my-kiev.com/wp-content/uploads/2026/09/kava-npc-scooter.png", "promoter": "https://my-kiev.com/wp-content/uploads/2026/09/kava-npc-promoter.png"};
globalThis.Kava.sceneURLs = {"khreshchatyk": "https://my-kiev.com/wp-content/uploads/2026/09/kava-scene-khreshchatyk.webp", "zoloti-vorota": "https://my-kiev.com/wp-content/uploads/2026/09/kava-scene-zoloti-vorota.webp", "arsenalna": "https://my-kiev.com/wp-content/uploads/2026/09/kava-scene-arsenalna.webp", "livoberezhna": "https://my-kiev.com/wp-content/uploads/2026/09/kava-scene-livoberezhna.webp"};
(function(K){
'use strict';
function initial(){return {unlocked:0,selected:0,best:{}};}
function normalize(value){
const p=value&&typeof value==='object'?value:{};
const unlocked=Number.isInteger(p.unlocked)?Math.max(0,Math.min(K.levels.length-1,p.unlocked)):0;
return {unlocked:unlocked,selected:Number.isInteger(p.selected)?Math.max(0,Math.min(unlocked,p.selected)):unlocked,best:p.best&&typeof p.best==='object'&&!Array.isArray(p.best)?p.best:{}};
}
function select(value,index){
const state=normalize(value);
if(!Number.isInteger(index)||index<0||index>state.unlocked)return null;
return Object.assign({},state,{selected:index});
}
function complete(value,index,score){
const state=normalize(value);
if(!Number.isInteger(index)||index<0||index>=K.levels.length||index>state.unlocked)return state;
const best=Object.assign({},state.best);
if(!best[index]||score.hot>best[index].hot)best[index]={hot:score.hot,cold:score.cold,spilled:score.spilled};
return {unlocked:Math.max(state.unlocked,Math.min(K.levels.length-1,index+1)),selected:index,best:best};
}
K.campaign={initial:initial,normalize:normalize,select:select,complete:complete};
})(globalThis.Kava);
(function (K) {
'use strict';
const image = new Image(); image.src = K.assetURL;
const scenes = {podil: image};
Object.keys(K.sceneURLs || {}).forEach(function(place) { const img = new Image(); img.src = K.sceneURLs[place]; scenes[place] = img; });
function sceneFor(s) { const level = K.levels[s.route]; return level && scenes[level.scene] ? scenes[level.scene] : image; }
const hero = new Image(); hero.src = K.heroURL;
const npcs = {};
Object.keys(K.npcURLs || {}).forEach(function(type) { const img = new Image(); img.src = K.npcURLs[type]; npcs[type] = img; });
function rect(c,x,y,w,h,color) { c.fillStyle=color;c.fillRect(Math.round(x),Math.round(y),w,h); }
function cup(c,s,menu) {
const x=90,y=menu?236:239,w=180,h=151,fill=s.coffee/16;
rect(c,x+18,y+h+7,152,9,'#12091d');
c.fillStyle='#ffe4bf';c.beginPath();c.moveTo(x,y);c.lineTo(x+w,y);c.lineTo(x+w-18,y+h);c.lineTo(x+18,y+h);c.closePath();c.fill();
c.save();c.beginPath();c.moveTo(x+8,y+7);c.lineTo(x+w-8,y+7);c.lineTo(x+w-26,y+h-9);c.lineTo(x+26,y+h-9);c.closePath();c.clip();
rect(c,x,y,w,h,'#e8bb91');
const level=y+h-12-fill*(h-28),wave=s.x*46;
for(let i=0;i<w;i+=4){const py=level+wave*(i-w/2)/(w/2)+Math.sin(i/28+s.t*7)*Math.abs(s.x)*5;rect(c,x+i,py,4,y+h-py,'#53261e');rect(c,x+i,py,4,6,'#dd9354');rect(c,x+i,py+6,4,4,'#9e522d');}
rect(c,x+13,y+12,7,h,'#fff2cf44');rect(c,x+w-27,y,9,h,'#3d1b2c33');c.restore();
rect(c,x-7,y-5,w+14,7,'#fff0d5');rect(c,x-3,y+2,7,7,'#fff0d5');rect(c,x+w-4,y+2,7,7,'#fff0d5');
if(s.temp>52){for(let i=0;i<3;i++){const dx=x+55+i*30;const shift=Math.sin(s.t*1.6+i)*5;rect(c,dx+shift,y-35-i%2*9,4,12,'#ebd7c5aa');rect(c,dx+shift+4,y-44-i%2*9,4,10,'#ebd7c577');}}
if(Math.abs(s.x)>.5){const right=s.x>0;rect(c,right?x+w+7:x-13,y+20,5,9,'#e8a66c');rect(c,right?x+w+16:x-23,y+38,4,6,'#e8a66c');}
}
function person(c,x,y,t,type) {
const coral=type==='scooter'?'#8af2c8':'#ffb786';
rect(c,x+3,y,8,8,'#ffc9a1');rect(c,x+2,y-3,10,5,'#38213d');rect(c,x,y+8,13,19,coral);
const step=Math.sin(t*12)>0?3:-3;rect(c,x+1+step,y+27,4,13,'#1c1833');rect(c,x+8-step,y+27,4,13,'#1c1833');
if(type==='phone')rect(c,x+15,y+10,4,7,'#9edbed');
if(type==='scooter'){rect(c,x-5,y+40,28,3,'#8af2c8');rect(c,x+20,y+20,3,20,'#8af2c8');rect(c,x-3,y+44,5,5,'#130b1d');rect(c,x+20,y+44,5,5,'#130b1d');}
}
function walker(c,s,reduced) {
// A short response to an actual step; the character rests between taps.
const elapsed = s.t - s.lastTap;
const lift = !reduced && elapsed >= 0 && elapsed < 0.24 ? Math.sin(elapsed / 0.24 * Math.PI) * 2 : 0;
rect(c,51,199,50,3,'#160c2688');
if(hero.complete && hero.naturalWidth) {
c.drawImage(hero,36,91-Math.round(lift),76,114);
} else {
person(c,58,155,0,'walker');
rect(c,72,172,12,5,'#ffc9a1');
rect(c,80,165,9,12,'#fff0d5');
rect(c,80,164,9,3,'#673524');
}
}
function eventArt(c,e,x,s,reduced) {
const sprite = npcs[e.type];
if(sprite && sprite.complete && sprite.naturalWidth) {
const h = e.type === 'promoter' ? 105 : 114;
const w = Math.round(h * sprite.naturalWidth / sprite.naturalHeight);
const bob = !reduced && e.type === 'phone' ? Math.round(Math.sin(s.t*6)) : 0;
rect(c,x-22,199,48,3,'#160c2688');
c.drawImage(sprite,Math.round(x-w/2),204-h-bob,w,h);
return;
}
if(e.type === 'puddle') {
rect(c,x-19,194,38,5,'#5da8c4');rect(c,x-13,192,25,3,'#87c9db');rect(c,x-9,194,12,1,'#d6f2ed');
} else if(e.type === 'wind') {
const shift = reduced ? 0 : Math.round(Math.sin(s.t*4)*4);
for(let i=0;i<3;i++){rect(c,x-24+shift+i*4,158+i*9,31-i*5,2,'#c8dfd9');rect(c,x+8+shift,164+i*7,4,3,'#d8b679');}
} else if(e.type === 'pigeons') {
for(let i=0;i<3;i++){const px=x+i*13-12;rect(c,px,191-i%2*4,11,6,'#99a5bd');rect(c,px+7,185-i%2*4,6,7,'#b4c7d4');rect(c,px+12,187-i%2*4,3,2,'#efbe85');rect(c,px+4,197-i%2*4,2,3,'#d68d87');}
} else {
// Distinct, small loading fallback for each person; never draw weather as a pedestrian.
person(c,x-6,155,reduced?0:s.t,e.type);
if(e.type === 'promoter'){rect(c,x-8,162,15,14,'#d9aa54');rect(c,x-16,170,9,6,'#fff0d5');}
}
}
K.art={draw:function(c,s,menu,reduced){
c.imageSmoothingEnabled=false;rect(c,0,0,360,430,'#28163d');
const scene = sceneFor(s);
if(scene.complete&&scene.naturalWidth){const crop=scene.naturalHeight*360/202,span=Math.max(0,scene.naturalWidth-crop);c.drawImage(scene,Math.max(0,span*s.distance/s.length),0,Math.min(scene.naturalWidth,crop),scene.naturalHeight,0,0,360,202);}
else{rect(c,0,0,360,202,'#573d75');for(let i=0;i<7;i++){rect(c,i*57,70-i%3*12,48,120,'#9677a4');for(let j=0;j<4;j++)rect(c,i*57+9,90+j*22,9,13,'#edbd83');}}
rect(c,0,198,360,6,'#be8a98');
const ground=K.model.surface(s);for(let i=0;i<24;i++){let px=(i*18-s.distance*3)%390;rect(c,px,205+(i%2)*6,ground==='asphalt'?16:13,4,ground==='asphalt'?'#5b426a':'#927196');}
if(!menu){const e=K.model.warning(s);if(e)eventArt(c,e,70+(e.at-s.distance)*11,s,reduced);walker(c,s,reduced);}
rect(c,0,218,360,212,'#28163d');for(let i=0;i<17;i++)rect(c,(i*67)%360,232+(i*29)%174,2,2,'#543565');
cup(c,s,menu);
if(!menu){c.fillStyle='#bca1cb';c.font='9px monospace';c.textAlign='center';c.fillText('СЛІДКУЙ ЗА ХВИЛЕЮ',180,420);}
},card:function(c,s,day){const score=K.model.score(s),scene=sceneFor(s);rect(c,0,0,900,600,'#241237');if(scene.complete&&scene.naturalWidth){c.imageSmoothingEnabled=false;c.drawImage(scene,0,0,900,210);}rect(c,30,235,5,310,'#8af2c8');c.fillStyle='#fff0d5';c.font='bold 38px monospace';c.fillText('Кава з собою',65,275);c.font='22px monospace';c.fillText(K.routes[s.route][0]+' · '+day,65,315);c.fillStyle='#ffb68a';c.font='bold 76px monospace';c.fillText(score.hot+' / 16',65,410);c.fillStyle='#fff0d5';c.font='24px monospace';c.fillText('гарячих ковтків · '+Math.round(s.temp)+'°',65,455);c.font='21px monospace';c.fillText(K.model.rank(s),65,500);c.fillStyle='#8af2c8';c.font='18px monospace';c.fillText('МІЙ КИЇВ · ПІКСЕЛЬНА ПРОГУЛЯНКА',65,553);}};
})(globalThis.Kava);
(function(K,C){
'use strict';
const root=document.getElementById('kvRoot');if(!root||root.dataset.ready)return;root.dataset.ready='1';
const $=function(id){return document.getElementById('kv'+id);},M=K.model;
function makeCanvas(id,host,width,height,label){const canvas=document.createElement('canvas');canvas.id='kv'+id;canvas.width=width;canvas.height=height;if(label)canvas.setAttribute('aria-label',label);host.append(canvas);return canvas;}
const gameCanvas=makeCanvas('Canvas',$('CanvasHost'),360,430,'Київська вулиця та хвиля кави');
makeCanvas('Card',$('CardHost'),900,600,'Картка результату');
const ctx=gameCanvas.getContext('2d');
const store=C.createStore('kava-pixel-levels-v1',K.campaign.initial,K.campaign.normalize);
let selected=store.load().selected,runDay=C.rng.kyivDay(),s=M.create(C.rng.dailySeed(runDay+':'+selected),selected),status='menu',last=0,accumulator=0,pending=false,firstTap=false,announced='';
const reduced=matchMedia('(prefers-reduced-motion: reduce)');
const icon={hot:String.fromCodePoint(0x2615),cold:String.fromCodePoint(0x2744),spilled:String.fromCodePoint(0x1F4A7)};
function emit(event,extra){const detail=Object.assign({event:event,game:'kava-na-bigu',day:runDay,mode:'campaign',level:selected+1,place:K.routes[selected][0],device:matchMedia('(pointer: coarse)').matches?'touch':'desktop'},extra);root.dispatchEvent(new CustomEvent('kava:analytics',{bubbles:true,detail:detail}));if(Array.isArray(window.dataLayer))window.dataLayer.push(detail);}
function progress(){
const saved=store.load();$('LevelNumber').textContent=(selected+1)+' / '+K.levels.length;
$('Daily').textContent=K.routes[selected][0]+' · '+K.levels[selected].subtitle;
$('DayLabel').textContent=(selected+1)+' / '+K.levels.length+' · '+K.routes[selected][0];
$('LevelDescription').textContent=K.levels[selected].subtitle+'. Фініш відкриває наступну вулицю.';
const best=saved.best[selected];$('Best').textContent=best?'Найкраще: '+best.hot+' / 16 гарячих ковтків':'Маршрут ще не пройдено';
$('Levels').replaceChildren();K.levels.forEach(function(level,i){const button=C.dom.el('button','',String(i+1)+(saved.best[i]?' ✓':''));button.type='button';button.title=K.routes[i][0]+(i>saved.unlocked?' — замкнено':'');button.setAttribute('aria-label','Рівень '+(i+1)+': '+K.routes[i][0]+(i>saved.unlocked?', замкнено':''));button.disabled=i>saved.unlocked||status==='running'||status==='paused';if(saved.best[i])button.className='kv-done';if(i===selected)button.setAttribute('aria-current','step');button.addEventListener('click',function(){select(i);});$('Levels').append(button);});
}
function select(index){if(status==='running'||status==='paused')return;const saved=K.campaign.select(store.load(),index);if(!saved)return;selected=index;store.save(saved);s=M.create(C.rng.dailySeed(C.rng.kyivDay()+':'+selected),selected);status='menu';$('Intro').hidden=false;$('Result').hidden=true;$('Paused').hidden=true;$('Pause').disabled=true;$('Step').disabled=true;$('Feedback').textContent='Твоя кава. Твій ритм.';progress();hud();}
function hud(){const e=M.warning(s);$('District').textContent=K.routes[s.route][0].toUpperCase();$('Route').textContent=K.routes[s.route][1];$('Distance').textContent=Math.max(0,Math.ceil(s.length-s.distance))+' м';$('Temp').textContent=Math.round(s.temp)+'°';$('Progress').style.width=(s.distance/s.length*100)+'%';$('Surface').textContent=K.surfaces[M.surface(s)][0].toUpperCase();$('Sips').textContent=Math.floor(s.coffee)+' / 16 КОВТКІВ';$('Fork').hidden=!s.fork||status!=='running';const message=s.feedbackUntil>s.t?s.feedback:e?'Попереду: '+K.events[e.type]+'. Зупинись на мить.':s.t>42?'Кава холоне. Час рухатися.':'Тапни будь-де. Спостерігай за хвилею.';if(status==='running'&&message!==announced){$('Feedback').textContent=message;announced=message;}}
function start(){runDay=C.rng.kyivDay();s=M.create(C.rng.dailySeed(runDay+':'+selected),selected);status='running';pending=false;firstTap=false;accumulator=0;last=0;announced='';$('Intro').hidden=true;$('Result').hidden=true;$('Paused').hidden=true;$('Step').disabled=false;$('Pause').disabled=false;$('Stage').focus({preventScroll:true});progress();emit('run_start');hud();}
function step(){if(status!=='running'||s.fork)return;pending=true;if(!firstTap){firstTap=true;emit('first_tap');}}
function pause(){if(status!=='running')return;status='paused';pending=false;$('Paused').hidden=false;$('Fork').hidden=true;$('Step').disabled=true;$('Resume').focus({preventScroll:true});progress();}
function resume(){if(status!=='paused')return;status='running';last=0;accumulator=0;$('Paused').hidden=true;$('Step').disabled=false;$('Stage').focus({preventScroll:true});progress();hud();}
function finish(){
status='result';pending=false;$('Pause').disabled=true;$('Step').disabled=true;$('Fork').hidden=true;
const score=M.score(s),saved=store.load();
if(s.completed)store.save(K.campaign.complete(saved,selected,score));
progress();$('Result').hidden=false;$('ResultMode').textContent='РІВЕНЬ '+(selected+1)+' / '+K.levels.length+' · '+K.routes[selected][0].toUpperCase();$('ResultTitle').textContent=M.rank(s);$('Score').textContent=score.hot;$('SipGrid').replaceChildren();Object.keys(score).forEach(function(type){for(let i=0;i<score[type];i++){const el=C.dom.el('span','',icon[type]);el.title={hot:'Гарячий ковток',cold:'Холодний ковток',spilled:'Розлитий ковток'}[type];el.setAttribute('aria-label',el.title);$('SipGrid').append(el);}});
$('ResultText').textContent=Math.round(s.temp)+'° · '+Math.round(s.t)+' с · '+score.cold+' холодних · '+score.spilled+' розлитих. '+(s.completed?(selected===K.levels.length-1?'Ти пройшов усі п’ять вулиць Києва!': 'Наступна вулиця відкрита: '+K.routes[selected+1][0]+'.'):'До фінішу не дійшли. Спробуй цей маршрут ще раз.');
$('Next').hidden=!s.completed||selected===K.levels.length-1;if(!$('Next').hidden)$('Next').textContent='ДАЛІ: '+K.routes[selected+1][0].toUpperCase()+' →';
$('ShareStatus').textContent='';$('Copy').hidden=true;K.art.card($('Card').getContext('2d'),s,runDay);(s.completed&&selected<K.levels.length-1?$('Next'):$('Retry')).focus({preventScroll:true});emit('run_finish',Object.assign({temperature:Math.round(s.temp),seconds:s.t,completed:s.completed},score));
}
function shareText(){const score=M.score(s);return ['Кава з собою · Рівень '+(selected+1)+' / '+K.levels.length+' · '+K.routes[s.route][0],score.hot+' з 16 гарячих ковтків · '+Math.round(s.temp)+'°',icon.hot.repeat(score.hot)+icon.cold.repeat(score.cold)+icon.spilled.repeat(score.spilled),M.rank(s)].join(String.fromCharCode(10));}
function frame(time){if(status==='running'){accumulator+=last?Math.min(.25,(time-last)/1000):0;while(accumulator>=M.DT&&!s.ended){M.tick(s,pending);pending=false;accumulator-=M.DT;}hud();if(s.ended)finish();}last=time;K.art.draw(ctx,s,status==='menu',reduced.matches);requestAnimationFrame(frame);}
$('Start').addEventListener('click',start);$('Retry').addEventListener('click',function(){emit('run_retry');start();});$('Next').addEventListener('click',function(){if(status!=='result'||!s.completed||selected+1>=K.levels.length)return;select(selected+1);start();});$('Pause').addEventListener('click',function(){status==='paused'?resume():pause();});$('Resume').addEventListener('click',resume);
[$('Stage'),$('Step')].forEach(function(el){el.addEventListener('pointerdown',function(e){if(e.target.closest('button')&&e.currentTarget!==$('Step'))return;if(e.button!==0)return;e.preventDefault();step();});el.addEventListener('contextmenu',function(e){e.preventDefault();});});
$('Stage').addEventListener('keydown',function(e){if(e.target!==$('Stage'))return;if(e.code==='Space'||e.code==='Enter'){e.preventDefault();if(!e.repeat)step();}});
$('Step').addEventListener('keydown',function(e){if(e.code==='Space'||e.code==='Enter'){e.preventDefault();if(!e.repeat)step();}});
root.addEventListener('keydown',function(e){if(e.code==='Escape'){status==='paused'?resume():pause();}});
root.querySelectorAll('[data-choice]').forEach(function(el){el.addEventListener('click',function(){M.choose(s,el.dataset.choice);$('Stage').focus({preventScroll:true});hud();});});
window.addEventListener('blur',pause);document.addEventListener('visibilitychange',function(){if(document.hidden)pause();});
$('Share').addEventListener('click',async function(){const text=shareText();emit('share_click');try{if(navigator.share){await navigator.share({title:'Кава з собою',text:text});$('ShareStatus').textContent='Результат надіслано.';}else if(navigator.clipboard){await navigator.clipboard.writeText(text);$('ShareStatus').textContent='Скопійовано. Надішли друзям!';}else throw new Error('clipboard');}catch(e){if(e.name==='AbortError')return;$('Copy').hidden=false;$('Copy').value=text;$('Copy').focus();$('Copy').select();$('ShareStatus').textContent='Скопіюй текст результату нижче.';}});
$('Download').addEventListener('click',function(){emit('share_click',{method:'png'});$('Card').toBlob(function(blob){if(!blob){$('ShareStatus').textContent='Не вдалося створити картку.';return;}const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='kava-level-'+(selected+1)+'.png';a.click();setTimeout(function(){URL.revokeObjectURL(url);},10000);},'image/png');});
progress();hud();requestAnimationFrame(frame);
})(globalThis.Kava,globalThis.GameCore);