Files
justice/www/html/Game/public/qb-editor.html
T
2026-07-31 03:36:58 +00:00

408 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ตั้งค่าฉาก Quiz Battle</title>
<style>
* { box-sizing: border-box; }
:root { --bg:#0a0f20; --panel:#141a32cc; --line:#2a3357; --txt:#eef2ff; --accent:#22d3ee; }
html,body { margin:0; height:100%; overflow:hidden; }
body { background:var(--bg); color:var(--txt); font-family:'Kanit','Sarabun','Segoe UI',system-ui,sans-serif; }
/* ===== map เต็มจอ อยู่ข้างหลัง ตรงกลาง ===== */
#workspace {
position:fixed; inset:0; z-index:1;
display:flex; align-items:center; justify-content:center;
overflow:auto; padding:10px;
background:radial-gradient(ellipse at 50% 0,rgba(40,60,120,.22),transparent 60%);
}
.canvas-wrap { position:relative; box-shadow:0 12px 50px rgba(0,0,0,.6); border-radius:6px; overflow:hidden; }
canvas { display:block; cursor:crosshair; touch-action:none; }
/* ===== แถบเครื่องมือลอย (ดัก/พับได้) ===== */
#toolbar { position:fixed; z-index:20; width:288px; max-height:94vh; overflow:auto;
background:var(--panel); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
border:1px solid var(--line); border-radius:16px; box-shadow:0 10px 40px rgba(0,0,0,.5);
display:flex; flex-direction:column; gap:12px; padding:14px; transition:opacity .15s, transform .15s; }
/* ตำแหน่งดัก */
#toolbar.dock-right { top:50%; right:14px; left:auto; bottom:auto; transform:translateY(-50%); }
#toolbar.dock-left { top:50%; left:14px; right:auto; bottom:auto; transform:translateY(-50%); }
#toolbar.dock-top { top:14px; left:50%; right:auto; bottom:auto; transform:translateX(-50%); width:min(96vw,720px); flex-direction:row; flex-wrap:wrap; align-items:center; }
#toolbar.dock-bottom { bottom:14px; left:50%; right:auto; top:auto; transform:translateX(-50%); width:min(96vw,720px); flex-direction:row; flex-wrap:wrap; align-items:center; }
#toolbar.dock-top .grow, #toolbar.dock-bottom .grow { flex:1 1 150px; }
#toolbar.dock-top .tools, #toolbar.dock-bottom .tools { grid-template-columns:repeat(4,1fr); }
#toolbar.collapsed { display:none; }
.tb-head { display:flex; align-items:center; gap:8px; }
.tb-title { font-size:15px; font-weight:700; flex:1; white-space:nowrap; }
.tb-head .ico-btn { font-size:15px; }
.ico-btn { border:1px solid var(--line); background:#0d1430; color:var(--txt); border-radius:9px; width:32px; height:32px; display:grid; place-items:center; cursor:pointer; font-size:14px; }
.ico-btn:hover { filter:brightness(1.2); }
.ico-btn.on { border-color:var(--accent); background:rgba(34,211,238,.16); }
.dockset { display:flex; gap:5px; }
.field { display:flex; flex-direction:column; gap:5px; }
.field > span { font-size:11.5px; color:#a9b4dd; font-weight:600; }
select { width:100%; background:#0d1430; color:var(--txt); border:1px solid var(--line); border-radius:10px; padding:9px 10px; font:inherit; font-weight:600; }
.tools { display:grid; grid-template-columns:1fr 1fr; gap:7px; }
.tool { display:flex; flex-direction:column; align-items:center; gap:3px; padding:10px 4px; border:2px solid var(--line); background:#0d1430; color:var(--txt); border-radius:11px; cursor:pointer; font-weight:600; font-size:12.5px; transition:.12s; }
.tool .ic { font-size:20px; line-height:1; }
.tool:hover { border-color:#3b4570; filter:brightness(1.1); }
.tool.active { border-color:var(--accent); background:rgba(34,211,238,.16); box-shadow:0 0 0 1px var(--accent) inset; }
.row { display:flex; gap:7px; } .row > * { flex:1; }
.btn { border:none; border-radius:10px; padding:10px 12px; font:inherit; font-weight:700; cursor:pointer; color:#04121f; background:linear-gradient(180deg,#67e8f9,#22d3ee); white-space:nowrap; }
.btn.sec { background:#1e2747; color:#eef2ff; border:1px solid var(--line); }
.btn:disabled { opacity:.5; cursor:default; }
.stat { font-size:12px; color:#cfd7ff; background:#0d1430; border:1px solid var(--line); border-radius:10px; padding:8px 11px; line-height:1.6; }
.stat b { color:var(--accent); }
.toast { font-size:12.5px; font-weight:600; min-height:16px; }
.toast.ok { color:#6ee7b7; } .toast.err { color:#fca5a5; }
.hint { font-size:11.5px; color:#9fb0e6; line-height:1.5; }
/* ปุ่มเปิดแถบ (ตอนพับ) */
#reopen { position:fixed; z-index:21; top:14px; right:14px; display:none;
border:1px solid var(--line); background:var(--panel); backdrop-filter:blur(8px);
color:var(--txt); border-radius:12px; padding:10px 16px; font:inherit; font-weight:700; cursor:pointer; box-shadow:0 6px 20px rgba(0,0,0,.4); }
#reopen.show { display:block; }
.brushlabel { position:fixed; bottom:12px; left:14px; z-index:15; background:rgba(8,12,28,.85); border:1px solid var(--line); padding:7px 13px; border-radius:999px; font-size:12.5px; font-weight:600; pointer-events:none; }
</style>
</head>
<body>
<!-- map เต็มจอ ข้างหลัง -->
<div id="workspace"><div class="canvas-wrap"><canvas id="cv"></canvas></div></div>
<!-- ปุ่มเปิดแถบเครื่องมือ (โผล่ตอนพับ) -->
<button id="reopen">🧰 เครื่องมือ</button>
<!-- แถบเครื่องมือลอย -->
<div id="toolbar" class="dock-right">
<div class="tb-head">
<span class="tb-title">🗺️ ตั้งค่าฉาก</span>
<div class="dockset" title="ย้ายตำแหน่งแถบ">
<button class="ico-btn" data-dock="top" title="บน"></button>
<button class="ico-btn" data-dock="left" title="ซ้าย"></button>
<button class="ico-btn" data-dock="right" title="ขวา"></button>
<button class="ico-btn" data-dock="bottom" title="ล่าง"></button>
</div>
<button class="ico-btn" id="btn-collapse" title="พับเก็บ"></button>
</div>
<div class="field grow"><span>ห้อง (หัวข้อ)</span><select id="room-select"></select></div>
<div class="field grow">
<span>เครื่องมือ — คลิก/ลากบนภาพ</span>
<div class="tools">
<button class="tool active" data-tool="path"><span class="ic">🛣️</span>เส้นทาง</button>
<button class="tool" data-tool="dome"><span class="ic"></span>จุดคำถาม</button>
<button class="tool" data-tool="spawn"><span class="ic">🚩</span>จุดเกิด</button>
<button class="tool" data-tool="fence"><span class="ic">🚧</span>รั้วกั้น</button>
<button class="tool" data-tool="erase"><span class="ic">🧽</span>ยางลบ</button>
</div>
</div>
<div class="field"><span>ขนาดพู่กัน</span>
<select id="brush"><option value="1">1×1</option><option value="2" selected>2×2</option><option value="3">3×3</option></select>
</div>
<div class="field" id="fence-field" style="display:none"><span>แบบรั้ว — คลิกวาง · ลากย้าย · คลิกขวาลบ</span>
<select id="fence-kind">
<option value="h1" selected>▬ แนวนอน สั้น (1 ช่อง)</option>
<option value="h2">▬▬ แนวนอน ยาว (2 ช่อง)</option>
<option value="v1">▮ แนวตั้ง สั้น (1 ช่อง)</option>
<option value="v2">▮▮ แนวตั้ง ยาว (2 ช่อง)</option>
</select>
</div>
<div class="stat grow" id="stat"></div>
<div class="row">
<button class="btn sec" id="btn-clear-path">ล้างเส้นทาง</button>
<button class="btn sec" id="btn-clear-dome">ล้างจุดคำถาม</button>
</div>
<button class="btn sec grow" id="btn-clear-fence">ล้างรั้วกั้น</button>
<button class="btn grow" id="btn-save">💾 บันทึก</button>
<button class="btn sec grow" id="btn-test">▶ ทดสอบเล่น</button>
<div class="toast" id="toast"></div>
<div class="hint">วางจุดคำถามให้ตรงแท่นในภาพ · ลากเส้นทางจากจุดเกิดด้านบนลงไปถึงปลายล่าง · ไม่วาดเส้นทาง = เดินอิสระ</div>
</div>
<div class="brushlabel" id="brushlabel">🛣️ เส้นทาง</div>
<script>
(function(){
'use strict';
var BASE = '/Game';
var ROOMS = ['กฎหมายใกล้ตัว','กฎหมายสิทธิ์พื้นฐาน','กฎหมายจราจร','คดีเกี่ยวกับทรัพย์','คดีหมิ่นประมาท','คดีทางเพศ','คดีอาชญากรรม','อาชญากรรมออนไลน์','กระบวนการยุติธรรม','งานบริการกระทรวงยุติธรรม'];
var qs = new URLSearchParams(location.search);
var curId = qs.get('id') || 'qbroom1';
var map=null, W=24, H=49, path=[], dome=[], spawn={x:11,y:2};
/* รั้วกั้น — [{x,y,w,h}] หน่วยเป็น "ช่อง" ทศนิยมได้ วางอิสระไม่ยึดกริด
กริด 20x40 หยาบเกินจะกั้นตรงรั้วที่อาร์ตวาดกลางแถบได้ (รั้วอยู่กลางช่อง ไม่ใช่ขอบช่อง)
เกมเช็คด้วยกล่องชนของสไปรต์ซ้อนกล่องรั้ว (server.js quizBattleFenceBlocksServer + play.js ตัวเดียวกัน)
ในเกมมองไม่เห็น — อาร์ตวาดรั้วไว้ให้แล้ว ตรงนี้เป็นแค่ตัวกั้นการเดิน */
var fences=[], dragFence=null;
/* คลังคำถามของ Quiz Battle มี 20 ข้อต่อหัวข้อ (1 หัวข้อ = 1 ฉาก) → ฉากหนึ่งควรมีจุดคำถาม 20 ข้อพอดี
ถ้าวางเกิน เลขข้อจะวนซ้ำ (quizBattleQuestionIndexForCompPlay ใช้ (idx % poolLen)) */
var QUESTIONS_PER_MAP = 20;
var bgImg=new Image(), tool='path', brush=2, painting=false, paintVal=1;
var cv=document.getElementById('cv'), ctx=cv.getContext('2d');
var toastEl=document.getElementById('toast'), statEl=document.getElementById('stat'), brushLabel=document.getElementById('brushlabel');
var toolbar=document.getElementById('toolbar'), reopen=document.getElementById('reopen');
function toast(m,ok){ toastEl.textContent=m||''; toastEl.className='toast '+(ok===false?'err':ok===true?'ok':''); }
function emptyGrid(w,h){ var a=[]; for(var y=0;y<h;y++){var r=[];for(var x=0;x<w;x++)r.push(0);a.push(r);} return a; }
function norm(src,w,h){ var a=emptyGrid(w,h); if(Array.isArray(src)){for(var y=0;y<h;y++){var row=src[y]||[];for(var x=0;x<w;x++)a[y][x]=row[x]===1?1:0;}} return a; }
// ===== แถบเครื่องมือ: ดัก + พับ (จำค่าใน localStorage) =====
function setDock(d){
toolbar.className = toolbar.className.replace(/dock-\w+/g,'').trim();
toolbar.classList.add('dock-'+d);
document.querySelectorAll('.dockset .ico-btn').forEach(function(b){ b.classList.toggle('on', b.getAttribute('data-dock')===d); });
try{ localStorage.setItem('qbEdDock', d); }catch(e){}
setTimeout(layout,0);
}
document.querySelectorAll('.dockset .ico-btn').forEach(function(b){ b.addEventListener('click',function(){ setDock(b.getAttribute('data-dock')); }); });
function setCollapsed(c){
toolbar.classList.toggle('collapsed', c);
reopen.classList.toggle('show', c);
try{ localStorage.setItem('qbEdCollapsed', c?'1':'0'); }catch(e){}
}
document.getElementById('btn-collapse').addEventListener('click',function(){ setCollapsed(true); });
reopen.addEventListener('click',function(){ setCollapsed(false); });
// restore prefs
var savedDock='right'; try{ savedDock=localStorage.getItem('qbEdDock')||'right'; }catch(e){}
setDock(savedDock);
try{ if(localStorage.getItem('qbEdCollapsed')==='1') setCollapsed(true); }catch(e){}
// room dropdown
var sel=document.getElementById('room-select');
for(var i=1;i<=10;i++){ var o=document.createElement('option'); o.value='qbroom'+i; o.textContent='ห้อง '+i+' — '+ROOMS[i-1]; sel.appendChild(o); }
sel.value=curId;
sel.addEventListener('change',function(){ location.search='?id='+encodeURIComponent(sel.value); });
// tools
document.querySelectorAll('.tool').forEach(function(b){
b.addEventListener('click',function(){
document.querySelectorAll('.tool').forEach(function(x){x.classList.remove('active');});
b.classList.add('active'); tool=b.getAttribute('data-tool');
brushLabel.textContent = b.querySelector('.ic').textContent+' '+b.textContent.trim();
/* ตัวเลือกแบบรั้วโผล่เฉพาะตอนใช้เครื่องมือรั้ว · ขนาดพู่กันไม่เกี่ยวกับรั้ว (รั้วไม่ยึดกริด) */
var ff=document.getElementById('fence-field');
if(ff) ff.style.display=(tool==='fence')?'':'none';
});
});
document.getElementById('brush').addEventListener('change',function(e){ brush=parseInt(e.target.value,10)||1; });
function load(){
toast('กำลังโหลดฉาก…');
fetch(BASE+'/api/maps/'+encodeURIComponent(curId)+'?_='+Date.now(),{cache:'no-store'})
.then(function(r){ if(!r.ok) throw new Error('โหลดฉากไม่ได้ ('+r.status+')'); return r.json(); })
.then(function(m){
map=m; W=m.width||24; H=m.height||49;
path=norm(m.quizBattlePathArea,W,H); dome=norm(m.quizBattleDomeArea,W,H);
fences=(Array.isArray(m.quizBattleFences)?m.quizBattleFences:[])
.map(function(f){ return f&&isFinite(f.x)&&isFinite(f.y)&&f.w>0&&f.h>0
? { x:+f.x, y:+f.y, w:+f.w, h:+f.h } : null; }).filter(Boolean);
spawn=(m.spawn&&typeof m.spawn.x==='number')?{x:m.spawn.x,y:m.spawn.y}:{x:11,y:2};
if(m.backgroundImage){ bgImg=new Image(); bgImg.onload=draw; bgImg.onerror=draw; bgImg.src=m.backgroundImage+(m.backgroundImage.indexOf('?')<0?('?_='+Date.now()):''); }
toast('โหลด "'+(m.name||curId)+'" แล้ว',true); layout();
})
.catch(function(e){ toast(e.message,false); });
}
function layout(){
var maxH=Math.max(300,window.innerHeight-24);
var maxW=Math.max(200,window.innerWidth-24);
var cell=Math.min(maxH/H, maxW/W); cell=Math.max(6,Math.floor(cell));
cv.width=W*cell; cv.height=H*cell; cv._cell=cell; draw();
}
function draw(){
var cell=cv._cell, w=cv.width, h=cv.height;
ctx.clearRect(0,0,w,h);
if(bgImg&&bgImg.complete&&bgImg.naturalWidth) ctx.drawImage(bgImg,0,0,w,h);
else { ctx.fillStyle='#10162e'; ctx.fillRect(0,0,w,h); }
ctx.strokeStyle='rgba(255,255,255,.06)'; ctx.lineWidth=1;
for(var x=0;x<=W;x++){ ctx.beginPath(); ctx.moveTo(x*cell+.5,0); ctx.lineTo(x*cell+.5,h); ctx.stroke(); }
for(var y=0;y<=H;y++){ ctx.beginPath(); ctx.moveTo(0,y*cell+.5); ctx.lineTo(w,y*cell+.5); ctx.stroke(); }
ctx.fillStyle='rgba(122,162,247,.42)';
/* [2026-07-22] กัน TypeError ตอนเปิดหน้าครั้งแรก — layout() เรียก draw() ตั้งแต่ยังไม่ได้โหลดแผนที่
ตอนนั้น path/dome ยังเป็น [] แต่ H=49 → path[py] undefined แล้ว throw ทั้ง draw() (กริด/ฉากไม่ขึ้นเลย) */
for(var py=0;py<H;py++) for(var px=0;px<W;px++) if(path[py]&&path[py][px]) ctx.fillRect(px*cell,py*cell,cell,cell);
/* [2026-07-30] "1 คำถาม" = "โดม 1 กลุ่มที่ติดกัน" ไม่ใช่ "1 ช่อง"
server รวมช่องที่ติดกันเป็นกลุ่มเดียว (normalizeQuizBattleDomeCompOnMap) แล้วให้เลขข้อ 1 เลขต่อกลุ่ม
ตั้งแต่ 07-30 โดมถูกวางเป็น "ทั้งคอลัมน์ของเลน" (2-3 ช่อง) เพราะเลนหนา 3 แถวแต่กล่องชนสูง 1.35 ช่อง
ถ้าโดมเป็นช่องเดียวผู้เล่นจะมุดใต้ได้แล้วไปตันที่ประตูถัดไปแบบแก้ไม่ได้
ของเดิมนับ/ใส่เลขทีละช่อง → แมป 20 ข้อโชว์เป็น 52-56 (user: "จริง ๆ ควรมีแค่ 20 คำถามต่อ map")
ตรงนี้จึงจัดกลุ่มแบบเดียวกับ server แล้ววาดเลขครั้งเดียวต่อกลุ่ม (ที่ช่องบน-ซ้ายสุด เหมือน play.js) */
var comp=[], nGroup=0;
for(var gy=0;gy<H;gy++){ comp[gy]=[]; for(var gx=0;gx<W;gx++) comp[gy][gx]=0; }
for(var qy=0;qy<H;qy++) for(var qx=0;qx<W;qx++){
if(!(dome[qy]&&dome[qy][qx])||comp[qy][qx]) continue;
nGroup++; var st=[[qx,qy]]; comp[qy][qx]=nGroup;
while(st.length){
var c=st.pop();
var nb=[[c[0]+1,c[1]],[c[0]-1,c[1]],[c[0],c[1]+1],[c[0],c[1]-1]];
for(var k=0;k<4;k++){
var nx=nb[k][0], ny=nb[k][1];
if(nx<0||ny<0||nx>=W||ny>=H) continue;
if(!(dome[ny]&&dome[ny][nx])||comp[ny][nx]) continue;
comp[ny][nx]=nGroup; st.push([nx,ny]);
}
}
}
for(var dy2=0;dy2<H;dy2++) for(var dx2=0;dx2<W;dx2++) if(dome[dy2]&&dome[dy2][dx2]){
var id=comp[dy2][dx2];
var cx=dx2*cell+cell/2, cy=dy2*cell+cell/2, r=Math.max(8,cell*0.62);
/* ช่องอื่นในกลุ่มเดียวกันวาดจาง ๆ ไม่ใส่เลข — ให้เห็นว่า "ก้อนนี้คือข้อเดียวกัน" */
var head=!((comp[dy2-1]&&comp[dy2-1][dx2]===id)||(comp[dy2][dx2-1]===id));
ctx.beginPath(); ctx.arc(cx,cy,head?r:r*0.5,0,Math.PI*2);
ctx.fillStyle=head?'rgba(255,209,102,.92)':'rgba(255,209,102,.34)'; ctx.fill();
ctx.lineWidth=head?2:1; ctx.strokeStyle=head?'#7a5a10':'rgba(122,90,16,.5)'; ctx.stroke();
if(!head) continue;
ctx.fillStyle='#3a2c00'; ctx.font='bold '+Math.max(10,cell*0.7)+'px Kanit,sans-serif';
ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText(String(id),cx,cy+1);
}
/* รั้วกั้น — วาดตามพิกัดทศนิยมจริง (ไม่ snap เข้าช่อง) จะได้เห็นว่ามันอยู่ตรงไหนของภาพจริง ๆ */
for(var fi=0;fi<fences.length;fi++){
var fr=fences[fi];
var rx=fr.x*cell, ry=fr.y*cell, rw=Math.max(2,fr.w*cell), rh=Math.max(2,fr.h*cell);
ctx.fillStyle='rgba(255,99,132,.55)'; ctx.fillRect(rx,ry,rw,rh);
ctx.lineWidth=2; ctx.strokeStyle='#ff4d6d'; ctx.strokeRect(rx+.5,ry+.5,rw-1,rh-1);
/* ขีดทแยงให้ดูเป็น "รั้ว" ไม่ใช่แค่แถบสี */
ctx.save(); ctx.beginPath(); ctx.rect(rx,ry,rw,rh); ctx.clip();
ctx.strokeStyle='rgba(255,255,255,.5)'; ctx.lineWidth=1;
for(var hx=rx-rh;hx<rx+rw;hx+=6){ ctx.beginPath(); ctx.moveTo(hx,ry+rh); ctx.lineTo(hx+rh,ry); ctx.stroke(); }
ctx.restore();
}
var sx=spawn.x*cell, sy=spawn.y*cell;
ctx.fillStyle='rgba(52,211,153,.9)'; ctx.fillRect(sx,sy,cell,cell);
ctx.fillStyle='#04241a'; ctx.font='bold '+Math.max(10,cell*0.6)+'px Kanit,sans-serif';
ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('🚩',sx+cell/2,sy+cell/2);
/* กันเคสเดียวกับด้านบน — นับก่อนโหลดแผนที่ path[a] ยัง undefined */
var np=0,nd=0; for(var a=0;a<H;a++) for(var b=0;b<W;b++){ if(path[a]&&path[a][b])np++; if(dome[a]&&dome[a][b])nd++; }
/* จำนวนคำถามจริง = จำนวน "กลุ่ม" (ตรงกับที่เกมถาม) · จำนวนช่องบอกไว้ในวงเล็บเผื่อเช็คว่าโดมกั้นเลนครบไหม */
var qWarn = (nGroup===QUESTIONS_PER_MAP) ? '' :
' <span style="color:#ff9f6e">— ควรมี '+QUESTIONS_PER_MAP+' ข้อต่อฉาก</span>';
statEl.innerHTML='ฉาก: <b>'+(map&&map.name||curId)+'</b><br>เส้นทาง <b>'+np+'</b> ช่อง · จุดคำถาม <b>'+nGroup+'</b> ข้อ <span style="opacity:.6">('+nd+' ช่อง)</span>'+qWarn
+' · รั้ว <b>'+fences.length+'</b> · เกิด ('+spawn.x+','+spawn.y+')';
}
function tileAt(ev){
var p=pointAt(ev); return { x:Math.floor(p.x), y:Math.floor(p.y) };
}
/** พิกัดแบบทศนิยม (หน่วยช่อง) — รั้วใช้ตัวนี้ เพราะวางอิสระไม่ยึดกริด */
function pointAt(ev){
var rect=cv.getBoundingClientRect();
var px=(ev.clientX!=null?ev.clientX:(ev.touches&&ev.touches[0].clientX))-rect.left;
var py=(ev.clientY!=null?ev.clientY:(ev.touches&&ev.touches[0].clientY))-rect.top;
var cell=cv._cell; return { x:px/cell, y:py/cell };
}
function fenceSize(){
var k=(document.getElementById('fence-kind')||{}).value||'h1';
if(k==='h2') return { w:2.0, h:0.22 };
if(k==='v1') return { w:0.22, h:1.0 };
if(k==='v2') return { w:0.22, h:2.0 };
return { w:1.0, h:0.22 };
}
function fenceAtPoint(p){
for(var i=fences.length-1;i>=0;i--){
var f=fences[i];
if(p.x>=f.x&&p.x<=f.x+f.w&&p.y>=f.y&&p.y<=f.y+f.h) return i;
}
/* เผื่อรั้วบางมาก (0.22 ช่อง) จิ้มโดนยาก — ขยายพื้นที่จับอีก 0.18 ช่องรอบตัว */
for(var j=fences.length-1;j>=0;j--){
var g=fences[j], m=0.18;
if(p.x>=g.x-m&&p.x<=g.x+g.w+m&&p.y>=g.y-m&&p.y<=g.y+g.h+m) return j;
}
return -1;
}
function round2(v){ return Math.round(v*100)/100; }
function applyAt(tx,ty){
if(tx<0||ty<0||tx>=W||ty>=H) return;
if(tool==='spawn'){ spawn={x:tx,y:ty}; return; }
var b=brush, half=Math.floor(b/2);
for(var dy=0;dy<b;dy++) for(var dx=0;dx<b;dx++){
var x=tx+dx-half, y=ty+dy-half; if(x<0||y<0||x>=W||y>=H) continue;
if(tool==='path') path[y][x]=paintVal;
else if(tool==='dome') dome[y][x]=paintVal;
else if(tool==='erase'){ path[y][x]=0; dome[y][x]=0; }
}
}
function onDown(ev){
ev.preventDefault();
/* รั้ว: ทำงานด้วยพิกัดทศนิยมล้วน ไม่แตะกริดเลย — คลิกที่ว่าง = วางใหม่ (จุดที่คลิกคือ "กลาง" รั้ว)
คลิกบนรั้วเดิม = เริ่มลากย้าย · ยางลบคลิกโดนรั้วก็ลบรั้วนั้น */
var p=pointAt(ev);
if(tool==='fence'||tool==='erase'){
var hit=fenceAtPoint(p);
if(tool==='erase'&&hit>=0){ fences.splice(hit,1); draw(); return; }
if(tool==='fence'){
if(hit>=0){ dragFence={ i:hit, dx:p.x-fences[hit].x, dy:p.y-fences[hit].y }; return; }
var s=fenceSize();
fences.push({ x:round2(p.x-s.w/2), y:round2(p.y-s.h/2), w:s.w, h:s.h });
dragFence={ i:fences.length-1, dx:s.w/2, dy:s.h/2 };
draw(); return;
}
}
var t=tileAt(ev);
if(tool==='path') paintVal=(path[t.y]&&path[t.y][t.x])?0:1;
else if(tool==='dome') paintVal=(dome[t.y]&&dome[t.y][t.x])?0:1;
else paintVal=1;
painting=true; applyAt(t.x,t.y); draw();
}
function onMove(ev){
if(dragFence){
ev.preventDefault(); var p=pointAt(ev), f=fences[dragFence.i];
if(f){ f.x=round2(p.x-dragFence.dx); f.y=round2(p.y-dragFence.dy); draw(); }
return;
}
if(!painting) return; ev.preventDefault(); var t=tileAt(ev); applyAt(t.x,t.y); draw();
}
function onUp(){ painting=false; dragFence=null; }
/* คลิกขวาบนรั้ว = ลบ (ไม่ต้องสลับไปยางลบ) */
cv.addEventListener('contextmenu',function(ev){
var hit=fenceAtPoint(pointAt(ev));
if(hit<0) return;
ev.preventDefault(); fences.splice(hit,1); draw();
});
cv.addEventListener('mousedown',onDown);
window.addEventListener('mousemove',onMove);
window.addEventListener('mouseup',onUp);
cv.addEventListener('touchstart',onDown,{passive:false});
cv.addEventListener('touchmove',onMove,{passive:false});
window.addEventListener('touchend',onUp);
window.addEventListener('resize',layout);
document.getElementById('btn-clear-path').addEventListener('click',function(){ path=emptyGrid(W,H); draw(); toast('ล้างเส้นทางแล้ว',true); });
document.getElementById('btn-clear-dome').addEventListener('click',function(){ dome=emptyGrid(W,H); draw(); toast('ล้างจุดคำถามแล้ว',true); });
document.getElementById('btn-clear-fence').addEventListener('click',function(){ fences=[]; draw(); toast('ล้างรั้วกั้นแล้ว',true); });
document.getElementById('btn-save').addEventListener('click',function(){
var btn=this; btn.disabled=true; toast('กำลังบันทึก…');
fetch(BASE+'/api/maps/'+encodeURIComponent(curId),{method:'PUT',headers:{'Content-Type':'application/json'},
body:JSON.stringify({ quizBattlePathArea:path, quizBattleDomeArea:dome, quizBattleFences:fences, spawn:spawn, width:W, height:H })})
.then(function(r){return r.json();}).then(function(res){ btn.disabled=false;
if(res&&res.ok) toast('บันทึก "'+curId+'" สำเร็จ ✓',true); else toast((res&&res.error)||'บันทึกไม่สำเร็จ',false);
}).catch(function(e){ btn.disabled=false; toast('บันทึกไม่สำเร็จ: '+e.message,false); });
});
document.getElementById('btn-test').addEventListener('click',function(){
toast('กำลังเปิดห้องทดสอบ…');
fetch(BASE+'/api/spaces',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({ mapId:curId, name:'TEST '+curId+' '+Date.now(), maxPlayers:50 })})
.then(function(r){return r.json();}).then(function(res){
if(res&&res.ok){ window.open(BASE+'/play.html?space='+encodeURIComponent(res.spaceId)+'&map='+encodeURIComponent(curId)+'&nick=Tester','_blank'); toast('เปิดแท็บทดสอบแล้ว',true); }
else toast((res&&res.error)||'เปิดทดสอบไม่ได้',false);
}).catch(function(e){ toast(e.message,false); });
});
load();
})();
</script>
</body>
</html>