change game to be server host on mg 2 3 4 5 6 7

This commit is contained in:
2026-06-28 05:20:00 +00:00
parent d599c5c72d
commit 6bc2ccea19
12 changed files with 51713 additions and 86 deletions
+3
View File
@@ -204,6 +204,9 @@ tr:last-child td{border-bottom:none}
</style></head>
<body><div class="wrap">
<h1>Justice — Test Case / Bug List<span class="sub">รายการบัก + ผลตรวจ QA · จัดหมวด · srv1361159.hstgr.cloud/testcase</span></h1>
<div class="toolbar" style="margin:10px 0 4px">
<a class="btn v" href="test.html" style="text-decoration:none">🎮 Test Console — เทสมินิเกม + Disconnect (ปุ่มเดียว)</a>
</div>
<div class="cards" id="cards"></div>
<div class="bar">
<input type="text" id="q" placeholder="ค้นหา… (ชื่อ / รายละเอียด / คำอธิบาย)">
+242
View File
@@ -0,0 +1,242 @@
<!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>