import {installLanguages,localize} from '../i18n.mjs';
installLanguages();
import {startState,advance,replay,won,available} from './core.mjs';
import {restore,remember,fresh} from './progress.mjs';
import {scene,position,miniature,prop,sprite,PALETTES} from './scene.mjs';
import {MODE_NAMES} from './maps.mjs';
import {lessonPages,situation,firstTarget} from './coach.mjs';
const $=id=>document.getElementById(id),qa=new URLSearchParams(location.search).has('qa');
const storageKey='3xquest-yard-routes-v1'+(qa?'-qa':'');
const lights=['day','dusk','night'],lightNames={day:'День',dusk:'Сумерки',night:'Ночь'},lightIcons={day:'☀',dusk:'◐',night:'☾'};
const color={A:'#ce7262',B:'#d2a243',C:'#668fae'};
const moveText=n=>`${n} ${n%100>=11&&n%100<=14?'ходов':n%10===1?'ход':n%10>=2&&n%10<=4?'хода':'ходов'}`;
let catalog,missions,progress=fresh(),mission,state,light='day',busy=false,animation=null,warning='';
const coachKey=storageKey+'-luiza-v1';
let coachSeen=new Set(),coachPages=[],coachPage=0,coachTarget=null;
try{const seen=JSON.parse(localStorage.getItem(coachKey));if(Array.isArray(seen))coachSeen=new Set(seen.filter(v=>typeof v==='string'));}catch{}
function rememberCoach(){coachSeen.add(mission.mode);try{localStorage.setItem(coachKey,JSON.stringify([...coachSeen]));}catch{}}
if(qa)$('classic').href='../?qa=1';
function save(){try{localStorage.setItem(storageKey,JSON.stringify(progress));}catch{warning='Сохранение недоступно в браузере. Оставь вкладку открытой.';}}
function store(){if(mission&&state)remember(progress,mission,state,light);save();}
function textFor(m,long=false){
let text={collect:'Забери все 3 вагона и привези состав к зелёному флагу.',ordered:`Забирай вагоны строго ${m.order.join(' → ')}. Затем приезжай к зелёному флагу.`,delivery:'В составе 2 места. Забирай вагоны и доставляй каждый на площадку ↓ с его буквой. Затем — к зелёному флагу.',gate:'Забери ключ K: он открывает мост K. Собери все вагоны и приезжай к зелёному флагу.',bridge:'Мосты 1× исчезают после одного проезда. Собери все вагоны и приезжай к зелёному флагу.'}[m.mode];
if(m.oneWay?.length)text+=' По стрелкам можно ехать только в одну сторону.';
if(m.limit)text+=` Уложись в ${m.limit} ходов.`;
if(long&&m.mode==='delivery')text+=' На площадке груз разгружается автоматически; буквы станций рядом с вагонами показывают место отправки.';
return text;
}
function gallery(){
$('total').textContent=`Выполнено ${Object.keys(progress.best).length} из 100 заказов`;
$('continue').textContent=progress.current?'Продолжить путешествие →':'Начать путешествие →';
$('maps').innerHTML=catalog.maps.map((map,i)=>{
const m=missions.find(m=>m.mapId===map.id),done=missions.filter(m=>m.mapId===map.id&&progress.best[m.id]).length;
return ``;
}).join('');localize();
}
function render(message=''){
$('game').dataset.light=light;$('game').dataset.mission=mission.id;$('game').dataset.position=state.position;
$('scene').innerHTML=scene(mission,state);
$('chapter').textContent=`ЗАКАЗ ${String(mission.number).padStart(3,'0')} / 100`;
$('map-name').textContent=mission.name;$('objective').textContent=MODE_NAMES[mission.mode];
$('lighting').textContent=lightIcons[light];$('lighting').setAttribute('aria-label',`Время суток: ${lightNames[light]}. Сменить фильтр`);
const ids=mission.mode==='ordered'?mission.order:mission.jobs.map(j=>j.id);
$('cargo-order').innerHTML=ids.map(id=>{const done=(mission.mode==='delivery'?state.delivered:state.picked).includes(id),job=mission.jobs.find(j=>j.id===id);return `${done?'✓ ':''}${id}${mission.mode==='delivery'?' → '+job.to.toUpperCase():''}`;}).join(mission.mode==='ordered'?'→':'');
$('instruction').textContent={collect:'Все вагоны → зелёный флаг',ordered:'Сначала собери в указанном порядке → флаг',delivery:'Груз → площадка ↓ с его буквой → флаг',gate:'Ключ K → мост K → все вагоны → флаг',bridge:'Мост 1× — только один проезд. Все вагоны → флаг'}[mission.mode];
if(coachTarget){
const stop=$('scene').querySelector(`[data-node="${coachTarget}"]`);
if(stop){stop.classList.add('coach-target');stop.setAttribute('aria-label',stop.getAttribute('aria-label')+'. Первый ход Луизы');}
$('instruction').textContent=`Луиза: начни со станции ${coachTarget.toUpperCase()} — я выделила её золотым.`;
}
$('moves').textContent=mission.limit?`${state.path.length} / ${moveText(mission.limit)}`:moveText(state.path.length);
$('capacity').textContent=`В составе: ${state.cargo.join(' · ')||'пусто'} (${state.cargo.length}/${mission.capacity})`;
$('formation').innerHTML=``;
$('undo').disabled=!state.path.length||busy;$('restart').disabled=busy;
$('mission').value=mission.id;$('previous').disabled=mission.number===1||busy;$('forward').disabled=mission.number===100||busy;
for(const id of ['mission','back','lighting','help'])$(id).disabled=busy;
const complete=won(mission,state);$('completion').hidden=!complete;
$('score').textContent=`${moveText(state.path.length)} · кратчайший путь: ${mission.par}. ${state.path.length===mission.par?'Идеально!':'Можно попробовать короче.'}`;
$('next').textContent=mission.number===100?'Вернуться к картам':'Следующее задание →';
$('live').textContent=warning||message||(!complete&&!available(mission,state).length?'Путь закончился. Отмени ход: мост или запас ходов уже использован.':'');
localize();
}
function coachRender(){
const page=coachPages[coachPage];
$('brief-label').textContent=`${mission.name} · ${MODE_NAMES[mission.mode]}`;
$('brief-title').textContent=page.title;$('brief-body').textContent=page.text;
$('brief-control').textContent=page.focus??'На поле можно пробовать разные пути. ↶ бесплатно отменяет ход.';
$('coach-page').textContent=`${coachPage+1} / ${coachPages.length}`;
$('coach-previous').hidden=coachPage===0;
$('coach-next').textContent=coachPagem.id===id)??missions[0];coachTarget=null;
const session=progress.sessions[mission.id];state=session?replay(mission,session.path):startState(mission);
light=session?.light??lights[Math.floor(Math.random()*lights.length)];
$('atlas').hidden=true;$('game').hidden=false;document.title=`${mission.name} — Pocket Shunting Yard`;
store();render();if(explain&&!state.path.length&&!coachSeen.has(mission.mode))brief({automatic:true});localize();
}
function back(){if(busy)return;store();$('game').hidden=true;$('atlas').hidden=false;gallery();document.title='Pocket Shunting Yard — мир маршрутов';localize();}
async function travel(node){
if(busy)return;
let next;try{next=advance(mission,state,node);}catch(error){render(error.message);return;}
const before=position(mission.nodes.find(n=>n.id===state.position)),after=position(mission.nodes.find(n=>n.id===node));
busy=true;render();$('game').classList.add('moving');
// Persist the accepted move immediately, so leaving mid-animation cannot lose it.
state=next;coachTarget=null;store();
try{
if(!matchMedia('(prefers-reduced-motion: reduce)').matches){
animation=$('train').animate([{transform:`translate(${before.x}px,${before.y}px)`},{transform:`translate(${after.x}px,${after.y}px)`}],{duration:Math.min(600,230+Math.hypot(before.x-after.x,before.y-after.y)*.5),easing:'ease-in-out',fill:'forwards'});
await animation.finished;
}
}catch{/* A cancelled visual never cancels the already validated move. */}
finally{animation=null;busy=false;$('game').classList.remove('moving');render();}
}
$('scene').addEventListener('click',e=>{const node=e.target.closest('[data-node]');if(node)void travel(node.dataset.node);});
$('scene').addEventListener('keydown',e=>{if(['Enter',' '].includes(e.key)){const node=e.target.closest('[data-node]');if(node){e.preventDefault();void travel(node.dataset.node);}}});
$('undo').onclick=()=>{if(!busy&&state.path.length){state=replay(mission,state.path.slice(0,-1));store();render();}};
function restart(){if(busy)return;coachTarget=null;state=startState(mission);store();render();}
$('restart').onclick=restart;$('again').onclick=restart;
$('back').onclick=back;$('help').onclick=brief;
$('coach-next').onclick=()=>{if(coachPage{if(coachPage){coachPage--;coachRender();if(!coachPage)$('coach-next').focus();}};
$('coach-skip').onclick=()=>{$('brief').close();};
$('brief').addEventListener('close',rememberCoach);
$('lighting').onclick=()=>{if(!busy){light=lights[(lights.indexOf(light)+1)%lights.length];store();render(`Время суток: ${lightNames[light]}`);}};
$('continue').onclick=()=>load(progress.current??missions[0].id);
$('mission').onchange=e=>load(e.target.value,{explain:false});
$('previous').onclick=()=>{if(mission.number>1)load(missions[mission.number-2].id,{explain:false});};
$('forward').onclick=()=>{if(mission.number<100)load(missions[mission.number].id,{explain:false});};
$('next').onclick=()=>{if(mission.number===100)back();else load(missions[mission.number].id);};
$('maps').onclick=e=>{
const card=e.target.closest('[data-map]');if(!card)return;const map=catalog.maps.find(m=>m.id===card.dataset.map),items=missions.filter(m=>m.mapId===map.id);
$('selection-title').textContent=map.name;$('selection-subtitle').textContent=map.subtitle;
$('variants').innerHTML=items.map(m=>``).join('');
$('map-dialog').showModal();localize();
};
$('variants').onclick=e=>{const button=e.target.closest('[data-mission]');if(button){$('map-dialog').close();load(button.dataset.mission);}};
document.addEventListener('visibilitychange',()=>{if(document.hidden){animation?.finish();store();}});
window.addEventListener('pagehide',()=>{animation?.finish();store();});
window.addEventListener('native-back',event=>{
const dialog=document.querySelector('dialog[open]');
if(dialog){event.preventDefault();dialog.close();return;}
if(!$('game').hidden){event.preventDefault();back();}
});
try{
const response=await fetch('./campaign.json');if(!response.ok)throw Error('Не удалось загрузить задания.');
catalog=await response.json();missions=catalog.missions;
let raw;try{raw=JSON.parse(localStorage.getItem(storageKey));}catch{}progress=restore(raw,missions);
$('mission').innerHTML=missions.map(m=>``).join('');
$('skip').hidden=!['localhost','127.0.0.1','[::1]'].includes(location.hostname);
gallery();
}catch(error){$('fatal').hidden=false;$('fatal').textContent=error.message;localize();}