update mg quest and mobile some path
This commit is contained in:
+2049
-2042
File diff suppressed because it is too large
Load Diff
+1999
-1988
File diff suppressed because it is too large
Load Diff
+337
-37
@@ -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 ⇒ **ป้ายใหญ่เกินแมปจนทับกัน**
|
||||
วัดจริง: เดสก์ท็อป ts≈29px (ฟอนต์ที่ควรได้ 9.0 → ใช้ 10 = เกินนิดเดียว)
|
||||
iPhone 13 Pro แนวนอน ts≈9.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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ที่โหลดอยู่ข้างล่าง -->
|
||||
|
||||
+1787
-1698
File diff suppressed because it is too large
Load Diff
+124
-12
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user