(function(){ var root=document.querySelector('[data-tma-codex]'); if(!root) return; if(root.dataset.init==='1') return; root.dataset.init='1'; /* ===== PERSISTENCE ===== */ var storeOK=true; try{localStorage.setItem('_probe','1');localStorage.removeItem('_probe');}catch(e){storeOK=false;} var KEY_CATALOG='TMA_SEMKO_CATALOG_V1'; var KEY_GRID='TMA_SEMKO_GRID_V1'; var defaults=[ {key:'lettuce',name:'Салат латук',secs:150,img:null}, {key:'romaine',name:'Салат ромэн',secs:160,img:null}, {key:'basil',name:'Базилик генуэзский',secs:150,img:null}, {key:'mint',name:'Мята',secs:180,img:null}, {key:'cilantro',name:'Кинза',secs:162,img:null}, {key:'arugula',name:'Руккола',secs:140,img:null} ]; function loadCatalog(){ if(!storeOK) return defaults.slice(); try{ var raw=localStorage.getItem(KEY_CATALOG); if(!raw) return defaults.slice(); var custom=JSON.parse(raw)||[]; var names=new Set(custom.map(c=>c.name)); return custom.concat(defaults.filter(d=>!names.has(d.name))); }catch(e){return defaults.slice();} } function saveCatalog(list){ if(!storeOK) return; try{ var custom=list.filter(c=>c.key && c.key.startsWith && c.key.startsWith('semko_') || c.img); localStorage.setItem(KEY_CATALOG, JSON.stringify(custom)); }catch(e){} } var CELLS=12; function blankGrid(){return Array.from({length:CELLS},()=>({crop:null,name:null,secs:150,img:null,plantedAt:null,boostW:0,boostL:0,boostC:0,water:0}))} function loadGrid(){ if(!storeOK) return blankGrid(); try{ var raw=localStorage.getItem(KEY_GRID); if(!raw) return blankGrid(); var g=JSON.parse(raw); if(!Array.isArray(g)||g.length!==CELLS) return blankGrid(); return g; }catch(e){return blankGrid();} } function saveGrid(g){ if(!storeOK) return; try{localStorage.setItem(KEY_GRID, JSON.stringify(g));}catch(e){} } var catalog=loadCatalog(); var gridState=loadGrid(); var selected=null; /* ===== REFS ===== */ var elCat=root.querySelector('[data-catalog]'); var elGrid=root.querySelector('[data-grid]'); var elLog=root.querySelector('[data-log]'); var elETA=root.querySelector('[data-eta]'); var sPlanted=root.querySelector('[data-s-planted]'); var sReady=root.querySelector('[data-s-ready]'); var sWater=root.querySelector('[data-s-water]'); /* ===== Helpers ===== */ function ringBG(p){var x=Math.max(0,Math.min(100,Math.floor(p)));return 'conic-gradient(var(--ok) '+x+'%, #eef2f7 0)';} function log(text){var time=new Date().toLocaleTimeString();var d=document.createElement('div');d.className='line';d.textContent='['+time+'] '+text; if(elLog.firstChild) elLog.insertBefore(d, elLog.firstChild); else elLog.appendChild(d);} function progress(c){ if(!c || !c.crop || !c.plantedAt) return 0; var e=(Date.now()-c.plantedAt)/1000; var m=1 + (c.boostW>Date.now()? .25:0) + (c.boostL>Date.now()? .25:0) + (c.boostC>Date.now()? .10:0); return Math.min(1,(e*m)/((c.secs||150))); } function pct(i){ var c = (typeof i === 'number') ? gridState[i] : i; return progress(c)*100; } function stage(i){var p=pct(i); if(p>=100) return 'Готово'; if(p>=50) return 'Растёт'; if(p>0) return 'Всходы'; return 'Пусто';} function boosts(c){ if(!c || !c.crop) return '—'; var n=Date.now(); return (c.boostW>n?'Вода+':'Вода—')+' | '+(c.boostL>n?'Свет+':'Свет—')+' | '+(c.boostC>n?'Уборка+':'Уборка—');} // helper to safely create CSS url from user input function cssUrl(u){ try{ return 'url("'+encodeURI(String(u))+'")'; }catch(e){ return 'none'; } } function updateStats(){ var planted=gridState.filter(c=>!!c.crop).length; var ready=gridState.filter((c,i)=>c.crop && pct(i)>=100).length; var water=gridState.reduce((a,c)=>a+(c.water||0),0); sPlanted.textContent=planted; sReady.textContent=ready; sWater.textContent=water; var best=null; var now = Date.now(); gridState.forEach(function(c){ if(!c || !c.crop) return; if(!c.plantedAt) return; var elapsed=(now - c.plantedAt)/1000; var m = 1 + (c.boostW>now? .25:0) + (c.boostL>now? .25:0) + (c.boostC>now? .10:0); if(progress(c)>=1) return; // already ready var remaining = Math.max(0, ((c.secs||150) - elapsed) / (m||1)); if(best === null || remaining < best) best = remaining; }); elETA.textContent = (best !== null) ? formatETA(best) : '—'; var badge = root.querySelector('[data-badge]'); if(badge){ badge.textContent = ready>0 ? 'урожай готов' : ('следующий урожай: ' + (best !== null ? formatETA(best) : '—')); } } function formatETA(sec){var s=Math.max(0,Math.round(sec));var m=Math.floor(s/60),r=s%60;return (m>0?(m+' мин '):'')+r+' сек';} /* ===== Catalog UI ===== */ function buildCatalog(list){ elCat.innerHTML=''; list.forEach(function(item){ var card=document.createElement('button'); card.type='button'; card.className='seed'; if(item.img){ card.style.setProperty('--bg', cssUrl(item.img)); } var body=document.createElement('div'); body.className='seed-body'; var nameEl = document.createElement('div'); nameEl.className='seed-name'; nameEl.textContent = item.name || 'Без названия'; var metaEl = document.createElement('div'); metaEl.className='seed-meta'; metaEl.textContent = Math.round((item.secs||150)/60) + ' мин'; body.appendChild(nameEl); body.appendChild(metaEl); card.appendChild(body); card.addEventListener('click', function(){ if(selected===null){ log('Выберите ячейку для посадки'); return; } if(selected < 0 || selected >= CELLS){ log('Неверная ячейка'); return; } var cell=gridState[selected]; if(cell.crop && pct(selected)<100){ log('Ячейка занята'); return; } gridState[selected] = { crop: item.key, name: item.name, secs: item.secs, img: item.img, plantedAt: Date.now(), boostW: 0, boostL: 0, boostC: 0, water: 0 }; saveGrid(gridState); log('Посадка: '+item.name+' → яч. '+(selected+1)); buildGrid(); }); elCat.appendChild(card); }); } // Add via Semko URL root.querySelector('[data-add-open]').addEventListener('click', function(){ var form=root.querySelector('[data-add-form]'); form.style.display=(form.style.display==='none'||form.style.display==='')?'flex':'none'; }); root.querySelector('[data-add-save]').addEventListener('click', function(){ var name=(root.querySelector('[data-add-name]').value||'').trim(); var img=(root.querySelector('[data-add-img]').value||'').trim(); var secs=parseInt(root.querySelector('[data-add-secs]').value,10)||150; if(!name||!img){ alert('Укажи название и URL обложки с semco.ru'); return; } // basic URL validation try{ new URL(img); }catch(e){ alert('Неверный URL обложки'); return; } var key='semko_'+Date.now(); catalog.unshift({key,name,secs,img}); saveCatalog(catalog); buildCatalog(catalog); root.querySelector('[data-add-name]').value=''; root.querySelector('[data-add-img]').value=''; root.querySelector('[data-add-secs]').value='150'; root.querySelector('[data-add-form]').style.display='none'; log('Добавлена культура из Семко: '+name+' (сохранено)'); }); root.querySelector('[data-search]').addEventListener('input', function(){ var q=this.value.toLowerCase().trim(); if(!q){ buildCatalog(catalog); return; } var f=catalog.filter(c=> (c.name||'').toLowerCase().includes(q) ); buildCatalog(f); }); /* ===== Grid UI ===== */ function buildGrid(){ elGrid.innerHTML=''; for(var i=0;i'; ring.appendChild(rin); var meta=document.createElement('div'); meta.className='kpis'; var k1=document.createElement('span'); k1.className='kpi'; k1.textContent = c.crop ? ('Рост: ' + Math.floor(pct(idx)) + '%') : 'Свободно'; var k2=document.createElement('span'); k2.className='kpi'; k2.textContent = boosts(c); meta.appendChild(k1); meta.appendChild(k2); mid.appendChild(ring); mid.appendChild(meta); wrap.appendChild(top); wrap.appendChild(mid); wrap.addEventListener('click', function(){ selected=idx; buildGrid(); }); elGrid.appendChild(wrap); })(i); } updateStats(); } /* ===== Actions (save after each) ===== */ function actWater(){ if(selected===null){ log('Выберите ячейку'); return; } var c=gridState[selected]; if(!c || !c.crop){ log('Ячейка пуста'); return; } c.boostW=Date.now()+60*1000; c.water=(c.water||0)+1; saveGrid(gridState); log('Полив яч. '+(selected+1)); buildGrid(); } function actLight(){ if(selected===null){ log('Выберите ячейку'); return; } var c=gridState[selected]; if(!c || !c.crop){ log('Ячейка пуста'); return; } c.boostL=Date.now()+60*1000; saveGrid(gridState); log('Свет яч. '+(selected+1)); buildGrid(); } function actClean(){ if(selected===null){ log('Выберите ячейку'); return; } var c=gridState[selected]; if(!c || !c.crop){ log('Ячейка пуста'); return; } c.boostC=Date.now()+30*1000; saveGrid(gridState); log('Уборка яч. '+(selected+1)); buildGrid(); } function harvestOne(){ if(selected===null){ log('Выберите ячейку'); return; } var c=gridState[selected]; if(!(c && c.crop && pct(selected)>=100)) {log('Ещё не готово'); return;} log('Сбор: '+c.name+' (яч. '+(selected+1)+')'); gridState[selected]={crop:null,name:null,secs:150,img:null,plantedAt:null,boostW:0,boostL:0,boostC:0,water:0}; saveGrid(gridState); buildGrid(); } function harvestAll(){ var cnt=0; gridState.forEach(function(c,i){ if(c && c.crop && pct(i)>=100){ cnt++; gridState[i]={crop:null,name:null,secs:150,img:null,plantedAt:null,boostW:0,boostL:0,boostC:0,water:0}; } }); if(cnt) saveGrid(gridState); log(cnt?('Собрано '+cnt+' шт.'): 'Готового нет'); buildGrid(); } root.querySelector('[data-act="water"]').addEventListener('click', actWater); root.querySelector('[data-act="light"]').addEventListener('click', actLight); root.querySelector('[data-act="clean"]').addEventListener('click', actClean); root.querySelector('[data-act="harvest-one"]').addEventListener('click', harvestOne); root.querySelector('[data-act="harvest-all"]').addEventListener('click', harvestAll); /* ===== Delivery / Gift ===== */ root.querySelector('[data-deliver]').addEventListener('click', function(){ var filled=gridState.filter(c=>c.crop).length; if(!filled){ log('Грядка пуста'); return; } log('Заявка на доставку: предметов '+filled); // fetch('/api/deliver', {...}) — подключается по необходимости }); var giftForm=root.querySelector('[data-gift-form]'); root.querySelector('[data-gift-open]').addEventListener('click', function(){ giftForm.style.display=(giftForm.style.display==='none'||giftForm.style.display==='')?'flex':'none'; }); root.querySelector('[data-gift-send]').addEventListener('click', function(){ var to=(root.querySelector('[data-gift-to]').value||'').trim(); var msg=(root.querySelector('[data-gift-msg]').value||'').trim(); if(!to){ alert('Укажи @username или телефон'); return; } var count=gridState.filter(c=>c.crop).length; if(!count){ log('Нечего дарить — грядка пуста'); return; } // Для логирования безопасности: не вставляем msg в innerHTML нигде — только textContent через log log('Подарок отправлен: '+to + (msg?(' • "'+msg+'"'):'') + ' • предметов: '+count); // fetch('/api/gift', {...}) }); /* ===== Live tick ===== */ setInterval(function(){ // быстрый апдейт прогресса (без полного рендера) var cells=elGrid.querySelectorAll('.cell'); cells.forEach(function(el){ var idxAttr = el.getAttribute('data-idx'); var idx = parseInt(idxAttr,10); if(isNaN(idx)) return; var ring=el.querySelector('.ring'); if(ring) ring.style.background=ringBG(pct(idx)); var st=el.querySelector('.cell-top span:last-child'); if(st) st.textContent = (gridState[idx] && gridState[idx].crop) ? stage(idx) : '+'; var meta=el.querySelector('.kpis'); if(meta){ // обновляем только текстовые KPI var k1 = meta.querySelector('.kpi:first-child'); var k2 = meta.querySelector('.kpi:last-child'); if(k1) k1.textContent = (gridState[idx] && gridState[idx].crop) ? ('Рост: ' + Math.floor(pct(idx)) + '%') : 'Свободно'; if(k2) k2.textContent = boosts(gridState[idx]); } }); updateStats(); }, 1000); /* ===== Init ===== */ buildCatalog(catalog); buildGrid(); window.addEventListener('beforeunload', function(){ saveGrid(gridState); saveCatalog(catalog); }); })();
Вертикальная грядка: упаковка
распредели урожай по ярусам и оформи доставку/подарок
инвентарь: 0 шт.

Инвентарь урожая

перетащи карточку на слот грядки (drag-and-drop)

Вертикальная грядка (6 ярусов × 4 слота)

Заполнено: 0/24 Вес: 0 г Оценка: 0 ₽

Камера упаковки (демо)

замени URL на свой HLS/WebRTC/YouTube Live

Доставка или подарок

Журнал операций