Files
justice/www/html/Game/public/qb-editor.html
T
2026-06-06 12:40:41 +00:00

285 lines
18 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="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>