Files
justice/www/html/testcase/test.html
T

243 lines
17 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>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)">MG3MG7</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>