import {ITEMS,RECIPES,ZONES,QUESTS,SPECIES,BAG_TYPES} from './content.mjs';
import {loadArt,createScene} from './scene.mjs';
import {setAudioEnabled,playSound} from './audio.mjs';
import {TOOLSET,SPAWN,normalizePlayer} from './world.mjs';
let state=null,art=null,scene=null,panelName=null,panelArg=null,busy=false,toastTimer,audioCtx=null,sound=false,soundTimer;
const $=s=>document.querySelector(s),esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const icon=(n,cls='')=>`
`;
const itemIcon=id=>icon(ITEMS[id]?.art??12);
function notify(message){$('#toast').textContent=message;$('#toast').classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>$('#toast').classList.remove('show'),4200);}
async function api(path,data){let r=await fetch('/api/'+path,{method:data?'POST':'GET',headers:data?{'Content-Type':'application/json'}:{},body:data?JSON.stringify(data):undefined});let b=await r.json();if(!r.ok){let e=Error(b.error||'เชื่อมต่อไม่ได้');e.status=r.status;throw e;}return b;}
const actionButton=(label,action,extra={},cls='')=>``;
const panelButton=(label,panel,arg='',cls='')=>``;
const price=id=>Math.max(1,Math.round(ITEMS[id].price*Math.max(.7,Math.min(1.3,1+(30-state.world.stock[id])/100+(state.player.zone==='market'?.15:0)))));
function ping(){if(!sound||!audioCtx)return;const o=audioCtx.createOscillator(),g=audioCtx.createGain();o.type='sine';o.frequency.setValueAtTime(660,audioCtx.currentTime);o.frequency.exponentialRampToValueAtTime(990,audioCtx.currentTime+.12);g.gain.setValueAtTime(.045,audioCtx.currentTime);g.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+.3);o.connect(g).connect(audioCtx.destination);o.start();o.stop(audioCtx.currentTime+.3);}
async function act(action,args={}){
if(action==='hint'){notify(args.text);return false;}
if(busy||!state)return false;busy=true;
try{const oldZone=state.player.zone;$('#save-status').textContent='กำลังบันทึก…';
const position=scene?.zone===oldZone?{x:scene.player.x,y:scene.player.y}:state.player.position;
const next=await api('action',{action,position,...args});state=next;render();
if(oldZone!==state.player.zone)scene?.reset();
if(action!=='position'){notify(next.message);ping();}
if(['travel','sleep'].includes(action))close();else if(panelName&&action!=='position')open(panelName,panelArg);
$('#save-status').textContent='บันทึกแล้ว';return true;
}catch(e){notify(e.message);$('#save-status').textContent='ยังไม่สำเร็จ';return false;}finally{busy=false;}
}
function render(){
if(!state)return;const p=normalizePlayer(state.player),w=state.world;
Object.assign(ITEMS,w.content?.item);Object.assign(RECIPES,w.content?.recipe);
ZONES.home={name:'บ้านอุ่นไอ',safe:true};
const z=ZONES[p.zone]||ZONES.farm,b=BAG_TYPES[p.bagLevel];
$('#player-name').textContent=p.name;$('#gold').textContent=p.gold;$('#location').textContent=z.name;
$('#safety').textContent=z.safe?'✿ เขตปลอดภัย':'⚑ เขตเสี่ยง · ระวังชีวิต';
$('#tool-label').textContent=TOOLSET[scene?.selected||0].name;$('#bag-count').textContent=`${p.bag.length}/${b.slots}`;$('#portrait').src=art.portrait;
$('#vitals').innerHTML=[['❧','แรงกาย',p.energy,100,'#96b670'],['♥','ชีวิต',p.hp,100,'#d58b79'],['✦','พลังเวท',p.mp,60,'#91a9c7']].map(([symbol,name,n,max,col])=>`
`).join('');
$('#belt').innerHTML=['potion','mana','scroll'].map(id=>actionButton(`${itemIcon(id)}${p.belt[id]||0}`,'use',{id})).join('');
document.querySelectorAll('#belt button').forEach((el,i)=>{el.title=ITEMS[['potion','mana','scroll'][i]].name;el.setAttribute('aria-label',el.title);});
$('#hotbar').innerHTML=TOOLSET.map((t,i)=>``).join('');
document.querySelectorAll('[data-art]').forEach(i=>i.src=art.icons[+i.dataset.art]);
const seasons=['ฤดูใบไม้ผลิ','ฤดูร้อน','ฤดูใบไม้ร่วง','ฤดูหนาว'];$('#season').textContent=`${seasons[Math.floor((w.number-1)/28)%4]} · วันที่ ${(w.number-1)%28+1}`;clock();
}
function clock(){if(!state?.world)return;const w=state.world,base=Number.isFinite(w.clock)?w.clock:360,elapsed=Math.max(0,Math.floor((Date.now()-(w.lastTick||Date.now()))/1200)),minute=(base+elapsed)%1440,hour=Math.floor(minute/60);$('#time').textContent=`${String(hour).padStart(2,'0')}:${String(minute%60).padStart(2,'0')} น.`;}
function close(){$('#modal').close();scene?.stop();panelName=null;panelArg=null;}
$('#close-modal').onclick=close;$('#modal').addEventListener('cancel',()=>{panelName=null;panelArg=null;});
function open(name,arg=''){playSound('menu');scene?.stop();$('#modal').classList.toggle('dialogue',name==='npc');panelName=name;panelArg=arg;const p=state?.player,w=state?.world;let title='',html='';const tile=(id,n,controls='')=>`${itemIcon(id)}
${ITEMS[id].name} ×${n}
${controls}`;
if(name==='welcome'){title='บ้านใหม่รอเธออยู่';html=`
เก็บผัก ทำซุป และค่อยๆ ทำความรู้จักหุบเขา
ที่นี่ไม่ต้องรีบ และไม่มีอาชีพที่ต้องเลือก
${panelButton('มีบัญชีแล้ว · กลับเข้าบ้าน','login','','wide')}ความคืบหน้าบันทึกในเครื่องเซิร์ฟเวอร์นี้อัตโนมัติ
`;}
else if(name==='login'||name==='account'){title=name==='login'?'กลับเข้าบ้าน':'เก็บกุญแจบ้านของเรา';html=`ผูกชื่อบัญชีเพื่อกลับมาเล่นเซฟเดิมจากเบราว์เซอร์อื่น
`;}
else if(name==='map'){title='แผนที่หุบเขา';html=`ดูเส้นทาง แล้วเดินไปตามป้ายในฉาก
${[['cave',5],['forest',1],['farm',0],['pond',8],['town',4],['market',4]].map(([id,i])=>`
${icon(i)}${ZONES[id].name}${p.zone===id?'เราอยู่ที่นี่':id==='market'&&w.bridge<30?'สะพานยังไม่เสร็จ':''}
`).join('')}
ถ้ำ ↕ ป่ากระซิบ ↔ สวนอุ่นไอ ↔ หมู่บ้าน ↔ เมืองลมทะเล
สวนอุ่นไอ ↓ บ่อประกายดาว
ประตูบ้านอยู่ทางเหนือของแปลงผัก
`;}
else if(name==='rest'){title='พักผ่อนที่บ้าน';html=`วางเครื่องมือ แล้วพักสักหน่อย
${actionButton('พักเติมพลัง','sleep',{},'primary wide')}พักเติมพลังได้วันละครั้งในเกม พืชเติบโตเมื่อเช้าวันใหม่ของหุบเขามาถึง
`;}
else if(name==='farm'){title='แปลงผักของเรา';html=`พรวนดิน → ปลูก → รดน้ำ → เติบโตเมื่อเช้าวันใหม่ → เก็บเกี่ยว
หนึ่งวันในหุบเขายาวประมาณ 29 นาทีจริง รดน้ำทุกวันเพื่อให้โตต่อ
${p.plots.map((plot,i)=>`
${icon(plot.crop?plot.crop==='carrot'?6:7:12)}
แปลงที่ ${i+1}
${plot.crop?`${(ITEMS[plot.crop]?.name||w.content?.crop?.[plot.crop]?.name)} · ${plot.age>=(w.content?.crop?.[plot.crop]?.days||2)?'พร้อมเก็บ':`วันที่ ${plot.age}/2`} ${plot.watered?'· รดน้ำแล้ว':''}`:plot.tilled?'ดินพร้อมปลูก':'ดินแข็ง · ใช้จอบ [1]'}${panelButton('ดูแลแปลงนี้','plot',i)}`).join('')}
`;}
else if(name==='plot'){const i=Number(arg),plot=p.plots[i];title=`แปลงผักที่ ${i+1}`;if(!plot)return;html=`${icon(plot.crop?plot.crop==='carrot'?6:7:12,'welcome-cat')}
`;if(!plot.crop)html+=plot.tilled?`ใช้เมล็ดจากกระเป๋าหรือกล่องที่บ้าน · แรงกาย 3
${actionButton('ปลูกแครอท','plant',{plot:i,crop:'carrot'},'primary wide')}${actionButton('ปลูกมะเขือเทศ','plant',{plot:i,crop:'tomato'},'wide')}${Object.entries(w.content?.crop||{}).filter(([,c])=>c.status==='active').map(([id,c])=>actionButton('ปลูก'+c.name,'plant',{plot:i,crop:id},'wide')).join('')}`:`ดินแข็งอยู่ · ใช้แรงกาย 2 เพื่อพรวน แล้วค่อยหยอดเมล็ด
${actionButton('พรวนดิน','till',{plot:i},'primary wide')}`;else html+=`${(ITEMS[plot.crop]?.name||w.content?.crop?.[plot.crop]?.name)} · ${plot.age>=(w.content?.crop?.[plot.crop]?.days||2)?'โตเต็มที่แล้ว':`โตวันที่ ${plot.age} จาก 2 วัน`}
${plot.age>=(w.content?.crop?.[plot.crop]?.days||2)?actionButton('เก็บเกี่ยวผลผลิต','harvest',{plot:i},'primary wide'):plot.watered?'รดน้ำแล้ว แวะกลับมาในวันถัดไปนะ
':actionButton('รดน้ำ · แรงกาย 2','water',{plot:i},'primary wide')}`;}
else if(name==='bag'){title='กระเป๋าเดินทาง';const b=BAG_TYPES[p.bagLevel];html=`${p.bag.length} / ${b.slots} ช่อง · ช่องละ ${b.stack} ชิ้น · ช่องสีทองปลอดภัย ${b.safe} ช่อง
${Array.from({length:b.slots},(_,i)=>{const s=p.bag[i];return `
ช่อง ${i+1}${i${s?`${itemIcon(s.id)}${ITEMS[s.id].name} ×${s.n}
${['soup','grilled','potion','mana','scroll','repair'].includes(s.id)?actionButton('ใช้ 1 ชิ้น','use',{id:s.id}):''}${['potion','mana','scroll'].includes(s.id)?actionButton('ใส่เข็มขัด','belt',{id:s.id}):''}${i>0?actionButton('ย้ายไปช่องแรก','moveSlot',{index:i}):''}`:'+
'} `;}).join('')}
เครื่องมือระดับ ${p.equipment.tool} · สภาพ ${p.equipment.durability}%
เข็มขัด: ยา ${p.belt.potion} · ชา ${p.belt.mana} · ม้วนกลับบ้าน ${p.belt.scroll}
${['farm','home'].includes(p.zone)?actionButton('เก็บของทั้งหมดเข้ากล่อง','deposit',{},'primary wide'):'กลับไปที่กล่องในสวนหรือในบ้านเพื่อเก็บของ
'}`;}
else if(name==='home'){title='บ้านอุ่นไอ';if(!['farm','home'].includes(p.zone)){html=panelButton('เดินทางกลับบ้านก่อน','map','','wide');}else html=`กล่องใบนี้เก็บของของเราไว้เสมอ คราฟต์ที่บ้านใช้วัตถุดิบจากกล่องได้เลย
${actionButton('เก็บของทั้งหมดเข้ากล่อง','deposit',{},'primary wide')}${Object.entries(p.storage).filter(([,n])=>n>0).map(([id,n])=>tile(id,n,actionButton('หยิบ 1 ชิ้น','withdraw',{id}))).join('')||'
กล่องยังว่างอยู่
'}
${panelButton('ครัวเล็กๆ','craft')}${panelButton('ตู้เสื้อผ้า','wardrobe')}${actionButton('พักเติมพลัง','sleep')}${actionButton('นั่งพัก · เวท +6','rest')}
พักเติมพลังได้วันละครั้งในเกม หุบเขาใช้เวลาเดียวกันสำหรับทุกคน และเช้าวันใหม่จะดูแลการเติบโตของพืช
`;}
else if(name==='craft'){title='ครัวเล็กๆ และงานฝีมือ';html=`${panelButton('สูตรที่รู้จัก','craft','','active')}${panelButton('สำนักวิชา','skills')}${panelButton('อัปเกรดอุปกรณ์','upgrades')}
ทำที่บ้านหรือในหมู่บ้าน · สูตรใหม่บางเล่มซ่อนอยู่ในถ้ำลึก
${Object.entries(RECIPES).filter(([,r])=>r.status!=='retired').map(([id,r])=>`${itemIcon(r.out)}
${r.name}
${Object.entries(r.needs).map(([k,n])=>ITEMS[k].name+' ×'+n).join(' · ')}
แรง ${r.energy}${w.discoveries[id]?' · ค้นพบโดย '+esc(w.discoveries[id]):''}${p.known.includes(id)?actionButton('ทำเลย','craft',{id}):actionButton('เรียน 20 เหรียญ','learnRecipe',{id})}
`).join('')}`;}
else if(name==='shop'){title=p.zone==='market'?'ตลาดลมทะเล':'ร้านของฟินน์';html=`${panelButton('ซื้อของ','shop','','active')}${panelButton('ขายของ','sell')}${panelButton('กระเป๋าและเครื่องมือ','upgrades')}
ของในร้านใช้ร่วมกันทั้งหุบเขา · เติมของตอนเริ่มวันใหม่
${['seed','tomatoSeed','flowerSeed','scroll','potion','mana','soup','mushroom','herb','repair',...Object.keys(w.content?.item||{})].filter(id=>ITEMS[id].status!=='retired').map(id=>tile(id,w.stock[id],`◉ ${price(id)} เหรียญ${actionButton('ซื้อ 1 ชิ้น','buy',{id})}`)).join('')}
`;}
else if(name==='sell'){title='แบ่งของให้ร้าน';html=`${panelButton('ซื้อของ','shop')}${panelButton('ขายของ','sell','','active')}
${p.bag.length?''+p.bag.map(s=>tile(s.id,s.n,actionButton('ขาย ◉ '+Math.floor(price(s.id)*.7),'sell',{id:s.id}))).join('')+'
':'ยังไม่มีของในกระเป๋า
'}`;}
else if(name==='upgrades'){title='ของคู่ใจนักเดินทาง';let b=BAG_TYPES[p.bagLevel+1];html=`พบช่างในหมู่บ้านเพื่ออัปเกรด ของสวมใส่ไม่หายเมื่อหมดแรง
${icon(16)}
กระเป๋าใบถัดไป
${b?`${b.slots} ช่อง · ช่องละ ${b.stack} · ปลอดภัย ${b.safe} ช่อง
ราคา ${b.cost} เหรียญ`:'อัปเกรดสูงสุดแล้ว'}${b?actionButton('อัปเกรด','upgradeBag'):''}
${icon(14)}
เครื่องมือระดับ ${p.equipment.tool+1}
แร่ทองแดง 3 · ${p.equipment.tool*90} เหรียญ
ระดับ 2 เปิดถ้ำชั้นลึก${p.equipment.tool<3?actionButton('ตีเครื่องมือ','upgradeTool'):''}
`;}
else if(name==='fishing'){title='รอเสียงน้ำกระเพื่อม';const f=p.fish;html=`${icon(15,'welcome-cat')}
หย่อนเบ็ด ใช้แรงกาย 4 หน่วย
รอประมาณ 2 วินาที แล้วดึงเบ็ดภายใน 15 วินาที
${f?`กำลังรอปลากินเหยื่อ…
${actionButton('ดึงเบ็ด!','reel',{},'primary wide')}${actionButton('เก็บเบ็ด / ลองใหม่','cancelFish',{},'wide')}`:actionButton('หย่อนเบ็ด','cast',{},'primary wide')}`;}
else if(name==='mine'){title='ใต้แสงของภูเขา';html=`${icon(3,'welcome-cat')}
ชั้นตื้นให้แร่ทองแดงและใช้แรงกาย 8 หน่วย
ชั้นลึกมีหินแสงจันทร์และตำรา แต่กับดักทำให้เสียชีวิต 10–18 หน่วย
${actionButton('ขุดแร่ชั้นตื้น','mine',{},'primary wide')}${actionButton('สำรวจชั้นลึก · เครื่องมือระดับ 2','mine',{deep:true},'wide')}เครื่องมือสึก 3% ต่อครั้ง ใช้หินลับจากร้านหรือเรียนสูตรจากฟินน์
`;}
else if(name==='hunt'){title='หมาป่าหมอก';html=`${icon(19,'welcome-cat')}
หมาป่าไม่โจมตีก่อน เธอเลือกบันทึกเรื่องราวของมัน หรือออกล่าเพื่อเก็บขนสัตว์ก็ได้
ออกล่าจะเสียชีวิต ${p.skills.includes('shield')?9:18} หน่วย และลดหมาป่าในระบบนิเวศ ถ้าชีวิตหมด ของในช่องปกติจะหาย
${actionButton('บันทึกลงสมุดธรรมชาติ','observe',{id:'wolf'},'wide')}${actionButton('เลือกออกล่า · ได้ขนสัตว์ 2','hunt',{},'wide')}${Object.entries(w.content?.creature||{}).filter(([,c])=>c.status==='active').map(([id,c])=>actionButton(esc(c.name)+' · เสียชีวิต '+c.attack,'hunt',{id},'wide')).join('')}`;}
else if(name==='quests'){title='เรื่องราวที่ค่อยๆ ผลิบาน';html=QUESTS.map(q=>`${icon(q.id==='cook'?22:q.id==='angler'?8:12)}
${q.name}
${q.text}
${Math.min(q.goal,p.stats[q.metric]||0)}/${q.goal} · รางวัล ${q.gold} เหรียญ${p.quests.includes(q.id)?'
✓ เสร็จแล้ว':(p.stats[q.metric]||0)>=q.goal?actionButton('รับรางวัล','quest',{id:q.id}):'
กำลังเดินทาง'}
`).join('');}
else if(name==='journal'){title='สมุดธรรมชาติของเรา';html=`บันทึกแล้ว ${p.journal.length} / ${Object.keys(SPECIES).length} ชนิด · สังเกตได้เมื่ออยู่ในพื้นที่ของสิ่งมีชีวิตนั้น
${Object.entries(SPECIES).map(([id,s])=>`
${icon(s.art)}
${s.name}
${s.text}${p.journal.includes(id)?'✓ บันทึกแล้ว':actionButton('สังเกตและบันทึก','observe',{id})}${w.journal[id]?`ผู้พบคนแรก: ${esc(w.journal[id])}`:''}`).join('')}
บันทึกของหุบเขา
ความอุดมสมบูรณ์${Math.round(w.ecology)}%
${Object.entries(w.pop).map(([k,n])=>`
${SPECIES[k].name}${Math.round(n)}
`).join('')}
ดอกไม้ → ผึ้ง → ผลผลิตเพิ่ม · หมาป่า → คุมกระต่าย → พืชปลอดภัย ทุกชนิดมีจำนวนขั้นต่ำและป่าฟื้นตัวได้เสมอ
`;}
else if(name==='skills'){title='สำนักวิชาใต้ต้นไม้';html=`เรียนในหมู่บ้าน · ทำได้ทุกสาย ไม่ต้องเลือกอาชีพ
${[['gentle','มือเบา','เก็บของและขุดแร่ใช้แรงน้อยลง'],['shield','ใจเย็น','ออกล่าเสียชีวิตน้อยลง'],['heal','ไออุ่น','ใช้เวท 12 ฟื้นชีวิต 30']].map(([id,n,d])=>`${icon(id==='heal'?23:9)}
${n}
${d} · 45 เหรียญ${p.skills.includes(id)?id==='heal'?actionButton('ใช้วิชา','heal'):'
เรียนแล้ว':actionButton('เรียน','learn',{skill:id})}
`).join('')}`;}
else if(name==='npc'){title='ฟินน์ · ร้านเล็กกลางหมู่บ้าน';html=`
${p.relations>=3?'“ทุกครั้งที่เธอกลับมา หุบเขาก็ดูอบอุ่นขึ้นอีกนิด”':'“ยินดีต้อนรับนะ ชอบซุปผักไหม? ฉันมีเห็ดสดอยู่พอดี”'}
ความสนิท ${p.relations} ♥ · คุยกันได้ทุกวัน
${actionButton('ทักทายฟินน์','talk',{},'primary wide')}${actionButton('มอบซุปผัก 1 ถ้วย · ความสนิท +2','gift',{},'wide')}${panelButton('แวะร้านค้า','shop','','wide')}${panelButton('ช่วยหมู่บ้าน','town','','wide')}`;}
else if(name==='town'){title='เติบโตไปด้วยกัน';html=`สะพานสู่เมืองลมทะเล
ทุกคนช่วยกันส่งไม้ 30 ชิ้น เพื่อเปิดเมืองใหม่พร้อมกัน
${w.bridge}/30 ชิ้น
${w.bridge<30?actionButton('บริจาคไม้ 1 ชิ้น','donate',{},'primary'):'
สะพานเปิดแล้ว เดินไปทางตะวันออกของหมู่บ้านได้เลย
'}
งานกิลด์วันนี้
หมอยาต้องการสมุนไพร 3 ชิ้น · รางวัล 35 เหรียญ
งานนี้รับได้วันละหนึ่งคนทั้งหุบเขา
${actionButton('ส่งสมุนไพรให้กิลด์','guild',{},'wide')}`;}
else if(name==='wardrobe'){title='ของเล็กๆ ที่เป็นเรา';html=`ซื้อของตกแต่งที่บ้านหรือหมู่บ้าน · ชิ้นละ 80 เหรียญ
${[['rose','ผ้าพันคอชมพู',12],['sage','ผ้าพันคอใบชา',12],['pet','เพื่อนกระต่าย',20],['decor','ต้นซากุระหน้าบ้าน',2]].map(([id,n,i])=>`
${icon(i)}
${n}
${(id==='pet'?p.pet:id==='decor'?p.decor:p.wardrobe.includes(id))?p.wardrobe.includes(id)?actionButton('สวมใส่','dress',{id}):'มีแล้ว':actionButton('ซื้อ 80 เหรียญ','cosmetic',{id})}`).join('')}
${actionButton('ใช้ชุดเริ่มต้น','dress',{id:'cream'},'wide')}`;}
else if(name==='settings'){title='กุญแจบ้านและการตั้งค่า';html=`✎ เครื่องมือจัดการภาพ
เซฟของ ${esc(p.name)} · บันทึกอัตโนมัติที่เซิร์ฟเวอร์นี้
${state.account?`ผูกบัญชีแล้ว: ${esc(state.account)}
`:panelButton('ผูกบัญชีเพื่อเก็บเซฟ','account','','primary wide')}${panelButton('เข้าบัญชีอื่น','login','','wide')}${panelButton('วิธีเล่น','help','','wide')}ข้อมูลจะอยู่ต่อเมื่อปิดเบราว์เซอร์ การย้ายเครื่องเซิร์ฟเวอร์ต้องย้ายฐานข้อมูลด้วย
`;}
else if(name==='help'){title='ชีวิตเล็กๆ ในหุบเขา';html=`เดินเข้าใกล้สิ่งของ แล้วใช้เครื่องมือในมือ
WASD / ลูกศร: เดิน · Shift: วิ่ง
1–6: เลือกเครื่องมือ · C / Space: ใช้เครื่องมือ
E / X: โต้ตอบ · B: กระเป๋า · M: ดูแผนที่ · J: ภารกิจ
แปลงว่าง: เลือกจอบ [1] กด C พรวนดิน แล้วเลือกเมล็ด [2] กด C ปลูก
พืชเล็ก: เลือกบัวรดน้ำ [3] แล้วกด C
พืชโต: ยืนข้างแปลงแล้วกด C เก็บเกี่ยว
ตกปลา: เดินถึงท่าน้ำ เลือกเบ็ด [5] แล้วกด C รอเครื่องหมาย ! แล้วกด C อีกครั้ง
ขุดแร่: ยืนข้างก้อนแร่ เลือกพลั่ว [4] แล้วกด C
เดินตามป้ายจนถึงขอบทางเพื่อข้ามแผนที่
กล่อง ครัว เตียง ร้านค้า และชาวเมืองอยู่ในฉาก เดินไปหาแล้วกด E
ของในเขตปลอดภัยไม่หาย ภารกิจคืบหน้าอัตโนมัติ
`;}
$('#modal-title').textContent=title;$('#modal-content').innerHTML=html;$('#close-modal').hidden=!state;if(!$('#modal').open)$('#modal').showModal();bindForms();}
function bindForms(){for(const type of ['start','login','account']){let form=$('#'+type+'-form');if(form)form.onsubmit=async e=>{e.preventDefault();const button=form.querySelector('button');button.disabled=true;try{let data=Object.fromEntries(new FormData(form));const r=await api(type==='account'?'account':type,data);if(type==='account'){state.account=r.account;open('settings');notify('ผูกบัญชีแล้ว');}else {state=r;$('#app').hidden=false;close();render();scene?.reset();notify(type==='start'?'ยินดีต้อนรับกลับบ้าน เริ่มเก็บแครอทกันเลย':'กลับบ้านแล้ว!');}}catch(e){notify(e.message);}finally{button.disabled=false;}}}}
document.addEventListener('click',e=>{const a=e.target.closest('[data-act]');if(a){act(a.dataset.act,JSON.parse(a.dataset.args||'{}'));return;}const b=e.target.closest('[data-panel]');if(b)open(b.dataset.panel,b.dataset.arg||'');});
$('#fullscreen').onclick=async()=>{try{if(document.fullscreenElement)await document.exitFullscreen();else await document.documentElement.requestFullscreen();}catch{notify('ใช้โหมดเต็มหน้าจอจากเมนูเบราว์เซอร์ได้ครับ');}};
$('#sound').onclick=()=>{sound=!sound;setAudioEnabled(sound);$('#sound').setAttribute('aria-label',sound?'ปิดเสียง':'เปิดเสียง');$('#sound').style.background=sound?'#dfeacd':'';if(sound){audioCtx ||=new AudioContext();audioCtx.resume();ping();soundTimer=setInterval(()=>{if(document.hidden||!sound)return;const o=audioCtx.createOscillator(),g=audioCtx.createGain();o.type='sine';o.frequency.value=[261.63,329.63,392,440,523.25][Math.floor(Math.random()*5)];g.gain.setValueAtTime(.012,audioCtx.currentTime);g.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+2.5);o.connect(g).connect(audioCtx.destination);o.start();o.stop(audioCtx.currentTime+2.5);},3000);}else clearInterval(soundTimer);};
setInterval(()=>{clock();if(panelName==='fishing'&&state?.player.fish){const f=state.player.fish,el=$('#fishing-status');if(el)el.textContent=Date.now()f.expires?'ปลาหลุดแล้ว เก็บเบ็ดแล้วลองใหม่ได้เลย':'ปลากินเหยื่อแล้ว! ดึงเบ็ดเลย';}},300);
try{art=await loadArt();try{state=await api('state');}catch(e){if(e.status!==401)throw e;}$('#loading').hidden=true;$('#app').hidden=!state;scene=createScene($('#scene'),art,()=>state,act,open);if(state)render();else open('welcome');}catch(e){$('#loading').innerHTML='หุบเขากำลังพัก
'+esc(e.message)+'
';}
setInterval(async()=>{if(!state||busy||document.hidden)return;try{state=await api('state');render();}catch{}},5000);
let lastSavedPosition='';setInterval(()=>{if(!state||busy||panelName||!scene||scene.zone!==state.player.zone||document.hidden)return;const pos={x:Math.round(scene.player.x),y:Math.round(scene.player.y)};const key=state.player.zone+JSON.stringify(pos);if(key===lastSavedPosition)return;lastSavedPosition=key;act('position',pos);},4000);