243 lines
17 KiB
HTML
243 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="th"><head>
|
||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Justice — Test Console (เทสมินิเกม + Disconnect)</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{--bg:#0a0f1e;--panel:#121a30;--panel2:#0e1526;--line:#22304f;--txt:#dce6ff;--mut:#8aa0c8;--cyan:#22d3ee;--green:#22c55e;--red:#f87171;--amber:#fbbf24;--violet:#a78bfa;}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font-family:Kanit,system-ui,sans-serif;background:radial-gradient(circle at 50% -10%,#13213f,#070b16 70%);color:var(--txt);min-height:100vh;padding:24px 16px 80px}
|
||
.wrap{max-width:1180px;margin:0 auto}
|
||
h1{font-weight:600;font-size:25px;letter-spacing:.4px;margin-bottom:2px}
|
||
h1 .sub{color:var(--mut);font-size:13.5px;font-weight:300;display:block;margin-top:5px;line-height:1.5}
|
||
h2{font-weight:600;font-size:18px;margin:26px 0 10px;display:flex;align-items:center;gap:9px}
|
||
h2 .tag{font-size:11px;font-weight:500;padding:3px 9px;border-radius:7px;background:rgba(34,211,238,.16);color:var(--cyan)}
|
||
.topbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin:14px 0 6px}
|
||
.btn{border:1px solid var(--line);background:var(--panel2);color:var(--txt);border-radius:9px;padding:8px 15px;font-size:13.5px;cursor:pointer;font-family:inherit;text-decoration:none;display:inline-flex;align-items:center;gap:7px}
|
||
.btn:hover{border-color:var(--cyan)}
|
||
.btn.primary{background:var(--green);color:#042c14;border-color:var(--green);font-weight:600}
|
||
.btn.v{background:rgba(167,139,250,.16);border-color:var(--violet);color:var(--violet)}
|
||
.btn.amber{background:rgba(251,191,36,.15);border-color:var(--amber);color:var(--amber)}
|
||
.btn[disabled]{opacity:.5;cursor:not-allowed}
|
||
.note{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--amber);border-radius:10px;padding:13px 16px;font-size:13px;color:#e8d9b4;line-height:1.6;margin:10px 0}
|
||
.note b{color:var(--amber)}
|
||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px;margin-top:6px}
|
||
.mg{background:var(--panel);border:1px solid var(--line);border-radius:13px;padding:14px 15px;display:flex;flex-direction:column;gap:9px}
|
||
.mg .hd{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
||
.mg .nm{font-weight:600;font-size:15px}
|
||
.mg .nm small{display:block;color:var(--mut);font-weight:300;font-size:11.5px;margin-top:2px}
|
||
.mg .mid{font-family:ui-monospace,monospace;font-size:11px;color:var(--mut)}
|
||
.auth{font-size:10.5px;font-weight:600;padding:3px 8px;border-radius:20px;white-space:nowrap}
|
||
.auth.srv{background:rgba(34,197,94,.16);color:var(--green)}
|
||
.auth.host{background:rgba(248,113,113,.16);color:var(--red)}
|
||
.auth.test{background:rgba(251,191,36,.18);color:var(--amber)}
|
||
.mgrow{display:flex;gap:7px;flex-wrap:wrap}
|
||
.mgrow .btn{flex:1;justify-content:center;padding:7px 10px;font-size:12.5px}
|
||
.panel{background:var(--panel);border:1px solid var(--line);border-radius:13px;padding:16px 18px}
|
||
.field{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:9px 0}
|
||
.field label{font-size:13px;color:var(--mut);min-width:130px}
|
||
.field input[type=number],.field select{background:var(--panel2);border:1px solid var(--line);border-radius:8px;color:var(--txt);padding:8px 11px;font-family:inherit;font-size:14px;width:90px}
|
||
.field input:focus,.field select:focus{outline:none;border-color:var(--cyan)}
|
||
.field select{width:auto;min-width:180px}
|
||
.links{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
|
||
.links a{font-size:12.5px}
|
||
ol.steps{margin:8px 0 4px 20px;font-size:13px;line-height:1.8;color:#cfe0ff}
|
||
ol.steps b{color:#fff}
|
||
ol.steps code{background:var(--panel2);border:1px solid var(--line);border-radius:5px;padding:1px 6px;font-size:12px}
|
||
.toggle{display:inline-flex;align-items:center;gap:9px;cursor:pointer;user-select:none;font-size:13.5px}
|
||
.sw{width:44px;height:24px;border-radius:13px;background:#33415c;position:relative;transition:.2s;flex:0 0 auto}
|
||
.sw::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;transition:.2s}
|
||
.toggle.on .sw{background:var(--green)}.toggle.on .sw::after{left:23px}
|
||
#status{font-size:12.5px;color:var(--cyan);min-height:18px;margin-top:8px}
|
||
.foot{color:var(--mut);font-size:12px;margin-top:26px;text-align:center;line-height:1.7}
|
||
.chk{font-size:13px;line-height:1.85;color:#cfe0ff}
|
||
.chk li{margin-left:18px}
|
||
</style></head>
|
||
<body><div class="wrap">
|
||
|
||
<h1>Justice — Test Console
|
||
<span class="sub">เครื่องมือเทสมินิเกม + ระบบ Disconnect แบบกดปุ่มเดียว · ใช้ flow เดียวกับเกมจริง (สร้างห้อง /Game/api/spaces)</span>
|
||
</h1>
|
||
|
||
<div class="topbar">
|
||
<a class="btn" href="index.php">← กลับ Bug List</a>
|
||
<span class="toggle" id="tmToggle" title="ตั้ง localStorage justiceTestMode สำหรับหน้า /Game ทั้งหมด (origin เดียวกัน)">
|
||
<span class="sw"></span><span>Test Mode (debug)</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="note">
|
||
<b>มินิเกมเริ่มได้ 2 ทาง — เลือกให้ตรงกับสิ่งที่จะเทส:</b><br>
|
||
• <b>โหมด A พรีวิว</b> = เปิดมินิเกมเดี่ยวพร้อมบอททันที (บอทจำลองฝั่ง host) → เหมาะเช็ค <b>ภาพ / texture / gameplay / UI</b> เร็วๆ ทุกเกมใช้ได้<br>
|
||
• <b>โหมด B ห้องจริง LobbyA</b> = flow จริง host→case→มินิเกม (บอทจาก <b>server</b>) → เป็นทางเดียวที่เทส <b>Server-Authority + Disconnect/Takeover</b> ได้จริง
|
||
</div>
|
||
|
||
<!-- ===================== โหมด A ===================== -->
|
||
<h2>โหมด A · พรีวิวมินิเกม (เช็คภาพ/gameplay) <span class="tag">บอทฝั่ง host</span></h2>
|
||
<div class="grid" id="mgGrid"></div>
|
||
|
||
<!-- ===================== โหมด B ===================== -->
|
||
<h2>โหมด B · เทส Server-Authority + Disconnect (ห้องจริง) <span class="tag">บอทฝั่ง server</span></h2>
|
||
<div class="panel">
|
||
<div class="field">
|
||
<label>จำนวนผู้เล่นจริง (แท็บ)</label>
|
||
<input type="number" id="humanCount" value="2" min="1" max="6">
|
||
<span style="font-size:12px;color:var(--mut)">เปิดหลายแท็บ = จำลองหลายคน (ทดสอบ disconnect)</span>
|
||
</div>
|
||
<div class="field">
|
||
<label>จำนวนบอท (server)</label>
|
||
<input type="number" id="botCount" value="4" min="0" max="6">
|
||
<span style="font-size:12px;color:var(--mut)">คน + บอท ≤ 6 (LobbyA cap)</span>
|
||
</div>
|
||
<div class="field">
|
||
<button class="btn primary" id="makeRoom">🎮 สร้างห้อง LobbyA + บอท แล้วเปิดแท็บ</button>
|
||
<button class="btn v" id="addTab" disabled>+ เปิดแท็บผู้เล่นเพิ่ม</button>
|
||
</div>
|
||
<div id="status"></div>
|
||
<div class="links" id="roomLinks"></div>
|
||
|
||
<ol class="steps">
|
||
<li>กด <b>สร้างห้อง</b> → เปิดแท็บ LobbyA ตามจำนวนผู้เล่น (อนุญาต popup ถ้าเบราว์เซอร์ถาม)</li>
|
||
<li>ในแท็บ host เดินตัวละครไป <b>โซนเริ่มเกมสีส้ม</b> → เกมจะสุ่ม case แล้วเข้ามินิเกม (บอทมาจาก server)</li>
|
||
<li>ระหว่างเล่นมินิเกม <b>ปิดแท็บผู้เล่น 1 แท็บ</b> เพื่อจำลอง disconnect → สังเกตว่าตัวนั้นกลายเป็น <b>"(บอท)"</b> เล่นต่อโดย server ไม่ค้าง/ไม่หาย</li>
|
||
<li>เปิดแท็บนั้นใหม่ (ลิงก์เดิม) ภายในเวลาที่กำหนด → ผู้เล่นควร <b>กลับเข้ามาแทนบอท</b> ตำแหน่ง/คะแนนเดิม</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<!-- ===================== Checklist ===================== -->
|
||
<h2>เช็กลิสต์ที่ต้องผ่านทุกเกม (Server-Authority + Disconnect)</h2>
|
||
<div class="panel chk">
|
||
<ul>
|
||
<li><b>บอทเดินจาก server:</b> ปิดแท็บ host แล้วบอทยัง <b>เคลื่อนไหว/ทำคะแนนต่อ</b> (ไม่ใช่ host จำลอง)</li>
|
||
<li><b>Host หลุด ≠ เกมหยุด:</b> host ออกกลางเกม เกมยังดำเนินต่อสำหรับคนอื่น</li>
|
||
<li><b>Takeover:</b> คนหลุด → กลายเป็นบอทชื่อ <code>(บอท)</code> สี/ตำแหน่งเดิม ไม่หายวับ</li>
|
||
<li><b>Reconnect:</b> เปิดลิงก์เดิมกลับมา → แทนที่บอท takeover (ไม่เกิดตัวซ้ำ)</li>
|
||
<li><b>คะแนน/เกรด:</b> สรุปผลรวมบอท ตรงกันทุกจอ (server เป็นแหล่งเดียว)</li>
|
||
<li><b>ไม่กระทบกันข้ามเกม:</b> เล่นเกมอื่นต่อในห้องเดิม สถานะเกมก่อนหน้าไม่รั่ว</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="foot">
|
||
สถานะ: <b style="color:var(--green)">MG1 quiz</b> · <b style="color:var(--green)">MG2 gauntlet</b> = server-auth แล้ว ·
|
||
<b style="color:var(--amber)">MG3–MG7</b> = กำลังทยอยย้าย · เครื่องมือนี้ไม่แก้ logic เกม (เปิดห้องผ่าน API จริงเท่านั้น)
|
||
</div>
|
||
|
||
</div>
|
||
<script>
|
||
(function(){
|
||
var GAME = '/Game';
|
||
var LOBBYA_MAP = 'mlsbbxfe';
|
||
var MGS = [
|
||
{ mg:'MG1', name:'Quiz', th:'ตอบคำถาม', map:'mng8a80o', max:8, auth:'srv' },
|
||
{ mg:'MG2', name:'Gauntlet', th:'วิ่งหลบอุปสรรค (มงกุฎ)', map:'mno9kb07', max:6, auth:'srv' },
|
||
{ mg:'MG3', name:'Stack Tower', th:'ต่อหอคอย', map:'mnn93hpi', max:8, auth:'host' },
|
||
{ mg:'MG4', name:'Quiz Carry', th:'แบกตอบคำถาม', map:'mnorwqx1', max:8, auth:'host' },
|
||
{ mg:'MG5', name:'Jump Survive', th:'กระโดดเอาตัวรอด', map:'mnptfts2', max:8, auth:'host' },
|
||
{ mg:'MG6', name:'Space Shooter', th:'ยิงอวกาศ', map:'mnpz6rkp', max:6, auth:'host' },
|
||
{ mg:'MG7', name:'Balloon Boss', th:'บอสลูกโป่ง (Mega Virus)', map:'mnq1eml7', max:6, auth:'host' }
|
||
];
|
||
var AUTH_LABEL = { srv:'server-auth ✓', host:'host-sim (ยังไม่ย้าย)', test:'กำลังเทส' };
|
||
|
||
function rnick(p){ return (p||'เทส') + Math.floor(Math.random()*9000+1000); }
|
||
function setStatus(t, color){ var s=document.getElementById('status'); s.textContent=t; s.style.color=color||'var(--cyan)'; }
|
||
|
||
// ---- สร้างห้องผ่าน API จริง ----
|
||
function createSpace(opts){
|
||
// spaceId ฝั่ง server = ชื่อห้อง → ใส่ suffix สุ่มกันชนกันข้ามผู้เทส
|
||
var uname = (opts.name || 'test') + '-' + Math.random().toString(36).slice(2,8);
|
||
return fetch(GAME + '/api/spaces', {
|
||
method:'POST', headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({
|
||
mapId: opts.mapId, isPrivate:true, name: uname,
|
||
maxPlayers: opts.maxPlayers || 6,
|
||
botSlotCount: (opts.botSlotCount != null ? opts.botSlotCount : 0)
|
||
})
|
||
}).then(function(r){ return r.json(); });
|
||
}
|
||
|
||
// ---- โหมด A: พรีวิว ----
|
||
function openPreview(mg){
|
||
setStatus('กำลังสร้างห้องพรีวิว ' + mg.mg + '…');
|
||
createSpace({ mapId: mg.map, name:'preview', maxPlayers: mg.max, botSlotCount:0 })
|
||
.then(function(d){
|
||
if (!d || !d.ok || !d.spaceId){ setStatus('สร้างห้องไม่ได้: ' + ((d&&d.error)||'?'),'var(--red)'); return; }
|
||
var q = GAME + '/play.html?space=' + encodeURIComponent(d.spaceId)
|
||
+ '&nick=' + encodeURIComponent(rnick('พรีวิว'))
|
||
+ '&map=' + encodeURIComponent(mg.map)
|
||
+ '&preview=1&defaultChar=1&editorEmbed=1&_h=' + Date.now();
|
||
window.open(q, '_blank', 'noopener');
|
||
setStatus('เปิดพรีวิว ' + mg.mg + ' แล้ว (แท็บใหม่)','var(--green)');
|
||
})
|
||
.catch(function(){ setStatus('สร้างห้องไม่ได้ (network)','var(--red)'); });
|
||
}
|
||
|
||
// ---- โหมด B: ห้องจริง LobbyA + บอท ----
|
||
var currentRoom = null; // { spaceId, opened }
|
||
function lobbyUrl(spaceId, nick){
|
||
return GAME + '/room-lobby.html?space=' + encodeURIComponent(spaceId)
|
||
+ '&nick=' + encodeURIComponent(nick) + '&map=' + encodeURIComponent(LOBBYA_MAP);
|
||
}
|
||
function makeRoom(){
|
||
var humans = Math.max(1, Math.min(6, parseInt(document.getElementById('humanCount').value,10)||2));
|
||
var bots = Math.max(0, Math.min(6, parseInt(document.getElementById('botCount').value,10)||0));
|
||
setStatus('กำลังสร้างห้อง LobbyA (บอท ' + bots + ')…');
|
||
createSpace({ mapId: LOBBYA_MAP, name:'test-lobby', maxPlayers: 6, botSlotCount: bots })
|
||
.then(function(d){
|
||
if (!d || !d.ok || !d.spaceId){ setStatus('สร้างห้องไม่ได้: ' + ((d&&d.error)||'?'),'var(--red)'); return; }
|
||
currentRoom = { spaceId: d.spaceId, opened: 0 };
|
||
document.getElementById('addTab').disabled = false;
|
||
renderLinks(humans);
|
||
for (var i=0;i<humans;i++) openTab(i===0 ? 'host' : null);
|
||
setStatus('สร้างห้องแล้ว · spaceId ' + d.spaceId + ' · เปิด ' + humans + ' แท็บ (ถ้า popup ถูกบล็อก ใช้ลิงก์ด้านล่าง)','var(--green)');
|
||
})
|
||
.catch(function(){ setStatus('สร้างห้องไม่ได้ (network)','var(--red)'); });
|
||
}
|
||
function openTab(role){
|
||
if (!currentRoom) return;
|
||
currentRoom.opened++;
|
||
var nick = rnick(role==='host' ? 'HOST' : 'P');
|
||
window.open(lobbyUrl(currentRoom.spaceId, nick), '_blank', 'noopener');
|
||
}
|
||
function renderLinks(humans){
|
||
var box = document.getElementById('roomLinks'); box.innerHTML='';
|
||
if (!currentRoom) return;
|
||
for (var i=0;i<humans;i++){
|
||
var a = document.createElement('a');
|
||
a.className = 'btn'; a.target='_blank'; a.rel='noopener';
|
||
a.href = lobbyUrl(currentRoom.spaceId, (i===0?'HOST':'P') + (1000+i));
|
||
a.textContent = (i===0?'แท็บ host':'แท็บผู้เล่น ' + (i+1));
|
||
box.appendChild(a);
|
||
}
|
||
}
|
||
|
||
// ---- render การ์ดมินิเกม ----
|
||
(function(){
|
||
var grid = document.getElementById('mgGrid');
|
||
MGS.forEach(function(mg){
|
||
var c = document.createElement('div'); c.className='mg';
|
||
c.innerHTML =
|
||
'<div class="hd"><div class="nm">' + mg.mg + ' · ' + mg.name + '<small>' + mg.th + '</small></div>'
|
||
+ '<span class="auth ' + mg.auth + '" title="'+AUTH_LABEL[mg.auth]+'">' + AUTH_LABEL[mg.auth] + '</span></div>'
|
||
+ '<div class="mid">map: ' + mg.map + '</div>'
|
||
+ '<div class="mgrow"><button class="btn amber" data-prev="1">▶ พรีวิวเล่น</button></div>';
|
||
c.querySelector('[data-prev]').addEventListener('click', function(){ openPreview(mg); });
|
||
grid.appendChild(c);
|
||
});
|
||
})();
|
||
|
||
document.getElementById('makeRoom').addEventListener('click', makeRoom);
|
||
document.getElementById('addTab').addEventListener('click', function(){ openTab(null); });
|
||
|
||
// ---- Test Mode toggle (localStorage justiceTestMode) ----
|
||
var tm = document.getElementById('tmToggle');
|
||
function syncTm(){ var on=false; try{ on = localStorage.getItem('justiceTestMode')==='1'; }catch(e){} tm.classList.toggle('on', on); }
|
||
tm.addEventListener('click', function(){
|
||
var on=false; try{ on = localStorage.getItem('justiceTestMode')==='1'; localStorage.setItem('justiceTestMode', on?'0':'1'); }catch(e){}
|
||
syncTm();
|
||
});
|
||
syncTm();
|
||
})();
|
||
</script>
|
||
</body></html>
|