update mg quest and mobile some path

This commit is contained in:
2026-08-03 03:25:36 +00:00
parent 1f5f1ee321
commit c73b27e89f
7 changed files with 6681 additions and 5787 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+337 -37
View File
@@ -2371,6 +2371,47 @@
ctx.restore();
}
/* ===== [2026-07-31] (/) =====
* เกมสวนใหญ fit แบบ contain จอทดสวนไมตรงแมปจะเหลอขอบส = เลนร "ไม่เต็มจอ"
* แกโดยปภาพพนหลงตวเดยวกนแบบ cover + เบลอ + หร ไว "ใต้" ฉากจร
* ามเอาภาพ cover ไปแทนฉากจร ฉากตองลอกกบกรดโลก (งานจดทางเด QB 17 รอบอยบนสมมตฐานน)
* วนเปนแคฉากหลงทจะถกฉากจรงทบทงหมดในพนทของโลก งไมกระทบพดใด
* perf: เรนเดอรครงเดยวเกบใส canvas สำรอง (เบลอทกเฟรมกนแรงเกนไปสำหรบมอถ)
* ขอบเขต: เฉพาะ pointer: coarse บน PC ภาพยงเปนแบบเดมเป */
let _bleedCache = null;
let _bleedCoarse = null;
function playIsCoarsePointer() {
if (_bleedCoarse != null) return _bleedCoarse;
try { _bleedCoarse = !!(window.matchMedia && window.matchMedia('(pointer: coarse)').matches); }
catch (e) { _bleedCoarse = false; }
return _bleedCoarse;
}
function drawBleedBackdropPlay(ctx2, img, cw, ch, fallbackColor) {
ctx2.fillStyle = fallbackColor;
ctx2.fillRect(0, 0, cw, ch);
if (!playIsCoarsePointer()) return;
if (!img || !img.complete || !img.naturalWidth) return;
const key = (img.currentSrc || img.src || '') + '|' + cw + 'x' + ch;
if (!_bleedCache || _bleedCache.key !== key) {
try {
const off = document.createElement('canvas');
off.width = Math.max(1, Math.round(cw));
off.height = Math.max(1, Math.round(ch));
const octx = off.getContext('2d');
const s = Math.max(cw / img.naturalWidth, ch / img.naturalHeight);
const dw = img.naturalWidth * s;
const dh = img.naturalHeight * s;
try { octx.filter = 'blur(18px) brightness(0.42) saturate(0.85)'; } catch (e) { /* เบราว์เซอร์ไม่รองรับ filter */ }
octx.drawImage(img, (cw - dw) / 2, (ch - dh) / 2, dw, dh);
try { octx.filter = 'none'; } catch (e) { /* ignore */ }
octx.fillStyle = 'rgba(10, 14, 34, 0.35)';
octx.fillRect(0, 0, off.width, off.height);
_bleedCache = { key, canvas: off };
} catch (e) { _bleedCache = null; return; }
}
if (_bleedCache) ctx2.drawImage(_bleedCache.canvas, 0, 0, cw, ch);
}
/** กล้องมองเหนือขอบบนแมป (worldMinY<0) + ใช้รูปแผนที่ — ยัด loop ท้องฟ้าเติมครึ่งบนจอ */
function drawStackTowerLoopSkyFillAboveMap(ctx, worldMinY, zDraw, cw, ch) {
if (!isStackTowerMissionUiMapPlay() || worldMinY >= 0) return;
@@ -6781,9 +6822,24 @@
zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96;
} else if (isJumpSurvive()) {
/* MG5 (jump_survive) zoom 1.4 ( fit = -)
fit ความกวางเตมจอ (vertical scroller เลอนแนวตงดวย jumpSurviveCamCenterY อยแล) */
fit ความกวางเตมจอ (vertical scroller เลอนแนวตงดวย jumpSurviveCamCenterY อยแล)
[2026-08-02] user: "mg5 ในมือถือเห็นไม่เท่า pc 1920×1080 · ควรเห็นทั้งเกม"
ตรเด `zDraw = cw / mwPx` fit **แคแนวนอน** แนวตงเหนเทาไรขนกบความสงจอลวน
จอเตยจงเหนแทนขางบน/างลางนอยกว PC (ดแลวเหล ~81% ของ PC และแยลงอกเมอแถบ URL กางอย)
แกแบบเดยวก lobby/MG4: บนจอสมผสใหเห "โลกแนวตั้ง" ไมอยกวาทเดสกอป 1920×1080 เห
zRef = 1920/mwPx · โลกแนวตงท PC เห = 1080/zRef
zDraw = min(cw/mwPx, ch / โลกแนวตงท PC เห)
ไดงความกวางเตมแมป และแนวตงไมอยกว PC (จอกวางมากจะเหลอขอบซายขวาแทน งดกวามองไมเหนแท)
PC ไมกระทบ pointer: fine นสตรเดมเป */
const mwPx = w * tileSize;
if (mwPx > 0) zDraw = canvas.width / mwPx;
if (mwPx > 0) {
zDraw = canvas.width / mwPx;
if (playIsCoarsePointer()) {
const zRef = JS_REF_VIEW_W / mwPx;
const refVertWorld = JS_REF_VIEW_H / zRef; /* โลกแนวตั้งที่เดสก์ท็อป 1920×1080 เห็น */
if (refVertWorld > 0) zDraw = Math.min(zDraw, canvas.height / refVertWorld);
}
}
} else if (isQuizQuestionMissionRenderLockPlay()) {
const qmc = getQuizQuestionMissionMapCenterWorldPxPlay();
if (qmc) {
@@ -6793,9 +6849,16 @@
} else if (isQuizCarry()) {
const mwPx = w * tileSize;
const mhPx = h * tileSize;
zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96;
zDraw = quizCarryEffectiveZoomPlay(mwPx, mhPx, canvas.width, canvas.height);
} else if (isQuizBattle()) {
zDraw *= quizBattleUserZoomMul;
/* [2026-08-02] Quiz Battle ** fit ** ( × ) PC
ดได: iPhone 13 Pro แนวนอน เห 67% กวาง · 64% ของเดสกอป 1920×1080
แกแบบเดยวก lobby/MG5: จอสมผสคณดวย min(1, cw/1920, ch/1080) เหนไมอยกว PC
PC ไดวค 1 พอด = ไมขย · และไมแตะ quizBattleUserZoomMul (เลนยงบบซมเองได) */
if (playIsCoarsePointer() && canvas.width > 0 && canvas.height > 0) {
zDraw *= Math.min(1, canvas.width / QB_REF_VIEW_W, canvas.height / QB_REF_VIEW_H);
}
} else if (isGauntletCrownHeistMapPlay() && !(previewMode && editorEmbedReturn)) {
/* #1 map fit: gauntlet fit " ()" resolution-adaptive
(เดมใช zoom ไมปรบตาม resolution = ไมพอด) · fit งอยางเดยว เพราะ runway เลอนแนวนอน */
@@ -9373,11 +9436,51 @@
return !!(isDetectiveMinigamePlay() && quizCarryUseMissionSummaryOverlay());
}
/** quiz_carry: จุดกลางแมป (world px) — กล้องไม่ตามตัวละคร; ใช้ร่วม draw + overlay DOM */
/* ===== [2026-08-02] MG4 1920×1080 =====
ญหา (user: "mg4 อ่านไม่รู้เรื่อง"): ายคำตอบ **วาดบน canvas** และฟอนตนลางตายต 10px
(`drawQuizCarryChoiceLabels`: `Math.max(10, Math.min(24, ts * 0.24 * ps))`)
กลองปายกวางตามความกวางขอความ พอฟอนตดพ ายคางขนาดเดมในหนวยพกเซลจอ
แตแมปหดลงตาม zoom **ายใหญเกนแมปจนทบก**
ดจร: เดสกอป ts29px (ฟอนตควรได 9.0 ใช 10 = เกนนดเดยว)
iPhone 13 Pro แนวนอน ts9.5px (ควรได 2.8 ใช 10 = **เก 3.6 เท**) · แนวต เก 4.3 เท
แก (ทางเลอก B user เคาะ): จอสมผสใช zoom ไมำกวาทเดสกอป 1920×1080 ใช
ตราสวน "ป้ายต่อช่อง" เทาก PC เป = ไม และอานออก · แลกกบเหนแมปแคบลงตองเดนด
PC ไมกระทบเลย `pointer: fine` นค fit เดมทกกรณ */
const QUIZ_CARRY_REF_VIEW_W = 1920;
const QUIZ_CARRY_REF_VIEW_H = 1080;
/* จอเดสก์ท็อปอ้างอิงของ MG5 — ใช้เป็นเกณฑ์ "ต้องเห็นไม่น้อยกว่านี้" บนจอสัมผัส (ดู computePlayCameraZDrawPlay) */
const JS_REF_VIEW_W = 1920;
const JS_REF_VIEW_H = 1080;
/* เกณฑ์เดียวกันสำหรับ Quiz Battle */
const QB_REF_VIEW_W = 1920;
const QB_REF_VIEW_H = 1080;
function quizCarryFitZoomPlay(mwPx, mhPx, cw, ch) {
return Math.min(cw / mwPx, ch / mhPx) * 0.96;
}
function quizCarryEffectiveZoomPlay(mwPx, mhPx, cw, ch) {
const fit = quizCarryFitZoomPlay(mwPx, mhPx, cw, ch);
if (!playIsCoarsePointer()) return fit;
const ref = quizCarryFitZoomPlay(mwPx, mhPx, QUIZ_CARRY_REF_VIEW_W, QUIZ_CARRY_REF_VIEW_H);
return Math.max(fit, ref);
}
function getQuizCarryMapCameraWorldCenterPxPlay() {
if (!mapData || mapData.gameType !== 'quiz_carry') return null;
const ww = mapData.width || 20, hh = mapData.height || 15;
const ts = tileSize;
return { cx: ww * ts * 0.5, cy: hh * ts * 0.5 };
const mwPx = ww * ts, mhPx = hh * ts;
const center = { cx: mwPx * 0.5, cy: mhPx * 0.5 };
if (!canvas || !canvas.width || !canvas.height) return center;
const fit = quizCarryFitZoomPlay(mwPx, mhPx, canvas.width, canvas.height);
const zd = quizCarryEffectiveZoomPlay(mwPx, mhPx, canvas.width, canvas.height);
if (zd <= fit + 1e-6) return center; /* แมปยังพอดีจอ → กลางแมปเหมือนเดิมเป๊ะ */
/* ซูมเข้าจนแมปไม่พอดีแล้ว → กล้องตามตัวเรา แต่ห้ามเลื่อนจนเห็นนอกขอบแมป */
const halfW = canvas.width / (2 * zd);
const halfH = canvas.height / (2 * zd);
const px = ((me && typeof me.x === 'number') ? me.x : ww * 0.5) * ts;
const py = ((me && typeof me.y === 'number') ? me.y : hh * 0.5) * ts;
const clamp = (v, half, span) => (half * 2 >= span ? span * 0.5 : Math.max(half, Math.min(span - half, v)));
return { cx: clamp(px, halfW, mwPx), cy: clamp(py, halfH, mhPx) };
}
function isQuizBattle() { return mapData && mapData.gameType === 'quiz_battle'; }
@@ -17793,6 +17896,11 @@
if (keys['ArrowRight'] || keys['KeyD']) vx += 1;
if (keys['ArrowUp'] || keys['KeyW']) vy -= 1;
if (keys['ArrowDown'] || keys['KeyS']) vy += 1;
/* virtual joystick (จอสัมผัส) — บังคับยานเหมือน WASD · clamp ±1 กันกดคีย์+จอยพร้อมกันแล้วเร็วเป็นสองเท่า */
if (shouldShowVirtualJoystickPlay()) {
vx = Math.max(-1, Math.min(1, vx + (Number(quizQuestionMissionJoyVecX) || 0)));
vy = Math.max(-1, Math.min(1, vy + (Number(quizQuestionMissionJoyVecY) || 0)));
}
}
const moveSpd = 280;
const canPilotMe = !(isSpaceShooterMissionUiMapPlay() && me.spaceShooterEliminated);
@@ -20281,8 +20389,9 @@
if (jq && jumpSurviveOnGround && window.jdSound) { try { window.jdSound.playSfx('jump', 'mg5'); } catch (e) { /* ignore */ } }
if (jq && jumpSurviveOnGround) { try { if (socket && myId != null) socket.emit('js-jump'); } catch (e) { /* ignore */ } } /* broadcast กระโดด → peer ได้ยินเสียง (mg5 ไม่มีสัญญาณ jump แยกใน sync) */
/* virtual joystick (touch) — แกน x ป้อนซ้าย/ขวาเหมือนคีย์ A/D (deadzone จัดการในตัว update แล้ว) */
const joyX = isJumpSurviveMissionHudActivePlay() ? (Number(quizQuestionMissionJoyVecX) || 0) : 0;
/* virtual joystick (touch) x / A/D (deadzone update )
MG5 ไมใชแกน y: กระโดดอยมกระโดดมมขวาลางตามเด */
const joyX = shouldShowVirtualJoystickPlay() ? (Number(quizQuestionMissionJoyVecX) || 0) : 0;
const moveLeftIn = !!(keys['ArrowLeft'] || keys['KeyA']) || joyX < -0.001;
const moveRightIn = !!(keys['ArrowRight'] || keys['KeyD']) || joyX > 0.001;
@@ -25181,6 +25290,77 @@
});
}
/** [2026-08-01] ""
(การแสดงผลจรงถกกรองอกชนดวย @media (pointer: coarse) ใน play.html PC ไมเห)
ไมเอา: MG2 gauntlet (งอตโนม · แตมกระโดด) · MG3 stack (ไมเดนเลย)
· MG7 balloon_boss (ยานลอยตามลม **ไมนพตทศทางในโคดเลย** แตมย) · frogger (กดทละชอง) */
function playSceneUsesWalkJoystick() {
if (!mapData) return false;
if (isGauntlet() || isStack() || isBalloonBoss() || isFrogger()) return false;
return isQuiz() || isQuizCarry() || isJumpSurvive() || isSpaceShooter() || isQuizBattle() || isLobby();
}
/** ""
ไมนจะไดจอยทกดแลวตวไมขย (เฟส howto/บถอยหล/านคำถาม/อปอพคำถาม QB) */
/** [2026-08-02] MG5/MG6 "" ( )
นขอความทจะขนบนปาย · นคาวางถายงเลนอย
เหตผล: เดมไมอะไรบอกวาตายแล เลนอานว "ปุ่มเสีย" (user แจงมาเองท MG5 และ MG6)
ดจร MG6: ยานถกทำลายทนาท 5.5-6.2 · เกมจร 2-6 คน องนงรอจนหมดเวลา ~83 นาท */
function meEliminatedNoticePlay() {
if (isJumpSurvive() && isJumpSurviveMissionHudActivePlay() && jumpSurviveEliminated) {
return 'ตกรอบแล้ว — รอเพื่อนเล่นให้จบรอบ';
}
if (isSpaceShooter() && isSpaceShooterMissionHudActivePlay() && me && me.spaceShooterEliminated) {
return 'ยานถูกทำลาย — รอรอบจบ';
}
return '';
}
/** ป้าย "ตกรอบแล้ว" + หรี่ปุ่มแอ็กชันให้เข้าชุดกับจอย (จอยหรี่เองผ่าน shouldShowVirtualJoystickPlay) */
function syncEliminatedBannerPlay() {
const txt = meEliminatedNoticePlay();
const on = !!txt;
const el = document.getElementById('play-eliminated-banner');
if (el) {
el.classList.toggle('is-on', on);
el.setAttribute('aria-hidden', on ? 'false' : 'true');
if (on) {
const t = document.getElementById('play-eliminated-text');
if (t && t.textContent !== txt) t.textContent = txt;
}
}
/* MG5 มีปุ่มกระโดดฝั่งขวา — ตกรอบแล้วต้องหรี่+กดไม่ติดเหมือนจอย (MG6 ยิงอัตโนมัติ ไม่มีปุ่ม) */
const jb = document.getElementById('gauntlet-crown-jump-btn');
if (jb) jb.classList.toggle('is-inert', on);
}
function shouldShowVirtualJoystickPlay() {
if (!playSceneUsesWalkJoystick()) return false;
if (isChatFocused()) return false;
if (meEliminatedNoticePlay()) return false; /* ตกรอบแล้ว → จอยหรี่ กดไม่ติด */
if (isQuizCarryEmbedLobbyBlockingMovement() || isQuizCarryEmbedCountdownBlockingMovement()) return false;
if (isQuizBattleQuestionBlockingMovementPlay()) return false;
if (isQuizCarry() && quizCarryReadFreezeActive()) return false;
if (isQuiz() && isQuizQuestionMissionUiMapPlay()) return isQuizQuestionMissionHudActivePlay();
if (isJumpSurvive() && isJumpSurviveMissionUiMapPlay()) return isJumpSurviveMissionHudActivePlay();
if (isSpaceShooter() && isSpaceShooterMissionUiMapPlay()) return isSpaceShooterMissionHudActivePlay();
return true;
}
/** url ของรูปจอยที่โหลดไม่ขึ้น — จำไว้ถาวรเพื่อ **กันลูป 404 ทุกเฟรม** (sync จะไปหยิบ url เดิมซ้ำไม่รู้จบ) */
const virtualJoystickFailedImgUrls = Object.create(null);
/** `btn-joystick-1/2.png` (md5 7 2026-08-01)
ฉากทไมสก mock (Quiz Battle / lobby map) ใชของ /img/QUESTION/ */
function virtualJoystickAssetUrlPlay(file) {
const fb = questionMissionAssetUrl(file);
let url = fb;
try {
if (isMissionMockHudPlay()) url = missionMockHudAssetUrl(file);
} catch (e) { /* ignore */ }
return virtualJoystickFailedImgUrls[url] ? fb : url;
}
function quizQuestionMissionJoystickResetVisual() {
quizQuestionMissionJoyVecX = 0;
quizQuestionMissionJoyVecY = 0;
@@ -25217,25 +25397,33 @@
knob.style.transform = 'translate(calc(-50% + ' + dx + 'px), calc(-50% + ' + dy + 'px))';
}
function syncQuizQuestionMissionJoystickPlay() {
function syncVirtualJoystickPlay() {
const root = document.getElementById('quiz-question-mission-joystick');
if (!root) return;
const jumperOn = isJumpSurviveMissionHudActivePlay();
const on = !!((isQuizQuestionMissionHudActivePlay() || jumperOn) && !isChatFocused());
root.classList.toggle('is-hidden', !on);
/* [2026-08-01] 2 :
อนสน (.is-hidden) = ฉากนไมการบงคบทศเลย (MG2/MG3/MG7)
หร+กดไมได (.is-inert) = ฉากนเดนได แต "ตอนนี้" เกมบลอกการเดนชวคราว
เหต: user แจ QB/MG4 "ขยับไม่ค่อยได้" นเหตอเดมพอบลอกแล **จอยหายไปทงอ**
(QB = อปอพคำถามเป · MG4 = เฟสอานคำถามท freeze กคน) วทวางอยงหล pointer capture
องยกนวแลวกดใหมกรอบ = กเหมอนคมไมได. คงปมไวแตหร = เหนว "ยังอยู่ แค่ยังเดินไม่ได้" */
const sceneOk = playSceneUsesWalkJoystick();
const on = sceneOk && shouldShowVirtualJoystickPlay();
root.classList.toggle('is-hidden', !sceneOk);
root.classList.toggle('is-inert', sceneOk && !on);
root.setAttribute('aria-hidden', on ? 'false' : 'true');
if (on) {
/* สลับรูป joystick ตามฉาก: jumper → /img/Jumper/, quiz คำถาม → /img/QUESTION/ (เปลี่ยน src เฉพาะตอน skin เปลี่ยน) */
const skin = jumperOn ? 'jumper' : 'question';
/* สลับรูป joystick ตามสกินของฉาก (เปลี่ยน src เฉพาะตอน url เปลี่ยน — ไม่ยิงโหลดซ้ำทุกเฟรม) */
const bgUrl = virtualJoystickAssetUrlPlay('btn-joystick-1.png');
const knobUrl = virtualJoystickAssetUrlPlay('btn-joystick-2.png');
const bg = document.getElementById('quiz-q-mission-joystick-bg');
const knobImg = document.getElementById('quiz-q-mission-joystick-knob-img');
if (bg && bg.dataset.joySkin !== skin) {
bg.src = jumperOn ? jumperAssetUrl('btn-joystick-1.png') : questionMissionAssetUrl('btn-joystick-1.png');
bg.dataset.joySkin = skin;
if (bg && bg.dataset.joyUrl !== bgUrl) {
bg.dataset.joyUrl = bgUrl;
bg.src = bgUrl;
}
if (knobImg && knobImg.dataset.joySkin !== skin) {
knobImg.src = jumperOn ? jumperAssetUrl('btn-joystick-2.png') : questionMissionAssetUrl('btn-joystick-2.png');
knobImg.dataset.joySkin = skin;
if (knobImg && knobImg.dataset.joyUrl !== knobUrl) {
knobImg.dataset.joyUrl = knobUrl;
knobImg.src = knobUrl;
}
}
if (!on) {
@@ -26073,6 +26261,21 @@
}
/* Stack Tower (mnn93hpi): ไม่เลื่อนกล้องตาม floorWorldY — ใช้ cam จาก getStackCameraCentersPx() เหมือน Stack ทั่วไป (วางบล็อกแล้วจอไม่ไหล) */
lastPlayZDrawForInput = zDraw;
/* [2026-08-02] driver ( )
จำเปนเพราะว "โลกที่มองเห็นกี่ช่อง" จากภาพไมได ฉากทเตมจอจะคนขนาดแมปเสมอ ไมาซมเทาไร
(เจอมาแล: scene-view-parity-check MG5 ได 35.0×20.0 = ขนาดแมปเปะทงมอถอและ PC = านไมออกวาซมตางก) */
try {
window.__jdPlayView = {
gameType: (mapData && mapData.gameType) || '',
zDraw,
tileSize,
cw: canvas.width,
ch: canvas.height,
tilesW: canvas.width / (tileSize * zDraw),
tilesH: canvas.height / (tileSize * zDraw),
coarse: playIsCoarsePointer(),
};
} catch (e) { /* ignore */ }
const stackTowerWorldScrollScreenY = getStackTowerWorldLayerScrollScreenOffsetYPlay(zDraw);
const halfW = canvas.width / (2 * zDraw);
const halfH = canvas.height / (2 * zDraw);
@@ -26122,8 +26325,10 @@
ctx.drawImage(mapBackgroundImg, (canvas.width - _bbDw) / 2, (canvas.height - _bbDh) / 2, _bbDw, _bbDh);
} else if (mapBackgroundImg && mapBackgroundImg.complete && mapBackgroundImg.naturalWidth) {
/* กล้องเห็นพื้นที่นอก [0,map] ได้ — ห้ามสุ่มต้นทาง drawImage เกินขอบภาพ (จะเกิดซ้ำ/เส้นตัด/ซ้อนกันที่ขอบจอ) */
ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26';
ctx.fillRect(0, 0, canvas.width, canvas.height);
/* [2026-07-31] fillRect ()
ขอบวางจากการ fit แบบ contain ไมเปนแถบทบอ · ฉากจรงวาดทบดานลางนตามพดโลกเหมอนเด */
drawBleedBackdropPlay(ctx, mapBackgroundImg, canvas.width, canvas.height,
(isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26');
drawStackTowerLoopSkyFillAboveMap(ctx, worldMinY, zDraw, canvas.width, canvas.height);
const natW = mapBackgroundImg.naturalWidth;
const natH = mapBackgroundImg.naturalHeight;
@@ -27081,7 +27286,8 @@
}
drawQuizTfScorePopupsLayer(ctx, worldToScreen, zDraw, timeMs);
syncPlayCyberHud();
syncQuizQuestionMissionJoystickPlay();
syncVirtualJoystickPlay();
syncEliminatedBannerPlay();
syncQuizCarryGrabButton();
syncGauntletCrownJumpButton();
syncStackTowerDropButtonPlay();
@@ -27538,9 +27744,11 @@
if (keys['ArrowRight'] || keys['KeyD']) kx += 1;
if (keys['ArrowUp'] || keys['KeyW']) ky -= 1;
if (keys['ArrowDown'] || keys['KeyS']) ky += 1;
/* virtual joystick () WASD
(MG1 quiz · MG4 quiz_carry · Quiz Battle · lobby map) */
let jx = 0;
let jy = 0;
if (isQuizQuestionMissionHudActivePlay()) {
if (shouldShowVirtualJoystickPlay()) {
jx = quizQuestionMissionJoyVecX;
jy = quizQuestionMissionJoyVecY;
}
@@ -27561,7 +27769,17 @@
const antiStuckGame = !pathStrict && (isQuiz() || isQuizCarry());
let normalMoved = false;
if (pathStrict) {
const moved = quizBattleTryStepMovePlay(me.x, me.y, tox, toy, (nx, ny, ox, oy) => canWalkLikeLobby(nx, ny, ox, oy));
const canWalkQb = (nx, ny, ox, oy) => canWalkLikeLobby(nx, ny, ox, oy);
const moved = quizBattleTryStepMovePlay(me.x, me.y, tox, toy, canWalkQb);
/* [2026-08-01] ** "" **
เหตผลทอยากใส: าวทแยงทชนขอบเลนตนทงกาว (สาขานไม fallback รายแกนเหมอนฉากอ)
เลน QB 3 แถว กลองชนส 1.35 วบนจอยทเอยงตลอดเวลารกวาเดนไมอยได
เหตผลทองถอด (ดดวย A/B บนเครองจร `justice-qb-playall` 10 อง):
ไมไถล = **10/10, 10/10** · ไถล (สองแกน) = 7/10, 9/10 · ไถล (แกนหลกอยางเดยว) = 8/10, 9/10
กหองท **จบท (11,6) เหมอนกนเป** = มขวาสดของเลนแถว 6
นเหตจรงอย **ปรางแผนท**: แถว 4-6 กวาง x1-11 แตทางลง (แถว 7) กวางแค x2-10
x=11 บนแถว 6 เป "กระเป๋าตัน" (กดลงไมลง) · การไถลทำใหไหลเขาไปตดงายมาก
าจะเอาการไถลกลบมา องแกแผนทใหแถว 6 บทางลงกวางเทากนกอน (งานขอม ปลอดภยกวาแกกส) */
me.x = moved.x;
me.y = moved.y;
normalMoved = true;
@@ -27731,26 +27949,66 @@
});
})();
(function wireQuizQuestionMissionJoystick() {
/* [2026-08-01] "" (MG2)
นเหต: เบราวเซอรหย requestAnimationFrame งหมดเมอแทบถกซอน วตดส "จบเกม" ของ MG2
งอยใน tickGauntletCrownRunwayBgPlay() (งเสนทาง "ตกขอบหมด" และ "ถึงเส้นชัย") ไมกเรยกเลย
เครองนนจงตองรอ event จาก server grace 3 = จบชากวาเพอนเห
แก: ระหวางถกซอน เดนลปเดมดวย setInterval แทน timer ในแทบพนหลงถ clamp (~1 ) แตงทำงาน
และ `gauntlet-sync` ทาง socket งเขามาเตมอตรา state สดพอใหดสนไดตรงจงหวะกบเครองอ
กลบมาแสดงผลเมอไหร ปลอยให rAF มตอทนท (นเดนลปซอนสองทาง) */
let hiddenRunwayHeartbeatTimer = null;
let hiddenRunwayHeartbeatLastMs = 0;
function stopHiddenRunwayHeartbeatPlay() {
if (hiddenRunwayHeartbeatTimer) {
clearInterval(hiddenRunwayHeartbeatTimer);
hiddenRunwayHeartbeatTimer = null;
}
hiddenRunwayHeartbeatLastMs = 0;
}
function startHiddenRunwayHeartbeatPlay() {
if (hiddenRunwayHeartbeatTimer) return;
hiddenRunwayHeartbeatLastMs = performance.now();
hiddenRunwayHeartbeatTimer = setInterval(function () {
if (typeof document !== 'undefined' && !document.hidden) { stopHiddenRunwayHeartbeatPlay(); return; }
if (!mapData || !isGauntlet()) return;
const now = performance.now();
const dt = Math.min(0.5, Math.max(0, (now - hiddenRunwayHeartbeatLastMs) / 1000));
hiddenRunwayHeartbeatLastMs = now;
try { tickGauntletCrownRunwayBgPlay(dt); } catch (_e) { /* ignore */ }
}, 250);
}
if (typeof document !== 'undefined') {
document.addEventListener('visibilitychange', function () {
if (document.hidden) startHiddenRunwayHeartbeatPlay();
else stopHiddenRunwayHeartbeatPlay();
});
}
(function wireVirtualJoystickPlay() {
const base = document.getElementById('quiz-q-mission-joystick-base');
const bg = document.getElementById('quiz-q-mission-joystick-bg');
const knobImg = document.getElementById('quiz-q-mission-joystick-knob-img');
if (!base) return;
if (bg) {
bg.src = questionMissionAssetUrl('btn-joystick-1.png');
bg.dataset.joySkin = 'question';
bg.onerror = function () {
this.onerror = null;
this.removeAttribute('src');
/* สกินไหนไม่มีไฟล์ → จำ url ที่พังไว้ (กัน sync หยิบซ้ำทุกเฟรม) แล้วตกไปใช้ของ /img/QUESTION/ ซึ่งมีแน่ */
const fallbackJoyImg = function (file) {
return function () {
const failed = this.dataset.joyUrl || this.getAttribute('src') || '';
if (failed) virtualJoystickFailedImgUrls[failed] = 1;
const fb = questionMissionAssetUrl(file);
if (failed === fb) { this.onerror = null; this.removeAttribute('src'); return; }
this.dataset.joyUrl = fb;
this.src = fb;
};
};
if (bg) {
bg.dataset.joyUrl = questionMissionAssetUrl('btn-joystick-1.png');
bg.src = bg.dataset.joyUrl;
bg.onerror = fallbackJoyImg('btn-joystick-1.png');
}
if (knobImg) {
knobImg.src = questionMissionAssetUrl('btn-joystick-2.png');
knobImg.dataset.joySkin = 'question';
knobImg.onerror = function () {
this.onerror = null;
this.removeAttribute('src');
};
knobImg.dataset.joyUrl = questionMissionAssetUrl('btn-joystick-2.png');
knobImg.src = knobImg.dataset.joyUrl;
knobImg.onerror = fallbackJoyImg('btn-joystick-2.png');
}
function endJoy(e) {
if (quizQuestionMissionJoyPointerId == null || e.pointerId !== quizQuestionMissionJoyPointerId) return;
@@ -27761,7 +28019,7 @@
quizQuestionMissionJoystickResetVisual();
}
base.addEventListener('pointerdown', (e) => {
if (!(isQuizQuestionMissionHudActivePlay() || isJumpSurviveMissionHudActivePlay()) || isChatFocused()) return;
if (!shouldShowVirtualJoystickPlay()) return;
if (e.button != null && e.button !== 0) return;
e.preventDefault();
try {
@@ -27991,6 +28249,48 @@
resizeCanvas();
draw();
});
/** [2026-07-31] " resize "
* - orientationchange: เดมไมไดกกบแคนวาสหลกเลย (อยเปนของ HUD MG1 คนละต)
* - visualViewport.resize: แถบ URL /กาง จอสงเปลยนโดยไม window resize ในบางเบราวเซอร
* งสองเหต เบราวเซอรอถอรายงานขนาด "ค่าเก่า" อย 1-2 เฟรม ดซำอ 2 งหวะ
* (rAF ดไป + 250ms) นคาคาง · นเรยกซอนดวย flag วเดยว */
(function setupPlayViewportChangeRemeasure() {
let pending = false;
const remeasure = () => {
if (!mapData || !canvas) return;
resizeCanvas();
draw();
};
const schedule = () => {
remeasure();
if (pending) return;
pending = true;
requestAnimationFrame(() => { remeasure(); });
setTimeout(() => { pending = false; remeasure(); }, 250);
};
window.addEventListener('orientationchange', schedule);
try {
if (window.visualViewport && window.visualViewport.addEventListener) {
window.visualViewport.addEventListener('resize', schedule);
}
} catch (e) { /* เบราว์เซอร์เก่าไม่มี visualViewport — resize เดิมยังทำงานอยู่ */ }
})();
/** [2026-07-31] hint "/" CSS
* (@media orientation: portrait and pointer: coarse) เดสกอปจงไมทางเห โดยไมอง sniff ปกรณ
* JS ทำแคเรองเดยว: จำวาผเลนกด "ข้าม" แล จะไดไมนกวนซำในรอบเลนเดยวก */
(function setupRotateHintDismiss() {
const KEY = 'jdRotateHintOff';
try { if (sessionStorage.getItem(KEY) === '1') document.documentElement.classList.add('rotate-hint-off'); }
catch (e) { /* private mode ปิด sessionStorage — ปล่อยให้ hint ขึ้นตามปกติ */ }
const btn = document.getElementById('rotate-hint-close');
if (!btn) return;
btn.addEventListener('click', (ev) => {
ev.preventDefault();
ev.stopPropagation();
document.documentElement.classList.add('rotate-hint-off');
try { sessionStorage.setItem(KEY, '1'); } catch (e) { /* ignore */ }
});
})();
/** แคนวาส flex ขยายทีหลัง (จอใหญ่ / editor iframe) — ResizeObserver จับได้ดีกว่า resize อย่างเดียว */
(function setupPlayCanvasStackResizeObserver() {
const stack = document.getElementById('play-canvas-stack');
+201 -6
View File
@@ -1338,10 +1338,38 @@
let lobbyLastTickTs = 0; /* timestamp เฟรมก่อน (ms) ใช้คำนวณ dt */
let lastMoveSend = 0;
const moveCodes = ['KeyW', 'KeyA', 'KeyS', 'KeyD', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
/* [2026-08-01] virtual joystick ( @media (pointer: coarse) room-lobby.html)
LobbyA และ LobbyB องเด ใชจอยชดเดยวก play.html (คลาส/ไอดเดยวก) */
let lobbyJoyVecX = 0;
let lobbyJoyVecY = 0;
let lobbyJoyPointerId = null;
let lobbyInteractPulse = null;
/* มือถือจอแคบ → zoom ต่ำลง ให้เห็นพื้นที่ lobby กว้างขึ้น (PC ย่อส่วน) · PC คงเดิม 1.2 */
let lobbyZoom = (typeof window !== 'undefined' && (window.innerWidth || 0) > 0 && window.innerWidth < 600) ? 0.85 : 1.2;
const LOBBY_ZOOM_MIN = 0.4;
/* [2026-08-02] zoom lobby user: "iPhone 13 Pro desktop 1920×1080"
ของเดมเปนคาคงท (`innerWidth < 600 ? 0.85 : 1.2`) ไมกกบขนาดจอเลย
iPhone 13 Pro แนวนอน (750×342) งได 1.2 เทาเดสกอป แตจอเลกกว ดไดาเหนโลกแค
**39% ของความกวาง · 32% ของความส** เดสกอป 1920×1080 เห
แก: บนจอสมผ zoom ให "พื้นที่โลกที่เห็น" เทาเดสกอป 1920×1080
zoom = 1.2 × min(1, vw/1920, vh/1080) iPhone 13 Pro ได 0.380 (ตรงกบทดไว)
PC ไมแตะ `pointer: fine` 1.2 เทาเดมทกกรณ (กฎ "ห้ามกระทบการเล่นบน PC") */
const LOBBY_ZOOM_DESKTOP = 1.2;
const LOBBY_VIEW_REF_W = 1920;
const LOBBY_VIEW_REF_H = 1080;
function lobbyDefaultZoom() {
try {
if (typeof window === 'undefined') return LOBBY_ZOOM_DESKTOP;
const coarse = !!(window.matchMedia && window.matchMedia('(pointer: coarse)').matches);
if (!coarse) return LOBBY_ZOOM_DESKTOP;
const vw = Math.max(1, window.innerWidth || 0);
const vh = Math.max(1, window.innerHeight || 0);
const k = Math.min(1, vw / LOBBY_VIEW_REF_W, vh / LOBBY_VIEW_REF_H);
return Math.max(0.2, LOBBY_ZOOM_DESKTOP * k);
} catch (e) { return LOBBY_ZOOM_DESKTOP; }
}
let lobbyZoom = lobbyDefaultZoom();
/** ผู้เล่นหมุนล้อ/ซูมเองแล้วหรือยัง — ถ้าเองแล้ว ห้ามไปคำนวณทับตอนหมุนจอ */
let lobbyZoomUserSet = false;
/* เพดานล่างต้องไม่ตัดค่า default ทิ้ง (จอเล็กมากคำนวณได้ต่ำกว่า 0.4) */
const LOBBY_ZOOM_MIN = Math.min(0.4, lobbyDefaultZoom());
const LOBBY_ZOOM_MAX = 2.5;
let mapTransform = { offsetX: 0, offsetY: 0, ts: 32, w: 20, h: 15 };
/** true เฉพาะช่องพิมพ์ข้อความ — ไม่รวม READY / แผนที่ (ให้กด F โต้ตอบได้โดยไม่ต้องกดพร้อมก่อน) */
@@ -1496,6 +1524,8 @@
const meY = (me && typeof me.y === 'number') ? me.y : 1;
const ts = tileSize * lobbyZoom;
mapTransform = { cw, ch, lobbyZoom, tileSize, meX, meY, w, h };
/* เปิดค่าที่ใช้วาดให้ตรวจได้จากภายนอก (driver วัด "โลกที่มองเห็นกี่ช่อง") — อ่านอย่างเดียว ไม่มีผลต่อเกม */
try { window.__jdLobbyView = { zoom: lobbyZoom, tileSize, cw, ch, tilesW: cw / (tileSize * lobbyZoom), tilesH: ch / (tileSize * lobbyZoom) }; } catch (e) { /* ignore */ }
ctx.fillStyle = '#303770';
ctx.fillRect(0, 0, cw, ch);
@@ -2310,7 +2340,16 @@
var byWidth = rect.width / baseWidth;
var byHeight = rect.height / baseHeight;
var targetScale = 1.08;
var scale = Math.max(0.56, Math.min(targetScale, Math.min(byWidth, byHeight) * targetScale));
/* targetScale "" targetScale ****
ของเดมค fit = min(byW,byH) × 1.08 จอส 342px ได 0.38×1.08 = 0.41 เนอหาส 369px = งล
กค fit = min(byW,byH) ตรง 0.38 342px พอดจอ · จอใหญงโดน clamp 1.08 เหมอนเด */
var fit = Math.min(byWidth, byHeight);
/* [2026-08-02] `Math.max(0.56, )`
จอเต (iPhone แนวนอน + แถบ URL/กมารกกนความส เหล ~342px) องการแค ~0.41 งพอดจอ
แตกดนขนเป 0.56 เนอหาส 0.56×900 = 504px ในจอส 342px = **การดคดโดนตดขอบลาง**
(user งภาพจากเครองจรงมายนย) · แก: นลางหามดนจนลนจอ เหลอแคนเลกเกนอานไมออก
PC ไมกระทบ จอใหญ fit > 1.08 อยแล าจงถ clamp targetScale เหมอนเด */
var scale = Math.max(0.25, Math.min(targetScale, fit));
preplayStepCase.style.setProperty('--lobby-preplay-case-scale', scale.toFixed(4));
}
@@ -2519,7 +2558,11 @@
wrapper aspect-ratio ไว าคดไหนไมไฟลอความ (404) อนแค มไมเลอน */
ov.innerHTML =
'<img id="case-cutscene-img" alt="" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;display:block">' +
'<div id="case-cutscene-txtwrap" style="position:absolute;left:50%;top:75%;transform:translateX(-50%);width:93.75%;max-width:1801px;aspect-ratio:1801/233;z-index:2;pointer-events:none">' +
/* [2026-08-02] `top:75%` +
จอเตยกวาง (iPhone แนวนอน) กลองสงเกนทเหล วหนงสอกบป "ถัดไป" ตกใตขอบลาง
แกเป **ดขอบลาง** + มความสงสงส: บนเดสกอป 1920×1080 ตำแหนงเทาเดมเป
(top 75% ของ 1080 = 810 · กลองส 233 างขอบลาง 37px = 3.4%) แตจอเตยจะไมนล */
'<div id="case-cutscene-txtwrap" style="position:absolute;left:50%;bottom:3.4%;transform:translateX(-50%);width:93.75%;max-width:1801px;aspect-ratio:1801/233;max-height:26%;z-index:2;pointer-events:none">' +
'<img id="case-cutscene-txt" alt="" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:none">' +
'<button type="button" id="case-cutscene-next" aria-label="ถัดไป" style="position:absolute;left:91.1%;top:86.7%;transform:translate(-50%,-50%);width:12.8%;background:none;border:none;padding:0;cursor:pointer;pointer-events:auto">' +
'<img src="/Main-Lobby/IMAGE/cutscene/btn-next.png" alt="ถัดไป" decoding="async" style="display:block;width:100%;height:auto">' +
@@ -2817,6 +2860,7 @@
}
const w = mapData.width || 20, h = mapData.height || 15;
const preWalkX = me.x, preWalkY = me.y;
syncLobbyJoystick();
let accX = 0, accY = 0;
let usePath = false;
if (!suspectPickOverlayOpen) {
@@ -2854,6 +2898,14 @@
if (keys['ArrowDown'] || keys['KeyS']) { accY = 1; me.direction = 'down'; }
if (keys['ArrowLeft'] || keys['KeyA']) { accX = -1; me.direction = 'left'; }
if (keys['ArrowRight'] || keys['KeyD']) { accX = 1; me.direction = 'right'; }
/* virtual joystick () ( )
านลางหารดวยความยาวเวกเตอรอยแล ความแรงทผลกจอยไมไดทำใหเดนเรวข ผลแค */
if (accX === 0 && accY === 0 && (lobbyJoyVecX !== 0 || lobbyJoyVecY !== 0)) {
accX = lobbyJoyVecX;
accY = lobbyJoyVecY;
if (Math.abs(accY) > Math.abs(accX)) me.direction = accY > 0 ? 'down' : 'up';
else me.direction = accX > 0 ? 'right' : 'left';
}
}
if (accX !== 0 || accY !== 0) {
const len = Math.sqrt(accX * accX + accY * accY) || 1;
@@ -5587,6 +5639,13 @@
'#trial-confirm-btn{position:absolute;left:50%;bottom:5%;transform:translateX(-50%);background:linear-gradient(180deg,#34d399,#059669);color:#04140d;border:none;border-radius:16px;padding:15px 40px;font:800 21px/1 Kanit,system-ui,sans-serif;cursor:pointer;z-index:61;box-shadow:0 8px 26px rgba(52,211,153,.55);display:none}' +
'#trial-confirm-btn:disabled{filter:grayscale(.5) brightness(.8);cursor:default;font-weight:700;font-size:17px}' +
'#trial-confirm-btn:not(:disabled):active{transform:translateX(-50%) scale(.96)}' +
/* [2026-08-02] ( 07-22 · user )
คำนวณการชน: timer อย `top:86.6%` 52 (ดกงกลาง) ขอบลาง = 0.866H + 26
มอย `bottom:5%` ~51 ขอบบน = 0.95H 51
ชนเม 0.866H + 26 > 0.95H 51 **H < 917px** (จอ 1080 ไมชน · iPhone แนวนอน 342 ชนเต )
แก: จอเตยกว 917px ให timer "เหนือปุ่ม" แทนเปอรเซนตของจอ
เดสกอป 1080 ไมเขาเงอนไข media query = ตำแหนงเดมเป */
'@media (max-height:917px){#trial-vote-timer{top:auto;bottom:calc(5% + 92px);transform:translateX(-50%)}}' +
// รอบจับผิดตัว: ใบที่โหวตผิดไปแล้ว — หรี่ + กันคลิก + ป้าย "โหวตผิดไปแล้ว"
'.suspect-card--excluded{filter:grayscale(1) brightness(.55);pointer-events:none;position:relative}' +
'.suspect-card--excluded .trial-vote-badge{display:none!important}' +
@@ -8451,6 +8510,7 @@
canvas.addEventListener('pointercancel', () => { _lobbyTapStart = null; });
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
lobbyZoomUserSet = true; /* ซูมเองแล้ว → ห้ามคำนวณ default ทับตอนหมุนจอ */
lobbyZoom *= e.deltaY > 0 ? 0.9 : 1.1;
lobbyZoom = Math.max(LOBBY_ZOOM_MIN, Math.min(LOBBY_ZOOM_MAX, lobbyZoom));
redrawLobbyMap();
@@ -8459,6 +8519,122 @@
canvas.addEventListener('dblclick', (e) => { lobbyWalkToClientXY(e.clientX, e.clientY); });
}
/* ===== [2026-08-01] virtual joystick (จอสัมผัส) — LobbyA / LobbyB =====
ยกกลไกมาจาก play.js งช (deadzone · pointer capture · transform ของแกน) ใหกดแลวรกเหมอนกนทกฉาก
ตำแหนงมมซายลางวางอยแล ( F อถอถกถอดไปตงแต 2026-07-04 · แชทอยขวาลาง) */
function lobbyJoystickAssetUrl(file) {
return BASE + '/img/QUESTION/' + String(file || '').replace(/^\//, '');
}
function lobbyJoystickResetVisual() {
lobbyJoyVecX = 0;
lobbyJoyVecY = 0;
const knob = document.getElementById('quiz-q-mission-joystick-knob');
if (knob) knob.style.transform = 'translate(-50%, -50%)';
}
/** lobbyTick
ไมนจะไดจอยทกดแลวตวไมขย (โอเวอรเลย z-index งกว 56 อยแล งทบจอยเองไมองเช) */
function shouldShowLobbyJoystick() {
if (!mapData || !canvas) return false;
if (!canvas.offsetParent) return false;
if (suspectPickOverlayOpen) return false;
if (isChatFocused()) return false;
return true;
}
function lobbyJoystickUpdateFromClientXY(clientX, clientY) {
const base = document.getElementById('quiz-q-mission-joystick-base');
const knob = document.getElementById('quiz-q-mission-joystick-knob');
if (!base || !knob) return;
const r = base.getBoundingClientRect();
const cx = r.left + r.width * 0.5;
const cy = r.top + r.height * 0.5;
let dx = clientX - cx;
let dy = clientY - cy;
const half = Math.min(r.width, r.height) * 0.5;
const maxKn = Math.max(18, half * 0.52);
const len = Math.hypot(dx, dy);
if (len > maxKn && len > 1e-6) {
dx = (dx / len) * maxKn;
dy = (dy / len) * maxKn;
}
const nlen = Math.hypot(dx, dy);
const dead = maxKn * 0.12;
if (nlen < dead) {
lobbyJoyVecX = 0;
lobbyJoyVecY = 0;
knob.style.transform = 'translate(-50%, -50%)';
return;
}
lobbyJoyVecX = dx / maxKn;
lobbyJoyVecY = dy / maxKn;
knob.style.transform = 'translate(calc(-50% + ' + dx + 'px), calc(-50% + ' + dy + 'px))';
}
function syncLobbyJoystick() {
const root = document.getElementById('quiz-question-mission-joystick');
if (!root) return;
/* / · "" ( / )
ใหหรไวแทน เหตผลเดยวกบใน play.js (จอยหายกลางคนแลวนวหล องยกกดใหม) */
const alive = !!(mapData && canvas && canvas.offsetParent);
const on = shouldShowLobbyJoystick();
root.classList.toggle('is-hidden', !alive);
root.classList.toggle('is-inert', alive && !on);
root.setAttribute('aria-hidden', on ? 'false' : 'true');
if (!on) {
const base = document.getElementById('quiz-q-mission-joystick-base');
if (base && lobbyJoyPointerId != null) {
try { base.releasePointerCapture(lobbyJoyPointerId); } catch (_e) { /* ignore */ }
}
lobbyJoyPointerId = null;
lobbyJoystickResetVisual();
}
}
(function wireLobbyJoystick() {
const base = document.getElementById('quiz-q-mission-joystick-base');
const bg = document.getElementById('quiz-q-mission-joystick-bg');
const knobImg = document.getElementById('quiz-q-mission-joystick-knob-img');
if (!base) return;
if (bg) {
bg.src = lobbyJoystickAssetUrl('btn-joystick-1.png');
bg.onerror = function () { this.onerror = null; this.removeAttribute('src'); };
}
if (knobImg) {
knobImg.src = lobbyJoystickAssetUrl('btn-joystick-2.png');
knobImg.onerror = function () { this.onerror = null; this.removeAttribute('src'); };
}
function endJoy(e) {
if (lobbyJoyPointerId == null || e.pointerId !== lobbyJoyPointerId) return;
try { base.releasePointerCapture(e.pointerId); } catch (_err) { /* ignore */ }
lobbyJoyPointerId = null;
lobbyJoystickResetVisual();
}
base.addEventListener('pointerdown', (e) => {
if (!shouldShowLobbyJoystick()) return;
if (e.button != null && e.button !== 0) return;
e.preventDefault();
/* จับจอย = เลิกเดินตามเส้นทางที่แตะไว้ (ไม่งั้นสองอินพุตแย่งกันคุมตัวละคร) */
lobbyPath = [];
try { base.setPointerCapture(e.pointerId); } catch (_err) { /* ignore */ }
lobbyJoyPointerId = e.pointerId;
lobbyJoystickUpdateFromClientXY(e.clientX, e.clientY);
});
base.addEventListener('pointermove', (e) => {
if (lobbyJoyPointerId == null || e.pointerId !== lobbyJoyPointerId) return;
e.preventDefault();
lobbyJoystickUpdateFromClientXY(e.clientX, e.clientY);
});
base.addEventListener('pointerup', endJoy);
base.addEventListener('pointercancel', endJoy);
base.addEventListener('lostpointercapture', (e) => {
if (lobbyJoyPointerId == null || e.pointerId !== lobbyJoyPointerId) return;
lobbyJoyPointerId = null;
lobbyJoystickResetVisual();
});
})();
/* F () "/" (lobbyTapInteractPlay)
เผอมมคางจาก build เก/cache ลบท */
(function removeLegacyMobileLobbyInteractBtn() {
@@ -8571,7 +8747,26 @@
});
}
window.addEventListener('resize', resizeAndDraw);
/* [2026-08-02] viewport `resize`
บน iOS การหมนจอ/แถบ URL -กาง `orientationchange` และ `visualViewport.resize`
งไมไดกไว ฟเฟอรางขนาดเกาอยหลายเฟรม = ภาพถกยดจนกวาจะม resize วถดไป
(อาการ "ยืด ๆ" user เจอ · แบบเดยวกบท play.js แกไปแล 07-31)
+ าผเลนยงไมเคยซมเอง ให default zoom ใหมตามขนาดจอใหมวย */
function lobbyViewportChangedRemeasure() {
if (!lobbyZoomUserSet) {
const z = lobbyDefaultZoom();
if (Math.abs(z - lobbyZoom) > 1e-6) lobbyZoom = z;
}
resizeAndDraw();
/* วัดซ้ำอีก 2 จังหวะ กันค่าค้าง (rAF ถัดไป + 250ms) — สูตรเดียวกับ setupPlayViewportChangeRemeasure ใน play.js */
try { requestAnimationFrame(resizeAndDraw); } catch (e) { /* ignore */ }
setTimeout(resizeAndDraw, 250);
}
window.addEventListener('resize', lobbyViewportChangedRemeasure);
window.addEventListener('orientationchange', lobbyViewportChangedRemeasure);
if (window.visualViewport && window.visualViewport.addEventListener) {
window.visualViewport.addEventListener('resize', lobbyViewportChangedRemeasure);
}
const wrapEl = document.getElementById('lobby-map-wrap');
if (wrapEl && typeof ResizeObserver !== 'undefined') {
new ResizeObserver(() => resizeAndDraw()).observe(wrapEl);
+184 -4
View File
@@ -2,7 +2,10 @@
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- [2026-07-31] viewport-fit=cover จำเป็นจริง ๆ: ไฟล์นี้ใช้ env(safe-area-inset-*) อยู่ 39 จุด
(จอยสติ๊ก/ปุ่มกระโดด/HUD) แต่ถ้า meta ไม่มี viewport-fit=cover เบราว์เซอร์คืนค่า 0 ทุกจุด
→ บนมือถือที่มีรอยบาก/แถบ home ปุ่มจะจมใต้แถบ = กดไม่ติด (ตรงกับอาการที่ผู้เล่นเจอ) -->
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta http-equiv="Cache-Control" content="no-store, max-age=0">
<title>เล่น — Game</title>
<!-- คลาส embed ต้องมีก่อน first paint — ถ้ารอ play.js ท้าย body จะ layout รอบแรกผิด (แคนวาสชิดซ้ายบน / stack สูงไม่เต็ม) -->
@@ -20,7 +23,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;800&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/style.css?v=20260628-stack-tower-contain">
<link rel="stylesheet" href="css/style.css?v=20260802-dvh">
<!-- Minigame-2 HOW TO PLAY mock (01-howto) — layer positions / popup / status / btn -->
<link rel="stylesheet" href="/onlydesign/Cut%20[Justice%20Game]_full1.1/Minigame-2/assets/css/game-hud.css?v=0.006130015">
<link rel="stylesheet" href="/onlydesign/Cut%20[Justice%20Game]_full1.1/Minigame-2/assets/css/howto.css?v=0.006130015">
@@ -32,6 +35,19 @@
box-sizing: border-box;
}
body { min-height: 100dvh; }
/* [2026-08-02] 🔑 ล็อกไม่ให้หน้าเลื่อน — user แจ้ง "MG6 กด control ลง มันติดเลื่อนจอ · MG5 ซ้ายขวาขึ้นลงลำบาก"
ต้นเหตุ: `play.html` **ไม่เคยล็อกการเลื่อนหน้าเลย** (room-lobby.html ล็อกไว้ตั้งแต่แรกด้วย
`html, body.room-lobby-page { overflow: hidden }`) พอเอกสารสูงเกิน visual viewport บน iOS
(แถบ URL + บุ๊กมาร์กกินที่) การลากนิ้วลงจึงกลายเป็น "เลื่อนหน้า" แทนที่จะไปที่จอย
`overscroll-behavior: none` กันเด้งยางของ iOS ด้วย
ห่อ pointer: coarse → PC ยังเลื่อนได้ตามเดิม (เผื่อโอเวอร์เลย์ไหนสูงเกินจอบนเดสก์ท็อป) */
@media (pointer: coarse) {
html, body {
overflow: hidden;
overscroll-behavior: none;
touch-action: manipulation;
}
}
/* ทุกเกม — ซ่อนเฉพาะแถบห้องบน (ออกจากห้อง + ชื่อห้อง); คง SCORE/TIME/หน้าผู้เล่นใน #play-cyber-hud */
html.play-hide-top-chrome .game-header {
display: none !important;
@@ -360,6 +376,75 @@
pointer-events: none;
user-select: none;
}
/* ===== [2026-07-31] ความแน่นของการกดบนจอสัมผัส (มือถือ/แท็บเล็ตเท่านั้น) =====
ห่อด้วย pointer: coarse ทั้งบล็อก → เมาส์ = pointer: fine ⇒ PC ไม่โดนกฎพวกนี้เลย
ผลตรวจจาก mobile-layout-check.js: ปุ่มทุกตัวใหญ่ ≥44px และไม่ทับกันอยู่แล้ว
จึงไม่ไปยุ่งกับขนาด/ตำแหน่ง (ตรงตามที่ user สั่ง "แค่ให้กดติดแน่นขึ้น")
เหลือแค่ปิดพฤติกรรมของเบราว์เซอร์ที่ไปแย่งการกดของเกม */
@media (pointer: coarse) {
#gauntlet-crown-jump-btn,
#quiz-carry-grab-btn,
#quiz-question-mission-joystick,
.rotate-hint-close {
-webkit-touch-callout: none; /* กดค้างแล้วเด้งเมนู "บันทึกรูป" ทับปุ่ม */
-webkit-user-select: none;
user-select: none;
touch-action: manipulation; /* ตัดดีเลย์รอดับเบิลแท็บ + กันดับเบิลแท็บซูม */
}
#gauntlet-crown-jump-btn img,
#quiz-carry-grab-btn img,
#quiz-question-mission-joystick img {
-webkit-touch-callout: none;
pointer-events: none; /* ให้ทุกการแตะตกที่ปุ่มตัวจริง ไม่ใช่ที่ <img> ข้างใน */
}
}
/* ===== [2026-07-31] hint ชวนหมุนจอ (มือถือ/แท็บเล็ต แนวตั้งเท่านั้น) =====
ซ่อนเป็นค่าเริ่มต้น แล้วเปิดเฉพาะ (orientation: portrait) and (pointer: coarse)
→ เมาส์ = pointer: fine ⇒ เดสก์ท็อปไม่มีทางเข้าเงื่อนไข ไม่ต้องพึ่ง JS ตรวจอุปกรณ์
เป็นแถบเล็กด้านบน ไม่คลุมจอ · ตัวแถบ pointer-events:none ให้กดทะลุไปที่เกมได้
(เปิด pointer-events เฉพาะปุ่มข้าม) ⇒ ไม่ขวางการเล่นแม้ยังไม่กดปิด */
.rotate-hint { display: none; }
html.rotate-hint-off .rotate-hint { display: none !important; }
@media (orientation: portrait) and (pointer: coarse) {
.rotate-hint {
display: flex;
position: fixed;
left: 50%;
transform: translateX(-50%);
top: calc(8px + env(safe-area-inset-top, 0px));
z-index: 70;
align-items: center;
gap: 8px;
max-width: min(94vw, 520px);
padding: 8px 10px 8px 12px;
border-radius: 999px;
background: rgba(10, 14, 34, 0.92);
border: 1px solid rgba(120, 200, 255, 0.45);
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.45);
color: #dbe7ff;
font-size: 13px;
line-height: 1.25;
pointer-events: none;
}
.rotate-hint-icon { font-size: 16px; }
.rotate-hint-text { min-width: 0; }
.rotate-hint-close {
pointer-events: auto;
flex: 0 0 auto;
min-width: 44px;
min-height: 44px;
padding: 0 12px;
border-radius: 999px;
border: 1px solid rgba(120, 200, 255, 0.5);
background: rgba(30, 44, 90, 0.9);
color: #dbe7ff;
font: inherit;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
}
/* Last Light (mno9kb07) — ปุ่มกระโดดมุมขวาล่าง · เทียบ Space / W / ↑ */
#gauntlet-crown-jump-btn {
position: absolute;
@@ -2090,7 +2175,10 @@
background: linear-gradient(180deg, #ff8a9c, #ef4d62);
box-shadow: 0 6px 18px rgba(239, 77, 98, 0.35);
}
/* mng8a80o — virtual joystick (Roblox-style) มุมซ้ายล่าง */
/* virtual joystick (Roblox-style) มุมซ้ายล่าง — ทุกฉากที่ต้องเดิน (MG1/4/5/6 · Quiz Battle · lobby map)
[2026-08-01] จอสัมผัสเท่านั้น (มือถือ/แท็บเล็ต): ค่าเริ่มต้น display:none แล้วเปิดใน @media (pointer: coarse)
→ เมาส์ = pointer: fine ⇒ เดสก์ท็อปไม่มีทางเห็น โดยไม่ต้อง sniff อุปกรณ์ใน JS
(ตัวคุม "ฉากไหนโชว์" ยังเป็นคลาส .is-hidden จาก syncVirtualJoystickPlay() เหมือนเดิม) */
.quiz-q-mission-joystick {
position: fixed;
z-index: 56;
@@ -2103,6 +2191,10 @@
-webkit-user-select: none;
pointer-events: auto;
box-sizing: border-box;
display: none;
}
@media (pointer: coarse) {
.quiz-q-mission-joystick { display: block; }
}
html.play-preview-editor-embed .quiz-q-mission-joystick {
position: absolute;
@@ -2111,6 +2203,44 @@
.quiz-q-mission-joystick.is-hidden {
display: none !important;
}
/* [2026-08-01] ช่วงที่เกมบล็อกการเดินชั่วคราว (ป๊อปอัพคำถาม QB · เฟสอ่านคำถาม MG4 · howto/นับถอยหลัง)
— คงจอยไว้แต่หรี่และกดไม่ติด แทนการเอาออกทั้งอัน (เอาออกแล้วนิ้วหลุด ต้องยกกดใหม่ทุกรอบ) */
.quiz-q-mission-joystick.is-inert,
#gauntlet-crown-jump-btn.is-inert,
#stack-tower-drop-btn.is-inert,
#quiz-carry-grab-btn.is-inert {
opacity: 0.32;
pointer-events: none;
filter: saturate(0.4);
}
/* [2026-08-02] ป้าย "ตกรอบแล้ว" — MG5 / MG6 · แถบเล็กด้านบนกลางจอ ไม่บังเกม */
.play-elim-banner {
display: none;
position: fixed;
z-index: 10120;
left: 50%;
top: max(10px, env(safe-area-inset-top, 0px));
transform: translateX(-50%);
align-items: center;
gap: 0.5em;
max-width: min(92vw, 560px);
padding: 0.45em 1.05em;
border-radius: 999px;
background: rgba(24, 8, 14, 0.88);
border: 1px solid rgba(255, 120, 140, 0.55);
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.45);
color: #ffd7de;
font-family: 'NotoSansThai', Kanit, sans-serif;
font-weight: 700;
font-size: clamp(0.82rem, 2.2vmin, 1.05rem);
line-height: 1.25;
white-space: nowrap;
pointer-events: none; /* ดูเกมเพื่อนต่อได้ ไม่บังการกดอะไรทั้งนั้น */
user-select: none;
}
.play-elim-banner.is-on { display: flex; }
.play-elim-icon { font-size: 1.15em; filter: drop-shadow(0 0 6px rgba(255, 90, 120, 0.6)); }
.quiz-q-mission-joystick-base {
position: relative;
width: 100%;
@@ -4964,6 +5094,13 @@
<!-- Minigame-2: ชื่อ+คะแนนใต้ตัวละคร + เลข -10 → DOM overlay (sync world→screen) แทนวาดบน canvas เพื่อให้ตรง mock -->
<div id="gauntlet-name-overlay" aria-hidden="true"></div>
</div>
<!-- [2026-07-31] hint ชวนหมุนจอ — โผล่เฉพาะ "จอสัมผัส + แนวตั้ง" (CSS คุมล้วน เดสก์ท็อปไม่มีทางเห็น)
เป็นแถบเล็กด้านบน ไม่ใช่ตัวบล็อก: กดข้ามแล้วเล่นแนวตั้งต่อได้ · จำการปิดไว้ใน sessionStorage -->
<div id="rotate-hint" class="rotate-hint" role="status" aria-live="polite">
<span class="rotate-hint-icon" aria-hidden="true">📱↻</span>
<span class="rotate-hint-text">หมุนจอเป็นแนวนอนจะเห็นภาพเต็มกว่า</span>
<button type="button" class="rotate-hint-close" id="rotate-hint-close" aria-label="ปิดคำแนะนำ">ข้าม</button>
</div>
<button type="button" id="quiz-carry-grab-btn" class="is-hidden" aria-label="หยิบหรือส่งคำตอบ (Grab)" title="หยิบ / ส่งคำตอบ — เหมือนกด F">
<img src="/Game/img/quiz-carry/btn-grab.png" alt="" width="256" height="256" decoding="async" / loading="lazy">
</button>
@@ -5055,6 +5192,14 @@
</div>
</div>
</div>
<!-- [2026-08-02] ป้ายบอกว่า "ตกรอบแล้ว" (MG5 ตกแท่น · MG6 ยานถูกทำลาย)
เดิมตายแล้วบังคับอะไรไม่ได้เลยทั้งเดินและยิง แต่**ไม่มีอะไรบอก** → ผู้เล่นอ่านว่าปุ่มเสีย
(วัดจริง MG6: ยานถูกทำลายที่วินาทีที่ 5.5-6.2 · เกม 2-6 คน ต้องรอจนหมดเวลา ~83 วิ)
เป็นแถบเล็กด้านบน pointer-events:none ไม่บังการดูเกมของเพื่อน -->
<div id="play-eliminated-banner" class="play-elim-banner" role="status" aria-live="polite" aria-hidden="true">
<span class="play-elim-icon" aria-hidden="true">💀</span>
<span class="play-elim-text" id="play-eliminated-text"></span>
</div>
</div>
<!-- MG3 Stack Tower (mnn93hpi) live HUD — DOM overlay (canvas object-fit:cover ครอป HUD ขอบจอ จึงย้ายมา DOM สเกลตาม stage) -->
<div id="mg3-tower-hud" class="mg3-tower-hud is-hidden" aria-hidden="true">
@@ -5458,7 +5603,7 @@
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/sound.js?v=2"></script>
<script src="js/play.js?v=0.006301196"></script>
<script src="js/play.js?v=0.006301211"></script>
<div class="version-tag">v —</div>
<style id="qc-howto-mg2-fix">
/* HOW TO PLAY ของ quiz_carry (Minigame-4) -> ให้เหมือน Minigame-2 (gch-mg2-mock)
@@ -5476,11 +5621,15 @@
}
#quiz-carry-pregame-overlay .quiz-carry-pregame-card {
position: relative;
/* [2026-08-02] iOS: 100vh = ความสูงตอนแถบ URL ซ่อน => การ์ด 16:9 ที่คิดจาก vh สูงเกินจอจริง
= HOW TO PLAY โดนตัดหัว/ท้ายบนแนวนอน ใส่ dvh ทับ (คง vh ไว้เป็น fallback) */
width: min(100vw, 177.78vh);
width: min(100vw, 177.78dvh);
height: auto;
aspect-ratio: 1920 / 1080;
max-width: 100vw;
max-height: 100vh;
max-height: 100dvh;
min-height: 0;
padding: 0;
background: transparent;
@@ -5553,11 +5702,14 @@
}
#gauntlet-crown-howto-overlay:not(.gch-quiz-tf-mock):not(.gch-mg2-mock) .gch-shell {
position: relative;
/* [2026-08-02] เหตุผลเดียวกับ quiz-carry-pregame-card ด้านบน vh บน iOS สูงเกินที่มองเห็นจริง */
width: min(100vw, 177.78vh);
width: min(100vw, 177.78dvh);
height: auto;
aspect-ratio: 1920 / 1080;
max-width: 100vw;
max-height: 100vh;
max-height: 100dvh;
min-width: 0;
min-height: 0;
margin: 0;
@@ -5650,6 +5802,34 @@
background-image: url('/Game/img/QUESTION/btn-ready-active.png') !important;
filter: drop-shadow(0 0 16px rgba(74, 222, 128, 0.7)) !important;
}
/* ===== [2026-08-01] ปุ่มแอ็กชันฝั่งขวา "หาไม่เจอ" บนมือถือ/แท็บเล็ต =====
ต้นเหตุ: ปุ่มพวกนี้เป็น `position: absolute` → right/bottom ยึด **กล่องฉาก** ไม่ใช่ขอบจอ
จอที่เตี้ยกว่ากล่องฉากเมื่อไหร่ ปุ่มจะไปอยู่ใต้ขอบล่าง = มองไม่เห็นและกดไม่ได้
วัดได้ (action-btn-bounds-check.js): มือถือ ปุ่ม MG2/MG5 อยู่ที่ y=818 ขณะจอสูง 769 → `elementFromPoint` คืน null
จอยฝั่งซ้ายไม่เจอปัญหานี้เพราะเป็น `position: fixed` อยู่แล้ว → ทำฝั่งขวาให้เหมือนกัน
⚠️ ห่อด้วย `pointer: coarse` → PC (pointer: fine) เรขาคณิตไม่ขยับเลยสักพิกเซล ตามกฎ "ห้ามกระทบการเล่นบน PC"
บล็อกนี้อยู่ท้ายสุดของ <style> เพื่อให้ชนะ base rule (#…-btn { position: absolute }) ที่ประกาศไว้ก่อนหน้า */
@media (pointer: coarse) {
#gauntlet-crown-jump-btn,
#stack-tower-drop-btn,
#quiz-carry-grab-btn {
position: fixed;
/* 🔑 ต้องล้าง left/top ด้วย — CSS ของทีมออกแบบ
`/onlydesign/Cut [Justice Game]_full1.1/Minigame-2/assets/css/game-hud.css`
ตรึง `.mg2-jump { left:1648px; top:818px }` ไว้ (คลาสนี้ถูกใส่ให้ปุ่มเดียวกันใน MG2/MG5/MG7)
base rule ตั้งแค่ right/bottom ไม่เคยล้าง left/top → เมื่อมีทั้งคู่ **left ชนะ right เสมอ**
ปุ่มเลยไปนั่งที่พิกัด mock 1920×1080 ตายตัวทุกจอ = ตกนอกจอบนแท็บเล็ต/มือถือ */
left: auto;
top: auto;
}
/* ในกรอบพรีวิวของ editor ปุ่มต้องยึดกรอบพรีวิวเหมือนเดิม (เหมือนที่จอยทำ) */
html.play-preview-editor-embed #gauntlet-crown-jump-btn,
html.play-preview-editor-embed #stack-tower-drop-btn,
html.play-preview-editor-embed #quiz-carry-grab-btn {
position: absolute;
}
}
</style>
<!-- [minigame transition] เล่นวิดีโอ transition ตอนเข้ามินิเกม (detectiveReturn=1) — ข้ามไม่ได้ · จบวิดีโอ → เผยหน้า loading/how-to ที่โหลดอยู่ข้างล่าง -->
File diff suppressed because it is too large Load Diff
+124 -12
View File
@@ -2078,14 +2078,34 @@ function evidencePoolCounts() {
return out;
}
/* แปลงเกรด -> rarity key ('legendary'=ชี้คนร้าย/'rare'/'common') ตามเปอร์เซ็นต์ที่กำหนด */
function rollEvidenceRarityKey(grade) {
/* แปลงเกรด -> rarity key ('legendary'=ชี้คนร้าย/'rare'/'common') ตามเปอรเซนตกำหนด
[2026-07-31] allowLegendary=false ดทองออกแล "เกลี่ยสัดส่วนที่เหลือตามเดิม"
(A: rare:common = 50:20 5/7 : 2/7 · B: 30:50 3/8 : 5/8) ไมใชดไปทางใดทางหน */
function rollEvidenceRarityKey(grade, allowLegendary) {
const r = Math.random();
if (grade === 'A') return r < 0.30 ? 'legendary' : (r < 0.80 ? 'rare' : 'common');
if (grade === 'B') return r < 0.20 ? 'legendary' : (r < 0.50 ? 'rare' : 'common');
const allow = allowLegendary !== false;
if (grade === 'A') {
if (allow) return r < 0.30 ? 'legendary' : (r < 0.80 ? 'rare' : 'common');
return r < (5 / 7) ? 'rare' : 'common';
}
if (grade === 'B') {
if (allow) return r < 0.20 ? 'legendary' : (r < 0.50 ? 'rare' : 'common');
return r < (3 / 8) ? 'rare' : 'common';
}
return r < 0.20 ? 'rare' : 'common'; // C ลงไป: ไม่มีชี้คนร้าย
}
/** [2026-07-31] "" (legendary = ) 2
* 1) แฟมทกำลงสบตองเป "คนร้ายตัวจริง" ทองในแฟมคนบรทธ = หลกฐานชดคน
* 2) งรอบมเกมออกไดใบเดยว ไมาจะมเลนกคน (รวมการดพเศษใบ 7 หลกฐานฟรวย)
* ธงข 2 อยบน space งคมทกเสนทางแจกในรอบเดยวกนโดยอตโนม */
function legendaryAllowedForRun(space, suspectIndex) {
if (!space) return false;
if (space.legendaryGrantedThisRun) return false;
const culprit = (typeof space.culpritIndex === 'number') ? space.culpritIndex : -1;
return culprit >= 0 && Number(suspectIndex) === culprit;
}
/* โฟลเดอร์รูปของผู้ต้องสงสัยตาม config คดี (fallback = suspect-(idx+1)) */
function evidenceImageDirForSuspect(space, suspectIndex) {
const def = EVIDENCE_IMAGE_DIRS[suspectIndex] || EVIDENCE_IMAGE_DIRS[0];
@@ -2104,10 +2124,12 @@ function pickEvidenceCardForSuspect(space, suspectIndex, grade) {
const imageDir = evidenceImageDirForSuspect(space, suspectIndex);
const pools = evidenceImagePools();
const buckets = pools[imageDir] || pools[EVIDENCE_IMAGE_DIRS[0]] || { common: [], rare: [], legendary: [] };
let want = rollEvidenceRarityKey(grade || 'C');
const allowLegendary = legendaryAllowedForRun(space, suspectIndex);
let want = rollEvidenceRarityKey(grade || 'C', allowLegendary);
let pool = buckets[want];
if (!pool || !pool.length) {
const tryOrder = ['common', 'rare', 'legendary'];
/* fallback ตอนพูลว่าง — ถ้าทองยังไม่ได้รับอนุญาต ห้ามหล่นมาลงทองแทน */
const tryOrder = allowLegendary ? ['common', 'rare', 'legendary'] : ['common', 'rare'];
for (const k of tryOrder) { if (buckets[k] && buckets[k].length) { pool = buckets[k]; want = k; break; } }
}
if (!pool || !pool.length) return null;
@@ -2119,11 +2141,14 @@ function evidenceGradeFromPct(avgPct) {
return avgPct >= 80 ? 'A' : avgPct >= 60 ? 'B' : 'C';
}
/* คะแนนดิบของผู้เล่นต่อ gameType (ใช้กรณี normalize เทียบ best) */
/* gameType ( normalize best)
[2026-07-31] MG5 เดมอาน p.gauntletScore jump_survive ไมเคยเขยน (เกมเขยนลง
p.reportedMiniScore = รอด 100 / ตาย 0 server.js:9686) ได 0 กคน = เกรด C ตลอด
แกใหานฟลดเกมเขยนจร เกรดกลายเป "% คนที่รอด" งตรงกบธรรมชาตเกมเอาชตรอด */
function evidencePlayerRawScore(space, p, gameType) {
switch (gameType) {
case 'gauntlet':
case 'jump_survive': return Math.max(0, p.gauntletScore | 0);
case 'gauntlet': return Math.max(0, p.gauntletScore | 0);
case 'jump_survive': return Math.max(0, p.reportedMiniScore | 0);
case 'space_shooter': return Math.max(0, p.spaceShooterScore | 0);
case 'balloon_boss': return Math.max(0, p.balloonBossScore | 0);
default:
@@ -2149,6 +2174,27 @@ function computeRunGradeForEvidence(space) {
return evidenceGradeFromPct(avgPct);
}
}
/* [2026-07-31] MG4 % MG1 ( )
เดมตกไปเขาสาขา default าน gauntlet/spaceShooter/balloonBossScore MG4 ไมเคยเขยน C ตลอด */
if (gameType === 'quiz_carry' && space.quizCarrySession && space.quizCarrySession.players) {
const len = Math.max(0, Number(space.quizCarrySession.sessionLength) || 0);
const total = len * QUIZ_CARRY_POINTS_PER_CORRECT;
const ids = Object.keys(space.quizCarrySession.players);
if (total > 0 && ids.length) {
const avgPct = ids.reduce((s, id) => {
const sc = Math.max(0, Number(space.quizCarrySession.players[id].score) || 0);
return s + Math.min(100, (sc / total) * 100);
}, 0) / ids.length;
return evidenceGradeFromPct(avgPct);
}
}
/* [2026-07-31] MG3 Tower block "" ( )
ในหองไมความหมาย ใชความคบหนาถอดรหสของทมเปนเกรดสมบรณ (แนวเดยวก MG7)
เดมตกไปสาขา default านฟลด MG3 ไมเคยเขยน (เกมเขยนลง p.reportedMiniScore) C ตลอด */
if (gameType === 'stack' && space.stackSession) {
const pct = Math.max(0, Math.min(100, Number(space.stackSession.progressPct) || 0));
return evidenceGradeFromPct(pct);
}
/* Last Light (crown gauntlet): mission payload ()
F (ไมรอด) ไม tier ในหลกฐาน map เป C */
if (gameType === 'gauntlet' && isGauntletCrownHeistMapBySpace(space) && space.lastCrownMissionGrade) {
@@ -4219,11 +4265,56 @@ function awardRandomEvidenceCardToPlayer(space, playerId, suspectIndex, grade) {
return null;
}
list.push(card);
/* [2026-07-31] "" () 3 return null
ไปกอนหนาน ทองจะยงไมกใชทธ คนถดไปในรอบเดยวกนยงมทธได */
if (card.rarity === 'legendary') space.legendaryGrantedThisRun = true;
space.playerEvidence[playerId][suspectIndex] = list;
syncEvidenceByNick(space, playerId);
return card;
}
/** [2026-07-31] "" ( 7 )
* MG3 stack / MG4 quiz_carry ไมตกรอบ กคนไดตามเด
* าน 2 ทาง: ธง/เซสชนฝ server + าท client รายงานตอนจบ (reportedMiniSurvived)
* เพอใหไดคำตอบเดยวกนทงเสนทาง returnDetective และ detective-minigame-finished */
const EVIDENCE_DEATH_GAME_TYPES = ['quiz', 'gauntlet', 'jump_survive', 'space_shooter', 'balloon_boss'];
function playerDiedThisRunForEvidence(space, playerId) {
const p = space && space.peers ? space.peers.get(playerId) : null;
if (!p) return false;
/* gameType space.mapId detective-minigame-finished
แมปถกสลบเป LobbyB ไปแล (server.js:4636) จะไดชนดผดแลวปลอยคนตายผาน
ใชาทจำไวงแต "ตอนเริ่มรอบ" แทน (fallback = แมปปจจ เผ space เกาทงไมลด) */
let gameType = space.detectiveRunGameType || null;
if (!gameType) {
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
gameType = md && md.gameType;
}
if (EVIDENCE_DEATH_GAME_TYPES.indexOf(gameType) < 0) return false;
if (p.reportedMiniSurvived === false) return true;
switch (gameType) {
case 'quiz': {
const st = space.quizSession && space.quizSession.players ? space.quizSession.players[playerId] : null;
return !!(st && st.eliminated);
}
case 'gauntlet':
return !!p.gauntletEliminated;
case 'jump_survive': {
const sj = space.jumpSurviveSession;
const st = sj && sj.players ? sj.players[playerId] : null;
return !!(st && st.eliminated);
}
case 'space_shooter': {
const ss = space.spaceShooterSession;
const st = ss && ss.players ? ss.players[playerId] : null;
return !!((st && st.eliminated) || p.spaceShooterEliminated);
}
case 'balloon_boss':
return !!p.balloonBossEliminated;
default:
return false;
}
}
/* [achv Phase A 2026-07-05] achievements
b4_data_miner(รวมทกใบ ×100) · b1_evidence_collector(common ×20) · a2_sharp_eye(rare/Silver ×10) · b3_hidden_fragment(legendary ครงแรก)
เฉพาะผเลนจร ( playerKey) ามบอท + เรยกจาก "จุดแจกจริง" 2 เทาน (ไมรวม debug hotkey) */
@@ -4278,10 +4369,22 @@ function grantDetectiveEvidenceForCurrentRun(sid, space, playerIds) {
if (space.detectiveAwardDoneForRun) {
return { suspectIdx, awarded: {}, alreadyGranted: true };
}
const ids = (playerIds && playerIds.length) ? playerIds : [...space.peers.keys()];
const grade = computeRunGradeForEvidence(space);
const idsRaw = (playerIds && playerIds.length) ? playerIds : [...space.peers.keys()];
/* [2026-07-31] ""
ครอบทงการแจกปกตและการดพเศษใบ 7 (หลกฐานฟร) วนรายชอชดเดยวก */
const died = [];
const ids = idsRaw.filter((pid) => {
if (!playerDiedThisRunForEvidence(space, pid)) return true;
died.push(pid);
return false;
});
if (died.length) {
try { console.log('[evidence] ตายในรอบนี้ ' + died.length + ' คน → ไม่ได้การ์ด (' + died.join(',') + ')'); } catch (e) { /* ignore */ }
}
/* [2026-07-31] ใช้เกรดที่ snapshot ไว้ตอนจบรอบ (ก่อนเซสชันถูกล้าง) — ถ้าไม่มีค่อยคิดสด */
const grade = space.pendingRunEvidenceGrade || computeRunGradeForEvidence(space);
const awardedList = {};
ids.forEach((pid) => { awardedList[pid] = []; });
idsRaw.forEach((pid) => { awardedList[pid] = []; });
const awarded = awardDetectiveEvidenceForMinigameEnd(space, suspectIdx, ids, grade);
ids.forEach((pid) => { if (awarded[pid]) awardedList[pid].push(awarded[pid]); });
/* Card 7 Free Evidence — ทุกคนรับหลักฐานเพิ่ม (after_game) */
@@ -4360,6 +4463,9 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
armLobbyBLoadedSafety(sid, space); /* backstop กัน deadlock ถ้า signal ไม่ครบ */
space.detectiveAwardDoneForRun = false;
space.coinAwardDoneForRun = false;
space.legendaryGrantedThisRun = false; /* [2026-07-31] ทองออกได้ใบเดียวต่อรอบมินิเกม */
space.detectiveRunGameType = md.gameType || null; /* [2026-07-31] จำชนิดเกมของรอบนี้ไว้ตัดสิน "ตายแล้วไม่ได้การ์ด" */
space.pendingRunEvidenceGrade = null; /* [2026-07-31] เกรดของรอบใหม่ ยังไม่คิด */
space.lastCrownMissionGrade = null;
/* รีเซ็ตคะแนนที่ client รายงาน (stack/quiz_carry) ของรอบก่อน */
space.peers.forEach((p) => { p.reportedMiniScore = 0; p.reportedMiniSurvived = true; });
@@ -4488,6 +4594,12 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
function returnDetectiveSpaceToLobbyB(sid, space, message, awardOptions) {
ensurePostCaseLobbyMapLoaded();
/* [2026-07-31] 🔴 ""
เดมไปคดตอนแจกการ (บรรท ~4657) งอย "หลัง" การลาง quiz/stack/quizCarry/jumpSurvive
Session = null (4600-4628) สาขาทานเซสชนตายหมด ตกไปสาขา default าน
gauntlet/spaceShooter/balloonBossScore ได 0 = เกรด C ตลอด (กระทบ MG1, MG3, MG4, MG5)
เกมทเกบคะแนนไวบน peer (MG2/MG6/MG7) ไมโดน เพราะ peer ไมกลาง */
space.pendingRunEvidenceGrade = computeRunGradeForEvidence(space);
/* แจกเหรียญตามอันดับ + คะแนนสะสม ก่อนล้างสถานะมินิเกม (คะแนน/ผู้รอดยังครบ) */
awardMinigameRankCoins(sid, space);
clearSpaceQuizTimers(space);