МАЛЕНЬКА ПОДОРОЖ ПІД КИЄВОМ
ЕскалаторАрсенальної Місто не поспішає. Знайди свій ритм — і піднімайся.
Маршрут дня Тренування
ДВА ПРОЛЬОТИ · ОДИН РИТМ
↟ Крок за кроком Натискай рівномірно, раз на 0,6–0,7 секунди. Натискай, коли маркер у світлій зоні.
“ Трохи ввічливості Хтось попереду? «Перепрошую» звільнить шлях, коли праворуч буде місце.
→ Дозволь собі паузу Праворуч можна крокувати, коли попереду вільно, або відновити дихання. Якщо хтось стоїть — обійди ліворуч.
Як грати та яку стратегію обрати ↗ ✧ ТВОЯ МАЛЕНЬКА ІСТОРІЯ Перший підйом ще попереду
Клавіатура: Пробіл крок · ← → смугаE перепрошую · F штовхнути · P пауза
Перевіряємо статус тривоги…
М АрсенальнаПЕРШИЙ ПРОЛІТ
♪ Ⅱ
0 кроків · 0 м пішки Як грати ?
У Києві тривога · деталі ↗
Один тап — один крок Почни з кнопки внизу. Тримай спокійний ритм.
Твоя героїня — у помаранчевому
Тапай у ритмі кроку. Не поспішай. ЛІВА СМУГА
Рівний крок
Тапай у світлій зоні Штовхнути F КРОК Перепрошую E Коли хтось попереду → Перепочити
М Запалюємо світло… Завантажуємо станцію та пасажирів.
Спробувати ще раз × ТВІЙ ПІДЙОМ, ТВОЯ ШВИДКІСТЬ Як грати Дійди нагору швидше, ніж стоячи, і не затримуй людей зліва. Результат — зекономлені секунди.
Крокуй, а не поспішай Натискай «Крок» раз на 0,6–0,7 с . Після кроку маркер починає рух спочатку. Наступний тап — у світлій зоні. Серія вдалих кроків показує твій ритм. Попроси пропустити Коли з’явиться пасажир і кнопка підсвітиться, натисни «Перепрошую» або клавішу E (У в українській розкладці). Іноді треба підійти ближче або повторити через секунду. Якщо праворуч тісно — зачекай на проміжок. Пульс близько 130? Перепочинь Перейди праворуч кнопкою або свайпом. Коли перед тобою вільно, тут теж можна крокувати; якщо людина попереду — обійди ліворуч. Для відпочинку зупинись і дочекайся пульсу приблизно 100–105 і повернись ліворуч. Понад 150 — примусова зупинка. Можна штовхнути — але це має ціну Підійди впритул і натисни «Штовхнути» або F (А в українській розкладці). Пасажир втратить рівновагу й упаде. Кожне штовхання додає 3 с штрафу й 12 пульсу ; штраф віднімається від зекономленого часу. Надійна стратегія Рівний темп → завчасне «Перепрошую» → короткий перепочинок праворуч. У холі дихання відновиться саме. Не стій зліва: позаду теж люди.
Пробіл / ↑ — крок · ← → — смуги E — перепрошую · F — штовхнути · P — пауза
Поки читаєш, час гри зупинено.
Зрозуміло, до гри ↑ МІСТО ЗАЧЕКАЄ Переведи подих Твій підйом на паузі.
Продовжити ↑ Почати спочатку М ПОЛОВИНА ШЛЯХУ ПОЗАДУ Трохи світла. Трохи повітря. Між двома прольотами — мить тиші. Дихання відновлюється.
3 І ЗНОВУ — КИЇВ Нагорі!
Зекономлено с
Зупинок зліва
Ввічливих прохань
Штовхань / штраф
Кроків у ритмі
↻ Ще раз Поділитися ↗ Картка ↓
На справжньому ескалаторі тримайся за поручень і не бігай.
ПОДБАЙ ПРО СЕБЕ У Києві тривога Прямуй до найближчого укриття. Якщо ти вже в безпечному місці, можеш продовжити гру.
Знайти укриття ↗ Продовжити грати Перевірити статус /* assets/rng.js */
(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);
/* assets/store.js */
(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);
/* model.js */
(function(root){
'use strict';
const TOTAL=105.5, BASE=.72, HALL=3, BASELINE=TOTAL/BASE+HALL;
const RIGHT_POSITIONS=Array.from({length:8},(_,i)=>6+i*14);
const TYPES=[
{name:'Турист із мапою',line:'Ой, тут праворуч? Дякую!',asks:2,sprite:1},
{name:'Задумлива пасажирка',line:'Звісно, проходьте.',asks:1,sprite:2},
{name:'Пасажир із портфелем',line:'Перепрошую, замислився.',asks:1,sprite:3},
{name:'Людина в навушниках',line:'А, так! Проходьте.',asks:1,sprite:2,near:true},
{name:'Гість міста',line:'Дякую, що підказали!',asks:1,sprite:1},
{name:'Пасажир із валізою',line:'Зараз посуну валізу…',asks:2,sprite:3},
{name:'Поціновувач архітектури',line:'Яке ж тут світло. Проходьте!',asks:1,sprite:1},
{name:'Читачка',line:'Ще один рядок… Уже йду!',asks:2,sprite:2}
];
function random(seed){return function(){seed|=0;seed=seed+0x6D2B79F5|0;let t=Math.imul(seed^seed>>>15,1|seed);t=t+Math.imul(t^t>>>7,61|t)^t;return ((t^t>>>14)>>>0)/4294967296;};}
function crowd(seed){const rng=random(seed);return Array.from({length:8},(_,i)=>({at:10+i*11+rng()*3,type:Math.floor(rng()*TYPES.length),asks:0,cleared:false,shift:0}));}
class Game{
constructor(seed=1){this.seed=seed;this.people=crowd(seed);this.rightPeople=RIGHT_POSITIONS.map((at,i)=>({at,sprite:2+i%2}));this.rightYield=false;this.phase='ready';this.time=0;this.distance=0;this.pulse=78;this.lane=0;this.steps=0;this.walked=0;this.good=0;this.stops=0;this.excuses=0;this.pushes=0;this.penalty=0;this.lastPush=-100;this.lastTap=-100;this.lastAsk=-100;this.stunned=0;this.hallDone=false;this.hallLeft=0;this.idle=0;this.idleCounted=false;this.message='Тапай у ритмі кроку. Не поспішай.';this.messageUntil=4;this.stepFlash=0;this.blocked=false;this.combo=0;this.bestCombo=0;this.lastStep=0;this.feedback='ready';}
say(s){this.message=s;this.messageUntil=this.time+3;}
start(){if(this.phase==='ready')this.phase='playing';}
rightObstacle(){
const shifted=this.people.filter(p=>p.cleared&&p.shift<.25).map(p=>({at:this.walked+p.at-this.distance,sprite:TYPES[p.type].sprite,source:p}));
return [...this.rightPeople,...shifted].filter(p=>p.at>=this.walked-.05&&p.at-this.walked<4).sort((a,b)=>a.at-b.at)[0];
}
obstacle(){return this.lane?this.rightObstacle():this.people.find(p=>!p.cleared&&p.at>=this.distance-.05&&p.at-this.distance<4);}
obstacleGap(p){return p.at-(this.lane?this.walked:this.distance);}
excuseState(){
if(this.phase!=='playing'||this.lane)return 'none';const p=this.obstacle();if(!p)return 'none';
const type=TYPES[p.type];if(type.near&&p.at-this.distance>1.6)return 'approach';
if(this.time-this.lastAsk<1)return 'cooldown';
if(p.asks>=type.asks-1&&!this.gap())return 'wait';return 'ready';
}
push(){
if(this.phase!=='playing'||this.lane||this.stunned>0)return false;
const p=this.obstacle();if(!p){this.say('Поруч нікого немає. Шлях вільний.');return false;}
if(p.at-this.distance>1.8){this.say('Для поштовху треба підійти ближче.');return false;}
if(this.time-this.lastPush<1.2)return false;
this.lastPush=this.time;this.pushes++;this.penalty+=3;this.pulse=Math.min(168,this.pulse+12);this.combo=0;
p.cleared=true;p.shift=1;p.pushed=true;p.fall=1.4;this.feedback='push';this.blocked=false;
this.say('Обережніше! Штовхання: +3 с штрафу, +12 пульсу.');return true;
}
gap(){const p=this.rightObstacle();return !p||p.at-this.walked>3.8;}
move(lane){if(this.phase!=='playing')return false;if(lane===this.lane)return true;if(lane===1&&!this.gap()){this.say('Зачекай на вільне місце праворуч.');return false;}this.lane=lane;this.stepFlash=0;this.combo=0;this.feedback=lane?'rest':'ready';this.idle=0;this.idleCounted=false;this.say(lane?'Праворуч вільно: крокуй або перепочинь.':'Ліва смуга. Знайди свій ритм.');return true;}
tap(){
if(this.phase==='ready')this.start();if(this.phase!=='playing')return false;
if(this.stunned>0){this.feedback='tired';this.say('Віднови дихання або стань праворуч.');return false;}
const obstacle=this.obstacle();if(obstacle&&this.obstacleGap(obstacle)<1.1){this.feedback='blocked';this.say(this.lane?'Праворуч людина. Перейди ліворуч, щоб обійти.':'Попереду пасажир. Скажи «Перепрошую».');return false;}
const delta=this.time-this.lastTap;this.lastTap=this.time;
if(delta<.42){this.feedback='fast';this.combo=0;this.pulse=Math.min(168,this.pulse+12);this.say('Повільніше. Поручень урятував.');this.stunned=Math.max(this.stunned,.7);return false;}
const steady=delta>=.5-1e-6&&delta<=.95+1e-6;this.pulse+=steady?2:3.2;
if(this.pulse>=150){this.feedback='tired';this.combo=0;this.stunned=4;this.stops++;this.idleCounted=true;this.say('Ноги просять паузу. Праворуч — перепочити.');return false;}
let step=steady?.66:.38;const p=this.obstacle();if(p)step=Math.min(step,Math.max(0,this.obstacleGap(p)-.8));
this.distance=Math.min(TOTAL,this.distance+step);this.walked+=step;this.steps++;this.lastStep=step;this.feedback=steady?'good':'slow';this.combo=steady?this.combo+1:0;this.bestCombo=Math.max(this.bestCombo,this.combo);if(steady)this.good++;this.stepFlash=.5;this.idle=0;this.idleCounted=false;return true;
}
excuse(){if(this.phase!=='playing'||this.lane)return false;const p=this.obstacle();if(!p){this.say('Попереду вільно. Можна йти.');return false;}if(this.time-this.lastAsk<1)return false;this.lastAsk=this.time;
const type=TYPES[p.type];if(type.near&&p.at-this.distance>1.6){this.say('У навушниках не чують. Підійди ближче.');return false;}
this.excuses++;p.asks++;if(p.asks<type.asks){this.say('Ще раз, трохи ближче: «Перепрошую».');return false;}
if(!this.gap()){this.rightYield=true;this.say('Праворуч тісно. За мить буде проміжок.');return false;}
p.cleared=true;p.shift=1;this.say(type.line);return true;
}
tick(dt){if(!['playing','hall'].includes(this.phase))return;this.time+=dt;this.stepFlash=Math.max(0,this.stepFlash-dt);this.stunned=Math.max(0,this.stunned-dt);
for(const p of this.people){p.shift=Math.max(0,p.shift-dt*(p.pushed?1.5:.9));p.fall=Math.max(0,(p.fall||0)-dt);}
if(this.rightYield){const p=this.rightObstacle();if(p)(p.source||p).at+=dt*2.3;if(this.gap())this.rightYield=false;}
if(this.phase==='hall'){this.hallLeft-=dt;this.pulse=Math.max(78,this.pulse-dt*9);if(this.hallLeft<=0){this.phase='playing';this.say('Другий проліт. Нагорі вже світліше.');}return;}
this.pulse=Math.max(78,this.pulse-dt*(this.lane&&this.time-this.lastTap>1.1?7:this.stunned?5:1.55));
this.distance=Math.min(TOTAL,this.distance+BASE*dt);
// Standing passengers travel with the escalator; only walking closes the gap.
for(const p of this.people)p.at+=BASE*dt;
this.blocked=!!(this.obstacle()&&this.obstacleGap(this.obstacle())<1.15);
if(this.lane===0&&this.time-this.lastTap>2&&!this.blocked){this.idle+=dt;if(this.idle>3&&!this.idleCounted){this.stops++;this.idleCounted=true;this.say('Позаду чекають. Праворуч є місце для паузи.');}}
if(!this.hallDone&&this.distance>=TOTAL/2){this.hallDone=true;this.phase='hall';this.hallLeft=HALL;for(const p of this.people)if(p.at<this.distance+4)p.cleared=true;this.say('Проміжний хол. Переведи подих.');}
if(this.distance>=TOTAL){this.distance=TOTAL;this.phase='finished';}
}
result(){const saved=Math.max(0,Math.round(BASELINE-this.time-this.penalty));const title=this.pushes?'Поспіх має ціну':saved<15?'Праворуч, як годиться':this.stops===0&&this.good>50?'Арсенальна пішки':saved>40&&this.stops<=2?'Печерські ноги':'Ліва смуга';return {time:this.time,saved,pushes:this.pushes,penalty:this.penalty,stops:this.stops,excuses:this.excuses,steps:this.steps,title,bestCombo:this.bestCombo,accuracy:this.steps?Math.round(this.good/this.steps*100):0};}
}
const api={Game,TYPES,TOTAL,BASELINE,RIGHT_POSITIONS,crowd};if(typeof module!=='undefined')module.exports=api;root.Escalator=api;
})(globalThis);
/* motion.js */
(function(root){
'use strict';
const STEP_DURATION=.5;
function stepPose(remaining,steps,reduced=false){
const progress=Math.max(0,Math.min(1,1-remaining/STEP_DURATION));
const lift=remaining>0&&!reduced?Math.sin(Math.PI*progress):0;
const side=steps%2? -1:1;
return {progress,lift,side,bodyY:lift?-7*lift:0,bodyAngle:lift?side*.025*lift:0,leftLift:side<0?lift:0,rightLift:side>0?lift:0};
}
function sceneLayout(width,height,controlsHeight,topHeight=170){
// Canvas coordinates are stable; compensate horizontal scaling for human proportions.
const scaleX=width/600,scaleY=height/900;
const ground=900-controlsHeight/scaleY-65;
const visibleTop=topHeight/scaleY;
const playerHeight=Math.min(240,Math.max(150,(ground-visibleTop-75)*.82));
const halfWidth=48+(ground-176)/724*139;
const leftX=300-halfWidth*.46,rightX=300+halfWidth*.46;
return {ground,visibleTop,playerHeight,leftX,rightX,proportion:Math.min(1.6,Math.max(.7,scaleY/scaleX))};
}
function climbPose(g,layout,visualWalked=g.walked,cameraWalked=g.walked){
const flightDistance=g.hallDone?Math.max(0,g.distance-52.75):g.distance;
const room=Math.max(20,layout.ground-layout.visibleTop-layout.playerHeight*.92-15);
// Traverse almost the full visible flight; leave the character's head below the HUD.
const rise=Math.min(room,flightDistance/52.75*room*.94);
const strideAdvance=Math.max(0,visualWalked-cameraWalked)*18;
return {rise,strideAdvance,ground:layout.ground-rise-strideAdvance};
}
function passengerY(gap,ground){return ground-gap*25;}
function fallPose(remaining){
const elapsed=Math.max(0,1.4-remaining);
const smooth=x=>{const t=Math.max(0,Math.min(1,x));return t*t*(3-2*t);};
const down=smooth(elapsed/.27),up=smooth((elapsed-.85)/.55);
return {angle:1.28*down*(1-up),impact:elapsed>.2&&elapsed<.8,progress:elapsed/1.4};
}
function stairY(worldMetres,visibleMetres,ground){return ground+(visibleMetres-worldMetres)*25;}
function sceneryCamera(distance,hallDone=false){
const flight=Math.max(0,Math.min(52.75,hallDone?distance-52.75:distance));
const progress=flight/52.75;
return {progress,zoom:1+progress*.46};
}
const api={STEP_DURATION,stepPose,sceneLayout,climbPose,passengerY,fallPose,stairY,sceneryCamera};root.EscalatorMotion=api;if(typeof module!=='undefined')module.exports=api;
})(globalThis);
/* render.js */
(function(root){
'use strict';
// Sprite meshes keep each character independent of the concept sheet.
const assetBase=new URL('https://my-kiev.com/wp-content/uploads/landings/eskalator-arsenalnoyi/badfcd53534e977f/');
const silhouettes=[
'M 124 117 Q 151 75 195 86 Q 244 92 261 135 L 278 189 263 210 246 224 269 242 296 283 304 350 302 417 292 480 300 559 316 641 276 653 264 724 248 837 237 860 241 898 Q 260 918 237 930 L 204 938 192 926 194 890 193 856 195 807 181 744 177 840 169 869 162 894 170 913 157 935 126 935 119 920 128 879 128 840 117 734 114 657 68 649 79 542 86 445 65 431 64 385 77 307 92 264 123 239 128 218 110 200 106 167 Z',
'M 512 78 Q 530 38 553 39 L 590 45 617 80 Q 638 83 645 105 L 648 134 625 155 613 162 605 179 613 191 646 203 681 227 693 253 715 256 723 236 744 222 754 190 764 192 744 244 731 282 707 308 685 325 681 392 687 465 671 485 652 490 649 563 646 661 632 752 625 849 613 865 610 887 631 908 630 927 608 936 573 936 561 922 565 886 565 854 558 782 548 679 538 623 531 712 524 792 514 855 513 881 511 900 539 914 536 930 494 934 465 924 465 905 477 880 480 855 478 776 470 688 462 602 469 512 473 485 449 477 445 422 421 392 413 365 416 304 443 249 477 224 525 201 528 182 517 165 495 148 479 132 478 113 494 97 Z',
'M 878 134 Q 902 88 943 88 Q 989 84 1018 138 L 1040 202 1021 228 1003 239 1031 260 1050 290 1069 353 1075 392 1061 417 1084 470 1074 485 1059 487 1067 555 1076 637 1069 685 1025 702 1016 777 1006 854 1009 883 1028 913 1020 934 984 937 971 926 974 886 974 855 969 759 954 742 943 829 937 876 947 910 939 932 903 935 887 917 898 886 900 854 892 763 893 719 826 702 831 616 848 478 840 440 820 401 820 362 837 301 867 263 893 242 875 227 857 207 860 176 Z',
'M 1293 66 Q 1333 24 1375 45 Q 1418 58 1420 98 L 1405 137 1392 154 1385 171 1420 192 1466 216 1490 265 1499 326 1491 379 1500 402 1498 439 1485 457 1492 515 1483 561 1453 579 1434 613 1414 622 1410 708 1400 798 1390 859 1391 889 1422 912 1414 932 1374 936 1350 928 1350 901 1354 875 1353 827 1341 745 1331 673 1325 755 1309 845 1305 880 1308 903 1325 920 1314 935 1276 938 1253 930 1237 915 1245 894 1265 874 1265 836 1264 757 1257 672 1253 615 1209 604 1214 538 1225 467 1215 451 1181 439 1172 416 1180 363 1200 288 1223 237 1260 211 1301 179 1304 156 1288 137 1275 100 Z'
];
class Renderer{
constructor(canvas){
this.canvas=canvas;this.ctx=canvas.getContext('2d');
this.assetError=false;this.loadAssets();
this.reduced=matchMedia('(prefers-reduced-motion: reduce)');this.laneX=0;this.visualDistance=0;this.visualWalked=0;this.cameraWalked=0;this.lastGame=null;
this.dust=Array.from({length:18},(_,i)=>({x:(i*137.51)%600,y:(i*97.33)%900,r:.6+(i%3)*.4}));
}
loadAssets(){this.assetError=false;this.sprites=[];this.bg=new Image();this.sheet=new Image();this.bg.onerror=this.sheet.onerror=()=>{this.assetError=true;};this.sheet.onload=()=>this.prepare();this.bg.src=new URL('assets/station.png',assetBase).href;this.sheet.src=new URL('assets/passengers.png',assetBase).href;}
get ready(){return !this.assetError&&this.sprites.length===4&&this.bg.complete&&this.bg.naturalWidth>0;}
prepare(){for(let i=0;i<4;i++){const c=document.createElement('canvas');c.width=384;c.height=1024;const x=c.getContext('2d');x.translate(-384*i,0);x.clip(new Path2D(silhouettes[i]));x.drawImage(this.sheet,0,0,1536,1024);this.sprites.push(c);}}
player(p,g,layout){
const c=this.ctx,sprite=this.sprites[0];if(!sprite)return;
const pose=EscalatorMotion.stepPose(g.stepFlash,g.steps,this.reduced.matches);
const unit=p.h/1024;
c.save();c.translate(p.x,p.y);c.scale(layout.proportion,1);
// A footprint stays on the tread while the alternate shoe lifts.
if(g.stepFlash>0){c.save();c.globalAlpha=this.reduced.matches?.8:(1-pose.progress)*.9;c.translate(pose.side*14,-12);c.rotate(pose.side*.15);c.fillStyle='#ffe4a2';c.beginPath();c.ellipse(0,0,9,4,0,0,Math.PI*2);c.fill();c.restore();}
c.fillStyle='#0005';c.beginPath();c.ellipse(0,-p.h*.075,36,6,0,0,Math.PI*2);c.fill();
c.translate(pose.side*pose.lift*2,pose.bodyY);c.rotate(pose.bodyAngle);
c.scale(unit,unit);c.translate(-192,-1024);
c.shadowColor='#fff3ce';c.shadowBlur=9;
// Legs pivot beneath the coat. Each successful tap starts one complete half-stride.
const leg=(side,lift)=>{
const sx=side<0?100:185,sw=side<0?94:96,pivot=side<0?151:223;
c.save();c.translate(pivot,650);c.rotate(side*lift*.1);c.scale(1,1-lift*.15);
c.drawImage(sprite,sx,646,sw,302,sx-pivot,-4-lift*32,sw,302);c.restore();
};
leg(-1,pose.leftLift);leg(1,pose.rightLift);
c.drawImage(sprite,0,0,384,658,0,0,384,658);c.restore();
if(g.stepFlash>0){
c.save();c.textAlign='center';c.font='bold 17px Arial';c.fillStyle=g.feedback==='good'?'#fff1c6':'#f5dcc5';c.shadowColor='#123638';c.shadowBlur=5;
const rise=this.reduced.matches?0:pose.progress*20;
c.fillText('+'+g.lastStep.toFixed(2).replace('.',',')+' м',p.x,p.y-p.h-12-rise);c.restore();
}
}
person(p,layout,t){
const c=this.ctx,w=p.h*.375;const bob=this.reduced.matches?0:Math.sin(t*2+p.sprite)*.5;
c.save();c.globalAlpha=1;c.translate(p.x,p.y+bob);c.scale(layout.proportion,1);
const fall=p.fall>0?EscalatorMotion.fallPose(p.fall):null;
c.fillStyle='#0006';c.beginPath();c.ellipse(fall?26:0,-p.h*.07,fall?w*.7:w*.35,fall?8:4,0,0,Math.PI*2);c.fill();
if(fall?.impact){c.strokeStyle='#ffe0a9';c.lineWidth=3;c.beginPath();c.arc(25,-11,18+fall.progress*18,.25,2.5);c.stroke();c.fillStyle='#fff0cc';c.font='bold 22px Arial';c.fillText('!',w*.8,-p.h*.42);}
if(this.sprites[p.sprite]){const top=[80,38,80,36][p.sprite];c.rotate(fall?.angle||0);c.filter='brightness(1.12) saturate(1.08)';c.drawImage(this.sprites[p.sprite],0,top,384,945-top,-w/2,-p.h*.92,w,p.h*.842);}c.restore();
}
scenery(g,ground,camera){
const c=this.ctx;
const first=Math.floor((this.visualDistance-(920-ground)/25)/7)-1;
for(let i=first;i<first+13;i++){
const y=EscalatorMotion.stairY(i*7,this.visualDistance,ground);
if(y<70||y>1040)continue;
const depth=Math.max(0,Math.min(1,(y-176)/724));
const rail=300-(48*camera.zoom+depth*139),scale=.46+depth*.62;
const motif=((i%4)+4)%4;
for(const side of [-1,1]){
const x=side<0?rail*.48:600-rail*.48;
c.save();c.translate(x,y);c.scale(scale,scale);
c.shadowColor='#1b171049';c.shadowBlur=10;
if(motif===0){
c.fillStyle='#7c6348';c.beginPath();c.roundRect(-45,-125,90,125,6);c.fill();
c.fillStyle='#173c43';c.beginPath();c.roundRect(-40,-120,80,115,3);c.fill();
c.strokeStyle='#d8b579';c.lineWidth=2;c.strokeRect(-35,-115,70,105);
c.fillStyle='#e7c895';c.textAlign='center';c.font='bold 22px Georgia';c.fillText(g.hallDone?'↗':'М',0,-76);
c.font='bold 9px Arial';c.fillText(g.hallDone?'ВИХІД':'АРСЕНАЛЬНА',0,-48);
c.strokeStyle='#c8a979';c.lineWidth=2;c.beginPath();c.moveTo(-22,-30);c.lineTo(22,-30);c.moveTo(-13,-20);c.lineTo(13,-20);c.stroke();
}else if(motif===1){
c.fillStyle='#86613e';c.fillRect(-6,-92,12,92);c.fillRect(-29,-94,58,8);
c.shadowColor='#ffdc8a';c.shadowBlur=30;c.fillStyle=g.hallDone?'#e5f3e8':'#ffe4a2';
c.beginPath();c.arc(0,-108,25,0,Math.PI*2);c.fill();
c.shadowBlur=0;c.strokeStyle='#cba56a';c.lineWidth=5;c.beginPath();c.arc(0,-108,26,0,Math.PI*2);c.stroke();
}else if(motif===2){
c.fillStyle='#ccb99d';c.beginPath();c.roundRect(-48,-145,96,145,34);c.fill();
c.fillStyle=g.hallDone?'#294a54':'#183d42';c.beginPath();c.roundRect(-40,-136,80,127,30);c.fill();
c.strokeStyle='#d7b87b';c.lineWidth=4;c.stroke();
c.strokeStyle='#e9d6a4';c.lineWidth=2;c.beginPath();c.moveTo(-30,-60);c.lineTo(30,-60);c.moveTo(-25,-40);c.lineTo(25,-40);c.stroke();
c.fillStyle='#efdbac';c.textAlign='center';c.font='24px Georgia';c.fillText(g.hallDone?'✧':'◈',0,-83);
}else{
c.fillStyle='#d6c3a1';c.fillRect(-48,-91,96,91);c.fillStyle='#24444a';c.fillRect(-43,-86,86,77);
c.fillStyle='#edd59b';c.textAlign='center';c.font='bold 20px Arial';c.fillText(g.hallDone?'↑':'↟',0,-54);
c.font='bold 9px Arial';c.fillText(g.hallDone?'ДО МІСТА':'ДО ХОЛУ',0,-30);
}
c.restore();
}
}
}
draw(g,clock){
const c=this.ctx,width=this.canvas.clientWidth,height=this.canvas.clientHeight;if(!width||!height)return;
const dpr=Math.min(devicePixelRatio||1,2);
if(this.canvas.width!==Math.round(width*dpr)||this.canvas.height!==Math.round(height*dpr)){this.canvas.width=Math.round(width*dpr);this.canvas.height=Math.round(height*dpr);}
c.setTransform(this.canvas.width/600,0,0,this.canvas.height/900,0,0);c.clearRect(0,0,600,900);
if(this.lastGame!==g){this.lastGame=g;this.visualDistance=g.distance;this.visualWalked=g.walked;this.cameraWalked=g.walked;this.laneX=g.lane;}
this.visualDistance+=(g.distance-this.visualDistance)*.3;this.visualWalked+=(g.walked-this.visualWalked)*.45;this.cameraWalked+=(this.visualWalked-this.cameraWalked)*.035;
const camera=EscalatorMotion.sceneryCamera(this.visualDistance,g.hallDone);
if(this.bg.complete&&this.bg.naturalWidth){const z=camera.zoom;c.drawImage(this.bg,300-300*z,176-176*z,600*z,900*z);}else{c.fillStyle='#c8b493';c.fillRect(0,0,600,900);}
if(g.hallDone){c.fillStyle='rgba(77,128,140,.08)';c.fillRect(0,0,600,900);}
const t=this.reduced.matches?0:clock;
const parent=this.canvas.parentElement;
const layout=EscalatorMotion.sceneLayout(width,height,parent.querySelector('.controls')?.offsetHeight||160,(parent.querySelector('.top-ui')?.offsetHeight||160)+18);
const climb=EscalatorMotion.climbPose(g,layout,this.visualWalked,this.reduced.matches?this.visualWalked:this.cameraWalked);
const people=[],py=climb.ground,bodyHeight=layout.playerHeight;
this.scenery(g,py,camera);
// Each tread belongs to a fixed point on the escalator. Walking advances past it.
const railOffset=48*(camera.zoom-1);
c.save();c.beginPath();c.moveTo(251-railOffset,176);c.lineTo(347+railOffset,176);c.lineTo(485+railOffset,900);c.lineTo(111-railOffset,900);c.closePath();c.clip();
const grad=c.createLinearGradient(0,176,0,900);grad.addColorStop(0,'#262a28');grad.addColorStop(1,'#343b39');c.fillStyle=grad;c.fillRect(90,176,420,724);
for(let x=95;x<500;x+=3){c.strokeStyle=x%2?'#ffffff09':'#00000040';c.lineWidth=1;c.beginPath();c.moveTo(300+(x-300)*.25,176);c.lineTo(x,900);c.stroke();}
const first=Math.floor((this.visualDistance-(900-py)/25)*2)-1;
for(let i=first;i<first+66;i++){
const y=EscalatorMotion.stairY(i/2,this.visualDistance,py);if(y<175||y>902)continue;
const depth=Math.max(0,Math.min(1,(y-176)/724)),half=48*camera.zoom+depth*139;
c.fillStyle='#080c0c99';c.fillRect(300-half,y,half*2,Math.max(2,depth*7));
c.fillStyle='#a18049';c.fillRect(300-half,y,half*2,Math.max(1,depth*2));
c.fillStyle='#e0b76c55';c.fillRect(300-half,y+2,half*2,1);
}
c.restore();
const addPassenger=(sprite,x,y,extra={})=>{
// Cull only once the entire person has physically left the canvas. Never fade to a shadow.
if(y< -30||y-bodyHeight>930)return;
people.push({sprite,x,y,h:bodyHeight,...extra});
};
for(const p of g.people){
const gap=p.at-g.distance,shift=p.cleared?1-p.shift:0;
const y=EscalatorMotion.passengerY(gap,py);
const fallPull=p.fall?Math.sin(Math.PI*EscalatorMotion.fallPose(p.fall).progress)*.45:0;
addPassenger(Escalator.TYPES[p.type].sprite,layout.leftX+Math.max(0,shift-fallPull)*(layout.rightX-layout.leftX),y,{fall:p.fall||0});
}
// The same physical passengers also block walking on the right.
for(const p of g.rightPeople){
const gap=p.at-g.walked,y=EscalatorMotion.passengerY(gap,py);
addPassenger(p.sprite,layout.rightX,y);
}
this.laneX+=this.reduced.matches?g.lane-this.laneX:(g.lane-this.laneX)*.14;
const player={sprite:0,x:layout.leftX+this.laneX*(layout.rightX-layout.leftX),y:py,h:bodyHeight,player:true};
people.push(player);
if(g.stops>0&&g.lane===0&&g.idle>3)people.push({sprite:3,x:layout.leftX-4,y:py+180,h:bodyHeight});
if(g.gap()&&g.phase==='playing'){
c.save();c.strokeStyle='#fff3d3bb';c.lineWidth=2;c.setLineDash([7,5]);c.beginPath();c.roundRect((g.lane?layout.leftX:layout.rightX)-33,py-27,66,25,6);c.stroke();c.restore();
}
c.save();c.font='15px Arial';c.textAlign='center';
for(let level=0;level<=110;level+=10){
const y=EscalatorMotion.stairY(level,this.visualDistance,py);if(y<180||y>890)continue;
const half=48*camera.zoom+(y-176)/724*139;c.fillStyle='#f1d79cb0';c.fillRect(300-half,y,half*2,2);
c.fillStyle='#14393cea';c.fillRect(279,y-20,42,18);c.fillStyle='#ffe4ab';c.fillText(String(Math.max(0,Math.floor(105.5-level)))+' м',300,y-6);
}c.restore();
people.sort((a,b)=>a.y-b.y).forEach(p=>p.player?this.player(p,g,layout):this.person(p,layout,t));
if(!this.reduced.matches)for(const p of this.dust){const y=(p.y-t*6+1800)%900,x=p.x+Math.sin(t*.3+p.y)*8;c.fillStyle='rgba(255,237,193,.3)';c.beginPath();c.arc(x,y,p.r,0,Math.PI*2);c.fill();}
const light=c.createRadialGradient(300,160,0,300,160,340);light.addColorStop(0,`rgba(255,230,168,${.05+g.distance/1055})`);light.addColorStop(1,'rgba(255,230,168,0)');c.fillStyle=light;c.fillRect(0,0,600,650);
this.lastLayout={...layout,climb,camera,player,actorHeights:people.map(p=>p.h),people:people.length,ready:this.sprites.length===4&&this.bg.complete&&this.bg.naturalWidth>0};
}
}
root.EscalatorRenderer=Renderer;
})(globalThis);
/* gate.js */
(function(root){
'use strict';
function parseAlert(data,now=Date.now()){
if(!data||data.stale!==false||!Number.isFinite(data.at)||now-data.at>180000||data.at-now>60000)return null;
if(data.alert===null)return false;
if(data.alert&&Array.isArray(data.alert.activeAlerts))return data.alert.activeAlerts.length>0;
return null;
}
// Consent lasts only in memory and only until a confirmed all-clear.
class AlertGate{
constructor(){this.active=false;this.continued=false;this.noticeOpen=false;}
get blocked(){return this.active&&this.noticeOpen;}
receive(state){
if(state!==true&&state!==false)return false;
const newAlert=state&&!this.active;
this.active=state;
if(newAlert){this.continued=false;this.noticeOpen=true;}
if(!state){this.continued=false;this.noticeOpen=false;}
return newAlert;
}
continuePlaying(){if(!this.active)return false;this.continued=true;this.noticeOpen=false;return true;}
showNotice(){if(this.active)this.noticeOpen=true;}
}
if(typeof module!=='undefined')module.exports={parseAlert,AlertGate};root.parseEscalatorAlert=parseAlert;root.EscalatorAlertGate=AlertGate;
})(globalThis);
/* input.js */
(function(root){
'use strict';
function editable(target){return !!target&&(target.isContentEditable||['INPUT','TEXTAREA','SELECT'].includes(target.tagName));}
function keyAction(event,playable){
if(event.altKey||event.ctrlKey||event.metaKey||editable(event.target))return null;
const key=(event.key||'').toLowerCase(),code=event.code;
if(code==='KeyP'||key==='p'||key==='з'||key==='escape')return 'pause';
if(!playable)return null;
if(code==='Space'||key===' '||key==='arrowup')return 'step';
if(code==='KeyE'||key==='e'||key==='у')return 'excuse';
if(code==='KeyF'||key==='f'||key==='а')return 'push';
if(key==='arrowright')return 'right';if(key==='arrowleft')return 'left';return null;
}
const api={keyAction,editable};root.EscalatorInput=api;if(typeof module!=='undefined')module.exports=api;
})(globalThis);
/* game.js */
(function(){
'use strict';
const $=id=>document.getElementById('ea-'+id), clock=t=>`${Math.floor(t/60)}:${String(Math.floor(t%60)).padStart(2,'0')}`;
const store=GameCore.createStore('arsenalna-v1',()=>({best:0,streak:0,lastDay:'',days:{}}),x=>({best:Number(x.best)||0,streak:Number(x.streak)||0,lastDay:typeof x.lastDay==='string'?x.lastDay:'',days:x.days&&typeof x.days==='object'?x.days:{}}));
let helpOpen=false,helpReturn=null,visualTime=0,lastUi=0;
let mode='daily',day=GameCore.rng.kyivDay(),g,paused=false,sound=false,audioCtx=null,finished=false,previous=0,accumulator=0,lastStatus='',lastBeat=-1,training=0;
const alertGate=new EscalatorAlertGate();let alertStatus='pending';
const renderer=new EscalatorRenderer($('scene'));
new ResizeObserver(entries=>{for(const entry of entries)$('scene').parentElement.style.setProperty('--control-height',entry.target.offsetHeight+'px');}).observe(document.querySelector('#eaRoot .controls'));
function emit(name,detail={}){window.dispatchEvent(new CustomEvent('arsenalna:'+name,{detail:{day,mode,...detail}}));}
function recordLabel(){const saved=store.load();$('personal-record').textContent=saved.best?`Найкраще: −${saved.best} с · Серія: ${saved.streak} дн.`:'Перший підйом ще попереду';if(!store.available())$('personal-record').textContent+=' · лише в цій вкладці';}
function reset(){day=GameCore.rng.kyivDay();g=new Escalator.Game(GameCore.rng.dailySeed(day+(mode==='practice'?':'+(++training):'')));finished=false;paused=false;accumulator=0;lastStatus='';visualTime=0;lastBeat=-1;helpOpen=false;$('help-overlay').hidden=true;$('pause').setAttribute('aria-label','Пауза');$('result-overlay').hidden=true;$('pause-overlay').hidden=true;$('hall-overlay').hidden=true;$('share-status').textContent='';$('daily').disabled=false;$('practice').disabled=false;$('day-label').textContent=new Intl.DateTimeFormat('uk-UA',{day:'numeric',month:'long',timeZone:'Europe/Kyiv'}).format(new Date());recordLabel();update();}
function tone(freq=200,duration=.06){if(!sound)return;try{audioCtx=audioCtx||new (window.AudioContext||window.webkitAudioContext)();audioCtx.resume();const oscillator=audioCtx.createOscillator(),gain=audioCtx.createGain();oscillator.type='sine';oscillator.frequency.setValueAtTime(freq,audioCtx.currentTime);gain.gain.setValueAtTime(.035,audioCtx.currentTime);gain.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+duration);oscillator.connect(gain);gain.connect(audioCtx.destination);oscillator.start();oscillator.stop(audioCtx.currentTime+duration);}catch(e){sound=false;$('sound').setAttribute('aria-pressed','false');}}
function canAct(){return renderer.ready&&!paused&&!helpOpen&&!alertGate.blocked&&!['hall','finished'].includes(g.phase);}
function step(){if(!canAct())return;if(g.phase==='ready'){emit('run_start');emit('first_tap');}if(g.tap()){tone(180+g.steps%2*35);$('step').classList.add('stepping');setTimeout(()=>$('step').classList.remove('stepping'),180);}update();}
function lane(to){if(!canAct())return;if(g.phase==='ready'){g.start();emit('run_start');}g.move(to);update();}
function excuse(){if(!canAct())return;if(g.excuse())tone(460,.12);update();}
function push(){if(!canAct())return;if(g.push())tone(110,.16);update();}
function pause(){if(alertGate.blocked||g.phase==='ready'||g.phase==='finished')return;paused=!paused;$('pause-overlay').hidden=!paused;$('pause').setAttribute('aria-label',paused?'Продовжити':'Пауза');if(paused)$('resume').focus();else $('step').focus({preventScroll:true});}
$('step').addEventListener('pointerdown',e=>{e.preventDefault();step();});$('step').addEventListener('click',e=>{if(e.detail===0)step();});
$('rest').onclick=()=>lane(g.lane?0:1);$('excuse').onclick=excuse;$('push').onclick=push;$('pause').onclick=pause;$('resume').onclick=pause;
$('restart').onclick=()=>{reset();$('step').focus({preventScroll:true});};$('again').onclick=()=>{emit('run_retry');reset();$('step').focus({preventScroll:true});};
for(const name of ['daily','practice'])$(name).onclick=()=>{mode=name;for(const id of ['daily','practice']){$(id).classList.toggle('selected',id===mode);$(id).setAttribute('aria-pressed',id===mode);}reset();};
$('sound').onclick=()=>{sound=!sound;$('sound').setAttribute('aria-pressed',sound);$('sound').setAttribute('aria-label',sound?'Вимкнути звук':'Увімкнути звук');$('sound').querySelector('span').hidden=sound;if(sound)tone(350,.15);};
function openHelp(){if(alertGate.blocked)return;helpReturn=document.activeElement;helpOpen=true;$('help-overlay').hidden=false;$('help-close').focus({preventScroll:true});update();}
function closeHelp(){helpOpen=false;$('help-overlay').hidden=true;if(alertGate.blocked)$('check-alert').focus({preventScroll:true});else if(helpReturn?.isConnected)helpReturn.focus({preventScroll:true});update();}
$('asset-retry').onclick=()=>renderer.loadAssets();
$('help').onclick=openHelp;$('help-side').onclick=openHelp;$('help-close').onclick=closeHelp;$('help-done').onclick=closeHelp;
// Modal keyboard focus stays inside the visible foreground dialog.
document.addEventListener('keydown',e=>{
if(e.key!=='Tab')return;
const dialog=['alert-overlay','help-overlay','pause-overlay','result-overlay','hall-overlay'].map($).find(el=>!el.hidden);
if(!dialog)return;
const items=[...dialog.querySelectorAll('button:not(:disabled),a[href],[tabindex="0"]')];
if(!items.length){e.preventDefault();return;}
const first=items[0],last=items[items.length-1];
if(e.shiftKey&&(document.activeElement===first||!dialog.contains(document.activeElement))){e.preventDefault();last.focus();}
else if(!e.shiftKey&&(document.activeElement===last||!dialog.contains(document.activeElement))){e.preventDefault();first.focus();}
});
document.addEventListener('keydown',e=>{
if(alertGate.blocked)return;
if(helpOpen){if(e.key==='Escape'){e.preventDefault();closeHelp();}return;}
const action=EscalatorInput.keyAction(e,canAct());if(!action)return;
e.preventDefault();if(e.repeat)return;
if(action==='step')step();else if(action==='excuse')excuse();else if(action==='push')push();
else if(action==='right')lane(1);else if(action==='left')lane(0);else if(action==='pause')pause();
},true);
document.addEventListener('keyup',e=>{if(canAct()&&!EscalatorInput.editable(e.target)&&(e.code==='Space'||e.key===' '))e.preventDefault();},true);
let touch=null;$('scene').addEventListener('pointerdown',e=>{touch={x:e.clientX,y:e.clientY};$('scene').setPointerCapture(e.pointerId);});$('scene').addEventListener('pointerup',e=>{if(!touch)return;const dx=e.clientX-touch.x,dy=e.clientY-touch.y;if(Math.abs(dx)>35&&Math.abs(dx)>Math.abs(dy))lane(dx>0?1:0);else if(Math.abs(dy)<20)step();touch=null;});$('scene').addEventListener('pointercancel',()=>touch=null);
document.addEventListener('visibilitychange',()=>{if(document.hidden&&!paused&&['playing','hall'].includes(g.phase))pause();if(!document.hidden)checkAlert();});
function finish(){if(finished)return;finished=true;const r=g.result();if(mode==='daily'){const s=store.load();s.best=Math.max(s.best,r.saved);const yesterday=new Date(day+'T12:00:00Z');yesterday.setUTCDate(yesterday.getUTCDate()-1);if(s.lastDay!==day){s.streak=s.lastDay===yesterday.toISOString().slice(0,10)?s.streak+1:1;s.lastDay=day;}if(!s.days[day]||r.saved>s.days[day].saved)s.days[day]=r;const keys=Object.keys(s.days).sort();while(keys.length>60)delete s.days[keys.shift()];store.save(s);}
$('result-title').textContent=r.title;$('result-time').textContent=clock(r.time);$('result-saved').textContent=r.saved;$('result-stops').textContent=r.stops;$('result-excuses').textContent=r.excuses;$('result-pushes').textContent=r.pushes+' / +'+r.penalty+' с';$('result-accuracy').textContent=r.accuracy+' %';$('result-overlay').hidden=false;$('again').focus();recordLabel();emit('run_finish',r);tone(523,.3);}
function stepWord(n){const m=n%100;return m>=11&&m<=14?'кроків':n%10===1?'крок':n%10>=2&&n%10<=4?'кроки':'кроків';}
function text(id,value){const el=$(id),v=String(value);if(el.textContent!==v)el.textContent=v;}
function update(){
$('alert-overlay').hidden=!alertGate.blocked;$('alert-reminder').hidden=!alertGate.active||alertGate.blocked;
let alertText=alertGate.active?(alertGate.continued?'У Києві триває тривога. Ти обрав продовжити гру.':'У Києві повітряна тривога. Гру призупинено.'):(alertStatus===false?'Статус перевірено: у Києві немає тривоги.':alertStatus==='pending'?'Перевіряємо статус тривоги…':'Статус тривоги не вдалося перевірити. Стеж за офіційними сповіщеннями.');
if(alertGate.active&&alertStatus===null)alertText+=' Статус не вдалося оновити.';
text('feed-status',alertText);
$('asset-overlay').hidden=renderer.ready||alertGate.blocked;
text('asset-title',renderer.assetError?'Не вдалося завантажити графіку':'Запалюємо світло…');text('asset-note',renderer.assetError?'Перевір з’єднання та повтори завантаження. Підйом не почався.':'Завантажуємо станцію та пасажирів.');$('asset-retry').hidden=!renderer.assetError;
const p=Math.round(g.distance/Escalator.TOTAL*100),elapsed=g.time-g.lastTap;
text('depth',g.distance>=Escalator.TOTAL?'0':'−'+Math.floor(Escalator.TOTAL-g.distance));text('pulse',Math.round(g.pulse));text('time',clock(g.time));
$('progress-fill').style.width=p+'%';document.querySelector('#eaRoot .progress').setAttribute('aria-valuenow',p);
text('flight',g.hallDone?'ДРУГИЙ ПРОЛІТ':'ПЕРШИЙ ПРОЛІТ');text('lane-label',g.lane?'ПРАВА СМУГА':'ЛІВА СМУГА');text('rest-text',g.lane?'Іти ліворуч':'Перепочити');text('rest-arrow',g.lane?'←':'→');
$('start-hint').hidden=g.phase!=='ready';$('hall-overlay').hidden=g.phase!=='hall'||paused||helpOpen||alertGate.blocked;text('hall-count',Math.max(1,Math.ceil(g.hallLeft)));
$('scene').parentElement.classList.toggle('overheated',g.pulse>130);
const status=g.time<g.messageUntil?g.message:g.stunned?'Віднови дихання.':g.lane?(g.blocked?'Праворуч людина. Перейди ліворуч.':'Праворуч можна крокувати або перепочити.'):g.blocked?'Попереду пасажир. «Перепрошую»?':g.pulse>=130?'Пульс росте. Праворуч — коротка пауза.':'Наступний тап — у світлій зоні.';
if(status!==lastStatus){text('status',status);lastStatus=status;}
const npc=g.obstacle();$('bubble').hidden=!npc||g.lane===1||g.phase!=='playing'||helpOpen;if(npc)text('bubble',Escalator.TYPES[npc.type].name);
const askState=g.excuseState(),askReady=askState==='ready';
$('excuse').classList.toggle('action-needed',askReady);$('excuse').dataset.state=askState;
text('excuse-hint',askReady?'НАТИСНИ E АБО ТУТ':askState==='cooldown'?'Ще мить…':askState==='wait'?'Чекаємо проміжок':askState==='approach'?'Підійди ближче':'Коли хтось попереду');
$('push').classList.toggle('push-available',!!npc&&!g.lane&&npc.at-g.distance<=1.8);
text('walk-stats',g.steps+' '+stepWord(g.steps)+' · '+g.walked.toFixed(1).replace('.',',')+' м пішки');
text('rhythm-title',g.pulse>135?'Перепочинь':g.lane?(g.blocked?'Шлях зайнятий':'Крокуй або відпочинь'):g.combo>=2?'Серія ×'+g.combo:'Рівний крок');
text('rhythm-hint',g.lane?(g.blocked?'Перейди ліворуч':'Тапай або відпочинь'):g.stunned?'Дай ногам паузу':g.steps?'Тапай у світлій зоні':'Почни з кнопки');
$('beat').style.left=(g.steps?Math.min(1,Math.max(0,elapsed/1.2))*100:0)+'%';
let feedback='';
if(g.phase==='ready')feedback='Ти — у помаранчевому. Один тап — один крок.';
else if(g.lane)feedback=g.blocked?'Праворуч людина · обійди ліворуч':elapsed<1.1&&g.steps&&['good','slow'].includes(g.feedback)?'Крокуєш праворуч · сходинки позаду':'Праворуч вільно · крокуй або перепочинь';
else if(g.stunned)feedback='Занадто швидко · дай ногам перепочити';
else if(askReady)feedback='↓ Натисни «Перепрошую» або E';
else if(g.feedback==='push'&&g.time-g.lastPush<3)feedback='Штовхання: +3 с штрафу · +12 пульсу';
else if(g.blocked)feedback=askState==='wait'?'Праворуч тісно · зачекай на проміжок':'Пасажир попереду · ще мить';
else if(elapsed<1.1&&g.steps&&['good','slow'].includes(g.feedback))feedback=g.feedback==='good'?'Влучний крок · серія ×'+g.combo:'Крок зараховано · спробуй рівніший темп';
else if(['fast','tired'].includes(g.feedback))feedback='Спробуй спокійніше · один тап на 0,6–0,7 с';
else if(g.phase==='playing')feedback='Твій наступний крок ↑';
text('step-feedback',feedback);$('step-feedback').dataset.kind=g.feedback;
for(const id of ['step','excuse','rest','push'])$(id).disabled=!renderer.ready||paused||helpOpen||alertGate.blocked||['hall','finished'].includes(g.phase);
$('daily').disabled=['playing','hall'].includes(g.phase);$('practice').disabled=$('daily').disabled;
if(g.phase==='finished')finish();
}
function frame(now){
const dt=Math.min(.1,(now-(previous||now))/1000);previous=now;
if(renderer.ready&&!paused&&!helpOpen&&!alertGate.blocked){
if(g.phase!=='finished')visualTime+=dt;
accumulator+=dt;while(accumulator>=1/60){const before=g.phase;g.tick(1/60);if(before!=='hall'&&g.phase==='hall')emit('hall_reach');accumulator-=1/60;}
if(g.phase==='playing'&&!g.lane&&g.steps!==lastBeat&&g.time-g.lastTap>=.62){lastBeat=g.steps;tone(620,.025);}
}
if(now-lastUi>50){update();lastUi=now;}renderer.draw(g,visualTime);requestAnimationFrame(frame);
}
function resultText(){const r=g.result();return `Ескалатор Арсенальної — ${r.title}! Мій підйом: ${clock(r.time)}. Зекономлено ${r.saved} с. Зупинок зліва: ${r.stops}. Штовхань: ${r.pushes}, штраф: ${r.penalty} с. ${mode==='daily'?'Маршрут дня '+day:'Тренування'}. Ігровий час умовний.`;}
$('share').onclick=async()=>{emit('share_intent');try{if(navigator.share){await navigator.share({title:'Ескалатор Арсенальної',text:resultText()});$('share-status').textContent='Результат передано.';}else if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(resultText());$('share-status').textContent='Результат скопійовано.';}else{$('share-status').textContent=resultText();}}catch(e){if(e.name!=='AbortError')$('share-status').textContent='Не вдалося поділитися. Збережи картку поруч.';}};
$('download').onclick=()=>{const c=document.createElement('canvas');c.width=1080;c.height=1350;const x=c.getContext('2d'),r=g.result();x.fillStyle='#f5eedf';x.fillRect(0,0,1080,1350);if(renderer.bg.complete)x.drawImage(renderer.bg,0,0,1080,1350);x.fillStyle='#f9efdbef';x.beginPath();x.roundRect(100,130,880,1090,35);x.fill();x.textAlign='center';x.fillStyle='#183f46';x.font='24px Arial';x.fillText('ЕСКАЛАТОР АРСЕНАЛЬНОЇ',540,225);x.font='100px Georgia';x.fillText('Нагорі!',540,365);x.font='38px Georgia';x.fillText(r.title,540,445);x.font='170px Georgia';x.fillText(clock(r.time),540,650);x.font='44px Georgia';x.fillText(`Зекономлено ${r.saved} с`,540,750);x.font='38px Georgia';x.fillText(`Зупинок зліва: ${r.stops}`,540,870);x.fillText(`Ввічливих прохань: ${r.excuses}`,540,940);x.font='30px Georgia';x.fillText(`Штовхань: ${r.pushes} · штраф +${r.penalty} с`,540,1010);x.font='26px Arial';x.fillText(mode==='daily'?`Маршрут дня · ${day}`:'Тренування',540,1080);x.font='22px Arial';x.fillText('Мій Київ · Ігровий час умовний',540,1150);c.toBlob(blob=>{if(!blob){$('share-status').textContent='Не вдалося створити картку.';return;}const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='arsenalna-'+day+'.png';a.click();setTimeout(()=>URL.revokeObjectURL(url),10000);emit('share_export');$('share-status').textContent='Картку збережено.';});};
let checking=false;
async function checkAlert(){if(checking)return;checking=true;const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),3500);let state=null;try{const response=await fetch('https://my-kiev.com/alerts/api/alerts/31',{signal:controller.signal,credentials:'omit',cache:'no-store'});if(response.ok)state=parseEscalatorAlert(await response.json());}catch(e){}finally{clearTimeout(timer);checking=false;}
const wasBlocked=alertGate.blocked;alertStatus=state;
const newAlert=alertGate.receive(state);if(newAlert)emit('alert_gate_shown');
if(state===false&&wasBlocked){
if(['playing','hall'].includes(g.phase)&&!paused)pause();else restoreGameFocus();
}
update();
if(newAlert)$('alert-shelter').focus({preventScroll:true});
}
function restoreGameFocus(){
const target=helpOpen?'help-close':paused?'resume':g.phase==='finished'?'again':renderer.ready?'step':'asset-title';
if(target==='asset-title')$(target).setAttribute('tabindex','-1');
$(target).focus({preventScroll:true});
}
$('continue-alert').onclick=()=>{if(alertGate.continuePlaying()){emit('alert_continue');update();restoreGameFocus();}};
$('alert-reminder').onclick=()=>{alertGate.showNotice();update();$('alert-shelter').focus({preventScroll:true});};
$('check-alert').onclick=checkAlert;reset();checkAlert();setInterval(checkAlert,60000);requestAnimationFrame(frame);
})();