minigame fixed zoom

This commit is contained in:
2026-05-05 07:17:20 +00:00
parent 8dcbdbc6eb
commit 91669bb65b
3 changed files with 209 additions and 55 deletions
+109 -31
View File
@@ -163,6 +163,17 @@
}
/** ทดสอบจากเอดิเตอร์: เติมบอทให้ครบจำนวน (รวมผู้เล่นจริง) — ?fillTotal=6 (ค่าเริ่ม 6) */
const previewFillBots = previewMode && editorEmbedReturn;
/** พรีวิวใน iframe เอดิเตอร์: ซูมมุมมองได้ (เดิมไม่มี — zDraw จาก zoom คงที่) */
const PLAY_EMBED_USER_ZOOM_MIN = 0.5;
const PLAY_EMBED_USER_ZOOM_MAX = 3;
const PLAY_EMBED_ZOOM_STEP_KEY = 1.12;
let playEmbedUserZoomMul = 1;
let lastPlayZDrawForInput = 1.4;
/** คูณ world→พิกเซลบนแคนวาสให้ตรงกับ zDraw ใน draw() (รวม preview+embed zoom) — ใช้ซิงก์ DOM ทับฉาก */
function playDomSyncZoom() {
const z = lastPlayZDrawForInput;
return Number.isFinite(z) && z > 0 ? z : zoom;
}
const previewTargetHeadcount = Math.min(24, Math.max(1, parseInt(params.get('fillTotal'), 10) || 6));
const PREVIEW_BOT_PREFIX = '__pv_bot_';
let previewBotSeq = 0;
@@ -2756,10 +2767,11 @@
}
const camX = me.x * tileSize;
const camY = me.y * tileSize;
const left = (bounds.minX * tileSize - camX) * zoom + canvas.width / 2;
const top = (bounds.minY * tileSize - camY) * zoom + canvas.height / 2;
const wPx = (bounds.maxX - bounds.minX + 1) * tileSize * zoom;
const hPx = (bounds.maxY - bounds.minY + 1) * tileSize * zoom;
const zDom = playDomSyncZoom();
const left = (bounds.minX * tileSize - camX) * zDom + canvas.width / 2;
const top = (bounds.minY * tileSize - camY) * zDom + canvas.height / 2;
const wPx = (bounds.maxX - bounds.minX + 1) * tileSize * zDom;
const hPx = (bounds.maxY - bounds.minY + 1) * tileSize * zDom;
textEl.textContent = text;
panel.style.left = Math.round(left) + 'px';
panel.style.top = Math.round(top) + 'px';
@@ -2782,7 +2794,7 @@
const thFont = isQuizCarry()
? getEffectiveCarryMapPanelThemeForApply()
: getEffectiveQuizMapPanelThemeForApply();
const startFontPx = quizMapQuestionFontPxLikeCarryPlaques(thFont, zoom);
const startFontPx = quizMapQuestionFontPxLikeCarryPlaques(thFont, zDom);
const mnFont = Math.max(8, Math.round(Number(thFont.questionFontMinPx) || 10));
/* ไม่โหลด hud-question-plaque.png อัตโนมัติ — บนเซิร์ฟมักไม่มีไฟล์ → 404 ใน Network; ใช้คลาส + CSS แทน (อัปโหลด PNG แล้วค่อยผูกทีหลังได้) */
if (isQuiz() && isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'live') {
@@ -2884,10 +2896,11 @@
}
const camX = me.x * tileSize;
const camY = me.y * tileSize;
const left = (bounds.minX * tileSize - camX) * zoom + canvas.width / 2;
const top = (bounds.minY * tileSize - camY) * zoom + canvas.height / 2;
const wPx = (bounds.maxX - bounds.minX + 1) * tileSize * zoom;
const hPx = (bounds.maxY - bounds.minY + 1) * tileSize * zoom;
const zDom = playDomSyncZoom();
const left = (bounds.minX * tileSize - camX) * zDom + canvas.width / 2;
const top = (bounds.minY * tileSize - camY) * zDom + canvas.height / 2;
const wPx = (bounds.maxX - bounds.minX + 1) * tileSize * zDom;
const hPx = (bounds.maxY - bounds.minY + 1) * tileSize * zDom;
anchor.style.left = Math.round(left) + 'px';
anchor.style.top = Math.round(top) + 'px';
anchor.style.width = Math.round(Math.max(48, wPx)) + 'px';
@@ -2955,10 +2968,11 @@
}
const camX = me.x * tileSize;
const camY = me.y * tileSize;
const l = (bounds.minX * tileSize - camX) * zoom + canvasEl.width / 2;
const t = (bounds.minY * tileSize - camY) * zoom + canvasEl.height / 2;
const wPx = (bounds.maxX - bounds.minX + 1) * tileSize * zoom;
const hPx = (bounds.maxY - bounds.minY + 1) * tileSize * zoom;
const zDom = playDomSyncZoom();
const l = (bounds.minX * tileSize - camX) * zDom + canvasEl.width / 2;
const t = (bounds.minY * tileSize - camY) * zDom + canvasEl.height / 2;
const wPx = (bounds.maxX - bounds.minX + 1) * tileSize * zDom;
const hPx = (bounds.maxY - bounds.minY + 1) * tileSize * zDom;
const r = canvasEl.getBoundingClientRect();
const sl = r.left + l;
const st = r.top + t;
@@ -12302,6 +12316,7 @@
const prevWasSpaceShooter = mapData && mapData.gameType === 'space_shooter';
const prevWasBalloonBoss = mapData && mapData.gameType === 'balloon_boss';
mapData = gameMapData;
playEmbedUserZoomMul = 1;
if (mapData.gameType !== 'quiz_carry') {
quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT;
}
@@ -13154,6 +13169,7 @@
if (!ev || !ev.mapId) return;
const applySnap = (md) => {
mapData = md;
playEmbedUserZoomMul = 1;
if (ev.mapId != null && String(ev.mapId).trim() !== '') {
playSessionMapId = String(ev.mapId).trim();
}
@@ -13521,11 +13537,19 @@
const vh = window.innerHeight || document.documentElement.clientHeight || 600;
const header = document.querySelector('.game-header');
const headerH = header && header.offsetHeight ? header.offsetHeight : 48;
const parent = canvas && canvas.parentElement;
const stage = document.getElementById('play-canvas-stage');
const parent = stage || (canvas && canvas.parentElement);
let cw = canvas.clientWidth || 0;
let ch = canvas.clientHeight || 0;
if (parent && parent.clientWidth > 80) cw = Math.max(cw, parent.clientWidth);
if (parent && parent.clientHeight > 80) ch = Math.max(ch, parent.clientHeight);
/* จอใหญ่ / iframe embed: client บางทียัง 0 ช้ากว่า layout — ใช้ offset เป็นทางเลือก */
if (parent) {
const ow = parent.offsetWidth || 0;
const oh = parent.offsetHeight || 0;
if (ow > 80) cw = Math.max(cw, ow);
if (oh > 80) ch = Math.max(ch, oh);
}
if (ch < 120) ch = Math.max(240, vh - headerH - 6);
if (cw < 120) cw = Math.max(320, vw);
canvas.width = Math.max(320, cw);
@@ -13537,6 +13561,29 @@
if (!mapData || !isStack() || isChatFocused()) return;
tryHumanStackDrop();
});
(function setupPlayEditorEmbedViewZoom() {
const stack = document.getElementById('play-canvas-stack');
if (!stack) return;
const wheelZoomHandler = (e) => {
if (!previewMode || !editorEmbedReturn || !mapData) return;
const t = e.target;
if (t && typeof t.closest === 'function') {
if (t.closest('button, input, textarea, select, a[href]')) return;
}
/* ล้อธรรม / trackpad pinch มักไม่มี ctrl — ให้ซูมได้ใน embed; Ctrl/⌘+ล้อยังใช้ได้ */
e.preventDefault();
e.stopPropagation();
const raw = e.deltaMode === 1 ? e.deltaY * 14 : e.deltaMode === 2 ? e.deltaY * 320 : e.deltaY;
const k = Math.exp(-raw * 0.002);
playEmbedUserZoomMul = Math.max(
PLAY_EMBED_USER_ZOOM_MIN,
Math.min(PLAY_EMBED_USER_ZOOM_MAX, playEmbedUserZoomMul * k),
);
draw();
};
stack.addEventListener('wheel', wheelZoomHandler, { passive: false, capture: true });
})();
canvas.addEventListener('dblclick', (e) => {
if (!mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss() || isChatFocused() || isQuizCarryEmbedCountdownBlockingMovement()) return;
const r = canvas.getBoundingClientRect();
@@ -13544,8 +13591,9 @@
const sy = e.clientY - r.top;
const camX = me.x * tileSize;
const camY = me.y * tileSize;
const gx = (sx - canvas.width / 2) / zoom + camX;
const gy = (sy - canvas.height / 2) / zoom + camY;
const zHit = lastPlayZDrawForInput > 0 ? lastPlayZDrawForInput : zoom;
const gx = (sx - canvas.width / 2) / zHit + camX;
const gy = (sy - canvas.height / 2) / zHit + camY;
const tx = Math.floor(gx / tileSize);
const ty = Math.floor(gy / tileSize);
const mw = mapData.width || 20, mh = mapData.height || 15;
@@ -14818,20 +14866,6 @@
statusEl.textContent = 'SPECTATOR · BALLOONS GONE';
} else if (isGauntletCrownHeistMapPlay()) {
statusEl.textContent = '';
const tag = document.createElement('div');
tag.className = 'play-cyber-self-tagline';
tag.textContent = 'QUIZ LINK ·';
const sub = document.createElement('div');
sub.className = 'play-cyber-self-subtag';
sub.textContent = 'ประลองความรู้';
const nm = document.createElement('div');
nm.className = 'play-cyber-self-nick-line';
const raw = String(me.nickname || nick || '').trim();
nm.textContent = raw || 'LINK';
nm.title = raw || '';
statusEl.appendChild(tag);
statusEl.appendChild(sub);
statusEl.appendChild(nm);
} else {
statusEl.textContent = 'OPERATOR ONLINE';
}
@@ -15119,11 +15153,15 @@
camY = mhPx * 0.5;
zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96;
}
if (previewMode && editorEmbedReturn && mapData) {
zDraw *= playEmbedUserZoomMul;
}
const gauntletGroupCam = getGauntletCrownHeistGroupCameraCenterPxPlay(tileSize, canvas.width, canvas.height, zDraw);
if (gauntletGroupCam) {
camX = gauntletGroupCam.px;
camY = gauntletGroupCam.py;
}
lastPlayZDrawForInput = zDraw;
const halfW = canvas.width / (2 * zDraw);
const halfH = canvas.height / (2 * zDraw);
@@ -15885,6 +15923,24 @@
document.addEventListener('keydown', (e) => {
if (isMovementKey(e.code) && isChatFocused()) return;
if (previewMode && editorEmbedReturn && mapData && !isChatFocused()) {
if (e.code === 'BracketLeft' || e.code === 'Minus' || e.code === 'NumpadSubtract') {
e.preventDefault();
playEmbedUserZoomMul = Math.max(PLAY_EMBED_USER_ZOOM_MIN, playEmbedUserZoomMul / PLAY_EMBED_ZOOM_STEP_KEY);
draw();
return;
}
if (
e.code === 'BracketRight'
|| e.code === 'NumpadAdd'
|| (e.code === 'Equal' && e.shiftKey)
) {
e.preventDefault();
playEmbedUserZoomMul = Math.min(PLAY_EMBED_USER_ZOOM_MAX, playEmbedUserZoomMul * PLAY_EMBED_ZOOM_STEP_KEY);
draw();
return;
}
}
if (isSpaceShooter() && mapData && !isChatFocused() && isSpaceShooterMissionPregameBlockingPlay()) {
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) {
e.preventDefault();
@@ -16097,7 +16153,8 @@
others.forEach((o) => {
if (o) clampPlayEntityFootprintToMap(o, mapData);
});
const zGa = zoom;
let zGa = zoom;
if (previewMode && editorEmbedReturn && mapData) zGa *= playEmbedUserZoomMul;
const gCam = getGauntletCrownHeistGroupCameraCenterPxPlay(tileSize, canvas.width, canvas.height, zGa);
if (gCam) maybeEliminateGauntletPreviewBotsTailOffCameraPlay(gCam.px, gCam.py, zGa);
draw();
@@ -16416,4 +16473,25 @@
resizeCanvas();
draw();
});
/** แคนวาส flex ขยายทีหลัง (จอใหญ่ / editor iframe) — ResizeObserver จับได้ดีกว่า resize อย่างเดียว */
(function setupPlayCanvasStackResizeObserver() {
const stack = document.getElementById('play-canvas-stack');
if (!stack || typeof ResizeObserver === 'undefined') return;
let roRaf = null;
const run = () => {
roRaf = null;
if (!mapData || !canvas) return;
resizeCanvas();
draw();
};
const ro = new ResizeObserver(() => {
if (roRaf) cancelAnimationFrame(roRaf);
roRaf = requestAnimationFrame(run);
});
ro.observe(stack);
const wrap = stack.closest('.game-wrap');
if (wrap) ro.observe(wrap);
const stageEl = document.getElementById('play-canvas-stage');
if (stageEl) ro.observe(stageEl);
})();
})();
+1 -1
View File
@@ -1,6 +1,6 @@
// ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001
// หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ
window.APP_VERSION = '0.0260';
window.APP_VERSION = '0.0286';
document.addEventListener('DOMContentLoaded', function () {
var t = document.querySelector('.version-tag');
if (t) t.textContent = 'v ' + window.APP_VERSION;
+99 -23
View File
@@ -10,8 +10,36 @@
<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=20260427-canvas-pixel">
<style>
html, body { height: 100%; margin: 0; }
html, body {
height: 100%;
width: 100%;
margin: 0;
box-sizing: border-box;
}
body { min-height: 100dvh; }
.game-wrap {
width: 100%;
box-sizing: border-box;
}
/* พรีวิว editor embed: ให้คอลัมน์แคนวาสเต็มพื้นที่ iframe/หน้าต่าง — กันพื้นดำล้นเมื่อจอใหญ่ */
html.play-preview-editor-embed,
html.play-preview-editor-embed body {
width: 100%;
min-height: 100vh;
min-height: 100dvh;
}
html.play-preview-editor-embed .game-wrap {
width: 100%;
min-height: 0;
flex: 1 1 auto;
}
html.play-preview-editor-embed .play-canvas-stack {
width: 100%;
min-height: 0;
}
html.play-preview-editor-embed .version-tag {
display: none !important;
}
.sr-only {
position: absolute !important;
width: 1px !important;
@@ -31,11 +59,27 @@
display: flex;
flex-direction: column;
}
.play-canvas-stack #game-canvas {
/* พื้นที่เต็มแถวแล้วจัดแคนวาสกลาง (กันจอใหญ่/iframe แล้วแคนวาสเล็กหลุดมุม) — ชนะ #game-canvas ใน style.css */
#play-canvas-stage.play-canvas-stage {
position: relative;
flex: 1 1 auto;
min-height: 0;
min-width: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
#play-canvas-stage #game-canvas {
flex: none;
display: block;
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
min-width: 0;
min-height: 0;
touch-action: none;
image-rendering: pixelated;
image-rendering: crisp-edges;
@@ -1717,6 +1761,25 @@
padding-right: max(8px, env(safe-area-inset-right));
padding-bottom: max(6px, env(safe-area-inset-bottom));
}
/* Last Light (ไม่ใช่ quiz_carry flush): จอให้ญ — ระยะ SCORE กับโปรไฟล์ + แถบคะแนนห่อได้ ไม่ชนมุมขวา */
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip:not(.play-cyber-hud--score-flush-left) {
column-gap: clamp(12px, 2.8vw, 44px);
padding-right: max(12px, env(safe-area-inset-right));
}
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip:not(.play-cyber-hud--score-flush-left) .play-cyber-score-list.play-cyber-score-list--crown-strip {
flex-wrap: wrap;
overflow-x: visible;
overflow-y: visible;
align-content: flex-start;
row-gap: clamp(4px, 0.8vw, 10px);
scrollbar-width: none;
-ms-overflow-style: none;
}
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip:not(.play-cyber-hud--score-flush-left) .play-cyber-score-list.play-cyber-score-list--crown-strip::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip.play-cyber-hud--score-flush-left .play-cyber-scoreboard.play-cyber-scoreboard--crown-strip {
padding-left: 0;
}
@@ -1857,19 +1920,29 @@
box-shadow: none;
padding: 0.28rem 0.2rem 0.32rem 0;
}
/* รายการคะแนน crown: ห่อหลายแถวได้เสมอ — อย่าใช้ overflow-x:auto ตรงนี้ (คลาส --crown-strip อาจยังไม่ติดรอบแรก = เลื่อนแนวนอนกลับมา) */
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip > .play-cyber-scoreboard .play-cyber-score-list {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
flex-wrap: wrap;
align-items: stretch;
align-content: flex-start;
gap: clamp(6px, 1vw, 12px);
row-gap: clamp(4px, 0.8vw, 10px);
flex: 1 1 auto;
min-width: 0;
max-height: none;
overflow-x: auto;
overflow-y: hidden;
overflow-x: visible;
overflow-y: visible;
width: 100%;
box-sizing: border-box;
scrollbar-width: none;
-ms-overflow-style: none;
}
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip > .play-cyber-scoreboard .play-cyber-score-list::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip .play-cyber-scoreboard.play-cyber-scoreboard--crown-strip {
grid-area: crownScore;
@@ -1931,30 +2004,26 @@
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip .play-cyber-score-list.play-cyber-score-list--crown-strip {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
flex-wrap: wrap;
align-items: stretch;
align-content: flex-start;
gap: clamp(6px, 1vw, 12px);
row-gap: clamp(4px, 0.8vw, 10px);
flex: 1 1 auto;
min-width: 0;
max-height: none;
overflow-x: auto;
overflow-y: hidden;
overflow-x: visible;
overflow-y: visible;
width: 100%;
scrollbar-width: thin;
scrollbar-color: rgba(0, 210, 255, 0.5) rgba(0, 0, 0, 0.28);
scrollbar-width: none;
-ms-overflow-style: none;
padding: 0 0.2rem 0.2rem 0;
box-sizing: border-box;
}
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip .play-cyber-score-list.play-cyber-score-list--crown-strip::-webkit-scrollbar {
height: 5px;
}
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip .play-cyber-score-list.play-cyber-score-list--crown-strip::-webkit-scrollbar-thumb {
background: linear-gradient(90deg, rgba(0, 200, 255, 0.55), rgba(0, 255, 220, 0.4));
border-radius: 4px;
}
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip .play-cyber-score-list.play-cyber-score-list--crown-strip::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.22);
border-radius: 4px;
display: none;
width: 0;
height: 0;
}
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip .play-cyber-score-row--crown-strip {
display: flex;
@@ -2185,8 +2254,13 @@
0 0 14px rgba(0, 220, 255, 0.32),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
/* ไมค์/LINK อยู่ใน op-widgets ที่ซ่อนแล้ว — กันค่า display:flex เก่าทำให้เห็นเป็นเส้น/ชิปหลุด */
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip .play-cyber-voice-chip {
display: flex;
display: none !important;
}
/* Last Light เท่านั้น (ไม่มี score-flush-left เหมือน quiz_carry): ไม่แสดงกล่องข้อความ mock ข้างโปรไฟล์ */
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip:not(.play-cyber-hud--score-flush-left) .play-cyber-self-status {
display: none !important;
}
.play-cyber-voice-chip::after {
content: '';
@@ -2673,7 +2747,9 @@
<button id="btn-leave">ออกจากห้อง</button>
</div>
<div class="play-canvas-stack" id="play-canvas-stack">
<canvas id="game-canvas"></canvas>
<div id="play-canvas-stage" class="play-canvas-stage">
<canvas id="game-canvas"></canvas>
</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" />
</button>
@@ -2878,8 +2954,8 @@
</div>
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0258"></script>
<script src="js/play.js?v=0.0269"></script>
<script src="js/version.js?v=0.0286"></script>
<script src="js/play.js?v=0.0286"></script>
<div class="version-tag">v —</div>
</body>
</html>