408 lines
27 KiB
HTML
408 lines
27 KiB
HTML
<!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>
|