285 lines
18 KiB
HTML
285 lines
18 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="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="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 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};
|
||
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();
|
||
});
|
||
});
|
||
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);
|
||
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)';
|
||
for(var py=0;py<H;py++) for(var px=0;px<W;px++) if(path[py][px]) ctx.fillRect(px*cell,py*cell,cell,cell);
|
||
var num=0;
|
||
for(var dy2=0;dy2<H;dy2++) for(var dx2=0;dx2<W;dx2++) if(dome[dy2][dx2]){
|
||
num++; var cx=dx2*cell+cell/2, cy=dy2*cell+cell/2, r=Math.max(8,cell*0.62);
|
||
ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.fillStyle='rgba(255,209,102,.92)'; ctx.fill();
|
||
ctx.lineWidth=2; ctx.strokeStyle='#7a5a10'; ctx.stroke();
|
||
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(num),cx,cy+1);
|
||
}
|
||
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);
|
||
var np=0,nd=0; for(var a=0;a<H;a++) for(var b=0;b<W;b++){ if(path[a][b])np++; if(dome[a][b])nd++; }
|
||
statEl.innerHTML='ฉาก: <b>'+(map&&map.name||curId)+'</b><br>เส้นทาง <b>'+np+'</b> ช่อง · จุดคำถาม <b>'+nd+'</b> · เกิด ('+spawn.x+','+spawn.y+')';
|
||
}
|
||
|
||
function tileAt(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:Math.floor(px/cell), y:Math.floor(py/cell) };
|
||
}
|
||
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 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(!painting) return; ev.preventDefault(); var t=tileAt(ev); applyAt(t.x,t.y); draw(); }
|
||
function onUp(){ painting=false; }
|
||
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-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, 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>
|